Skip to content

fix(#2873): stub-mint undeclared edge endpoints, prune manifest deps, fix merge repos-union clobbering - #2878

Open
AromalBiju1 wants to merge 2 commits into
Graphify-Labs:v8from
AromalBiju1:fix/2873-dangling-edge-endpoints
Open

fix(#2873): stub-mint undeclared edge endpoints, prune manifest deps, fix merge repos-union clobbering#2878
AromalBiju1 wants to merge 2 commits into
Graphify-Labs:v8from
AromalBiju1:fix/2873-dangling-edge-endpoints

Conversation

@AromalBiju1

Copy link
Copy Markdown
Contributor

Fixes #2873

Summary

Two related problems from #2873, plus one bug found while fixing them.

1. Undeclared edge endpoints (the core report)

graph.json could contain edges pointing at node ids that were never
emitted — 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 still
not 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/requires relations, or
pkg_* ids) are excluded from stub-minting on purpose — the issue itself
noted 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-graphs leaves them
unprefixed so typing unifies across repos instead of becoming
repoA::typing / repoB::typing.

While testing this, found a second bug: nx.compose merges shared-node
attributes with dict.update, so on a 3+ repo merge, each pass's repos
list on a shared external node overwrote the previous one instead of
unioning — only the last repo survived. cli.py now accumulates the
repos union itself and reattaches it after composing. Added
test_merge_graphs_external_node_repos_union_not_clobbered for 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 calls edges to unchanged files silently
vanishing — surfacing as #2406/#2437/#2438 symptoms). Root cause: the
undeclared-endpoint check only looked at all_nodes (this run's freshly
extracted nodes), not at resolution_context_nodes — the read-only
persisted 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_nodes ids into the declared-id set
before deciding what's undeclared.

Files changed

  • graphify/extract.py — stub-minting, manifest-relation carve-out,
    resolution_context_nodes fix
  • graphify/build.py — external items excluded from carry-forward-drop
    logic in the semantic/LLM merge path (defensive, same principle as the
    incremental fix above)
  • graphify/cli.pyrepos union fix for merge-graphs
  • tests/test_csharp_type_resolution.py — assertions updated now that
    previously-None import targets resolve to real stub types
  • tests/test_merge_graphs_cli.py — new coverage for the repos-union fix

Test 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_size
fails intermittently in full-suite runs, passes 6/6 in isolation.
Reproduces identically on a clean v8 HEAD (b14b52e, no changes applied)
— order-dependent, unrelated to anything touched here. Filed separately as
#2877

…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
Copilot AI lite review requested due to automatic review settings August 19, 2026 18:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IDsgraphify/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 repo attributegraphify/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 stubbedgraphify/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.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 contractgraphify/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 endpointsgraphify/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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants