Brigade wanted to attach a structural delta to its run receipts: not “the tests passed” but “this run added these symbols and rewired these call edges.” GraphTrail had the graph and no way to compare two versions of it. 0.3.0 is that comparison, plus the freshness work that makes the graph worth comparing.
Diff two graphs
graphtrail diff --before <db> --after <db> --json compares two indexed databases into added, removed, and changed symbols plus added and removed call edges, opening both read-only. Node identity is (file_path, qualified_name, kind) with line numbers deliberately excluded, so a function that only moves down the file is not a spurious remove-and-add. The same comparison is an MCP tool, and the JSON is compact on purpose: it is built to ride inside a receipt.
The JSON shape is locked by a golden fixture that asserts the full serialized output, and the fixture earned its keep the same afternoon it merged. A downstream consumer shipped code reading a counts key that never existed (the block is named summary), and the golden test turned what would have been a silent forever-zero into a 30-second grep.
A body edit can no longer hide
The first diff had a documented blind spot: a body edit that kept the same signature and the same line span reported nothing. return 1 becoming return 2 was invisible. Schema v3 adds a per-symbol body hash, sha256 of the exact indexed span, and folds it into the changed-node check. That same probe now reports changed_nodes=1, and changed nodes carry a previous block with the before-side signature and start line.
Line shifts got the mirror treatment. Call-edge identity includes the call line for provenance, so inserting one line above an untouched call reads as one removed edge plus one added edge in the raw counts. The summary now carries added_edges_line_insensitive and removed_edges_line_insensitive alongside the raw pair, and a pure line shift cancels to zero there. Both numbers are true. They answer different questions.
Existing v2 databases upgrade in place: the next sync adds the column and runs one full re-extract pass. On an 11 MB production database that was 5,126 symbols hashed in a single sync, and a fleet script migrated every live index in one run.
The index cleans itself
Sync now respects .gitignore in git repositories, nested files and .git/info/exclude included. The motivating case was a repo whose in-tree Python venv put 2,803 site-packages files into a 2,955-file graph, which got it dropped from the sync fleet entirely. Post-change: 152 files, zero noise, and the repo is back on the 15-minute timer. Files that become ignored read as deleted on the next sync, so already-polluted databases fix themselves without anyone asking. Non-git roots keep the hardcoded skip list, which now also covers bare venv, and the first index of a git repo writes .graphtrail/ into .gitignore itself.
The skip check knows about the extractor
Incremental sync used to skip a file when its content hash matched, which is correct right up until the extractor changes and the skip check starts serving stale rows. The v3 migration needed a hand-rolled full-reindex for exactly this reason. Schema v4 retires that class of hack: every language extractor declares a version fingerprint, the fingerprint is recorded per file, and the skip check compares both. Bump the Rust extractor’s fingerprint and only .rs files re-extract, lazily, on the next pass. The bump rule is one line in AGENTS.md, next to the constants.
refresh and doctor
MCP query tools take an opt-in refresh: true that runs the incremental sync before answering, fail-open, with the query itself staying on a read-only connection. And graphtrail doctor answers the question stats never did: is this index trustworthy right now. It reports schema and tool versions, last-sync age, four distinct pending counts (new, changed, deleted, fingerprint-stale), and ignored-entry totals, then verdicts FRESH, STALE, or NEEDS-MIGRATION with exit codes 0, 1, and 2 so a hook can gate on it. Doctor deliberately has no refresh: it diagnoses, the query tools treat.
First field test of the verdict logic: a probe file dropped into a synced repo failed to flip STALE, which looked like a bug for about a minute. The probe was a .md file, and GraphTrail only indexes code. The tool was right and the tester was wrong, which is roughly what you want from a command named doctor.
The release runs 111 tests, up from 84 at 0.2.0.