audit: prove every permanent-refusal, plugin and generation coverage state #72

Closed
opened 2026-08-18 12:34:17 +02:00 by buildagent · 1 comment
Member

I034 fixed a freshness barrier that read an ABSENT row as proof of exclusion, permanently bricking changed_symbols and review_diff. The fix taught the WALKER stage to prove exclusion.

I040 found two more doors onto the same brick, both live in production, neither filed:

Door Symptom Closed
Walk — skip dirs, hidden, ignore files tracked file under dist/ bricked both diff tools I034
Extension — no plugin / not text-eligible / plugin-EXCLUDED *.Designer.cs reported index_stale: true FOREVER; the barrier waited for a row that can never exist I040
Content — non-UTF-8 code, binary text, undecodable text a tracked Latin-1 .rs denied changed_symbols on EVERY call, burning 1.24 s of bounded wait each time and hiding the fresh data in the same diff I040, after review

The pattern is identical every time: "will this file ever be indexed?" is answered in stages, and a prover was built for one stage while the later stages kept refusing files silently. walker::Coverage models the walk; index::path_eligibility now models the extension gate; index::content_eligibility now models the content gates.

Ask

Audit for any REMAINING stage that can permanently refuse a file, and check each is modelled:

  • the writer — can it reject a parsed file after classify_walked accepted it?
  • parse errors — a file that parses to an error gets a row with parse_error set, so it is not this class, but confirm the barrier treats it as settled rather than pending
  • the size cap re-check between walk and read (index.rs has a TOCTOU guard) — a file that grows past the cap mid-walk
  • text-only files that grow past the cap between walk and read
  • anything platform-specific (Windows sharing violations, EACCES) that is TRANSIENT and must therefore stay pending, not become never — the failure mode in this direction is the opposite one and equally bad

The generalisation worth writing down

A three-state disclosure needs every state audited, not just the alarming one. I040's review found pending — the state nobody was worried about — was a promise ("retry, the watcher will get it") that eight classes of path could never keep. never was carefully proved; pending was assumed safe because it sounded neutral.

Runtime-plugin architecture expansion

#75 adds new refusal and waiting stages, so this audit becomes a prerequisite to #78/#80 rather than a one-time walker review.

Additional doors to model:

  • package requested but not installed;
  • package installed but digest/ABI invalid;
  • capability approval absent;
  • grammar/query/extractor rejected;
  • worker timeout/crash/quarantine;
  • claim conflict/precedence rejection;
  • pending generation build or validation;
  • failed activation with previous generation still active;
  • contribution-level refusal in a mixed-language file;
  • package removal returning a path to text-only/unclaimed;
  • linked projects using different plugin sets.

Each state must answer two independent questions:

  1. Can the active generation answer now?
  2. Can the requested state converge without user action?

pending is valid only for state that can progress automatically. not_installed, approval_required, rejected and quarantined require explicit action and must never carry “retry in a moment.” A failed pending generation must not hide usable results from the previous active generation.

Acceptance now includes a registry-style stage matrix covering walk, extension/claim, content, plugin host, fact validator, generation and writer. Every terminal state has a proving component and stable reason; every transient state has a tested convergence mechanism.

I034 fixed a freshness barrier that read an ABSENT row as proof of exclusion, permanently bricking `changed_symbols` and `review_diff`. The fix taught the WALKER stage to prove exclusion. I040 found **two more doors onto the same brick, both live in production, neither filed:** | Door | Symptom | Closed | |---|---|---| | **Walk** — skip dirs, hidden, ignore files | tracked file under `dist/` bricked both diff tools | I034 | | **Extension** — no plugin / not text-eligible / plugin-EXCLUDED | `*.Designer.cs` reported `index_stale: true` FOREVER; the barrier waited for a row that can never exist | I040 | | **Content** — non-UTF-8 code, binary text, undecodable text | a tracked Latin-1 `.rs` denied `changed_symbols` on EVERY call, burning 1.24 s of bounded wait each time and hiding the fresh data in the same diff | I040, after review | The pattern is identical every time: **"will this file ever be indexed?" is answered in stages, and a prover was built for one stage while the later stages kept refusing files silently.** `walker::Coverage` models the walk; `index::path_eligibility` now models the extension gate; `index::content_eligibility` now models the content gates. ## Ask Audit for any REMAINING stage that can permanently refuse a file, and check each is modelled: - the writer — can it reject a parsed file after `classify_walked` accepted it? - parse errors — a file that parses to an error gets a row with `parse_error` set, so it is not this class, but confirm the barrier treats it as settled rather than pending - the size cap re-check between walk and read (`index.rs` has a TOCTOU guard) — a file that grows past the cap mid-walk - text-only files that grow past the cap between walk and read - anything platform-specific (Windows sharing violations, EACCES) that is TRANSIENT and must therefore stay pending, not become `never` — the failure mode in this direction is the opposite one and equally bad ## The generalisation worth writing down A three-state disclosure needs **every** state audited, not just the alarming one. I040's review found `pending` — the state nobody was worried about — was a *promise* ("retry, the watcher will get it") that eight classes of path could never keep. `never` was carefully proved; `pending` was assumed safe because it sounded neutral. ## Runtime-plugin architecture expansion #75 adds new refusal and waiting stages, so this audit becomes a prerequisite to #78/#80 rather than a one-time walker review. Additional doors to model: - package requested but not installed; - package installed but digest/ABI invalid; - capability approval absent; - grammar/query/extractor rejected; - worker timeout/crash/quarantine; - claim conflict/precedence rejection; - pending generation build or validation; - failed activation with previous generation still active; - contribution-level refusal in a mixed-language file; - package removal returning a path to text-only/unclaimed; - linked projects using different plugin sets. Each state must answer two independent questions: 1. Can the active generation answer now? 2. Can the requested state converge without user action? pending is valid only for state that can progress automatically. not_installed, approval_required, rejected and quarantined require explicit action and must never carry “retry in a moment.” A failed pending generation must not hide usable results from the previous active generation. Acceptance now includes a registry-style stage matrix covering walk, extension/claim, content, plugin host, fact validator, generation and writer. Every terminal state has a proving component and stable reason; every transient state has a tested convergence mechanism.
buildagent changed title from audit: find the remaining "absent ⇒ never" doors — the I034 brick had three to audit: prove every permanent-refusal, plugin and generation coverage state 2026-08-26 13:39:08 +02:00
Author
Member

The stage matrix exists. Closing — and it found a live defect on its first run.

crates/mcp-server/tests/refusal_stage_registry.rs, 14 tests, in the four sibling registries' idiom.

It caught a stale wire contract immediately

Resp::stage's schema doc read "walk" | "classify" | "index" and the source stamps five. Stale for two releases — a client reading the published schema saw three of the five values it could receive. Fixed, and a gate now compares that doc line against the stages the source actually stamps.

Why it cannot go stale

The population is the scan, not a list. Every stage: "…" literal any production source stamps must have a row, and every row claiming to be stamped must be found. Each row carries a prover (machine-checked to be a real item in the crate the row names — bounding_site_registry's known escape), the (verdict, reason) pairs it emits compared against the arms in source order, and a Convergence answering this issue's second question.

Reason families the arms compute (WalkVerdict::code, ineligible_reason_code) are read out of the producer function and compared set-for-set, so a new door cannot arrive with an unregistered code.

The two-question discipline is gated symmetrically: Terminal must contain no progress marker and must name the action; Automatic must have its mechanism exist as an item and be named in the hint. Emission::Unstamped is what keeps the next door from opening quietly — plugin_host, fact_validator and writer reach no per-path verdict today, each records why and where its refusals surface instead, and the gate reddens the day one starts stamping.

Three things this issue and my brief got wrong

  1. My brief's writer claim was wrong. I wrote that write_stat_touch "always writes a row and has no refusal path". It does have one (#80 S56): a touch that would change size is refused. The honest verdict is better than "structurally benign" — it refuses the touch, never the row, and invalidates (mtime_ns = -1, zero hash) so classify_hash reparses. Transient by construction. write_upsert genuinely has zero early returns. Both are now graded structurally.
  2. This issue's stage list is missing one. It names walk / extension-claim / content / plugin host / fact validator / generation / writer. The wire also has index — the database stage, which is where both pending verdicts and the good verdict are stamped, i.e. precisely the promise-shaped states this issue exists to audit.
  3. reason_code_registry never graded index_coverage's verdict reason codes at all. no_row_yet, content_refused, row_present, unclaimed_by_activation appear nowhere in it. This registry is the first thing that does.

The three-state check against live states: empty, and that is a measurement

All eight live outcomes comply. No terminal hint promises progress; both automatic ones name their mechanism. The interesting one is eligibility_unavailable — a pending that is terminal (it needs a daemon restart) and correctly promises nothing.

That is this issue's own lesson holding: "never was carefully proved; pending was assumed safe because it sounded neutral."

The gate's own control was vacuous twice, and running it is what found that

assigned_literal first matched only stage: "…" at the start of a trimmed line. So the "stage": "…" JSON spelling was excluded by the matcher, and then the /// prefix by the anchor — the comment strip graded nothing either time, and the control passed with the strip deleted. Widening to both spellings and to boundary-aware substring closed two real holes in the population: a stage emitted through a one-line json!, and any stage written mid-line.

Nine mutations run with cp snapshots and md5-verified restores, including the inversion (an unregistered new stage → red naming it), #82's shape (appending "Retry in a moment." to a terminal hint → red), a new walk door, the writer growing a refusal, and an anti-vacuity control proving the scan reaches another crate rather than being a server.rs-only gate.

Gates green in an isolated worktree at 01a478b (the shared tree was mid-edit by the #79 lane): 2946 passed / 0 failed, daemon leg 599 / 0, precision_gate 7/7, baseline unmoved.

Split out rather than swept

  • #136 — index_coverage reports "Indexed and current." for a file whose facts the validator refused. FileOverlap carries no parse_error, so the coverage path structurally cannot see it. The barrier is correct (a row exists, so it is settled — this issue's own bullet); the tool over-promises. Needs a wire field plus both skew directions.
  • #137 — a linked project running a different package set is indistinguishable from one running the same. The last of this issue's eleven doors, blocked by seven tokens of project_overview headroom and a live collision with #79.
  • The activation stage's two verdicts are not exercised end-to-end through MCP — graded against source here and at daemon level by reader_epoch_e2e, but an MCP-leg probe needs a two-generation database. Worth filing.
## The stage matrix exists. Closing — and it found a live defect on its first run. `crates/mcp-server/tests/refusal_stage_registry.rs`, 14 tests, in the four sibling registries' idiom. ### It caught a stale wire contract immediately `Resp::stage`'s schema doc read `"walk" | "classify" | "index"` and the source stamps **five**. **Stale for two releases** — a client reading the published schema saw three of the five values it could receive. Fixed, and a gate now compares that doc line against the stages the source actually stamps. ### Why it cannot go stale The population is **the scan, not a list**. Every `stage: "…"` literal any production source stamps must have a row, and every row claiming to be stamped must be found. Each row carries a `prover` (machine-checked to be a real item *in the crate the row names* — `bounding_site_registry`'s known escape), the `(verdict, reason)` pairs it emits compared against the arms **in source order**, and a `Convergence` answering this issue's second question. Reason families the arms *compute* (`WalkVerdict::code`, `ineligible_reason_code`) are read out of the producer function and compared set-for-set, so a new door cannot arrive with an unregistered code. The two-question discipline is gated symmetrically: `Terminal` must contain **no** progress marker and must name the action; `Automatic` must have its mechanism exist as an item **and** be named in the hint. `Emission::Unstamped` is what keeps the next door from opening quietly — `plugin_host`, `fact_validator` and `writer` reach no per-path verdict today, each records why and where its refusals surface instead, and the gate reddens the day one starts stamping. ### Three things this issue and my brief got wrong 1. **My brief's writer claim was wrong.** I wrote that `write_stat_touch` "always writes a row and has no refusal path". It **does** have one (#80 S56): a touch that would change `size` is refused. The honest verdict is better than "structurally benign" — it refuses the *touch*, never the row, and **invalidates** (`mtime_ns = -1`, zero hash) so `classify_hash` reparses. Transient by construction. `write_upsert` genuinely has zero early returns. Both are now graded structurally. 2. **This issue's stage list is missing one.** It names walk / extension-claim / content / plugin host / fact validator / generation / writer. The wire also has **`index`** — the database stage, which is where *both* `pending` verdicts and the good verdict are stamped, i.e. precisely the promise-shaped states this issue exists to audit. 3. **`reason_code_registry` never graded `index_coverage`'s verdict reason codes at all.** `no_row_yet`, `content_refused`, `row_present`, `unclaimed_by_activation` appear nowhere in it. This registry is the first thing that does. ### The three-state check against live states: empty, and that is a measurement All eight live outcomes comply. No terminal hint promises progress; both automatic ones name their mechanism. The interesting one is `eligibility_unavailable` — a `pending` that is **terminal** (it needs a daemon restart) and correctly promises nothing. That is this issue's own lesson holding: *"`never` was carefully proved; `pending` was assumed safe because it sounded neutral."* ### The gate's own control was vacuous twice, and running it is what found that `assigned_literal` first matched only `stage: "…"` at the start of a trimmed line. So the `"stage": "…"` JSON spelling was excluded by the **matcher**, and then the `///` prefix by the **anchor** — the comment strip graded nothing either time, and the control passed with the strip deleted. Widening to both spellings and to boundary-aware substring closed **two real holes in the population**: a stage emitted through a one-line `json!`, and any stage written mid-line. Nine mutations run with `cp` snapshots and md5-verified restores, including the inversion (an unregistered new stage → red naming it), #82's shape (appending "Retry in a moment." to a terminal hint → red), a new walk door, the writer growing a refusal, and an anti-vacuity control proving the scan reaches **another crate** rather than being a `server.rs`-only gate. Gates green in an isolated worktree at `01a478b` (the shared tree was mid-edit by the #79 lane): **2946 passed / 0 failed**, daemon leg **599 / 0**, `precision_gate` 7/7, baseline unmoved. ### Split out rather than swept - **#136** — `index_coverage` reports `"Indexed and current."` for a file whose facts the validator **refused**. `FileOverlap` carries no `parse_error`, so the coverage path structurally cannot see it. The *barrier* is correct (a row exists, so it is settled — this issue's own bullet); the *tool* over-promises. Needs a wire field plus both skew directions. - **#137** — a linked project running a different package set is indistinguishable from one running the same. The last of this issue's eleven doors, blocked by seven tokens of `project_overview` headroom and a live collision with #79. - **The `activation` stage's two verdicts are not exercised end-to-end** through MCP — graded against source here and at daemon level by `reader_epoch_e2e`, but an MCP-leg probe needs a two-generation database. Worth filing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
h-dv/code-index#72
No description provided.