fix(watch): validate .graphify_root before anchoring stored sources on it (#2603) - #2778
fix(watch): validate .graphify_root before anchoring stored sources on it (#2603)#2778catpotd wants to merge 1 commit into
Conversation
…n it (Graphify-Labs#2603) An absolute .graphify_root holds the SCAN root, but stored source_file values are relative to the BUILD's cwd (the skill builds from the repo root scoped to a subfolder). Trusting the marker blindly re-anchors 'src/mod.py' under src/, doubling the path; every unchanged source is then judged deleted and evicted, collapsing the graph. Adopt an anchor only when stored paths actually resolve under it; otherwise try project_root, then the invocation cwd. When nothing anchors, keep the marker so a fully-deleted corpus still evicts.
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
This PR modifies the _StoredSourcePaths logic in graphify/watch.py for how the .graphify_root marker is used to anchor stored source paths during incremental rebuilds. Instead of trusting an absolute marker path directly, it adds a new _anchors_stored_sources helper that samples stored relative source_file entries to check whether they actually resolve under a candidate root, falling back through project_root, cwd, and finally the marker value. A new end-to-end test is added in tests/test_watch.py covering the subfolder-scoped build scenario referenced by issue #2603, and various unrelated test/rationale symbols in the same files appear in the change set.
No blocking issues surfaced. 3 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 484 functions depend on the 331 functions this change touches.
Health — this change adds coupling hotspots:
- new:
_rebuild_code()— 96 callers, 51 callees - new:
dispatch_command()— 2 callers, 117 callees - new:
watch()— 5 callers, 7 callees - new:
_reconcile_existing_graph()— 1 callers, 7 callees - new:
test_poisoned_manifest_is_healed()— 0 callers, 6 callees
Verification — 484 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 342 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_\_init\_\_.
The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
· 5 more finding(s) on lines outside this diff (see the check run).
Summary
Addresses the silent graph collapse in #2603 — the part its reporter "could not
fully isolate". A graph built on a subfolder loses almost every unchanged node
on the first incremental (hook or
update) rebuild.Scope: this PR fixes the mass eviction / collapse. The other quirk described in
#2603 — the rebuild output landing under the scan root rather than next to the
existing
graphify-out/— is a separate, smaller issue and is not changed here,so this intentionally does not say "Fixes".
Root cause
_StoredSourcePathstrusts an absolute.graphify_rootunconditionally as the anchor for stored relativesource_filevalues (watch.py:373). But the two are written by different parties with different bases:source_filerelative to the build cwd (src/mod.py).graphify_rootas an absolute subfolder path (/repo/src)The incremental hook path then resolves
src/mod.pyagainst/repo/src→/repo/src/src/mod.py. The doubled path exists for no file, so every unchanged source is judged deleted and evicted. The graph collapses to roughly the changed files' nodes.Reproduction
Minimal (6-file Python corpus, scan scoped to
src/, output at repo root): one changed file collapses 48 nodes → 2. On larger corpora the collapse is partial — most nodes are silently lost while the rebuild still reports success, which makes it easy to miss.The trigger is binary: with the absolute marker present,
preserved=0; delete the marker file and rerun,preserved=48. A/B verified on 0.9.44.Approach
Before adopting the marker as the anchor, verify that stored relative paths actually resolve under it (
_anchors_stored_sources, first-hit sampling over 25 entries). If they don't, tryproject_root, then the invocation cwd. If nothing anchors, keep the marker — a fully-deleted corpus must still evict, and behavior stays identical to today's. This mirrors whatremap_communities_to_previousalready does for community IDs: prefer the interpretation with actual overlap over a stored claim.Known limit (documented in the docstring): a commit deleting 25+ files whose nodes sort first would reject the correct anchor and fall back to the marker — i.e. the pre-fix behavior, no worse.
Verification
test_subfolder_root_marker_preserves_unchanged_nodespins the scenario; it fails on unpatched v8 and passes with the fixuv sync --all-extras --frozen, same commands as CI): 4517 passed / 0 failed on both Python 3.10 and 3.12; the five skillgen checks all passHappy to adjust sampling size or fallback order if you prefer a different trade-off.