Skip to content

Fix JS/TS graph resolution and preserve parallel edges - #2883

Open
Neonsy wants to merge 4 commits into
Graphify-Labs:v8from
Neonsy:fix/2849-graph-integrity
Open

Fix JS/TS graph resolution and preserve parallel edges#2883
Neonsy wants to merge 4 commits into
Graphify-Labs:v8from
Neonsy:fix/2849-graph-integrity

Conversation

@Neonsy

@Neonsy Neonsy commented Aug 20, 2026

Copy link
Copy Markdown

Summary

This fixes missing and overwritten relationships in JavaScript and TypeScript graphs.

Graphify now resolves common source layouts that previously produced edges to nodes that did not exist.
With --multigraph, it also preserves distinct relationships between the same two nodes instead of overwriting all but one.

An explicit --out selection now persists for the scanned folder.
Later commands reuse the external output without adding graphify-out to the source repository.

Addresses #2849

What changes for users

  • Workspace imports prefer source files included in the scan instead of ignored dist output.
  • .js and .jsx specifiers can resolve TypeScript source and declaration files.
  • Generated JavaScript modules and their companion .d.ts files provide one resolvable export set.
  • Imported members resolve through aliases, barrels, and workspace re-exports to their declaration origin.
  • Resource imports such as styles.css?url resolve without losing the query context.
  • Collision-remapped nodes receive their final edge endpoints.
  • External, excluded local, and unresolved internal references have separate classifications.
  • --multigraph preserves parallel directed relationships and their source evidence.
  • An explicit --out applies to later extract, update, watch, query, path, explain, diagnose, tree, benchmark, memory, and export commands.

Why relationships went missing

A dangling endpoint is an edge whose source or target has no matching node.

Several resolver paths could produce these endpoints.
Workspace imports could select ignored build output, .js specifiers did not try every TypeScript counterpart, and imported members could stop at a barrel instead of reaching their declaration.

A separate problem occurred after extraction.
A NetworkX DiGraph stores one edge for each ordered node pair, so adding another relationship between the same nodes replaced the previous relationship.

These failures affected graph queries, path searches, exports, and reports because the stored graph no longer matched the extracted relationships.

How the fix works

JavaScript and TypeScript resolution

The resolver now considers all eligible workspace export conditions and prefers candidates included in the scanned source set.

Explicit JavaScript extensions receive replacement candidates instead of appended extensions.
For example, button.js can resolve to button.ts, button.tsx, or button.d.ts.

Runtime JavaScript modules can use symbols from companion declaration files.
This allows imports such as type MutationCtx from server.js to resolve to server.d.ts while runtime values still resolve to server.js.

Re-export resolution follows imported bindings, aliases, local barrels, and workspace exports.
Cycle detection uses the file and exported name together.

Edge storage

When --multigraph is enabled, directed builds use MultiDiGraph as the stored graph.

Each distinct edge receives a deterministic key based on its relationship and evidence.
Exact duplicate records can share an occurrence_count, while records with different relationships, locations, or contexts remain separate.

Algorithms that require one edge per node pair receive a separate simple projection.
The stored graph remains the source of truth.

Endpoint classification

The build materializes endpoint nodes before graph construction and classifies non-source references as:

  • external
  • excluded_local
  • unresolved_internal

Diagnostics report these categories separately.
External dependencies no longer appear as graph corruption merely because they are outside the scanned repository.

Persistent external output

A successful explicit --out writes a small per-source binding under:

~/.graphify/output-roots/

Each binding contains the canonical source root and output root.
The filename is a hash of the source root, so separate processes can update different projects without replacing one shared configuration file.

Commands started in a project subdirectory inherit the nearest source binding.
A later explicit --out replaces the binding.
A failed extraction leaves the previous binding unchanged.

The binding write is atomic.
A non-default GRAPHIFY_OUT environment value keeps its existing precedence.

Incremental detection and AST caches use the selected output root too, so update and watch operations do not recreate cache files inside the source repository.

NeonFlux verification

A fresh code-only build used the NeonFlux snapshot from the issue at 341d66b.

