When evaluating test-level references in ADRs, also check for spx/local/rust-tests.md and apply any repo-local routing to governing test-level specs or decisions.
A local overlay supplements skill behavior; it does not declare product truth.
The ADR template (from /understanding) is decision-first — the decision is stated directly under the title, with no Purpose heading and no preamble:
- Title + decision --
# {Decision Name}, then the decision stated directly as permanent truth in 1-3 sentences: what it governs and what it decides. - Rationale -- Why this is right given the constraints. Name a rejected alternative only when it sharpens the decision. Omit if self-evident.
- Invariants (optional) -- Algebraic properties that hold for ALL governed code. Omit if none apply.
- Verification -- Each rule is an ALWAYS guarantee or a NEVER boundary, grouped under the one subsection naming how it is verified:
### Testing(deterministic test,([{assertion type}])),### Eval(graded LLM behavior,([eval])),### Audit(agent judgment,([audit])), ordered by decreasing enforcement strength. Include only the subsections that apply.
This is the complete list. An ADR has no other sections. There is no Purpose heading, no Context section, no Trade-offs section, no Testing Strategy section, no Status field, no Level Assignments table — business context and trade-offs fold into the decision statement and Rationale. Rust architecture rules — DI mandates, mocking prohibitions, unsafe boundaries — require agent judgment, so they live under ### Audit with ([audit]).
When an ADR is required: Every module or boundary that makes architectural decisions -- module layout, trait seams, library choice, async runtime, DI patterns, persistence boundaries, unsafe boundaries -- requires an ADR. Missing ADRs are violations.
ADRs do not assign testing levels. They establish constraints that make levels achievable. The /testing router and /testing-rust decide how assertions are verified once they combine spec assertions, architecture constraints, and product infrastructure.
The mechanism: Verification rules under ### Audit that mandate observable seams, explicit ownership and boundary types, DI, and the absence of mocking frameworks.
Correct pattern -- testability as ALWAYS/NEVER under ### Audit:
## Verification
### Audit
- ALWAYS: external tool invocations accept an injected runner trait or function parameter -- enables isolated testing without framework mocks ([audit])
- ALWAYS: configuration is parsed into typed structs at load time -- enables Level 1 verification of config logic ([audit])
- NEVER: `mockall`, `faux`, or generated mocks as the primary testing strategy for architectural seams -- violates reality-based testing ([audit])
- NEVER: direct `std::process::Command` construction in domain logic without an injected seam -- prevents isolated testing ([audit])
What this replaces -- the following does NOT belong in an ADR:
Testing Strategy
Level Assignments
| Component | Level | Justification |
| ---------------- | ----- | ------------------------------- |
| Command building | 1 | Pure function, no external deps |
| CLI execution | 2 | Needs real binary |
Escalation Rationale
- Level 1->2: binary required for acceptance
Why: Level assignments depend on spec assertions, local tooling, and the /testing analysis flow. The ADR cannot know those details at authoring time. The ADR's job is to establish constraints that make the right tests possible.
ADRs state architectural truth. They never narrate code history, current implementation snapshots, or migration plans. This is a rejection-level violation in any section.
An ADR that references current code ("The current module uses X", "The file Y does not exist") becomes stale when the code changes. Code that violates an ADR is discovered later through code review and test evidence.
Temporal patterns to reject:
- "The current
parser.rsuses..." -- narrates code state - "The file
legacy/mod.rsdoes not exist" -- narrates filesystem state - "We need to replace..." / "We need to migrate..." -- narrates a plan
- "Currently X uses..." -- snapshot that expires
- "The existing implementation..." -- references code, not architecture
- "After evaluating options..." -- narrates decision history
- "Previously..." / "Before this..." -- there is no before
- "Going forward..." / "In the future..." -- there is only product truth
The rewrite pattern:
TEMPORAL: "The current command runner in
cli.rsconstructsCommanddirectly."ATEMPORAL: "Command orchestration accepts an injected runner seam."
TEMPORAL: "We discovered that direct process calls make testing impossible."
ATEMPORAL: "Direct process invocation prevents isolated testing. Injected runner seams enable L1 verification."
TEMPORAL: "The existing async handler holds a mutex across await."
ATEMPORAL: "Async request paths release locks before await points."
When an ADR mandates dependency injection, these are acceptable Rust patterns to reference in ## Verification ### Audit rules.
Trait-based DI:
pub trait CommandRunner {
fn run(&self, program: &str, args: &[&str]) -> Result<CommandOutput, CommandError>;
}
pub fn build_site<R: CommandRunner>(
config: &BuildConfig,
runner: &R,
) -> Result<BuildResult, BuildError> {
runner.run("hugo", &["--destination", config.output_dir.as_str()])?;
Ok(BuildResult::success())
}
Function-based DI:
pub fn sync_repo<F>(
source: &Path,
destination: &str,
run: F,
) -> Result<SyncResult, SyncError>
where
F: Fn(&str, &[&str]) -> Result<CommandOutput, CommandError>,
{
run("git", &["fetch"])?;
Ok(SyncResult::success())
}
ADR Compliance rule to code mapping:
| ADR Verification rule | Code implements |
|---|---|
| "ALWAYS accept runner as parameter" | fn f<R: Runner>(deps: &R) or fn f(run: impl Fn(...)) |
| "ALWAYS validate config at load time" | serde + typed structs + constructor/TryFrom validation |
| "NEVER use mock frameworks" | hand-written trait impls or recorder structs in tests |
| "NEVER shell out without DI wrapper" | no bare std::process::Command in core logic |
Mocking prohibition in ADR language:
The auditor checks for these violations in ADR text:
mockall,faux, "auto-generated mocks", or "mock the boundary" described as the intended testing strategy- spying on the function under test rather than exercising it through a real seam
- generated doubles where a small controlled implementation would preserve coupling
Correct ADR language: "Use dependency injection to isolate X from Y" or "Accept X as a parameter implementing trait Y."
The architect needs enough testing context to write effective Verification rules. The auditor uses the same context to check whether those rules actually enable the right tests.
| Level | Name | Rust Infrastructure | When to Use |
|---|---|---|---|
| 1 | Unit | Rust stdlib, cargo test, temp dirs, git |
Pure logic, parsing, validation, command building |
| 2 | Integration | workspace binaries, local services, Docker, databases | Real CLI binaries, real DB adapters, local worker flows |
| 3 | E2E | external network, SaaS APIs, browsers, deployed systems | Full workflows with remote systems or browser UI |
Key rules:
- Git, filesystem, tempdirs, and standard Rust tooling are Level 1
- Real workspace binaries, codegen tools, and local services that require setup are Level 2
- External APIs, SaaS systems, browsers, and deployed environments are Level 3
- Product specs or decisions may disable Level 3 when the suite cannot safely stand up, isolate, or clean up the external collaborator;
spx/local/rust-tests.mdmay point the skill to that declaration
How levels relate to ADRs: The ADR does not assign levels. It establishes constraints that determine what levels are achievable. "ALWAYS accept a runner as parameter" makes Level 1 possible for command-building logic. "NEVER call external APIs from domain logic" preserves Level 1 for the domain and pushes the real remote boundary to Level 3 or to a repo-specific product decision.
| Anti-pattern | Why it is wrong | Where it belongs |
|---|---|---|
## Testing Strategy section |
Not in the authoritative ADR template | /testing skill output |
| Level assignment tables | Downstream concern; depends on spec assertions | /testing Stage 2 |
| Escalation rationale | Downstream concern; depends on product infra | /testing Stage 2 |
## Status field |
Not in the authoritative ADR template | Git history / commit metadata |
| File names to delete | Temporal; becomes stale immediately | Code review against ADR invariants |
| Migration plans | Temporal; narrates a transition | Code review / work items |
| Implementation code | ADRs constrain implementation, not provide it | /coding-rust |
Source: outcomeeng/plugins — distributed by TomeVault.