Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rullst CI/CD and Verification Contract

This document describes what the repository’s automation currently executes. It is not evidence that a workflow has passed for a particular commit. A green claim must always point to the GitHub Actions run, commit SHA, logs, and produced artifacts.

Last source-level review: 2026-09-20 UTC.

Status language

StatusMeaning
BlockingA failing command fails that workflow run. Branch protection still determines whether the check is required for merging.
Automated evidenceThe workflow runs automatically, but part of its result is external, uploaded, or deliberately non-blocking.
InformationalThe workflow is explicitly advisory and must not be described as a release gate. A manual trigger alone does not make a strict candidate check informational.
RoadmapThe idea is preserved, but this repository does not yet provide reproducible evidence for it.

The distinction matters: Kani, Miri, mutation testing, or a scanner can be very valuable without proving that the entire framework is panic-free, race-free, memory-safe, or compliant with a regulation.

Branch transition

On 21 September 2026, v12 was preserved from stable source 184bc1f7. The hosted main and v12 protections were read back with the same 43 required GitHub Actions checks as v13, an up-to-date base, administrator enforcement, conversation resolution and disabled force pushes/deletion. This is a settings observation, not a successful test receipt. Promotion of v13 source to main requires a normally admitted PR; existing tags, packages and running feature campaigns are retained. v13 remains transitional while that admission runs.

The default-branch Dependabot configuration targets Cargo (the root and the eleven fuzz workspaces) and Actions on main only; v12 receives no updates after its end of life on 16 October 2026. Minor/patch Cargo changes share a PR, the OpenTelemetry and utoipa families each have their own, and Actions share one tools PR. Updates still require compatibility/MSRV and protected CI; group membership is not approval. Old Dependabot PRs are closed after equivalent updates have entered main, or with a comment when its policy excludes them.

Mainline execution model

The development dashboard and its automatic status badges follow main. The declared automatic release workflows accept pushes and pull requests targeting main, stable maintenance v12, and transitional integration v13, and expose workflow_dispatch where a safe rerun is useful. Superseded runs of these workflows are cancelled per workflow and ref so rapid development does not spend runner capacity proving an obsolete commit.

Release policy schema 4 binds v12 to v12 and v13 to main. Historical schemas 2 and 3 retain their original bindings. Before building release artifacts, the tag, every inventory package version and the current protected branch head must agree. Exact-source workflow admission and the crates.io deployment approval remain mandatory. Fuzz receipts are limited to the candidate’s release line, including the source policy of reused runs.

ci.yml deliberately treats the expensive operating-system matrix differently. Format and Clippy continue to give feedback on draft pull requests. A ready pull request to main runs the fast Linux gate: the workspace and cli-standard shards, MSRV, strict database features, Redis/S3 contracts, generated access, facade composition and AI evals. Line coverage, SemVer and CodeQL skip pull requests to main. The resulting main push runs every Linux shard, public feature boundaries, the threat-model minimum, coverage, SemVer and CodeQL. A nightly scheduled run (05:30 UTC) executes the complete Linux/macOS/Windows matrix on the default branch. A maintainer can add the full-matrix label to a pull request to main to run that complete Linux/macOS/Windows test matrix (all seven shards on each system) before merge: the labeled event starts it, and every later push re-runs it while the label stays. The label changes only the Run Tests matrix; feature boundaries, the threat-model minimum, the scorecard and the release-only packaging jobs keep their pull-request guards. Every pull-request event, labeled included, uses the pull request’s concurrency group, so the newest run replaces the older one and still reports every required check (.github/test-ci-matrix.py evaluates the workflow’s expressions to check this). Adding any other label re-runs the same Linux gate rather than skipping it, because a skipped job would report a passing required check. Ready pull requests to the maintained v12 line and transitional v13 keep the complete matrix, blocking line coverage, SemVer fan-out and CodeQL before merge. Any matrix can be requested manually. Each operating system executes seven parallel shards: the non-CLI workspace (which includes the Blog, ERP, LMS, Portfolio and WebGPU examples), ordinary CLI targets, three public-profile groups and two generated-starter groups. The basic/relational/polyglot and foundation/product partitions retain every original case while bounding the longest Windows and macOS jobs. No test is omitted; this changes wall-clock scheduling rather than the assertions being executed. Hosted CI permits two nested compiler jobs for the otherwise serial generated profile/blueprint builds; constrained local runs retain their one-job default. Each CLI shard fetches the locked registry inventory before its generated applications prove that they compile without network access. After a reviewed commit is merged, the automatic main or v13 push runs Linux only; for main, macOS and Windows evidence comes from the nightly run. Release candidates must use the manual full matrix when no successful full-matrix run points to the exact candidate tree.

The SHA-bound quality scorecard is generated only by a ready pull request to a maintained line, the nightly run or a manual full-matrix run. It is deliberately skipped on Linux-only runs (automatic branch pushes and pull requests to main), because they cannot honestly award cross-platform verification credit. Run ci.yml manually on a final release-branch candidate to produce the exact-SHA release scorecard. Manual diagnostic runs may select one operating system and one test shard; those deliberately do not produce a full-matrix scorecard and do not replace final-candidate evidence. The manual cli-updates diagnostic selects discovery/cache/artifact tests, isolated project preparation/verification and legacy upgrade process fixtures. It does not replace the complete cli-standard shard in release matrices. Manual CLI-only shards also skip unrelated ORM/Redis/feature/threat/eval/facade and MSRV jobs, keeping correction runs bounded. Manual workspace and all selections, automatic runs and ready PRs retain those jobs. Strict workspace Clippy/format still runs for every diagnostic; release admission continues to require every job from the full all/all matrix.

The manual all/all matrix additionally packages the 20 release-inventory crates, including rullst-labs, audits their contents and uses the release pipeline’s archive-only consumer and isolated CLI installation/blueprint checks. Packaged distribution and installed CLI is a required exact-SHA release-admission job. It runs without registry publication credentials and never uploads to crates.io. Automatic development runs and other diagnostic subsets deliberately skip this expensive job. The explicit platform=all, shard=packaged-distribution diagnostic runs this same archive/installed-CLI check and workspace Clippy/format, without repeating the OS test matrix or native artifact builds. It emits no full-matrix scorecard and cannot satisfy complete release admission; the final all/all and tag pipelines retain their existing package verification.

Generated VM deployment (docker) and (podman) run .github/check-vm-deploy.py (about ten minutes each): they install the CLI from the packaged archives and, for the Blank and SaaS starters, run cargo rullst deploy, copy the project as the VM guide does, check .env.production with deploy:doctor, build the image with the generated Dockerfile and start the generated compose.yaml and Caddyfile. They check /health over HTTPS through Caddy (verified against Caddy’s local CA for localhost), the HTTP-to-HTTPS redirect, that the app port is not published, the read-only root filesystem, capabilities and no-new-privileges of both containers (read from /proc), Caddy’s trusted-proxy address, db:migrate, a database-backed page (SaaS: registration and sign-in with a Secure session cookie), that the SQLite database stays in its volume across docker compose restart (Podman: down and up -d, because podman-compose’s restart can leave Caddy stopped), the guide’s backup command and that no container logged a panic. The release crates enter the build context as packaged archives through [patch.crates-io]; the generated deployment files run unmodified. Docker uses the runner’s daemon; Podman runs rootless through podman compose with podman-compose and the net.ipv4.ip_unprivileged_port_start=80 setting the guide documents. Full manual runs, the nightly run and the manual vm-deployment diagnostic run both jobs; a ready pull request runs them only when it changes cargo-rullst/src/generators/deploy.rs, generators/project/docker.rs, blueprints/deploy.rs or the check (otherwise the job succeeds without building), and branch pushes skip them, so the push inventory that site-only admission reads is unchanged. They are not listed in the release policy’s required jobs, but a failure fails the full manual run that admission requires. Real DNS, Let’s Encrypt, provider accounts, Arm images and reboots are not covered.

Manual all-platform all and cli-standard selections also call the native CLI artifact builder for Linux x64, Windows x64, macOS ARM64 and macOS x64. The committed target inventory selects explicit runner labels. Both executable entry points must run and report the candidate version before bounded files, digests and source/platform metadata are retained. These ordinary CI artifacts have no release tag and no installation or publisher-verification authority. Exact full-candidate admission requires all four native jobs; a diagnostic CLI shard still does not replace the full matrix.

The tag-only release also calls this builder after protected release-branch admission. The separate attestation job verifies downloaded checksums and includes native executables/manifests in build provenance without executing source or binaries. The GitHub release job adds those assets only after attestation and registry publication succeed. This prepares distribution, not an installer; no new installation command or platform recovery claim is implied.

The existing Linux workspace shard also requires RULLST_UI_BROWSER_TESTS=1 with Node 24 and Chromium. It feeds HTML from the real Nexus renderer and the Portfolio example’s view (examples/portfolio) into the bounded .github/mobile-ui-browser-smoke.mjs contract: drawer dismissal/focus/no-JS behavior, responsive layout at 320–1440px, long content and reduced motion. Missing browser prerequisites fail that shard. Other platforms still run the render assertions and the complete existing project matrix; they do not claim Chromium evidence. Local runs opt in with the same flag and an absolute RULLST_UI_BROWSER_SCRIPT path. CDN resources are blocked in this UI fixture. The Nexus run serves the shell with the default production CSP and its real same-origin assets, and fails on a CSP violation; this is not live-provider, WebKit/Firefox, hardware-device, or WCAG certification. No extra Rust compilation matrix is added.

GitHub executes schedule events from the repository’s default branch, so scheduled checks follow v13 development on main. Stable v12 receives its own push/PR checks and explicit manual campaigns; a scheduled main run is not stable-line evidence. Tag publication remains deliberately unavailable through a manual button.

Manual and periodic execution map

Every verification workflow except the PR-context-only pr-security-evidence.yml and mutants-pr.yml, reusable cli-artifacts.yml and tag-only release.yml can be started from Actions → select workflow → Run workflow. A manual run checks the selected branch’s current SHA; record that SHA and the run URL before treating it as release evidence. The release workflow intentionally has no button because its publication authority begins only with an exact version tag.

Fuzz target jobs have one bounded exception to repeating expensive work on an unchanged input surface: verified evidence reuse. The candidate still requires its own successful fuzz workflow; the admission checker independently verifies every reused original job and source digest. No other workflow gains cross-commit credit from this exception.

The workflows below are started manually for release evidence. kani.yml and miri.yml also run weekly on main (next table), but release admission counts only a dispatched run:

WorkflowEvidenceRC interpretation
dast-zap.ymlOWASP ZAP baseline against a release blog showcase, a fresh generated REST API, a fresh generated SaaS starter (public pages, then its signed-in pages with a disposable account’s session) and the LMS example (examples/lms, release-built from the workspace)REST/SaaS/LMS warnings and failures block unless an exact rule ID is versioned as INFO with a local explanation in .zap/; those configs are passed explicitly to the pinned scanner and unlisted warnings remain live. The showcase is informational because it deliberately uses third-party presentation assets; reports and application logs are retained. This remains representative, not universal deployment coverage.
fuzzing.ymlAll 42 declared libFuzzer targets from the validated shared inventoryRequired release evidence: release mode validates all 42 targets and independently verifies reusable input-equivalent evidence; packages with remaining targets run locked compilation preflights and new 5.5-hour campaigns; target-specific corpora are restored and saved, while failure reproducers are retained. Dependency-lock drift fails preflight and campaign jobs. The proc-macro parser uses strict processes of at most 30 minutes sharing one corpus, which bounds sanitizer RSS without weakening the total budget. Original reused results must be at most seven days old, come from an ancestor on the same release line, and prove the complete budget. The final candidate still runs the evidence boundary and publication independently recomputes all 42 results. Reuse policy. A strict five-minute single-target diagnostic accelerates correction feedback but is explicitly ineligible as release evidence. This is bounded evidence, not proof for every input.
kani.ymlTwenty named bounded formal harnesses in ten supported runtime/library packagesRequired release evidence for the declared harnesses: every proof has an isolated strict matrix job. Rullst itself stays on stable Rust 1.99.0 with a Rust 1.96 MSRV; only the separately built Kani verifier uses its pinned nightly-2026-08-01 compiler (rustc 1.99.0-nightly) because the latest stable Kani bundle’s Rust 1.93 compiler cannot compile the framework. The proc-macro-only rullst-macros target remains unsupported by Kani and is covered by compile-pass/fail and generated-project evidence instead.
miri.ymlRandomized-layout Miri execution over 15 named pure-Rust/default-feature scopesRequired release evidence for the declared scopes: every selected scope is strict. This nightly-only interpreter uses pinned nightly-2026-08-21 (rustc 1.100.0-nightly); it does not change the project’s stable toolchain or MSRV. Native FFI, OS syscall, network/provider, umbrella re-export, and example-application boundaries are excluded explicitly rather than emitted as tolerated errors.
mutants.ymlA source-bound discovered inventory, eighty lossless shards with at most four running concurrently, their artifacts and a strict aggregateInformational: a cheap all-feature --list --json preflight records the selected source’s complete unique inventory before runners start; every shard then uses that release surface, and aggregation requires every reviewed candidate to receive exactly one classification before reporting the conservative caught percentage. A targeted mode retests one validated production Rust file after a correction, with explicit per-mutant test/build timeouts and an optional library-only test scope; it does not replace the complete campaign. Every shard has a pinned Redis service, so the RULLST_TEST_REDIS_URL contracts run for each mutant. A contract mode runs one owned-server wrapper’s mutation mode, which mutates only that wrapper’s PostgreSQL or Redis adapter files and runs only its contract tests against the server it owns; it checks its own outcome count instead of the workspace inventory and aggregate. Recovery modes accept only the repository-reviewed campaign policy, bind the source/run/branch/tool/inventory digest, bisect explicitly authorized failed fragments, reuse immutable successful artifacts and emit a content-addressed aggregate. Missed/time-out exit codes remain findings, while a broken baseline, incomplete artifact set, preflight/classification mismatch, invalid invocation or cargo-mutants internal failure fails the workflow. The v12 campaign completed 14,391/14,391 classifications in run 34761010296; “pass” does not mean every mutant was killed.

