Code-graph correctness. impact and reference resolution stop asserting edges
they cannot justify — a closure that returned half the repo for one seed, and a
name-matched resolver that credited std methods to same-named project functions.
Alongside that, the MCP server gains aggregate queries that answer questions about
a whole view rather than about a symbol name the caller already has.
Upgrading: the first index after this release does a one-time full re-walk
of every view (see EXTRACTION_VERSION below) — expected, not a fault. Reinstall
the binary as well as re-indexing: the resolution fixes live in the query path, so
a stale gonzalo-mcp keeps returning the old answers over freshly indexed data.
Fixed
-
A method call no longer claims a same-named free function (#223).
Resolution::UniqueGlobalattributed a reference to the sole definition of
that name in the view — including when the name was really a std or dependency
method. In gonzalo,.chain(ours_obj.keys())ingonzalo-coreresolved to
fn chain(), a test fixture ingonzalo-graph, a crategonzalo-coredoes
not depend on.References now record the shape of the call site (
RefKind::{Free, Method}),
and a cross-file method call resolves to the new
Resolution::ReceiverUnknownrather than guessing. Measured over gonzalo's
own source: of the 388 cross-file method calls that used to resolve
UniqueGlobal, 294 pointed at a different crate —push,filter,
send,bytes,nextand friends. Same-file method calls still resolve
Local, and free and path calls (foo(),a::b::foo()) are unaffected.Effect on
impact(#207), same 122-file source both runs:seed before after build_rust185 126 assemble60 23 resolve_references_to6 6 The 24 provably-false
gonzalo-corenodes in thebuild_rustclosure are now
0. Dropped edges are reported asreceiver_unknown_edges, counted
separately fromambiguous_edgesbecause the cause differs: not "too many
candidates" but "cannot claim any candidate".RefKindis omitted from the serialized slice when free, so a file of plain
calls keeps its existing content hash. -
impactno longer merges unrelated code through shared identifiers (#207).
The closure walked the name-matched caller graph, so one hop into a name with
several definitions absorbed every subgraph sharing that identifier. The walk
now keys nodes on(name, defining path)and consults the resolver for every
edge: anAmbiguousreference is counted and dropped rather than traversed.On the gonzalo view, seeded at
build_rust: 356 → 178 reached names, with
10 ambiguous edges reported rather than followed. Seeds that were already sharp
are unchanged (resolve_references_to: 5 → 5).The result is now a report rather than a name list — every node carries the path
defining it,ambiguous_edgessays how many edges could not be attributed (so a
non-zero count means the true set may be larger), andtruncatedreports a walk
stopped by the new optionalmax_depth. The daemon's HTTP and gRPC transports
keep their existing name-list shape and so get the precision fix without the
report fields.Of the remaining 178, 17 are still provably false and trace to a single
UniqueGlobalover-attribution — std'sIterator::chainresolving to a
same-named test fixture. That is a distinct defect, filed as #223. -
An incremental re-index now prunes paths a laxer run admitted (#209
follow-up). The filter added in #218 only applied to newly walked or changed
files, so an existing view kept its vendored bundles forever: a bundle never
changes, so it never appears in the git diff and was never reconsidered — and
once a base commit is recorded there is no full walk to clean it up. Upgrading
therefore fixed new views only, which is the case least in need of fixing.The carried-forward set is now re-checked against the current rules, including
.gitignore— necessary becausedocs/guide/book/is build output excluded by
ignore rules rather than by any directory-name rule, so a path-only prune left
it behind.Re-indexing the existing
caliban-ai/calibanview: 17 162 symbols → 8 549,
with vendored symbols going 8 618 → 0 and the largest file becoming
caliban/src/tui/events.rs(142) instead of a copy ofmermaid.min.js. -
An unknown
repo/view_idis now an error, not an empty result (#210).
Every graph query returned[]withisError: falsewhen the selector named
no indexed view, so a one-character typo inview_idwas indistinguishable
from a symbol that genuinely is not there — an agent read it as "nothing calls
this" and reported a wrong answer as fact.Service::viewnow fails with
NotFound, and the MCP layer turns that into a tool error naming the
unresolved selector and listing the views that do exist, so a caller can
correct itself in one round trip. A real miss inside a real view still returns
[], so the two cases are finally distinguishable.diffgets the same check on bothview_aandview_b. -
Calls inside Rust macro arguments are now recorded as references (#216).
Macro arguments parse as atoken_treeof raw tokens rather than expressions,
soassert_eq!(f(), 1)contained nocall_expressionand the call tofwas
never seen. Because assertions are where much of a codebase is exercised, this
silently removed a large share of the call graph:callers,callees,
impactandtop by=fan_inall undercounted, andunreferencedreported
live functions as dead.Re-indexing gonzalo itself, with an identical file set (1 852 symbols both
runs), references go from 10 247 to 12 196 — +1 949 edges, +19.0%.
Language::from_extension, the symbol that exposed the bug, goes from 0
recorded references to 28.Detection is token-level: an identifier whose immediate next sibling is a
parenthesised token tree. A nested macro has a!between the two and is
excluded. It is deliberately over- rather than under-inclusive — a tuple-struct
pattern likeSome(_)reads as a call — which matches a graph that already
records constructors and enum variants as calls.The other 17 grammars were audited for the same opaque-node hole. Only C/C++
has one: a#definebody is a single opaquepreproc_argtoken with no child
nodes to read. It is left in place and pinned by a test so the gap is
discoverable rather than silent. -
The indexer no longer walks vendored bundles or gitignored build output
(#209).is_indexableskipped onlytarget,.git, and dotted components, so
half of a real repo's graph was not that repo's code. Membership now lives in
one place (IndexFilter), shared by the full walk and the git-incremental
driver so they cannot disagree: dependency/output directories (node_modules,
vendor,dist,build,site-packages,third_party) and generated files
(*.min.js,*.min.css,*.bundle.js,*-lock.json) are dropped on both
paths, and the full walk additionally honours.gitignore.Re-indexing
caliban-ai/calibandrops it from 16 986 symbols to 8 501 (-50.0%)
with zero symbols frombook/**or any*.min.js; the largest file in the
view is nowcaliban/src/tui/events.rs(142 symbols) rather than a 4 231-symbol
copy ofmermaid.min.js. This also removes a reproducibility hole — indexing
gitignored output made the graph depend on whether anyone had run a build.gonzalo indexnow reports what it excluded (ignored: N files, M dirs not descended), and--include <path>re-admits a vendored path that a built-in
rule would drop.--includedeliberately cannot override.gitignore, so no
flag can make a view irreproducible.
Added
-
EXTRACTION_VERSION, and a full walk when it changes (#223). The
incremental driver carries unchanged slices forward untouched, so a parser
improvement never reached files that did not change — an existing view stayed
permanently half-upgraded.gonzalo indexnow records the extraction format
alongside the view and rebuilds in full when it differs, which is what lets
#216's and #223's parsing changes actually reach an established view. -
A guide chapter for the MCP server (#208) —
docs/guide/src/mcp.md, covering
install → index → register → verify → keep fresh, a tool reference grouped by the
question each tool answers, and the capability boundaries. It leads with the thing
nothing in the repo stated: the server only reads,gonzalo indexwrites, so an
unindexed setup answers every query forever with no indication why. It also records
the traps found while wiring the server up for real —GONZALO_ROOTnot expanding
~(#211),~/.cargo/binmissing from the non-interactive shells an MCP client
spawns, and needing to reconnect the server to pick up a newly installed binary —
plus a troubleshooting table keyed by symptom. -
viewsdiscovery tool and a view count instatus(#210).viewslists
every indexed(repo, view_id)with its file count and the commit it was
indexed at, which makes the server self-describing rather than dependent on
out-of-band documentation; comparingbase_commitagainst the checkout's HEAD
also surfaces a stale view, the quieter form of the same problem.statusnow
reports how many views are indexed, so the natural health-check call actually
detects a server pointed at an empty or wrong store. Therepo/view_id
schema descriptions now say they must match an indexed view and point at
views. -
Aggregate code-graph queries (#214) — three MCP tools that answer questions
about a view rather than about a symbol name the caller already has:
overview(file/symbol/reference counts, a breakdown by kind and language, and
the largest files),top(rank byfan_in,fan_out, ordefinitions— a
definitionsscore above 1 marks an ambiguous name), andlist(enumerate
symbols filtered bypath_prefix,kind, andname_contains). Backed by new
defaultGraphStoremethods, so every store implementation inherits them.
Results are bounded and reporttotal+truncatedrather than silently
cutting. -
unreferenceddead-code candidates (#214) — a fourth aggregate tool
listing symbols with no inbound reference, filtered by the same
path_prefix/kind/name_containsand bounded the same way.exclude_tests
(default on) drops members of amod tests/mod testblock by line range and
anything under atests/directory; on gonzalo itself that is the difference
between 515 hits and 40. Deliberately errs toward silence — a reference from
anywhere counts, including from tests and from the symbol itself. Its false
positives are documented in the tool description, the rustdoc, and a pinned
test: calls inside macro arguments are not recorded at all (assert_eq!(f(), 1)registers nothing), and a function passed as a value is a path expression
rather than a call, so both look uncalled.