Skip to content

v0.5.0 — code-graph correctness

Latest

Choose a tag to compare

@johnford2002 johnford2002 released this 22 Aug 22:24
c73a647

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::UniqueGlobal attributed 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()) in gonzalo-core resolved to
    fn chain(), a test fixture in gonzalo-graph, a crate gonzalo-core does
    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::ReceiverUnknown rather 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 cratepush, filter,
    send, bytes, next and 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_rust 185 126
    assemble 60 23
    resolve_references_to 6 6

    The 24 provably-false gonzalo-core nodes in the build_rust closure are now
    0. Dropped edges are reported as receiver_unknown_edges, counted
    separately from ambiguous_edges because the cause differs: not "too many
    candidates" but "cannot claim any candidate".

    RefKind is omitted from the serialized slice when free, so a file of plain
    calls keeps its existing content hash.

  • impact no 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: an Ambiguous reference 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_edges says how many edges could not be attributed (so a
    non-zero count means the true set may be larger), and truncated reports a walk
    stopped by the new optional max_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
    UniqueGlobal over-attribution — std's Iterator::chain resolving 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 because docs/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/caliban view: 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 of mermaid.min.js.

  • An unknown repo/view_id is now an error, not an empty result (#210).
    Every graph query returned [] with isError: false when the selector named
    no indexed view, so a one-character typo in view_id was 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::view now 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.

    diff gets the same check on both view_a and view_b.

  • Calls inside Rust macro arguments are now recorded as references (#216).
    Macro arguments parse as a token_tree of raw tokens rather than expressions,
    so assert_eq!(f(), 1) contained no call_expression and the call to f was
    never seen. Because assertions are where much of a codebase is exercised, this
    silently removed a large share of the call graph: callers, callees,
    impact and top by=fan_in all undercounted, and unreferenced reported
    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 like Some(_) 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 #define body is a single opaque preproc_arg token 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_indexable skipped only target, .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/caliban drops it from 16 986 symbols to 8 501 (-50.0%)
    with zero symbols from book/** or any *.min.js; the largest file in the
    view is now caliban/src/tui/events.rs (142 symbols) rather than a 4 231-symbol
    copy of mermaid.min.js. This also removes a reproducibility hole — indexing
    gitignored output made the graph depend on whether anyone had run a build.

    gonzalo index now 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. --include deliberately 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 index now 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 index writes, 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_ROOT not expanding
    ~ (#211), ~/.cargo/bin missing 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.

  • views discovery tool and a view count in status (#210). views lists
    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; comparing base_commit against the checkout's HEAD
    also surfaces a stale view, the quieter form of the same problem. status now
    reports how many views are indexed, so the natural health-check call actually
    detects a server pointed at an empty or wrong store. The repo/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 by fan_in, fan_out, or definitions — a
    definitions score above 1 marks an ambiguous name), and list (enumerate
    symbols filtered by path_prefix, kind, and name_contains). Backed by new
    default GraphStore methods, so every store implementation inherits them.
    Results are bounded and report total + truncated rather than silently
    cutting.

  • unreferenced dead-code candidates (#214) — a fourth aggregate tool
    listing symbols with no inbound reference, filtered by the same
    path_prefix/kind/name_contains and bounded the same way. exclude_tests
    (default on) drops members of a mod tests/mod test block by line range and
    anything under a tests/ 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.