Result Count
Nodes 7,972
Edge records 22,403
Dangling endpoints 0
Unclassified endpoints 0
External endpoint edges 1,209
Excluded local endpoint edges 1
Unresolved internal endpoint edges 2
Parallel endpoint groups 639
Records a directed simple graph would collapse 668
Relationship variant groups 385
Stored graph type MultiDiGraph

The build also verified the cases named in the issue:

  • Workspace imports resolve to scanned source files, with no dist nodes or endpoints.
  • dashboard-ui.js resolves to dashboard-ui.tsx.
  • Convex QueryCtx, MutationCtx, api, and Doc imports resolve to generated declaration nodes.
  • Re-exported blueprint and captcha symbols resolve to their declaration files.
  • The resource-query import from __root.tsx resolves to styles.css.
  • routeTree.gen.ts contributes 213 edges with no dangling endpoints.
  • All 668 distinct parallel records survive graph construction.

The audited NeonFlux checkout does not contain the .source files referenced by collections/browser and collections/server.
Those two references are therefore unresolved_internal.
A separate regression test verifies that an alias pointing to an existing but unscanned file receives excluded_local.

Output persistence verification

The process-level regression test performs this sequence:

  1. Extract a repository with an external --out and --multigraph.
  2. Add a source file.
  3. Extract again without --out.
  4. Run update without --out.
  5. Run a query without an explicit graph path.
  6. Export GraphML without an explicit graph path.
  7. Export call-flow HTML by passing the source directory.
  8. Confirm that every artifact and cache remains under the external output.
  9. Confirm that the source repository has no graphify-out directory.

Checks

  • Focused resolver, graph construction, export, watch, path, report, and persistence tests pass.
  • Ruff reports no errors.
  • The targeted Pyright run reports no errors in the new persistence helpers and regressions.
  • The source distribution and wheel build successfully.
  • graphify update . --no-cluster completes successfully.

The full Windows test run reports 4,694 passed, 67 skipped, 18 failed, and 2 deselected.
The same 18 failures reproduce on the base branch.
They cover unavailable Unix APIs and tools, optional dependencies, installer home assumptions, and existing Windows path or encoding behavior.

@Neonsy Neonsy changed the title fix: preserve JavaScript graph relationships Fix JS/TS graph resolution and preserve parallel edges Aug 20, 2026

@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 5 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) alter behavior, breaking input(s) attached.

Behavior changes: \_default\_graph\_path changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_default\_graph\_path behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This is a sound refutation from differential testing (both versions run on many generated inputs): a concrete input on which the two versions provably differ.


Graphify review — findings

Adds a --multigraph build flag that stores the authoritative graph as a directed MultiDiGraph (persisted per graph) so parallel edges between the same node pair aren't collapsed, routing degree/betweenness analysis through a new analysis_projection helper in build.py. Adds an --out/--output selection that is remembered per scanned source root under ~/.graphify/output-roots/, so later extract/update/watch/query/export commands reuse it; updates all CLI help defaults to "configured source output". Updates README with the new flags and output-root behavior.

Worth a look

  • MultiDiGraph merge attrs still contains 'key' filtered but occurrence_count included in hashgraphify/build.py:1360 · Escalate · high
    • 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.
  • edge_betweenness returns keys not present in original G for edge_data lookupgraphify/analyze.py:359 · 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.
  • Non-multigraph edge attribute named key is droppedgraphify/build.py:1263 · 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.
  • build_from_json drops existing edge key attribute for non-multigraph graphsgraphify/build.py:1264 · 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.
  • update trusts persisted scan root without containment validationgraphify/cli.py:2155 · 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 — 3730 functions depend on the 1580 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 475 callers, 43 callees
  • new: _rebuild_code() — 99 callers, 55 callees
  • new: build_from_json() — 185 callers, 20 callees
  • new: detect() — 108 callers, 15 callees
  • new: build_merge() — 48 callers, 14 callees
  • new: _extract_generic() — 18 callers, 24 callees
  • new: save_manifest() — 39 callers, 11 callees
  • new: to_obsidian() — 32 callers, 12 callees
  • …and 105 more — each is listed as a finding

