Skip to content

fix(witan-code): report an unreadable code graph instead of rendering it as empty - #291

Open
blarghmatey wants to merge 5 commits into
mainfrom
witan-code-stale-store-schema
Open

fix(witan-code): report an unreadable code graph instead of rendering it as empty#291
blarghmatey wants to merge 5 commits into
mainfrom
witan-code-stale-store-schema

Conversation

@blarghmatey

Copy link
Copy Markdown
Member

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_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.

Three gaps:

  • No remediation on the error. witan-code's OmnigraphClient never set _STORAGE_MISMATCH_HINT — machinery witan_core has always supported and witan has 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.
  • No readiness check covering the bridge. witan-code doctor and the code_store_health tool 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, yet code_interface_search / _providers / _consumers / code_cross_repo_impact all read it and nothing else does. code_indexed_repos now carries an unreadable reason alongside files, so "cannot be opened" stops rendering identically to "empty".
  • No recovery path. witan-code reindex --rebuild deletes 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?

just test-witan-code    # 538 passed

The 4 failures (test_branches.py ×3, test_graph.py ×1) are pre-existing on main — they assume a logged-out actor. Tracked as tk-four-witan-code-tests-fail-on-any-machine-with-a-298214; verified failing on a clean main checkout before starting.

End-to-end, against real stores rather than fixtures:

  1. witan-code doctor on a healthy code dir — table of stores, "Every code graph reads.", exit 0.
  2. Plant a v4-era store (any .omni written by omnigraph 0.8.x) into a scratch WITAN_CODE_DIR and re-run — the store is classified stale schema, one error body is printed rather than one per store, exit 1.
  3. witan-code reindex <repo-root> --rebuild --yes — deletes and reindexes, then doctor reports it healthy. Re-running --rebuild on 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_dependencies now returns 20 repos / 11 edges and code_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 matches CodeFile — a node the bridge schema does not define. A healthy bridge therefore read as broken, and since --rebuild keys off that verdict, the rebuild sweep was deleting each freshly-rebuilt bridge on the very next repo. Fixed in c1b27c6 with bridge.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:

  • 20 unreadable stores have no local checkout, so they cannot be rebuilt: 19 stale-schema, plus scratchpad.omni, whose failure is a missing manifest rather than a stale schema. Three of the 19 are pre-normalization case-variant duplicates (BoundlessNotions/* alongside boundlessnotions/*). doctor stays 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.
  • A repo checked out on a feature branch indexes onto its branch view, so its main view reads 0 files. Correct branch-indexing behaviour, but it means main coverage for those repos stays empty until someone indexes them there.

witan-code bumped 0.15.0 → 0.16.0. Docs regenerated (just docs-check clean), README and the /witan-code skill updated.

blarghmatey and others added 4 commits August 25, 2026 16:32
… 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
Copilot AI balanced review requested due to automatic review settings August 26, 2026 11:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --rebuild recovery.
  • 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.

Comment thread mcp/servers/witan-code/witan_code/cli.py Outdated
Comment thread mcp/servers/witan-code/witan_code/store.py Outdated
Comment thread mcp/servers/witan-code/witan_code/store.py Outdated
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
@blarghmatey

Copy link
Copy Markdown
Member Author

Addressed all three Copilot findings in ace9fe0 — all real, and two of them defeated guarantees this PR adds:

  1. The root guard did not cover a file path. git -C <file> exits 128, which read as "no repo" and skipped the guard, so reindex some_file.py --rebuild deleted the whole store and refilled it with one file — the exact outcome the guard was written to prevent.
  2. rmtree(ignore_errors=True) reported a deletion that had not happened, then reindexed into the same unreadable store.
  3. code_store_health could answer ok: true with a dead remote bridge, because StoreRef.exists degrades a failed remote probe to False and the bridge row was gated on it. That is the precise condition this readiness check was added to detect.

Also fixed the identical gate in context._bridge_ok, which the review did not flag — the prompt hook would otherwise 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 before it could tell the two spellings apart; the first version passed against the broken code. Suite: 542 passed, plus the 4 pre-existing failures tracked in tk-four-witan-code-tests-fail-on-any-machine-with-a-298214.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants