Mutation oracle — scope candidates and their measured survivor inventory

Companion to .cargo/mutants.toml and .github/workflows/mutants.yml, for #216 item 1 ("fault injection over src/"). The committed scope is 213 of 1427 mutants (14.9%). This file records what the next tranches would cost, measured rather than estimated, so a widening is a decision with a price attached instead of a glob edit.

The rule this file exists to serve: pay first, widen last. A tranche is admitted to examine_globs only once its survivors are killed or individually argued. Widening into a red budget would produce exactly what .cargo/mutants.toml warns about — "a red check nobody can explain gets disabled, after which the real regression walks through."


Candidate 1 — crates/acdp-registry-sqlite/src/store.rs (138 mutants)

Status: measured, deferred. Tracked in #307.

The strongest candidate on the merits, and the reason it is not yet in scope is its bill, not its value.

mutants138 (cargo mutants --list --no-config --file <path>)
verdicts obtained95 of 138 (69%)
caught47
missed (survivors)14 — 6 killed, 7 equivalent, 1 needs a seam, 0 open
unviable34
survivor rate among viable14 / 61 = 23%
extrapolated survivors at 138~20, against a budget of 5

Measured at 67da995 under the committed config's settings (test_workspace, copy_vcs, ACDP_SPEC_DIR at the .spec-pin revision in ACDP_REQUIRE_CONFORMANCE=1 mode).

Why 95 and not 138 — the runs were stopped on disk headroom; see Cost below. The 95 are the union of three overlapping runs, which agreed on every mutant they shared (0 conflicting verdicts). The remaining 43 have no verdict and are not guessed at.

The 14 survivors

Each is a change to src/ that the whole workspace suite did not notice. None has been triaged into "real gap" vs "equivalent mutant" — that triage is the work of the units that pay them down, and is deliberately not pre-judged here.

sitemutationstatus
store.rs:49:16delete ! in SqliteStore::connectKILLED (U-547)
store.rs:154:9count_idempotency_records → Ok(Some(0))KILLED (U-543)
store.rs:342:26+ → * in list_contextsKILLED (U-543)
store.rs:342:26+ → - in list_contextsKILLED (U-543)
store.rs:380:9lifecycle_events_of_ctx → Ok(vec![])KILLED (U-543)
store.rs:568:9put → Ok(())KILLED (U-552)
store.rs:821:9mark_superseded → Ok(())KILLED (U-552)
store.rs:832:9first_version_ctx_id → Ok(None)KILLED (U-552)
store.rs:923:9idempotency_evict_expired → Ok(())KILLED (U-552)
store.rs:994:35> → < in commit_publishKILLED (U-552)
store.rs:994:35> → == in commit_publishKILLED (U-552)
store.rs:994:35> → >= in commit_publishEQUIVALENT (U-544)
store.rs:1090:59== → != in commit_publishKILLED (U-544)
store.rs:1306:35!= → == in commit_publishUNREACHABLE-BY-DESIGN (U-544/U-552)

The one survivor that was run to ground

store.rs:342:26 is q = q.bind(limit + 1) — the LIMIT limit+1 sentinel that tells try_paginate_rows whether more rows exist. limit * 1 removes the sentinel, so next_cursor can never be set.

It was verified by hand, not accepted from the verdict: applying the mutation and running the suite leaves all 14 store_contract tests green, including visibility_sql::pages_are_full_and_total_is_honest_under_mixed_visibility by name. The oracle's MISSED is honest.

And the reason is sharper than "untested", which is what the verdict alone would have suggested. list_contexts has 10 call sites across crates/:

  • 1 production caller — acdp-registry-core/src/handlers/admin.rs:117
  • 2 delegating wrapper impls that forward whatever they are given — acdp-registry-store/src/parity.rs:864, acdp-registry-server/tests/http_integration.rs:8758
  • 7 test callers, every one passing a limit of 50 or 100
  • 0 that exercise the limit + 1 boundary

The only small-limit pagination test in the repo (parity.rs, const LIMIT: u32 = 2) exercises search, a different method. So the gap is not a method nobody calls — it is a branch nobody reaches, inside a method with ten callers, on a path reachable from the admin HTTP surface.

That is the general lesson and the argument for widening eventually: a method with ten callers reads as covered. The oracle is what distinguishes "this method is called" from "this path through it is asserted on". lifecycle_events_of_ctx → Ok(vec![]) is the same shape one level up — the whole method can return an empty event list unnoticed.

Cost — and the finding that the tranche does not fit locally in one pass

throughput6.6 s/mutant (9.1 mutants/min), warm cache
peak live temp tree2566 MiB, reached ~50s in and then flat
sustained consumption−544 MiB/min for 290s with that tree flat ⇒ ~60 MiB/mutant
headroom needed, 138 in one pass~8.1 GiB
headroom needed, 351 in one pass~20.5 GiB

cargo mutants on this tranche cannot complete in one pass at the headroom this machine has had (4–10 GiB). Three runs were stopped on the disk floor. Run it sharded (--shard k/8) — consumption is fully released when the process exits (free returned 4116 → 7208 MiB on termination), so shards fit where one pass does not.

Two traps found the hard way, both worth inheriting:

  • Killing cargo mutants does not stop the run. It spawns cargo test --verbose --workspace, which spawns rustc and clang, and those children outlive the parent. After pkill -f 'cargo mutants' reported nothing alive, a live cargo test --no-run, two rustc and a clang were still building into a new temp tree. Kill by temp-tree path and by cwd — the cargo test driver does not name the tree in its argv. This is also why two full 2566 MiB trees can be resident during a strictly sequential shard loop.
  • Never clear target/ mid-run. Doing so to buy headroom coincided with a healthy run dying at 50/138. It also forces a cold rebuild, which pushed consumption to ~1566 MiB/min — so the 60 MiB/mutant figure above is a warm-cache number and not a ceiling.

The ~60 MiB/mutant mechanism is now SUBSTANTIALLY EXPLAINED — corrected here, because this file previously recorded it as open. #309 (lane-3) found a SQLite sidecar leak: harnesses owned a tempfile::NamedTempFile, which deletes exactly the path it owns, while SQLite writes -wal and -shm beside it. Those outlive the test. Lane-3 reports that in the same 290s window measured above, the leak alone produced 5,286 files / 1,689 MiB = 349 MiB/min — about two-thirds of the 544 MiB/min observed.

Stated as two-thirds rather than "solved" on purpose: ~195 MiB/min remains unattributed, and this file should not trade one confident wrong answer for another. The leak is fixed at source; the files already leaked are still resident.

Why neither of the two sessions watching found it: both instrumented the cargo-mutants temp tree and target/, and the leak was in neither — it was loose in /private/var/folders, a root nobody had enumerated.

One hypothesis was separately tested and eliminated: deleted-but-still-open files. Sampled during a live run, lsof +L1 held flat at 192 MiB across 362–368 fds while 1 GiB disappeared — the same figure measured with nothing running.

U-543 found and fixed a second instance of the same leak that #309 did not cover, because tests/tmpdir_hygiene.rs guards the server harness only. Measured like-for-like on one command, cargo test -p acdp-registry-sqlite --test store_contract:

sourcefiles leaked per run, before → after
acdp-registry-sqlite/tests/{store_contract,parity}.rs28 → 0
acdp-registry-sqlite/src/store.rs #[cfg(test)] (13 sites)26 → 0

The "before" figure was nearly missed: a first measurement scoped to $TMPDIR at depth 1 returned a confident 0. An empty result is only as wide as its root.


Candidate 2 — crates/acdp-registry-pg/src/store.rs (136 mutants)

Status: disqualified until a database is wired into the runner.

Its tests skip to success when ACDP_REGISTRY_TEST_PG_URL is unset (pg_url_or_skip, crates/acdp-registry-pg/tests/parity.rs), and the mutation runner provides no database. Every viable mutant would therefore report MISSED — stated that way deliberately, since unviable mutants never compile and so have no verdict to be wrong about.

A survivor list that is entirely false positives is what .cargo/mutants.toml already calls worse than no baseline at all. This is a prediction from the gating code, not a measured run — confirming it would cost a 136-mutant run to observe a skip that pg_url_or_skip states in four lines.


Candidate 3 — crates/acdp-registry-store/src/parity.rs (89 mutants)

Status: rejected on mechanism, not cost.

test-support harness code, off by default, that never ships. Mutating a test's own assertion weakens the test; the weakened test still passes; so the mutant survives by construction and no survivor has a remedy. Mutation testing asks whether tests catch defects in production code — pointing it at the tests inverts the question.


Candidate 4 — crates/acdp-registry-store/src/lib.rs (38 mutants)

Status: deferred, needs a real answer first.

Holds retrieve_visible, the authoritative RFC-ACDP-0008 §4.5 predicate whose shared-centre blind spot U-534 documented — genuinely worth covering. But roughly 30 of its 38 mutants target default trait method bodies that both real backends override (tenants_of_ctxs, visible_ctx_ids, tenant_of_ctx, log_leaf_hashes all reappear as <impl … for SqliteStore>). Those are unreachable and would arrive as budgeted survivors sharing one repetitive excuse.

The open question is whether unreachable default bodies are worth mutating at all, or should be excluded by a mechanism narrower than a glob. That deserves its own unit.


Remaining scope, for whoever sizes the next tranche

Workspace total is 1427 (cargo mutants --list --no-config). Largest files not yet in scope and not covered above:

filemutants
acdp-registry-server/src/main.rs99
acdp-registry-types/src/config.rs92
acdp-registry-core/src/rate_limit.rs65
acdp-registry-auth/src/revocation_store.rs61
acdp-registry-types/src/error.rs52
acdp-registry-auth/src/service.rs51
acdp-registry-core/src/handlers/admin.rs49
acdp-registry-auth/src/jwt.rs47

main.rs carries the same hazard as pg: it is heavily feature-gated (storage-pg, storage-memory, playground), so survivors there would include code the default-feature workspace suite never compiles.

U-544: two of the five commit_publish comparison survivors are not coverage gaps

(was "four of five" — U-552 killed 994:35 < and ==; see the corrected block below.)

Slice 2 set out to kill five comparison mutants and killed one. The other four were run to ground, and the result matters more than the kill: a survivor is not automatically a missing test.

store.rs:994:35 (if expires_at > now, ×3) — TWO OF THE THREE ARE KILLABLE. U-544's EQUIVALENT label was wrong for < and ==, and U-552 killed them.

U-544's probe was correct and its generalisation was not. With < applied, skipping the TTL branch lets the publish proceed to INSERT … ON CONFLICT(agent_id, key) DO NOTHING, which collides with the live record, rolls the new context back and replays the stored response — the same IdempotentReplay, the same ctx_id, by a second route. That holds only for a request that does not supersede. The second enforcement lives at store.rs:1284-1318, which is reached only AFTER step 2.

For a keyed publish that DOES supersede, step 6 (store.rs:1241) marks the predecessor superseded inside the same transaction, so a replay with step 1 skipped reaches step 2's coherence check at store.rs:1118-1125 and returns Err(SupersededTarget { AlreadySuperseded }) instead of Ok(IdempotentReplay). Killed by a_keyed_superseding_publish_replays_instead_of_failing_as_already_superseded in crates/acdp-registry-sqlite/tests/store_contract.rs.

Measured, not argued. The exact reproducible command — --file is mandatory, because store.rs is deliberately outside examine_globs and --file UNIONS with it; omitting it matches zero mutants and exits 0, which reads as success:

cargo mutants --file crates/acdp-registry-sqlite/src/store.rs -j1 \
  -F '^crates/acdp-registry-sqlite/src/store\.rs:994:35: replace > with (<|==) in <impl RegistryStore for SqliteStore>::commit_publish$'

Both come back CaughtMutant with that test the SOLE failure (26 passed, 1 failed) — not a harness-wide failure. The four trait-method mutants are reproduced the same way with -F '^crates/acdp-registry-sqlite/src/store\.rs:(568:9|821:9|832:9|923:9):' → 4 caught, 1 unviable, each killed by its own named test at 30 passed / 1 failed.

Check the killer by NAME before believing any of this. On a machine where the worktree's owner differs from the running user, git refuses cargo-mutants' temp copy (dubious ownership), conformance_gate panics, cargo test --workspace stops at that first failing binary, and every mutant is reported "caught" while the test under evaluation never runs. Export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0='*' first. This is the class mutants.yml's AC8 harness check exists to detect, and it occurred during U-552.

Why the mutated branch is unconditionally false rather than merely different: step 1 first runs DELETE … WHERE expires_at_ms <= ? bound to now (store.rs:963-966), so any surviving row has expires_at_ms >= now_ms + 1. expires_at > now is therefore unconditionally TRUE for a surviving row, and < / == unconditionally false.