Verification — 3730 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: 3596 function(s) in the blast radius were not formally verified this run

Formal verification

Behavior changes: \_default\_graph\_path changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_default\_graph\_path behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This is a sound refutation from differential testing (both versions run on many generated inputs): a concrete input on which the two versions provably differ.

No difference found (not proven): No behavior difference found in \_cross\_community\_surprises (not a proof).

The verifier ran both versions of \_cross\_community\_surprises 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.

No difference found (not proven): No behavior difference found in \_cross\_file\_surprises (not a proof).

The verifier ran both versions of \_cross\_file\_surprises 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.

No difference found (not proven): No behavior difference found in god\_nodes (not a proof).

The verifier ran both versions of god\_nodes 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 build.

The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_from\_json.

The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_merge.

The verifier did not have enough to check build\_merge, 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 | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_load\_existing\_graph.

The verifier did not have enough to check \_load\_existing\_graph, 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 `Path` — outside the synthesizable primitive/collection set

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 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)

No difference found (not proven): No behavior difference found in cluster (not a proof).

The verifier ran both versions of cluster 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.

No difference found (not proven): No behavior difference found in cohesion\_score (not a proof).

The verifier ran both versions of cohesion\_score 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.

No difference found (not proven): No behavior difference found in label\_communities\_by\_hub (not a proof).

The verifier ran both versions of label\_communities\_by\_hub 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.

No difference found (not proven): No behavior difference found in score\_all (not a proof).

The verifier ran both versions of score\_all 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 detect\_incremental.

The verifier did not have enough to check detect\_incremental, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify diagnose\_extraction.

The verifier did not have enough to check diagnose\_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: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify diagnose\_file.

The verifier did not have enough to check diagnose\_file, 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 `path` is annotated `str | Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify format\_diagnostic\_report.

The verifier did not have enough to check format\_diagnostic\_report, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in to\_graphml (not a proof).

The verifier ran both versions of to\_graphml 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 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

Could not verify: Could not verify \_import\_js.

The verifier did not have enough to check \_import\_js, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_apply\_symbol\_resolution\_facts.

The verifier did not have enough to check \_apply\_symbol\_resolution\_facts, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_js\_exported\_declaration\_names.

The verifier did not have enough to check \_js\_exported\_declaration\_names, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_package\_entry\_candidates.

The verifier did not have enough to check \_package\_entry\_candidates, 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 `package_dir` is annotated `Path` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_resolve\_export\_target (not a proof).

The verifier ran both versions of \_resolve\_export\_target 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.

· 4 grounded finding(s) anchored inline below; 109 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/build.py
Comment thread graphify/extractors/resolution.py
Comment thread graphify/paths.py
Comment thread graphify/paths.py Outdated

@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 5 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) alter behavior, breaking input(s) attached.

Behavior changes: \_default\_graph\_path changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_default\_graph\_path behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This is a sound refutation from differential testing (both versions run on many generated inputs): a concrete input on which the two versions provably differ.


Graphify review — findings

Adds a --multigraph flag to graphify extract/update that persists parallel directed edges as a MultiDiGraph, routing analytics (god_nodes, cross-file/cross-community surprise detection) through a new analysis_projection in build.py so degree and betweenness stay correct on the multigraph. Introduces per-source-root output persistence under ~/.graphify/output-roots/ so --out selections are remembered and reused across extract/update/watch/query/export, and updates the CLI help text to reflect "configured source output" defaults. Extends watch/extract/export plumbing (relativize, AST manifest, resolution, obsidian stems) to carry the multigraph and remembered-output settings.

Worth a look

  • god_nodes degree field no longer reports input graph degreegraphify/analyze.py:115 · 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.
  • god_nodes degree uses projection topology but iterates over original G.nodesgraphify/analyze.py:115 · 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.
  • Projected degree lookup can crash on filtered community membersgraphify/cluster.py:104 · 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.
  • Inserted cache_root breaks positional gitignore callersgraphify/detect.py:2221 · 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.
  • New optional parameter breaks positional gitignore callersgraphify/detect.py:2221 · 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 — 3769 functions depend on the 1665 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 475 callers, 43 callees
  • new: _rebuild_code() — 99 callers, 55 callees
  • new: build_from_json() — 186 callers, 20 callees
  • new: detect() — 108 callers, 15 callees
  • new: build_merge() — 48 callers, 14 callees
  • new: _extract_generic() — 18 callers, 24 callees
  • new: save_manifest() — 39 callers, 11 callees
  • new: to_obsidian() — 32 callers, 12 callees
  • …and 105 more — each is listed as a finding

Verification — 3769 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: 3635 function(s) in the blast radius were not formally verified this run

Formal verification

Behavior changes: \_default\_graph\_path changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_default\_graph\_path behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This is a sound refutation from differential testing (both versions run on many generated inputs): a concrete input on which the two versions provably differ.

No difference found (not proven): No behavior difference found in \_cross\_community\_surprises (not a proof).

The verifier ran both versions of \_cross\_community\_surprises 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.

No difference found (not proven): No behavior difference found in \_cross\_file\_surprises (not a proof).

The verifier ran both versions of \_cross\_file\_surprises 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.

No difference found (not proven): No behavior difference found in god\_nodes (not a proof).

The verifier ran both versions of god\_nodes 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 build.

The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_from\_json.

The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_merge.

The verifier did not have enough to check build\_merge, 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 | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_load\_existing\_graph.

The verifier did not have enough to check \_load\_existing\_graph, 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 `Path` — outside the synthesizable primitive/collection set

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 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)

No difference found (not proven): No behavior difference found in cluster (not a proof).

The verifier ran both versions of cluster 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.

No difference found (not proven): No behavior difference found in cohesion\_score (not a proof).

The verifier ran both versions of cohesion\_score 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.

No difference found (not proven): No behavior difference found in label\_communities\_by\_hub (not a proof).

The verifier ran both versions of label\_communities\_by\_hub 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.

No difference found (not proven): No behavior difference found in score\_all (not a proof).

The verifier ran both versions of score\_all 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 detect\_incremental.

The verifier did not have enough to check detect\_incremental, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify diagnose\_extraction.

The verifier did not have enough to check diagnose\_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: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify diagnose\_file.

The verifier did not have enough to check diagnose\_file, 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 `path` is annotated `str | Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify format\_diagnostic\_report.

The verifier did not have enough to check format\_diagnostic\_report, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in to\_graphml (not a proof).

The verifier ran both versions of to\_graphml 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 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

Could not verify: Could not verify \_import\_js.

The verifier did not have enough to check \_import\_js, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_apply\_symbol\_resolution\_facts.

The verifier did not have enough to check \_apply\_symbol\_resolution\_facts, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_js\_exported\_declaration\_names.

The verifier did not have enough to check \_js\_exported\_declaration\_names, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_package\_entry\_candidates.

The verifier did not have enough to check \_package\_entry\_candidates, 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 `package_dir` is annotated `Path` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_resolve\_export\_target (not a proof).

The verifier ran both versions of \_resolve\_export\_target 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.

· 4 grounded finding(s) anchored inline below; 109 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/build.py
Comment thread graphify/cli.py
Comment thread graphify/extractors/resolution.py
Comment thread graphify/paths.py
@Neonsy

Neonsy commented Aug 21, 2026

Copy link
Copy Markdown
Author

Resolving with a rebase

@Neonsy
Neonsy force-pushed the fix/2849-graph-integrity branch from 5516564 to 0808ec3 Compare August 21, 2026 16:32

@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 5 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) alter behavior, breaking input(s) attached.

Behavior changes: \_default\_graph\_path changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_default\_graph\_path behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This is a sound refutation from differential testing (both versions run on many generated inputs): a concrete input on which the two versions provably differ.


Graphify review — findings

Adds a --multigraph extract flag that stores the authoritative graph as a MultiDiGraph to preserve parallel directed edges, persisting the setting with the graph. Introduces per-source-root output-root remembering under ~/.graphify/output-roots/ so --out can point anywhere and is reused across extract/update/watch/query/export; adds an analysis_projection helper so degree-based analytics collapse parallel edges correctly. Updates CLI help text and README to describe the new flag and configured-output behavior.

