Skip to content

docs(adr-0005): §2 needs a mutation aimed at the guard's input - #117

Merged
dlovell merged 4 commits into
mainfrom
docs/adr-0005-mutation-pair
Aug 6, 2026
Merged

docs(adr-0005): §2 needs a mutation aimed at the guard's input#117
dlovell merged 4 commits into
mainfrom
docs/adr-0005-mutation-pair

Conversation

@dlovell

@dlovell dlovell commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Found by the scoped audit of the fail-open extraction class.

The evidence is named cases, not a statistic (the ADR text says why: an
earlier draft of this PR claimed "12 of 19", then "16 of 24", and the command
it published to reproduce that returned 3 — the population spans open branches
and moves hourly, and the round marker is a floor anyway, so the amendment
deliberately quotes no count). The named cases: no fail-open in this repo has
been found by the guard's own author — every one came from a reviewer or an
auditor. A [[ ]] dispatch arm invisible to a strict-shape parser (#96), COPY
coverage satisfied by a comment (#97), a pin anchor restated rather than read
(#95), a workflow omitting itself from its own trigger paths (#109), and —
in the PR that existed to fix this class — five successive rounds of
comment- and spelling-blind parses (#110).

Why that is structural, not a lapse. "Break the invariant" aims the
mutation at the thing the author is already thinking about. It reliably
confirms the hole they just closed and reliably misses the parsing assumption
they did not know they had made. The first three cases predate this ADR, so
their authors never ran §2 at all — that is the weaker half of the case. The
stronger half is #93's and #110's guards, which DID carry recorded §2 runs and
grew fail-opens anyway: a single mutation aimed at the invariant is not enough
even when it is performed.

The amendment

§2 now asks for a pair:

  1. Form-only — reformat the parsed source without changing meaning (wrap a
    line, requote, swap YAML flow for block, add a comment mentioning the
    identifier). Must stay green and the assertion count must not fall. A
    silent drop is the fail-open; that is what an evaporating mapfile looks
    like.
  2. Semantic, in a form you did not write — comment the call out rather
    than deleting it; set the key in block form rather than flow. Must go
    red.

Cost: two extra runs per new or materially-changed guard. Against that, these
would have caught the post-ADR fail-opens above, each of which instead cost a
review round.

§3 gains one sentence: the rung is a property of an assertion, not of a
suite.
test-volume-chown-guard.sh was rung 2 on the chown semantics and
rung 3 on the driver line, and the derived half vouched for the restated one.

CLAUDE.md's Conventions bullet, which restates §2, is updated in the same PR
per the ADR convention.

Explicitly not proposed

No new rung, and no new annotation kind for coverage or wiring. Coverage is
already rung-2-able — §3's own rung-2 example (parsing a Dockerfile's COPYs
rather than listing them) is set-derivation, and #101/#109 fixed the
coverage holes by climbing the existing ladder. Wiring is a fact to state in
the invariant, as #110 does, not a guard kind; folding it into
test:/tool:/ci:/structural/unguarded would mix the guard-kind axis
with the fact axis.

Docs only. tests/run-all 28 suites / exit 0; pre-commit run --all-files
clean.

🤖 Generated with Claude Code

dlovell and others added 4 commits August 6, 2026 12:06
Of 19 `Verified (ADR-0005` blocks across main and the open branches, 12 carry
a later-round marker — 63% of this repo's mutation records document a hole a
reviewer or auditor found, not the author.

That is structural, not a lapse. "Break the invariant" aims the mutation at
the thing the author is already thinking about, so the §2 run confirms the
hole they just closed and misses the parsing assumption they did not know they
had made. Every input-shape hole this repo has shipped since ADR-0005 — a
`[[ ]]` dispatch arm, a YAML block spelling, a comment matched as code — was
found by someone else, after §2 passed.

§2 now asks for a pair: a form-only mutation that must stay green WITH NO DROP
IN ASSERTION COUNT (a silent drop is what an evaporating extraction looks
like), and a semantic mutation expressed in a form you did not write —
comment it out rather than delete it — which must go red.

§3 gains one sentence: the rung is a property of an assertion, not of a suite.
test-volume-chown-guard.sh was rung 2 on the chown semantics and rung 3 on the
driver line, and the derived half vouched for the restated one.

Explicitly not proposed: a new rung, or a new annotation kind for coverage or
wiring. Coverage is already rung-2-able — §3's own example is set-derivation.
Wiring is a fact to state in the invariant, not a guard kind.

Found by the scoped audit of the fail-open extraction class.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d it

Two problems with the figure this amendment rests on, both mine.

I took 19/12 from the audit and never re-ran it before writing it into an ADR
— the same unverified-propagation that put a false claim into #112 and had to
be caught in review.

Re-running it now gives 24 blocks, and the cited command reports 12 of them as
later-round when the true number is 16. The command grepped
`review round|audit round|second|third`, and #110 went on to need a FOURTH and
FIFTH round, which that pattern does not match. So the ADR shipped a
measurement command that undercounts by three — a guard-adjacent measurement
missing cases, which is precisely the defect the amendment is about.

Now 24/16 (67%), counted on `round`, which is a clean discriminator: no
first-time record uses the word, so it needs no ordinal list to keep current.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review took the measurement apart three ways, and all three hold.

The claimed scope ("main and the open branches") did not give 24/16 — two
further open branches carry ten Verified blocks each, so the honest figure
under that scope was different again. The command published as re-runnable
returns 3, because the commit titled "correct the command that produced it"
dropped the branch loop the number depended on. And `round` is a floor, not an
exact marker: records like "two mutations, both previously green" document an
externally-found hole without the word.

An ADR arguing for measurement rigor should not ship a statistic that cannot
be reproduced, so it no longer ships one. The argument now rests on named,
stable cases — #96's invisible `[[ ]]` arm, #97's comment-satisfied COPY
coverage, #95's restated pin anchor, a workflow omitting itself, and the five
rounds inside the PR that existed to fix this class. Those are checkable and
do not decay.

Also from review:
- A carve-out the rule needed: where the invariant IS the byte form (the
  BASE_IMAGE pin's four encodings), a form-only mutation correctly goes red,
  so record that rather than running the form-only half. As written the rule
  gave the next pin-guard author a mandatory step that cannot be satisfied.
- Assertion counts are only comparable within a paired run on one machine;
  suites skipping absent zsh/fish report different absolute totals elsewhere.
- CLAUDE.md no longer embeds a decaying percentage.
- Both amendments now use ADR-0002's `## Amendment (date)` + Status style, and
  the §3 sentence moved out of the heading position into its own amendment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I removed a statistic that could not be re-derived and replaced it with a
universal that its own examples falsify. Same defect, restated in prose.

"Every fail-open since ADR-0005 LANDED was found AFTER the author's §2 run"
is wrong for three of the five cases it then lists: test-completions-sync.sh
(143bbf7), test-image-fingerprint.sh (fc2ca02) and test-nix-base-pin.sh
(343632a) all predate dd18207, and none carried a mutation record at
dd18207^ — their authors never ran §2 because §2 did not exist.

The surviving claim is narrower and stronger: no fail-open here has been
found by the guard's own author, and #93's and #110's guards DID carry
recorded §2 runs and grew fail-opens anyway. One mutation aimed at the
invariant is insufficient even when performed.

Also: the paragraph quoted its own prior numbers inverted — "19 of 12" and
"24 of 16", hits exceeding totals — in the passage whose subject is quoting
your own numbers accurately. Now 12 of 19 and 16 of 24.

Promoting the first amendment to `##` split the Decision section, orphaning
`### 3. Derive over restate`. Moved after Alternatives considered, matching
ADR-0002's placement, with a pointer under §2.

CLAUDE.md's restatement had dropped "landed" (garden-pathing the sentence),
dropped "or auditor" though most finds were the auditor's, and dropped the
"say which invariant makes it so" clause that fences the byte-form carve-out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dlovell
dlovell force-pushed the docs/adr-0005-mutation-pair branch from 2bd222e to 355dda8 Compare August 6, 2026 12:07
@dlovell
dlovell merged commit cc3f699 into main Aug 6, 2026
2 checks passed
@dlovell
dlovell deleted the docs/adr-0005-mutation-pair branch August 6, 2026 12:08
dlovell added a commit that referenced this pull request Aug 6, 2026
…ch-commit history (#122)

Three commits, one story: what an audit is required to leave behind, and
what the auditor is able to read.

## 1. `810dbad` — ADR-0006 amendment: committing an audit report is
optional

Reverses the "audits get a baseline" requirement. Closure is untouched —
every surviving shape is still filed, landed, or recorded as an accepted
`unguarded:` invariant — but the report itself is no longer mandatory.

Why: the report is the second copy and the weaker one. A finding's
durable form is an issue, a landed guard, or an `unguarded:` invariant,
and all three live where the next reader already looks; a committed
report restates that in prose nothing keeps true. It is the
restate-over-derive shape ADR-0005 rejects, applied to the auditor's own
output. A mandatory artifact also taxes the run that was already easiest
to skip — the failure mode is not a missing file, it is a skipped audit.

The cost is accepted explicitly rather than waved off: with no
guaranteed prior report there is no guaranteed baseline, so measurements
may not form a time series and a checked-and-dropped list may be
re-litigated. Recorded in `unguarded:` form with a revisit trigger.
Existing reports and the directory stay.

## 2. `934e999` — the auditor reads branch commits; the vocabulary is
derived, not named

**Branch history as an input.** `main` is squash-merged, so a hunk fixed
three times on a branch lands as one clean commit and the fix-the-fix
chain is unreachable from `git log`. Both audit agents excluded that
history — `pr-reviewer` reviews the net `base...head` diff and
deliberately anchors its history probe to `origin/main`, and the auditor
is repo-scoped — so nobody read it.

This PR's own subject demonstrates the loss. #90, which created these
agents, contains:

```
1254335 fix(agents): repair the log command; drop the ADR-0005 hard dependency
ab9e77d fix(agents): anchor the repeat-offender log to origin/main; tidy audit
```

Two corrections to the same three lines of prose. On `main` that is one
commit, `ba15520`. `gh pr view <n> --json commits` still returns the
pre-squash sequence after merge, so this needs no pre-merge window.
Scoped to PRs the other probes already implicated, so it stays one API
call per candidate rather than per PR in the window. The squash fact is
written as a check, not an assertion, so a change of merge strategy is
noticed instead of assumed.

Also adds "a set of merged PRs" to the scope list, making a
retrospective over recent reviews a first-class scope.

**Vocabulary drift.** #100 (`04e9eee`) typed the invariant ledger into
the ADR-0005 vocabulary and touched `CLAUDE.md` only.
`structural-auditor.md` reads that ledger and still named `(—)` in two
places — a marker with zero remaining instances — so an agent following
the instruction searched for a token that no longer exists and would
have skipped every accepted-risk invariant in the repo. Silent
under-coverage, in the agent whose job is finding unguarded couplings.

Fixed by removing the copy rather than guarding it: the cross-section
bullet names the categories and defers to the ledger's own header for
the markers. A legacy untyped citation is now itself a finding, matching
"type it when you touch it".

**`pr-reviewer`** gains a note that the `origin/main` anchor also puts
the branch's own commits out of scope, and that this is a handoff to the
auditor. Without it the exclusion reads as an oversight and the next
editor removes the anchor, restoring the self-trigger `ab9e77de` fixed.

## 3. `0d35e4c` — stop enumerating the audit reports by name

The amendment named the two files in `docs/audits/` as "the existing
reports". #112 commits a third under the now-optional rule, so that list
goes stale on merge — a second copy of a directory listing, which is the
shape the amendment itself argues against. De-enumerated in all three
places with the reason stated inline.

## Verification

- `pre-commit run --files` on all three files: clean. The
`check-gitignore-agents` probe passes, confirming the agent files can
actually be staged (ADR-0003).
- The `gh pr view` command was **run as written** before being embedded.
The two commands this file has shipped previously were both wrong on
first landing; embedding a third unverified one would have repeated the
exact failure this PR documents.
- No test covers `.claude/agents/`, which is the point of the issue
below.

## Related

- #121 — the finding this work produced, filed rather than fixed here:
`.claude/agents/*.md` ships shell commands and ledger vocabulary with no
guard, and both halves have now broken once. This PR fixes the two
symptoms; the mechanism is open. Deliberately **not** `Closes` — and
#121 argues accept-with-a-reason is a legitimate disposal.
- Merge-tested against all seven open PRs. Clean against `main`, #112,
#113, #110, #109, #108.
- **#117** conflicts with this branch only (it is clean against `main`):
it rewrites the "Guards follow ADR-0005" bullet while `810dbad` extends
the "A structural audit…" bullet directly beneath it. Adjacent lines,
independent content — whoever merges second keeps both.
- **#81** already conflicts with `main` independently of this branch,
but note it edits the *same sentence* `810dbad` does: it replaces
"ADR-0004 is reserved by #81" with the real entry while this adds
ADR-0006. Both additions must survive the resolution; taking either side
wholesale drops an ADR from the index.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_011KsnnuPRJW5sUPppm3RSRo

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: t <t@t>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant