Scope
- Applies to all Rust changes (bins, libs, build scripts).
- If repo-specific Rust docs conflict, repo docs win.
Non-negotiables
- Use the edition and toolchain defined by the repo (Cargo.toml edition + rust-toolchain.toml). Do not assume 2024.
- No
unsafeunless explicitly requested, no practical safe alternative exists, and the call site is justified with a preciseSAFETY:proof plus tests. - No stringly errors in public APIs.
Build & lint loop (required)
For any meaningful change:
cargo fmtcargo test(smallest scope that covers the change)cargo clippy(smallest scope that covers the change)
If the repo requires it: run cargo clippy --fix -p <crate> first, then rerun clippy.
If you cannot run commands, say exactly which command to run and why you couldn’t.
In any change summary, explicitly list Cargo.toml changes (deps/features) and the reason.
API design
- Document public APIs with
///including:- side effects, blocking/async behavior
- errors (what can fail and why)
- invariants and ordering guarantees
- Make optionality explicit in types:
Option<T>or enums; avoid prose-only “optional”. - When a closed set of string keys/roles/selectors is used internally, model it as an enum and centralize string conversion via
Display/EnumString(orFrom/TryFrom), using a single#[strum(serialize_all = "...")]scheme; keep raw strings at API boundaries. - For any closed set of features/capabilities/toggles that drives Rust behavior, define a semantic enum and use it as the single source of truth for iteration, summaries, and dispatch.
- Prefer this pattern:
enum FeatureKind { ... }FeatureKind::iter()match feature_kind { ... }
- If the wire/config shape is still a struct of booleans for serde or CLI reasons, add one accessor like
FeatureKind::is_enabled(&Config) -> bool. - That accessor MUST destructure the source struct without
..and MUST use an exhaustivematch. - Goal: adding a new feature should fail compilation until all enum-driven behavior is updated. Do not allow silent omission from summaries, dispatch tables, or validation.
- Do not maintain repeated
if cfg.foo { ... } if cfg.bar { ... }lists across multiple functions when those flags form a closed set. Replace them with an enum-driven loop plus one exhaustive accessor.
- Prefer this pattern:
- MUST keep branches pure (input shaping only) and execute shared side effects exactly once.
- MUST decompose multi-phase functions into named phase helpers with typed intermediate artifacts.
- MUST lift helper types to module scope when used by multi-phase orchestration; avoid ad-hoc local type declarations in large functions.
- MUST enforce function complexity limits via lint/CI; exceptions require explicit, documented justification.
- MUST NOT introduce pass-through abstraction layers that merely mirror lower-level APIs.
- If an abstraction wraps lower-level primitives, it MUST add boundary value (policy, invariants, validation, orchestration, or domain semantics).
- Keep storage/transport mechanics in the infrastructure layer and keep project-layer contracts focused on behavior and rules.
- When pulling multiple fields from a struct, prefer destructuring (
let Foo { a, b, .. } = foo;) over repetitivelet a = foo.a;bindings to reduce omissions and keep intent clear.
Host Inputs And Testability
- Do not make core logic read process-global state directly when those values affect behavior. Examples include environment variables, current directory, runtime/config paths, wall clock, command paths, sockets, and host service locations.
- Prefer a shared typed context as the production and test interface.
Production should build it once at the boundary, for example
HostContext::discover(), and tests should construct the same type directly with deterministic values. - Use plain structs for value snapshots captured at one point in time, such as env-derived paths, cwd, runtime dirs, config paths, command paths, and fixed timestamps.
- Use small semantic traits for behaviorful dependencies, such as command execution, filesystem mutation, clocks that advance, network calls, secret resolution, or session catalogs.
- Prefer domain traits over pass-through global wrappers.
For example, prefer
SecretResolver::resolve(&grant)over a genericEnvProvider::get("KEY")unless environment lookup is the actual domain contract. - Keep production adapters thin and boring: read
std::env,std::env::current_dir, wall clock, host paths, or process-global values once, map them into typed inputs, then call the shared core. - In tests, do not mutate process-global state with
std::env::set_var,std::env::remove_var, orstd::env::set_current_dirto control core behavior. Use typed contexts, trait fakes, temp dirs, or child-processCommand::envinstead. - Use
Command::envandCommand::env_clearfor subprocess tests when validating CLI or process-boundary behavior. - Avoid test-only global mock contexts as a first choice. They are acceptable only as a temporary compatibility bridge when extracting a shared production/test interface would be too large for the current change.
- If adding a trait introduces generic clutter through many call layers, move the trait to the boundary and pass a typed result/context inward instead.
CLI + Error Discipline (canonical IDs)
R9 THIN_CLI_TYPED_CONTRACTS: Keep business logic out of CLI crates and default to typed command/behavior tests.- Use argv parser tests sparingly as boundary contract checks for stable/documented CLI surfaces (for example, one smoke test per critical command shape/flag family).
- Prefer
Command::debug_assert()for command-shape sanity and integration-style tests (for exampletrycmd) for documented command examples. - Avoid broad stringly argv test matrices that duplicate typed behavior tests.
R10 STRUCTURED_ERROR_DISCIPLINE: Preserve structured error chains with call-site/location metadata; avoid stringifying source errors and prefer structuredtracingdiagnostics.
Concurrency
- Prefer channels (
mpsc,broadcast,watch) over shared mutable state when feasible. - Never block in async contexts. Use tokio primitives (
spawn_blocking, async IO,tokio::fs). - If an API is async end-to-end, keep it async; avoid hidden runtimes/blocking shims.
Dependencies
- Prefer established crates for common tasks (tempfile, tracing, bytes).
- Minimize dependencies, but do not reinvent correctness.
- Run
cargo auditwhen dependency changes are security-relevant. - When changing Rust code that requires new deps or feature flags, update Cargo.toml and mention it in the change summary.
Non-empty collections (type-level)
- When non-emptiness is a semantic requirement, represent it at the type level.
- Prefer the
mitseinnon-empty collection types (e.g.,mitsein::Vec1,mitsein::Slice1) overVec/Optionwhen emptiness is invalid. - When a field is optional but semantically non-empty when present, prefer an outer optional non-empty shape such as
Option<mitsein::vec1::Vec1<T>>(or the repo's established equivalent) instead ofVec<T>plus emptiness conventions. - When an outer optional non-empty collection already carries presence, do not re-encode absence inside nested selector/item variants with shapes like
Option<String>or empty-capable inner vectors. Flatten the enum/item contract under the outer collection unless the nested grouping is itself semantically required.
Agent instruction (ask once):
- If the repo already depends on or consistently uses
mitseinfor non-empty semantics, treat that as the established default and do not ask again. - Ask once only when the repo does not already establish a non-empty collection choice. If the user confirms
mitsein, use it consistently for that repo from then on.
Trait usage and testing pattern
Production/library code:
- Prefer generics /
impl Traitfor performance and clarity.
Test harnesses:
- Use trait objects (
Box<dyn Trait>) when it reduces boilerplate or you need to swap implementations.
Contract tests:
- Contract test logic should be generic (
fn contract<S: Trait>(...)orasync fn ...). - Fixtures should be thin and may return
Box<dyn Trait>.
Decision signal:
- If you are threading many generic type parameters just to run tests, switch the fixture to
Box<dyn Trait>.
Error handling
Error Stack Choice
- Follow the repo’s existing error strategy if one is already established.
- For new crates (or new major subsystems without an existing convention), prefer
n0-errorfor typed errors with call-site location.
n0-error usage rules:
- Libraries: define concrete error enums/structs that participate in the stack error chain (via n0-error derive/macros) so call-site metadata is preserved through sources.
- Applications/tests: prefer
n0_error::AnyErrorfor ergonomic error propagation while preserving location when originating errors are stack errors. - When converting plain std errors into
AnyError, use the crate’s extension methods (e.g., std-context / anyerr) rather than relying on blanketFrom, which can lose stack metadata. - Construct errors using
Meta::new()or the crate’s constructor macro (e.g.,e!()) so call-site location is recorded.
Do not refactor existing crates to n0-error unless:
- the repo explicitly requests it, or
- the change is already touching most error surfaces and the migration is low risk.
Error boundaries (typed vs. AnyError / anyhow)
- Public library boundaries that callers may branch on MUST return a typed error (enum/struct).
- Example: adapter ingress, domain command handlers, validation APIs.
- Internal helpers MAY return
n0_error::AnyError(oranyhow::Errorin legacy crates) for ergonomic context and propagation. - Boundary rule: never let
AnyError/anyhow::Errorescape through a typed boundary.- Map at the boundary into the typed error:
- Provide
TypedError::Other { source: AnyError }(or an equivalentother(source)constructor). - Implement
From<AnyError> for TypedErroronly if it maps unconditionally intoOther { source }to keep?ergonomic.
- Provide
- Map at the boundary into the typed error:
- Prefer preserving a small set of semantic variants in typed errors (e.g.,
Unmatched,InvalidInput,MissingMapping), and funnel everything else intoOther { source }.
Practical guidance:
- Inside a typed boundary, avoid
bail!()/anyhow!()unless you immediately map into the typed error. - If a dependency error is common (e.g., parse errors), add a targeted
From<DepError> for TypedErrorso?stays ergonomic.
Logging
- Use
tracingwith structured fields. - Log: startup config summary, client connects/disconnects, auth/session events, and errors with context.
- Avoid logging secrets and raw payload bytes.
Testing
- Test code only: MUST use
#[tokio::test(flavor = "multi_thread")]for async tests unless the repo specifies otherwise. - Test code only: MUST keep exactly one async test runner attribute on a test function (for example
tokio::test). - Test code only: MAY combine
test-casewithtokio::teston one async test function.- MUST place each
#[test_case(...)]attribute before#[tokio::test(...)]so cases are discovered once and executed once.
- MUST place each
- Test code only: SHOULD prefer one parameterized contract test over many near-identical tests.
- Add cases only for new semantic branches/invariants.
- Test code only: MUST prefer deterministic tests and avoid timing-dependent sleeps.
- Keep tests in dedicated test modules rather than mixing test-only imports and helpers into production scope.
- For modules over 200 LOC, prefer a separate
tests.rssibling module with#[cfg(test)] mod tests;in the main module file.
Bytes guideline (variable payloads vs fixed IDs)
Use bytes::Bytes for owned, variable-length payload bytes (network/file payloads, serialized messages, blob contents). Bytes clones are cheap and avoid copying.
Do not use Bytes for fixed-size identifiers; use [u8; N] newtypes (e.g., [u8; 32]) for hashes/keys/IDs. If serialized, encode as byte strings (e.g., via serde_bytes field attributes).
Serde:
- Enable bytes serde:
bytes = { version = "...", features = ["serde"] }. - Add a codec-specific test when necessary to ensure byte buffers encode as byte strings (not arrays of integers).
CLI Contracts (Clap-Derive-First)
Use clap derive as the primary contract surface for Rust CLIs.
- Model each user-selectable domain as a typed enum deriving
clap::ValueEnum. - Prefer
#[arg(value_enum)]+default_value_tover manual parsing. - For list args, prefer clap-native tokenization (
value_delimiter,num_args) over ad-hoc split logic. - Do not parse raw strings to decide allowed values; let enum variants define the accepted set.
- Implement special list semantics (for example
all) as typed post-parse normalization on enum values. - Enforce cross-flag invariants in one typed validation step after clap parsing.
- Reuse the same enums/validators across handlers and tests to keep CLI behavior DRY and single-source.
Typed Boundary Contracts (Single-Source)
Use typed structs/enums as the canonical contract surface for internal Rust boundaries.
- Map external/raw payloads into typed domain models exactly once at the boundary.
- Do not use
BTreeMap<String, ...>(or equivalent string-key maps) as authoritative internal contracts. - Do not round-trip enums through strings in Rust logic/tests when typed values are available.
- Build typed contract values directly in tests; only parse string-key maps when explicitly testing boundary interop.
- For persisted, cross-module, or branch-driving key namespaces (for example metadata keys, storage key prefixes, discriminator tags), MUST model the namespace as a typed enum and centralize wire conversion on that type (prefer
strumderives for stable string forms). When a closed domain identity or selector in that role also needs typed payloads plus variant-kind introspection, MUST use one tagged data-carrying enum as the source of truth and derive the discriminant enum from it (for example viaEnumDiscriminants) rather than maintaining parallel payload and kind enums by hand. Prefer derive-basedserde/strumsupport over custom serialization when the wire shape permits it. - If string-key interop is unavoidable, centralize keys in shared enums/discriminants and helpers; avoid inline magic strings.
- When a wire contract remains
BTreeMap<String, ...>, MUST provide namespace-scoped map extension traits that accept typed keys; Rust logic MUST NOT use inline key literals for those namespaces. - Persisted or cross-module composite tokens (for example anchors/keys/ids with structured segments) MUST use a single typed codec/newtype in a shared module. If the semantic source of truth is already a typed struct or enum, that typed contract SHOULD own the canonical string encoding and decoding used for routes, query params, DOM/data keys, and other boundary strings rather than introducing a second wrapper or helper type as the primary string surface.
- Producers and consumers MUST use that codec/newtype for all encode/decode/normalize behavior; ad-hoc
format!()/split/starts_withparsing is not allowed. - Legacy wire-format compatibility, when needed, MUST be implemented only inside that codec/newtype.
- Required tests for such codecs: producer-consumer roundtrip plus delimiter-collision and case-normalization contracts.
- Inline key literals are allowed only in boundary interop tests or one-off local debug output.
- Repeated semantic strings MUST NOT remain inline once they define stable behavior at a Rust boundary.
- Any semantic tag/code/route/key that crosses a module, crate, API, persistence, or test boundary MUST have a single exported contract definition.
- Prefer enums for closed vocabularies and public constants for raw wire values.
- Consumers and tests MUST import that authority instead of duplicating literals.
- If direct import is impossible due to feature or target constraints, define exactly one local shim at the boundary.
- Cross-module or externally addressable strings such as route paths, persisted filenames, shared protocol field names, and stable test-facing contracts MUST live in one shared constant or module.
- Surface-local URL or query keys SHOULD live as module-local
constvalues next to that surface's parse and format logic rather than being duplicated inline. - Parseable UI or API state values SHOULD use typed enums with centralized parse and serialize behavior instead of repeated string matches.
- Required operator-facing labels or copy that drive control flow MUST live in named constants or total typed methods, not panic-prone runtime metadata lookups.
- Keep stringly handling at the boundary; keep the rest of the Rust code typed.
- Integration tests SHOULD reuse exported canonical constants for stable app contracts when that keeps the contract single-sourced without widening the public API excessively.
- Example pattern to follow: a reserved-key enum plus a map-extension trait (for example
ReservedMetaKey+ReservedMetaMapExt) that funnels all metadata reads/writes through typed keys, defined once in the crate that owns the namespace.
Durable Guardrail (Application Discipline)
- When touching existing code, if a semantic key namespace is represented by inline string literals in internal Rust logic, you MUST opportunistically replace that touched namespace with typed keys/enums and centralized conversion.
- Do not defer this cleanup solely because tests pass; string-key drift is a correctness risk, not just style.
- Scope this to touched code paths to avoid unrelated broad refactors.