Worth a look

  • analysis_projection may drop nodes, causing KeyError when indexing degree by G.nodesgraphify/analyze.py:116 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • god_nodes can raise for nodes omitted from analysis_projectiongraphify/analyze.py:116 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • graphify update without a path now rejects a saved scan root when it differs from the configured bindinggraphify/cli.py:99 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • --no-dedup is ignored for no-cluster multigraph extractiongraphify/cli.py:3925 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • cluster() no longer clusters over all caller-supplied edgesgraphify/cluster.py:159 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 3845 functions depend on the 1684 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 477 callers, 43 callees
  • new: _rebuild_code() — 99 callers, 54 callees
  • new: build_from_json() — 189 callers, 20 callees
  • new: detect() — 108 callers, 15 callees
  • new: build_merge() — 48 callers, 14 callees
  • new: _extract_generic() — 18 callers, 24 callees
  • new: save_manifest() — 39 callers, 11 callees
  • new: to_obsidian() — 34 callers, 12 callees
  • …and 107 more — each is listed as a finding

Verification — 3845 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: 3694 function(s) in the blast radius were not formally verified this run

Formal verification

Behavior changes: \_default\_graph\_path changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_default\_graph\_path behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This is a sound refutation from differential testing (both versions run on many generated inputs): a concrete input on which the two versions provably differ.

No difference found (not proven): No behavior difference found in \_cross\_community\_surprises (not a proof).

The verifier ran both versions of \_cross\_community\_surprises 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.

No difference found (not proven): No behavior difference found in \_cross\_file\_surprises (not a proof).

The verifier ran both versions of \_cross\_file\_surprises 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.

No difference found (not proven): No behavior difference found in god\_nodes (not a proof).

The verifier ran both versions of god\_nodes 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 build.

The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_from\_json.

The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_merge.

The verifier did not have enough to check build\_merge, 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 | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_load\_existing\_graph.

The verifier did not have enough to check \_load\_existing\_graph, 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 `Path` — outside the synthesizable primitive/collection set

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 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)

No difference found (not proven): No behavior difference found in cluster (not a proof).

The verifier ran both versions of cluster 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.

No difference found (not proven): No behavior difference found in cohesion\_score (not a proof).

The verifier ran both versions of cohesion\_score 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.

No difference found (not proven): No behavior difference found in label\_communities\_by\_hub (not a proof).

The verifier ran both versions of label\_communities\_by\_hub 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.

No difference found (not proven): No behavior difference found in score\_all (not a proof).

The verifier ran both versions of score\_all 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 detect\_incremental.

The verifier did not have enough to check detect\_incremental, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify diagnose\_extraction.

The verifier did not have enough to check diagnose\_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: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify diagnose\_file.

The verifier did not have enough to check diagnose\_file, 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 `path` is annotated `str | Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify format\_diagnostic\_report.

The verifier did not have enough to check format\_diagnostic\_report, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in to\_graphml (not a proof).

The verifier ran both versions of to\_graphml 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 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

Could not verify: Could not verify \_import\_js.

The verifier did not have enough to check \_import\_js, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_apply\_symbol\_resolution\_facts.

The verifier did not have enough to check \_apply\_symbol\_resolution\_facts, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_js\_exported\_declaration\_names.

The verifier did not have enough to check \_js\_exported\_declaration\_names, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_package\_entry\_candidates.

The verifier did not have enough to check \_package\_entry\_candidates, 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 `package_dir` is annotated `Path` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_resolve\_export\_target (not a proof).

The verifier ran both versions of \_resolve\_export\_target 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.

· 4 grounded finding(s) anchored inline below; 111 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/build.py
Comment thread graphify/cli.py
Comment thread graphify/extractors/resolution.py
Comment thread graphify/paths.py
@Neonsy

Neonsy commented Aug 24, 2026

Copy link
Copy Markdown
Author

@safishamsi When you have time, could you review this PR? It addresses #2849 and includes regression coverage for the reported JS/TS resolution and parallel-edge failures. The branch is rebased onto v8 and the visible checks pass.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant