Covers the exact shape from the issue: a file imports a symbol that a
second file contains, with no direct file to file edge between them.
Confirmed to fail against pre fix code (reports no path found despite
both halves of the route existing).
(cherry picked from commit 6a7a720894)
graphify path and the MCP shortest_path tool now build a digraph from the
true _src/_tgt directions and respect edge direction by default, so a
returned path no longer traverses edges backwards. --undirected (CLI) /
undirected=true (MCP) opt out; no directed path is reported plainly
instead of silently returning a reversed one.
Thanks @luliaz0601.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
graphify path decided hop direction from the persisted source/target order,
so a link stored in flipped order (pre-#563 graphs, raw dumps, merge-driver
output) printed backwards; explain had the same defect, and the query/merge
load shims clobbered in-file _src/_tgt markers. All now honor _src/_tgt (the
build-side direction truth), falling back to arc order for markerless files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`graphify path` (and the MCP shortest_path tool) ran shortest_path over
G.to_undirected(as_view=True), whose neighbor iteration is a hash-seeded set
union, so among equal-length paths BFS returned a route that varied per process.
Build a sorted, materialized undirected graph so the chosen path is canonical.
The hop label also printed a relation read from an arbitrarily-collapsed parallel
edge, so it could show `calls` on a pair that only carries `references`. Force
multigraph on the cli path reload so parallel links survive, and render the
ACTUAL stored relation(s) via edge_datas, falling back to an honest "related"
when the edge has none. Serve's shared graph is left untouched (its degree feeds
query-seed tie-breaks); the fix is applied locally in both path readers.
`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>