docs(readme): correct the scenario census and the #5 citation

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.
This commit is contained in:
2026-08-24 14:02:10 -05:00
parent bd60b430e0
commit eb7878b822
2 changed files with 39 additions and 11 deletions
+27 -10
View File
@@ -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