The previous wording said the unshare pass aborts the clone "before the copy's
own exit status is ever consulted", which describes neither version. Unmutated,
the status is consulted immediately after the copy and fires first, so the
unshare pass is never reached; mutated, there is no check left to consult at
either point. As written a reader could take it for a claim that
unshare_mutable_paths runs before the torn-clone condition inside
hardlink_clone_into, which is the kind of ordering this file is otherwise
careful to state exactly (see the reader-marker ordering proof it sits under).
Names the mutation instead: deleting the exit-status check does not change the
outcome, because the unshare pass aborts the clone in its place. The mechanism
is unchanged and still holds — `cp -al` over a mode-000 source creates the
destination preserving mode 000 before failing, and `find | xargs` over that
returns 1 under pipefail, which _unshare_files propagates.
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.
hardlink_clone_into's torn-clone detection is a four-term condition, and a
mutation sweep found two of the four unpinned: removing either
`[ "$cp_rc" -eq 0 ]` (issue #5) or `[ "$i_before" != missing ]` left all five
suites green. They were unpinned for the same reason — they mask each other.
A source that vanishes mid-clone reads as `missing` at both ends AND fails
`cp -al`, so with both terms present either one catches it and neither is
individually necessary.
Isolating them needs a state each term alone can see:
8c cp reports failure over a tree that is in fact whole. Neither inference
sees anything — 0 entries short, one unchanged inode — so the exit
status is the only witness. Forced with the PATH stub 8a/8b already
use, on the consumer's own top-level clone.
8d both identity reads fail while the copy succeeds. `_dir_inode` folds
every stat failure into the string `missing`, so two failed reads
compare equal TO EACH OTHER; without the sentinel term the tree is
published on the strength of two errors. Stubs `stat` narrowly — only
the `%i` reads of this clone's own source — because taking the source
away would fail `cp -al` too and pin 8c's property over again.
The sweep also found `_unshare_files`'s xargs status unpinned, which is the
guard that stops a staging tree whose dep-info files still point at the
SOURCE's inodes from being renamed into place — not a tear, so all four
clone checks pass it, and exactly the silent cross-branch stale-reuse the
scheme exists to prevent. Scenario 10 pins it by refusing the dep-info
unshares and asserting the clone discards rather than publishes.
assert_tear gains an `unreadable` identity expectation, and its `same` case
now demands a READ identity rather than two equal strings — `missing` equals
`missing`, which is the exact confusion 8d exists to pin.
Each of the five pinning scenarios was run in isolation against each
mutation; the result is a clean diagonal, so every scenario fails only for
its own term.
Closes#5