> → >= remains genuinely EQUIVALENT by that same arithmetic — true on exactly the same inputs — and stays in MUTANTS_SURVIVORS with that reason. Note this is a STRONGER argument than the millisecond-boundary one previously given here: it needs no claim about clock resolution, because the DELETE makes the boundary case unreachable outright.

store.rs:1306:35 (prior_hash != content_hash) — NEEDS A SEAM, not a test. An eprintln! at inserted == 0 fires zero times across the entire suite, including both racing tests. SQLite's BEGIN IMMEDIATE serialises the racers, so every loser finds the committed record at the step-1 read and is refused at store.rs:1003 instead. The obvious deterministic route — pre-expire the record so the claim collides — does not work either: step 1 DELETEs an expired record for that key (store.rs:964) precisely so the claim cannot collide with a stale row. Tried, and it published cleanly. Reaching :1306 needs interleaving this harness cannot produce.

store.rs:1090:59 — a genuine gap, now KILLED. Every existing supersession test supersedes as the original producer, where the first arm of prev_agent == req.agent_id || prev_contributors.iter().any(…) short-circuits and the contributor arm is never consulted. Inverted, the arm reads "any contributor who is NOT you", which both refuses genuine contributors and admits any signer whenever v1 lists a contributor other than them — the lineage takeover the comment at that site says the check prevents.

Consequence for sizing this issue: the 14 known survivors are not 14 units of work. At least 3 are equivalent and 1 needs infrastructure. Expect that ratio to hold for the 43 unjudged mutants too.

U-546: the whole -> Ok(()) write-path family is dead trait surface

U-552 KILLED all four. The census below is right; the label on it was not. These were carried as EQUIVALENT (U-546), and "equivalent" was the wrong word: an equivalent mutant cannot change behaviour, whereas these change behaviour perfectly well and simply have nothing calling them. U-552 first proposed relabelling them UNREACHED-BY-PRODUCTION and carrying them as budgeted survivors, on the argument that a test whose only purpose is to call an uncalled method converts a true finding into a permanently green line that hides it.

That was reversed. Two rows above, 380:9 lifecycle_events_of_ctx is recorded KILLED (U-543) by a direct-call test — and it has no originating production caller either. Carrying these four while counting that one a win asserts both positions at once. What settled it: .cargo/mutants.toml's governing rule is "PAY FIRST, WIDEN LAST"; acdp-registry-pg implements all four, so they are genuine backend-CONTRACT surface rather than dead code; and parity.rs exists precisely to compare the two backends.

The finding is not lost by killing them — it is this section, plus a block comment above the four tests in crates/acdp-registry-sqlite/tests/store_contract.rs. What changed is that the contract is now pinned, so a future originating caller inherits a tested method rather than an untested one.

Slice 3 killed nothing, and that is the correct outcome. All four are RegistryStore trait methods that SqliteStore must implement because the trait requires them, and that nothing in this workspace ever calls.