The v13 candidate also provides verus.yml as a manual-only pilot. Its source linkage/proof/negative-control checks are strict when invoked, while the pilot remains outside release admission. A new workflow must first be available in GitHub’s default-branch workflow registry before its manual button/dispatch can be assumed available; prepared YAML is not hosted evidence.

These workflows are periodic and manually runnable:

CadenceWorkflowsMode
Dailyci.yml, sanitizers.yml, supply-chain.ymlSupply Chain (Cargo Audit of the production and fuzz locks, Cargo Deny and the advisory-exception check) is blocking; the nightly ci.yml run executes the complete Linux/macOS/Windows matrix on main; TSan/ASan are blocking when executed.
Weeklybench.yml, codeql.yml, coverage.yml, crypto-containment.yml, documentation.yml, kani.yml, miri.yml, proptest.yml, scorecards.yml, trufflehog.yml, udeps.ymlThe inventory below identifies which results are blocking, automated evidence, or informational. Miri (Wednesday 01:17 UTC) and Kani (Thursday 01:47 UTC) run their full strict matrices so that a regression shows up within a week rather than at release admission.

All remaining test/build workflows run on the documented push, pull-request or path filters and also expose a manual rerun. For an RC or stable checkpoint, first use the automatic mainline suite, then run the exact-SHA manual admission set: full-platform ci.yml, release-mode fuzzing.yml, dast-zap.yml, kani.yml, miri.yml, sanitizers.yml and proptest.yml. The tag workflow checks those receipts through .github/release-required-workflows.json; the fuzz receipt is accepted only when all 42 named target jobs and its evidence boundary succeeded. A scheduled run of ci.yml, sanitizers.yml, kani.yml, miri.yml or proptest.yml is never a receipt, even on the candidate commit: admission accepts only workflow_dispatch runs, and each scheduled run has its own concurrency group (-nightly or -weekly suffix), so it cannot cancel, or be cancelled by, the dispatched admission run on main. Physical devices, store approval, live provider accounts, external security review and human release approval remain outside GitHub Actions.

Required local and release baseline

The contributor baseline from AGENTS.md is:

cargo test --workspace --all-features
cargo clippy --workspace --all-features -- -D warnings
cargo fmt --all

The main CI uses the stricter all-target Clippy form and checks formatting without modifying files:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features

Rust CI disables Cargo incremental compilation and uses the pinned sccache Action and binary to store content-addressed compiler outputs in GitHub Actions cache. The current cc build dependency also honors the same Rust compiler wrapper, so compatible bundled DuckDB C++ objects can be reused. Same-repository pull requests can populate only their GitHub-isolated refs/pull/.../merge cache scope, making a failed-job rerun useful without modifying the trusted default-branch namespace. Fork pull requests remain read-only. Pushes and explicit manual runs on main populate entries reusable by later pull requests. Cache contents never substitute for a test result. The tag-only verifier uses the same namespace strictly read-only, so it can reuse an exact trusted compiler output but cannot alter the cache while creating release artifacts; every release command and assertion still runs.

The setup action’s job-scoped Cargo archive is disabled in these compiler-cache jobs, so neither the raw workspace target tree nor duplicate registry bundles compete with compiler objects. CLI integration tests create and remove nested Cargo targets, and archiving whole mutable trees previously produced false missing-directory annotations, duplicated roughly 9.56 GiB across twenty active main caches, and caused eviction churn at GitHub’s default 10 GiB repository limit. Cargo may redownload registry sources on a fresh runner; this small network cost is preferable to storing the same registry/target archive under many job-specific keys. A first run on a new cache namespace is still a cold build; evaluate acceleration using the reported cache hit ratio and a later compatible run, never by weakening or omitting assertions.

The same content-addressed approach accelerates LLVM coverage, benchmark, mutation/fuzz compilation and the scheduled release-mode regression suite. Benchmarks remain sequential on one runner so comparisons do not mix host variance. Coverage deliberately remains one report job because splitting it without a reviewed profile-data merge could change the repository percentage. Its second default-feature pass is limited to ORM, Studio and the public facade: those are the packages with default-SQLite tests excluded by the mutually exclusive all-feature graph, so unrelated workspace tests are not repeated. The release-mode workspace is safe to split because every shard returns an ordinary test result, while the two source locations that actually use proptest! still receive their configured 10,000-case runs. SemVer checks fan out from the machine-readable release order and validate one published API per job, so adding or removing a release package cannot silently drift from the matrix.

The all-feature coverage pass uses pinned cargo-nextest to schedule the same discovered unit and integration tests concurrently. It performs no retry and a flaky retry could not be normalized into success. A repository profile fixes four global slots and permits at most two integration tests that launch nested generated-application Cargo builds, preventing compiler fan-out from trading latency for memory or disk exhaustion. The exact per-test JUnit result and duration record is retained for 30 days. This runner is a coverage scheduler, not a replacement for Rust CI: the required multi-platform shards continue to execute the complete inventory with ordinary cargo test, preserving both traditional libtest process semantics and the materialized-project gates.

CodeQL runs separate Rust and JavaScript/TypeScript analysis jobs. The Rust job’s Cargo target cache is disabled so the extractor observes compilation for the exact SHA instead of inheriting a fresh artifact from another run; the analysis database itself is not interchangeable with ordinary test shards. Rust CodeQL’s faster buildless mode is intentionally not used because manual compilation gives the extractor the stronger generated code boundary needed by this release. The JavaScript job uses buildless analysis and retains the /language:javascript category to compare against existing mainline results without leaving that language unexamined.

ci.yml also compiles and exercises each ORM strict database feature in isolation (PostgreSQL, MySQL, and SQLite), exercises the runtime-only Core and all 70 public umbrella features in isolated additive graphs with automatic manifest-drift detection, runs the portable database matrix on Linux, and tests the complete all-feature workspace in seven parallel shards on Linux, macOS, and Windows. Feature-boundary rows and threat-model negative tests also fan out into four deterministic strict shards each; their matrix job remains a single blocking dependency for the quality scorecard. Each threat-model shard primes the reviewed lockfile before its deliberately offline generated-project checks, so it does not inherit a hidden source-cache dependency from another job. The umbrella’s cfg(doctest) aggregation reads all 48 public tutorial files directly, so that same command discovers the versioned Rust blocks, compiles or executes complete examples, and records explicitly contextual fragments as ignored instead of pretending they are standalone programs. Its pinned live Redis job also proves that scheduled Core jobs are not claimed early, that Core cache inspection returns bounded metadata without values, plus ORM cache hit/TTL/recovery, tenant/table invalidation, rollback preservation, process-local post-commit observers and Scout commit ordering. A separate SQLite outbox contract runs on all three operating systems and covers atomicity, conflicting idempotency keys, claim races, lease expiry, retry and dead-letter; the relational matrix repeats the core outbox lifecycle against PostgreSQL, MySQL, MariaDB and strict SQLite. A dedicated job checks the declared MSRV, Rust 1.96.0. The Linux provider matrix also runs the feature-gated Scout adapter against a digest-pinned Meilisearch image; Algolia and Elasticsearch use bounded local protocol fixtures because no hosted provider account is part of CI. The same matrix runs typed, parameterized L2 and cosine queries against a digest-pinned PostgreSQL + pgvector image. It also runs Nexus’s default Any/SQLite HTTP contract explicitly, because the global all-feature graph intentionally selects a strict database profile and excludes that materialized tenant/audit target. Coverage separately merges the default workspace pass, so those routes contribute real executed-line evidence.

After a ready-PR or manual full-matrix Rust CI run finishes, an observational job emits a SHA-bound per-crate quality scorecard into the workflow summary and a 90-day artifact. The score combines versioned expert-audit ceilings with the actual gate results; a failed/skipped/cancelled gate can remove the dimensions it was meant to prove, while a green gate cannot inflate a crate beyond its audited ceiling. This is engineering-evidence reporting, not capability completion or certification. See the scorecard methodology.

Rows with no feature selected compile every package target. Feature-selected rows compile the isolated library graph; feature-enabled tests, examples, and benchmarks remain covered by the workspace and specialist jobs. This avoids pulling unrelated development dependencies into every boundary while retaining real integration coverage.

The tag-only packaged-distribution gate reads the complete feature set from the extracted rullst package manifest and compiles that crates-only consumer with defaults disabled and every public feature enabled. A partial hand-maintained feature allowlist therefore cannot make a monorepo-only integration appear release-ready.

main (v13 development) requires the fast gate of 19 checks that every ready pull request emits: Code Quality & Format; Run Tests (ubuntu-latest / workspace) and (ubuntu-latest / cli-standard); Check MSRV (Rust 1.96.0); ORM strict-sqlite, strict-postgres and strict-mysql; Redis live contracts; Generated release access boundaries; Facade shared-local recovery composition; Versioned deterministic AI evals; Build book and validate local links; Validate workflow syntax and expressions; Enforce Zero Panics (published runtime targets); Enforce production unsafe boundary; Check Dependencies (Licenses, Vulnerabilities); Scan for leaked secrets; Enforce reviewed internal dependency graph; and Typo Check. It does not require an up-to-date base: the full main push run and the nightly matrix detect interactions between independently green pull requests, and each additional rebase would otherwise repeat the whole gate. Cargo Deny’s job in supply-chain.yml is the required vulnerability gate. Until 13.0.0-alpha.4 the two Cargo Audit workflows both emitted an ambiguous audit context; their jobs now have distinct names (Cargo Audit (production and fuzz locks), Advisory exception governance), so either could be required without a collision. The macOS/Windows and CLI-profile shards, public feature boundaries, threat-model minimum, coverage, SemVer and CodeQL run after merge, nightly or weekly. They remain release-admission requirements.

v12 (frozen; end of life on 16 October 2026) keeps the complete profile of 46 checks with an up-to-date base: every Linux/macOS/Windows test shard, public feature boundaries, the threat-model minimum, CodeQL, documentation and workflow lint.

Do not configure a path-filtered, scheduled, manual, deployment, or tag-only workflow as a universal required check: an intentionally skipped workflow may never create the check context. In particular, IoT and Vault Crypto Containment is blocking when relevant paths change. Pages, benchmarks, fuzzing, sanitizers, Kani, Miri, mutation testing, udeps, ZAP, Scorecard, and release provenance belong to deeper evidence or release policy. GitHub repository rulesets remain the enforcement source; this document records the recommended profile. On 29 October 2026 the hosted rulesets matched it: the development-branch ruleset on main and the maintained-branch ruleset on v12 (see GOVERNANCE.md).

Observed v13 protection

On 20 September 2026 UTC, the hosted v13 branch protection was enabled and read back from the GitHub API. It requires 43 existing GitHub Actions contexts: the 25 Linux runtime jobs, all sixteen corresponding macOS/Windows test shards, documentation and workflow lint. Contexts are bound to GitHub Actions, the base must be current, a pull request and resolved conversations are required, and administrators are included. Force pushes and branch deletion are disabled. No additional human approving review is required. These are hosted settings at that observation, not a guarantee that an administrator cannot change them.

This initial merge profile does not replace the release policy’s 28 complete workflow receipts, manual native/security jobs, package inspection or protected crates.io deployment approval. The additional automatic security workflows are enabled on v13 by this preparation change; the profile above names only checks already emitted by the integration candidate. The v13 branch was later retired into main, and the release policy now requires 25 workflow receipts (.github/release-required-workflows.json).

Phase 4 release-engineering status

Release-engineering goalCurrent statusAssessment
Trifecta with all featuresImplementedCI and tag release both run format, all-target/all-feature Clippy, and all-feature tests.
Strict DB features in isolationImplemented in workflowstrict-postgres, strict-mysql, and strict-sqlite compile independently and each runs a backend-specific CRUD test with only the selected strict feature enabled.
Honest blocking/informational labelsImplementedUnsafe and Wasm checks are blocking in continuous CI; the declared Kani, Miri and fuzzing scopes are strict release gates; mutation testing and udeps explicitly remain informational.
Cover every fuzz targetImplemented in workflow.github/fuzz-targets.json is the inventory for the manual campaign and the workflow-lint and Rust CI lock checks. A blocking validator compares it with all eleven fuzz manifests and their 42 source files. This records configuration, not a successful six-hour run.
Package all crates before publishingImplemented in workflowThe tag-only release validates versions, packages all publishable workspace crates, hashes and attests the archives, then publishes in dependency order.
Unified evidence bundle per tagImplemented in workflowThe tag-scoped bundle contains Cargo.lock, Cargo metadata, CycloneDX 1.5 SBOM, Cargo Audit JSON, deny.toml, bounded compliance evidence, governed advisory exceptions, commit/tag context, and checksums. The bundle and .crate archives are included in build-provenance attestation.
Align manifest, changelog, tag, registry, and notesImplemented in workflowPreflight validates manifests and tag, extracts the exact dated changelog section into the GitHub release, and verifies registry checksums after publication.

Important evidence boundaries

Zero-panics and unsafe Rust

zero-panics.yml denies Clippy’s unwrap, expect, panic, todo, and unimplemented lints for published runtime libraries, procedural-macro engines, CLI production targets, generated runtime templates, and the Wasm Core path. Tests are excluded where assertion panics are test semantics.

unsafe-policy.yml compiles production libraries and binaries with -Dunsafe-code. The exact reviewed source allowlist contains the Radar OS probe, dynamic-library loader, the CLI’s Windows cache/installation owner/DACL boundary, Windows project access-policy preservation and macOS extended-ACL inspection. Both inner and outer unsafe-lint attributes enter the source inventory. The macOS module borrows an open descriptor, inspects the first ACL entry and frees the returned allocation; it never edits an ACL. Its native regression adds an ACL and verifies that replacement rejects without discarding it. Windows descriptors are installed atomically and validated through owned handles. Each unsafe call documents pointer/handle ownership and lifetime; the workflow fails if the source allowlist changes. Windows ACL counts come from GetAclInformation into owned output structures; ACE pointers must be non-null before creating bounded borrowed views. SID lengths are checked before OS validation and no borrowed pointer outlives its descriptor. This is an enforced boundary, not a claim that all dependencies contain no unsafe Rust.

Coverage

coverage.yml runs LLVM coverage over workspace all-features and default profiles plus the live database matrix, then uploads LCOV to Codecov using GitHub OIDC rather than a long-lived upload secret. It also retains exact JSON and text line summaries for 30 days so a passing upload cannot be confused with the coverage percentage. The all-feature pass uses pinned cargo-nextest only to run the same discovered tests concurrently, with zero retries, bounded nested-Cargo concurrency and a retained JUnit execution record. Rust CI still runs ordinary cargo test across Linux, macOS and Windows. The default-profile pass explicitly includes ORM, Studio, Nexus and the umbrella facade so their real SQLite contracts are not hidden by mutually exclusive all-feature database profiles. Before upload, the workflow independently rejects an LLVM summary below 90% for either the whole repository or the governed framework-library paths. codecov.yml also requires at least 90%, with zero tolerance, for those views and changed lines; failure to upload LCOV also fails the workflow. The report filters examples, benchmarks, auxiliary test support, and separate test files. CLI and proc-macro code therefore remains part of the blocking repository aggregate and is additionally visible as informational components. Their stronger semantic evidence still comes from materialized scaffolds and compile-pass/compile-fail contracts. The README exposes both the public overall badge and the separate framework_libraries badge rather than substituting the higher component result for the repository total.

Formal, dynamic, and stress analysis

  • Kani and Miri are bounded evidence scoped to the harnesses/packages that actually execute. Both run weekly on main so that a regression appears between releases; release admission still requires a dispatched run on the exact candidate commit. Rullst itself remains pinned to stable Rust 1.99.1 and keeps Rust 1.96 as its declared MSRV. Kani builds reviewed upstream revision 8fcd6d90ed07b559e553ca8a92b95f2db69b2c78 with the verifier’s own pinned nightly-2026-08-01 compiler (rustc 1.99.0-nightly) into a bundle and installer, then treats proof failures in twenty isolated harness jobs across ten supported packages as real matrix failures. That revision intentionally predates a compare_bytes compiler crash reproduced with Kani’s first rustc 1.100.0-nightly snapshot. The workflow does not patch manifests, bypass MSRV data, or change Rullst’s stable toolchain. Kani cannot verify the proc-macro-only rullst-macros target. Miri, which only runs on nightly Rust, pins nightly-2026-08-21 (rustc 1.100.0-nightly) and strictly executes 15 named pure-Rust/default-feature scopes. Its matrix excludes native ring, AWS-LC, SQLite, OS-syscall and network/provider execution that Miri cannot interpret; native CI, integration tests and sanitizers remain the applicable evidence for those paths. The rullst umbrella re-export facade and Blog example add no separate interpreter scope. A selected-scope failure fails the run. The Kani Security harnesses prove pure production decisions such as Vault key-ID character policy, DLP buffer admission, ASCII-folded RASP matching, SRI asset limits and bounded Login Guard delay; the IoT matrix also proves the complete CoAP option-component classification. They do not claim that Kani verifies zeroize’s unsupported inline assembly, cryptographic implementations or the entire concurrent middleware implementations.
  • Mutation testing is manual, split into 80 lossless shards with at most four running concurrently, and intentionally informational. A new campaign discovers its selected source’s inventory; the historical 14,391-candidate receipt is not a permanent limit for later sources. Before the expensive matrix starts, a fail-fast --list --json preflight verifies the exact unique candidate set; the final aggregate must match that reviewed list, not merely its count. Full mode optionally accepts a complete source_sha that must be an ancestor of the workflow commit, allowing a repaired controller to measure an immutable published release. Provenance distinguishes the measured source from the workflow source. Targeted and recovery modes reject that override; recovery still enforces its reviewed historical digest. The hosted command makes --all-features explicit and .cargo/mutants.toml applies the same feature policy locally; the ignored legacy root configuration and its exclusions were not silently activated. Targeted mode accepts exactly one tracked production .rs path so a correction can be retested without restarting the complete workspace campaign. Each targeted run uses its own per-file concurrency group, an explicit per-mutant test timeout (timeout_seconds, default 300 s, which also bounds the unmutated baseline) and a 300 s build timeout, so one hanging mutant cannot consume the job budget. Its optional test_scope=lib runs only the package’s library unit tests (cargo test --lib) to avoid rebuilding generated projects in slow integration suites; that narrower scope can report a mutant as missed when only an integration test would catch it, and such a mutant still gets a unit test. Each targeted test binary runs under an 8 GiB address-space limit (prlimit as the Cargo test runner), so a mutant that allocates without bound aborts its own tests instead of exhausting the runner. An optional shard=k/n slice (n at most 16) splits a file that is too slow for one job; the preflight inventory and the aggregate then cover exactly that slice. Every full and targeted shard has the pinned Redis service of ci.yml’s Redis job and sets RULLST_TEST_REDIS_URL, so the Security rate-limit and Core cache inspection Redis contracts run in the baseline and for every mutant (Core queue scheduling and the ORM Redis data matrix start their own testcontainer). The ORM query-cache and hash-scope Redis contracts need the non-strict pool, so the all-feature build compiles them out and no mutation run reaches them. Contract mode covers the adapters that only a wrapper-owned server reaches. Recovery is governed by a committed, versioned policy binding the originating and continuation runs, attempts, branches, workflow identity, measured source SHA, cargo-mutants version, exact shard set and inventory digest. It bisects only authorized incomplete fragments and combines them with successful immutable artifacts. The content-addressed mixed aggregate still has to match the complete reviewed inventory exactly once. Exit statuses for missed and timed-out mutants remain findings; baseline, usage and internal failures do not get normalized into green jobs. The aggregate also fails closed when an artifact is absent, a shard is incomplete or the reviewed full inventory drifts; its conservative percentage never treats a timeout as caught.
  • Fuzzing pins nightly-2026-08-21 instead of following a moving nightly alias. This verifier-only toolchain does not change the framework’s stable Rust 1.99.1 toolchain or its Rust 1.96 MSRV. Before a release campaign starts, eleven strict preflight jobs compile every target so a stale import or broken fuzz manifest fails in minutes rather than alongside hours of valid campaigns. A five-minute one-target diagnostic is correction feedback only; the evidence-boundary job refuses to call it release evidence. Each of the eleven fuzz packages has a checked-in dependency lock; inventory validation requires it, and locked metadata plus a post-command drift check prevents preflight or campaign jobs from silently resolving a different dependency graph. The parser campaign restarts its ASan process every 30 minutes while retaining one corpus and the full 5.5-hour target budget, preventing instrumentation RSS accumulation from masquerading as a parser crash.
  • Branch coverage, cargo-udeps, TSan and ASan share the reviewed nightly-2026-08-21 analysis snapshot instead of following a moving nightly alias. The first two remain observational/informational; sanitizer failures remain blocking whenever their daily/manual matrix executes.
  • cargo-udeps is weekly/manual and explicitly non-blocking.
  • TSan and ASan run daily/manual across twelve runtime/domain packages; Messaging runs its integration contract so its concurrent state is actually exercised rather than reporting a zero-test library pass. There is no MSan job in the current sanitizer workflow.
  • The manual ZAP workflow materializes, release-builds and migrates a fresh REST API and a fresh SaaS starter through the real CLI, and release-builds and migrates the LMS example from the workspace (the LMS starter moved to examples/lms in 13.0.0-alpha.4). The generated manifests name the framework crates as a published CLI writes them, and .github/dast-generated-app.py resolves them from the checkout through [patch.crates-io], so the build is the same before and after a stable release. The SaaS starter runs in production with placeholder Stripe settings (production refuses mock billing; no checkout is reached). One baseline crawls its public pages (pricing, sign-in, registration, password reset); its form submissions fail the CSRF check because ZAP does not return the Secure cookie over plain HTTP. A second baseline sends the session cookie of an account registered for the run with every request (ZAP_AUTH_HEADER), so it crawls the dashboard and password-change pages and submits the forms with a valid CSRF token. .zap/saas-starter-account.tsv puts only the POST-only sign-out and billing commands out of scope (signing out everywhere would revoke the scanning session; checkout and portal open a provider session), and the session must still open the dashboard after the scan. These baselines fail on any warning/failure, preserve INFO observations and use no ignored rules. The release blog showcase is scanned separately but remains informational because its documented presentation boundary deliberately uses a relaxed CSP and third-party assets. Its rules retain those external-asset findings for review and reduce only evidenced token/state signals or escaped showcase reflections to INFO; they do not hide findings with IGNORE. These four targets are representative evidence, not coverage of every starter, authenticated role (Nexus administrators are not scanned), browser, proxy or deployment.
  • Property tests and benchmarks are scheduled/manual evidence. The property workflow preserves the complete all-feature release-mode regression suite in seven parallel shards and separately runs the ORM and Connect property contracts with 10,000 generated cases. The eight published benchmark groups, backed by nine Criterion binaries, emit non-blocking alerts at a 20% regression and feed the public benchmark hub; they are not a promise against every nanosecond-level regression.

Pull-request mutation gate

mutants-pr.yml keeps the campaign’s result from decaying between campaigns: on every pull request to main that changes Rust, it mutates only the changed lines and fails when a mutant survives. Resolve a red result before merging, with a test that fails with the mutant or, when the mutant is truly equivalent, with an entry in the list below. Branch protection does not require the check yet. .github/mutants-pr-exclude.txt lists the files it skips: the workspace example applications, test-only modules and adapters exercised only by wrappers that own a disposable PostgreSQL, MySQL or authenticated Redis server. Those files remain the manual campaign’s responsibility, through its contract mode.

The CLI’s integration suites build generated projects, which is too slow to repeat for every mutant, so .github/mutants-pr.sh runs two passes. The workspace pass tests each mutant with its package’s whole test suite and leaves cargo-rullst out; the CLI pass mutates only the files listed in .github/mutants-pr-cli.txt and tests them with the CLI’s library unit tests (cargo test --lib), the scope of a targeted mutants.yml run with test_scope=lib. A CLI file joins the list once such a run leaves no missed mutant apart from accepted equivalents; the other CLI files are not mutated by the gate.

Accepted equivalent mutants

The v13 focused campaign (targeted runs of security and correctness files before 13.0.0-alpha.1), the local whole-crate campaign that followed it (rullst-iot, rullst-privacy, rullst-labs, rullst-supervision), the security campaign (targeted runs of every rullst-security file), the rullst-auth campaign (targeted runs of every file outside the PostgreSQL adapters), the rullst-core campaign (targeted runs of its security and correctness files and, before 13.0.0-alpha.3, of every remaining file the first pass measured, with the Redis service), the rullst-nexus campaign (targeted runs of every file; the all-feature build selects the PostgreSQL pool, so the PostgreSQL SQL matrix test reaches the database paths wherever Docker is available), the rullst-capital campaign (targeted runs of every file; mutants that only a live Stripe API response can reach are left to provider sandbox testing), the rullst-messaging campaign (targeted runs of every file, with the recurring PostgreSQL and Redis Streams contract runs for those adapters), the service-backed campaign (the passkey-postgres, auth-recovery-postgres, privacy-postgres and mail-postgres contract runs, and targeted runs of the Redis rate limiter with the Redis service) and the sensitive-file campaign (targeted runs of the OAuth/OIDC, attachment, link-security, tracking, egress and guardrail files of rullst-connect, rullst-mail and rullst-ai; the peer-address, status and content-type checks of EgressFetcher::fetch_bytes run only after a connection to a public HTTPS origin, which the egress policy refuses to loopback test servers, and are left to live integration testing; in rullst-core, the pool_ready guard of run_artisan_command needs an already initialized ORM pool, which the all-feature build can only open against a PostgreSQL server: rullst-core/tests/artisan_open_pool.rs opens one in a PostgreSQL testcontainer and runs db:status in a child process, so runs with Docker measure the guard, and runs without it skip the test unless RULLST_REQUIRE_TESTCONTAINERS=true), the cargo-rullst campaign (targeted runs, with the CLI’s library unit tests, of the self-update, audit, add and consumer-file, doctor, deploy and upgrade files; it covers the files .github/mutants-pr-cli.txt lists, and ROADMAP.md names the survivors left in the others) and the Studio remote-mode campaign (targeted runs of every rullst-studio/src/remote file and of the rullst-studio binary with the package’s tests) gave every missed mutant in their files a test, except these. Each change has no observable effect in the all-feature Linux build the campaign measures, so a test cannot tell it apart. Keep this list short: a new entry needs the same one-line justification. The pull-request gate (mutants-pr.yml) skips an entry only when its exact cargo-mutants name is also listed in .github/mutants-pr-equivalents.txt.

MutantWhy it is equivalent
rullst-core/src/queue/deferral/window.rs: TimeWindow::length_ms, > with >=TimeWindow::daily rejects equal start and end times, so the two never compare equal.
rullst-core/src/queue/deferral/window.rs: TimeWindow::intervals, day < until_ms with <=The extra day starts at or after until_ms, so it never yields an interval.
rullst-core/src/testing/html_normalize.rs: mask_csrf_headers, found < copied with <=copied indexes the closing quote of a masked value, so no x-csrf-token key can start there.
rullst-media/src/s3/signing.rs: settings with Default::default()Adapter URLs hold only unreserved characters, %XX and / without empty or dot segments, so single or double encoding and path normalisation give the same canonical URI.
rullst-mail/src/facade/driver.rs: DigestHasher::finish with 1finish is never called; the digest is read from the SHA-256 state.
rullst-mail/src/facade/driver.rs: delete the (Some(_), Some(_)) arm of ses_credentialsThat arm exists only without the aws-ses feature, which the all-feature build does not compile.
cargo-rullst/src/generators/audit_report/a11y.rs: attributes, the end index + 1 after > with index - 1 or index * 1The tag scanner resumes at the next <; the byte before a closing > cannot open a tag, because a tag name never starts with >.
cargo-rullst/src/generators/green/procfs.rs: ticks_per_second, > 0 with >= 0The clock tick rate is always positive.
cargo-rullst/src/generators/green/procfs.rs: the non-Unix ticks_per_second with Some(0) or Some(1)Not compiled on Linux.
cargo-rullst/src/generators/doctor/probe.rs: run, stdout.len() > OUTPUT_LIMIT with >=An output of exactly OUTPUT_LIMIT bytes is cut at its own length, which leaves it unchanged.
cargo-rullst/src/generators/doctor/database.rs: tcp_reachable, delete the TimedOut | WouldBlock armTcpStream::connect_timeout reports its timeout as a TimedOut error whose text is connection timed out, which the catch-all arm passes through unchanged, and never returns WouldBlock on Linux.
cargo-rullst/src/generators/output_guard.rs: write_output, the NotFound guard of fs::metadata with truereject_symlink has already returned every other lstat error, and for a path that is not a symlink stat fails exactly when lstat does.
cargo-rullst/src/generators/output_guard.rs: write_output, the !parent.as_os_str().is_empty() guard with truetempfile creates its file for the empty directory "" in the current directory, as it does for ".".
cargo-rullst/src/generators/consumer_files.rs: reject_links, the NotFound guard with trueAny other lstat failure of an ancestor (permission, not a directory, name too long) recurs with the same error when the path itself is read or created.
cargo-rullst/src/update/project/permissions_unix.rs: xattr_names, size > MAX_XATTR_NAMES with == or >=Linux never reports a name list longer than 64 KiB, and a list of exactly 64 KiB cannot hold only security.selinux, so has_unreproduced_attribute refuses it with the same error.
cargo-rullst/src/update/cache/unix.rs: options, NOFOLLOW | NONBLOCK with ^The two flags occupy disjoint bits, so OR and XOR give the same value.
cargo-rullst/src/update/cache/unix.rs: load_at, FILE_LIMIT + 1 with - or *validate_file has already refused a file longer than FILE_LIMIT. Reading one byte less shortens only a file of exactly FILE_LIMIT bytes, whose body decode refuses either way because it allows 64 bytes less, and a file that grows during the read is refused either way.
cargo-rullst/src/generators/audit_source.rs: tokenized_views and mark_code_tokens, index < tokens.len() with <=, and tokenized_views, index = last + 1 with last - 1 or last * 1At tokens.len() neither attribute lookup finds a token, so the loop steps past the end or stops. Resuming on an item’s last token or the one before it revisits its terminating ; or { .. }, which cannot start #[cfg(test)]; at the end of the input a re-found attribute only adds a range inside the item already blanked.
cargo-rullst/src/generators/build/upgrade/backup/restore.rs: is_link with false, and its Windows reparse-point test with & replaced by | or ^ or != by ==On Linux lstat never reports a symlink as the regular file or directory that validate_file and validate_directory_creation also require, and the Windows test is not compiled.
cargo-rullst/src/generators/build/upgrade/backup/restore.rs: restore_from, the index read’s MAX_INDEX_BYTES + 1 and the staging copy’s expected_size + 1 with *The index and each snapshot have already been bounded by their metadata, so reading one byte more matters only for a file that grows between that check and the read, which a test cannot arrange.
cargo-rullst/src/generators/deploy_doctor/input.rs: linked with false and its Windows reparse-point test; in read, the ancestor linked(..) || !is_dir(), the file !is_file() || linked(..) || len > MAX_FILE and the opened-file re-check with &&, NOFOLLOW | NONBLOCK with & or ^, and MAX_FILE + 1 with *On Linux lstat never reports a symlink as a file or directory, and the Windows test is not compiled. A non-directory ancestor fails the final lookup with NotADirectory, and a directory, FIFO or oversized file that passes a weakened check is refused after opening or reading with the same File error. The open flags, the re-check and the extra byte matter only when the file is replaced or grows between lstat, open and the read, which a test cannot arrange.
cargo-rullst/src/update/project/snapshot.rs: the non-Windows linked with false; in read, NOFOLLOW | NONBLOCK with & or ^, the opened-file re-check !opened.is_file() || linked(..) || len != ... with &&, and MAX_FILE + 1 with *lstat never reports a symlink as a file or directory, so the type checks refuse links either way. read has already refused links, special files and files over 64 MiB, so the open flags, the re-check after opening and the extra byte matter only when the file is replaced or grows between lstat, open and the read, which a test cannot arrange.
rullst-studio/src/remote/history.rs: Sample::from_outcome, window.requests > 0 with >=With no request the rate is 0 / 0, which finite drops, so the sample stores no rate either way.
rullst-iot/src/coap.rs (CoapRequest::encode twice, append_option), rullst-iot/src/mqtt.rs (MqttPublish::encode) and rullst-iot/src/i2c.rs (I2cHelper::try_build_read_frame): | with ^The operands occupy disjoint bits, so OR and XOR build the same byte.
rullst-iot/src/coap.rs: CoapRequest::encode, payload.len() > MAX_COAP_DATAGRAM_BYTES with == or >=CoapRequest::payload already bounds the payload, and ensure_room rejects the same lengths with the same error.
rullst-iot/src/mqtt.rs: MqttPublish::encode, remaining_length > MQTT_MAX_VARIABLE_BYTE_INTEGER with == or >=The builders keep topic and payload far below that limit, and variable_byte_integer repeats the check with the same error.
rullst-privacy/src/age_assurance/challenge_tokens.rs: ChallengeTokens::open_with_clock, token.len() > MAX_TOKEN_BYTES and payload.len() > MAX_PAYLOAD_BASE64 with == or >=, the || before payload.is_empty(), the payload bound and tag.len() != 43 with &&, and MAX_PAYLOAD_BASE64 with its + or / replaced by *These pre-authentication bounds only reject early what key lookup, tag verification, the 4096-byte decoded limit or JSON parsing reject with the same InvalidChallengeToken: a well-formed token is far below 8192 bytes, and 5463 base64 characters already decode to 4097 bytes.
rullst-privacy/src/age_assurance/sqlite.rs: SqliteReplayStore::open, the error.kind() != NotFound guard with falseEvery other metadata failure (permission, name length, symlink loop) also makes the SQLite open fail with the same StoreUnavailable.
rullst-privacy/src/consent/gate.rs: allows_with_clock, completed < record.valid_until() with <=A grant with completed >= valid_until has already returned false, so the two never compare equal there.
rullst-labs/src/sqlite/completion.rs: finish, lease.until <= tx.now || record.view.expires_at <= tx.now with &&narrow_deadline with the earlier of the two returns the same Expired.
rullst-labs/src/sqlite/jobs.rs: cancel, revision <= 0 || record.view.revision != revision with &&save_job updates only the row at that revision, so a stale revision still returns Conflict.
rullst-labs/src/sqlite/jobs.rs: load_job, body.len() > 32796, and seal_record, body.len() > 32768, each with == or >=Bounded record fields serialize far below 32 KiB, and a longer stored body fails authentication with the same Integrity.
rullst-labs/src/sqlite/jobs.rs: payload, the || before the scope and id comparisons with &&The exercise digest covers scope, id and revision, so none of them can differ alone.
rullst-labs/src/sqlite/retention.rs: expire_queued, either || with &&, and purge_terminal, the || after the state match with &&The SQL selection, load_job’s column checks and Record::validate already guarantee a queued, expired, unleased record, or an unleased one that is neither queued, running nor Uncertain (which always holds its lease).
rullst-supervision/src/exam/observation.rs: Observation::from_code, (code - 10) % 4 with +(code + 10) % 4 equals (code - 10) % 4, because 20 is a multiple of 4.
rullst-supervision/src/sqlite/events.rs: events, and rullst-supervision/src/sqlite/observations.rs: observations, expires_at > MAX_TIME with == or >=An event is received before its session expires, at least an hour before the session’s retention ends at or before MAX_TIME, so with its fixed retention it always expires before MAX_TIME.
rullst-supervision/src/sqlite/observations.rs: record_observation, both source match guards with truerecord_browser and record_capture pass only browser or capture observations with the browser source, and analyze only presence or audio findings with an adapter source.
rullst-supervision/src/sqlite/transaction.rs: Operation::begin, now < before || now < previous_time with &&finish checks the time against the same high-water mark and returns the same Clock.
rullst-supervision/src/sqlite/mod.rs: resolved_path, both metadata match guards with falseopen checks for a regular file again, initialize creates the file exclusively and fails on any existing entry, and every other metadata error also fails the open with the same Storage.
rullst-auth/src/auth/app_key.rs: parse_dotenv, delete the dotenvy::Error::Io armThe parser reads an in-memory &str, and valid UTF-8 read line by line cannot raise an I/O error.
rullst-auth/src/auth/passkey/ceremony.rs: ChallengeStore::issue, challenge.expires_at > now with >=It keeps only a challenge whose expiry equals a later Instant::now() reading to the nanosecond, which consume already rejects as expired.
rullst-auth/src/auth/passkey/shared/manager.rs: SharedPasskeyAuth::start_authenticate, allowed.is_empty() || allowed.len() > MAX_CREDENTIALS with &&CeremonyIntent::new rejects an empty or 33-entry authentication allowlist with the same InvalidInput before the store is called.
rullst-auth/src/auth/passkey/shared/manager.rs: validate_passkey, the || before first() != Some(&4), and rullst-auth/src/auth/passkey/sqlite.rs: validate_passkey, ||, each with &&A key that from_sec1_bytes accepts has either 65 bytes and tag 4 or neither, so a key failing only one check fails parsing with the same error.
rullst-auth/src/auth/passkey/verification.rs: verify_assertion, FLAG_ATTESTED_CREDENTIAL_DATA | FLAG_EXTENSION_DATA with ^The flags occupy disjoint bits, so OR and XOR build the same mask.
rullst-auth/src/recovery/connection.rs: the fallback configure_postgres and configure_sqlite with Ok(())They compile only without recovery-postgres or without both SQLite families, which the all-feature build does not compile.
rullst-auth/src/recovery/email_login/delivery.rs: <impl Drop for WireNotice>::drop with ()Zeroizing a buffer that is being freed has no effect safe code can observe.
rullst-auth/src/recovery/email_login/types.rs: safe_path, part == "." || part == ".." with &&The byte allowlist that follows already rejects every ..
rullst-auth/src/recovery/store.rs: SqlRecoveryStore::connect, the || before the recovery-postgres clause with &&With every feature enabled both feature clauses are always false, so joining them with && changes nothing.
rullst-security/src/telemetry.rs: SecurityStore::push_live_event, events.len() > MAX_LIVE_EVENTS with >=Truncating a buffer that holds exactly 50 events to 50 changes nothing.
rullst-security/src/telemetry/event.rs: security_event_schema_version with 1SECURITY_EVENT_SCHEMA_VERSION is 1.
rullst-security/src/telemetry/event.rs: truncate_utf8, boundary > 0 with >=is_char_boundary(0) is always true, so the loop stops at 0 either way.
rullst-security/src/sentinel/pow/codec.rs: decode_token, token.is_empty() || token.len() > 128 with &&, and > with == or >=Decoding rejects every token that does not decode to exactly 82 bytes with the same InvalidToken.
rullst-security/src/siem/spool.rs: decode_file, and rullst-security/src/siem/authenticated/codec.rs: read_and_verify, events.len() + 1 in the invalid frame offset error of cursor.checked_add(relative_end) with - or *A line end found inside the buffer cannot overflow usize, so the closure never runs.
rullst-security/src/recovery_codes.rs: GeneratedRecoveryCodes::drop with ()Zeroizing the plaintext codes changes only memory that is freed right after; safe code cannot read it.
rullst-security/src/mfa.rs: verify_totp_step_at, code.len() != 6 || !all digits with &&Every other input fails the constant-time comparison with the six-digit expected code.
rullst-security/src/mfa.rs: decode_base32 and generate_totp_at_counter (three times), | with ^The operands occupy disjoint bits, so OR and XOR build the same value.
rullst-security/src/schema_guard.rs: StrictJsonVisitor::expecting with Ok(Default::default())serde_json calls a visit method for every JSON value, so the message is never formatted, and inspect_json_payload maps every error to fixed text.
rullst-security/src/dlp/masking.rs: mask_database_passwords, pass_end + 1 with - or *The next scheme search starts at or just before the @, where no scheme can begin.
rullst-security/src/log_redactor.rs: identifier_continuation, end + 1 with -A _ or . followed by a non-alphanumeric byte then yields an empty last component, which is skipped like the identifier without it: neither byte is a separator, quote or space.
rullst-security/src/timing_guard.rs: TimingScope::finish, elapsed < target_duration with <=Sleeping for a zero remainder returns immediately.
rullst-security/src/login_guard.rs: record_login_failure, the window match guard with trueprune_expired has just removed every counter whose last attempt is a window or more old.
rullst-security/src/rate_limit.rs: is_rate_limited_in, the retention < with <=Two Instant::now() readings would have to differ by exactly the window, and an entry kept at that instant restarts its count on its next use.
rullst-core/src/security/pii.rs: decode_json_char, delete the ", \, /, b, f, n, r or t armEach of these escapes decodes to a character that, like the opaque stand-in, is neither a digit, a card separator, an address character nor @, and both keep the escape’s original spelling.
rullst-core/src/security/pii.rs: mask_chars, idx - start with + and username_len > 1 with >=A local part of at most one character has nothing to mask after its first character, and skipping to the end of the domain passes only domain characters, never another @.
rullst-core/src/security/pii.rs: mask_chars, the domain slice start idx + 1 with idx * 1 or idx - 1The domain holds a dot, so the leading @ never reaches the top-level domain; when its only dots are trailing ones, the text after the last dot starts with @ and fails both domain tests.
rullst-core/src/security/csrf_multipart.rs: scan, available < first.len() with >A prefix shorter than the first delimiter reaches the loop’s two-byte read past its end, which returns the same Incomplete.
rullst-core/src/security/csrf.rs: is_csrf_exempt_path, the first two || with &&/robots.txt, /sitemap.xml and /favicon.ico stay exempt through their .txt, .xml and .ico suffixes.
rullst-core/src/queue/mod.rs: the default QueueDriver::push_at, available_at > SystemTime::now(), and Queue::dispatch_at, delay > MAX_SCHEDULE_DELAY, each with >=They differ only when the requested time equals a clock reading taken inside the call, which a caller cannot arrange.
rullst-core/src/queue/worker/execution.rs: record_outcome, the error.is_panic() guard with trueexecute_job handles every cancelled handler before it calls record_outcome, so the only join error left is a panic.
rullst-core/src/storage/tenant.rs: confine_local_root, the !create && NotFound guard with false or without !Below a missing tenant directory, get, metadata and delete then fail with the same NotFound that the operation itself reports; put creates the directory first.
rullst-core/src/cache/memory.rs: MemoStore::get, now < entry.expires_at, and MemoStore::set, the expiry retain’s <, each with <=An Instant reading would have to equal the entry’s expiry to the nanosecond.
rullst-core/src/lifecycle.rs: begin_draining, in_flight_requests() == 0 with !=, and release_request, && with ||wait_for_drain waits only on a draining or stopped lifecycle and re-reads the count after every wake-up, so a wake-up that no waiter can receive, or an extra one, changes nothing.
rullst-core/src/storage/cloud/multipart/codec.rs: MultipartStorage::open, checkpoint.0.len() > MAX_TOKEN with == or >=, and bytes.len() < 28 with <=No authentic checkpoint reaches 256 KiB (256 receipts and a 2,048-byte upload id stay under 180 KiB), and a 28-byte token has an empty plaintext; both fail with the same InvalidCheckpoint.
rullst-core/src/testing/snapshot.rs: line_diff, the deletion test’s table[i * width + j + 1] with j * 1Without a match, table[i][j] is the larger of table[i + 1][j] and table[i][j + 1], so table[i + 1][j] >= table[i][j] holds exactly when table[i + 1][j] >= table[i][j + 1].
rullst-core/src/queue/deferral/planner.rs: lowest_intensity, the tie-break overlap_start < best_start with <=A tie at the same start stores the same start and value again.
rullst-core/src/live/recovery/types.rs: the duplicate-free field visitor’s expecting with Ok(Default::default())LiveCommand::decode turns every deserialization error into the same Invalid, so the expectation text is never seen.
rullst-core/src/rpc.rs: request_contract_failure, delete the BodyTooLarge armto_bytes already refuses a body over the same limit with the same 413 and code, so decoding never reports it.
rullst-core/src/rpc.rs: RpcFailure::from_detail with Default::default()Compiled only for wasm32.
rullst-core/src/rpc.rs: the test helper failure_for, 64 * 1024 with +Test code: every failure envelope it reads is far below 1,088 bytes.
rullst-core/src/storage/cloud/multipart/protocol.rs: parse, delete the allow_dtd fieldroxmltree’s default ParsingOptions already rejects a DTD.
rullst-core/src/live/recovery/session.rs: run, delete the Ok(Ok(())) armA session ends normally only when the peer closed: tungstenite answers a close frame itself, so the code chosen here never reaches the peer.
rullst-core/src/live/recovery/session.rs: send_snapshot, encoded.len() > 512 * 1024 with == or >=A snapshot holds at most 64 KiB of HTML, which JSON escaping expands to under 400 KiB.
rullst-core/src/cache/memory/exact_json.rs: Display for Inexact with Ok(Default::default())is_exact_json only checks whether serialization succeeds; the error text is never formatted.
rullst-core/src/radar.rs: get_windows_memory_mb, get_windows_process_cpu_usage, filetime_100ns, calculate_windows_cpu_percent, and the Windows and other-platform get_process_cpu_usageNot compiled on Linux.
rullst-core/src/feature/db.rs: DbFeatureDriver::resolve_with, the freshness test Instant::now() < entry.expires_at, and remember, the eviction retain’s <, each with <=An Instant reading would have to equal the entry’s expiry to the nanosecond.
rullst-core/src/cache.rs: MemoryDriver::cleanup_if_due, the sweep’s now < expires_at, and inspect, the filter’s expires_at > now, with <= and >=An Instant reading would have to equal the entry’s expiry to the nanosecond.
rullst-core/src/storage/local_paths.rs: reject_symlink_components, the NotFound | NotADirectory guard with trueAny other symlink_metadata failure (permission, name length) recurs in the canonicalize, create_dir_all or write that follows, with the same Io text.
rullst-core/src/telemetry/distributed/processor.rs: MinimizedProcessor::on_start with ()The wrapped BatchSpanProcessor::on_start ignores its arguments (opentelemetry_sdk 0.33).
rullst-core/src/server/dylib_loader.rs: is_expected_loaded_library_removal_error with true or falseNot compiled on Linux.
rullst-core/src/config/project_setting.rs: parse_dotenv_entries, delete the dotenvy::Error::Io armThe entries are read from a &str, which never fails with an I/O error.
rullst-nexus/src/nexus/crud/form.rs: form_html, the Password guard with falserender_field_widget renders a Password input empty and without a placeholder, whatever value or state it receives.
rullst-capital/src/invoice.rs: money_to_minor, (scaled - rounded).abs() > 1e-7 with >=When scaled rounds to at least one minor unit, scaled - rounded is a multiple of 2^-53 and never equals the f64 1e-7, so the two differ only for an amount that rounds to zero minor units, which rounded < 1.0 rejects either way.
rullst-capital/src/invoice/pdf.rs: render_pdf, bytes.len() > MAX_PDF_BYTES with == or >=, and the || with &&The embedded font is at most 8 MiB and a bounded invoice adds well under 1 MiB, so the document never reaches 16 MiB, and printpdf always writes the %PDF- header.
rullst-capital/src/providers/mod.rs: http_client with a leaked default clientAdapters only build requests with it; execute always sends them through the bounded client from http::client.
rullst-capital/src/providers/stripe.rs: create_customer_portal, || with &&require_mock_operation accepts the same empty and mock_* keys and the same fixture URL is returned; every other key fails there with the same error.
rullst-orm/src/value.rs: the SQLx Any pool’s temporal::date_time, naive_date_time, date and time with String::new() or "xyzzy".into()They exist only without a strict driver feature, and the all-feature build selects the PostgreSQL pool; the_any_pool_binds_dates_as_iso_text checks their exact text in default-feature builds.
rullst-orm/src/schema/schema_builder.rs: prepare_postgres_enums, delete ! in the !definitions.is_empty() guardThat guard exists only without strict-postgres, which the all-feature build enables; postgres_enum_preparation_validates_before_any_statement covers it in default-feature builds.
rullst-orm/src/pool/dsn.rs: percent_decode_utf8, (high << 4) | low with ^The high and low hex digits occupy disjoint bits, so OR and XOR build the same byte.
rullst-orm/src/privacy.rs: the default ComplianceModel::personal_fields with vec![]The default already returns an empty vector.
rullst-orm/src/polyglot/turso/model.rs: the default TursoModel::opaque_columns with Vec::leak(Vec::new())The default already returns an empty slice.
rullst-orm-macros/src/feature_gates.rs: redis and ai with Default::default()With every feature the runtime gates are on and the feature is enabled, so both return an empty token stream, which is the default.
rullst-orm/src/polyglot/recovery/codec.rs: base64url_value, the digit arm’s byte - b'0' with +The value is read only for its low four padding bits, and the two results differ by 96, a multiple of 16.
rullst-orm/src/polyglot/qdrant/mod.rs: LiveQdrant::send_json, bytes.len() > MAX_REQUEST_BYTES with == or >=No request reaches 4 MiB: vectors hold at most 65,536 values and payloads at most 1 MiB, so the body stays near 2 MiB; a test still sends a 1 MiB payload.
rullst-orm/src/outbox.rs: Outbox::install, case_insensitive_keys > 0 with >=The MySQL key-collation conversion is idempotent: run on keys that are already ascii_bin it leaves the same columns and only costs a table rebuild. The MySQL matrix converts a legacy table, which kills the == and < mutants.
rullst-orm/src/schema/migration_lock.rs: MigrationLock::release with ()The lock lives in the dedicated connection’s session. The replaced body still drops self, which closes the socket and ends the session, freeing the lock as the graceful close does.
rullst-messaging/src/webhooks/signature.rs: WebhookSignature::from_headers, the || before kind.len() > 128, timestamp.len() > 12 or signature.len() != 46, and tag.len() != 32 || …, each with &&MessageId::from_stored, EventKind, the canonical timestamp parse and the strict v1= decode repeat each length check with the same InvalidSignature, and 43 strict base64url characters always decode to 32 canonical bytes.
rullst-messaging/src/sqlite/storage/crypto.rs: open, the minimum sum with a + replaced so that it shrinks, and envelope.len() < minimum with <= or ==The shortest authentic envelope is 38 bytes; a shorter one fails the key-ID, nonce or tag parse with the same StorageAuthenticationFailed.
rullst-messaging/src/schedules/crypto.rs: open, value.len() > MAX_CONTENT_BYTES + 128 with == or >= or its + with *, the || after it with &&, and a + in 8 + 1 + 1 + 12 + 16 replaced so that the sum shrinksSealed values span 38 to 131,173 bytes, so each change only moves which later parse or authentication step rejects a value, with the same Encryption.
rullst-messaging/src/schedules/delivery.rs: verify, lease.namespace != … || now >= lease.expires with &&Occurrence IDs digest their namespace, so another namespace’s lease is not found by its ID, and an expired lease fails the commit deadline; both return the same InvalidLease.
rullst-messaging/src/wire.rs: FIXED_BYTES, a 2 + 2 with 2 * 2, and Reader::is_finished with true2 * 2 equals 2 + 2; decode re-encodes the envelope and compares it with the frame, which rejects trailing bytes with the same InvalidWireEnvelope.
rullst-messaging/src/redis_streams/transport.rs: Remote::decode, delete the "missing" | "corrupt" armThe catch-all arm returns the same CorruptStorage.
rullst-connect/src/provider/id_token.rs: validate_claims_with_presenters, the || before nonce.is_empty() with &&verify_nonce compares SHA-256 digests, so a nonce empty on one side only already fails it, and two empty nonces still satisfy the joined condition.
rullst-mail/src/security.rs: redact_labelled_private_keys, MAX_PEM_LABEL_BYTES + 1 with *Reading at most 64 label bytes cuts a longer label inside its run of label bytes, so the ----- check that follows fails as the length check would.
rullst-mail/src/security.rs: href_values, value_start + length + 1 with - or *The scan resumes on the closing quote or the byte before it (the opening quote for an empty value), and no href match can contain a quote.
rullst-mail/src/inspection/sniff.rs: root_element, open < close with <=Both are positions just after one byte, a [ and a >, so they are never equal.
rullst-ai/src/ai/egress.rs: EgressPolicy::strict with Default::default()strict already returns Self::default().
rullst-ai/src/ai/egress.rs: is_public_ipv6, is_unspecified() || is_loopback() with &&:: and ::1 lie outside 2000::/3, which the next clause rejects.
rullst-ai/src/ai/guardrails/markdown_inline.rs: open_tag_end, the self-closing then_some(index + 1) with - or *The tag then ends on or just before its /; the byte before that / is whitespace, a quote or a name byte, never a bracket.
rullst-ai/src/ai/guardrails/markdown_images.rs: definition_destination, the <...> end end + 1 with *It drops only the closing > or line ending, which neither the scheme nor the authority check reads.
rullst-ai/src/ai/guardrails/markdown_code.rs: indentation, the tab step 4 - column % 4 with 4 + column % 4Indentation is only compared with 4, and either step takes a tab to at least four columns.
rullst-ai/src/ai/guardrails/tax_ids.rs: token_end, bytes.get(index + 1) with index - 1A separator then joins when a digit precedes it instead of follows it, but the end moves only on digits and a second separator stops both scans, so every token ends at the same digit.
rullst-ai/src/ai/guardrails/tax_ids.rs: is_valid_tax_id, the CNPJ all_digits && token.len() == 14 with ||digit_values still requires exactly 14 digits, which only a 14-digit token or the CNPJ layout holds.

Timed-out mutants are not counted as caught, and cargo-mutants reports them separately. The campaign rewrote the scanning loops of the WAF detector, the RASP decoder, the HTML snapshot normaliser and the accessibility scanner so that every step consumes input, and bounded the WAF linear-time test, which removed their timeouts. The core campaign ran the queue worker, realtime, multipart CSRF, byteranges and long PII masking tests against a deadline instead, which turned their timeouts into caught mutants; the CLI campaign bounded the update cache’s FIFO test the same way. The remaining ones turn forward progress into a loop that never ends without allocating, in code every test of the file reaches, so no single test can fail first:

  • rullst-core/src/error_console/store.rs: Store::record with len() >= MAX_ENTRIES replaced by < pops an empty queue forever.
  • cargo-rullst/src/generators/build/upgrade/rules/tokens.rs: the token scanner’s level and path loops, where an index step (+= 1, += 2, next + 1, end + 1, end + 2) becomes - or *, or path returns 0 or 1, and the scan revisits the same token forever.
  • cargo-rullst/src/generators/audit_source.rs: the token scans of tokenized_views, item_end, mark_code_tokens and doc_attribute, where an index step (+= 1) becomes -= or *=, item_end returns 0 or 1, or doc_attribute returns Some(0) or Some(1) or its next + 1 becomes -, and the scan revisits the same token forever.
  • rullst-security/src/log_redactor.rs: the bearer, identifier, separator and escaped-quote scans, where an index step (+= 1, += 2) becomes - or *, a key offset (cursor + offset, key_start + key.len()) becomes - or *, or identifier_continuation returns 0 or 1, and the scan finds the same key or byte again forever.
  • rullst-security/src/dlp/byteranges.rs: Lines::next, where the non-empty filter is negated or the next-line offset (start + offset + 1) becomes - or *, and the reader returns the same line forever; rullst-security/src/dlp/masking.rs: the AWS-key and database scans, where cursor + offset becomes * or -, or + scheme.len() becomes -, and the scan restarts before the same match.
  • rullst-core/src/security/pii.rs: measure_digit_run returning (0, 0), (0, 1), (1, 0) or (1, 1), its separators < 3 with == or >, or its end += 1 with *= or -=, and the address and JSON-string scans of mask_chars and mask_json_strings, where start -= 1 becomes /=, end += 1 or idx += 1 becomes *=, or the dotted-domain end += 1 becomes -=, so the card, address or string scan never advances; and rullst-core/src/security/pii_byteranges.rs: Lines::next, where the next-line offset becomes - or *, as in the DLP reader above.
  • rullst-core/src/server/dev_telemetry/recorder.rs: push_bounded with entries.len() >= capacity replaced by < pops an empty queue forever, in code every recorded request or query reaches.
  • rullst-core/src/scheduler.rs: shutdown_requested returning true or false, and wait_for_shutdown returning at once or without its !, so a task loop never runs or never sees shutdown, and the scheduler, server and supervision tests that start and stop a scheduler wait forever.
  • rullst-security/src/login_guard/state.rs: pop_earliest returning None or Some(Default::default()), or make_room with && replaced by ||, so pruning or eviction never removes an entry; and rullst-security/src/rasp/user_agent.rs: the version scan’s index += 1 replaced by *=.
  • rullst-mail/src/security.rs: the redaction scans, where start + relative_end in redact_values_after becomes -, offset + relative in redact_aws_access_keys or redact_labelled_private_keys becomes *, or start + prefix.len() becomes - or *, and the href_values scan, where index += 1 in skip_whitespace or the non-match pos += 1 becomes *=, or the pos += 4 after an href without = becomes -=; each scan restarts before the same match.
  • rullst-ai/src/ai/guardrails/tax_ids.rs: mask_tax_ids without the ! of its digit test or with start += 1 as -= or *=, token_end returning 0 or 1, and its index += 1 steps as -= or *=; rullst-ai/src/ai/guardrails/markdown_inline.rs: code_span_hides_bracket with either backtick test negated, index += run as -= or *= or index += 1 as *=, and the whitespace steps of open_tag_end as *=; rullst-ai/src/ai/guardrails/markdown_images.rs: the index += 1 steps of unescaped_close and label_end as -= or *=; and rullst-ai/src/ai/guardrails/markdown_code.rs: the index steps of lines, mark_fenced and span_contents as *=, and blank_code with code[group_start] || is_blank(...) as &&, which stops on a blank line. The guardrail tests reach these scans, which then never finish.

rullst-security/src/timing_guard.rs: TimingScope::finish with the jitter % replaced by + does not loop: it sleeps for up to 72 minutes in the real-time padding tests.

A targeted run sets each test binary’s address space to 8 GiB, so a loop that does allocate aborts its tests and counts as caught instead. rullst-ai/src/ai/guardrails/markdown_code.rs: blank_code with group_start += 1 replaced by *= does allocate, but it repeats one line forever; on a blank line the output grows one byte per turn and reaches that limit only after the timeout.

Owned-server contract mutation runs

Some adapters are reached only by ignored contract tests that a wrapper runs against a disposable Docker server it owns, so a full or targeted run reports their database-bound mutants as missed. mode=contract measures them: it runs .github/check-<contract>.py --mutants REGEX [--shard k/n] on a hosted runner and uploads mutants.out as mutants-out-contract. The wrapper starts its server, mutates only its adapter files and runs only the first (exercise) phase of its contract tests, one at a time, never the restart phase. .github/mutation_contract.py is the Cargo target runner for those tests: it resets the server before every test binary (a recreated database of the same name, no leftover runtime roles, ALTER SYSTEM RESET ALL, or FLUSHALL for Redis), because a mutant can stop a test halfway and leave a role, a disabled fsync or a full Redis behind. Missed and timed-out mutants are findings; a failed baseline, a baseline that did not run every contract test, a failed reset or an incomplete classification fails the run. Because only the contract tests run, a mutant that a unit test of the same file catches can still be reported as missed: compare it with a targeted run of that file. One build directory serves every mutant, so the job compiles incrementally, without sccache (which refuses incremental builds), and rebuilds a mutant in about a second.

contractMutated filesContract tests
passkey-postgresrullst-auth/src/auth/passkey/shared/manager.rs, shared/postgres/operations.rspostgres_contract
auth-recovery-postgresrullst-auth/src/recovery/connection.rs, store.rs, email_login/guard.rs, email_login/storage.rs, api_tokens/storage.rs: the files with PostgreSQL-only branchesrecovery, sessions, email login, API token (with HTTP) and the exercise phase of the three restart journeys; not the browser journey
privacy-postgresrullst-privacy/src/age_assurance/postgres.rs, consent/postgres.rsshared_replay_contract, shared_consent_contract
mail-postgresrullst-mail/src/suppression/postgres/**suppression contract and restart exercise
recurring-postgresrullst-messaging/src/schedules/** (schedules-postgres)recurring contract and restart exercise
messaging-redisrullst-messaging/src/redis_streams/**the password, TLS, fault, ordering, outbox and bounds contracts and the AOF restart exercise
connect-sqlrullst-connect/src/challenge/sql/postgres.rs, sql/mysql.rs (the wrapper also owns a MySQL server, which each contract resets by dropping its table)postgres_challenge_contract, mysql_challenge_contract
orm-sqliterullst-orm/src/** and rullst-orm-macros/src/**, test files excluded; select files with mutants_regex (for example ^rullst-orm/src/pool/savepoint\.rs:)every rullst-orm test target, built with drivers-all (the SQLx Any pool)
orm-polyglotthe same files; select the optional-backend modules (polyglot, scout, redis_data, query_cache) with mutants_regexevery rullst-orm test target, built with drivers-all and the Redis, MongoDB, SurrealDB, Turso, Scout HTTP and Qdrant features, with an owned Redis server for the RULLST_TEST_REDIS_URL contracts
orm-sql-serversthe same files; select the PostgreSQL and MySQL/MariaDB branches with mutants_regexmatrix_postgres, matrix_mysql and matrix_mariadb, built with drivers-all; their testcontainers start a disposable server per test binary, and a server that cannot start fails the run
orm-strict-postgresthe same filesmatrix_postgres, built with strict-postgres only, which also exercises the native enum types and typed binds of that pool
orm-strict-mysqlthe same filesmatrix_mysql, built with strict-mysql only
orm-polyglot-serversthe same files; select the optional backends’ live paths with mutants_regexthe MongoDB, SurrealDB, document-recovery, Qdrant, Meilisearch, Redis data and Turso testcontainers matrices, built like orm-polyglot

orm-sqlite owns no server. Targeted and full runs build the workspace with every feature, which selects the strict PostgreSQL pool and compiles the SQLite behaviour tests of rullst-orm out, so they report the mutants that only a database statement reaches as missed. This contract builds the ORM with drivers-all instead and runs its whole suite on SQLite for every mutant, including the code #[derive(Orm)] generates; the testcontainers matrices are skipped. Code compiled only with every feature, such as the PostgreSQL-only branches, is measured by the targeted runs. orm-polyglot adds the optional backends, whose tests use mocks and loopback servers, and runs the query-cache and Redis hash-scope contracts against a Redis server it owns; DuckDB, whose bundled build is large, stays with targeted runs. orm-sql-servers measures the dialect branches (placeholders, migration locks, collations and payload types) against real servers; every mutant waits for three of them, so select only those branches. orm-strict-postgres and orm-strict-mysql run one of those matrices in the build of a strict pool, for code that the Any pool does not compile.

The MySQL statement files rullst-capital/src/quota/sql/mysql.rs and rullst-ai/src/ai/memory/sql/mysql.rs hold only string constants, so cargo-mutants generates no mutant for them. The privacy URL policy in postgres_connection.rs and the capacity check have unit tests. So do the mail adapter’s clock, event-time, stored-time and URL checks, the privacy choice-code decoding, and, in passkey-postgres and auth-recovery-postgres, most branches of manager.rs and the recovery files: a contract run reports those mutants as missed, and a targeted run of the file catches them.

Fuzzing and OSS-Fuzz

The v13 manual fuzzing.yml matrix covers all 42 declared libFuzzer targets (the immutable v12 line retains 40): Core 12, ORM 5, Security 7, Connect 3, Mail 4, AI 3, IoT 3, Capital 1, Nexus 1, Studio 1 and Privacy 2. The checked-in .github/fuzz-targets.json is validated against every */fuzz/Cargo.toml, corresponding lockfile and source file before the manual campaign can fan out. Release mode verifies input-equivalent historical results and compiles the remaining targets in package-level preflight jobs before starting any long campaign. Campaign jobs restore and save versioned per-target corpora in the Actions cache and share one content-addressed compiler-cache namespace; campaign failures retain their exact reproducer. The weekly corpus-sync.yml warm-up was removed in 13.0.0-alpha.4: the repository cache runs above its 10 GiB limit, so no saved corpus survived to the next weekly run or to a release campaign (the weekly runs of 11, 18 and 25 October 2026 and the release campaign of 24 October all reported “Cache not found”), and its one-minute runs never produced a reproducer. The same eviction applies to the corpora the campaign saves. A clean run retains its original source SHA, target, corpus, toolchain and time budget; reuse adds a separately verified relationship to the candidate inputs and never claims a fresh execution. Diagnostic mode is limited to one exact inventory target for five minutes and is never counted as the complete release gate.

The oss-fuzz/projects/rullst directory is a local integration draft. It is not proof of upstream acceptance, continuous ClusterFuzz execution, or coverage of all 42 targets; its helper build must be completed and validated against the official OSS-Fuzz repository before submission. The integration is worth finishing, but a “100% first-pass acceptance” promise is not meaningful and should not be made.

Supply chain and release provenance

All direct third-party GitHub Actions references in this repository’s workflow files are pinned to full commit SHAs. A pinned composite action can still carry its own transitive downloads or references, so blocking integrations must also be reviewed for that behavior. RustSec exceptions are limited by deny.toml and documented with owners, controls, and expiry dates in docs/src/security-advisory-exceptions.md.

scorecards.yml runs the pinned OpenSSF Scorecard action on main pushes and weekly, uploads SARIF to GitHub code scanning, and publishes OIDC-authenticated results to the public Scorecard API so the README badge follows the latest completed analysis. The numeric score is supply-chain evidence, not a security certification.

release.yml is tag-only. It verifies source, validates the exact semantic tag against every publishable crate, packages before the first publish, and creates a tag-bound evidence bundle containing the lockfile, Cargo metadata, CycloneDX 1.5, Cargo Audit JSON, dependency policy, bounded compliance evidence, advisory exceptions, commit context, checksums, and release notes rendered from the exact matching changelog section: the section’s introduction and “Known limitations”, plus a link to the full section at the tag (the whole section when it has no introduction). The .crate archives and evidence receive GitHub’s SHA-pinned build-provenance attestation. This does not by itself establish a project-wide SLSA level, Sigstore Cosign binary signing, independent review, or regulatory compliance.

workflow-lint.yml validates all workflow syntax, GitHub expressions, and embedded shell with Actionlint 1.7.7. Its container is pinned to an immutable linux/amd64 digest, just like third-party GitHub Actions are pinned to full commit SHAs.

architecture.yml is repository-owned and deterministic. It rejects any internal dependency edge or optionality change that is not reflected in the reviewed crate-architecture-policy.json. The earlier TangleGuard integration was removed because its composite action downloaded an unversioned latest binary without a repository-pinned checksum, which was unsuitable for a blocking supply-chain gate.

Reviewed Action updates on the v12 maintenance line

Keep CodeQL init, analyze, and upload-sarif on one reviewed Action revision. Dependabot groups these sub-actions, and the local pin validator rejects mixed CodeQL revisions inside a workflow. Review the scanner bundle change as well as the wrapper SHA; the 4.38.0 update selects CodeQL 2.27.0.

setup-rust-toolchain 2.0.0 changes warning enforcement from RUSTFLAGS to CARGO_BUILD_WARNINGS (Cargo 1.97+). The v12 migration explicitly retains rustflags: "-D warnings" and build-warnings: "", preserving the previous compiler flags, strict warning behavior and compiler-cache inputs. This does not disable Clippy’s explicit -D warnings. The MSRV job still uses a separate toolchain installer and Rust 1.96.0. Adopting the new Cargo warning mechanism is a separate measured migration, not an implicit side effect of updating an Action. See the upstream migration notes.

TruffleHog’s composite Action pin does not pin its default latest scanner image. The workflow therefore also specifies the reviewed 3.97.4 multi-platform image digest. Future scanner updates must review and change that digest; Dependabot updating the wrapper alone is insufficient. The scan’s existing scope and verified-secret failure policy remain unchanged. These automation updates do not change published crate versions or constitute a v12.0.1 release.

Workflow inventory (34 definitions)

Durations are intentionally omitted because runner load, cache state, and the dependency graph make static estimates unreliable.

WorkflowTriggerModeActual scope
architecture.ymlmain/v12/v13 push and PR, manualBlockingCompares Cargo’s publishable non-dev internal dependency graph with the reviewed crate-architecture-policy.json; unreviewed normal/build edges, removals, or optionality changes fail, while test-only dev-dependencies do not masquerade as production coupling.
bench.ymlmain push, weekly, manualAutomated evidenceEight published groups backed by nine Criterion binaries, with non-blocking 20% regression alerts and gh-pages data consumed by the benchmark hub. Scheduled runs use the repository default branch.
ci.ymlmain/v12/v13 push and PR (Linux gate on PRs to main; full OS matrix with the full-matrix label), nightly full matrix, manualBlocking plus observational reportFormat, all-target/all-feature Clippy, seven-shard multi-OS tests including Cargo-aware doctests sourced from all 48 tutorials, four-way feature/threat partitions, the SQLite transactional outbox contract and Messaging concurrency suite, relational/polyglot live matrices, isolated strict-DB/feature boundaries, MSRV, the generated VM deployment built and started with Docker and rootless Podman (manual, nightly and deploy-changing pull requests), and a ready-PR/manual full-matrix SHA-bound per-crate quality scorecard artifact. A targeted manual OS/shard run is diagnostic and cannot emit the full scorecard.
cli-artifacts.ymlreusable call from full/CLI manual Rust CI or admitted tag releaseBlocking caller job; required release evidenceBuilds and runs both CLI entry points on four explicit native targets; stages bounded executables and source/version/platform/digest inventories before attestation. CI artifacts are diagnostic; only the tag pipeline adds separate provenance and release assets. No installation occurs.
codeql.ymlmain/v12/v13 push, v12/v13 PR, weekly, manualBlocking runRust CodeQL after an all-target/all-feature workspace check, plus JavaScript/TypeScript analysis.
coverage.ymlmain/v12/v13 push, v12/v13 PR, weekly, manualBlocking plus observational jobLLVM LCOV generation with a pinned, zero-retry, bounded-concurrency nextest scheduler and retained JUnit inventory; a focused default-SQLite pass for ORM/Studio/Nexus/the facade; exact local 90% floors; and blocking OIDC-authenticated Codecov upload. Scheduled/manual branch instrumentation is non-blocking and uses the pinned verifier-only nightly.
crypto-containment.ymlrelevant main changes and PRs, weekly, manualBlockingSigned OTA and Vault tests, RustSec audit, and simulator-boundary checks; explicitly no PQC/HSM certification. Until 13.0.0-alpha.4 this was pqc-compliance.yml; the pqc and hsm modules it scans are deterministic simulator fixtures.
dast-zap.ymlmanualBlocking generated targets plus informational showcasePins the ZAP image by digest, scans fresh release/migrated REST API, SaaS starter (public and signed-in) and LMS surfaces as blocking gates, scans the CDN-backed blog showcase informationally, and uploads separate reports plus application logs.
documentation.ymlmain/v12/v13 push and PR, weekly, manualBlocking plus informational external scanBuilds the mdBook; validates landing/benchmark templates, project identity, the README workflow count, the inventory below and every workflow badge or link, local assets, pinned external chart scripts and all requested social links. Real Chromium checks desktop/390px/320px layout, keyboard/mobile navigation, clipboard success/denial, privacy disclosure, reduced motion, no-JS navigation, and absence of external landing requests/browser storage. This is a bounded browser contract, not WCAG certification. Also validates the 190-claim historical roadmap denominator and repository-local links. Scheduled/manual runs preserve an informational external-link report.
e2e-smoke.ymlmain/v12/v13 push and PR, manualBlockingBoots the release Blog application and checks HTTP, headers, CSRF form flow, SQLite persistence, and the persisted page parsed by real headless Chromium.
fuzzing.ymlmanualBlocking release campaign or diagnosticRelease mode maintains all 42 targets through independently verified, at-most-seven-day input-equivalent evidence or fresh 5.5-hour jobs. A package’s inputs are the crates it reaches through path dependencies, files they include from elsewhere and a few global files; a change reruns the packages whose inputs it touches (all targets for global inputs, none for docs, changelog, CLI or examples), and a package whose inputs cannot be bounded falls back to the whole repository. Selected packages retain locked preflights, corpus caching and failure reproducers. force_full always requests a new full campaign; the current-commit boundary and independent tag-admission check remain mandatory. Lock drift is always rejected. The parser restarts its ASan process at most every 30 minutes while preserving the budget. Single-target diagnostic mode runs for five minutes and the evidence boundary marks it ineligible for release.
iot-integration.ymlmain/v12/v13 push and PR, manualBlockingHost IoT tests, signed OTA invariants, and one Cortex-M no-std build; no hardware claim.
kani.ymlweekly (Thursday 01:47 UTC), manualBlocking release scope; bounded evidenceBuilds an immutable reviewed Kani snapshot with the verifier-only nightly-2026-08-01 compiler, while Rullst stays on stable Rust 1.99.0 with a Rust 1.96 MSRV. It verifies twenty named bounded harnesses in isolated jobs across ten supported packages. Proof failures fail their matrix jobs; the proc-macro-only crate remains outside Kani’s supported targets.
machete.ymlmain/v12/v13 push and PR, manualBlockingUnused dependency scan with configured exceptions.
miri.ymlweekly (Wednesday 01:17 UTC), manualBlocking release scope; bounded evidencePinned nightly-only Miri executes 15 named pure-Rust/default-feature scopes with randomized layouts without changing Rullst’s stable toolchain or MSRV. Native FFI/syscall/network paths, the umbrella re-export facade, and the Blog example are explicit boundaries; selected-scope failures fail the workflow.
mutants.ymlmanualInformationalA source-bound inventory preflight followed by eighty lossless pinned cargo-mutants 27.1.0 shards with four concurrent jobs over that source’s all-feature workspace, each shard with a pinned Redis service for the RULLST_TEST_REDIS_URL contracts, one validated production-file diagnostic, one owned-server contract (mode=contract: a PostgreSQL or Redis wrapper’s adapter mutants against the server it owns, or the ORM crates against the rullst-orm SQLite suite), or policy-bound exact-SHA recovery of reviewed failed fragments. Successful immutable artifacts may be reused, but the content-addressed aggregate still requires every reviewed candidate exactly once and carries source/run/branch/tool/inventory provenance. Findings stay informational, while baseline/tool/invocation failures, missing artifacts, incomplete classification and reviewed-inventory drift fail the run. The completed v12 receipt is run 34761010296: 14,391/14,391 classified and a conservative 70.57% caught.
mutants-pr.ymlpull requests to main that change RustBlocking by policy; not a required checkLists the pinned cargo-mutants 27.1.0 all-feature mutants on the Rust lines the pull request changes, skipping the globs in .github/mutants-pr-exclude.txt and every CLI file not listed in .github/mutants-pr-cli.txt (listed CLI files run against the CLI’s library tests), and tests them in at most eight shards with a Redis service and the targeted-run timeouts and memory cap. A missed mutant fails the run; unviable and timed-out mutants do not. See Pull-request mutation gate.
no_std-build.ymlmain/v12/v13 push and PR, manualBlockingBuilds rullst-iot for three bare-metal targets; this is compile evidence, not hardware execution.
pages.ymlmain push, manualDeployValidates and deploys the current development landing page, local visual assets, mdBook and benchmark hub/dashboards to GitHub Pages while preserving history data fetched from gh-pages.
pr-security-evidence.ymlpull requestsAutomated evidenceGenerates bounded CLI audit, compliance report, and CycloneDX SBOM artifacts; no certification claim. Until 13.0.0-alpha.4 this was ai-sentinel-pr.yml; it calls no AI service.
proptest.ymlweekly, manualBlocking runEight parallel release-mode workspace shards plus dedicated ORM and Connect property contracts with configured case counts.
release.ymlexact-looking version tagsReleaseTag/changelog validation, exact-SHA admission of the full CI matrix (not repeated in the tag run), package-all, evidence bundle, checksums, GitHub build-provenance attestation, changelog-derived release notes, and dependency-order publication.
sanitizers.ymldaily, manualBlocking runTSan and ASan library matrices on pinned nightly-2026-08-21; this verifier toolchain does not change Rullst’s stable compiler or MSRV.
scorecards.ymlmain push, weekly, manualAutomated evidenceOpenSSF Scorecard analysis and SARIF/artifact upload; not SLSA certification.
semver.ymlmain/v12/v13 push, v12/v13 PR, manualBlockingFans out one job per machine-readable release-order entry and compares each supported library API with the latest stable, non-yanked crates.io release of the same major version (.github/check-semver.sh; selection tested by .github/test-semver-baseline-constraints.py). Prereleases may change the API, so a line without a stable release has no baseline: each 13.0 prerelease job reports that with a notice and compares nothing, and the 13.x comparison starts by itself once 13.0.0 is published. A tree whose version still equals that release is checked as the next minor release (breaking changes fail, deprecations pass); a bumped version is checked for the change it declares. The frozen v12 branch runs its own copy of the workflow and script. Never-published packages and proc-macro/binary API surfaces unsupported by cargo-semver-checks are reported explicitly.
spellcheck.ymlmain/v12/v13 push and PR, manualBlockingRepository typo scan.
supply-chain.ymlmain/v12/v13 push and PR, daily, manualBlockingThree jobs, which until 13.0.0-alpha.4 were cargo-deny.yml, audit.yml and security-audit.yml. Check Dependencies (Licenses, Vulnerabilities) runs Cargo Deny’s advisory, license, ban and source policy from deny.toml (a required check on main). Cargo Audit (production and fuzz locks) audits the production lock and all eleven fuzz-package locks with one advisory-database fetch. Advisory exception governance checks that the active advisory IDs in the ledger, deny.toml and every workflow’s --ignore/--audit-ignore flags agree, and that each ledger row has an owner and an unexpired date. No advisory is ignored today. The daily scheduled run uses its own concurrency group, so it never cancels the main push run that release admission requires. The v12 lock audit was removed at the 12.x end of life (16 October 2026); the frozen v12 branch keeps its own copies of the old workflows.
trufflehog.ymlmain/v12/v13 push and PR, weekly, manualBlockingVerified-secret scan over the configured Git history range.
udeps.ymlweekly, manualInformationalcargo-udeps signal on pinned nightly-2026-08-21; command failures are tolerated.
unsafe-policy.ymlmain/v12/v13 push and PR, manualBlockingDenies new production unsafe code and validates the reviewed exception allowlist.
verus.ymlmanual-only v13 candidateStrict pilot, outside release admissionVerifies the source-linked age-policy method predicate with pinned Verus/Rust/Z3 and requires three postcondition-failing controls. Uploads fingerprints, tool identities and resource measurements; hosted acceptance/promotion pending.
wasm-matrix.ymlmain/v12/v13 push and PR, manualBlockingCompiles Core, the public rullst facade (without features, with its defaults and with the CLI-generated application feature set) and macros for wasm32-unknown-unknown and wasm32-wasip1.
workflow-lint.ymlmain/v12/v13 push and PR, manualBlockingValidates the shared fuzz inventory, then Actionlint checks workflow syntax, GitHub expressions and embedded shell using an immutable container digest.
zero-panics.ymlmain/v12/v13 push and PR, manualBlockingPanic-family Clippy lints plus generated-code regression checks for published runtime targets.

Informational workflows reviewed for 13.0.0-alpha.4

Each informational workflow’s run history was read for 13.0.0-alpha.4, asking whether a finding ever led to a commit and whether another workflow already covers it. corpus-sync.yml was removed (see Fuzzing and OSS-Fuzz); these two stay:

  • mutants.yml stays: it measures the mutation campaigns (MAINTAINING.md section 6), admits CLI files to .github/mutants-pr-cli.txt, and its targeted runs are dispatched and acted on routinely.
  • udeps.yml stays: it is the only check of unused dev- and build-dependencies, because cargo machete reads only [dependencies]; its weekly report has listed the same findings since September 2026 (insta in cargo-rullst, proptest in rullst and loom in rullst-connect are unused; the rullst-orm and rullst-studio entries are test dependencies the all-feature build compiles out), so clear them to make a new finding visible.

Verification efficiency — v12 maintenance and v13

Priority: first, before the planned 12.1.0 updater and concentrated v13 product development. Compatible policy/tooling can serve both release lines after review; it does not require a new framework capability release. The draft/ready split, seven OS shards, Linux-only post-merge repetition, mutation recovery and targeted diagnostic modes described above already exist. Measurement and change-impact observation tooling now exists on v13. A narrowly scoped development-only site admission path is being validated below; affected-crate execution and general cross-commit release-evidence reuse are not enabled.

The ci.yml, documentation.yml and workflow-lint.yml development triggers include v13, as do those of the other branch-filtered workflows except the main-only benchmark, Pages, Scorecard and IoT cryptography workflows; these three jobs are not the whole release gate. The workflow-lint job retains a source/policy-bound rullst.verification-plan.v1 observation. It uses committed Git objects and TOML metadata without executing Cargo, build scripts, macros or project tests. All normal/optional/target/build/dev dependency edges participate in its reverse closure. Renames, deletions, symlinks, unknown paths, policy/lockfile changes, critical crates, generators and potentially executable Markdown retain the full recommendation. Missing history or unsupported graph shapes also fall back to full. may_skip_checks is always false and no workflow consumes this report to skip a job. Candidate package lists are not a complete test/feature matrix.

The separate admit-site-only.py / plan-ci-scope.sh path can avoid repeating Rust CI only for a development main or transitional v13 push whose committed changes exclusively touch docs/home_template.html, docs/site.css or docs/site.js. It requires the immediately preceding commit to have a successful full Linux push CI in this repository within 72 hours, on the same branch, with all 24 expected runtime jobs completed successfully. Run/attempt/source/repository/branch identity and the entire job inventory are checked. A skipped runtime job, unknown job, failure, missing history, unavailable API, changed policy, symlink, mode change or stale receipt retains full runtime checks. The source-equivalence check also prevents an untested intermediate code change from being hidden by a later CSS-only push.

The helpers are loaded from the preceding committed source and executed with isolated Python imports. A candidate cannot replace its own admission helper; the shell’s error path and every runtime job guard also retain full execution if scope planning fails. Admitted pushes still build the book and run static site validation and real Chromium tests, plus the separate documentation and workflow checks. They do not produce a quality scorecard or qualify as release evidence. PRs, v12, manual matrices, fuzzing, mutation campaigns and release admission are unchanged. The first implementation deliberately does not chain presentation-only receipts or search arbitrarily old baselines; a missing immediate full baseline incurs a normal run.

Hosted rollout evidence on September 15, 2026: full Linux push CI 35000185080 at 42dd0545 passed all 25 runtime jobs in 20m57s creation-to-completion. The following presentation-only CI 35002518137 at 3229b762 passed scope admission and fresh site validation in 43s, with runtime jobs skipped as intended. Separate documentation and workflow checks also passed. These are two observed Rust CI elapsed times, not a universal speedup, a sum of all workflow durations, or permission to reuse release evidence.

Documentation and Pages builds install the same pinned mdBook version from its prebuilt release instead of recompiling mdBook on each runner. Book, link and browser checks remain enabled. Dependency changes still require runtime CI.

Before workspace Clippy, check-fuzz-locks.sh resolves all eleven fuzz dependency graphs with cargo metadata --locked and the campaign’s explicit Linux target. It does not compile or execute fuzzers. This catches stale fuzz lockfiles after workspace dependency updates without waiting for a long campaign build. Both campaign preflights use the same full graph resolution; --no-deps is forbidden because a real offline Cargo regression demonstrates that it accepts stale path-dependency locks. Five tests cover that failure, the complete inventory, early failure, argument rejection and CI wiring. Neither the forty-two-target inventory nor campaign duration or release admission is reduced.

After formatting, check-package-reproducibility.sh packages rullst-macros and rullst-orm-macros twice with --no-verify: once from the CI checkout and once from a fresh worktree of the same commit at another path, with new file times and umask 077, each into an empty target directory. Different SHA-256 digests fail the job. It compiles nothing; the release preflight still compares every admitted archive. See reproducible crate archives.

The threat-model runner now validates all evidence rows before downloads or compilation and runs each unique exact test once, instead of first invoking Cargo again to list that target. The current 66 evidence mappings, 50 threat IDs and 65 unique tests therefore need one Cargo test invocation per unique test instead of two, excluding each shard’s retained locked fetch. Every selected test still runs in a separate all-feature process on its deterministic hash shard. Both a successful process exit and a bounded libtest log showing exactly one named, passed, non-ignored test are required. An ignored or missing test can no longer produce a false pass from libtest’s zero exit status.

The LMS threat mapping (ACADEMY-02; v13 retired the other Academy mappings with the complete Academy scaffold) selects security_tests::another_learners_lessons_are_refused in the LMS example (rullst-lms-example, --lib). Since 13.0.0-alpha.4 the LMS starter is that workspace example, so the exact test needs no nested generated build; the workspace shard also runs it with the example’s other tests. The normal foundation and product shards exercise the four reviewed generated configurations: Blank, the hot-reload JSON API, hot-reload SaaS and SaaS’s release build.

Before 13.0.0-alpha.4, the mapping materialized the generated LMS starter (materialized_lms_executes_security_contracts). That narrowing addressed an observed bottleneck: in Rust CI 35007899011, the old all-blueprint wrapper consumed 958.22 seconds inside threat shard 0, whose complete job took 26m12s. That gate needed LMS evidence, not another execution of the seven other project configurations. Hosted threat jobs now use the same bounded two-job nested compiler setting as the normal generated matrix. Removing warm listing calls alone saves overhead, not half the compilation time. The complete optimized Linux run passed all 25 required runtime jobs, plus documentation and workflow validation.

MeasurementBefore: 35011900532After: 35016405011
Required runtime jobs successful25/2525/25
Threat-model shard 0 execution28m34s10m22s
Generated-project threat wrapper1,063.37 seconds227.75 seconds
Summed measured runner time196.68 minutes147.55 minutes
Whole Rust CI elapsed time43m43s61m12s

These source revisions are 0b3588a4 and d0898516. Execution work decreased, but some jobs waited up to 51m40s between creation and start, so the whole workflow took longer. Waiting can include orchestration and runner availability. Cache warmth and overlapping workflows differ; this is not a controlled benchmark or a fixed speedup guarantee. Do not increase shard counts blindly: runner contention can outweigh shorter individual jobs. Complete and validate one feedback batch before pushing another when practical; no required check is cancelled merely to improve the displayed elapsed time.

The timing reporter leaves skipped jobs visible but does not assign them an execution duration. GitHub can synthesize reversed timestamps for an unexecuted job. Malformed timestamps, contradictory executed steps and negative intervals in jobs that actually ran still fail. This observation-only correction does not change site admission or release-evidence rules. The compatible v12 backport must earn its own platform evidence; a v13 run is not a main acceptance receipt.

Six parser tests include a real compiled Rust harness; five runner tests check the full exact inventory, shard partition and failure paths. Four generated scheduling tests compile the actual selectors and wrappers with a recorded verifier, proving the four-case partition without building applications locally. That recorder is policy evidence only: real generated application tests and full CI remain required. These changes do not enable general fuzz-result reuse or relax release gates.

Single-target fuzz diagnostics now compile only the requested target in their package preflight, rather than every other target in that package. The exact target/package pair is checked again before Cargo; unknown, ambiguous or option-like targets fail before compilation. Release mode still compiles every target in all eleven packages before its forty-two 5.5-hour campaigns. Five scheduling tests cover all forty-two diagnostic selections, the complete package inventory, invalid inputs and compiler failure propagation. No sanitizer, instrumentation, corpus or campaign duration is weakened, and diagnostics remain ineligible as release evidence. Fuzzing and mutation campaigns remain manual, and Miri and Kani run weekly besides their dispatched admission runs; an ordinary push does not start any of them.

Read-only local commands (the planner inspects commits, not uncommitted files):

python3 .github/plan-verification.py --base main --head HEAD
python3 .github/test-plan-verification.py
python3 .github/test-report-ci-timings.py
python3 .github/test-admit-site-only.py
python3 .github/test-ci-scope.py
python3 .github/test-exact-rust-test.py
python3 .github/test-threat-model-runner.py
python3 .github/test-generated-evidence.py
python3 .github/test-fuzz-preflight.py
gh api --paginate --slurp \
  'repos/Rullst/Rullst/actions/runs/34980693742/attempts/1/jobs?per_page=100' \
  | python3 .github/report-ci-timings.py --top 10

Initial timing observation: Rust CI run 34980693742, attempt 1 reported head 792c1d554465c016abfdc223cbcaf7d94ec8c0c5 and 42 completed jobs. The longest job was Windows/workspace at 30.05 minutes (27.83 in the combined build/test step); the longest creation-to-start wait was 17.50 minutes. Those waits can include orchestration, not just runner queues. The summed 386.72 runner-minutes are neither elapsed workflow time nor a billing estimate. This one run is a baseline, not a measured speedup, cold/warm comparison or evidence that compilation and test execution have been timed separately.

OrderImprovementAcceptance evidence
1Measure the existing bottlenecks before changing schedulingRecord queue, restore, native compilation, Rust compilation and test durations separately, by OS and cold/warm cache. Compare equivalent inventories and runner conditions. Do not promise a fixed speedup.
2Classify documentation, site, runtime, generator, dependency and verification-policy changesRun the relevant book/link/browser/ledger tests for presentation changes. Markdown containing executable examples still needs its doctests. Unknown paths, renames, deletions and classifier failures fall back to broad verification; .github/** is never automatically a documentation-only exemption.
3Select affected crates and reverse dependencies for development feedbackUnit-test the selection policy against runtime, macros, manifests, lockfiles, build scripts, generated templates, shared fixtures and security controls. Prove no required test or feature combination disappears before enabling skips. Keep full/manual and broad scheduled/release modes.
4Reduce duplicate compilation and balance shards using measurementsIsolate trusted and untrusted caches; key compatible artifacts by toolchain, target, profile, features and dependency inputs. Preserve every case and assertion, report cache misses honestly and measure total runner time as well as elapsed time.
5Reuse expensive verification only under an explicit admission policyBind receipts to source inputs, dependency locks, tool versions, flags, inventory, artifacts and policy. Failed, partial, diagnostic, expired or mismatched evidence cannot become a release pass. Keep conservative reruns for security-relevant or uncertain impact.

The invariants remain unchanged: repository and library coverage floors stay at 90%; negative authorization and tenant-isolation tests remain enforced; no timeout/crash becomes a tolerated success merely to shorten a run. A reduction in redundant work must not be presented as stronger security proof.

Until a reviewed implementation changes the relevant contract, the current AGENTS.md local baseline and exact-source release admission rules still apply. Update automation, policy tests and this document together; a roadmap paragraph alone must never authorize skipping a required check. Carry compatible fixes from v12 to v13, without importing unrelated next-major features into v12.

Preserved next-generation roadmap

These ideas remain valuable, but are not current guarantees:

IdeaStatus and recommendation
Verus proofs for selected production contractsLocally verified v13 candidate; hosted acceptance pending. verus.yml is manual-only, with a pinned archive/compiler/solver, syntax-linked AgePolicy::permits projection, one verified predicate, three mandatory postcondition-failing controls and source/resource receipts. Normal CLI CI checks the source linkage without installing Verus. The pilot remains outside release admission; Auth, Capital, age evidence/time/storage and promotion remain separate work. See the measured scope and assumptions. No additional v12.1 gate.
Loom and Shuttle concurrency explorationNot implemented — worth implementing for the small shared-state primitives that have explicit concurrency invariants. Do not apply them indiscriminately to the whole workspace.
cargo-vet dependency reviewNot implemented — worth implementing once review ownership and audit criteria are defined; an empty policy file would add ceremony without assurance.
cargo-careful and zero-allocation assertionsNot implemented — worth targeted experiments. Allocation claims need stable benchmarks and explicit hot paths before becoming gates.
PGO and BOLTNot implemented — defer until production profiles exist. Fixed throughput-gain percentages must not be promised in advance.
Chaos testing with fail-rsNot implemented — worth implementing around queues, database retries, and provider timeouts after deterministic failure contracts exist.
AFL.rs/honggfuzz differential fuzzingNot implemented — valuable after the 42 libFuzzer targets have healthy corpora and triage ownership.
Cross-platform CI accelerationPartial — first engineering priority, also applicable to compatible v12 maintenance. The inherited v12 baseline has eight cargo test shards per OS and isolated fixture targets. Coverage separately uses nextest; the ordinary CI runner has not migrated to it. Preserve workspace, feature, generated-project, outbox, doctest and live-provider contracts while measuring further scheduling/cache improvements. Speed alone must never reduce assertions or supported-platform evidence.
Differential database testingNot implemented — high-value v13 work. Run equivalent generated ORM operations against the supported relational backends and compare normalized results, errors and transaction behavior; keep provider-specific semantics explicit instead of forcing false equivalence.
Cross-browser and accessibility testingNot implemented — high-value v13 work. Exercise generated applications with Playwright across Chromium, Firefox and WebKit, add keyboard and automated accessibility checks, and retain traces/screenshots for failures. This would complement, not replace, ZAP and server-level integration tests.
Mobile physical-device farmsNot implemented — requires external infrastructure. Add Android and iOS device-farm execution, lifecycle/network interruption scenarios and signed-package evidence when accounts and secrets are governed. Simulator and compile checks must not be presented as physical-device or store-acceptance proof.
Desktop GUI, installer and signing matrixNot implemented — requires platform signing operations. Exercise real WebView sessions and packaged installers on supported Linux, Windows and macOS versions, then validate signatures/notarization without exposing signing credentials to untrusted pull requests.
Live provider sandbox conformanceNot implemented — requires governed provider accounts. Add opt-in acceptance suites for payment, mail, OAuth, AI, search and storage providers, recording provider/version/region while preserving deterministic offline fixtures as the contribution baseline.
Independent security review and penetration testExternal evidence — strongly recommended before broad production assurances. Define scope, remediation ownership and retest criteria; repository automation cannot award this evidence to itself.
Sigstore Cosign signingNot implemented. Consider it for separately distributed binaries/containers; current .crate provenance and checksums should remain the immediate priority.
Absolute “100% pure Rustls” mandateNot established and not recommended as a marketing absolute. Enforce an audited TLS dependency policy based on supported platforms and threat model instead.
Complete upstream OSS-Fuzz integrationPartial draft — worth finishing. Validate every intended target with helper.py build_fuzzers and check_build, then submit upstream; do not imply acceptance before merge.

Cross-platform CI acceleration acceptance plan

The September 6 v12 candidate measurements record the pre-optimization baseline: the all-feature workspace and follow-up contracts took approximately 70 minutes on Linux, 59 minutes on macOS, and 107 minutes on Windows. Most time was spent in the clean cargo test --workspace --all-features build. At that checkpoint, only Cargo registry data was cached. Workspace target/ caching was disabled after CLI integration tests reused and cleaned nested target paths while the post-job cache collector traversed them, producing false missing-file annotations and multi-gigabyte uploads.

This is historical context, not the current configuration or expected duration. The stable v12 baseline now uses the sharded cargo test system documented above; pinned nextest currently schedules the coverage pass only. New experiments must measure that inherited configuration, not claim a speedup against obsolete commands or omit checks that moved into other jobs.

An acceleration change is acceptable only when all of these conditions hold:

  1. Give repository compilation and every generated-project family distinct, explicit target roots. A generated fixture may clean only its own disposable root and must never mutate a cached workspace target.
  2. Record cold and warm wall time, cache size/hit data, discovered test counts, failures and doctest results on Linux, macOS and Windows. Compare equivalent commit content; queue time is reported separately from execution time.
  3. Preserve the all-feature workspace test inventory, portable transactional outbox contract and Linux live-provider matrices until an alternative proves identical coverage. Package sharding must not weaken Cargo feature unification.
  4. Evaluate a pinned compiled-artifact cache and sccache independently before composing them. Keys must include OS, compiler, lockfile and relevant profile inputs; caches are performance hints, never release artifacts or evidence that tests ran. Bound storage and prevent untrusted pull requests from replacing a protected default-branch cache.
  5. Evaluate nextest for ordinary CI (or change its coverage configuration) only if tests, ignored-test policy, retries, process cleanup and failure reporting remain equivalent. Run Cargo doctests separately because nextest does not replace them. Keep plain cargo test as a documented recovery path.
  6. Promote the experiment to blocking CI only after repeated green cold and warm runs on every supported OS show a material wall-time reduction without new flakes, lost tests, hidden failures or multi-gigabyte cache churn. Retain the previous workflow as a quick rollback during the observation window.

For development-only presentation skips, comparing with the immediately preceding commit is insufficient: that commit might have an unfinished or failed runtime run which a new push cancels. Admission must bind a completed successful runtime inventory to its exact baseline commit, compare that baseline with the candidate, and reject incomplete, stale, diagnostic or presentation-only receipts. Manual/release runs remain full. Do not infer receipt validity from a green workflow badge, file extensions alone or the observation report.

This work optimizes feedback latency, not the evidence boundary. Any v12 maintenance change must preserve the immutable v12.0.0 artifacts and carry its own applicable verification. Further scheduling/cache changes need comparable cross-platform A/B receipts before their claimed benefit is accepted.

The goal of this roadmap is stronger, reproducible evidence—not a larger number of badges or absolute claims that no finite test suite can establish.