Skip to content

Fix/graph truth layer - #162

Merged
Anandb71 merged 10 commits into
mainfrom
fix/graph-truth-layer
Jul 31, 2026
Merged

Fix/graph truth layer#162
Anandb71 merged 10 commits into
mainfrom
fix/graph-truth-layer

Conversation

@Anandb71

Copy link
Copy Markdown
Collaborator

Description

Brief description of what this PR does. Link any related issues.

Fixes #(issue number)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Performance improvement
  • Code refactoring

Changes Made

  • List the key changes made in this PR
  • Be specific about files/modules affected

Testing

Describe how you tested your changes:

  • Ran cargo test --all
  • Ran cargo clippy --all
  • Ran flutter test (if applicable)
  • Tested manually with a real codebase

Screenshots (if applicable)

For visualizer changes, include before/after screenshots.

Checklist

  • My code follows the project's style guidelines
  • I have added tests for my changes
  • I have updated the documentation where necessary
  • All new and existing tests pass
  • I have added appropriate comments where the code isn't self-explanatory

Anandb71 added 7 commits July 30, 2026 21:14
The `export_statement` arm recursed into its declaration children, and then
the generic child loop below recursed into those same children again. Every
exported symbol became two graph vertices sharing one `CodeNode::id`, since
the id hashes (file, qualified_name, kind).

On a 149-file Next.js app that was 133 phantom nodes — 25% of the graph.
Each duplicate halved its symbol's centrality and double-counted it in blast
radius.

The arm was redundant anyway: the generic recursion already reaches every
declaration, and `is_node_exported` reads the parent node, so export status
survives without it.
`SymbolTable::insert` used `HashMap::insert`, so a second definition of an
FQN silently replaced the first. Colliding names are ordinary — `handler` in
twelve route files, `new` on forty structs, `process` in both `utils.py` and
`helpers.py` — and the loser became unreachable: zero callers, zero
centrality, invisible to blast radius. A silent false negative in the exact
analysis the product sells.

The table now keeps every definition and reports how a reference matched:
exact FQN, same file, unique suffix, same directory, or genuinely ambiguous.

Resolution is also deterministic now. It previously 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 pick
differently between runs. Candidates are sorted by (file, node index) before
any choice is made, and a reverse file index replaces the scan.

That scan was also the hot path: resolution walked every FQN in the
repository per reference, with a nested scan over every file's export list.
Unresolvable references — stdlib and third-party calls, the majority of call
sites in real code — paid the full cost. Segment-aligned suffixes are now
indexed, making it an O(1) lookup.
Edges were all created as bare `Edge::new`, so a proven same-file call and a
heuristic suffix guess were indistinguishable downstream. BFS weighted them
identically and every consumer treated the whole graph as fact.

`Edge` now carries a confidence in [0,1]. The field defaults to 1.0 on
deserialize, so graphs cached by an older build keep their previous
behaviour exactly.
Edge construction now reads the resolution kind and stamps the matching
confidence, so an exact fully-qualified match and a same-directory guess are
no longer equal evidence.

Ambiguous references produce a low-confidence edge to each candidate rather
than being dropped, capped at four. Dropping them hid real call paths;
emitting them unlabelled overstated certainty.

The import filter also stops exempting same-directory targets. It only ever
fired when a reference was unimported *and* the target lived elsewhere in
the tree, which meant that in a flat `src/` layout — the common case for JS
and Python projects — it never fired at all. Suspicion is now expressed as a
confidence multiplier applied uniformly instead of a hard drop with a
loophole.
`obj.method()` was discarded outright, on the grounds that typing `obj`
needs inference we do not have. But that is the dominant call shape in real
TypeScript and JavaScript, so the graph came out nearly edgeless on the
largest ecosystem Arbor supports — and an empty graph reports a blast radius
of zero, which reads as "safe" rather than "unknown".

Such calls now emit a marker that the graph builder resolves by method name,
under a tighter fan-out cap and a reduced confidence. Capitalised receivers
(`Logger.info`, `MathUtils.add`) keep their qualifier and match exactly,
since every mainstream convention reserves those for classes, enums, and
namespace imports.

Precision is preserved by the ambiguity machinery rather than by silence:
a name shared by three or more definitions still links to nothing.

Measured on identical node sets, after the duplicate-extraction fix:

  arbor-cloud/web/src (TS)   172 -> 196 edges  (+14%)
  arbor-graph/src (Rust)     116 -> 167 edges  (+44%)
Scores were divided by the largest score in the graph. That bounded the
range but destroyed comparability: the top node is 1.0 by construction
whether it has four callers or four hundred, so a threshold like 0.6 means
"60% as central as whatever the biggest thing in this repository happens to
be". In a codebase with one god object nothing ever cleared it; in a flat
service almost everything did.

