From eb7878b82282eb89837e2fa7baedba4e93327d5f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 14:02:10 -0500 Subject: [PATCH] docs(readme): correct the scenario census and the #5 citation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of #9 found the methodology section falsified by that PR and owned by nobody — the scoping that fenced it off was wrong, #8 never touches these paragraphs. Three fixes: * The PATH-stub paragraph named 8a and 8b as the seed suite's stubs. It is now four scenarios on two commands: 8a/8b/8c stub `cp` at the clone, 10 stubs it one level down at the per-file unshare, and 8d stubs `stat` — a mechanism the paragraph did not mention at all, and the only way to make an identity that could not be READ the sole witness. * The assert-which-guard-fired paragraph cited issue #5 as a live example of a surviving mutation. #5 is the issue this PR closes, so a reader following that citation landed on "removing it leaves every suite green", which is no longer true. Scenario 9's own sentence stands — the matrix confirms it survives every mutant — so it now says WHY it survives (an unreadable source leaves the staging dir at mode 000, and the unshare pass aborts the clone before the copy's exit status is consulted) instead of citing a closed issue. * Added the mutual-masking hazard the sweep turned up, since it is the general lesson rather than a fact about two particular terms: two guards that can each catch the same fault make each other unnecessary, so no fixture built around that fault pins either one. Also names scenario 8d for what it is in its own comment — a regression guard on a defensive term, not a reproduction of a reachable state. Every route to the state it constructs is closed off (a rotation hands the witness to 8a, a genuinely absent source hands it to 8c), which is the reason it is worth pinning rather than a reason to doubt it. --- README.md | 37 +++++++++++++++++++++-------- scripts/seed-target-dir-selftest.sh | 13 +++++++++- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 941eee6..a7a21ec 100644 --- a/README.md +++ b/README.md @@ -381,13 +381,20 @@ key and asserts only what must be true whichever of them wins the rename. which places the interference inside the window rather than hoping it lands there. `prune-cache-selftest.sh` scenario 12 stubs `du`, so the pass's own measurement publishes a reader marker strictly between its check and its -unlink; `seed-target-dir-selftest.sh` scenarios 8a and 8b stub `cp`, so the -consumer's own clone is what rotates the snapshot underneath it, or what -loses a subtree of its own source, strictly inside the identity window. 8a -does start a second real process — the actual `publish-snapshot.sh` — but the -stub is what fixes where its swap lands; the concurrency is incidental to the -determinism. Every stub asserts that it fired, because a scenario whose -interference silently did not happen passes for the wrong reason. +unlink. `seed-target-dir-selftest.sh` uses the shape four times over, on two +different commands. Scenarios 8a, 8b and 8c stub `cp`, so the consumer's own +clone is what rotates the snapshot underneath it, loses a subtree of its own +source, or reports a failure over a tree that is in fact whole — each strictly +inside the window that clone's checks cover; scenario 10 stubs `cp` one level +down instead, refusing the per-file copies that unshare the mutable paths. +Scenario 8d stubs `stat`, because the check it pins fires on an identity that +could not be READ rather than on one that changed, and the only way to make +that the sole witness is to fail the identity reads while the copy between +them succeeds. 8a also starts a second real process — the actual +`publish-snapshot.sh` — but the stub is what fixes where its swap lands; the +concurrency is incidental to the determinism. Every stub asserts that it +fired, because a scenario whose interference silently did not happen passes +for the wrong reason. **A synthetic stand-in for the other side, where that artefact *is* the contract.** `publish-snapshot-selftest.sh` scenarios 6 to 8 hold a @@ -404,9 +411,19 @@ does it any more. Where more than one guard could catch a fault, a scenario should assert *which* one did — otherwise deleting the guard under test leaves the suite -green because a sibling fires in its place. Scenarios 8a and 8b of the seed -suite do; scenario 9 of the same suite does not yet, which is why a mutation -survives it (issue #5). +green because a sibling fires in its place. Scenarios 8a to 8d and 10 of the +seed suite do, and each is reddened by exactly one mutation of the clone's +checks. Scenario 9 does not, and a mutation still survives it: with its source +unreadable, `cp -al` leaves the staging directory at mode `000`, so the +unshare pass aborts the clone before the copy's own exit status is ever +consulted, and the assertion is satisfied down a path it was not written for. + +That is the standing hazard here, and it is not hypothetical. Two guards that +can each catch the same fault mask each other, so **neither** is individually +necessary and no fixture built around that fault can pin either one — which is +how both `[ "$cp_rc" -eq 0 ]` and `[ "$i_before" != missing ]` sat unpinned +(issue #5) while looking well covered. Isolating a check means constructing +the state only it can see, not the state that trips several at once. The action YAML holds no logic beyond wiring; everything testable lives in `scripts/`. A composite action needs `shell: bash` on every `run:` step, and diff --git a/scripts/seed-target-dir-selftest.sh b/scripts/seed-target-dir-selftest.sh index a7f026e..dbf85f5 100755 --- a/scripts/seed-target-dir-selftest.sh +++ b/scripts/seed-target-dir-selftest.sh @@ -52,7 +52,9 @@ # identity read is reported as the string `missing`, so two of them # compare equal to each other; without the term that rejects the # sentinel, a clone whose source could not be identified at either end -# is published on the strength of two errors. +# is published on the strength of two errors. A regression guard on a +# defensive term rather than a reproduction of a reachable state — see +# the scenario's own comment. # All four scenarios force their interleaving rather than racing for it, # and each asserts WHICH check caught the tear, so none stays green if # the check it exercises is removed. @@ -486,6 +488,15 @@ echo "=== 8d: an identity that could not be read at either end ===" # would differ from `missing` and the identity COMPARISON would become the # witness instead — 8a's property, not this one. The assertion that the stub # fired is therefore a count rather than a flag. +# +# So be clear about what this scenario is. It is NOT a reproduction of a state +# a CI job reaches: every route to it is closed off — a rotation hands the +# witness to 8a, and a source that is genuinely gone fails `cp -al` and hands +# it to 8c. It is a REGRESSION GUARD ON A DEFENSIVE TERM, and the thing it +# defends against is a sentinel comparing equal to itself, which is a property +# of the code rather than of the filesystem. That is worth pinning precisely +# because nothing else can reach it: a term no fixture exercises is the one a +# refactor drops without argument. IDENT=$(cache_key feat/identity-unreadable) IDENT_BASE=$(cache_key release/3) make_wide_tree "$root/snapshot-$IDENT_BASE" identgen 4