Development
The toolchain version is pinned in rust-toolchain.toml, today at 1.97.0, unsafe is
forbidden in every crate, and the lint table denies panics, lossy casts, wildcard
matches, and undocumented errors. The version in that sentence is this book’s first live
value claim: the definition below pins the pinning line itself, so a
toolchain bump that forgets this page fails the repository’s own gate with the corrected
expectation in the finding.
Gate-tool versions live in one place, the workspace.metadata.tools tables of the root
manifest. Runtime consumers use Cargo’s projection directly: the CI tools composite and
ratchet hooks query cargo metadata with jq. The pinned tool names and versions key a shared
cache warmed by main; jobs install only their requested tools on a miss, and pull requests
never save partial benches. The agent lanes use the same composite and gh-aw’s native Copilot
installer without private CLI caches. A documentation-contract test parses the
manifest independently and refuses any workflow spelling a declared tool at another version.
Bumping a tool is one edit, except cargo-about: a release restores no cache, so the release job
and the self-scan that mirrors it spell its version in the workflow, and that test holds both to
the manifest.
Hooks run through prek: formatting and the cheap checks
on commit, then Clippy with
warnings denied, the full test suite, cargo deny, cargo shear, and a
similarity-rs twin-function gate on push. The tool
compares functions within one file, so a file that did not change keeps its edges, and the gate
scans only the Rust files that differ from the base commit, in both trees. It also concatenates
the deliberately parallel provider transports, lane-test harnesses, service runtimes, and
verification files, and compares those when one of them changed, mapping the generated lines
back to their source paths. Pair identities are stable and follow renames. Every candidate edge
must already exist in the base set, so removals pass while new relationships, including an
equal-count remove-and-replace, fail. Nothing is cached, and no baseline or allowlist lives in
the tree.
A last push-stage hook
runs cargo-sweep over target/, dropping artifacts and
incremental sessions older than two days; cargo never collects superseded builds, and this
repository mints a fresh copy of every test binary on each lockfile or version change. Five days
held 86 GB and the sweep reclaimed nothing from it, because every generation was inside the
window. The hook
is a no-op where cargo-sweep is not installed. Code CI runs the same two hook stages. What CI adds on
top is the work that does not belong on a developer’s machine: the fuzz packages’ release runs,
whose release builds cost minutes, and mutation, which costs ten of them for a code change. A
push should not buy what a pull request already measures.
CI also runs the module-cycles lint through
Dylint, pinned by commit under workspace.metadata.dylint.
It reports sibling modules that depend on each other in a cycle, where a module counts together
with everything nested in it and a parent and its own child are never compared. The workspace has
none since #1133 was worked through, so the job
denies the lint and a new cycle fails it.
The shared change detector
compares Git revisions, including renames and deletions. Documentation-only changes retain the
cheap hooks, documentation contracts, book build, and self-scan, but skip unrelated platform,
coverage, fuzz, packaging, and mutation builds. Reviewer-only changes retain the cheap hooks
and Actions analysis. Rust, fixtures, schemas, build configuration, and unknown paths take the
full code lane. The required workflow always starts, and gates fails if classification fails;
there are no workflow-level path filters. Weekly and manual CI runs force full code validation.
The platform and coverage suites include the JSON, generated-report, and documentation
contracts. The separate contract job runs documentation contracts on documentation-only changes.
The commit hooks also reject handwritten Serde implementations, callbacks, trait-bound overrides, and custom JSON representation guards. Models use derives and library adapters; domain checks remain separate. See Architecture for the standard Serde input behavior and the canonical representation required by authenticated boundaries.
Tests use the production wire models too. Load published examples into those types, mutate typed fields for domain cases, and call the production validators. Raw JSON mutations and independent byte fixtures cover representation and malformed-input cases; they do not need parallel wire structs or test-only encode/decode helpers.
Coverage executes the whole workspace with instrumentation and keeps the 85% line floor, but
excludes the three fixture-only packages from the report. It does not replace uninstrumented
platform tests or the release-mode eligibility checks. Rust caches remain separate across
platforms and build settings; disposable CI runners remove their unused floating stable
toolchain before restoring them so runner image updates cannot invalidate the pinned-toolchain key.
The test profile strips debug information from executable fixtures, which are repeatedly copied
and verified. Symbols and assertions remain; source-line backtraces can be restored locally with
CARGO_PROFILE_TEST_STRIP=none. Every dependency is optimized in development and test builds,
since dependencies are cached and most tests spend their time in them. The workspace crates are
not: a package override reaches the package’s test binaries too, and those are what a build waits
on. Debug assertions and overflow checks stay on, and release settings are unchanged.
Compiling the tests costs far more than running them, so platform CI builds the binaries in one
step and runs them in another: a slow run says which half it was. The build is throughput bound
on a four-core runner, so each platform splits in two. The libraries half builds the engine’s
library crates; the programs half builds the CLI, the bootstrap wrapper, and every package under
controller/ and api/, whose tests start a process per case. A new crate lands in the libraries
half unless it lives under those two directories. Each half keeps its own dependency cache, since
Cargo resolves features for the packages it builds and the halves resolve some shared dependencies
differently. A restored dependency cache
still leaves every workspace crate to compile. To see where that time goes, add --timings to
cargo nextest run --workspace --locked locally and read Cargo’s report next to nextest’s summary.
Windows links the test binaries with rust-lld and runs eight test threads, since starting a test process there costs more than running most tests.
Platform CI also builds without debug information. The test profile strips it at link time, so
generating it only hands the linker bytes it discards. Local builds keep their line tables, since
the setting is an environment variable on the CI job rather than a profile change.
CARGO_PROFILE_TEST_DEBUG and CARGO_PROFILE_DEV_DEBUG turn it back on anywhere.
The wire integration tests share one executable, so the Serde code they instantiate compiles once
instead of four times. That crate sets autotests = false and names its own target, so a new file
under crates/amiss-wire/tests runs nothing until a module declares it; add cases to the modules
that are already there. Targets with no unit tests set test = false, which skips an empty
executable per target; turn it back on when you add one, since integration tests reach those
libraries and binaries either way. Two targets stay on their own. The fatal-envelope test counts
allocations process wide, and the two release eligibility checks live in eligibility so the
extra release build stays small. They run in their own job, on Linux for a pull request and on
every release platform for main and the weekly run.
Two similarly named files point in opposite directions. .pre-commit-config.yaml is the hook
table this repository runs on itself through prek. .pre-commit-hooks.yaml is the hook this
repository publishes: a consumer’s own pre-commit configuration names this repository and reads
that manifest to discover the amiss staged-index check shown in
Running it in CI.
cargo nextest run --workspace --locked
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test -p amiss-fuzz -p amiss-controller-fuzz --locked --release
The first pair checks every crate, engine and provider alike, from one lockfile. The third
replays the committed fuzz seeds in release; the coverage-guided runs need nightly and cargo-fuzz
on top, from the same lockfile. The trust boundary is a dependency boundary rather than a workspace boundary:
the compiler-output specialist under api/ and the HTTP, provider API, Git acquisition,
credential, storage, and service-runtime crates under controller/ are unpublished.
deny-engine.toml drops them from the graph and then bans the network and async stack, so what an
amiss user downloads cannot acquire it. The prek hooks run the first pair and Linux CI runs both.
The macOS and Windows jobs also run the controller tests, including the cross-process file stores,
provider authentication, worker, and supervised-process cases. The supported service deployments
are documented in
Provider-verified controls.
Tests answer to a house rule called the teeth check: important tests are exercised against deliberately broken behavior before they are trusted. The mutation lanes publish a non-gating measurement of that property, in three sizes: per pull request, per push to main, and on demand, each on its own trigger.
Every pull request measures only the mutants the change itself reaches, over shards counted
from the mutants the diff actually reaches rather than from a guess, because listing them needs
no build. Zero mutants skip the worker entirely; nonempty diffs use ten mutants per shard,
capped at eight shards. That lane asks the whole workspace whether each mutant lives, at about twenty seconds
per mutant. The shards used to pay a worse floor, a cold workspace build plus a baseline test
pass repeated in every shard; now each shard restores the build cache that the baseline job
saves on code pushes to main, and skips its own baseline because required CI proves the same commit,
so what remains is the build delta against the last merge and the mutants
themselves. A release pull request measures its version and generated-file diff like every other
pull request. The code it packages was already measured before each change reached main; measuring
the accumulated release again would duplicate that work and eventually exceed the bounded PR lane.
The sweep over every mutant in both workspaces runs only when someone asks for it, through
workflow_dispatch. It is split across shards sized from the mutant count rather than a fixed
number, it takes tens of minutes, and it exists to find gaps in code that no longer changes,
which is not something a release should pay for. Fixture crates are excluded, because code that
exists to be exercised by its callers says nothing about the tests. Unlike the smaller lanes it
runs each mutant against its own package’s tests, which is what makes it affordable and also
means a mutant that a sibling package covers is reported as surviving. Its output is a list to
verify, not a verdict.
The sweep of 2026-07-28 is the reading to compare against: 6,523 mutants, 4,103 caught, 1,335 missed, 1,085 unviable, no timeouts. Roughly half of those missed are the scoping artifact above, measured at 43% on one file and 62% on another, so the number of real gaps is nearer seven hundred. A later sweep that misses far more has either lost tests or gained untested code, and the point of writing the figure down is to be able to tell which.
None of the three gates a merge and none certifies a global mutation threshold: a surviving mutant is a place where a lie would go unnoticed, to be judged against whether the perturbed value is observable through real behavior, not a score to raise.
Three agent lanes sit beside the gates, none of them gating, written as
gh-aw workflows whose agents hold no write token at
all: they run read-only on DeepSeek inside the repository’s runners, and a separate harness
job posts what their structured outputs request, nothing else. A new issue gets its premise
checked against the tree before a maintainer reads it, whoever opened it. Every push to a
same-repository, non-draft pull request gets one consolidated review, summary and
line-anchored comments in a single card, and a review can be dispatched by hand for any PR
number. And /oc in a comment summons the agent, which can answer, fix, and propose a pull
request through the same structured channel. What the lanes read is a claim under test,
never instructions; only the mention lane takes tasks, and only from a collaborator’s
comment. Each comment carries its run link, and the full agent log survives as a run
artifact.
The parsers sit under a vendored test corpus, pinned by digest, whose manifest records node
counts, extraction results, and byte positions for every case from the upstream CommonMark,
GFM, and MDX suites; the
corpus notes document every
known difference. Scanner parsers that take untrusted bytes have targets under fuzz/.
The controller fuzz package
signs generated provider requests before varying their facts, so its account-free targets reach
the provider identity and binding checks. Both suites carry committed seeds and a
nightly coverage-guided run.
The scanner runs on its own repository under --profile enforce in CI. This documentation
passes through that same gate: every relative link in this book resolves in the tree, or
the pull request that broke it fails.
Every pull request also packages what a release would upload. cargo package over the
publishable members resolves their siblings through a temporary local registry and builds every
tarball, so a file dropped from a package, a path dependency missing a version, or a new crate
its dependants cannot see fails on the pull request instead of halfway through an upload that
cannot be taken back. It is cargo package rather than cargo publish --dry-run because the
dry run prefers a version already on crates.io over the tree, which makes it blind to exactly
the crate a change is adding.
Releases are automated. A bot keeps a release pull request current with the version bump,
changelog, and exact Action-dispatch ref. Merging it publishes the crates and source tag while
the GitHub release remains a draft. The release workflow then assembles the immutable
action/vX.Y.Z tree and exercises both that exact tree and the source-tag dispatcher on Linux,
both macOS architectures, and Windows. Only a green smoke matrix advances the stable major ref
without rewriting history and makes the release public; prereleases never advance the major ref.
The same gate governs the release assets: the per-platform engine and prober binaries, their
THIRD_PARTY_LICENSES.txt, their SHA256SUMS, and the sigstore bundle attesting that file attach
to the draft, so a release that fails the matrix never publishes a binary.
If a forge outage leaves that pull request stale, manually dispatching the
release automation on main refreshes its metadata
without running the publishing job; crate publication remains restricted to pushes on main.
Security checks layer in CI as well. Dependency update PRs arrive with a cooldown, a
weekly advisory re-check runs against a fresh database, and
CodeQL covers both the Rust and the workflows. The Rust analysis takes
about ten minutes, so it runs on main and weekly, and a pull request gets the workflow analysis.
Scorecard, secret scanning with push protection, and build
provenance attestations on release binaries round it out.