From c67b33cdcd14d313f580ca18c3e499de5ec4a158 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 5 Sep 2026 14:09:22 +0200 Subject: [PATCH] =?UTF-8?q?docs(901):=20a=20mutation=20OUTCOME=20is=20a=20?= =?UTF-8?q?manifest=20entry=20or=20it=20is=20not=20written=20=E2=80=94=20s?= =?UTF-8?q?tate=20the=20selector=20instead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round three, blocking. The record's `mechanics:` field asserted a measured mutation outcome — that appending an `eval` which composes `ETV_HOOK_FIRE_LIB` at runtime leaves `instrumentation_faults` returning `[]`, identical to the unmutated baseline — with no `CLAIMS` entry in scripts/tests/mutation_manifest.py. `testing.mutation-claims-are-executed` as amended by #881 puts a prose claim about a mutation's outcome under the executed-claim rule wherever it is written, a decision record included, and this branch's own docs/README.md row restates that. A record whose rule text requires "each defeat the matcher claims to catch is a DECLARED, executed mutation" cannot itself carry an undeclared one. scripts/tests/ is held by #885, which is open, so the entry cannot be added here. What replaces the outcome claim is the structural fact that carries the same point and needs no execution: the compared set is the lines containing the literal `ETV_HOOK_FIRE_LIB` that are not comment-led, so the pin reaches exactly the two preamble lines and a later reassignment which never spells the literal is outside the selection — whatever the checker then returns. The `CLAIMS` entry is owed once scripts/tests/ is free. Three more from the same round: - `_normalise_lines` was a universal over three pins that holds for one. The stage commands compare through `_normalise` (continuations joined, whitespace within one command collapsed); the script map is dict equality over parsed JSON and compares no text; only PINNED_VITE_CONFIG uses `_normalise_lines`. The three are now stated separately, with the note that a residual read off one does not transfer. - docs/guard-inventory.md's pointer restated the "shell or config TEXT" framing the record exists to reject. It now says what the record says: an artifact with a grammar the predicate does not implement. - Rider 1 stated vite's DEFAULT_CONFIG_FILES ordering unbound to a version. The ordering belongs to a release and expires with one, so the record cites the guard-inventory row rather than keeping a second copy, and that row now dates the reading and names the release web/package-lock.json pins. Refs #901, #891, #887, #881, #885 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV --- .../guard-pins-the-artifact-not-a-shape.md | 29 ++++++++++--------- docs/guard-inventory.md | 7 +++-- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/docs/decisions/records/testing/guard-pins-the-artifact-not-a-shape.md b/docs/decisions/records/testing/guard-pins-the-artifact-not-a-shape.md index 563bb1285..ec9b79df1 100644 --- a/docs/decisions/records/testing/guard-pins-the-artifact-not-a-shape.md +++ b/docs/decisions/records/testing/guard-pins-the-artifact-not-a-shape.md @@ -7,7 +7,7 @@ supersedes: none superseded-by: none rule: 'A predicate that decides something by matching a SHAPE inside an artifact whose GRAMMAR it does not implement pins the artifact WHOLE instead — the whole file, or a whole named span, compared for equality under a STATED normalisation. The boundary is the grammar and never the file type: shell source, a workflow `run:` body, TOML, YAML, a jq expression and JSON5 all qualify, and a shape matched over the OUTPUT of a real parser for that grammar does not. Extracting a `run:` value with a real YAML parser and then matching shell inside it IS in the class, because the shell is the unparsed half. THE PIN IS THE DEFAULT FOR A NEW PREDICATE; REJECTED is the alternative of writing the shape-matcher and pinning only once it has been defeated. The defeat a remedy policy waits for is a false GREEN, so it is found by a reviewer or by an incident and never by the guard: "not defeated yet" is a statement about who has looked, not about the input space. A SHAPE-MATCHER IS THE EXCEPTION, and its argument carries FOUR things, written where the predicate is and re-read whenever a clause of it is widened. (1) The GRAMMAR named, and the parser that implements it — if the shape is matched over a real parse there is no exception to write. (2) The INPUT SPACE as a CLOSED enumeration with the reason it is closed: a generator whose output form is fixed, or a canonicaliser the guard itself runs. A list of the spellings the author thought of is not that reason, and is the thing that was defeated nine, seven and five times. (3) The FAILURE DIRECTION measured rather than asserted: each defeat the matcher claims to catch is a DECLARED, executed mutation under `testing.mutation-claims-are-executed`, because a shape-matcher''s miss is a false green and an unrun claim about a fail direction is not evidence. (4) WHAT IT BUYS over the pin, priced in a cost the pin actually charges — the artifact is edited routinely or written by tooling, so a pin would redden as background noise and train its readers to ignore it; or the artifact''s population is DISCOVERED rather than fixed, so there is nothing to pin. Fewer than four is a refusal, and neither elegance nor an unwillingness to update a constant is one of the four. TWO OTHER BARS ARE REJECTED for this exception: a NUMERIC one ("survives N adversarial spellings"), which measures the reviewer''s imagination rather than the input space and would have licensed the parser above through most of nine rounds; and a REVIEWER SIGN-OFF, which makes the exception depend on the very signal the threshold below says arrives late. THE THRESHOLD for withdrawing a shape-matcher already shipped is not a count of spellings: it is the moment the NEXT spelling is found by the REVIEWER rather than by the author, which is evidence that the author''s model of the input space is not the input space. TWO RIDERS, both measured. A pin assumes it is pinning the artifact that still DECIDES — ask what else could decide this and whether the pin would still match: another FILE that outranks it, another OCCURRENCE in the same file, another WORKFLOW, or a HOOK the pinned command invokes. And WIDENING A CLAUSE RETIRES THE PROOFS CALIBRATED AGAINST THE NARROW ONE: a canary asserting that a finer mutation still SURVIVES has as its precondition that the guard is deliberately coarse there, so widening the main clause turns the canary into a tautology, which reads exactly like a passing proof — re-derive every proof calibrated against the narrow clause in the same commit that widens it. THE COST IS STATED RATHER THAN DISCOVERED LATER: any edit to a pinned artifact reddens, and the pin is updated deliberately in the same commit, with a reason. That red IS the review trigger the pin exists to be. A pin also declares its NORMALISATION and what the normalisation cannot see, because "pinned whole" invites a reader to assume byte equality.' signals: 'pin the artifact whole · shape-matcher is the exception that argues for itself · the boundary is the grammar not the file type · predicate does not implement the grammar it parses · defeated by the next spelling · the reviewer found the next spelling · false RED versus false GREEN · nine defects one mechanism · seven respellings of a partial config match · five spellings of a lexical preamble rule · the four-part exception argument: grammar and parser, closed input space, measured fail direction, what it buys · authority moves where the pin is not looking · an outranking config file · a decoy occurrence · an npm lifecycle hook · widening a clause retires its own canary · a survived-clause canary becomes a tautology · a pin declares its normalisation · instances: `ci.image-build-delegates-the-spa-suite`, `process.hook-resolves-inputs-from-repo-root` · paths: `scripts/tests/test_image_build_delegates_the_spa_suite.py`, `scripts/tests/test_hook_fire_log.py`, `docs/guard-inventory.md`, `docs/defect-shapes-773.md` · issues: #901, #887, #891, #774, #790, #881, #773' -mechanics: 'Two worked pins. `scripts/tests/test_image_build_delegates_the_spa_suite.py` pins command TEXT (`PINNED_STAGE_COMMANDS`), `web/package.json`''s script map (`PINNED_PACKAGE_SCRIPTS`) and `web/vite.config.ts` entire (`PINNED_VITE_CONFIG`), the last two after a SELECTION and then a partial match were defeated; its comparison runs through `_normalise_lines`, which drops blank lines and collapses whitespace within a line, and the test states that residual rather than claiming byte equality. `scripts/tests/test_hook_fire_log.py` pins the hook preamble''s two lines byte for byte (`CANONICAL_SINK_ASSIGNMENT`, `CANONICAL_SINK_SOURCE`), named once so the checker, the mutation proofs and the hooks cannot come to mean different strings — but its normalisation is a SELECTION, and the rule above obliges this record to say what that selection cannot see, because the pin itself does not. The comparison runs over the lines that contain the literal `ETV_HOOK_FIRE_LIB` and are not comment-led, so a writer of that variable which never spells the literal never enters the compared set at all. MEASURED 2026-09-05 against `.claude/hooks/decisions-guard.sh`: with `eval "$(printf %s%s=/dev/null ETV_HOOK_FIRE _LIB)"` appended after the canonical assignment, `instrumentation_faults` returns `[]`, identical to the unmutated baseline, while those two lines run under bash leave the sink pointing at /dev/null. Byte-identity WITHIN a selection is not byte-identity over the file — the pin''s reach ends where its selector does. The residual is #891 code and predates this record; stating it is this record''s own rule applied to one of its own worked pins. Each of the three image-build pins faults with a request for the reason in the same commit, and two of them name the CONSTANT to update (`PINNED_STAGE_COMMANDS`, `PINNED_VITE_CONFIG`) while the script-map fault names the FILE and prints both maps. The hook-preamble fault asks for neither: it reports the divergent lines, since the two canonical strings ARE what a deliberate change edits.' +mechanics: 'Two worked pins. `scripts/tests/test_image_build_delegates_the_spa_suite.py` pins command TEXT (`PINNED_STAGE_COMMANDS`), `web/package.json`''s script map (`PINNED_PACKAGE_SCRIPTS`) and `web/vite.config.ts` entire (`PINNED_VITE_CONFIG`), the last two after a SELECTION and then a partial match were defeated. The three normalise DIFFERENTLY, so a residual read off one of them does not transfer to the others: the stage commands are compared after line continuations are joined and whitespace within each command is collapsed (`_normalise`), so a reflow is free and a line boundary is not; the script map is dict equality over parsed JSON, so the file''s own formatting and key ORDER are free while every script body is exact; and of the three only `PINNED_VITE_CONFIG` runs through `_normalise_lines`, which drops blank lines and collapses whitespace within a line — including inside a string literal, the residual that test states rather than claiming byte equality. `scripts/tests/test_hook_fire_log.py` pins the hook preamble''s two lines byte for byte (`CANONICAL_SINK_ASSIGNMENT`, `CANONICAL_SINK_SOURCE`), named once so the checker, the mutation proofs and the hooks cannot come to mean different strings — but its normalisation is a SELECTION, and the rule above obliges this record to say what that selection cannot see, because the pin itself does not. The compared set is the lines that contain the literal `ETV_HOOK_FIRE_LIB` and are not comment-led, so the pin reaches exactly the two preamble lines and nothing else in the file: a later reassignment of that variable which never spells the literal — one composed at runtime and `eval`ed, say — is outside the selection, and therefore outside the pin, however the checker then behaves. Byte-identity WITHIN a selection is not byte-identity over the file — the pin''s reach ends where its selector does. That is stated here as a structural fact about the selector and NOT as a measured checker outcome, because an outcome claim about a mutation is a declared, harness-executed `CLAIMS` entry under `testing.mutation-claims-are-executed` or it is not written, wherever it is written — this record included. The residual is #891 code and predates this record; stating it is this record''s own rule applied to one of its own worked pins. Each of the three image-build pins faults with a request for the reason in the same commit, and two of them name the CONSTANT to update (`PINNED_STAGE_COMMANDS`, `PINNED_VITE_CONFIG`) while the script-map fault names the FILE and prints both maps. The hook-preamble fault asks for neither: it reports the divergent lines, since the two canonical strings ARE what a deliberate change edits.' --- **The class, and what approximating it costs.** A predicate over an artifact with a grammar is a @@ -40,14 +40,15 @@ requirement 2, the only candidate that is a claim about the ARTIFACT rather than spent on it, and requirement 3, which forces the fail-direction claim to be executed, not reasoned. **Rider 1 — a pin assumes it pins the artifact that still DECIDES.** This is where the pinned form -fails, and it fails silently, so it is the question to ask of every new pin. In #887 every hole found -after the first withdrawal was authority moving where the pin was not looking: another FILE (vitest -prefers `vitest.config.*`, and `vite build` prefers `vite.config.js`/`.mjs` to the pinned `.ts`, from -vite's own `DEFAULT_CONFIG_FILES`), another OCCURRENCE in the same file, another WORKFLOW (a `needs:` -edge naming a different job called `test`), a HOOK the pinned command invokes (a vite plugin's -`buildStart`, an npm `prebuild`/`preinstall` script). A pin removes the SPELLING problem outright and -reduces the MEANING problem to a list that can be worked through; a pin sold as immune to both stops -being re-examined. +fails, and it fails silently, so it is the question to ask of every new pin. In #887 every hole +found after the first withdrawal was authority moving where the pin was not looking: another FILE +(vitest prefers `vitest.config.*`, and `vite build` prefers `vite.config.js`/`.mjs` to the pinned +`.ts` — an ordering that belongs to a vite VERSION, so the release it was read at is dated in +`docs/guard-inventory.md`'s `test_image_build_delegates_the_spa_suite.py` row rather than copied +here), another OCCURRENCE in the same file, another WORKFLOW (a `needs:` edge naming a different job +called `test`), a HOOK the pinned command invokes (a vite plugin's `buildStart`, an npm +`prebuild`/`preinstall` script). A pin removes the SPELLING problem outright and reduces the MEANING +problem to a list that can be worked through; a pin sold as immune to both stops being re-examined. **Rider 2 — widening a clause can retire its own evidence.** A canary asserting that a finer mutation still SURVIVES is calibrated against a deliberately coarse clause. Widen that clause and the canary @@ -58,12 +59,12 @@ only while the precondition that made it survive still holds. **Where this sits against its neighbours, so it is not read as a duplicate.** The two incidents keep their own records — `ci.image-build-delegates-the-spa-suite` and -`process.hook-resolves-inputs-from-repo-root` — and this is the class they instantiate. +`process.hook-resolves-inputs-from-repo-root` — and this is the class they instantiate, while `testing.guard-derives-population-from-source` answers the case where the authoritative source is -MISSING — create one, never approximate it with a predicate over text — and this record answers the -case where the artifact IS present and authoritative and what is approximated is its GRAMMAR. -`docs/defect-shapes-773.md` §4 argues the general form. What was missing is a rule resolvable BY -TOPIC: `docs/guard-inventory.md` carries its precedent per incident, findable only from inside one. +MISSING (create one, never approximate it with a predicate over text) and this one the case where +the artifact IS present and authoritative and what is approximated is its GRAMMAR. +`docs/defect-shapes-773.md` §4 argues the general form; what was missing is a rule resolvable BY +TOPIC, since `docs/guard-inventory.md` carries its precedent per incident, findable only inside one. **What this does not cover.** Predicates over structured input a real parser produces — an AST, the git index, parsed workflow YAML consumed as data — are not approximations and keep their shape diff --git a/docs/guard-inventory.md b/docs/guard-inventory.md index 9e091de63..0ea990399 100644 --- a/docs/guard-inventory.md +++ b/docs/guard-inventory.md @@ -14,8 +14,9 @@ and `dotnet format`, and `grep -rn pytest .husky/` returns nothing. The runner i **Read `docs/decisions/records/testing/guard-derives-population-from-source.md`, `…/guard-ships-with-mutation-proof.md` and — for the SHAPE of the predicate itself, whenever it -decides by reading shell or config TEXT — `…/guard-pins-the-artifact-not-a-shape.md` before editing -a guard or adding a row.** +decides by matching inside an artifact with a grammar the predicate does not implement (shell, a +workflow `run:` body, TOML, YAML, a jq expression, JSON5) — `…/guard-pins-the-artifact-not-a-shape.md` +before editing a guard or adding a row.** ## Columns @@ -175,7 +176,7 @@ The audit #806 asked for, recorded whichever way it came out, because "we looked | `test_workflow_job_guards.py` | `.gitea/workflows/*.y*ml` | **yes** — every tracked workflow's jobs must each declare a `CI_JOB_ROLE`, and the guard-declaring ones must match `## Workflow-job guards` both ways | index-derived from the start (ersatztv#786); registered in `test_guard_populations_derive_from_git.py` so the shared proof covers it | | `test_pr_changed_files.py` | `.gitea/workflows/*.y*ml` | **yes** — "no OTHER workflow writes the review-verdict status", and since ersatztv#748 "every tracked workflow declares a `permissions:` block", with NO exemption list (the one `ci-image.yml` briefly needed was deleted when #744 landed, per #835) | converted. Not on #806's list: an untracked `.yaml` dropped in `.gitea/workflows/` reddened two guards while absent from the index — the issue's list of files to assess was a starting point, not the population | | `test_workflow_persist_credentials.py` | `.gitea/workflows/*.yml` + `*.yaml` | **yes, over TWO invariants on one population** — every `actions/checkout` in every workflow must drop the persisted credential (#835), and since #885 no job the `pull_request` trigger reaches, and no workflow scope outside `jobs:`, may name a stored secret (`ci.pr-route-carries-no-stored-credential`) | derived from the index from the start (#835). Shipped with NO exemption list, which is why it waited for #744: the one non-compliant checkout would otherwise have bought a permanent entry that keeps passing after its reason expires. The second invariant reuses that population and adds two derivations of its own — which jobs the trigger reaches (an `if:` PIN, not a parser) and where a reference to the secret store can be written — and it carries the same no-exemption rule, `secrets.GITEA_TOKEN` excepted as INJECTED rather than stored. THE GRADE BELOW COVERS ONE HALF: the manifest keys at most one declared clause mutation per GUARD file, so that entry stands for the checkout invariant. The route invariant is bound instead by a `CLAIMS` entry on this sentence — deleting `build`'s `if: github.event_name != 'pull_request'` from the shipped `docker-build.yml` reddens `test_no_PULL_REQUEST_route_job_names_a_STORED_secret` — which the harness executes every run for the same reason a `MUTATION` row is executed. A row that understates its guard is invisible to `test_guard_inventory.py`, which compares FILE sets, so this prose is the only thing that carries the second invariant | -| `test_image_build_delegates_the_spa_suite.py` | DERIVED: tracked Dockerfiles and workflows from the git index; the stages within them that carry the SPA source. PINNED (hand-written, and the distinction is the whole design): the commands each such stage runs, and the gating `Test SPA` step's `run:` body and `if:` | **yes on the derived populations, by pin rather than by predicate** — every SPA-carrying stage must have a pin and match it; every image-publishing job whose Dockerfile has such a stage must transitively `needs:` the gating job | ersatztv#887. **THE PARSER WAS WITHDRAWN, and that is the finding worth carrying.** Three versions of this guard asked "does this command RUN the suite, and can it FAIL?" of arbitrary shell text, and that predicate was wrong NINE times across three cold-review rounds, always the same mechanism: heredoc bodies skipped as data when BuildKit EXECUTES `RUN < `run: >` folds two commands into one; (3) Gitea's treatment of a SKIPPED `needs:` job is not modelled — the guard forbids the job-level `if:` that would create one rather than reasoning about it; (4) the pins are hand-written and a legitimate edit reddens this guard, which is the intended review trigger and the stated cost | +| `test_image_build_delegates_the_spa_suite.py` | DERIVED: tracked Dockerfiles and workflows from the git index; the stages within them that carry the SPA source. PINNED (hand-written, and the distinction is the whole design): the commands each such stage runs, and the gating `Test SPA` step's `run:` body and `if:` | **yes on the derived populations, by pin rather than by predicate** — every SPA-carrying stage must have a pin and match it; every image-publishing job whose Dockerfile has such a stage must transitively `needs:` the gating job | ersatztv#887. **THE PARSER WAS WITHDRAWN, and that is the finding worth carrying.** Three versions of this guard asked "does this command RUN the suite, and can it FAIL?" of arbitrary shell text, and that predicate was wrong NINE times across three cold-review rounds, always the same mechanism: heredoc bodies skipped as data when BuildKit EXECUTES `RUN < `run: >` folds two commands into one; (3) Gitea's treatment of a SKIPPED `needs:` job is not modelled — the guard forbids the job-level `if:` that would create one rather than reasoning about it; (4) the pins are hand-written and a legitimate edit reddens this guard, which is the intended review trigger and the stated cost | | `web/src/api/pageSizeCallSites.guard.test.ts` | `import.meta.glob` over `web/src/**/*.{ts,tsx,mts,cts}`, INTERSECTED with the git index | **yes** — an unregistered discovered site fails | converted under #819. The population is the glob INTERSECTED with the git index, and the walk/index divergence is asserted in BOTH directions WITHIN a shared scope: a key the index does not carry is dropped, and a tracked, on-disk, in-scope path that never reached the scanner FAILS rather than shrinking the population silently (that direction catches a dotfile, which the glob cannot match, and a disk/index spelling divergence, which `core.ignorecase` and NFD/NFC normalisation make permanent). The SCOPE itself cancels out of that equality — both sides call one `isInScopeSourcePath` — so narrowing it moves both sets together and no assertion comparing the two sides can see it. That is closed separately, by re-deriving the predicate from its two components over the whole tracked index (`the shared scope predicate is EXACTLY its two documented components`). Two mechanisms carry that. FIRST, the scope predicate is proved by a CLOSED-FORM restatement — it may share no helper, at any depth, with the predicate it checks, since anything shared sits on both sides of the comparison and cancels. FOUR earlier attempts were each measured going green while removing real files: a table of example paths (four of `src/`'s eight directories, missed a 23-file narrowing), a decomposition delegating its filename half to `isScannableSourceFileName` (11 files), one still sharing a `basename` helper (15 files, and blind to a planted call site), and one still sharing the tracked-file array every comparison was derived from (8 files, also blind). SECOND — because that last one showed a restatement cannot police the population it reads — the plugin runs a separate `git ls-files --others` query and the guard requires every in-scope walked path to appear in `tracked ∪ others`; narrowing `tracked` cannot suppress that, since it adds nothing to `others`. SIX residuals, NOT a claim of closure, each with its direction MEASURED by planting a real `pageSize` call site rather than reasoned about. FAIL-NOISY (they redden a checkout; they cannot hide a call site): (2) a scope term matching no tracked path today survives until the day it first matches one; (4) the WIRING in `listSourceFiles`, indistinguishable on a clean checkout where the walk set and the index set agree on every in-scope key (they are not literally equal — the index also carries `.css`, which the glob never yields), so no assertion COMPARING THE TWO POPULATIONS can tell which is passed; (5) `ABSENT_FROM_DISK` emptied. BLIND — each hid a planted `pageSize: 100` with the whole suite green: (1) a COORDINATED edit of the scope predicate and BOTH closed-form restatements, which is three sites and not two, since a second restatement guards `expectedTrackedSources` (measured: the two-site edit reddens, the three-site edit does not) — tolerated because a three-site policy edit is review-visible in a way a one-line slip is not; and (3) a misdescription by the plugin's THREE outputs — not only a mispartition of `tracked`/`others`, which preserves the union any consumer compares, but a FATTENED `absentFromDisk`, which subtracts real files from the hole-detection direction. The fattening is now caught wherever it overlaps a file the walk found (a walked path is on disk by construction, so it can never be legitimately absent); restricted to paths the walk cannot see anyway — a dotfile, a case-divergent name — it stays invisible from the consuming side and is answerable only by testing the derivation directly, and the real-git tests that do so catch an UNCONDITIONAL misdescription, not one keyed on a path pattern. Neither blind residual is tolerated for being harmless. A sixth is specific to WATCH mode and is stale in BOTH directions, so it belongs to neither group: the virtual module has no backing file, so the index is read once per dev-server lifetime while the glob refreshes. A file CREATED mid-session reddens the cross-check misleadingly (noisy); a file already UNTRACKED when the watcher started keeps that classification when staged mid-session, so its call sites are never scanned — MEASURED green across both phases while `npx vitest run` on the same tree reports `UNREGISTERED`, i.e. blind, and a window that did not exist before #819 because the population was then the walk. A watch-mode green is therefore not authoritative for this guard; restarting the watcher clears the noisy case and opens the blind one, so the remedy is `npm test -- --run`. Invalidating the module from `configureServer` was implemented, measured and REJECTED: it fixes the created-mid-session red and additionally blinds the create-then-stage sequence, while the already-untracked-then-staged sequence is blind either way, since `git add` fires no watcher event in either design. The index is read by `web/vite-plugins/trackedSourceFiles.ts` in Vite's own Node context and handed to the app project as the virtual module `virtual:etv-tracked-source-files`. That is what reaches the index without admitting `@types/node` to `tsconfig.app.json` — the obstacle that deferred this, since wiring those types in was tried and reverted (it leaked Node's `setTimeout` into the app project and broke three unrelated tests). The plugin throws rather than falling back to the unfiltered walk when git fails, reports zero files, or cannot learn the Vite root. Residual, stated: a file tracked but DELETED in the working tree is subtracted deliberately, since an unstaged deletion is a normal developer state and a guard red on one gets ignored (#806); it is distinguished from the hole cases above by an on-disk existence check, not conflated with them | | `web/src/api/completeAnnotations.guard.test.ts` | TWO derived populations: the `Complete<…>` annotations across `web/src/**/*.{ts,tsx,mts,cts}` INTERSECTED with the git index, and the droppable SCHEMAS parsed out of the generated `src/api/generated/v1.d.ts` | **partial, and the split is stated** — set equality holds for the SCHEMA population (a new optional member fails until dispositioned) and every tracked in-scope path must be supplied by the glob; there is NO closed-form restatement of the scope predicate, so a coordinated edit to it is caught by five named path pins AND a 0.95 population-ratio floor, not by a completeness proof | ersatztv#820. Turns `test_optional_request_members.py`'s `COVERED` disposition — worded "the builder is annotated `Complete`", a claim about ANOTHER LANGUAGE'S source that nothing checked — into a check, and makes `docs/spa-conventions.md` §4b's prohibitions executable as asserted ABSENCES. It found one live defect: `playouts.ts` declared two request types as hand-written mirrors SHADOWING generated schemas of the same name, so their `Complete<>` was checking a local copy rather than the contract. **NARROWED after four BLOCKED review rounds, and the narrowing is the point.** It originally also derived the write WRAPPERS and required the annotation on the wrapper parameter. Every blocker across those rounds came out of that one mechanism — the obligation attached to the wrong population; reachability mistaken for protection (`Complete` is shallow, so a wrapper annotation never reached a nested schema); body discovery keyed on a parameter NAME, then on parameter-versus-local; and finally an `export function` -> `export const` refactor that removed real protection while both the AST scan and its supposedly independent regex cross-check stayed blind TOGETHER, because both keyed on the same token. Five defects from one mechanism is the `process.enumerate-workaround-behaviors-before-deleting` signal to remove it rather than patch a sixth time, and ~250 lines went with it. WHAT IS NOT COVERED, listed because the earlier version of this row stated residuals in the direction that was actually covered: (1) the obligation is per-SCHEMA, not per-SITE and not per-WRAPPER — deleting the `Complete<>` from an API wrapper stays green as long as some production file still names that schema, so the wrapper half of §4b rests on review; (2) it is a TOKEN-PRESENCE check, so a dead `export type X = Complete` that nothing uses discharges the obligation as well as a live builder does — it catches deletion, which is the failure actually observed in #807, not substitution; (3) the PHANTOM direction needs a fresh literal in a contextually typed position and is not checked at all (sites-in-code, #777); (4) the test-file exclusion covers `*.test.*`, `*.spec.*` and the ONE setup file `vite.config.ts` names, whose path is pinned so a rename reddens — a SECOND `setupFiles` entry is loaded by vitest every run and WOULD discharge the obligation; that needs a three-site coordinated edit and is tolerated on the terms the sibling guard states for its own; (5) a MUST-NOT-ANNOTATE violation inside a `*.guard.test.ts` is not seen, since that file class is excluded to keep `completeRequest.guard.test.ts`'s synthetic `Complete<{…}>` fixtures out of the resolver. The disposition VALUES are cross-checked against the authoritative Python table by `scripts/tests/test_complete_annotation_dispositions.py` — without it, flipping one row from `ANNOTATED` to `CREATE` silently retired the requirement, which cold review demonstrated. Nine mutations were witnessed by hand across development, NOT re-executed per suite, so this guard claims no standing `MUTATION` grade — the same footing as `pageSizeCallSites.guard.test.ts`. (Its Python cross-check `test_complete_annotation_dispositions.py` DOES carry a declared harness-executed mutation and is graded accordingly.) A SIXTH residual, shared with its sibling: every population here derives from `trackedSources.tracked`, so a narrowing inside `web/vite-plugins/trackedSourceFiles.ts` cancels out of every comparison — this guard never reads the plugin's separate `others` query, and relies on `pageSizeCallSites.guard.test.ts` and `trackedSourceFiles.test.ts` policing the plugin. A coordinated scope edit is caught by five named path pins AND a 0.95 population-ratio floor whose denominator is computed by a different expression, not by a completeness proof | | `test_ci_release_path_scan_job.py` | `.gitea/workflows/*.y*ml` + `scripts/**` | **no** — a fixture assembling a tmp harness, asserted about behaviour not membership | takes its file LIST from the index anyway, for hermeticity not completeness: `shutil.copytree` copied whatever was on disk, including untracked files and `scripts/__pycache__`, into a tree whose behaviour the probes then measure. Content still comes from the working tree. The copy is not a git repo, so the two files this step RUNS may not use the helper — see the fixture docstring |