Resolved by TYPE, because the name is ambiguous. A bare .put( count is meaningless here: ChallengeStore::put(ChallengeRecord) -> Result<(), AuthError> owns 13 of the call sites, all in acdp-registry-auth, and is a different trait entirely. RegistryStore::put(Body) -> Result<(), AcdpError> has exactly three, and all three are delegating wrapper impls that forward to another implementation:

methodcall sitesall delegating?
putparity.rs:772, http_integration.rs:8665, memory_ext.rs:38yes
mark_supersededparity.rs:793, http_integration.rs:8692, memory_ext.rs:50yes
first_version_ctx_idparity.rs:799, http_integration.rs:8698, memory_ext.rs:53yes
idempotency_evict_expiredparity.rs:828, http_integration.rs:8730, memory_ext.rs:74yes

No originating caller exists. The UFCS form (RegistryStore::put(&x, …)) that a dot-grep would miss returns nothing either.

One near-miss worth keeping. The upstream acdp crate does call self.idempotency_evict_expired(...) in non-test code — acdp-0.1.0/src/registry/store.rs:405. That is inside impl RegistryStore for **InMemoryStore** (line 308), a different type, so it says nothing about SqliteStore's implementation. SqliteStore::idempotency_lookup deliberately does not evict at lookup — its comment says the background task evict_idempotency keeps the table bounded instead. A workspace-only grep would have missed that call entirely, and a type-blind reading of it would have wrongly promoted this one to "reachable".

Confirmed empirically, with a control. panic! armed in all four methods, full cargo test --workspace with ACDP_SPEC_DIR set and ACDP_REQUIRE_CONFORMANCE=1: 0 runtime panics, 0 failed binaries. The same probe placed in get() — a method that is called — produced 9 panic lines, so the detector demonstrably works and the zero is a real zero rather than a broken check.

Revisit trigger — SUPERSEDED BY U-552. This was written as "this equivalence expires the moment any of the four gains an originating caller". Two corrections: the word was never equivalence (these mutations change behaviour perfectly well — nothing calls the code), and U-552 stopped waiting for the trigger. All four are now KILLED by direct contract tests in crates/acdp-registry-sqlite/tests/store_contract.rs, on the reasoning that .cargo/mutants.toml's "PAY FIRST, WIDEN LAST" rule governs, that acdp-registry-pg implements all four so they are real backend-contract surface, and that the repo had already made exactly this call for the identical shape (380:9 lifecycle_events_of_ctx, KILLED by U-543, which has no originating caller either). The call-graph census below remains correct and is why the tests are written as direct trait-method calls.

Running classification (as recorded at U-547): 5 killed, 7 equivalent, 1 needs a seam, 1 open — out of 14 known survivors, from 95 of 138 mutants judged. U-552 revised this: 11 killed, 1 equivalent, 1 unreachable-by-design, 1 open (see "The known list" below). The one still open is 49:16 delete ! in SqliteStore::connect.

U-547: the last known survivor, and what the whole exercise showed

store.rs:49:16 is if !parent.as_os_str().is_empty(), guarding create_dir_all(parent). Deleting the ! inverts it to "create the parent only when there isn't one".

It survived because every existing test hands connect a path whose parent already exists — tempfile::tempdir() creates it — so create_dir_all is a no-op and skipping it changes nothing. A real deployment pointed at /var/lib/acdp/registry.sqlite before that directory exists would fail to start. Killed by connect_creates_a_missing_parent_directory_chain, which asserts the parent is absent as an explicit precondition, so the test cannot silently stop exercising the branch if someone changes the fixture.

The known list is now fully resolved

as of U-547after U-552
killed612
equivalent71 (994:35 > → >=)
unreachable-by-design—1 (1306:35, race-only)
open00 (true only of the KNOWN list; U-549 measured all 138 and found 11 more — see below)

U-552 REVISED THE HEADLINE THIS SECTION USED TO CARRY. It read: "Half the known survivors were not coverage gaps. Seven of fourteen were equivalent — code whose mutation cannot change observable behaviour." Six of those seven have since been killed, so the sentence was wrong in the direction that matters — it told a reader that most of a survivor list is not worth working, which is the most quotable and most load-bearing claim in this document.

What survives of the original point, stated accurately: a survivor is a fact, not a verdict, and the verdict has to be re-derived rather than inherited. Of the fourteen known survivors, exactly ONE is genuinely equivalent (994:35 > → >=, where a preceding DELETE … expires_at_ms <= now makes the comparison unconditionally true) and ONE is unreachable by design (1306:35, behind BEGIN IMMEDIATE). The other twelve were killable, including four that spent two units labelled EQUIVALENT on a census that was correct and a WORD that was not, and two whose EQUIVALENT label rested on a probe that was correct for the case probed and did not generalise.

So #307 should still not have been sized by its raw survivor count — but the error runs the other way from what was written here: the list under-counted the work, it did not over-count it.

U-548: the disk block is lifted, and the judged/unjudged split was lost

Both halves of the sentence that used to close this section were wrong, and they are corrected here rather than quietly deleted. It read: "the only remaining work on this file is the 43 mutants that have never been judged — 95 of 138 have verdicts. That is blocked on disk headroom for a sharded run."

"43 unjudged" can no longer be resolved to a list. The per-mutant verdict files for the 95 judged mutants lived only in a session scratchpad and were deleted during an ENOSPC cleanup that freed 59 MiB. Every surviving outcomes.json on the machine was parsed to look for them — with a control confirming the parser finds the pattern when it is present — and none contains a sqlite/src/store.rs verdict. The remaining work on this file is therefore all 138, not 43. The per-run ledger is committed under docs/mutation-runs/ from now on precisely so this cannot recur; a 40-minute measurement must not live somewhere a cleanup can reach.

"Blocked on disk headroom" is disproven. --copy-target=false removes the block outright:

measurementvalue
commandcargo mutants --no-config --file crates/acdp-registry-sqlite/src/store.rs --shard 1/8 --copy-target=false --test-workspace=true --minimum-test-timeout=300 -j1
wall clock, 18 mutants97 s
peak consumption below the post-clean baseline2547 MiB (free 4770 → 2223)
peak temp tree observed2649 MiB
residual after exit0 — free returned 4770 → 4770, temp trees 0
sidecar .sqlite files created by the run0

Two caveats that keep those numbers honest. The temp tree was sampled at 30 s and the last in-run sample landed 26 s before exit, so the true peak is ≥ 2649 MiB, not exactly it. And the residual figure is a net-zero equality, not a rate: 97 s spans too few steps to support a MiB/min claim, and the tree grew monotonically throughout so there was never a flat phase to measure a rate against. What is established is that a complete shard leaves nothing behind.

The 0 sidecar files line is a direct count under $TMPDIR, date-bounded to the run window with TZ=UTC, with two controls: a probe file created in the same root was visible to the same find, and an unbounded -newermt returned the full 312 900 pre-existing files. Those 312 900 are the known pre-existing backlog, untouched by this run — the leak fix holds.

Projected cost for the full 138 in one pass, from this shard's measured per-class means (unviable 2.2 s, caught 9.0 s, baseline 16.3 s once) against the ≆33 % viable rate seen here: ≈ 10.5 minutes. Treat that as a floor, not an estimate — a caught mutant fails fast, while a missed one runs the entire workspace suite to green, so any shard containing survivors costs more per mutant than this one did.

A note on a non-finding, kept because the error is instructive. The ~30 s cold build initially looked anomalous and was nearly written up as unexplained. It is not. An independent cargo test -p acdp-registry-server --test conformance_gate from a cleaned target/ compiled the workspace in 25.77 s — this machine simply builds this workspace that fast. The recorded budget I was measuring against, cargo test --locked --workspace --no-run ≈ 2.0 GiB cold, is a disk figure; the "several minutes" I was implicitly comparing against was never recorded anywhere and was my own addition. A fabricated premise had produced a real-looking anomaly, and it would have shipped inside a paragraph whose every other sentence was measured.

RETRACTED — U-548's re-run proved nothing (see U-549)

The section that stood here claimed U-543's kills were confirmed by cargo-mutants. That run was void. It was invoked with --no-config, which discards the whole of .cargo/mutants.toml; the settings restored by hand omitted copy_vcs = true, so .git was absent from the mutant tree and three git-dependent conformance tests panicked in every mutant tree, marking every mutant caught for a reason unrelated to the mutation. The pre-registered prediction matched for the wrong reason. Full mechanism in docs/mutation-runs/README.md; the ledger is kept as VOID-u548-...json.

What was never retracted: U-543's three mutants were killed by hand-application with a red observed each time. That evidence never depended on this harness. And U-549's valid run has since confirmed all three as CaughtMutant — so the claim is now true on evidence that actually supports it.

U-549 — the full 138 under a valid harness, and D-W5-182 re-tested

Eight shards, one uniform method: --config docs/mutation-runs/u549-sqlite-tranche.toml, --copy-target=false, -j1. Every ledger is committed beside this file.

shardcaughtmissedunviablen
0/8110718
1/8601218
2/8121518
3/8112518
4/8134118
5/867518
6/8441018
7/851612
total681951138

Validity checks, all passed: end_time present on all eight (a null end_time is the in-flight tell, and a grep for the key rather than its value is a false positive); mutant rows match every header; zero logs containing not a git repository; sizes sum to 138 and the ledgers carry 138 distinct mutants with 0 duplicates — sizes alone miss a duplicated shard, the union alone misses a dropped one.

--shard k/N is ZERO-INDEXED. Valid shards are 0/8–7/8; 8/8 is empty. An ordinal range of "2/8 through 8/8" covers 120 of 138 and silently drops shard 0/8 while producing a confident eight ledgers.

The answer: D-W5-182's 50%-of-viable does NOT survive

Both rates, each with its denominator on the same line as the number, because a rate whose denominator lives in a footnote gets quoted without it:

  • RAW: 19 survivors / 87 viable = 21.8% of viable.
  • UNJUDGED: 11 previously-unjudged survivors / 87 viable = 12.6% of viable.

12.6% is the figure comparable to D-W5-182's 50%, which was 3 missed / 6 viable from shard 1/8 and was a pure-gap rate — its numerator contained no known-equivalents, because nothing in that shard had been judged yet. So the comparable measured figure is about one quarter of the ruling's premise, and the ruling was extrapolated from a single 18-mutant shard whose three survivors were all subsequently killed.

All 8 already-judged non-gaps (7 equivalent + 1 needs-a-seam) came back MissedMutant, exactly as recorded — so the exclusion set was computed from observed verdicts, not assumed from the prior count. That distinction is load-bearing: a judged-equivalent that had been caught would never appear in missed at all, and subtracting a fixed 8 would have removed a survivor that was never counted.

The 11 previously-unjudged survivors

sitemutationarea
1585:37delete !tag filter in list_contexts
1588:24delete !tag filter in list_contexts
1588:74!=tag filter in list_contexts
1596:86!=derived_from filter
1632:9Ok(())idempotency_evict_inner
1644:9Ok(())evict_idempotency
1869:5String::new()context_type_str
1869:5"xyzzy".into()context_type_str
1897:26truestatus/expiry computation
1897:26falsestatus/expiry computation
1897:30>status/expiry computation

All are production code — #[cfg(test)] begins at line 1987. Not paid down in this unit by instruction: measure, report, stop. Whether to widen scope or pay these down is a scoping decision, not a lane call.

What the correction did NOT void: the disk findings

U-548's verdicts were void; its disk measurements were not, and they have now been re-confirmed under the corrected configuration. Across all eight valid shards free space stayed flat — 4617 → 4587 MiB, a −30 MiB drift that includes the committed ledgers themselves — with 0 temp trees and 0 sidecar .sqlite files left behind. Residual is still zero, now with .git actually present in the mutant tree.

The one figure that needs a caveat rather than a retraction: U-548's peak of 2547 MiB was measured on a run that did not copy .git, so it understates the corrected configuration by roughly the size of the repository history. The conclusion it supported — that the tranche fits comfortably and disk is not the constraint — is unaffected, and the whole 138 completed here in about 24 minutes of wall clock.

rc=2 from cargo mutants means survivors were found, not that the run failed. Shards 0/8 and 1/8 exited 0 (no survivors); the other six exited 2. Treating a non-zero exit as a failed run would have discarded six valid shards.

U-550 — the 11 unjudged survivors, paid down

Five tests in crates/acdp-registry-sqlite/tests/store_contract.rs kill all 11 previously-unjudged survivors. Confirmed by two independent instruments, because U-548 is the standing argument against trusting one.

Instrument 1 — hand-application. Each of the 11 mutants applied to src/store.rs by hand, the named test run, and the mutant reverted. All 11 reddened. Each anchor was verified to match exactly once on clean source first, so a silently-unapplied mutation could not masquerade as a kill.

Instrument 2 — the oracle, re-run under docs/mutation-runs/u549-sqlite-tranche.toml against a committed baseline. Ledgers committed beside this file.

shardmissed before (U-549)missed afteroutcome
5/871six killed; 1306:35 survives, as predicted
6/840all four killed
7/810killed

All 11 came back CaughtMutant. 1306:35 remains MissedMutant — U-544 proved that branch unreachable at this API, and this unit deliberately did not manufacture a test to force a kill on it. Had it flipped to caught, that would have refuted U-544's seam verdict and been reported as such; it did not.

Tranche status: 19 survivors → 8, and all 8 are already judged (7 equivalent, 1 needs a seam). There are now zero unjudged survivors in sqlite/src/store.rs.

Two seams worth recording, because the obvious test would have passed vacuously

context_type_str is observable only through the FILTER, never a read-back. SearchResult.context_type is rebuilt from the stored body JSON (store.rs:1615), so asserting on a returned context's type passes against both constant mutants and proves nothing. The mutated value is read in exactly one place: store.rs:1416, AND context_type = ?. The test therefore publishes two types and queries each, since no single constant can satisfy both queries.

Several of these mutants WIDEN the result rather than emptying it. 1585:37 makes every context pass the tag filter; 1588:74 admits any context carrying some other tag. A contains-style assertion passes against both. Every U-550 assertion is on an exact set for that reason — and 1596:86 is the mirror case, emptying the result, so an assertion that merely required the child to be absent would also have passed. The two directions need opposite assertion shapes, and a set equality covers both.