It was also unstable. Adding a single new hub rescaled every other node, so
a PR could change the reported centrality — and any risk level derived from
it — of code it never touched.

`centrality()` now returns a percentile rank: 0.9 means "more central than
90% of this repository", and carries that meaning everywhere. Raw PageRank
is kept alongside it because warm-start recomputation needs the true fixed
point, and `centrality_map()` returns raw so the watcher's incremental path
converges exactly.
`changed_node_ids` works at file granularity: every symbol in a touched file
counts as changed. For a one-line edit in a file with sixty functions that
overstates the blast radius roughly sixtyfold, which is the common case for
any PR-driven integration.

`changed_node_ids_for_ranges` keeps only symbols whose line spans overlap
the changed lines, and `parse_unified_diff_ranges` reads those ranges
straight from a patch. The whole hunk span is used, context included: a
delete-only hunk has no added lines to point at, but the deletion still
changes the enclosing symbol and the context is what locates it.

Files whose changes fall outside every tracked symbol — an import block, a
comment, a top-level constant — are reported rather than silently dropped,
so a caller can say "changed outside any tracked symbol" instead of
implying zero impact.
@arbor-cloud

arbor-cloud Bot commented Jul 31, 2026

Copy link
Copy Markdown

🔴 Arbor PR Walk

Path heat HIGH █░░░░░░░░░ 16%
Branch fix/graph-truth-layermain
Impact 18 files · 408 symbols · 169 reachable callers
Entry Points 8 production endpoints reached
Languages Rust

Changed Files

File Symbols Direct Transitive Risk
🔴 crates/arbor-cli/src/commands.rs 91 75 26 HIGH
🔴 crates/arbor-graph/src/search_index.rs 41 22 HIGH
🔴 crates/arbor-graph/src/ranking.rs 24 21 1 HIGH
🔴 crates/arbor-graph/src/symbol_table.rs 35 17 1 HIGH
🔴 crates/arbor-graph/src/diff.rs 29 15 HIGH
🔴 crates/arbor-core/src/languages/typescript.rs 34 9 HIGH
🔴 crates/arbor-graph/benches/graph_bench.rs 8 6 HIGH
🔴 crates/arbor-watcher/tests/determinism.rs 7 5 HIGH
🟡 crates/arbor-graph/src/lexicon.rs 24 4 MEDIUM
🟡 crates/arbor-graph/src/builder.rs 30 3 MEDIUM
🟡 crates/arbor-graph/src/graph.rs 59 2 MEDIUM
crates/arbor-watcher/examples/graph_stats.rs 2 NONE
crates/arbor-graph/src/lib.rs 14 NONE
crates/arbor-graph/src/edge.rs 10 NONE

🎯 Production Entry Points Reached

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

  • add_node
  • adding_a_hub_does_not_rescale_unrelated_nodes
  • agent_guard
  • agent_onboard
  • agent_review
  • ambiguous_reports_all_candidates
  • audit
  • bench_centrality

✅ Before You Merge

  • Manually verify the affected entry points: add_node, adding_a_hub_does_not_rescale_unrelated_nodes, agent_guard (+5).
  • Consider splitting this PR — 169 reachable callers were found. Smaller PRs are safer to review and roll back.
  • Request a senior engineer review before merging.

🛑 Sensitive Path Check — GATE

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

Category File Symbols
Authentication crates/arbor-graph/src/lexicon.rs login_and_authenticated_share_a_concept
Authentication crates/arbor-graph/src/search_index.rs login_finds_get_authenticated
File I/O crates/arbor-graph/src/graph.rs remove_file, test_graph_remove_file_cleanup
Input Validation crates/arbor-watcher/tests/determinism.rs Command Injection (CWE-78), line 139
Input Validation crates/arbor-cli/src/commands.rs Command Injection (CWE-78), line 857
Input Validation crates/arbor-graph/src/ranking.rs Unsafe Pointer Operation (CWE-119), line 71
Sensitive call paths
  • caller reaches change: login_finds_get_authenticatednode_id (1 hop) — login_finds_get_authenticated (Authentication) depends on changed code, 1 hop away
  • change reaches sink: same_process_repeat_indexing_is_stablewrite_fixturefixture_dir (2 hops) — full graph path to Command Injection in crates/arbor-watcher/tests/determinism.rs line 139
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 857
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 883
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 888
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 1482
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 1503
  • change reaches sink: indexindex_changed_onlyload_or_index_graphauto_index_enabled (3 hops) — full graph path to Command Injection in crates/arbor-cli/src/commands.rs line 1789
  • 🟡 Fix Command Injection in determinism.rs (line 139) [5 internal callers — inspect full call path before merge; path: same_process_repeat_indexing_is_stable -> write_fixture -> fixture_dir] — Command::new() with non-literal command name — validate all inputs.
  • 🟡 Fix Command Injection in commands.rs (line 857) [30 internal callers — inspect full call path before merge; path: index -> index_changed_only -> load_or_index_graph -> auto_index_enabled] — Command::new() with non-literal command name — validate all inputs.
  • 💬 Fix Unsafe Pointer Operation in ranking.rs (line 71) [zero internal callers found — not proven externally exposed; verify dead-code vs runtime entry] — Raw pointer operation — ensure validity and lifetime guarantees.
  • 🔴 Review authentication changes with the engineer who owns that path
  • 🟡 Verify input validation still rejects malformed and malicious input
  • 🔴 Require owner sign-off before merging
📊 Analysis confidence: High · 1710 nodes · 9924ms
  • Graph has 1710 nodes and 841 edges — well-connected codebase
  • 408 symbols changed, 169 upstream nodes analyzed

Arbor · View full report → · 9924ms · 1710 nodes · Deterministic PR blast-radius · Was this useful? 👍 👎

@github-actions

Copy link
Copy Markdown

🌳 Arbor Impact Report

Risk Level: 🔴 Critical | Blast Radius: 241 nodes | Changed Symbols: 487

Changed Files

File Status
.gitignore Modified
Cargo.lock Modified
Cargo.toml Modified
README.md Modified
crates/arbor-cli/src/commands.rs Modified
crates/arbor-core/src/languages/typescript.rs Modified
crates/arbor-graph/benches/graph_bench.rs Modified
crates/arbor-graph/src/builder.rs Modified
crates/arbor-graph/src/diff.rs Modified
crates/arbor-graph/src/edge.rs Modified
crates/arbor-graph/src/graph.rs Modified
crates/arbor-graph/src/lexicon.rs Modified
crates/arbor-graph/src/lib.rs Modified
crates/arbor-graph/src/ranking.rs Modified
crates/arbor-graph/src/search_index.rs Modified
crates/arbor-graph/src/symbol_table.rs Modified
crates/arbor-watcher/examples/graph_stats.rs Modified
crates/arbor-watcher/tests/determinism.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;
  class Golden changed;
  class Features changed;
  class Arbor changed;
  class Development changed;
  class Configuration changed;
Loading

Impact Summary

Metric Count
Direct callers affected 177
Indirect callers affected 77
API entrypoints impacted 134
Files likely requiring updates 9
Total blast radius 241

⚠️ Warning: 134 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 4579985 into main Jul 31, 2026
10 of 11 checks passed
Search matched character substrings against names, so asking for `login`
found nothing at all in a codebase whose function is called
`get_authenticated`. The two share no substring. Anyone asking Arbor about
an auth surface hit this immediately.

Identifiers are now tokenized — snake, camel, Pascal, kebab, acronym runs —
and expanded through curated concept clusters, so `login` reaches
`authenticate`, `session`, `credential`, and the rest of its cluster. This
is vocabulary resolution, not a semantic model: deterministic, offline, and
auditable, with the clusters right there in the source.

Docstrings, signatures, qualified names, and file paths are indexed too.
All four were already parsed into every `CodeNode` and then ignored, so a
function documented as "validates the user's login credentials" was
unreachable by a search for "login".

Results carry the match kind, so an exact hit and a concept guess are
distinguishable instead of blended into one opaque ranking. `search()` keeps
its literal-substring semantics; `search_ranked()` is the new lens.

The substring verifier also stops rescanning the entire name index once per
candidate, which made the old `search` slower than the linear scan its
documentation claimed to replace.
Arbor's headline claim is a deterministic walk, and nothing tested it.

Running the builder twice inside one process would not have caught the bug
it is written for: a single process shares one hash seed, so `HashMap`
iteration order is stable within a run and varies only between runs. These
tests shell out to fresh processes and assert one identical graph across
eight of them.

The fixture is built to exercise every ambiguous path — colliding bare names
across directories, same-name methods on different classes, and calls on
untyped receivers.

Also adds a `graph_stats` example reporting node count, edge count, edge
density, confident-vs-weak edge split, and orphan count. It is what the edge
recall figures in this branch were measured with, so the numbers are
reproducible rather than asserted.
Documents the correctness work and what each defect actually cost, with the
measured edge-recall figures and the command to reproduce them.

The version bump is load-bearing, not ceremony: `CACHE_VERSION` derives from
the package version, and centrality now means percentile rank rather than a
max-normalized score. A cache written by 2.5.0 would deserialize cleanly and
be interpreted wrongly, so it has to be invalidated.

Concept search is documented as a library capability. `arbor query` still
does literal substring matching and is described that way — the CLI wiring
is not done, and claiming otherwise is the kind of overclaim this release
exists to remove.
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