Skip to content

fix(graph): correct symbol resolution, centrality, and edge recall (v2.6.0) - #163

Merged
Anandb71 merged 3 commits into
mainfrom
fix/graph-truth-layer
Aug 2, 2026
Merged

fix(graph): correct symbol resolution, centrality, and edge recall (v2.6.0)#163
Anandb71 merged 3 commits into
mainfrom
fix/graph-truth-layer

Conversation

@Anandb71

@Anandb71 Anandb71 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Nine defects in the analysis core, each found by running Arbor against real
codebases (honojs/hono, psf/requests) rather than fixtures, and each with a
regression test.

Correctness

Determinism. resolve_with_context decided same-directory locality by
iterating a HashMap and taking the first hit. Rust seeds RandomState per
process, so the same binary on the same input could build different edges
between runs — the opposite of the deterministic walk Arbor advertises.
Resolution now works from an explicit index and sorts candidates by
(file, node index). A cross-process test indexes a fixture eight times in
fresh processes and asserts one identical graph.

Symbol collisions. The FQN map used HashMap::insert, so a second
definition of handler, new, or process silently replaced the first. The
loser became unreachable: zero callers, zero centrality, invisible to blast
radius. The table now keeps every definition and reports ambiguity.

Ambiguous lookups answered confidently and wrongly. arbor inspect getPath
on hono reported "unreachable, 0 callers, may be dead code" about a function
called from ten places. Eleven call sites across the CLI and MCP bridge each
resolved names with find_by_name(..).first() — whichever file parsed first.
ArborGraph::resolve_symbol_ranked is now the single policy, and the CLI says
which definition it chose and what else matched.

TypeScript symbols were extracted twice. The export_statement arm recursed
into its declaration children, then the generic loop recursed into the same
children again. Every exported symbol became two vertices sharing one
CodeNode::id. On a 149-file Next.js app that was 133 phantom nodes — 25% of
the graph — splitting centrality and double-counting blast radius.

Prose was indexed as code. Markdown headings became graph nodes, so a
# fetch benchmark heading in a README competed with real fetch definitions
during resolution. Shell comments became functions: # Compare app.fetch() between refs yielded a function named "# Compare app.fetch". 59 phantom
nodes on hono, every one a false resolution candidate.

Centrality was not comparable between repositories. Scores were divided by
the graph maximum, so the top node was 1.0 by construction and a threshold
like 0.6 meant "60% as central as whatever the biggest thing here happens to
be". Adding one hub rescaled every other node. Centrality is now a percentile
rank; raw PageRank is retained for warm starts.

Recall

obj.method() was discarded outright in TS/JS on the grounds that typing the
receiver needs inference we lack. That is the dominant call shape in real
TypeScript, so the graph came out nearly edgeless on the largest supported
ecosystem — and an empty graph reports a blast radius of zero, which reads as
"safe" rather than "unknown". Such calls now resolve by method name under a
fan-out cap and reduced confidence.

Measured on identical node sets, after the duplicate fix:

before after
arbor-cloud/web/src (TS) 172 edges 196 (+14%)
arbor-graph/src (Rust) 116 edges 167 (+44%)

New capability

Edge confidence. Every edge carries [0,1] derived from how it resolved,
so BFS no longer treats a proven same-file call and a heuristic suffix guess as
identical evidence. confident_edge_count exposes how much of a graph rests on
inference.

Hunk-level impact. changed_node_ids_for_ranges maps diff line ranges to
the symbols that actually overlap them; parse_unified_diff_ranges reads them
from a patch. File granularity treats a one-line edit in a 60-symbol file as
changing all 60.

Concept search. Identifiers are tokenized (snake, camel, Pascal, acronym
runs) and expanded through curated concept clusters, so login finds
get_authenticated — which shares no substring and was previously unfindable.
Docstrings, signatures, and file paths are now indexed too; they were parsed
into every node and then ignored. Results carry the match kind, so an exact hit
and a concept guess stay distinguishable.

Verification

320 tests pass, clippy --all-targets -D warnings clean, fmt clean.
Exercised end to end via the CLI and the MCP bridge against hono (307 TS files)
and psf/requests, plus a real merged PR through the full analysis pipeline.

Version bumped to 2.6.0. The bump also invalidates graph caches via
CACHE_VERSION, which is required: the serialized format gained edge
confidence and raw centrality.

Found by running the CLI over honojs/hono.

Markdown was in the code-graph extension list "for knowledge graphs
(Lattice)", so a `# fetch benchmark` heading in a README became a graph node
named `fetch` — and then competed with the real `fetch` definitions when
resolving TypeScript call sites. Markdown is still parseable, just no longer
part of the default code index.

Shell comments became functions. `parse_shell_line` looks for `()` anywhere
on the line, so `# Compare app.fetch() between the working tree and a git
ref` produced a function named "# Compare app.fetch". The comment guard ran
*after* parsing and only skipped a line when nothing had matched, so any
comment that happened to parse was kept. Comments are now skipped before
parsing, and shell function names must be bare identifiers.

Together these were 59 phantom nodes on hono — every one of them a false
candidate during symbol resolution.
`arbor setup` on hono printed dozens of WARN lines before its success
message — every ambiguous method name and every unimported cross-module
reference. Names like `.get`, `.encode`, and `.match` are shared by many
types in any real codebase, and most references are stdlib or third-party
with no definition to find. None of it is a user problem.

These are diagnostics for someone debugging resolution, so they move to
`debug!`. A first run should end with a clean success line, not a wall of
warnings about normal behaviour.
`arbor inspect getPath` on hono reported "Role: unreachable, Callers: 0" and
suggested the symbol might be dead code. `getPath` is one of hono's hottest
utilities, called from ten places including the line directly below it.

The graph was right; the lookup was not. `getPath` names five nodes — a
function in `utils/url.ts` and methods on four AWS Lambda event processors —
and every caller resolved it with `find_by_name(..).first()`, i.e. whichever
file happened to be parsed first. The CLI and the MCP bridge each had their
own copy of that mistake, eleven call sites in total.

`ArborGraph::resolve_symbol_ranked` becomes the single resolution policy:
rank by graph degree, then centrality, then file path, so the pick is both
meaningful and independent of parse order. The CLI additionally prints which
definition it chose and what else matched, because answering an ambiguous
question silently is how this turned into a confidently wrong answer.

Also strips Windows' `\?\` verbatim prefix from resolved paths, which was
leaking into every line of user-facing output, and makes file lookup
separator-insensitive — callers join a project root with a forward-slash
relative path, producing `C:\root\src/lib.rs` where the parser recorded
`C:\root\src\lib.rs`, so `file-graph` reported "No symbols found" for files
full of symbols.
@arbor-cloud

arbor-cloud Bot commented Aug 2, 2026

Copy link
Copy Markdown

🔴 Arbor PR Walk

Path heat HIGH ████░░░░░░ 42%
Branch fix/graph-truth-layermain
Impact 5 files · 44 symbols · 71 reachable callers
Entry Points 8 production endpoints reached
Languages Rust

Changed Files

File Symbols Direct Transitive Risk
🔴 crates/arbor-core/src/fallback_parser.rs 18 16 HIGH
🔴 crates/arbor-cli/src/commands.rs 11 8 24 HIGH
🔴 crates/arbor-graph/src/graph.rs 11 3 HIGH
crates/arbor-mcp/src/lib.rs 2 NONE
crates/arbor-graph/src/builder.rs 2 NONE

⚠️ High-Risk Symbols

  • take_ident in crates/arbor-core/src/fallback_parser.rs — centrality 100%, 6 callers

    Load-bearing function — among the most connected symbols in this repository. 6 callers depend on it.

  • parse_fallback_source in crates/arbor-core/src/fallback_parser.rs — centrality 99%, 13 callers

    Load-bearing function — among the most connected symbols in this repository. 13 callers depend on it.

  • resolve_project_path in crates/arbor-cli/src/commands.rs — centrality 99%, 29 callers

    Load-bearing function — among the most connected symbols in this repository. 29 callers depend on it.

  • find_workspace_root in crates/arbor-cli/src/commands.rs — centrality 99%, 1 caller

    Load-bearing function — among the most connected symbols in this repository. 1 callers depend on it.

  • strip_verbatim_prefix in crates/arbor-cli/src/commands.rs — centrality 99%, 1 caller

    function is more central than 99% of this repository.

