Extend mutation-claim enforcement to prose (#881) #914

Merged
timothy merged 18 commits from 881-mutation-claim-prose-scope into main 2026-09-05 08:40:34 +02:00
Owner

fixes #881

Root cause

testing.mutation-claims-are-executed established a real mechanism (a declared clause mutation
re-run every suite) but scoped its population to docs/guard-inventory.md's MUTATION rows only.
#812's four review-round defects were all the same shape one level up — a prose sentence asserting
that a specific mutation reddens (or does not redden) a named test, written in a docstring, a
decision record, or a code comment, with nothing binding the prose to the referent it describes.
The rule that would have caught all four already existed; it just wasn't reachable from where the
claims were being written (the #773 shape: a rule fixed where first noticed, not extended to its
class).

What shipped

  • Population, derived from source, not the issue's four-item table. A mechanical
    over-approximation (a mutation verb within 80 chars of an outcome word, over tracked scripts/,
    .claude/, .gitea/, docs/decisions/, web/src/, fixtures excluded) returned 128 candidate
    lines across 65 files at efadbec29, against the 27 MUTATION-graded rows the existing rule
    reached — 47 of the 128 carry the NEGATIVE direction ("reddens nothing"), which no MUTATION row
    can express, so the rule was literally unsatisfiable for them. 128 is a candidate count, not the
    population: nothing separates machinery prose from a real claim mechanically, which is why the
    record declares the referent (any prose sentence asserting a specific mutation's outcome) rather
    than trying to detect it.
  • Extended testing.mutation-claims-are-executed in place, not a sibling record. A per-site-class
    record recreates the exact defect the issue is about (next site class falls outside both — see
    #773/#784/#743). The subject is unchanged: a mutation-outcome claim is executed or not written;
    only the population widens.
  • A CLAIMS tuple in scripts/tests/mutation_manifest.py, parallel to the existing MUTATION
    rows: each entry carries a tracked site, a verbatim quote, and an outcome of RED or GREEN.
    RED reuses the existing mutate-and-verify path. GREEN (a claim that a mutation is not
    noticed) is the half no MUTATION row could express, and is read through three separate gates in
    verify_claim/mutation_harness_lib.py because a green from an unrelated proof is otherwise
    indistinguishable from the claimed one: the run exited 0, something actually passed, and the
    named proof was shown to REACH the mutated clause (a required second, declared mutation of the
    same clause that must redden the same proof). Round 3 of review demonstrated the hole directly by
    retargeting the shipped GREEN entry's proof to an unrelated test file (test_bom_guard_detection.py,
    which never touches scripts/check-doc-narrative.py) and showing verify_claim still passed —
    fixed by adding the relevance gate as a required third check.
  • Proposal 2 (a quotation of another file is a claim about that file) — adopted with a mechanism:
    the site/quote pair on every CLAIMS entry, checked exactly once per run; a reworded sentence
    now reports as a RETARGET. The general form (any quotation anywhere) is rejected for the same
    reason inference is rejected elsewhere in this harness: an unnamed referent has nothing to resolve.
  • Proposal 3 (never anchor prose to a state your own commit moves) — rejected as a detector, kept
    as a phrasing rule.
    The only plausible pattern set ("the last edit to", "the only X that", "as of
    today", …) matched 16 lines across the scanned corpus on 2026-09-04, and all 16 were legitimate
    rationale prose (0/16 true positives) — a detector that is pure false positives stops an advisory
    check from being read. Kept under docs.no-session-narrative's dated-measurement clause; inside
    this rule's own population the anchor is the manifest entry, which is re-executed and cannot go
    stale.
  • Proposal 4 (prose answering a review finding gets the same scrutiny as a code fix) — not
    adopted
    , argued in the record: its own fourth proposal is prose written while answering a review
    finding, "the moment of least slack" — exactly when a rule needs to be mechanical rather than
    exhortative to survive. For the shape this record already covers (a checkable mutation-outcome
    claim), the CLAIMS mechanism supplies that; for prose outside that shape, the scrutiny proposal 4
    asks for stays where it already lives (docs.no-session-narrative), not duplicated here.
  • docs/README.md's task-signal row for testing.mutation-claims-are-executed updated to mention
    the CLAIMS population alongside the MUTATION-row mechanism (deferred twice earlier only for
    slot-ownership reasons; #907 freed the file).
  • The harness's shipped GREEN entry (scripts/check-doc-narrative.py) was itself falsified on
    first execution of the reworked mechanism — the mutation it named did not, in fact, leave the
    suite green in the scope claimed — and was corrected in the same branch (see review history).

Measured numbers

  • Population scan at efadbec29: 128 candidate lines / 65 files (mechanical over-approximation);
    47/128 carry the unsatisfiable NEGATIVE direction; 27 MUTATION-graded rows in the existing
    inventory.
  • Proposal-3 detector scan re-run at efadbec29 (round-2 finding): 58 lines matched the record's own
    pinned scan; besides the two named examples, scripts/tests/test_pr_changed_files.py:5267 is a
    third instance of the same shape under the record's operative definition — the record now states
    the invariant (three kinds under one pattern) rather than an exact count requiring judgement to
    reproduce.
  • CLAIMS cost, re-measured 2026-09-05 with the relevance gate, -p no:randomly, whole file, three
    baseline/branch pairs back to back: +31.7–43.6% (46.8s→67.2s, 50.1s→67.2s, 48.9s→64.4s), varying
    concurrent load.
  • Finisher local gate: PYTHONPATH=. python3 -m pytest scripts/tests -q → 1580 passed, 3 skipped,
    875.33s. ruff check on all touched .py files → all checks passed. Decisions validator → OK
    (advisory-only 60-line-ceiling warnings, none new). Catalog regeneration → no diff (already
    current). check-doc-narrative.py --diff origin/main → 0 advisory warnings. No .cs or web/
    files touched by this branch, so the .NET and SPA gates do not apply.

Review history

<<REVIEW HISTORY
round 1: 2 lens(es); 1 blocking, 0 should-fix, 1 nit; answered by fix commit(s): ff65e5e7c fix(881): the docstring quoted an outcome the branch had already narrowed
round 2: 2 lens(es); 1 blocking, 1 should-fix, 1 nit; answered by fix commit(s): 0f8e73ab0 fix(881): the replacement clause asserted an absolute I had not measured | f34828806 fix(881): the gate order I called load-bearing is a cost choice, measured
round 3: 2 lens(es); 0 blocking, 0 should-fix, 2 nit; clean — loop ended
REVIEW HISTORY>>

What each fix commit changed (from git show, not memory)

ff65e5e7c fix(881): the docstring quoted an outcome the branch had already narrowed —
scripts/tests/mutation_harness_lib.py: the Claim docstring's GREEN-direction motivation
restated scripts/check-doc-narrative.py's outcome as "reddens no test" — the universal an earlier
commit (a9341d841) had already narrowed on the source file itself to
"test_check_doc_narrative.py stays green with this arm removed". git grep -F "reddens no test"
returned exactly one hit — the docstring making the stale claim, reproducing the branch's own
defect #2 inside its own fix. The enumeration now names the three corpus sites and the mutation
each describes, states the shared shape, and explicitly does not restate any site's outcome wording
— the only two copies of an outcome left are the site's own comment and the CLAIMS quote bound to
it, which is the binding by construction.

f34828806 fix(881): the gate order I called load-bearing is a cost choice, measured —
docs/decisions/records/testing/mutation-claims-are-executed.md: the record's rule and
mechanics fields both asserted the relevance gate must run last or the earlier two gates could
never be witnessed refusing alone. Executed at ff65e5e7c: reordering the relevance gate first and
running pytest scripts/tests/test_mutation_harness.py -p no:randomly -k "GREEN_EXIT_STATUS or GREEN_VACUITY or GREEN_RELEVANCE or UNKNOWN_outcome" still gives 4 passed — the earlier gates are
still independently witnessed. What the order actually buys is cost (the relevance gate is the only
one that pays for a second full run of the proof), and both sites are corrected to say that instead.
Also corrected a second, unrelated absolute in the same record: a sentence gave "line-wrapping" as
the reason a sentence was paraphrased rather than quoted at a second site, when the real reason is
that the quoted sentence no longer exists in the tree at all (measured 2026-09-05 via a fixed-string
git ls-files search returning no file).

0f8e73ab0 fix(881): the replacement clause asserted an absolute I had not measured —
docs/decisions/records/testing/mutation-claims-are-executed.md and
scripts/tests/mutation_harness_lib.py: the previous commit's replacement text ("that is the whole
of what the order buys") was itself an unmeasured absolute — what was actually measured is narrower
(the three gates are each still independently witnessed with the relevance gate moved first); it
does not establish that reordering buys nothing else (the sandbox-reset placement is a second,
unmeasured candidate). The absolute is removed from both sites, leaving only the cost reason, which
is readable directly from the control flow.

Cross-family review

not required (routine risk class under process.independent-review-rubric)

Deferred / left for follow-up

  • None from this issue's own scope. The record's re-derived proposal-3 scan (58 lines) and the
    population scan (128 candidates / 65 files) are both pinned dated measurements in the record body,
    re-derivable by the stated predicate rather than a bare figure.
  • docs/README.md's wider CLAIMS-population mention was previously deferred twice for
    slot-ownership reasons against #907; #907 is merged and the update ships in this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV

fixes #881 ## Root cause `testing.mutation-claims-are-executed` established a real mechanism (a declared clause mutation re-run every suite) but scoped its population to `docs/guard-inventory.md`'s `MUTATION` rows only. #812's four review-round defects were all the same shape one level up — a prose sentence asserting that a specific mutation reddens (or does not redden) a named test, written in a docstring, a decision record, or a code comment, with nothing binding the prose to the referent it describes. The rule that would have caught all four already existed; it just wasn't reachable from where the claims were being written (the #773 shape: a rule fixed where first noticed, not extended to its class). ## What shipped - **Population, derived from source, not the issue's four-item table.** A mechanical over-approximation (a mutation verb within 80 chars of an outcome word, over tracked `scripts/`, `.claude/`, `.gitea/`, `docs/decisions/`, `web/src/`, fixtures excluded) returned 128 candidate lines across 65 files at `efadbec29`, against the 27 `MUTATION`-graded rows the existing rule reached — 47 of the 128 carry the NEGATIVE direction ("reddens nothing"), which no `MUTATION` row can express, so the rule was literally unsatisfiable for them. 128 is a candidate count, not the population: nothing separates machinery prose from a real claim mechanically, which is why the record declares the referent (any prose sentence asserting a specific mutation's outcome) rather than trying to detect it. - **Extended `testing.mutation-claims-are-executed` in place**, not a sibling record. A per-site-class record recreates the exact defect the issue is about (next site class falls outside both — see #773/#784/#743). The subject is unchanged: a mutation-outcome claim is executed or not written; only the population widens. - **A `CLAIMS` tuple in `scripts/tests/mutation_manifest.py`**, parallel to the existing `MUTATION` rows: each entry carries a tracked `site`, a verbatim `quote`, and an `outcome` of `RED` or `GREEN`. `RED` reuses the existing mutate-and-verify path. `GREEN` (a claim that a mutation is *not* noticed) is the half no `MUTATION` row could express, and is read through three separate gates in `verify_claim`/`mutation_harness_lib.py` because a green from an unrelated proof is otherwise indistinguishable from the claimed one: the run exited 0, something actually passed, **and** the named proof was shown to REACH the mutated clause (a required second, declared mutation of the same clause that must redden the same proof). Round 3 of review demonstrated the hole directly by retargeting the shipped GREEN entry's proof to an unrelated test file (`test_bom_guard_detection.py`, which never touches `scripts/check-doc-narrative.py`) and showing `verify_claim` still passed — fixed by adding the relevance gate as a required third check. - **Proposal 2 (a quotation of another file is a claim about that file) — adopted with a mechanism**: the `site`/`quote` pair on every `CLAIMS` entry, checked exactly once per run; a reworded sentence now reports as a RETARGET. The general form (any quotation anywhere) is rejected for the same reason inference is rejected elsewhere in this harness: an unnamed referent has nothing to resolve. - **Proposal 3 (never anchor prose to a state your own commit moves) — rejected as a detector, kept as a phrasing rule.** The only plausible pattern set ("the last edit to", "the only X that", "as of today", …) matched 16 lines across the scanned corpus on 2026-09-04, and all 16 were legitimate rationale prose (0/16 true positives) — a detector that is pure false positives stops an advisory check from being read. Kept under `docs.no-session-narrative`'s dated-measurement clause; inside this rule's own population the anchor is the manifest entry, which is re-executed and cannot go stale. - **Proposal 4 (prose answering a review finding gets the same scrutiny as a code fix) — not adopted**, argued in the record: its own fourth proposal is prose written while answering a review finding, "the moment of least slack" — exactly when a rule needs to be mechanical rather than exhortative to survive. For the shape this record already covers (a checkable mutation-outcome claim), the `CLAIMS` mechanism supplies that; for prose outside that shape, the scrutiny proposal 4 asks for stays where it already lives (`docs.no-session-narrative`), not duplicated here. - **`docs/README.md`'s task-signal row for `testing.mutation-claims-are-executed`** updated to mention the `CLAIMS` population alongside the `MUTATION`-row mechanism (deferred twice earlier only for slot-ownership reasons; #907 freed the file). - The harness's shipped `GREEN` entry (`scripts/check-doc-narrative.py`) was itself falsified on first execution of the reworked mechanism — the mutation it named did not, in fact, leave the suite green in the scope claimed — and was corrected in the same branch (see review history). ## Measured numbers - Population scan at `efadbec29`: 128 candidate lines / 65 files (mechanical over-approximation); 47/128 carry the unsatisfiable NEGATIVE direction; 27 `MUTATION`-graded rows in the existing inventory. - Proposal-3 detector scan re-run at `efadbec29` (round-2 finding): 58 lines matched the record's own pinned scan; besides the two named examples, `scripts/tests/test_pr_changed_files.py:5267` is a third instance of the same shape under the record's operative definition — the record now states the invariant (three kinds under one pattern) rather than an exact count requiring judgement to reproduce. - `CLAIMS` cost, re-measured 2026-09-05 with the relevance gate, `-p no:randomly`, whole file, three baseline/branch pairs back to back: +31.7–43.6% (46.8s→67.2s, 50.1s→67.2s, 48.9s→64.4s), varying concurrent load. - Finisher local gate: `PYTHONPATH=. python3 -m pytest scripts/tests -q` → 1580 passed, 3 skipped, 875.33s. `ruff check` on all touched `.py` files → all checks passed. Decisions validator → OK (advisory-only 60-line-ceiling warnings, none new). Catalog regeneration → no diff (already current). `check-doc-narrative.py --diff origin/main` → 0 advisory warnings. No `.cs` or `web/` files touched by this branch, so the .NET and SPA gates do not apply. ## Review history <<REVIEW HISTORY round 1: 2 lens(es); 1 blocking, 0 should-fix, 1 nit; answered by fix commit(s): ff65e5e7c fix(881): the docstring quoted an outcome the branch had already narrowed round 2: 2 lens(es); 1 blocking, 1 should-fix, 1 nit; answered by fix commit(s): 0f8e73ab0 fix(881): the replacement clause asserted an absolute I had not measured | f34828806 fix(881): the gate order I called load-bearing is a cost choice, measured round 3: 2 lens(es); 0 blocking, 0 should-fix, 2 nit; clean — loop ended REVIEW HISTORY>> ### What each fix commit changed (from `git show`, not memory) **`ff65e5e7c` fix(881): the docstring quoted an outcome the branch had already narrowed** — `scripts/tests/mutation_harness_lib.py`: the `Claim` docstring's GREEN-direction motivation restated `scripts/check-doc-narrative.py`'s outcome as "reddens no test" — the universal an earlier commit (`a9341d841`) had already narrowed on the source file itself to "`test_check_doc_narrative.py` stays green with this arm removed". `git grep -F "reddens no test"` returned exactly one hit — the docstring making the stale claim, reproducing the branch's own defect #2 inside its own fix. The enumeration now names the three corpus sites and the mutation each describes, states the shared shape, and explicitly does not restate any site's outcome wording — the only two copies of an outcome left are the site's own comment and the `CLAIMS` quote bound to it, which is the binding by construction. **`f34828806` fix(881): the gate order I called load-bearing is a cost choice, measured** — `docs/decisions/records/testing/mutation-claims-are-executed.md`: the record's `rule` and `mechanics` fields both asserted the relevance gate must run *last* or the earlier two gates could never be witnessed refusing alone. Executed at `ff65e5e7c`: reordering the relevance gate first and running `pytest scripts/tests/test_mutation_harness.py -p no:randomly -k "GREEN_EXIT_STATUS or GREEN_VACUITY or GREEN_RELEVANCE or UNKNOWN_outcome"` still gives 4 passed — the earlier gates are still independently witnessed. What the order actually buys is cost (the relevance gate is the only one that pays for a second full run of the proof), and both sites are corrected to say that instead. Also corrected a second, unrelated absolute in the same record: a sentence gave "line-wrapping" as the reason a sentence was paraphrased rather than quoted at a second site, when the real reason is that the quoted sentence no longer exists in the tree at all (measured 2026-09-05 via a fixed-string `git ls-files` search returning no file). **`0f8e73ab0` fix(881): the replacement clause asserted an absolute I had not measured** — `docs/decisions/records/testing/mutation-claims-are-executed.md` and `scripts/tests/mutation_harness_lib.py`: the previous commit's replacement text ("that is the whole of what the order buys") was itself an unmeasured absolute — what was actually measured is narrower (the three gates are each still independently witnessed with the relevance gate moved first); it does not establish that reordering buys nothing else (the sandbox-reset placement is a second, unmeasured candidate). The absolute is removed from both sites, leaving only the cost reason, which is readable directly from the control flow. ## Cross-family review not required (routine risk class under process.independent-review-rubric) ## Deferred / left for follow-up - None from this issue's own scope. The record's re-derived proposal-3 scan (58 lines) and the population scan (128 candidates / 65 files) are both pinned dated measurements in the record body, re-derivable by the stated predicate rather than a bare figure. - `docs/README.md`'s wider CLAIMS-population mention was previously deferred twice for slot-ownership reasons against #907; #907 is merged and the update ships in this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
timothy added 18 commits 2026-09-05 08:18:52 +02:00
`testing.mutation-claims-are-executed` was the right rule scoped to its first
site: `mutation_manifest.py` declared itself "one per `MUTATION`-graded row of
`docs/guard-inventory.md`", so the same claim written in a code comment, a test
docstring or a decision record was outside it by construction. That is where all
four of ersatztv#812's consecutive review-round defects lived.

Extend the rule in place rather than adding a sibling record: a sibling would
recreate the exact shape (a rule per site class, with the next site class outside
both) that #773, #784 and #743 each are. The subject is unchanged; only the
population widens.

Mechanism: `CLAIMS` in `scripts/tests/mutation_manifest.py`, keyed on the PROSE.
Each entry carries the tracked `site` and the verbatim `quote`, checked every run,
so a reworded sentence reports as a retarget instead of drifting from the entry
that justifies it — this is proposal 2 (a quotation of another file is a claim
about that file) adopted where the referent is declared. Each entry also declares
RED or GREEN and is executed in the existing sandbox. GREEN is new: 47 of the 128
candidate lines the corpus grep returns at efadbec29 assert that a mutation is NOT
noticed, and no `MUTATION` row can express that, so the rule was unsatisfiable for
them. The green direction is read by two separate clauses (exited 0, and something
actually passed) so neither can mask the other, and each carries its own disarm
proof.

Proposal 3 (never anchor prose to a state your own commit moves) is rejected as a
DETECTOR and kept as a phrasing rule: measured 2026-09-04, the only plausible
pattern set for it matched 16 lines across the scanned corpus and every one was
legitimate rationale prose.

The seed set falsified a shipped claim on its first run: `post-review-verdict.sh`
asserted that disarming its array-TYPE read-back test left the suite green. It
does not — jq refuses to iterate a `null` `.statuses` and the script dies with the
parse message, reddening `test_a_readback_whose_statuses_array_is_NULL_is_refused`.
Comment corrected, entry graded RED.

fixes #881

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
`test_mutation_harness.py` now imports the shared index derivation, which
`test_every_index_derived_module_is_registered` requires be registered or exempt.
It is exempt: its population is `CLAIMS`, and it consults the index only per
member, to answer whether a declared `site` is a path git tracks. The exemption
list's own docstring counted its entries, so that count and its review date move
with it.

Three claims written by this change were falsified by this change, which is the
shape it exists to catch:
- the binding test's docstring said membership comes from the index "not from
  `Path.is_file`", while the same test now asserts existence with `is_file`;
- the record quoted the manifest docstring this change rewrites — an anchor to a
  state the commit moves, which the record itself rejects. It now anchors to
  `efadbec29`;
- the `Claim` docstring quoted three files without naming them. They are named.

refs #881

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
Four review findings, every one the defect class this record is about: a prose
assertion with nothing binding it to what it asserts.

THE DERIVED POPULATION WAS NOT REPRODUCIBLE. "128 lines across 65 files" and
"47 of the 128 carry the NEGATIVE direction" cannot be reached from the predicate
the record described, while the record told the reader to re-run it there. A
review swept ~40 readings of that description at efadbec29 and none returns
either number. The scan is now pinned VERBATIM as the command that produced it,
and the figures are what that command prints at efadbec29 on 2026-09-05:

  104 files, 311 lines  - candidates
   39 files,  69 lines  - the same command with the outcome half narrowed to
                          `green`, i.e. the negative direction

Both were re-run by extracting the fenced command from the committed file and
executing it, so the text and the numbers cannot have diverged. This supersedes
the 47/128 figures quoted in 05992bec7's message.

CLAIMS[0]'s QUOTE BOUND THE WRONG HALF. It stopped at the comma after the
mutation, leaving "so `test_a_readback_whose_statuses_array_is_NULL_is_refused`
reddens" outside the binding - the words that make the sentence a claim. The
harness counts occurrences of the quote alone, so the outcome could be rewritten,
or the test renamed in the prose, with the entry still reporting the red as
verified. The quote now spans both halves, and the scope rule is stated in the
record's `rule` and in the manifest beside the entries, where the next one is
written.

"BOTH WERE CORRECTED IN THE SAME CHANGE" WAS FALSE. Only the shell comment was:
the Python test has asserted the shape diagnostic since 5d955000f (#889), and
this branch does not touch it. What this change adds beside the rewritten comment
is the binding.

THE COST BAND CONTRADICTED ITS OWN MEASUREMENTS. `mechanics` said 13-15% over
three pairs spanning 12.7%, 16.6% and 14.4%. It now states 13-17% and the
per-pair figures, since the record tells the reader to carry the percentage
forward rather than the seconds.

refs #881

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
Self-review over the fix commit, on the same predicate the record states.

The cost band said 13-17% over measurements spanning 12.7% to 16.6% - a band
that ROUNDS is still a band the data does not support at its lower edge. It now
states 12.7-16.6%, which is the span itself, with the per-pair figures beside it.

The quote-scope rule was stated as an OUTCOME claim ("free to be rewritten under
a green harness", "with the entry still reporting the red as verified") in the
record's `rule`, in the manifest preamble and beside the entry. That is a
mutation-outcome claim about the harness with no `CLAIMS` entry behind it -
manufactured by the sentence that introduces the rule against it. All three now
state the STRUCTURE, which is what a reader can check by looking: the assertion
and the test it names are outside the binding.

The manifest preamble said "three of the entries below assert that a mutation is
NOT noticed". `CLAIMS` holds three entries and exactly ONE is GREEN; the three
the `Claim` docstring names are CORPUS sites, not entries. Corrected to one, and
"the most common shape prose actually takes" - a frequency nothing measured -
dropped rather than quantified.

Two claims in the new record prose were themselves overstated. The 69-line green
narrowing was described as the negative direction rather than as candidates for
it: sampling the hits shows `green` in this corpus is as often a CI job's colour
as a mutation's outcome, so both figures are now labelled CANDIDATE counts. And
the "the number moves under the commit that records it" sentence now carries the
figure that shows it - the same command with `HEAD` in place of the sha prints
`107 files, 344 lines`, measured on the committed tree.

refs #881

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
The generated catalog embeds each record's `rule`, so rewording the quote-scope
clause in 38bdf7bd0 left `docs/decisions/README.md` behind the record. Nine tests
red on it - the four `build_catalog_check_path` reformat cases, its stale-catalog
CLI proof, two `decisions_validate` main() cases, and the two mutation-harness
entries whose positive control runs that validator.

refs #881

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
The sentence explaining why the scan pins a sha quoted a second figure for
`HEAD`, which is exactly the anchor-to-a-moving-state shape this record settles:
correct on this branch, wrong the moment anything else lands. The reason it was
supporting is checkable without a number - the paragraph's own prose, the pinned
command included, matches the pattern.

The first draft of that replacement said "twice over". Three lines of the
paragraph match, so the count is dropped rather than corrected; a count of
matching lines in a paragraph nobody will re-measure is the same defect one size
smaller.

refs #881

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
Two more in the same class, both in prose this branch wrote.

The calibration paragraph quoted `post-review-verdict.sh` as asserting "the
absent-entry check catches every case on its own". That string occurs in no file:
the comment wraps it across a line break at `catches`/`every`, so `git grep` for
it finds exactly one hit - the record asserting it. That is ersatztv#812's second
defect reproduced inside the record written to end it. Paraphrased without quote
marks and pinned to lines 316-317 at `efadbec29`, which is what this record's own
proposal-2 clause prescribes for a quotation that cannot be checked.

The closing paragraph called the scan's hits "the remaining population" and "a
backlog", three paragraphs after establishing that both figures are CANDIDATE
counts and that reading them as a backlog of real claims overstates them. The
closing text now says what is actually known: a place to look, with nobody having
established how many are claims.

refs #881

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
`docs/defect-shapes-773.md` §4's sentence was quoted verbatim-looking but with a
lowered initial capital, and the source wraps it across a line at
`evidence`/`behind`, so neither the written form nor the corrected one is
findable by grep. Same treatment as the `post-review-verdict.sh` one: paraphrase
without quote marks, keep the section reference, say why.

The section reference itself was checked - the sentence is at line 351, under
`## 4. Detectors`.

refs #881

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
"one of the three entries below asserts that a mutation is NOT noticed" is a
count of the entries it sits above, and the preamble's last line - and the file's
own docstring - say no count is kept here, because a count of the entries is a
second copy of them. The previous wording said "three", which was also wrong: one
entry is GREEN. Corrected to "three" would have been an accurate second copy;
the sentence now states the shape and counts nothing.

refs #881

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
Four review findings, all of the class this branch is about - prose asserting
what a mechanism does, with nothing binding it to the mechanism.

The record's `mechanics:` said a GREEN claim over an already-red proof "would
otherwise be satisfied by the redness it is supposed to rule out". Inverted:
`verify_claim`'s GREEN branch REFUSES any non-zero exit, so redness refutes a
GREEN claim and can never satisfy one - which is what
test_MUTATION_disarming_the_GREEN_EXIT_STATUS_gate_accepts_a_proof_that_WENT_RED
asserts. The hazard the control removes is the same one it removes for the
rows, and it runs in both directions: an already-red proof satisfies a RED
claim with redness its mutation did not cause, and refuses a GREEN one for a
reason unrelated to its mutation. Both the record and the fixture comment now
say that, and both say what the control CANNOT do - its assertions are over the
aggregate of every proof ref, so a single ref collecting nothing is invisible to
it and is caught per-claim by the vacuity gate instead.

The manifest's `why` had widened a scoped sentence into "THE OTHER GATES EACH
CARRY THEIR OWN PROOF" and then enumerated them, which made the enumeration a
completeness claim it could not meet: the identical-replacement refusal carried
no proof at all. The review measured that at 8adf21eff - `if mutated ==
original:` disarmed, whole file 49 passed 1 skipped. That gate is the one a red
proof cannot be told apart from: the mutant is byte-identical, so the proof runs
against the original tree and an already-red one reddens exactly like a
detection. Disarmed, the harness certifies it as "the named test went red under
the declared mutation, with the declared diagnostic" - witnessed here on the
real library, restored after. So the measurement above no longer holds, by
construction: the gate now has a disarm proof, and the sentence says explicitly
that naming the gates is not a claim the list is closed.

Proposal 3's rejection quoted "16 lines" with no predicate - the defect the
record's own body names three paragraphs later, where the population scan is
pinned verbatim for exactly that reason. The figure is not reproducible from the
text. Replaced by a pinned `git grep` over the same corpus at the same sha
(`32 files, 58 lines`), with what reading all 58 shows: they are rationale, the
class the rule carves out, and the few real state anchors among them are not
separable by pattern, because the difference is whether the sentence explains or
asserts.

refs #881

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
Self-review of the previous commit, on the same class it fixes.

"none of them asserts a mutation outcome", written about the 58 lines the
proposal-3 scan returns, is false. Two do: the mutation table at
`docs/decisions/records/ffmpeg/watermark-resolution-unified.md` line 104, which
names a dropped discriminator and the single test that catches it, and
`web/src/screens/AutoTuneScreen.test.tsx` line 174, which says what a revert to
the old flex row can redden. Both read in full at `efadbec29` rather than from
the truncated grep line - the truncation is how the first pass missed them.

Three kinds of sentence under one pattern is a better argument than the one the
false claim was making: it is not that the pattern finds only rationale, it is
that it finds rationale, state anchors and mutation-outcome claims side by side
and nothing in the string tells them apart.

Two smaller ones in the same commit. The new test's docstring said the gate is
"the one gate the others cannot cover" and the manifest said "the one PRE-FLIGHT
refusal a red proof cannot be told apart from" - both assert uniqueness among
the pre-flight refusals that neither measured, and a clause occurring zero times
also leaves the text identical. Narrowed to what the mutant demonstrates: no
later gate stands in for it. And the fixture comment glossed `verify_claim`'s
GREEN refusal in quote marks, which under this record's own proposal-2 clause
reads as a quotation of the library; it is not one, so the marks are gone.

refs #881

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
Round three found the one half of the new mechanism with no relevance gate.
`verify_claim`'s GREEN path read exactly two things — the run exited 0, and
something PASSED — and both are satisfied by a proof that never touches the
mutated file at all. Reproduced before fixing: retargeting the shipped GREEN
entry's proof from `test_check_doc_narrative.py` to `test_bom_guard_detection.py`
changed nothing, and the entry still reported verified. The RED direction never
had this hole, because a proof that ignores the mutation stays green and is
refused as "the clause is not load-bearing".

So a GREEN entry now declares a `reach_replacement` and its `reach_expect`: a
SECOND mutation of the SAME clause, required to REDDEN the same proof, executed
through `verify_mutation` so its red is read through the diagnostic gate rather
than on exit status. The shipped entry declares `path = p` — dropping the `b/`
stripping every scanned diff header goes through — and the run then scans
NOTHING, which is what the declared diagnostic reads. The same retarget now
fails, naming the reach verdict.

The gate runs LAST of the three: run first it would refuse before the status and
vacuity gates were read and neither could be witnessed failing alone (#685), and
the sandbox is reset between a claim's two proof runs for the reason it is reset
between mutations. It has its own disarm proof, and the two synthetic claim
sandboxes are now real git repositories so `reset_sandbox` has a baseline;
`_lib_with` shares the baseline registry, since a copied module's own starts
empty.

Also from that round:

- The record no longer counts the mutation-outcome claims in the pinned
  proposal-3 scan. A third of the same shape sits in the same result set
  (`test_a_verdict_BEYOND_A_SHORT_PAGE_is_still_found`), and which side of the
  line a sentence falls on is a judgement, so an exact count is a figure the
  next reader re-derives differently — the failure this record is about.
- The calibration paragraph no longer restates the post-review-verdict outcome
  as a dated witnessing. It points at the `CLAIMS` entry that executes it, which
  is the form the rewritten shell comment beside it demands.
- The comment in `check-doc-narrative.py` claimed a universal ("reddens no
  test") while one file is executed. It now names that file, so the quote binds
  an outcome no wider than what is checked.
- Proposal 4 from the issue is dispositioned explicitly: rejected as a rule
  here, on the issue's own argument that an exhortation does not fire at the
  moment of least slack.
- `docs/README.md`'s task-signal parenthetical now names the `CLAIMS`
  population; the file was owned by another slot when this branch started.

Cost re-measured 2026-09-05, three baseline/branch pairs: the `CLAIMS` half adds
31.7-43.6%, up from the 12.7-16.6% measured before the gate existed. The old
figure is retired rather than scaled — growing the population invalidates the
measurement that described it.

Refs #881

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
The claim the shipped GREEN entry binds — what removing the `+++ /dev/null` arm
does — was written twice: in `check-doc-narrative.py`, where the entry binds it,
and again in `test_a_DELETED_doc_is_not_reported_as_added_content`'s docstring,
where nothing does. That is the copy-of-an-outcome shape this rule forbids, in a
site class the rule names, found while reading the proof for the residual below.
The docstring now points at the manifest entry and keeps its rationale (a
deletion yields no `+` lines either way), which is the half the carve-out
protects.

The residual paragraph is also made exact rather than general. The reach
mutation proves the proof depends on the clause through the `b/` stripping every
scanned header goes through, not through the `/dev/null` arm itself, so in
general such a green cannot separate "no test feeds that input" from "the arm
changes nothing". For this entry it can, by reading the proof: the deleted-doc
test deletes a tracked file, and a deletion diff under the flags `run_diff` pins
carries a `+++ /dev/null` header — probed rather than reasoned.

Refs #881

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
The residual paragraph called it "a stronger mutation", which is a judgement
about size; what the mechanism requires is a second mutation of the same clause,
declared and required to redden the proof. One vocabulary across the rule field,
the manifest and the body, so a reader does not have to decide whether two
descriptions are the same thing.

Refs #881

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
Line-wrap only; the declared quote and the mutated clause are untouched.

Refs #881

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
The `Claim` docstring motivated the GREEN direction by enumerating three corpus
sites and restating what each asserts. The middle restatement said
`scripts/check-doc-narrative.py` "says removing its `/dev/null` arm reddens no
test" — the universal a9341d841 removed from that file when it narrowed the
comment to the scope the harness actually executes
("`test_check_doc_narrative.py` stays green with this arm removed"). The
docstring was written before that narrowing and kept re-asserting the wider
claim, attributed to a file that no longer makes it: `git grep -F "reddens no
test"` returned exactly one hit, the line asserting it. That is #881's own
defect #2 reproduced inside the fix.

Restating an outcome is what makes it drift, so the enumeration now names the
three sites and the mutation each describes, states the shape they share, and
says why the outcome wording is not repeated. The only two copies of that
outcome left in the tree are the site comment and the `CLAIMS` quote bound to
it, which is the binding by construction. Both other members were re-checked
today and hold: `.gitea/workflows/review-verdict.yml:2411` and
`scripts/tests/hook_fire_isolation.py:82`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
`verify_claim`'s docstring and the record's `mechanics` both said the relevance
gate must run LAST or the status and vacuity gates could never be witnessed
failing alone. Executed at ff65e5e7c: moving the `reset_sandbox` + reach
`verify_mutation` + `if not reach.ok:` block ahead of both earlier gates and
changing nothing else, then running `pytest scripts/tests/test_mutation_harness.py
-p no:randomly -k "GREEN_EXIT_STATUS or GREEN_VACUITY or GREEN_RELEVANCE or
UNKNOWN_outcome"` gives 4 passed. Two of those four assert the reasons the LATER
gates produce ("exited 1", "NOTHING PASSED"), so with the relevance gate first
both earlier gates were still read and still witnessed refusing alone. It cannot
hold, and the branch already said why one line away: the reach mutation injects a
failing test, so it reddens in every fixture except the relevance one, which is
what `_inert_claim_sandbox`'s own docstring states. What the order actually buys
is cost - the relevance gate is the only one of the three that costs a second run
of the proof - and that is what both sites now say. #881's own defect shape,
inside the record that establishes the rule against it.

Second, the record twice gave line-wrapping as the reason a sentence was
paraphrased rather than quoted. The branch's own first `CLAIMS` entry quotes a
sentence that spans a comment line break, embedding the `# ` continuation, and
the harness resolves it exactly once - so a wrapped sentence is quotable by this
very mechanism. The real reason at the calibration site is the replacement
itself: that sentence is not in the tree any more, measured 2026-09-05 by a
fixed-string search over `git ls-files`, which returns no file. At the second
site the referent (`docs/defect-shapes-773.md` section 4) is present and
quotable, so the causal clause is dropped and only the paraphrase marker stays.

refs #881

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
fix(881): the replacement clause asserted an absolute I had not measured
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 6s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 20s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 17s
PR Gates / decisions lifecycle (pull_request) Successful in 23s
PR Gates / Docs update reminder (pull_request) Successful in 14s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 11s
Review verdict / Set review-verdict status (pull_request_target) Successful in 37s
review-verdict/h10 Review-verdict: MERGEABLE @ 0f8e73a (base: main)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m47s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m3s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m41s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m47s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
0f8e73ab05
The previous commit replaced "the order is load-bearing" with "and that is the
whole of what the order buys". What was measured is narrower: with the relevance
gate moved first, the three gates are each still witnessed refusing alone. That
does not establish the order buys nothing else - the reset placement is a second
candidate, unmeasured either way - so the absolute is gone from both sites and
what stays is the cost reason, which is readable from the control flow.

refs #881

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
Author
Owner

Review-verdict: MERGEABLE @ 0f8e73a

Orchestrated-session workflow: six cold review rounds across the build and the resume (correctness Opus high in its own worktree, conformance sonnet high), every blocking finding measured and answered by a fix commit the PR body lists from git show; final round nits only. Full Python suite 1580 passed / 3 skipped on the pushed head; catalog byte-stable. Cross-family review: not required (routine risk class; scripts/tests + docs). Verdict on the pushed head; patch unchanged since review.

Review-verdict: MERGEABLE @ 0f8e73a Orchestrated-session workflow: six cold review rounds across the build and the resume (correctness Opus high in its own worktree, conformance sonnet high), every blocking finding measured and answered by a fix commit the PR body lists from git show; final round nits only. Full Python suite 1580 passed / 3 skipped on the pushed head; catalog byte-stable. Cross-family review: not required (routine risk class; scripts/tests + docs). Verdict on the pushed head; patch unchanged since review.
timothy merged commit 8575c02ad2 into main 2026-09-05 08:40:34 +02:00
timothy deleted branch 881-mutation-claim-prose-scope 2026-09-05 08:40:36 +02:00
Sign in to join this conversation.