Skip to content

feat: self-provisioning nix toolchain with turbo-cached mutation gate - #14

Open
younna-ai-opencode wants to merge 7 commits into
masterfrom
turbo-rust
Open

feat: self-provisioning nix toolchain with turbo-cached mutation gate#14
younna-ai-opencode wants to merge 7 commits into
masterfrom
turbo-rust

Conversation

@younna-ai-opencode

@younna-ai-opencode younna-ai-opencode commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

The repo now provisions its entire toolchain from one lockfile (nix develop), and the mutation gate — the most expensive CI check — replays a content-addressed verdict from the turbo cache when its inputs are unchanged. A fresh machine or runner gets cargo, rustc, clippy, rustfmt, cargo-mutants, node, and pnpm at the exact pinned versions; an unchanged tree answers in milliseconds instead of re-running 117 mutants (~2m36s).

What changed and why

  • Mutation gate as a turbo task (turbo.json mutants, cache enabled). The cache key covers exactly what determines mutant survival: crate sources and tests, workspace manifests, dependency lockfile, build config, and the toolchain definition + pin. Anything outside that set cannot change the verdict; anything inside it flips the key and forces a genuine re-run. A miss errs toward re-execution, never a stale pass.
  • cacheDir pinned to .turbo/cache — turbo's default in a git worktree silently shares the main checkout's cache; a concurrent writer was observed serving an illegitimate hit mid-session. Pinning gives each worktree its own cache and matches the path CI persists.
  • flake.nix + flake.lock lock the toolchain (cargo + rustc 1.97, clippy, rustfmt, cargo-mutants 27.1, gcc, node 24, pnpm 11.21.0 — exactly the packageManager pin). rustc is explicit: cargo resolves rustc via PATH, and without it in the shell the runner's rustup proxy won the lookup and failed to link std (fixed in 2f32cceed; CI commands also dropped the login shell so runner profiles cannot re-prepend ~/.cargo/bin, 7dce7b7e0). Replaces rustup/manual cargo install/third-party toolchain steps: nix develop is the single source of toolchain truth for local dev and both CI jobs.
  • CI: gate + mutation jobs enter the nix env via nix-installer-action; the mutation job runs pnpm mutants with .turbo/cache persisted by actions/cache (keyed on the gate's input set plus the files turbo self-hashes, prefix restore-keys), a pnpm-store cache, and — restored in this branch — a cargo build-state cache for the miss path. GitHub Actions cache chosen over a remote cache backend to keep the setup credential-free; the tradeoff is hits within re-runs/same-branch rather than across branches.
  • Gate semantics unchanged: same command, --timeout 90, 100%-kill demand. The CI log surfaces whether a run was a fresh execution or a FULL TURBO replay — replay transparency is deliberate (see open question below).

Validation

Check Result
nix develop -c toolchain versions cargo/clippy/rustfmt/cargo-mutants/gcc/node/pnpm at pinned versions
Mutants cache loop (each step checksummed) input change -> miss, 117 mutants, 113 caught, 4 unviable, 0 survived, 2m36s; unchanged -> hit 10ms FULL TURBO; flake.nix edit -> miss 2m36s (the stale-replay hole, closed); revert -> hit 8ms
One-shot gate in nix shell fmt clean, clippy -D warnings clean, 90 tests pass
pnpm trio lint, typecheck, build green
Deno release-script gates lint + check-matrix green
ci.yml / turbo.json YAML + JSON parse validated; 6 mutation-job steps

Actions (run on 2f32cceed, all three jobs green): gate 3m45s incl. nix provisioning + full compile; npm 18s; mutation ran the full gate cold — 117 mutants tested in 7m, 113 caught, 4 unviable, 0 survived. A re-run of the mutation job should now log FULL TURBO (exact-key restore) — that replay is the cache proof's last piece, left visible for the reviewer.

Two earlier runs failed and are part of the record: bash -lc let the runner's rustup shim hijack the toolchain, then the flake's missing rustc let the rustup proxy win the PATH lookup — both diagnosed from Actions logs, both fixed with runner-simulation verification (env -i, no host toolchain on any path).

Review

A 4-persona review plus an independent empirical validator ran on this branch before opening (two adversarial P1s were falsified by hash probes — turbo self-hashes the gate script and env values, so those cannot replay stale; one correctness P2 was confirmed — flake.nix outside both keys — and is fixed in cae08e3bd, along with the dropped cargo build-state cache and the CI-key persistence gap). Residual decision for the reviewer, not the code: AGENTS.md's anti-bypass section says evidence comes from the current run, while this branch's CI deliberately replays content-addressed verdicts. The log makes replay visible per run; amending the AGENTS.md wording to permit content-addressed replay is a locked-surface change left to a maintainer.

New concepts

  • $TURBO_ROOT$ — task inputs outside the package directory: turbo hashes task inputs relative to the package; ../.. globs silently match nothing outside the package (first attempt hashed 1 file instead of 20). The $TURBO_ROOT$ prefix re-bases the glob to the repo root so an npm-package task can declare the Rust crate as its input. Use when a task consumes files outside its own package; don't pull the whole repo into a key.

Also captured: docs/solutions/integration-issues/turbo-mutants-flake-nix-cache-key.md (hash-coverage invariants + probe protocol) and the Verdict cache entry in CONCEPTS.md.

The cargo-mutants verdict is now a turbo task keyed on exactly the files
that determine mutant survival (crate sources, tests, workspace and
toolchain manifests). An unchanged tree replays the cached verdict in
milliseconds instead of re-running the 3-minute mutant loop; any change
to the classifier inputs re-executes. cacheDir is pinned to .turbo/cache
so git worktrees stop silently sharing one cache with the main checkout.
flake.nix locks the entire toolchain (cargo, clippy, rustfmt,
cargo-mutants, gcc, node, pnpm pinned to packageManager) behind one
`nix develop`. CI's gate and mutation jobs provision from the same flake
instead of dtolnay/rust-toolchain plus a per-run `cargo install
cargo-mutants`, and the mutation job runs the gate through turbo with
.turbo/cache persisted by actions/cache, so re-runs and same-input PR
builds replay the cached verdict instead of re-executing mutants.
The actions/cache key now hashes the same input set the mutants task
hashes (sources, tests, manifests, flake.lock) instead of the commit
sha, so unchanged inputs restore an exact key, skip the post-job save,
and stop growing the cache store on every commit. flake.lock joins the
task inputs: nixpkgs pins cargo-mutants and rustc, so a toolchain bump
regenerates a different mutant set and must invalidate the cached
verdict. The pnpm store is cached too — pnpm install runs before turbo
can replay anything and was a cold fetch on every run.
Review fixes (ce-code-review, validated):

- turbo.json: add $TURBO_ROOT$/flake.nix to mutants inputs. flake.nix
  defines the toolchain; without it in the key a flake.nix-only edit
  replayed the previous verdict verbatim (probe: hash b956bf4d... stayed
  put, Cached (Local) = true). With it: miss + full re-execution.
- ci.yml: add flake.nix, turbo.json and the npm package.json (which
  carries the gate script) to the turbo-bag hashFiles key. turbo
  self-hashes the latter two, so an exact-key restore was suppressing
  the post-job save on config-only edits: fresh verdicts were re-run
  every time and never persisted.
- ci.yml: restore cargo build-state caching in the mutation job
  (origin/master had it; the turbo rewrite dropped it) keyed on
  Cargo.lock + flake.lock, sharing the gate job's cargo- restore prefix.
- ci.yml: set CARGO_BUILD_JOBS=4 on the mutation run line for parity
  with the gate job and deterministic env hashing.

Verified: mutants loop miss(117 green, 2m36s) -> hit(10ms) ->
flake.nix edit miss(2m36s) -> revert hit(8ms); one-shot gate green
(fmt, clippy -D warnings, 90 tests).
…oncept

Compound of this session's learning: the turbo-cached mutation gate hashed
the toolchain pin (flake.lock) but not the toolchain definition (flake.nix),
so flake.nix-only edits replayed stale verdicts. Doc captures the fix, the
two falsified adversarial hypotheses (script-body and env staleness — both
busted by hash-movement probes), the transport-superset invariant, and the
probe protocol. CONCEPTS.md gains the Verdict cache entry (two hash
surfaces).
First Actions run failed in both gate and mutation jobs: 'bash -lc' inside
nix develop -c re-sources the runner profiles, which re-prepend
~/.cargo/bin ahead of the dev shell PATH. The rustup shim then owned the
toolchain: it synced the stable channel mid-build, removed the previous
rust-std/rustc components, and every compile died with E0463 (can't find
crate for std). cargo-mutants hit the identical failure in its scratch
tree.

'bash -c' keeps the dev shell's PATH first; nix develop -c already
exports the full toolchain. Comment added at the first use so the flag
isn't reintroduced.

Verified locally in the exact new form: one-shot gate green (90 tests),
mutants cold miss (117 mutants, 113 caught, 4 unviable, 0 survived,
2m36s) then FULL TURBO hit in 9ms with CARGO_BUILD_JOBS=4 matching CI.
Run 2 failed differently from run 1: nix's cargo resolved rustc via PATH,
the dev shell never shipped one, and on Actions runners the rustup proxy
won the lookup — its stable toolchain then failed to link (std rlibs not
applicable/missing). Locally the same shell only worked because ambient
PATH satisfied the lookup by accident; 'type -P rustc' inside a clean
'nix develop -c bash -c' proved MISSING.

cargo resolves rustc from PATH; a toolchain-provisioning flake that lists
cargo without rustc is incomplete. Add rustc beside cargo with a comment
naming the failure mode.

Verified in a runner simulation (env -i, PATH=nix:/usr/bin:/bin, CI=true,
cold pnpm store, no rustup on any path): pnpm install 2.9s; mutants cache
miss -> full re-execution, 117 mutants green, 2m37s, exit 0; replay hit.
The flake.nix edit itself flipped the turbo input hash, so the gate
re-verified under the new toolchain set rather than replaying.
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.

1 participant