🎯 Production Entry Points Reached

This change propagates to these entry points (HTTP handlers, jobs, CLI commands):

  • add_node
  • agent_guard
  • agent_onboard
  • agent_review
  • audit
  • bridge
  • callees
  • callers

✅ Before You Merge

  • Manually verify the affected entry points: add_node, agent_guard, agent_onboard (+5).
  • Write or update tests for take_ident — 100% centrality means failures cascade to 6 callers.
  • Write or update tests for parse_fallback_source — 99% centrality means failures cascade to 13 callers.
  • Consider splitting this PR — 71 reachable callers were found. Smaller PRs are safer to review and roll back.
  • Request a senior engineer review before merging.

🔍 Sensitive Path Check — REVIEW REQUIRED

2 sensitive surfaces · 0 entry points reachable · Confidence: 80%

Category File Symbols
File I/O crates/arbor-graph/src/graph.rs remove_file, test_graph_remove_file_cleanup
Input Validation crates/arbor-cli/src/commands.rs Command Injection (CWE-78), line 878
Sensitive call paths
  • change reaches sink: agent_onboardresolve_project_pathfind_workspace_rootstrip_verbatim_prefix (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 878
  • change reaches sink: agent_onboardresolve_project_pathfind_workspace_rootstrip_verbatim_prefix (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 904
  • change reaches sink: agent_onboardresolve_project_pathfind_workspace_rootstrip_verbatim_prefix (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 909
  • change reaches sink: agent_onboardresolve_project_pathfind_workspace_rootstrip_verbatim_prefix (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 1503
  • change reaches sink: agent_onboardresolve_project_pathfind_workspace_rootstrip_verbatim_prefix (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 1524
  • change reaches sink: agent_onboardresolve_project_pathfind_workspace_rootstrip_verbatim_prefix (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 1810
  • change reaches sink: agent_onboardresolve_project_pathfind_workspace_rootstrip_verbatim_prefix (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 2615
  • 🟡 Fix Command Injection in commands.rs (line 878) [31 internal callers — inspect full call path before merge; path: agent_onboard -> resolve_project_path -> find_workspace_root -> strip_verbatim_prefix] — Command::new() with non-literal command name — validate all inputs.
  • 🟡 Verify input validation still rejects malformed and malicious input
📊 Analysis confidence: High · 1727 nodes · 10035ms
  • Graph has 1727 nodes and 856 edges — well-connected codebase
  • 44 symbols changed, 71 upstream nodes analyzed

Arbor · View full report → · arbor v2.5.0 · 10035ms · 1727 nodes · verify locally with arbor diff · Deterministic PR blast-radius · Was this useful? 👍 👎

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

🌳 Arbor Impact Report

Risk Level: 🔴 Critical | Blast Radius: 180 nodes | Changed Symbols: 273

Changed Files

File Status
crates/arbor-cli/src/commands.rs Modified
crates/arbor-core/src/fallback_parser.rs Modified
crates/arbor-graph/src/builder.rs Modified
crates/arbor-graph/src/graph.rs Modified
crates/arbor-mcp/src/lib.rs Modified

📊 Visual Impact Graph

graph TD
  classDef changed fill:#ef4444,stroke:#333,stroke-width:2px,color:#fff;
  classDef caller fill:#f59e0b,stroke:#333,stroke-width:1px,color:#fff;
  find_workspace_root[find_workspace_root] --> strip_verbatim_prefix[strip_verbatim_prefix]
  resolve_project_path[resolve_project_path] --> find_workspace_root[find_workspace_root]
  class Result changed;
  class DiffSummary changed;
  class find_workspace_root changed;
  class ROOT_MARKERS changed;
  class strip_verbatim_prefix changed;
  class resolve_project_path caller;
Loading

Impact Summary

Metric Count
Direct callers affected 118
Indirect callers affected 52
API entrypoints impacted 88
Files likely requiring updates 9
Total blast radius 180

⚠️ Warning: 88 API entrypoints are affected. Integration tests recommended.

🔍 Suggestion: Consider breaking this change into smaller PRs.


Powered by Arbor v2.6.0 — graph-native code intelligence

@Anandb71
Anandb71 merged commit 0c729ea into main Aug 2, 2026
8 checks passed
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