v1.108.168 — a rebuild underneath a scan cannot prove absence (5th refusal rule)
A rebuild underneath a scan cannot prove absence
v1.108.166 shipped four refusal rules for absence evidence: only absent proves
absence; low_confidence/degraded do not; a stale index does not; a truncated
index does not. None of them covered an index being rewritten while the scan
reads it.
The hole was structural. channels.index is fed by a git-SHA freshness probe —
stored HEAD vs live HEAD — which is blind to a reindex of an unchanged tree.
A watcher rebuild after an uncommitted edit, or a stalled index run, reported
index: "fresh", reached state absent, and note_absence handed back a
citable absent:<sha> ref.
In other words: the contract whose entire purpose is to make "we searched the
complete index and it is not there" auditable could attest that claim over a
half-written index.
The fix
Zero results plus a detected rewrite now yields degraded instead of absent.
Because degraded already cannot prove absence, the fifth rule falls out of the
existing "only absent proves absence" check — there is no parallel rule to
drift out of sync. The refusal names the rebuild rather than the generic state.
channels.index gains "rebuilding", disclosed on every state, not only
the refused one: a caller reading an ok result still deserves to know the index
moved under it. Only the absence claim is withheld — a scan that returned
results still returns them, because those symbols really were in the index.
Why a filesystem signal
Detection deliberately does not use in-process reindex state. That state is
per-process, so a server answering a search cannot see a reindex driven by a
separate watch-all service — the common deployment. Routing the guard through
it would have produced a safety rule that reads as enforced and silently is not.
Instead the index is stamped with its .db path and mtime at load, and the
verdict re-stats it. The .db/.db-wal mtime crosses process boundaries, and
the helper already existed for cache invalidation. Unknown is never treated as
changed, and the cost is two stat calls confined to the zero-result path.
Byte-identical for existing callers when nothing is rebuilding. No new tool, no
tool-count or INDEX_VERSION change. Suite parity shipping to jdocmunch-mcp and
jdatamunch-mcp alongside this.