fix(#2873): stub-mint undeclared edge endpoints, prune manifest deps, fix merge repos-union clobbering - #2878
Conversation
…s, prune manifest deps, fix merge repos-union clobbering - extract.py: mint external stub nodes for import edges pointing at undeclared endpoints (stdlib/third-party); manifest/package deps (depends_on/requires, pkg_* ids) keep prior prune-not-fabricate behavior - extract.py: include resolution_context_nodes when computing declared ids, so an incremental rebuild's cross-file-resolved-but-not-locally-extracted targets (e.g. an unchanged file's function) aren't wrongly treated as undeclared and stub-shadowed — this was silently corrupting 4 incremental-rebuild test cases (Graphify-Labs#2406/Graphify-Labs#2437/Graphify-Labs#2438 symptoms, actually caused by this interaction) - cli.py: accumulate external node 'repos' as a union across merge-graphs inputs instead of letting nx.compose's dict.update clobber it to the last repo's value - tests: strengthen csharp import-edge assertions now that stub nodes resolve; add coverage for the repos-union fix
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 4 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Materializes explicit stub nodes for dangling edge endpoints in extract, minting an external: True module node for any edge target still absent from the node set after all resolution passes (skipping manifest/package deps). Teaches prefix_graph_for_global to leave external nodes un-namespaced so nx.compose unifies stdlib/third-party references across repos, and accumulates the repos tag list into a union; merge_raw_extraction now never prune-drops external items, and dispatch_command reattaches the union after compose to work around nx.compose's clobbering attr merge. Updates C# import-resolution test expectations from None to the new external stub type/label, and adds a 3-way merge test guarding the repos union.
Worth a look
- External IDs can collide with synthetic repo-prefixed local IDs —
graphify/build.py:1999· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- External nodes never get 'repo' attribute, breaking downstream consumers expecting node['repo'] —
graphify/build.py:2007· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- External nodes no longer receive the documented
repoattribute —graphify/build.py:2008· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- Dangling edge sources are not stubbed —
graphify/extract.py:6720· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2373 functions depend on the 389 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 472 callers, 42 callees - new:
_rebuild_code()— 98 callers, 51 callees - new:
build_from_json()— 182 callers, 18 callees - new:
build_merge()— 46 callers, 14 callees - new:
to_obsidian()— 32 callers, 12 callees - new:
extract_files_direct()— 17 callers, 20 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
to_wiki()— 41 callers, 7 callees - …and 64 more — each is listed as a finding
Verification — 2373 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: 2195 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify merge\_raw\_extraction.
The verifier did not have enough to check merge\_raw\_extraction, 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: parameter `graph_path` is annotated `str | Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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: not verifiable: all 115 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, 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: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
· 72 more finding(s) on lines outside this diff (see the check run).
…nodes Bot review on Graphify-Labs#2878 flagged that external nodes (Graphify-Labs#2873) never carry a 'repo' attribute, only 'repos' — so prune_repo_from_graph's repo==repo_tag filter silently skipped them, leaving orphaned external stubs behind after the last referencing repo was pruned. Now removes the repo tag from 'repos' and only drops the node once no repo remains.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Formal verification. 1 change(s) tested, no difference found (not proven).
Graphify review — findings
Materializes explicit stub nodes for dangling edge endpoints in extract(), marking them external: True and skipping manifest/package deps (depends_on/requires/pkg_ targets) so consumers no longer see attribute-less phantom nodes. Teaches the global-graph passes to treat external nodes as shared: prefix_graph_for_global leaves them un-namespaced and accumulates a repos list, prune_repo_from_graph only removes them once no repo references them, and merge_raw_extraction never prunes them. Fixes nx.compose clobbering the repos union in dispatch_command by accumulating and reattaching it after compose.
Worth a look
- External nodes no longer follow prefix_graph_for_global namespacing contract —
graphify/build.py:1998· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- External stub pass ignores missing source endpoints —
graphify/extract.py:6724· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2386 functions depend on the 417 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 472 callers, 42 callees - new:
_rebuild_code()— 98 callers, 51 callees - new:
build_from_json()— 182 callers, 18 callees - new:
build_merge()— 46 callers, 14 callees - new:
to_obsidian()— 32 callers, 12 callees - new:
extract_files_direct()— 17 callers, 20 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
to_wiki()— 41 callers, 7 callees - …and 64 more — each is listed as a finding
Verification — 2386 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: 2208 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify merge\_raw\_extraction.
The verifier did not have enough to check merge\_raw\_extraction, 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: parameter `graph_path` is annotated `str | Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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: not verifiable: all 115 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
No difference found (not proven): No behavior difference found in prune\_repo\_from\_graph (not a proof).
The verifier ran both versions of prune\_repo\_from\_graph on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, 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: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
· 72 more finding(s) on lines outside this diff (see the check run).
Fixes #2873
Summary
Two related problems from #2873, plus one bug found while fixing them.
1. Undeclared edge endpoints (the core report)
graph.jsoncould contain edges pointing at node ids that were neveremitted — stdlib/third-party imports being the main case. Every consumer
that loads the file (networkx included) then materializes those endpoints
as attribute-less phantom nodes, so the declared node set and the produced
node set disagreed.
Fix: after every resolution/repoint/disambiguation pass runs, mint an
explicit, minimal stub node (
external: True) for any edge endpoint stillnot in the node set. This makes the "outside the graph" case explicit
instead of an implicit, per-consumer materialization.
Manifest/package-dependency edges (
depends_on/requiresrelations, orpkg_*ids) are excluded from stub-minting on purpose — the issue itselfnoted that not emitting nodes for external packages may be deliberate, and
existing tests (
test_external_dependency_edge_pruned_not_orphaned)already encode prune-not-fabricate as the intended behavior for that case.
Kept it that way.
2. merge-graphs fragmenting external modules per repo
Since external stub nodes aren't repo-local,
merge-graphsleaves themunprefixed so
typingunifies across repos instead of becomingrepoA::typing/repoB::typing.While testing this, found a second bug:
nx.composemerges shared-nodeattributes with
dict.update, so on a 3+ repo merge, each pass'sreposlist on a shared external node overwrote the previous one instead of
unioning — only the last repo survived.
cli.pynow accumulates thereposunion itself and reattaches it after composing. Addedtest_merge_graphs_external_node_repos_union_not_clobberedfor this.3. Stub-minting interaction with incremental rebuilds (found during testing)
Applying the stub-minting fix broke 4 previously-passing incremental
rebuild tests (cross-file
callsedges to unchanged files silentlyvanishing — surfacing as #2406/#2437/#2438 symptoms). Root cause: the
undeclared-endpoint check only looked at
all_nodes(this run's freshlyextracted nodes), not at
resolution_context_nodes— the read-onlypersisted nodes from unchanged files that #2406's cross-file resolver
uses. So a real call target living in an unchanged file looked
"undeclared" during an incremental rebuild, got a stub minted over its
real id, and the real preserved node was reconciled away in favor of the
fabricated stub.
Fix: union
resolution_context_nodesids into the declared-id setbefore deciding what's undeclared.
Files changed
graphify/extract.py— stub-minting, manifest-relation carve-out,resolution_context_nodesfixgraphify/build.py— external items excluded from carry-forward-droplogic in the semantic/LLM merge path (defensive, same principle as the
incremental fix above)
graphify/cli.py—reposunion fix for merge-graphstests/test_csharp_type_resolution.py— assertions updated now thatpreviously-
Noneimport targets resolve to real stub typestests/test_merge_graphs_cli.py— new coverage for the repos-union fixTest results
uv run pytest tests/ -q
4783 passed, 11 skipped
One unrelated, pre-existing flake:
tests/test_labeling.py::test_label_communities_batches_when_over_batch_sizefails intermittently in full-suite runs, passes 6/6 in isolation.
Reproduces identically on a clean
v8HEAD (b14b52e, no changes applied)— order-dependent, unrelated to anything touched here. Filed separately as
#2877