Two correctness defects found in a head-to-head benchmark of 0.9.22.
Caller line numbers: explain/affected/get_neighbors/query printed the caller
node's def line for an incoming call, presented as a precise citation, so
click-through landed in the wrong place. The `calls` edge already carries the
true call-site line (engine.py sets it); every caller/relation listing now reads
the traversed edge's source_file:source_location, falling back to the node's own
line only when the edge lacks one.
Silent query truncation: rendered nodes were degree-ordered (a low-degree
definition node ranked last, cut first), the queried symbol wasn't guaranteed to
appear, and the truncation marker sat only at the end so silence read as absence.
Nodes are now ranked by hop distance from the seeds (deterministic), the seed the
question named is rendered first and never truncated, and a prominent TRUNCATED
notice at the top states shown/total counts and how to widen the budget. Also
rewires the seed-first ordering the renderer already supported — a branch merge
had silently dropped the `seeds=` argument, leaving it dead code.
Projects the verdicts `graphify reflect` already distills (preferred /
tentative / contested, exponential time-decayed) into a derived
experiential layer the read surfaces consume, so accumulated agent
experience actually shows up where you look — without polluting the
structural graph.
Design (grounded in agent-memory + provenance literature; a redesign of
the #1542 approach):
- SIDECAR, not graph.json stamping. `reflect` writes `.graphify_learning.json`
next to graph.json (an additional output, so the git hooks produce it
automatically). graph.json stays purely structural; nothing leaks into
GraphML; no graph.json churn. Mirrors the named-graph / event-sourcing
separation of durable truth from a derived layer.
- Reuses the existing reflect aggregate (its `_decay` is the
recency-weighted exponential model; `_finalize_sources` the
classification) — no new scoring.
- PROVENANCE: each verdict carries the source questions/dates that produced
it (cap 5, most-recent first).
- STALENESS: each verdict stores the node's file fingerprint; on read, a
changed source file flags the verdict stale ("code changed since —
re-verify") rather than presenting a confident lesson on rewritten code.
- CONTESTED surfaced distinctly (useful N / dead-end M), not averaged away.
- DEAD-ENDS stay QUERY-SCOPED — never a node-level status; they appear only
in the report as question -> nodes.
- Read surfaces (explain / query+MCP / GRAPH_REPORT / graph.html) merge the
overlay at read time, sanitized; un-annotated graphs are byte-identical.
Deferred (logged): letting verdicts influence query/seed traversal — the
recommender feedback-loop / Matthew-effect risk means that needs
propensity correction + exploration, not naive biasing.
Builds on the idea in #1441/#1542 (thanks @TPAteeq).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A path query like `explain "app/api/route.ts"` tokenized to terms that matched no
node, so explain/affected returned "No node matching". Source-file paths are now
part of the search index and matched exactly (serve._find_node gains a leading
source-exact tier; affected.resolve_seed gains a source-file match). When several
nodes share a source_file (e.g. a file-level node plus a function node), the lookup
prefers the file-level node — the L1 node whose label basename matches the queried
filename, falling back to the unique L1 or unique basename match, else None.
Ported from PR #1503 by @behavio1. Maintainer fixes on top: aligned trailing-
separator handling between resolve_seed and _find_node (affected previously
returned None for a trailing-slash path that explain resolved), corrected the
stale "three-tier" _find_node docstring, and added regression tests for the
trailing-slash parity and the ambiguous-no-file-node -> None case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>