Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Route get_node through the same resolver as get_neighbors - #3161

Closed
noQbot wants to merge 1 commit into
Graphify-Labs:v8from
VinvAI:fix-get-node-resolution
Closed

Route get_node through the same resolver as get_neighbors#3161
noQbot wants to merge 1 commit into
Graphify-Labs:v8from
VinvAI:fix-get-node-resolution

Conversation

@noQbot

@noQbot noQbot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Route the MCP get_node tool through the same node resolver as get_neighbors, so both tools resolve a label the same way. Previously get_node did a raw substring scan over G.nodes(data=True) and returned matches[0] — an arbitrary match determined by graph iteration order — while get_neighbors used 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

  • Added one module-level helper, _resolve_single_node(G, label), that does the tiered _find_node match plus the cross-file find_node_ambiguity check and returns (node_id, None) for a clean winner or (None, message) for no-match / ambiguity.
  • _tool_get_node now routes through it — replacing the substring-scan + [0]. It returns the tiered winner, or the same Ambiguous — retry with the repo-relative path or the full node id message get_neighbors already used, when the winning tier spans multiple files.
  • _tool_get_neighbors now 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_ambiguity exists specifically so that [0]-of-a-tie doesn't present one arbitrary file as "the answer". get_node never went through it, so a hub lookup (e.g. extract) silently returned whichever extract_* helper came first in G.nodes() order — and a graph rebuild that reorders nodes could change the answer.

Testing

  • New unit test for _resolve_single_node (ambiguous → message, unique → id, missing → not-found) — runs offline, passes.
  • New HTTP end-to-end test: get_node and get_neighbors both report "Ambiguous" for a cross-file label and both resolve a unique label cleanly.
  • Existing serve tests pass (test_serve.py: 146 passed). ruff clean.

Notes

  • Root cause and fix were identified using Cursor (Composer 2.5) + Vinv AI

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

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.

Formal verification. 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 resolvegraphify/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 matchgraphify/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).

Comment thread graphify/serve.py
return matches[0], None


def _shortest_path_text(G: nx.Graph, arguments: dict) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_shortest_path_text()

high coupling complexity (Ca·Ce = 20).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@safishamsi

Copy link
Copy Markdown
Collaborator

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

@safishamsi safishamsi closed this Aug 29, 2026
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.

get_node and get_neighbors disagree on the same label — get_node returns an arbitrary iteration-order match

2 participants