fix(witan-code): report an unreadable code graph instead of rendering it as empty - #291
fix(witan-code): report an unreadable code graph instead of rendering it as empty#291blarghmatey wants to merge 5 commits into
Conversation
… it as empty Every local code store on this machine — 53 per-repo graphs and the shared bridge — has been unreadable since omnigraph moved past internal-schema v4, the bridge for six weeks. Nothing said so. The failure reached a user only as an identical Rust backtrace per tool call, `code_indexed_repos` listed all 54 repos happily because `files: null` renders the same as a small index, and the UserPromptSubmit block asserted "53 other repos indexed, so cross-repo `code_interface_*` resolve" on every single prompt while none of those queries could run. An agent that believes that line concludes a contract has no consumers when the truth is that the query failed. Three gaps, closed: - The client never set `_STORAGE_MISMATCH_HINT`, which witan-core has always supported and witan has always set, so a stale on-disk format re-raised omnigraph's raw error — ANSI escapes, `Location:`, backtrace boilerplate — and advised an export path needing the binary the upgrade replaced. The hint is set now, and says to reindex: a code graph is derived from a checkout that is still on disk, so it is rebuilt rather than migrated (tk-rebuild-derived-graphs-by-reindexing-not-by-expo-3b781b). - No readiness check covered the bridge. It belongs to no repo, so it is in no repo listing, yet every `code_interface_*` / `code_cross_repo_impact` tool reads it and nothing else does — which is exactly how it stayed broken while every listing looked healthy. `witan-code doctor` / `code_store_health` probe it alongside the per-repo stores; `code_indexed_repos` now carries the reason a store could not be read, so "unreadable" stops rendering as "empty". - No recovery path. `witan-code reindex --rebuild` deletes the stores it could not read — this repo's, and the bridge if it is unreadable too — and indexes from scratch. Only the failing ones: rebuilding a healthy bridge would take every other repo's cross-repo bindings with it. Nothing is kept aside, unlike witan's `migrate storage`: a memory graph holds the only copy of what it knows, a code graph holds a re-derivable one, and a copy no installed binary can open is just disk (27 GB for one store here). The deployed cluster is unaffected — checked live: 14 repos with real file counts, bridge resolving 11 cross-repo links. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162A7PPanL5ZWKd9BAJmAhn
…ally has The readiness probe asked every store `code_read.gq/count_files`, which matches `CodeFile` — a node the bridge schema does not define. So a perfectly healthy bridge answered with a schema error and was reported unreadable. Not cosmetic, because `reindex --rebuild` keys off the same verdict. Caught mid-sweep doing exactly what that implies: rebuilding the bridge for one repo, then deleting it again on the next repo, and the next, so each rebuilt repo's cross-repo bindings were the only ones the bridge ever held. Nine repos in before the pattern showed up in the log. `bridge.gq/count_bindings` is its counterpart — same shape, same count-in-the-engine, same positional read. `store_health` takes an explicit `bridge=` rather than sniffing the ref, and records it on the result so a caller labelling the bridge in a listing does not compare path spellings. The regression test asserts both directions against a real indexed bridge: the bridge query answers, and the per-repo query does not — otherwise it would pass against the code it exists to catch. Also fixes the bridge verdict's cache, which cached nothing: `witan-code inject-context` is a fresh process per prompt, so a module-level memo was born and died inside one hook run and the block cost a second store query on every prompt. It is a TMPDIR sidecar now, keyed by a digest of the code dir. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162A7PPanL5ZWKd9BAJmAhn
The skill's Coverage table answered "which repos are indexed" but had no question for "can those stores be read", which is the one that matters when code_interface_* comes back empty: those four tools read one shared bridge graph that appears in no repo listing, so a dead bridge and a genuinely unused contract are the same observation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162A7PPanL5ZWKd9BAJmAhn
…mptied `reindex src/ --rebuild` deleted the whole store and then indexed only `src/`, leaving the rest of the repo unindexed — a worse state than the unreadable store it started from, and one nothing afterwards reports, since a partially indexed store answers every query without complaint. Refuses a path that is not the working-tree root rather than quietly widening the path the caller asked for. `repo.git_toplevel` asks git rather than walking up to a `.git` directory: in a linked worktree `.git` is a file, so the walk finds the wrong root or none, and the guard would silently not apply in exactly the checkout layout this repo is developed in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162A7PPanL5ZWKd9BAJmAhn
There was a problem hiding this comment.
Pull request overview
Adds detection, reporting, and recovery for unreadable witan-code graphs, including the shared bridge.
Changes:
- Adds store health probes,
doctor, and clearer readiness reporting. - Adds guarded
reindex --rebuildrecovery. - Updates tests, documentation, and versioning to 0.16.0.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Updates the package version. |
witan_code/store.py |
Adds health reporting and store deletion. |
witan_code/skills/witan-code/SKILL.md |
Documents health checks. |
witan_code/server.py |
Exposes health data through MCP tools. |
witan_code/repo.py |
Adds Git repository-root detection. |
witan_code/queries/bridge.gq |
Adds the bridge health query. |
witan_code/graph.py |
Adds stale-schema classification and remediation. |
witan_code/context.py |
Reports unreadable graphs in prompt context. |
witan_code/cli.py |
Adds doctor and rebuild recovery. |
tests/test_store.py |
Tests health probes and deletion. |
tests/test_graph.py |
Tests stale-schema diagnostics. |
tests/test_context.py |
Tests prompt health reporting and caching. |
tests/test_cli_dispatch.py |
Tests rebuild path validation. |
README.md |
Documents diagnostics and recovery. |
pyproject.toml |
Bumps witan-code to 0.16.0. |
CHANGELOG.md |
Records the 0.16.0 release. |
docs/reference/mcp-tools/index.md |
Updates MCP tool counts. |
docs/reference/mcp-tools/code.md |
Documents the health tool. |
docs/reference/cli.md |
Documents CLI health and rebuild options. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
All three were real, and two of them defeated the very guarantees this PR
adds.
THE GUARD DID NOT COVER THE ARGUMENT IT WAS WRITTEN FOR. `index`/`reindex`
accept a file path, and `git -C <file>` exits 128 ("Not a directory"), which
`git_toplevel` reads as "no repo" and the root guard then skips entirely. So
`reindex some_file.py --rebuild` deleted the whole store and refilled it with
one file — the exact outcome the guard exists to prevent, reachable by the
argument shape most likely to be typed by accident. Resolved from the parent
when the path is a file.
A FAILED DELETE REPORTED SUCCESS. `shutil.rmtree(..., ignore_errors=True)`
left an unreadable store in place on a permissions or filesystem error, took
the sidecars anyway, and returned a byte count the CLI printed as
"Deleted … (N freed)" before reindexing into the same store that could not be
opened. It raises now; the sidecars surviving next to the store they describe
is the correct state to stop in.
`code_store_health` COULD ANSWER `ok: true` WITH A DEAD BRIDGE. `StoreRef.exists`
degrades every failed remote probe to `False` on purpose, so gating the bridge
row on it dropped an unreachable cluster bridge out of the report entirely and
`all(h.ok …)` came back true while every `code_interface_*` tool was failing —
precisely the condition this check was added to catch. A remote bridge is now
always probed so `store_health` keeps the error; a missing LOCAL bridge stays
absence rather than failure, since a repo with no contracts yet has none.
The prompt hook's own bridge probe had the same defect and is fixed with it.
Copilot flagged only the `health_report` instance; the same three lines in
`context._bridge_ok` would have gone back to claiming cross-repo resolution
works against an unreachable deployment.
Each fix has a test that fails without it — verified by reverting them one at
a time. The rmtree test needed its stub to honour `ignore_errors` to be able
to tell the two spellings apart at all; the first version passed against the
broken code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162A7PPanL5ZWKd9BAJmAhn
|
Addressed all three Copilot findings in ace9fe0 — all real, and two of them defeated guarantees this PR adds:
Also fixed the identical gate in Each fix has a test that fails without it, verified by reverting them one at a time. The |
What are the relevant tickets?
N/A on GitHub. Tracked in witan as
tk-the-local-cross-repo-bridge-store-is-unreadable--1147bf.Description (What does it do?)
All but one of the local code stores on my machine — 53 of 54 per-repo graphs, plus the shared bridge — had been unreadable since omnigraph moved past internal-schema v4, the bridge for six weeks. Nothing reported it. The failure reached a user only as an identical Rust backtrace per tool call,
code_indexed_reposlisted all 54 repos happily becausefiles: nullrenders the same as a small index, and theUserPromptSubmitblock asserted "53 other repos indexed, so cross-repocode_interface_*resolve" on every single prompt while none of those queries could run.Three gaps:
OmnigraphClientnever set_STORAGE_MISMATCH_HINT— machinerywitan_corehas always supported andwitanhas always set — so a stale on-disk format re-raised omnigraph's raw error, ANSI escapes and backtrace included, advising an export path that needs the binary the upgrade replaced. The hint is set now and points at reindexing: a code graph is derived from a checkout still on disk, so it is rebuilt rather than migrated.witan-code doctorand thecode_store_healthtool probe every per-repo store and the bridge. The bridge is the whole reason this needed to exist: it belongs to no repo, so it appears in no repo listing, yetcode_interface_search/_providers/_consumers/code_cross_repo_impactall read it and nothing else does.code_indexed_reposnow carries anunreadablereason alongsidefiles, so "cannot be opened" stops rendering identically to "empty".witan-code reindex --rebuilddeletes the stores it could not read — this repo's, plus the bridge if it is also unreadable — and indexes from scratch. Only the failing ones: rebuilding a healthy bridge would take every other repo's bindings with it. It refuses a path that is not the repo root, since it deletes the whole store and would otherwise refill only a subdirectory.Nothing is kept aside, deliberately unlike
witan migrate storage. A memory graph holds the only copy of what it knows; a code graph holds a re-derivation of a checkout that is still on disk, and a copy no installed binary can open is just disk — 27 GB for one store here.How can this be tested?
The 4 failures (
test_branches.py×3,test_graph.py×1) are pre-existing onmain— they assume a logged-out actor. Tracked astk-four-witan-code-tests-fail-on-any-machine-with-a-298214; verified failing on a cleanmaincheckout before starting.End-to-end, against real stores rather than fixtures:
witan-code doctoron a healthy code dir — table of stores, "Every code graph reads.", exit 0..omniwritten by omnigraph 0.8.x) into a scratchWITAN_CODE_DIRand re-run — the store is classifiedstale schema, one error body is printed rather than one per store, exit 1.witan-code reindex <repo-root> --rebuild --yes— deletes and reindexes, thendoctorreports it healthy. Re-running--rebuildon the now-healthy store prints "Nothing to rebuild" and does not delete anything.I also ran the real recovery: 34 stores rebuilt on this machine.
code_repo_dependenciesnow returns 20 repos / 11 edges andcode_interface_search("BASE_URL")answers — both errored outright before.Additional Context
A bug I shipped and caught mid-sweep, worth a look during review. The readiness probe first asked every store
code_read.gq/count_files, which matchesCodeFile— a node the bridge schema does not define. A healthy bridge therefore read as broken, and since--rebuildkeys off that verdict, the rebuild sweep was deleting each freshly-rebuilt bridge on the very next repo. Fixed in c1b27c6 withbridge.gq/count_bindings; the regression test asserts both directions against a real indexed bridge, so it cannot pass against the broken code.The deployed cluster is unaffected — checked live before assuming: 14 repos with real file counts, bridge resolving 11 cross-repo links. This was local-only.
Two things this does not fix, both needing a decision rather than code:
scratchpad.omni, whose failure is a missing manifest rather than a stale schema. Three of the 19 are pre-normalization case-variant duplicates (BoundlessNotions/*alongsideboundlessnotions/*).doctorstays red on them until they are deleted or their repos are cloned and indexed: it reported 54 unreadable before the sweep and 20 after, and that 20 does not go down on its own.mainview reads 0 files. Correct branch-indexing behaviour, but it meansmaincoverage for those repos stays empty until someone indexes them there.witan-code bumped 0.15.0 → 0.16.0. Docs regenerated (
just docs-checkclean), README and the/witan-codeskill updated.