Route get_node through the same resolver as get_neighbors - #3161
Conversation
get_node did a raw substring scan over G.nodes(data=True) and returned matches[0], so a hub label like "extract" resolved to whichever extractor helper came first in graph iteration order. get_neighbors already used _find_node + find_node_ambiguity and reported the same lookup as ambiguous, so the two tools disagreed on the same label (ADR-0001 finding 1). Extract one shared _resolve_single_node(G, label) helper (tiered _find_node match plus the cross-file ambiguity check) and route both get_node and get_neighbors through it. get_neighbors keeps identical behaviour and messages; get_node now returns the tiered winner, or the same "Ambiguous ... retry with path or id" message when the winning tier spans several files, instead of an arbitrary iteration-order match. Tests: a unit test for the shared resolver (ambiguous -> message, unique -> id, missing -> not-found) and an HTTP end-to-end test asserting get_node and get_neighbors agree on an ambiguous label. Co-Authored-By: Vinv-AI <[email protected]>
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Routes the get_node MCP tool through the same _resolve_single_node helper that get_neighbors already used, so both now apply the tiered _find_node ranking and return the same "Ambiguous" message when a label matches nodes across multiple files. This replaces get_node's previous behaviour of silently returning the first G.nodes() iteration-order match for a hub name; unique labels still resolve cleanly and misses return a not-found message.
Worth a look
- get_node changes matching semantics: substring label matches no longer resolve —
graphify/serve.py:1787· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- get_node now rejects ambiguous labels instead of returning the first match —
graphify/serve.py:1787· 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 — 577 functions depend on the 368 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 122 callees - new:
_query_graph_text()— 20 callers, 9 callees - new:
_score_query()— 15 callers, 5 callees - new:
_query_terms()— 20 callers, 3 callees - new:
run_benchmark()— 16 callers, 3 callees - new:
_build_server()— 2 callers, 16 callees - new:
_load_graph()— 9 callers, 3 callees - new:
_query_subgraph_tokens()— 7 callers, 3 callees - …and 8 more — each is listed as a finding
Verification — 577 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: 410 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_build\_server.
The verifier did not have enough to check \_build\_server, 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
· 1 grounded finding(s) anchored inline below; 15 more finding(s) on lines outside this diff (see the check run).
| return matches[0], None | ||
|
|
||
|
|
||
| def _shortest_path_text(G: nx.Graph, arguments: dict) -> str: |
There was a problem hiding this comment.
_shortest_path_text()
high coupling complexity (Ca·Ce = 20).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Shipped in v0.9.52 via authorship-preserving cherry-pick so you keep contributor-graph credit. Thanks @noQbot! Release: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.52 |
Summary
Route the MCP
get_nodetool through the same node resolver asget_neighbors, so both tools resolve alabelthe same way. Previouslyget_nodedid a raw substring scan overG.nodes(data=True)and returnedmatches[0]— an arbitrary match determined by graph iteration order — whileget_neighborsused tiered matching plus ambiguity detection. For a hub name that matches several nodes in different files, the two tools gave different answers.Fixes #3160
What changed
_resolve_single_node(G, label), that does the tiered_find_nodematch plus the cross-filefind_node_ambiguitycheck and returns(node_id, None)for a clean winner or(None, message)for no-match / ambiguity._tool_get_nodenow routes through it — replacing the substring-scan +[0]. It returns the tiered winner, or the sameAmbiguous — retry with the repo-relative path or the full node idmessageget_neighborsalready used, when the winning tier spans multiple files._tool_get_neighborsnow calls the same helper. Its behaviour and messages are unchanged — the resolution logic it already had just moved into the shared function, so both tools have one resolution contract.Why
find_node_ambiguityexists specifically so that[0]-of-a-tie doesn't present one arbitrary file as "the answer".get_nodenever went through it, so a hub lookup (e.g.extract) silently returned whicheverextract_*helper came first inG.nodes()order — and a graph rebuild that reorders nodes could change the answer.Testing
_resolve_single_node(ambiguous → message, unique → id, missing → not-found) — runs offline, passes.get_nodeandget_neighborsboth report "Ambiguous" for a cross-file label and both resolve a unique label cleanly.test_serve.py: 146 passed). ruff clean.Notes