Skip to content

fix(path): prefer full-token label matches when resolving path endpoints#1785

Closed
CJNA wants to merge 1 commit into
Graphify-Labs:v8from
CJNA:fix/path-endpoint-full-token-match
Closed

fix(path): prefer full-token label matches when resolving path endpoints#1785
CJNA wants to merge 1 commit into
Graphify-Labs:v8from
CJNA:fix/path-endpoint-full-token-match

Conversation

@CJNA

@CJNA CJNA commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Problem

graphify path resolves each endpoint by committing to _score_nodes()[0]. The full-query bonus tier in _score_nodes only fires when the query equals or prefixes a label, so a query that is a token subset of the intended label gets no bonus, and a node prefix-matching a single rare token can out-score it on IDF alone.

Real-world repro (27k-node graph):

$ graphify path "Reject-everything judge" "Drift guard"
warning: source match was ambiguous (top score 82.5292, runner-up 82.5292)
No path found between 'Reject-everything judge' and 'Drift guard'.

The head of the scored list was an exact 3-way tie of disconnected doc-island leaves ("Rejection Summary", "Rejected Alternatives", "Rejected / not surfaced (with reason)" — each matching only the reject token as a prefix), while the intended node "Degenerate Reject-Everything Judge" — which contains every query token — ranked 94th at 1/24th the head score. The path anchored on a degree-1 leaf in another component → false "No path found".

Fix

_pick_scored_endpoint(G, scored, query) in serve.py: scan the score-ordered list and take the first candidate whose label contains every query token; fall back to scored[0] when none does. When the head already full-matches (the common case — including every whole-query-tier hit), the pick is exactly scored[0], so existing resolutions are unchanged.

Wired into both endpoint resolvers:

  • cli.py path command
  • serve.py _tool_shortest_path (MCP)

Also: the close-runner-up "ambiguous" warning is now emitted only when the picked endpoint is the raw score head — when the full-token override chose a different node, the head's margin didn't decide anything and the warning was misleading.

After

$ graphify path "Reject-everything judge" "Drift guard"
Shortest path (3 hops):
  Degenerate Reject-Everything Judge --semantically_similar_to [INFERRED]--> Vacuous-Pass Guard <--references [EXTRACTED]-- Anchored, fail-closed detection ... <--semantically_similar_to [INFERRED]-- Drift guard

Tests

  • test_endpoint_prefers_full_token_match — minimal deterministic graph reproducing the misranking (decoy "Rejection Summary" out-scores the full match via IDF; verified the decoy genuinely wins under current scoring before the fix); asserts the path resolves through the full-match node.
  • test_endpoint_falls_back_to_score_head — query with no full-token candidate behaves byte-identically to the old scored[0] pick.
  • Full tests/test_serve.py + tests/test_path_cli.py: 84 passed. ruff check clean on all touched files.

🤖 Generated with Claude Code

graphify path committed to _score_nodes()[0] for each endpoint. The
full-query bonus tier only fires when the query equals or prefixes a
label, so a query that is a token subset of the intended label (query
"Reject-everything judge" vs. label "Degenerate Reject-Everything
Judge") gets no bonus, and a node prefix-matching one rare token
("Rejection Summary") can out-score it on IDF alone — on a real 27k-node
graph the intended node ranked 94th at 1/24th the head score, behind an
exact 3-way tie of disconnected doc-island leaves, producing a false
"No path found".

Add _pick_scored_endpoint(): scan the score-ordered list and take the
first candidate whose label contains every query token, falling back to
scored[0] when none does (or when the head already full-matches, where
it is a no-op). Wire it into both endpoint resolvers — the path CLI and
the MCP shortest-path tool — and suppress the close-runner-up ambiguity
warning when the picked endpoint is not the raw score head, since the
head's margin didn't decide anything there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
safishamsi pushed a commit that referenced this pull request Jul 11, 2026
…nts (#1785)

`graphify path` committed each endpoint to _score_nodes()[0]. The full-query
bonus tier only fires when the query equals/prefixes a label, so a query that is
a token subset of the intended label ("Reject-everything judge" vs "Degenerate
Reject-Everything Judge") got no bonus and a node prefix-matching one rare token
("Rejection Summary") could out-score it on IDF alone — anchoring the path on an
unrelated, often disconnected node and returning a false "No path found".

_pick_scored_endpoint() scans the score-ordered list and takes the first
candidate whose label contains EVERY query token, falling back to scored[0] when
none does — so when the head already full-matches (the common case) resolution
is unchanged. Wired into both the `path` CLI and the MCP _tool_shortest_path.
The close-runner-up ambiguity warning now fires only when the picked endpoint is
the raw score head (a full-token override was chosen on coverage, not score, so
the head's margin is irrelevant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Collaborator

Merged on v8, authored to you. Verified on a synthetic version of your repro: with distractors prefix-matching only the reject token out-scoring the intended node, path used to anchor on reject filler 0 (score head); _pick_scored_endpoint now correctly picks Degenerate Reject-Everything Judge (the full-token match), and a head-full-match query is unchanged (returns scored[0]). Nice call scoping the ambiguity warning to only fire when the head is what got picked. 80 serve tests + full suite (3156) green. Thanks.

@safishamsi safishamsi closed this Jul 11, 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.

3 participants