Skip to content

fix(cache): re-scope mutation gate to a root task so releases stop busting it - #35

Merged
ryanleecode merged 2 commits into
masterfrom
fix/mutants-root-task-scope
Aug 23, 2026
Merged

fix(cache): re-scope mutation gate to a root task so releases stop busting it#35
ryanleecode merged 2 commits into
masterfrom
fix/mutants-root-task-scope

Conversation

@systemfsoftware-maker

Copy link
Copy Markdown
Collaborator

Summary

The mutation gate re-ran its full 4-minute mutant loop on every release, and the fresh verdict was then discarded — so the miss never self-healed.

Root cause (measured, not inferred)

turbo run //#mutants --dry=json before the fix:

inputKeys: [ ...20 Rust determinants..., "package.json" ]

The mutants task was package-scoped on the npm launcher, while its command ran cd ../.. into the repo root against the Rust crate. Turbo unconditionally hashes a package task's own manifest, so the launcher's version — rewritten by release-version.ts on every release — sat in the verdict's cache key. It was never declared in inputs; a reading pass of the declared inputs cannot see it.

Hit rate on release runs was therefore exactly 0.

Why it never self-healed

The transport key hashed only the Rust determinants, so it hit while the turbo hash missed. Exact-key restore suppresses the post-job save:

Cache restored successfully   (actions/cache — key hit)
cache miss, executing 25b3e0a760776fef   (turbo — hash miss)
Cache hit occurred on the primary key ..., not saving cache   (deposit refused)

Two keys for one artifact, moving independently.

Fix — align scope, determinants and key

  • turbo.json: mutants//#mutants (root task); inputs = determinants only.
  • root manifest: mutants is the raw cargo command turbo executes; gate:mutants is the cached entrypoint. A root task runs the identically-named root script, so a wrapper under the same name would recurse — hence the distinct name.
  • launcher manifest: drops the script it never legitimately owned.
  • ci.yml: transport key gains the root manifest, so key = engine surface = determinants by construction rather than as a maintained superset that silently drifts. Job now calls pnpm gate:mutants.

Verification

Probe Result
Cold run miss, 117 mutants, 113 caught, 4 unviable, 0 survived, 2m4s
Immediate re-run hit, 9ms >>> FULL TURBO
Simulated release bump (version rewrite + changelog append) hit, 8ms
Rust source edit hash moves 1019d456…2117ac58…, miss

The last row matters: caching that cannot still detect a real Rust change is worthless. The third row is the regression being fixed — that exact edit previously forced a full re-run whose result was then thrown away.

Full gate green: cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test --all-targets (13 passed), turbo lint typecheck build, deno fmt/lint/check-matrix, actionlint.

Docs

The existing solution doc prescribed inputs that no longer exist (flake.nix, flake.lock) and named package.json as a required input — the very defect. Left in place it would have been a second correct-sounding source contradicting the code. Rewritten in place with the two-sided invariant:

$$D \subseteq I \text{ buys soundness}; \quad I \subseteq D \text{ buys value}$$

The prior revision audited only the first direction, which is how a cache with a hit rate of zero passed review. Renamed to match its corrected subject.

No changeset: CI-only, no consumer-observable change.

…sting it

The mutants task was package-scoped on the npm launcher while its command
ran `cd ../..` into the repo root against the Rust crate. Turbo always
hashes a package task's own manifest, so the launcher's version field --
rewritten by release automation on every release -- sat in the verdict's
cache key. Measured via `turbo --dry=json`: the input set contained
`package.json` beside the 20 Rust determinants, despite never being
declared. Hit rate on release runs was therefore exactly 0.

The transport layer made it permanent: the actions/cache key hashed only
the Rust determinants, so it hit while the turbo hash missed, and
exact-key restore suppresses the post-job save. The fresh verdict was
discarded every run, so the miss never self-healed.

Fix aligns scope, determinants and key:
- turbo.json: `mutants` -> `//#mutants`, inputs = determinants only
- root manifest: `mutants` is the raw cargo command turbo executes;
  `gate:mutants` is the cached entrypoint (distinct name avoids recursion)
- launcher manifest: drops the script it never legitimately owned
- ci.yml: transport key gains the root manifest so K = I = D by
  construction, and the job calls the cached entrypoint

Verified: cold miss 117 mutants / 113 caught / 4 unviable / 0 survived
in 2m4s; immediate re-run hit in 9ms; simulated release bump (version
rewrite + changelog append) hit in 8ms -- previously a 4min re-run whose
result was then thrown away.

No changeset: CI-only, no consumer-observable change.
@ryanleecode
ryanleecode merged commit 023a10d into master Aug 23, 2026
3 checks passed
@ryanleecode
ryanleecode deleted the fix/mutants-root-task-scope branch August 23, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants