Removing [ "$cp_rc" -eq 0 ] (issue #5) or[ "$i_before" != missing ] left
all five suites green. This PR adds a scenario for each, plus a third for a
guard the wider sweep turned up in unshare_mutable_paths.
This is the finding behind #5, and it is why the sweep was worth doing rather
than fixing cp_rc alone.
_dir_inode prints the string missing when stat fails. A source that
really vanishes mid-clone therefore reads missing at both ends — so i_before = i_after is true — and also fails cp -al. With both terms
present, either one catches that case, so neither is individually
necessary and no fixture built around a disappearing source can isolate
either. Each term needed a state only it can see.
Mutation matrix
Every mutation was applied to a scratch copy of cache-lib.sh outside the
worktree; scripts/cache-lib.sh is untouched by this PR. Each of the five
pinning scenarios was then run in isolation — the other four excised, so
an early abort cannot hide a later scenario's colour. The result is a clean
diagonal:
mutation (guard removed)
8a
8b
8c
8d
10
(none — control)
GREEN
GREEN
GREEN
GREEN
GREEN
[ "$cp_rc" -eq 0 ]
GREEN
GREEN
RED
GREEN
GREEN
[ "$i_before" != missing ]
GREEN
GREEN
GREEN
RED
GREEN
[ "$i_before" = "$i_after" ]
RED
GREEN
GREEN
GREEN
GREEN
[ "$n_after" -eq "$n_before" ]
GREEN
RED
GREEN
GREEN
GREEN
_unshare_files xargs status
GREEN
GREEN
GREEN
GREEN
RED
unshare_subtree's cp -a status
GREEN
GREEN
GREEN
GREEN
GREEN
Red transitions, verbatim:
[cp_rc -> 8c] ASSERTION FAILED: no torn read was reported at all (the failure was caught by the copy's exit status alone)
[imissing -> 8d] ASSERTION FAILED: no torn read was reported at all (the unreadable identity was caught by the sentinel check alone)
[ieq -> 8a] ASSERTION FAILED: no torn read was reported at all (the substitution was caught by the source's identity alone)
[neq -> 8b] ASSERTION FAILED: no torn read was reported at all (the silent truncation was caught by the entry count alone)
[unshare_files_rc -> 10] ASSERTION FAILED: the seed reported success over a staging tree that still aliased its source
The new scenarios
8c — a copy that reports failure over a tree that looks whole. The exit
status is the only witness the tool volunteers; the other two are inferences
with blind spots. The entry count is find | wc -l over two trees, so a
failure below a directory neither walk could enumerate moves it not at all,
and a lost entry against a gained one cancels out. A cp -a that links every
entry and still fails — it could not preserve a directory's ownership, say —
is invisible to both. Forced with the PATH stub 8a/8b already use, on the
consumer's own top-level clone: the tree really is copied, then the failure is
reported, once. The report reads cp rc=1, 249/249 entries, source inode N -> N
— both inferences saw nothing, so deleting the status check leaves nothing to
report at all.
8d — an identity that could not be read at either end. The sentinel-aliasing
case: two failed identity reads compare equal to each other, so without this
term a clone whose source could not be identified is renamed into place having
proven nothing. It cannot be forced by removing the source (that fails cp -al
too, and would pin 8c's property over again), so the stub goes on stat, and
narrowly: only the %i reads of this clone's own source, leaving the reader
markers' %Y reads alone. Both reads of the attempt must fail — if only
one did, the survivor would differ from missing and the identity comparison
would become the witness, which is 8a's property. The stub-fired assertion is
therefore a count (-eq 2), not a flag. Report: cp rc=0, 249/249 entries, source inode missing -> missing.
Worth naming what this scenario is, because it is not the same kind of thing as
8a to 8c. Those reproduce states a job reaches. 8d does not — every route to its
state is closed off, since a rotation hands the witness to 8a and a genuinely
absent source fails cp -al and hands it to 8c. It is a regression guard on a
defensive term, and what it defends against is a sentinel comparing equal to
itself: a property of the code, not of the filesystem. That is the argument for
pinning it rather than against — a term no fixture exercises is precisely the
one a refactor drops without anyone arguing about it.
10 — a staging tree that cannot be privately owned is never published. Not a
tear at all: a tree whose dep-info files still point at the source's inodes
has every entry present and an unmoved source, so all four clone checks
pass it. Publishing it wires two branches onto one set of fingerprints, which
is the silent cross-branch stale-artifact reuse documented at length in cache-lib.sh. The only thing between that tree and DST is _unshare_files's status being propagated, and that status was unpinned.
Forced by refusing exactly the cp -p -- <f> <f>.unshare.<pid> copies of .d
files, which leaves the directory unshares (cp -a) and the clone (cp -al)
untouched so the failure reaching the clone is unambiguous.
Sweep record
Scope, stated plainly so the next reader knows what this does and does not
cover. Seventeen mutants, each applied to a scratch copy of cache-lib.sh
outside the worktree: the four terms of the torn-clone condition, and thirteen
more across hardlink_clone_into, unshare_mutable_paths, unshare_subtree, _unshare_files, strip_cargo_locks and live_reader_count. It is a
guard-by-guard sweep of the clone path, not an exhaustive mutation run over
the library — cache_key, the disk-accounting helpers, wait_for_readers's
grace arithmetic and everything in prune-cache.sh / publish-snapshot.sh
were not swept.
It took three passes to get this list right, which is itself the useful
datum. The first pass missed reader_lock_acquire; a review sweep found it. The
second pass, written to be complete, still missed three propagations inside the
two functions this table claims to have swept guard-by-guard; a second review
sweep of twelve mutants disjoint from mine found those. Read the rows below as
what has been checked, not as what exists.
Unpinned, filed rather than fixed: reader_lock_acquire (#10)
Missed by the first pass of this sweep and found in review. Delete reader_lock_acquire "$parent" "$src_name" "$tag" (cache-lib.sh:399) and all
five suites pass at the identical 129 assertions. That is the consumer's
half of the seed-vs-republish interlock, one line above the four terms this PR
pins, directly under the comment calling its ordering load-bearing.
It is exercised but not asserted, which is the interesting part. In scenario 8a
the publisher genuinely blocks on the marker — readers: waiting for 1 in-flight clone(s) then drained after 1s in logPub. Remove the line and
those log lines vanish, the publisher never waits, and 8a passes identically.
Filed as #10 rather than fixed here because the cheap fix costs a property
worth more than it. Asserting on the publisher's log inside 8a would work — the
wait is deterministic there by construction — but 8a currently pins exactly one
property and is reddened by exactly one mutant, and that one-scenario-one-mutant
diagonal is the thing that makes this suite's coverage legible. The interlock
deserves its own scenario, next to the publisher-side half already in publish-snapshot-selftest.sh scenarios 6 to 8. #10 carries that reasoning and
the AC.
Unpinned because nothing can drive them: three more propagations
Found by a second review sweep, verified here. All five suites stay green at the
identical 129 assertions with any of these removed:
mutant
unshare_subtree "$d" || { … return 1; } → || true
cache-lib.sh:203-206
the .rustc_info.json propagation → || true
cache-lib.sh:212-215
loop-top rm -rf "$tmp" deleted
cache-lib.sh:395
These are the write_cache_lock class, not the reader_lock_acquire class — no fixture can construct the condition each one guards, so the mutants prove
nothing about coverage beyond that fact:
The subtree-loop propagation needs unshare_subtree to fail. It runs
in several scenarios, but scenario 10's stub matches cp -p -- only and unshare_subtree calls cp -a, so nothing makes it fail.
The .rustc_info.json propagation needs that copy to fail. The file
exists only in hardlink-clone-selftest.sh's real cargo build (which does
assert it gets unshared); no fixture makes copying it fail.
The loop-top rm -rf is only load-bearing when a .stage-<tag> already
exists at loop entry — otherwise it is a no-op. Retries do not drive it,
because a torn attempt already removes $tmp before looping; it would take a
stale staging directory under the same tag, which no fixture creates and
which per-job-per-run tags make unreachable in production.
The honest summary: unshare_mutable_paths has three failure propagations,
one pinned (by scenario 10) and two undriven. Recorded on #10 rather than
given scenarios, for the same reason write_cache_lock was — each needs a
fixture that can reach the failure at all before mutating it means anything.
Unpinned, near-equivalent: unshare_subtree's cp -a status
cache-lib.sh:133. Swallow the status and the following rm -rf "$d" + mv -T "$tmp" "$d" still fails, because $tmp was removed on the copy's
failure path — so the function reports failure anyway and the mutant is close
to equivalent rather than a live hole. A scenario would pin "unshare_subtree
reports failure", which scenario 10 already covers through the sibling path.
Unpinned, judged not worth pinning: [ ! -d "$src" ]
cache-lib.sh:383. A fast path, not a distinct correctness property. Removing
it changes the message and costs four attempts plus 3s of sleep, but the
function still returns 2 via the retry ceiling — which is exactly what scenario
9 asserts.
Caught only by hanging: the CACHE_CLONE_ATTEMPTS ceiling
cache-lib.sh:414. Scenario 9 does detect its removal, but by spinning forever
on a permanently unreadable source rather than by failing — a regression a CI
timeout would surface as an infrastructure flake. Not changed here; fixing it
means giving scenario 9 a bounded run.
No coverage, and the mutant cannot show it: write_cache_lock
Removing the write_cache_lock "$tmp" "$lock_id" call leaves every suite green,
but as an equivalent mutant: no selftest passes seed-target-dir.sh its
optional 6th argument, so LOCK_ID is always empty and the function
early-returns at [ -n "$id" ] || return 0. The .ci-lock-* files the prune
and publish suites use are hand-written fixtures, not its output. The
documented property — a seeded directory carries its lock the instant it
appears under its final name, so a concurrent prune cannot evict it — is
untested end to end. Recorded in #10; it needs a fixture that passes a lock id
before any mutation of it means anything.
A wrong-reason pass found in scenario 9
Worth recording, because it is the mechanism the README already suspected.
Scenario 9's unreadable-source half stays green under the cp_rc mutation not because it exercises that guard but because cp -al leaves the staging
directory at mode 000, so _unshare_files's find fails and unshare_mutable_paths aborts the clone first:
::error::unshare_mutable_paths: failed to unshare dep-info files under .../.stage-jobU
::error::clone: could not privately own the mutable paths of ...
::error::seed: could not clone base snapshot ... consistently
The assertion it satisfies (refusing to build against a partial cache) is
reached down a different path entirely. cp_rc and _unshare_files's status
were mutually masking there in exactly the way cp_rc and the missing
sentinel were in the clone condition. Scenarios 8c and 10 now pin both
independently, so the masking no longer hides either — but scenario 9 itself
still does not assert which guard fired, and is unchanged by this PR.
assert_tear
Identity fields now match ([0-9]+|missing), so a report whose inode field
is neither is a loud parse failure rather than a silently-compared string.
New unreadable expectation: both reads resolved to missing.
The same case now demands a read identity, not merely two equal
strings — missing equals missing, which is the exact confusion 8d exists
to pin, and 8a/8b would otherwise have accepted it.
README.md — the seed-target-dir-selftest.sh suite-table row, and the two
methodology paragraphs this PR falsifies: the PATH-stub census (now four
scenarios over two commands — 8a/8b/8c on cp, 10 on cp one level down,
8d on stat, a mechanism that paragraph did not mention) and the
assert-which-guard-fired paragraph, which cited #5 — the issue this PR
closes — as a live example of a surviving mutation. Scenario 9's own sentence
stands, since the matrix confirms it survives every mutant; it now says why
it survives instead of citing a closed issue. The mutual-masking hazard is
added there too, as the general lesson rather than a fact about two terms.
(These paragraphs were initially scoped to a sibling PR and left alone. That
scoping was wrong — the sibling's README diff never touches them — so they
would have been left falsified by nobody.)
scripts/cache-lib.sh is not modified, and no production logic changed.
Every guard this sweep mutated behaved correctly; the findings are all about
what the suite does and does not pin. That is a statement about the fourteen
guards in the table above, not about the library as a whole.
Test plan
bash scripts/selftest.sh # full, incl. compiler-backed: all 5 suites pass, 129 assertions
shellcheck scripts/seed-target-dir-selftest.sh # only the pre-existing SC1091 on the source line
Every scenario in the matrix was run in isolation — the other four excised
from a scratch copy — because the suite aborts on first failure, so a
whole-suite run cannot tell you the colour of anything downstream of the first
red. Re-run in full after the doc changes: still 129 assertions.
Fixtures run in mktemp -d scratch only; nothing touched the live CI cache
volume. No app run applies — this repo's deliverable is the suite itself.
## Summary
`hardlink_clone_into`'s torn-clone detection is a **four**-term condition, and a
mutation sweep of all four found **two** of them unpinned, not one:
```bash
if [ "$cp_rc" -eq 0 ] && [ "$i_before" != missing ] && [ "$i_before" = "$i_after" ] \
&& [ "$n_after" -eq "$n_before" ]; then
```
Removing `[ "$cp_rc" -eq 0 ]` (issue #5) **or** `[ "$i_before" != missing ]` left
all five suites green. This PR adds a scenario for each, plus a third for a
guard the wider sweep turned up in `unshare_mutable_paths`.
Closes #5.
## Why both were unpinned: they mask each other
This is the finding behind #5, and it is why the sweep was worth doing rather
than fixing `cp_rc` alone.
`_dir_inode` prints the string `missing` when `stat` fails. A source that
really vanishes mid-clone therefore reads `missing` at **both** ends — so
`i_before = i_after` is *true* — and also fails `cp -al`. With both terms
present, either one catches that case, so **neither is individually
necessary** and no fixture built around a disappearing source can isolate
either. Each term needed a state only it can see.
## Mutation matrix
Every mutation was applied to a **scratch copy** of `cache-lib.sh` outside the
worktree; `scripts/cache-lib.sh` is untouched by this PR. Each of the five
pinning scenarios was then run **in isolation** — the other four excised, so
an early abort cannot hide a later scenario's colour. The result is a clean
diagonal:
| mutation (guard removed) | 8a | 8b | 8c | 8d | 10 |
|---|---|---|---|---|---|
| *(none — control)* | GREEN | GREEN | GREEN | GREEN | GREEN |
| `[ "$cp_rc" -eq 0 ]` | GREEN | GREEN | **RED** | GREEN | GREEN |
| `[ "$i_before" != missing ]` | GREEN | GREEN | GREEN | **RED** | GREEN |
| `[ "$i_before" = "$i_after" ]` | **RED** | GREEN | GREEN | GREEN | GREEN |
| `[ "$n_after" -eq "$n_before" ]` | GREEN | **RED** | GREEN | GREEN | GREEN |
| `_unshare_files` xargs status | GREEN | GREEN | GREEN | GREEN | **RED** |
| `unshare_subtree`'s `cp -a` status | GREEN | GREEN | GREEN | GREEN | GREEN |
Red transitions, verbatim:
```
[cp_rc -> 8c] ASSERTION FAILED: no torn read was reported at all (the failure was caught by the copy's exit status alone)
[imissing -> 8d] ASSERTION FAILED: no torn read was reported at all (the unreadable identity was caught by the sentinel check alone)
[ieq -> 8a] ASSERTION FAILED: no torn read was reported at all (the substitution was caught by the source's identity alone)
[neq -> 8b] ASSERTION FAILED: no torn read was reported at all (the silent truncation was caught by the entry count alone)
[unshare_files_rc -> 10] ASSERTION FAILED: the seed reported success over a staging tree that still aliased its source
```
## The new scenarios
**8c — a copy that reports failure over a tree that looks whole.** The exit
status is the only witness the tool volunteers; the other two are inferences
with blind spots. The entry count is `find | wc -l` over two trees, so a
failure below a directory *neither* walk could enumerate moves it not at all,
and a lost entry against a gained one cancels out. A `cp -a` that links every
entry and still fails — it could not preserve a directory's ownership, say —
is invisible to both. Forced with the PATH stub 8a/8b already use, on the
consumer's own top-level clone: the tree really is copied, then the failure is
reported, once. The report reads `cp rc=1, 249/249 entries, source inode N -> N`
— both inferences saw nothing, so deleting the status check leaves nothing to
report at all.
**8d — an identity that could not be read at either end.** The sentinel-aliasing
case: two *failed* identity reads compare equal to each other, so without this
term a clone whose source could not be identified is renamed into place having
proven nothing. It cannot be forced by removing the source (that fails `cp -al`
too, and would pin 8c's property over again), so the stub goes on `stat`, and
narrowly: only the `%i` reads of this clone's own source, leaving the reader
markers' `%Y` reads alone. **Both** reads of the attempt must fail — if only
one did, the survivor would differ from `missing` and the identity *comparison*
would become the witness, which is 8a's property. The stub-fired assertion is
therefore a count (`-eq 2`), not a flag. Report:
`cp rc=0, 249/249 entries, source inode missing -> missing`.
Worth naming what this scenario is, because it is not the same kind of thing as
8a to 8c. Those reproduce states a job reaches. 8d does not — every route to its
state is closed off, since a rotation hands the witness to 8a and a genuinely
absent source fails `cp -al` and hands it to 8c. It is a **regression guard on a
defensive term**, and what it defends against is a sentinel comparing equal to
itself: a property of the code, not of the filesystem. That is the argument for
pinning it rather than against — a term no fixture exercises is precisely the
one a refactor drops without anyone arguing about it.
**10 — a staging tree that cannot be privately owned is never published.** Not a
tear at all: a tree whose dep-info files still point at the *source's* inodes
has every entry present and an unmoved source, so **all four** clone checks
pass it. Publishing it wires two branches onto one set of fingerprints, which
is the silent cross-branch stale-artifact reuse documented at length in
`cache-lib.sh`. The only thing between that tree and `DST` is
`_unshare_files`'s status being propagated, and that status was unpinned.
Forced by refusing exactly the `cp -p -- <f> <f>.unshare.<pid>` copies of `.d`
files, which leaves the directory unshares (`cp -a`) and the clone (`cp -al`)
untouched so the failure reaching the clone is unambiguous.
## Sweep record
**Scope, stated plainly so the next reader knows what this does and does not
cover.** Seventeen mutants, each applied to a scratch copy of `cache-lib.sh`
outside the worktree: the four terms of the torn-clone condition, and thirteen
more across `hardlink_clone_into`, `unshare_mutable_paths`, `unshare_subtree`,
`_unshare_files`, `strip_cargo_locks` and `live_reader_count`. It is a
guard-by-guard sweep of the clone path, **not** an exhaustive mutation run over
the library — `cache_key`, the disk-accounting helpers, `wait_for_readers`'s
grace arithmetic and everything in `prune-cache.sh` / `publish-snapshot.sh`
were not swept.
It took **three passes to get this list right**, which is itself the useful
datum. The first pass missed `reader_lock_acquire`; a review sweep found it. The
second pass, written to be complete, still missed three propagations inside the
two functions this table claims to have swept guard-by-guard; a second review
sweep of twelve mutants disjoint from mine found those. Read the rows below as
what has been checked, not as what exists.
| guard | verdict |
|---|---|
| `[ "$cp_rc" -eq 0 ]` | was unpinned (#5) — **now pinned by 8c** |
| `[ "$i_before" != missing ]` | was unpinned — **now pinned by 8d** |
| `[ "$i_before" = "$i_after" ]` | pinned (8a) |
| `[ "$n_after" -eq "$n_before" ]` | pinned (8b) |
| `_unshare_files` xargs status (the `.d` pass) | was unpinned — **now pinned by 10** |
| `unshare_mutable_paths`'s subtree-loop propagation | unpinned, undriven — see below |
| `unshare_mutable_paths`'s `.rustc_info.json` propagation | unpinned, undriven — see below |
| loop-top `rm -rf "$tmp"` | unpinned, undriven — see below |
| `if ! unshare_mutable_paths` | pinned (3 suites red) |
| `reader_lock_release` | pinned (2 suites red) |
| `strip_cargo_locks` body | pinned (2 suites red) |
| `live_reader_count` staleness check | pinned (publish suite) |
| `mv -T "$tmp" "$dst"` status | pinned (8's leftover check) |
| **`reader_lock_acquire`** | **unpinned — filed as #10, not fixed here** |
| `unshare_subtree`'s `cp -a` status | unpinned, near-equivalent — see below |
| `[ ! -d "$src" ]` early return | unpinned, judged not worth pinning — see below |
| `CACHE_CLONE_ATTEMPTS` ceiling | caught, but by hanging — see below |
| `write_cache_lock` call | equivalent mutant, no coverage — see below |
### Unpinned, filed rather than fixed: `reader_lock_acquire` (#10)
Missed by the first pass of this sweep and found in review. Delete
`reader_lock_acquire "$parent" "$src_name" "$tag"` (`cache-lib.sh:399`) and all
five suites pass at the **identical 129 assertions**. That is the consumer's
half of the seed-vs-republish interlock, one line above the four terms this PR
pins, directly under the comment calling its ordering load-bearing.
It is exercised but not asserted, which is the interesting part. In scenario 8a
the publisher genuinely blocks on the marker — `readers: waiting for 1
in-flight clone(s)` then `drained after 1s` in `logPub`. Remove the line and
those log lines vanish, the publisher never waits, and 8a passes identically.
**Filed as #10 rather than fixed here** because the cheap fix costs a property
worth more than it. Asserting on the publisher's log inside 8a would work — the
wait is deterministic there by construction — but 8a currently pins exactly one
property and is reddened by exactly one mutant, and that one-scenario-one-mutant
diagonal is the thing that makes this suite's coverage legible. The interlock
deserves its own scenario, next to the publisher-side half already in
`publish-snapshot-selftest.sh` scenarios 6 to 8. #10 carries that reasoning and
the AC.
### Unpinned because nothing can drive them: three more propagations
Found by a second review sweep, verified here. All five suites stay green at the
identical 129 assertions with any of these removed:
| mutant | |
|---|---|
| `unshare_subtree "$d" \|\| { … return 1; }` → `\|\| true` | `cache-lib.sh:203-206` |
| the `.rustc_info.json` propagation → `\|\| true` | `cache-lib.sh:212-215` |
| loop-top `rm -rf "$tmp"` deleted | `cache-lib.sh:395` |
These are the `write_cache_lock` class, not the `reader_lock_acquire` class —
**no fixture can construct the condition each one guards**, so the mutants prove
nothing about coverage beyond that fact:
- The **subtree-loop** propagation needs `unshare_subtree` to *fail*. It runs
in several scenarios, but scenario 10's stub matches `cp -p --` only and
`unshare_subtree` calls `cp -a`, so nothing makes it fail.
- The **`.rustc_info.json`** propagation needs that copy to fail. The file
exists only in `hardlink-clone-selftest.sh`'s real cargo build (which does
assert it gets unshared); no fixture makes copying it fail.
- The **loop-top `rm -rf`** is only load-bearing when a `.stage-<tag>` already
exists at loop entry — otherwise it is a no-op. Retries do not drive it,
because a torn attempt already removes `$tmp` before looping; it would take a
stale staging directory under the *same* tag, which no fixture creates and
which per-job-per-run tags make unreachable in production.
The honest summary: **`unshare_mutable_paths` has three failure propagations,
one pinned (by scenario 10) and two undriven.** Recorded on #10 rather than
given scenarios, for the same reason `write_cache_lock` was — each needs a
fixture that can reach the failure at all before mutating it means anything.
### Unpinned, near-equivalent: `unshare_subtree`'s `cp -a` status
`cache-lib.sh:133`. Swallow the status and the following `rm -rf "$d"` +
`mv -T "$tmp" "$d"` still fails, because `$tmp` was removed on the copy's
failure path — so the function reports failure anyway and the mutant is close
to equivalent rather than a live hole. A scenario would pin "`unshare_subtree`
reports failure", which scenario 10 already covers through the sibling path.
### Unpinned, judged not worth pinning: `[ ! -d "$src" ]`
`cache-lib.sh:383`. A fast path, not a distinct correctness property. Removing
it changes the message and costs four attempts plus 3s of `sleep`, but the
function still returns 2 via the retry ceiling — which is exactly what scenario
9 asserts.
### Caught only by hanging: the `CACHE_CLONE_ATTEMPTS` ceiling
`cache-lib.sh:414`. Scenario 9 does detect its removal, but by spinning forever
on a permanently unreadable source rather than by failing — a regression a CI
timeout would surface as an infrastructure flake. Not changed here; fixing it
means giving scenario 9 a bounded run.
### No coverage, and the mutant cannot show it: `write_cache_lock`
Removing the `write_cache_lock "$tmp" "$lock_id"` call leaves every suite green,
but as an **equivalent** mutant: no selftest passes `seed-target-dir.sh` its
optional 6th argument, so `LOCK_ID` is always empty and the function
early-returns at `[ -n "$id" ] || return 0`. The `.ci-lock-*` files the prune
and publish suites use are hand-written fixtures, not its output. The
documented property — a seeded directory carries its lock the instant it
appears under its final name, so a concurrent prune cannot evict it — is
untested end to end. Recorded in #10; it needs a fixture that passes a lock id
before any mutation of it means anything.
## A wrong-reason pass found in scenario 9
Worth recording, because it is the mechanism the README already suspected.
Scenario 9's unreadable-source half stays green under the `cp_rc` mutation
**not** because it exercises that guard but because `cp -al` leaves the staging
directory at mode `000`, so `_unshare_files`'s `find` fails and
`unshare_mutable_paths` aborts the clone first:
```
::error::unshare_mutable_paths: failed to unshare dep-info files under .../.stage-jobU
::error::clone: could not privately own the mutable paths of ...
::error::seed: could not clone base snapshot ... consistently
```
The assertion it satisfies (`refusing to build against a partial cache`) is
reached down a different path entirely. `cp_rc` and `_unshare_files`'s status
were mutually masking there in exactly the way `cp_rc` and the `missing`
sentinel were in the clone condition. Scenarios 8c and 10 now pin both
independently, so the masking no longer hides either — but scenario 9 itself
still does not assert *which* guard fired, and is unchanged by this PR.
## `assert_tear`
- Identity fields now match `([0-9]+|missing)`, so a report whose inode field
is neither is a loud parse failure rather than a silently-compared string.
- New `unreadable` expectation: both reads resolved to `missing`.
- The `same` case now demands a **read** identity, not merely two equal
strings — `missing` equals `missing`, which is the exact confusion 8d exists
to pin, and 8a/8b would otherwise have accepted it.
## Files affected
- `scripts/seed-target-dir-selftest.sh` — scenarios 8c, 8d, 10; `assert_tear`
extended; header scenario census updated. **30 → 49 assertions.**
- `README.md` — the `seed-target-dir-selftest.sh` suite-table row, and the two
methodology paragraphs this PR falsifies: the `PATH`-stub census (now four
scenarios over two commands — 8a/8b/8c on `cp`, 10 on `cp` one level down,
8d on `stat`, a mechanism that paragraph did not mention) and the
assert-which-guard-fired paragraph, which cited **#5** — the issue this PR
closes — as a live example of a surviving mutation. Scenario 9's own sentence
stands, since the matrix confirms it survives every mutant; it now says *why*
it survives instead of citing a closed issue. The mutual-masking hazard is
added there too, as the general lesson rather than a fact about two terms.
(These paragraphs were initially scoped to a sibling PR and left alone. That
scoping was wrong — the sibling's README diff never touches them — so they
would have been left falsified by nobody.)
`scripts/cache-lib.sh` is **not** modified, and no production logic changed.
Every guard this sweep mutated behaved correctly; the findings are all about
what the suite does and does not pin. That is a statement about the fourteen
guards in the table above, not about the library as a whole.
## Test plan
```
bash scripts/selftest.sh # full, incl. compiler-backed: all 5 suites pass, 129 assertions
shellcheck scripts/seed-target-dir-selftest.sh # only the pre-existing SC1091 on the source line
```
Every scenario in the matrix was run **in isolation** — the other four excised
from a scratch copy — because the suite aborts on first failure, so a
whole-suite run cannot tell you the colour of anything downstream of the first
red. Re-run in full after the doc changes: still 129 assertions.
Fixtures run in `mktemp -d` scratch only; nothing touched the live CI cache
volume. No app run applies — this repo's deliverable is the suite itself.
claude
added the bug label 2026-08-24 18:28:24 +00:00
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
The pinning work itself is the best-verified thing in this repo right now, and I did not take the PR body's word for any of it — I rebuilt the mutation matrix independently, with checked single-occurrence replacements on a scratch copy of cache-lib.sh and each scenario mechanically extracted and run alone, so no early abort can hide a later scenario's colour. The diagonal is real. What holds this back is not the scenarios: it is two defects in the PR's record — a sweep report that presents a complete "unpinned and not fixed" list while missing an unpinned guard in the same function, and a README census this diff falsifies that no PR updates.
Independence: reproduced, clean diagonal
Seven mutants × six scenarios, each scenario run in isolation (preamble + one scenario block, nothing else):
mutant
8a
8b
8c
8d
9
10
(control)
GREEN
GREEN
GREEN
GREEN
GREEN
GREEN
[ "$cp_rc" -eq 0 ] removed
GREEN
GREEN
RED
GREEN
GREEN
GREEN
[ "$i_before" != missing ] removed
GREEN
GREEN
GREEN
RED
GREEN
GREEN
[ "$i_before" = "$i_after" ] removed
RED
GREEN
GREEN
GREEN
GREEN
GREEN
[ "$n_after" -eq "$n_before" ] removed
GREEN
RED
GREEN
GREEN
GREEN
GREEN
_unshare_files xargs status dropped
GREEN
GREEN
GREEN
GREEN
GREEN
RED
unshare_subtree's cp -a status swallowed
GREEN
GREEN
GREEN
GREEN
GREEN
GREEN
Each mutation reddens exactly one scenario and each scenario is reddened by exactly one mutation, with the failure messages the PR quotes. The unshare_subtree row confirms the PR's own honest note — that mutant is masked by the rm -rf "$tmp" on the failure path, so it is near-equivalent rather than merely unpinned. [ ! -d "$src" ] removed: all fixture suites green, as claimed.
#5's AC, per item: a scenario that reddens on cp_rc removal — 8c, scripts/seed-target-dir-selftest.sh:404, red/green demonstrated above, not asserted. Fails for its own reason — the row above, and 8c stays green under all four sibling mutants. Loud when the stub does not fire — :447 (cp_reported_failure), and 8d's is a count (:512, -eq 2) precisely because one surviving read would hand the witness to 8a's comparison. Comment states the property pinned — :405-421. All four pass.
Neither new fixture satisfies its property by construction: 8c's same/same on both inferences is what forces the exit status to be the only witness, and deleting the status check leaves no report at all rather than a differently-shaped one. 8d's stub is correctly narrowed to %i reads of this clone's own source (_dir_inode at cache-lib.sh:327 is the sole stat -c '%i' caller on this path; the reader markers' %Y reads pass through). Scenario 10's link-count assertion on $alias_dep is a real post-condition, not a tautology.
Full bash scripts/selftest.sh (including both compiler-backed suites): 5 suites, 129 assertions, green. shellcheck scripts/seed-target-dir-selftest.sh: only the pre-existing SC1091; whole-scripts/ finding count is 11 on both ec3ab6c and this head — no drift. cache-lib.sh, cargo-cache/ and cargo-cache-publish/ untouched.
Blocking
1. The sweep missed an unpinned guard in the function it swept — reader_lock_acquire.
Delete the reader_lock_acquire "$parent" "$src_name" "$tag" line from hardlink_clone_into (cache-lib.sh:399) and all five suites pass, at the identical 129 assertions. That is the consumer's half of the seed-vs-republish interlock, one line above the four terms this PR pins, directly under the comment calling it the ordering proof — and it is live in the fixtures, not dormant: scenario 8a's publisher genuinely blocks on that marker (readers: waiting for 1 in-flight clone(s) of snapshot-dev-… / drained after 1s in logPub). The scenario keeps passing without it.
I found this with a five-mutant spot check outside the compound condition, which is what makes it a finding: the PR body affirmatively presents "Guards found unpinned and NOT fixed, with reasons" as the sweep's complete residue, and "every guard the sweep touched was found correct, only unpinned". Both read as stronger than the sweep supports. This does not require a new scenario — reporting it in that section (or filing it, per the issue-tracker rule) discharges it. But the list has to be accurate, because the list is what a future reader will trust instead of re-running the sweep.
2. The README methodology census this diff falsifies is updated by nobody.
README.md:405-409 currently reads: "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)." After this merge 8c, 8d and 10 also assert which guard fired, so the enumeration understates the suite, and the cited issue is the one this PR closes — a reader following it lands on "removing it leaves every suite green", which this PR makes false. README.md:384 has the smaller sibling: "scenarios 8a and 8b stub cp", now also 8c and 10, while 8d stubs stat — a shape the paragraph does not mention.
That section was rewritten two PRs ago under the commit title "census the three concurrency-scenario shapes honestly", which is exactly the claim this diff dents. The scoping premise that #8 owns it does not hold: #8's README diff adds a new Scratch names in a cache root section and does not touch these paragraphs. Scenario 9's own sentence stays true — my matrix confirms it survives every mutant — so this is a census-and-citation update, not a rewrite.
Nits (not blocking)
write_cache_lock is unreachable from every suite. Removing write_cache_lock "$tmp" "$lock_id" from hardlink_clone_into leaves all fixture suites green — but as an equivalent mutant: no seed call in any suite passes the optional 6th lock-id argument, so LOCK_ID is always empty and the function early-returns. The documented property ("the directory already carries the lock the instant it appears under its final name, so a concurrent prune cannot evict it") has no coverage at all. Worth a line in the sweep report even though the mutant itself proves nothing.
Say which kind of scenario 8d is.:479 models it as "a transient stat error over a source that is otherwise perfectly readable". For this term to be the sole witness, both reads of one attempt must fail while cp -al succeeds in between — a rotation gives you a numeric i_after and hands the witness to 8a, and a source that is really gone fails the copy. That makes 8d a regression guard for a defensive term against sentinel equality, which is valuable and worth keeping, rather than a reproduction of a condition a job hits. The PR body should name it as the former.
publish-snapshot-selftest.sh:44 ("scenarios 8a and 8b") is not stale — it scopes itself to the rotation-race half, which is still exactly 8a and 8b. No numbering collision: 10 is new.
VERDICT: REQUEST-CHANGES
The pinning work itself is the best-verified thing in this repo right now, and I did not take the PR body's word for any of it — I rebuilt the mutation matrix independently, with checked single-occurrence replacements on a scratch copy of `cache-lib.sh` and each scenario mechanically extracted and run **alone**, so no early abort can hide a later scenario's colour. The diagonal is real. What holds this back is not the scenarios: it is two defects in the PR's *record* — a sweep report that presents a complete "unpinned and not fixed" list while missing an unpinned guard in the same function, and a README census this diff falsifies that no PR updates.
## Independence: reproduced, clean diagonal
Seven mutants × six scenarios, each scenario run in isolation (preamble + one scenario block, nothing else):
| mutant | 8a | 8b | 8c | 8d | 9 | 10 |
|---|---|---|---|---|---|---|
| *(control)* | GREEN | GREEN | GREEN | GREEN | GREEN | GREEN |
| `[ "$cp_rc" -eq 0 ]` removed | GREEN | GREEN | **RED** | GREEN | GREEN | GREEN |
| `[ "$i_before" != missing ]` removed | GREEN | GREEN | GREEN | **RED** | GREEN | GREEN |
| `[ "$i_before" = "$i_after" ]` removed | **RED** | GREEN | GREEN | GREEN | GREEN | GREEN |
| `[ "$n_after" -eq "$n_before" ]` removed | GREEN | **RED** | GREEN | GREEN | GREEN | GREEN |
| `_unshare_files` xargs status dropped | GREEN | GREEN | GREEN | GREEN | GREEN | **RED** |
| `unshare_subtree`'s `cp -a` status swallowed | GREEN | GREEN | GREEN | GREEN | GREEN | GREEN |
Each mutation reddens exactly one scenario and each scenario is reddened by exactly one mutation, with the failure messages the PR quotes. The `unshare_subtree` row confirms the PR's own honest note — that mutant is masked by the `rm -rf "$tmp"` on the failure path, so it is near-equivalent rather than merely unpinned. `[ ! -d "$src" ]` removed: all fixture suites green, as claimed.
**#5's AC, per item:** a scenario that reddens on `cp_rc` removal — 8c, `scripts/seed-target-dir-selftest.sh:404`, red/green demonstrated above, not asserted. Fails for its own reason — the row above, and 8c stays green under all four sibling mutants. Loud when the stub does not fire — `:447` (`cp_reported_failure`), and 8d's is a **count** (`:512`, `-eq 2`) precisely because one surviving read would hand the witness to 8a's comparison. Comment states the property pinned — `:405-421`. All four pass.
Neither new fixture satisfies its property by construction: 8c's `same/same` on both inferences is what forces the exit status to be the only witness, and deleting the status check leaves *no report at all* rather than a differently-shaped one. 8d's stub is correctly narrowed to `%i` reads of this clone's own source (`_dir_inode` at `cache-lib.sh:327` is the sole `stat -c '%i'` caller on this path; the reader markers' `%Y` reads pass through). Scenario 10's link-count assertion on `$alias_dep` is a real post-condition, not a tautology.
Full `bash scripts/selftest.sh` (including both compiler-backed suites): 5 suites, **129 assertions**, green. `shellcheck scripts/seed-target-dir-selftest.sh`: only the pre-existing SC1091; whole-`scripts/` finding count is 11 on both `ec3ab6c` and this head — no drift. `cache-lib.sh`, `cargo-cache/` and `cargo-cache-publish/` untouched.
## Blocking
**1. The sweep missed an unpinned guard in the function it swept — `reader_lock_acquire`.**
Delete the `reader_lock_acquire "$parent" "$src_name" "$tag"` line from `hardlink_clone_into` (`cache-lib.sh:399`) and **all five suites pass, at the identical 129 assertions**. That is the consumer's half of the seed-vs-republish interlock, one line above the four terms this PR pins, directly under the comment calling it the ordering proof — and it is live in the fixtures, not dormant: scenario 8a's publisher genuinely blocks on that marker (`readers: waiting for 1 in-flight clone(s) of snapshot-dev-…` / `drained after 1s` in `logPub`). The scenario keeps passing without it.
I found this with a five-mutant spot check outside the compound condition, which is what makes it a finding: the PR body affirmatively presents "Guards found unpinned and NOT fixed, with reasons" as the sweep's complete residue, and "every guard the sweep touched was found correct, only unpinned". Both read as stronger than the sweep supports. This does **not** require a new scenario — reporting it in that section (or filing it, per the issue-tracker rule) discharges it. But the list has to be accurate, because the list is what a future reader will trust instead of re-running the sweep.
**2. The README methodology census this diff falsifies is updated by nobody.**
`README.md:405-409` currently reads: *"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)."* After this merge 8c, 8d and 10 also assert which guard fired, so the enumeration understates the suite, and the cited issue is the one this PR closes — a reader following it lands on "removing it leaves every suite green", which this PR makes false. `README.md:384` has the smaller sibling: *"scenarios 8a and 8b stub `cp`"*, now also 8c and 10, while 8d stubs `stat` — a shape the paragraph does not mention.
That section was rewritten two PRs ago under the commit title *"census the three concurrency-scenario shapes honestly"*, which is exactly the claim this diff dents. The scoping premise that #8 owns it does not hold: #8's README diff adds a new *Scratch names in a cache root* section and does not touch these paragraphs. Scenario 9's own sentence stays true — my matrix confirms it survives every mutant — so this is a census-and-citation update, not a rewrite.
## Nits (not blocking)
- **`write_cache_lock` is unreachable from every suite.** Removing `write_cache_lock "$tmp" "$lock_id"` from `hardlink_clone_into` leaves all fixture suites green — but as an *equivalent* mutant: no `seed` call in any suite passes the optional 6th `lock-id` argument, so `LOCK_ID` is always empty and the function early-returns. The documented property ("the directory already carries the lock the instant it appears under its final name, so a concurrent prune cannot evict it") has no coverage at all. Worth a line in the sweep report even though the mutant itself proves nothing.
- **Say which kind of scenario 8d is.** `:479` models it as "a transient stat error over a source that is otherwise perfectly readable". For this term to be the sole witness, *both* reads of one attempt must fail while `cp -al` succeeds in between — a rotation gives you a numeric `i_after` and hands the witness to 8a, and a source that is really gone fails the copy. That makes 8d a regression guard for a defensive term against sentinel equality, which is valuable and worth keeping, rather than a reproduction of a condition a job hits. The PR body should name it as the former.
- `publish-snapshot-selftest.sh:44` ("scenarios 8a and 8b") is **not** stale — it scopes itself to the rotation-race half, which is still exactly 8a and 8b. No numbering collision: 10 is new.
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.
claude-reviewer
approved these changes 2026-08-24 19:23:11 +00:00
Both blocking findings from the previous round are genuinely closed, and I re-verified each rather than reading the diff for intent. The sweep record now scopes itself honestly and carries reader_lock_acquire as a filed gap; the README census is corrected in both paragraphs and the new root-cause explanation is, in mechanism, empirically right. Approving.
One thing I will not do is certify what you asked me to certify.
The residue is not complete, and I would not write a sentence saying it is
I ran a second sweep — twelve mutants of my own, disjoint from the PR's twelve, all inside the stated scope. Nine reddened a suite. Three did not, and all three are inside hardlink_clone_into / unshare_mutable_paths, which the sweep record names as swept guard-by-guard:
the .rustc_info.json propagation → || true (cache-lib.sh:212-215)
all five suites green, 129 assertions
the loop-top rm -rf "$tmp" deleted (cache-lib.sh:395)
all five suites green, 129 assertions
The first is the sibling of the one scenario 10 pins. unshare_mutable_paths has three status propagations — the subtree loop, the *.d pass, the .rustc_info.json pass — and the residue table lists neither the first nor the third. Swallow the subtree one and a staging tree whose .fingerprint/ still aliases its source is published, which is the corruption the whole scheme exists to prevent; the .d sibling one line down is the thing this PR just spent a scenario pinning.
But the category matters, and it is not the reader_lock_acquire category. No fixture can make cp -a fail (scenario 10's stub matches -p -- only), no fixture creates a .rustc_info.json in a seeded tree at all, and no fixture leaves a stale .stage-<tag> for a clone of the same tag to trip over. All three are undriven paths: the mutant is green because nothing exercises the branch, exactly as the PR already documents for write_cache_lock. reader_lock_acquire was worse than this — exercised on every clone, load-bearing in 8a's own fixture, and still unasserted.
So: three rows for the residue table (or three lines on #10), same "no coverage" wording the write_cache_lock section already uses. Not a third review round, and not a scenario each — the honest statement is that unshare_mutable_paths's failure propagations have one of three pinned and the other two undriven.
For the record on what I could not close: a live_reader_count mutant ([ -e "$marker" ] || continue deleted) was still running when I posted. It is outside the stated scope and does not bear on the verdict.
Finding 1, verified
reader_lock_acquire re-checked from the new head: deleted, all five suites pass at the identical 129 assertions. The filed-rather-than-fixed reasoning holds up — asserting on the publisher's log inside 8a would work, but it would give 8a a second property and cost the one-scenario-one-mutant diagonal, which is this suite's most legible asset. #10 is the right home. The write_cache_lock characterisation is right too: no suite passes seed-target-dir.sh a 6th argument, LOCK_ID is empty, the function early-returns, and the mutant proves nothing — accurately described as equivalent rather than unpinned.
Finding 2, verified
:384 is accurate on all four uses of the stub shape: 8a/8b/8c stub cp on the top-level clone, 10 stubs cp at the per-file unshare, 8d stubs stat, and the stated reason stat is the lever is the correct one (the check fires on an identity that could not be read, so the reads must fail while the copy between them succeeds). :411's "8a to 8d and 10 … each reddened by exactly one mutation" matches my own independently-rebuilt diagonal exactly. The #5 citation no longer points at a claim this PR falsifies.
The scenario-9 explanation: mechanism right, one clause wrong
I checked this rather than reading it. Scenario 9 run in isolation under the cp_rc mutant fails down the unshare path, verbatim:
cp: cannot access '…/snapshot-feat-unreadable-…': Permission denied
::error::unshare_mutable_paths: failed to unshare dep-info files under …/.stage-jobUnread
::error::clone: could not privately own the mutable paths of …
::error::seed: could not clone base snapshot … consistently
and the mode-000 link in the chain is real, not a plausible story: cp -al over a mode-000 source creates the destination directory and preserves mode 000 before failing, and find … | xargs … over that directory returns 1 under pipefail, which is what _unshare_files propagates. Verified directly, outside the suite.
The clause that is wrong is "so the unshare pass aborts the clone before the copy's own exit status is ever consulted." That ordering does not exist in either version. Unmutated, the exit status is consulted immediately after the copy and fires first — the unshare pass is never reached (::warning::clone: attempt 1/2 … was torn (cp rc=1, 0/0 entries …)). Mutated, the check is gone, so there is nothing to consult before or after. The accurate sentence names the mutation:
Scenario 9 does not, and a mutation still survives it: with its source unreadable, cp -al leaves the staging directory at mode 000, so deleting the exit-status check does not change the outcome — the unshare pass aborts the clone instead, and the assertion is satisfied down a path it was not written for.
As written a reader can come away believing unshare_mutable_paths runs before the torn-clone condition inside hardlink_clone_into, which is the kind of ordering claim this file is otherwise careful about (cache-lib.sh:396-398). One clause, not a re-diagnosis — the root cause identified is correct.
The generalisation holds
"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" is sound, including the part a fixture cannot escape: when a fault trips two terms of one compound condition, assert_tear reads values rather than terms, so deleting either still yields a report and the scenario still passes. It applies to both named terms, with one asymmetry worth knowing if the paragraph is ever revisited — the masking partner actually demonstrated for cp_rc is _unshare_files's status in scenario 9, while for [ "$i_before" != missing ] the partner is cp_rc and the claim is that no natural fixture could have isolated it. Same principle, two different partners; the PR body says this plainly, the README compresses it. Fine as compressed.
Nits
The scenario-9 clause above. It is the one I would most want fixed before merge, and it is a one-line edit.
Three residue rows, per the section at the top.
8d's reframing is right and the file-level comment (:55-57) and scenario comment (:492-499) now agree with each other and with what the fixture does.
Re-verified this round
Full bash scripts/selftest.sh at eb7878b: 5 suites, 129 assertions, green. shellcheck scripts/seed-target-dir-selftest.sh: pre-existing SC1091 only. scripts/cache-lib.sh, cargo-cache/, cargo-cache-publish/ untouched; the only code change since bd60b43 is documentation. All mutation work on scratch copies outside the worktree; nothing ran against a live cache volume.
VERDICT: APPROVE-WITH-NITS
Both blocking findings from the previous round are genuinely closed, and I re-verified each rather than reading the diff for intent. The sweep record now scopes itself honestly and carries `reader_lock_acquire` as a filed gap; the README census is corrected in both paragraphs and the new root-cause explanation is, in mechanism, empirically right. Approving.
One thing I will not do is certify what you asked me to certify.
## The residue is not complete, and I would not write a sentence saying it is
I ran a second sweep — twelve mutants of my own, disjoint from the PR's twelve, all inside the stated scope. Nine reddened a suite. Three did not, and all three are inside `hardlink_clone_into` / `unshare_mutable_paths`, which the sweep record names as swept guard-by-guard:
| mutant | result |
|---|---|
| `unshare_subtree "$d" \|\| { echo…; return 1; }` → `\|\| true` (`cache-lib.sh:203-206`) | all five suites green, 129 assertions |
| the `.rustc_info.json` propagation → `\|\| true` (`cache-lib.sh:212-215`) | all five suites green, 129 assertions |
| the loop-top `rm -rf "$tmp"` deleted (`cache-lib.sh:395`) | all five suites green, 129 assertions |
The first is the sibling of the one scenario 10 pins. `unshare_mutable_paths` has **three** status propagations — the subtree loop, the `*.d` pass, the `.rustc_info.json` pass — and the residue table lists neither the first nor the third. Swallow the subtree one and a staging tree whose `.fingerprint/` still aliases its source is published, which is the corruption the whole scheme exists to prevent; the `.d` sibling one line down is the thing this PR just spent a scenario pinning.
**But the category matters, and it is not the `reader_lock_acquire` category.** No fixture can make `cp -a` fail (scenario 10's stub matches `-p --` only), no fixture creates a `.rustc_info.json` in a seeded tree at all, and no fixture leaves a stale `.stage-<tag>` for a clone of the same tag to trip over. All three are *undriven paths*: the mutant is green because nothing exercises the branch, exactly as the PR already documents for `write_cache_lock`. `reader_lock_acquire` was worse than this — exercised on every clone, load-bearing in 8a's own fixture, and still unasserted.
So: three rows for the residue table (or three lines on #10), same "no coverage" wording the `write_cache_lock` section already uses. Not a third review round, and not a scenario each — the honest statement is that `unshare_mutable_paths`'s failure propagations have one of three pinned and the other two undriven.
For the record on what I could not close: a `live_reader_count` mutant (`[ -e "$marker" ] || continue` deleted) was still running when I posted. It is outside the stated scope and does not bear on the verdict.
## Finding 1, verified
`reader_lock_acquire` re-checked from the new head: deleted, all five suites pass at the identical **129 assertions**. The filed-rather-than-fixed reasoning holds up — asserting on the publisher's log inside 8a would work, but it would give 8a a second property and cost the one-scenario-one-mutant diagonal, which is this suite's most legible asset. #10 is the right home. The `write_cache_lock` characterisation is right too: no suite passes `seed-target-dir.sh` a 6th argument, `LOCK_ID` is empty, the function early-returns, and the mutant proves nothing — accurately described as equivalent rather than unpinned.
## Finding 2, verified
`:384` is accurate on all four uses of the stub shape: 8a/8b/8c stub `cp` on the top-level clone, 10 stubs `cp` at the per-file unshare, 8d stubs `stat`, and the stated reason `stat` is the lever is the correct one (the check fires on an identity that could not be *read*, so the reads must fail while the copy between them succeeds). `:411`'s "8a to 8d and 10 … each reddened by exactly one mutation" matches my own independently-rebuilt diagonal exactly. The `#5` citation no longer points at a claim this PR falsifies.
### The scenario-9 explanation: mechanism right, one clause wrong
I checked this rather than reading it. Scenario 9 run in isolation under the `cp_rc` mutant fails down the unshare path, verbatim:
```
cp: cannot access '…/snapshot-feat-unreadable-…': Permission denied
::error::unshare_mutable_paths: failed to unshare dep-info files under …/.stage-jobUnread
::error::clone: could not privately own the mutable paths of …
::error::seed: could not clone base snapshot … consistently
```
and the mode-`000` link in the chain is real, not a plausible story: `cp -al` over a mode-000 source **creates the destination directory and preserves mode 000** before failing, and `find … | xargs …` over that directory returns 1 under `pipefail`, which is what `_unshare_files` propagates. Verified directly, outside the suite.
The clause that is wrong is *"so the unshare pass aborts the clone before the copy's own exit status is ever consulted."* That ordering does not exist in either version. Unmutated, the exit status is consulted immediately after the copy and fires first — the unshare pass is never reached (`::warning::clone: attempt 1/2 … was torn (cp rc=1, 0/0 entries …)`). Mutated, the check is gone, so there is nothing to consult before or after. The accurate sentence names the mutation:
> Scenario 9 does not, and a mutation still survives it: with its source unreadable, `cp -al` leaves the staging directory at mode `000`, so **deleting the exit-status check does not change the outcome — the unshare pass aborts the clone instead**, and the assertion is satisfied down a path it was not written for.
As written a reader can come away believing `unshare_mutable_paths` runs before the torn-clone condition inside `hardlink_clone_into`, which is the kind of ordering claim this file is otherwise careful about (`cache-lib.sh:396-398`). One clause, not a re-diagnosis — the root cause identified is correct.
### The generalisation holds
"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" is sound, including the part a fixture cannot escape: when a fault trips two terms of one compound condition, `assert_tear` reads values rather than terms, so deleting either still yields a report and the scenario still passes. It applies to both named terms, with one asymmetry worth knowing if the paragraph is ever revisited — the masking partner actually demonstrated for `cp_rc` is `_unshare_files`'s status in scenario 9, while for `[ "$i_before" != missing ]` the partner is `cp_rc` and the claim is that no natural fixture *could* have isolated it. Same principle, two different partners; the PR body says this plainly, the README compresses it. Fine as compressed.
## Nits
- The scenario-9 clause above. It is the one I would most want fixed before merge, and it is a one-line edit.
- Three residue rows, per the section at the top.
- 8d's reframing is right and the file-level comment (`:55-57`) and scenario comment (`:492-499`) now agree with each other and with what the fixture does.
## Re-verified this round
Full `bash scripts/selftest.sh` at `eb7878b`: 5 suites, 129 assertions, green. `shellcheck scripts/seed-target-dir-selftest.sh`: pre-existing SC1091 only. `scripts/cache-lib.sh`, `cargo-cache/`, `cargo-cache-publish/` untouched; the only code change since `bd60b43` is documentation. All mutation work on scratch copies outside the worktree; nothing ran against a live cache volume.
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.
claude
marked the pull request as ready for review 2026-08-24 19:49:11 +00:00
claude
merged commit a9e9190e5a into main2026-08-24 19:49:21 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
hardlink_clone_into's torn-clone detection is a four-term condition, and amutation sweep of all four found two of them unpinned, not one:
Removing
[ "$cp_rc" -eq 0 ](issue #5) or[ "$i_before" != missing ]leftall five suites green. This PR adds a scenario for each, plus a third for a
guard the wider sweep turned up in
unshare_mutable_paths.Closes #5.
Why both were unpinned: they mask each other
This is the finding behind #5, and it is why the sweep was worth doing rather
than fixing
cp_rcalone._dir_inodeprints the stringmissingwhenstatfails. A source thatreally vanishes mid-clone therefore reads
missingat both ends — soi_before = i_afteris true — and also failscp -al. With both termspresent, either one catches that case, so neither is individually
necessary and no fixture built around a disappearing source can isolate
either. Each term needed a state only it can see.
Mutation matrix
Every mutation was applied to a scratch copy of
cache-lib.shoutside theworktree;
scripts/cache-lib.shis untouched by this PR. Each of the fivepinning scenarios was then run in isolation — the other four excised, so
an early abort cannot hide a later scenario's colour. The result is a clean
diagonal:
[ "$cp_rc" -eq 0 ][ "$i_before" != missing ][ "$i_before" = "$i_after" ][ "$n_after" -eq "$n_before" ]_unshare_filesxargs statusunshare_subtree'scp -astatusRed transitions, verbatim:
The new scenarios
8c — a copy that reports failure over a tree that looks whole. The exit
status is the only witness the tool volunteers; the other two are inferences
with blind spots. The entry count is
find | wc -lover two trees, so afailure below a directory neither walk could enumerate moves it not at all,
and a lost entry against a gained one cancels out. A
cp -athat links everyentry and still fails — it could not preserve a directory's ownership, say —
is invisible to both. Forced with the PATH stub 8a/8b already use, on the
consumer's own top-level clone: the tree really is copied, then the failure is
reported, once. The report reads
cp rc=1, 249/249 entries, source inode N -> N— both inferences saw nothing, so deleting the status check leaves nothing to
report at all.
8d — an identity that could not be read at either end. The sentinel-aliasing
case: two failed identity reads compare equal to each other, so without this
term a clone whose source could not be identified is renamed into place having
proven nothing. It cannot be forced by removing the source (that fails
cp -altoo, and would pin 8c's property over again), so the stub goes on
stat, andnarrowly: only the
%ireads of this clone's own source, leaving the readermarkers'
%Yreads alone. Both reads of the attempt must fail — if onlyone did, the survivor would differ from
missingand the identity comparisonwould become the witness, which is 8a's property. The stub-fired assertion is
therefore a count (
-eq 2), not a flag. Report:cp rc=0, 249/249 entries, source inode missing -> missing.Worth naming what this scenario is, because it is not the same kind of thing as
8a to 8c. Those reproduce states a job reaches. 8d does not — every route to its
state is closed off, since a rotation hands the witness to 8a and a genuinely
absent source fails
cp -aland hands it to 8c. It is a regression guard on adefensive term, and what it defends against is a sentinel comparing equal to
itself: a property of the code, not of the filesystem. That is the argument for
pinning it rather than against — a term no fixture exercises is precisely the
one a refactor drops without anyone arguing about it.
10 — a staging tree that cannot be privately owned is never published. Not a
tear at all: a tree whose dep-info files still point at the source's inodes
has every entry present and an unmoved source, so all four clone checks
pass it. Publishing it wires two branches onto one set of fingerprints, which
is the silent cross-branch stale-artifact reuse documented at length in
cache-lib.sh. The only thing between that tree andDSTis_unshare_files's status being propagated, and that status was unpinned.Forced by refusing exactly the
cp -p -- <f> <f>.unshare.<pid>copies of.dfiles, which leaves the directory unshares (
cp -a) and the clone (cp -al)untouched so the failure reaching the clone is unambiguous.
Sweep record
Scope, stated plainly so the next reader knows what this does and does not
cover. Seventeen mutants, each applied to a scratch copy of
cache-lib.shoutside the worktree: the four terms of the torn-clone condition, and thirteen
more across
hardlink_clone_into,unshare_mutable_paths,unshare_subtree,_unshare_files,strip_cargo_locksandlive_reader_count. It is aguard-by-guard sweep of the clone path, not an exhaustive mutation run over
the library —
cache_key, the disk-accounting helpers,wait_for_readers'sgrace arithmetic and everything in
prune-cache.sh/publish-snapshot.shwere not swept.
It took three passes to get this list right, which is itself the useful
datum. The first pass missed
reader_lock_acquire; a review sweep found it. Thesecond pass, written to be complete, still missed three propagations inside the
two functions this table claims to have swept guard-by-guard; a second review
sweep of twelve mutants disjoint from mine found those. Read the rows below as
what has been checked, not as what exists.
[ "$cp_rc" -eq 0 ][ "$i_before" != missing ][ "$i_before" = "$i_after" ][ "$n_after" -eq "$n_before" ]_unshare_filesxargs status (the.dpass)unshare_mutable_paths's subtree-loop propagationunshare_mutable_paths's.rustc_info.jsonpropagationrm -rf "$tmp"if ! unshare_mutable_pathsreader_lock_releasestrip_cargo_locksbodylive_reader_countstaleness checkmv -T "$tmp" "$dst"statusreader_lock_acquireunshare_subtree'scp -astatus[ ! -d "$src" ]early returnCACHE_CLONE_ATTEMPTSceilingwrite_cache_lockcallUnpinned, filed rather than fixed:
reader_lock_acquire(#10)Missed by the first pass of this sweep and found in review. Delete
reader_lock_acquire "$parent" "$src_name" "$tag"(cache-lib.sh:399) and allfive suites pass at the identical 129 assertions. That is the consumer's
half of the seed-vs-republish interlock, one line above the four terms this PR
pins, directly under the comment calling its ordering load-bearing.
It is exercised but not asserted, which is the interesting part. In scenario 8a
the publisher genuinely blocks on the marker —
readers: waiting for 1 in-flight clone(s)thendrained after 1sinlogPub. Remove the line andthose log lines vanish, the publisher never waits, and 8a passes identically.
Filed as #10 rather than fixed here because the cheap fix costs a property
worth more than it. Asserting on the publisher's log inside 8a would work — the
wait is deterministic there by construction — but 8a currently pins exactly one
property and is reddened by exactly one mutant, and that one-scenario-one-mutant
diagonal is the thing that makes this suite's coverage legible. The interlock
deserves its own scenario, next to the publisher-side half already in
publish-snapshot-selftest.shscenarios 6 to 8. #10 carries that reasoning andthe AC.
Unpinned because nothing can drive them: three more propagations
Found by a second review sweep, verified here. All five suites stay green at the
identical 129 assertions with any of these removed:
unshare_subtree "$d" || { … return 1; }→|| truecache-lib.sh:203-206.rustc_info.jsonpropagation →|| truecache-lib.sh:212-215rm -rf "$tmp"deletedcache-lib.sh:395These are the
write_cache_lockclass, not thereader_lock_acquireclass —no fixture can construct the condition each one guards, so the mutants prove
nothing about coverage beyond that fact:
unshare_subtreeto fail. It runsin several scenarios, but scenario 10's stub matches
cp -p --only andunshare_subtreecallscp -a, so nothing makes it fail..rustc_info.jsonpropagation needs that copy to fail. The fileexists only in
hardlink-clone-selftest.sh's real cargo build (which doesassert it gets unshared); no fixture makes copying it fail.
rm -rfis only load-bearing when a.stage-<tag>alreadyexists at loop entry — otherwise it is a no-op. Retries do not drive it,
because a torn attempt already removes
$tmpbefore looping; it would take astale staging directory under the same tag, which no fixture creates and
which per-job-per-run tags make unreachable in production.
The honest summary:
unshare_mutable_pathshas three failure propagations,one pinned (by scenario 10) and two undriven. Recorded on #10 rather than
given scenarios, for the same reason
write_cache_lockwas — each needs afixture that can reach the failure at all before mutating it means anything.
Unpinned, near-equivalent:
unshare_subtree'scp -astatuscache-lib.sh:133. Swallow the status and the followingrm -rf "$d"+mv -T "$tmp" "$d"still fails, because$tmpwas removed on the copy'sfailure path — so the function reports failure anyway and the mutant is close
to equivalent rather than a live hole. A scenario would pin "
unshare_subtreereports failure", which scenario 10 already covers through the sibling path.
Unpinned, judged not worth pinning:
[ ! -d "$src" ]cache-lib.sh:383. A fast path, not a distinct correctness property. Removingit changes the message and costs four attempts plus 3s of
sleep, but thefunction still returns 2 via the retry ceiling — which is exactly what scenario
9 asserts.
Caught only by hanging: the
CACHE_CLONE_ATTEMPTSceilingcache-lib.sh:414. Scenario 9 does detect its removal, but by spinning foreveron a permanently unreadable source rather than by failing — a regression a CI
timeout would surface as an infrastructure flake. Not changed here; fixing it
means giving scenario 9 a bounded run.
No coverage, and the mutant cannot show it:
write_cache_lockRemoving the
write_cache_lock "$tmp" "$lock_id"call leaves every suite green,but as an equivalent mutant: no selftest passes
seed-target-dir.shitsoptional 6th argument, so
LOCK_IDis always empty and the functionearly-returns at
[ -n "$id" ] || return 0. The.ci-lock-*files the pruneand publish suites use are hand-written fixtures, not its output. The
documented property — a seeded directory carries its lock the instant it
appears under its final name, so a concurrent prune cannot evict it — is
untested end to end. Recorded in #10; it needs a fixture that passes a lock id
before any mutation of it means anything.
A wrong-reason pass found in scenario 9
Worth recording, because it is the mechanism the README already suspected.
Scenario 9's unreadable-source half stays green under the
cp_rcmutationnot because it exercises that guard but because
cp -alleaves the stagingdirectory at mode
000, so_unshare_files'sfindfails andunshare_mutable_pathsaborts the clone first:The assertion it satisfies (
refusing to build against a partial cache) isreached down a different path entirely.
cp_rcand_unshare_files's statuswere mutually masking there in exactly the way
cp_rcand themissingsentinel were in the clone condition. Scenarios 8c and 10 now pin both
independently, so the masking no longer hides either — but scenario 9 itself
still does not assert which guard fired, and is unchanged by this PR.
assert_tear([0-9]+|missing), so a report whose inode fieldis neither is a loud parse failure rather than a silently-compared string.
unreadableexpectation: both reads resolved tomissing.samecase now demands a read identity, not merely two equalstrings —
missingequalsmissing, which is the exact confusion 8d existsto pin, and 8a/8b would otherwise have accepted it.
Files affected
scripts/seed-target-dir-selftest.sh— scenarios 8c, 8d, 10;assert_tearextended; header scenario census updated. 30 → 49 assertions.
README.md— theseed-target-dir-selftest.shsuite-table row, and the twomethodology paragraphs this PR falsifies: the
PATH-stub census (now fourscenarios over two commands — 8a/8b/8c on
cp, 10 oncpone level down,8d on
stat, a mechanism that paragraph did not mention) and theassert-which-guard-fired paragraph, which cited #5 — the issue this PR
closes — as a live example of a surviving mutation. Scenario 9's own sentence
stands, since the matrix confirms it survives every mutant; it now says why
it survives instead of citing a closed issue. The mutual-masking hazard is
added there too, as the general lesson rather than a fact about two terms.
(These paragraphs were initially scoped to a sibling PR and left alone. That
scoping was wrong — the sibling's README diff never touches them — so they
would have been left falsified by nobody.)
scripts/cache-lib.shis not modified, and no production logic changed.Every guard this sweep mutated behaved correctly; the findings are all about
what the suite does and does not pin. That is a statement about the fourteen
guards in the table above, not about the library as a whole.
Test plan
Every scenario in the matrix was run in isolation — the other four excised
from a scratch copy — because the suite aborts on first failure, so a
whole-suite run cannot tell you the colour of anything downstream of the first
red. Re-run in full after the doc changes: still 129 assertions.
Fixtures run in
mktemp -dscratch only; nothing touched the live CI cachevolume. No app run applies — this repo's deliverable is the suite itself.
VERDICT: REQUEST-CHANGES
The pinning work itself is the best-verified thing in this repo right now, and I did not take the PR body's word for any of it — I rebuilt the mutation matrix independently, with checked single-occurrence replacements on a scratch copy of
cache-lib.shand each scenario mechanically extracted and run alone, so no early abort can hide a later scenario's colour. The diagonal is real. What holds this back is not the scenarios: it is two defects in the PR's record — a sweep report that presents a complete "unpinned and not fixed" list while missing an unpinned guard in the same function, and a README census this diff falsifies that no PR updates.Independence: reproduced, clean diagonal
Seven mutants × six scenarios, each scenario run in isolation (preamble + one scenario block, nothing else):
[ "$cp_rc" -eq 0 ]removed[ "$i_before" != missing ]removed[ "$i_before" = "$i_after" ]removed[ "$n_after" -eq "$n_before" ]removed_unshare_filesxargs status droppedunshare_subtree'scp -astatus swallowedEach mutation reddens exactly one scenario and each scenario is reddened by exactly one mutation, with the failure messages the PR quotes. The
unshare_subtreerow confirms the PR's own honest note — that mutant is masked by therm -rf "$tmp"on the failure path, so it is near-equivalent rather than merely unpinned.[ ! -d "$src" ]removed: all fixture suites green, as claimed.#5's AC, per item: a scenario that reddens on
cp_rcremoval — 8c,scripts/seed-target-dir-selftest.sh:404, red/green demonstrated above, not asserted. Fails for its own reason — the row above, and 8c stays green under all four sibling mutants. Loud when the stub does not fire —:447(cp_reported_failure), and 8d's is a count (:512,-eq 2) precisely because one surviving read would hand the witness to 8a's comparison. Comment states the property pinned —:405-421. All four pass.Neither new fixture satisfies its property by construction: 8c's
same/sameon both inferences is what forces the exit status to be the only witness, and deleting the status check leaves no report at all rather than a differently-shaped one. 8d's stub is correctly narrowed to%ireads of this clone's own source (_dir_inodeatcache-lib.sh:327is the solestat -c '%i'caller on this path; the reader markers'%Yreads pass through). Scenario 10's link-count assertion on$alias_depis a real post-condition, not a tautology.Full
bash scripts/selftest.sh(including both compiler-backed suites): 5 suites, 129 assertions, green.shellcheck scripts/seed-target-dir-selftest.sh: only the pre-existing SC1091; whole-scripts/finding count is 11 on bothec3ab6cand this head — no drift.cache-lib.sh,cargo-cache/andcargo-cache-publish/untouched.Blocking
1. The sweep missed an unpinned guard in the function it swept —
reader_lock_acquire.Delete the
reader_lock_acquire "$parent" "$src_name" "$tag"line fromhardlink_clone_into(cache-lib.sh:399) and all five suites pass, at the identical 129 assertions. That is the consumer's half of the seed-vs-republish interlock, one line above the four terms this PR pins, directly under the comment calling it the ordering proof — and it is live in the fixtures, not dormant: scenario 8a's publisher genuinely blocks on that marker (readers: waiting for 1 in-flight clone(s) of snapshot-dev-…/drained after 1sinlogPub). The scenario keeps passing without it.I found this with a five-mutant spot check outside the compound condition, which is what makes it a finding: the PR body affirmatively presents "Guards found unpinned and NOT fixed, with reasons" as the sweep's complete residue, and "every guard the sweep touched was found correct, only unpinned". Both read as stronger than the sweep supports. This does not require a new scenario — reporting it in that section (or filing it, per the issue-tracker rule) discharges it. But the list has to be accurate, because the list is what a future reader will trust instead of re-running the sweep.
2. The README methodology census this diff falsifies is updated by nobody.
README.md:405-409currently reads: "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)." After this merge 8c, 8d and 10 also assert which guard fired, so the enumeration understates the suite, and the cited issue is the one this PR closes — a reader following it lands on "removing it leaves every suite green", which this PR makes false.README.md:384has the smaller sibling: "scenarios 8a and 8b stubcp", now also 8c and 10, while 8d stubsstat— a shape the paragraph does not mention.That section was rewritten two PRs ago under the commit title "census the three concurrency-scenario shapes honestly", which is exactly the claim this diff dents. The scoping premise that #8 owns it does not hold: #8's README diff adds a new Scratch names in a cache root section and does not touch these paragraphs. Scenario 9's own sentence stays true — my matrix confirms it survives every mutant — so this is a census-and-citation update, not a rewrite.
Nits (not blocking)
write_cache_lockis unreachable from every suite. Removingwrite_cache_lock "$tmp" "$lock_id"fromhardlink_clone_intoleaves all fixture suites green — but as an equivalent mutant: noseedcall in any suite passes the optional 6thlock-idargument, soLOCK_IDis always empty and the function early-returns. The documented property ("the directory already carries the lock the instant it appears under its final name, so a concurrent prune cannot evict it") has no coverage at all. Worth a line in the sweep report even though the mutant itself proves nothing.:479models it as "a transient stat error over a source that is otherwise perfectly readable". For this term to be the sole witness, both reads of one attempt must fail whilecp -alsucceeds in between — a rotation gives you a numerici_afterand hands the witness to 8a, and a source that is really gone fails the copy. That makes 8d a regression guard for a defensive term against sentinel equality, which is valuable and worth keeping, rather than a reproduction of a condition a job hits. The PR body should name it as the former.publish-snapshot-selftest.sh:44("scenarios 8a and 8b") is not stale — it scopes itself to the rotation-race half, which is still exactly 8a and 8b. No numbering collision: 10 is new.VERDICT: APPROVE-WITH-NITS
Both blocking findings from the previous round are genuinely closed, and I re-verified each rather than reading the diff for intent. The sweep record now scopes itself honestly and carries
reader_lock_acquireas a filed gap; the README census is corrected in both paragraphs and the new root-cause explanation is, in mechanism, empirically right. Approving.One thing I will not do is certify what you asked me to certify.
The residue is not complete, and I would not write a sentence saying it is
I ran a second sweep — twelve mutants of my own, disjoint from the PR's twelve, all inside the stated scope. Nine reddened a suite. Three did not, and all three are inside
hardlink_clone_into/unshare_mutable_paths, which the sweep record names as swept guard-by-guard:unshare_subtree "$d" || { echo…; return 1; }→|| true(cache-lib.sh:203-206).rustc_info.jsonpropagation →|| true(cache-lib.sh:212-215)rm -rf "$tmp"deleted (cache-lib.sh:395)The first is the sibling of the one scenario 10 pins.
unshare_mutable_pathshas three status propagations — the subtree loop, the*.dpass, the.rustc_info.jsonpass — and the residue table lists neither the first nor the third. Swallow the subtree one and a staging tree whose.fingerprint/still aliases its source is published, which is the corruption the whole scheme exists to prevent; the.dsibling one line down is the thing this PR just spent a scenario pinning.But the category matters, and it is not the
reader_lock_acquirecategory. No fixture can makecp -afail (scenario 10's stub matches-p --only), no fixture creates a.rustc_info.jsonin a seeded tree at all, and no fixture leaves a stale.stage-<tag>for a clone of the same tag to trip over. All three are undriven paths: the mutant is green because nothing exercises the branch, exactly as the PR already documents forwrite_cache_lock.reader_lock_acquirewas worse than this — exercised on every clone, load-bearing in 8a's own fixture, and still unasserted.So: three rows for the residue table (or three lines on #10), same "no coverage" wording the
write_cache_locksection already uses. Not a third review round, and not a scenario each — the honest statement is thatunshare_mutable_paths's failure propagations have one of three pinned and the other two undriven.For the record on what I could not close: a
live_reader_countmutant ([ -e "$marker" ] || continuedeleted) was still running when I posted. It is outside the stated scope and does not bear on the verdict.Finding 1, verified
reader_lock_acquirere-checked from the new head: deleted, all five suites pass at the identical 129 assertions. The filed-rather-than-fixed reasoning holds up — asserting on the publisher's log inside 8a would work, but it would give 8a a second property and cost the one-scenario-one-mutant diagonal, which is this suite's most legible asset. #10 is the right home. Thewrite_cache_lockcharacterisation is right too: no suite passesseed-target-dir.sha 6th argument,LOCK_IDis empty, the function early-returns, and the mutant proves nothing — accurately described as equivalent rather than unpinned.Finding 2, verified
:384is accurate on all four uses of the stub shape: 8a/8b/8c stubcpon the top-level clone, 10 stubscpat the per-file unshare, 8d stubsstat, and the stated reasonstatis the lever is the correct one (the check fires on an identity that could not be read, so the reads must fail while the copy between them succeeds).:411's "8a to 8d and 10 … each reddened by exactly one mutation" matches my own independently-rebuilt diagonal exactly. The#5citation no longer points at a claim this PR falsifies.The scenario-9 explanation: mechanism right, one clause wrong
I checked this rather than reading it. Scenario 9 run in isolation under the
cp_rcmutant fails down the unshare path, verbatim:and the mode-
000link in the chain is real, not a plausible story:cp -alover a mode-000 source creates the destination directory and preserves mode 000 before failing, andfind … | xargs …over that directory returns 1 underpipefail, which is what_unshare_filespropagates. Verified directly, outside the suite.The clause that is wrong is "so the unshare pass aborts the clone before the copy's own exit status is ever consulted." That ordering does not exist in either version. Unmutated, the exit status is consulted immediately after the copy and fires first — the unshare pass is never reached (
::warning::clone: attempt 1/2 … was torn (cp rc=1, 0/0 entries …)). Mutated, the check is gone, so there is nothing to consult before or after. The accurate sentence names the mutation:As written a reader can come away believing
unshare_mutable_pathsruns before the torn-clone condition insidehardlink_clone_into, which is the kind of ordering claim this file is otherwise careful about (cache-lib.sh:396-398). One clause, not a re-diagnosis — the root cause identified is correct.The generalisation holds
"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" is sound, including the part a fixture cannot escape: when a fault trips two terms of one compound condition,
assert_tearreads values rather than terms, so deleting either still yields a report and the scenario still passes. It applies to both named terms, with one asymmetry worth knowing if the paragraph is ever revisited — the masking partner actually demonstrated forcp_rcis_unshare_files's status in scenario 9, while for[ "$i_before" != missing ]the partner iscp_rcand the claim is that no natural fixture could have isolated it. Same principle, two different partners; the PR body says this plainly, the README compresses it. Fine as compressed.Nits
:55-57) and scenario comment (:492-499) now agree with each other and with what the fixture does.Re-verified this round
Full
bash scripts/selftest.shateb7878b: 5 suites, 129 assertions, green.shellcheck scripts/seed-target-dir-selftest.sh: pre-existing SC1091 only.scripts/cache-lib.sh,cargo-cache/,cargo-cache-publish/untouched; the only code change sincebd60b43is documentation. All mutation work on scratch copies outside the worktree; nothing ran against a live cache volume.