agentleFS
base58edbase58ed/claude_code_files/skills/rust-skill/SKILL.md

rust

Skill1 starsChanged 3 months agoMITView on GitHub

ALWAYS load for .rs files, Rust code, Cargo projects, or async/tokio work. Trigger vocabulary includes borrow checker, lifetime, ownership, trait bounds, async/await, futures, send/sync, pin, type-state, newtypes, anyhow, thiserror, axum, sqlx, reqwest, the tracing crate, tracing-subscriber, EnvFilter, winnow, parser combinators, clap, cargo workspace, criterion, insta, proptest — even when the user doesn't say "Rust" explicitly.

38 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use node, golang and 6 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • mcollinaSkill

    node

    Provides domain-specific best practices for Node.js development with TypeScript, covering type stripping, async patterns, error handling, streams, modules, testing, performance, caching, logging, and more. Use when setting up Node.js projects with native TypeScript support, configuring type stripping (--experimental-strip-types), writing Node 22+ TypeScript without a build step, or when the user mentions 'native TypeScript in Node', 'strip types', 'Node 22 TypeScript', '.ts files without compilation', 'ts-node alternative', or needs guidance on error handling, graceful shutdown, flaky tests, profiling, or environment configuration in Node.js. Helps configure tsconfig.json for type stripping, set up package.json scripts, handle module resolution and import extensions, and apply robust patterns across the full Node.js stack.

    Also in 3 projects that use this

  • rogue-dev-studioSkill

    golang

    Go development environment. Use when the project needs this capability or the user / team manifest asks for it. Use for specialized `golang` work when listed in TEAM.yaml or explicitly requested.

    Also in 3 projects that use this

  • trotsky1997Skill

    ast-grep

    Guide for writing ast-grep rules to perform structural code search and analysis. Use when users need to search codebases using Abstract Syntax Tree (AST) patterns, find specific code structures, or perform complex code queries that go beyond simple text search. This skill should be used when users ask to search for code patterns, find specific language constructs, or locate code with particular structural characteristics.

    Also in 3 projects that use this

  • OpenAISkill

    nextjs

    Next.js App Router expert guidance. Use when building, debugging, or architecting Next.js applications — routing, Server Components, Server Actions, Cache Components, layouts, middleware/proxy, data fetching, rendering strategies, and deployment on Vercel.

    Also in 3 projects that use this

  • SentrySkill

    create-pr

    Create pull requests following Sentry conventions. Use when opening PRs, writing PR descriptions, or preparing changes for review. Follows Sentry's code review guidelines.

    Also in 3 projects that use this

  • GoogleSkill

    tdd

    Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.

    Also in 5 projects that use this

The file

Show its rules and text

We split this skill into the rules it gives an agent

17 rules. 0 are common, 9 are distinctive, and 8 are very specific to this project.

  • 0

    Common

    Most of the 31 edited versions we read keep these.

  • 9

    Distinctive

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use Rust 2024 edition. Verify feature availability when uncertain.

    line 8
  2. Distinctive:

    Delete aggressively — git preserves history

    line 23
  3. Distinctive:

    No global variables or shared mutable state

    line 25
  4. Distinctive:

    When a function exceeds 30-40 lines, look for natural phases to extract: validation, side-effect, verification.

    line 33
  5. Distinctive:

    When Ritchie flags long-function, deep-nesting, or too-many-args, these are the canonical shapes:

    line 49
  6. Distinctive:

    When a file grows past the soft limit, ask: *"what are the 2–3 distinct concerns this file mixes?"* Each concern becomes a sibling module.

    line 68
  7. Distinctive:

    Reader test: read any function's signature aloud.

    line 89
  8. Distinctive:

    When clarity is sacrificed for a measured hot path, comment the why (which benchmark, what gain).

    line 140
  9. Distinctive:

    Use dynamic dispatch only when you need heterogeneous collections or type erasure:

    line 193
  10. Very specific:

    Always .context("...") on the ?

    line 225
  11. Very specific:

    When writing byte-indexed loops, slicing &str, or converting bytes to chars, read references/utf8.md (the push_char_at pattern, byte-loop …

    line 270
  12. Very specific:

    Structured concurrency: prefer JoinSet over fire-and-forget tokio::spawn whose handle is dropped (silent error swallowing).

    line 373
  13. Very specific:

    ALWAYS set timeouts — connect AND request.

    line 421
  14. Very specific:

    Behavioral over interaction testing: Verify state and output, not method calls.

    line 508
  15. Very specific:

    Every Rust project ships clippy.toml + [lints] config aligned to the skill's rules.

    line 523
  16. Very specific:

    Read references/checklist.md before committing or completing a review.

    line 532
  17. Very specific:

    Every service log line has trace_id / correlation_id

    line 539

“Common” is measured: kept by at least 6 in 10 of the 31 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Version Policy

Use Rust 2024 edition. Verify feature availability when uncertain.

Philosophy

  • Modular, clear, readable code with care for performance and efficiency
  • Obvious over clever — if it needs a comment to be readable, simplify it first
  • Ownership-first thinking, then borrowing
  • Immutable by default — let over let mut; iterator chains over mutating loops; local mut only for buffers, builders, and accumulators
  • Make illegal states unrepresentable (newtypes, type-state, sum types)
  • Each module does ONE thing well — clear, minimal interfaces
  • Depend on abstractions, not implementations
  • Compose small pieces into larger systems
  • Flat over nested (early returns, ? operator, let-else guard clauses)
  • Guard clauses over expect/unwrap — return errors, don't panic
  • Solve the actual problem, not a generalized version
  • Delete aggressively — git preserves history
  • debug_assert! for invariants
  • No global variables or shared mutable state

Function Discipline

  • Max ~40 lines per function, 3-4 parameters, 2-3 nesting levels (deliberately looser than the global 2-level default — a match arm inside a loop is idiomatic Rust)
  • Single responsibility: one reason to change
  • Early returns over nested conditionals

When a function exceeds 30-40 lines, look for natural phases to extract: validation, side-effect, verification. Each phase becomes a self-naming helper. Linear flows (e.g., a transactional commit) can stay together if extracting would just be passing a context bag around.

Comments are for the person reading and reviewing the code: the public API, anything not obvious, and the reason behind the code as a plain sentence (no WHY: label). Keep the work plan out of comments — no phases, plan steps, ticket numbers, or words like for now / temporary / later. Those go stale and someone has to find and delete them. Unfinished work belongs in plan.md.

Any function that does real work gets a header comment above it: one line saying what it is for (this matters most for public APIs), plus its big-O for compute AND memory, derived DFS through the call tree: callees count toward the bound, and recursion counts its activation frames as memory. Tiny obvious helpers and trivially O(1)/O(n) glue are exempt. The big-O lives in the header, never on a line inside the body.

/// Merge sorted layers into one ordered run.
/// O(n log k) compute, O(n) memory — k-way heap merge over n total items.
fn merge_layers(layers: &[Layer]) -> Vec<Node> { /* ... */ }

When you change a function, bring its comments to this same bar: keep the comments already there, fix any your change made wrong, and add the missing header / big-O / reason. Do this only for code you add or change, not for nearby code you did not touch.

Three Refactor Shapes (north star)

When Ritchie flags long-function, deep-nesting, or too-many-args, these are the canonical shapes:

PatternTriggerFix
1 — Phase ExtractionFunction exceeds 30-40 linesExtract validation / side-effect / verification phases into self-naming helpers; the entry fn reads as a 5-line narrative
2 — Params Struct + Newtypes4+ args appearing in 3+ signatures (rule of three — don't preemptively bag)Merge cohesive params into reused structs (13 args → 3)
3 — Hoist Invariant Work + AHashMap IndexingLoop-invariant work recomputed per iteration, or find() scans inside loopsHoist invariants out of the loop; pre-build AHashMap indexes — O(N²) → O(N)

The three patterns cascade: too-many-args produces long functions; long functions produce deep nesting; deep nesting hides quadratic work. Fix the signature first and the rest unravels. Full before/after pairs (with real codebase examples) live in references/decomposition.md.

Module Discipline

Per-file analog of Function Discipline. Long files almost always mix concerns — split by responsibility, not by type. Don't group every struct into types.rs and every trait into traits.rs; group code by what it does.

  • Soft limit: ~800 source LOC per file (excluding tests).
  • Hard smell beyond ~1500. That's not "Rust is verbose," it's a module that should be three.
  • Tests inside a #[cfg(test)] mod tests block don't count toward the limit — they're allowed to be long.
  • Integration test modules are exempt for the same reason. Keep them as topic files under tests/it/ (modules of one entry binary), not as tests/<topic>.rs files at the root — each root file is a separate binary that re-links the whole crate. See Testing → references/integration-test-layout.md.

When a file grows past the soft limit, ask: "what are the 2–3 distinct concerns this file mixes?" Each concern becomes a sibling module. Honest signals that a split is overdue:

  • The module-level doc comment names more than one responsibility.
  • Section banners (// ── Foo ────────) divide the file into 200+ LOC blocks — those blocks want to be files.
  • A struct + its closely-related ADTs + the algorithm operating on them is a natural module boundary; extract them as a sibling module, leave the rest behind.

This is review-time guidance, not a build gate — no clippy lint exists for per-file LOC, and a script gate creates more friction than benefit at this scale. Ritchie checklist item #20 catches it before LGTM.

No Magic Constructs

Idiomatic Rust uses powerful features — Deref coercion, blanket impls, generics, ?. The rule isn't avoidance; it's deliberateness. If a construct hides non-obvious behavior, comment the why. If a reader can't follow it without footnotes, simplify.

ConstructIdiomatic — use freelyStop and comment / simplify
Deref / AsRef / IntoSmart pointers (Arc<T>, Box<T>), DTO conversionsHidden expensive conversions, unexpected method resolution
Blanket impls (impl<T: Bound> Trait for T)When the bound captures a real categoryJust to avoid two explicit impl blocks
Generic signatures1-3 type params, single where line, reads aloud>3 type params, HRTBs, multi-line where clauses without comments
macro_rules!After 3+ identical-shape duplications AND traits/generics can't unify the shapeFirst copy of a pattern; anything a function would handle; validated newtype families (use phantom-type generic — see Newtype Wrappers)
Proc macrosCrate authors, framework attrs (#[derive], #[tokio::main])Application logic
unsafeWhen truly required (FFI, no safe alternative, measured perf)ALWAYS with // SAFETY: <invariant> comment explaining the why
Magic numbers / stringsNeverUse named const with units in the name (TIMEOUT_SECS)

Reader test: read any function's signature aloud. If it requires footnotes, leave a comment or simplify.

Performance

  • Correct first, then clear, then fast
  • Measure before optimizing — intuition is often wrong
  • Optimize hot paths only — 90% of code doesn't matter
  • Simple code is often fast code (cache-friendly, predictable)

Allocation Discipline

Allocations are not free. In hot paths they dominate.

// PREFER: borrow over own
fn parse(input: &str) -> Result<Token>             // not String

// PREFER: Cow when output is sometimes-owned
fn normalize(s: &str) -> Cow<'_, str>              // borrow when no change, own only on edit

// PREFER: iterators that collect once
let total: u64 = items.iter().map(|i| i.price).sum();

// AVOID: cloning in a loop
for s in &strings {
    process(s.clone());                            // why clone?
}

// PREFER: pre-size collections you know the capacity of
let mut buf = String::with_capacity(items.len() * 16);

// PREFER: stack collections for bounded sizes (`smallvec`, `arrayvec`)
let mut codes: SmallVec<[u8; 8]> = SmallVec::new();

// PREFER: reuse buffers across iterations
let mut scratch = Vec::with_capacity(1024);
for input in inputs {
    scratch.clear();
    encode_into(input, &mut scratch)?;
    write(&scratch)?;
}

Hot Path Discipline

  • Profile first (cargo flamegraph, samply, tokio-console for async). Don't guess.
  • #[inline] on small, frequently-called functions across crate boundaries — but only after measurement.
  • Lookup tables over computation when the input space is small (see the hex-encoding lookup table in references/tracing.md).
  • Lock-free where possible: AtomicUsize counter, thread-local RNG, append-only logs.
  • Minimize work inside locks — read, copy out, drop, then process.

When clarity is sacrificed for a measured hot path, comment the why (which benchmark, what gain).

Hash Maps

Use ahash::AHashMap and ahash::AHashSet for all in-memory hash maps. ~2-3× faster than std::collections::HashMap (AES-NI on x86_64), and randomly keyed per instance, which raises the bar against HashDoS but is not a cryptographic guarantee like SipHash — do not rely on it for maps keyed by hostile input whose iteration order is observable (std's SipHash default is right for those).

use ahash::{AHashMap, AHashSet};

let mut index: AHashMap<&str, &Row> = AHashMap::new();
let mut seen: AHashSet<UserId> = AHashSet::new();

One carve-out: for stable hashes (content-addressed storage, persistent dedup keys serialized to disk), AHashMap is wrong — its output is not stable across versions. Use SHA-256 or xxhash for those. For in-memory HashMap-style use, AHashMap is the default.

Mechanically enforced by assets/clippy.toml which disallows std::collections::HashMap and HashSet with a migration message.

Ownership & API Design

Borrow by default; own only when storing; Cow for sometimes-owned. Return owned values when creating; accept impl Trait (e.g. impl Iterator<Item = i32>) for flexibility.

SituationUse
Read-only access&str, &[T], &T
Need to store/ownString, Vec<T>, T
Sometimes owned, sometimes borrowedCow<'_, str>, Cow<'_, [T]>
Shared ownership across threadsArc<T>
Shared ownership single-threadedRc<T>
Heap-allocated trait objectBox<dyn Trait>

Trait Design

Small, focused traits — compose via supertraits.

trait ReadRepo: Send + Sync {
    async fn find(&self, id: UserId) -> Result<Option<User>>;
}

trait WriteRepo: Send + Sync {
    async fn save(&self, user: &User) -> Result<()>;
}

trait UserRepo: ReadRepo + WriteRepo {}
impl<T: ReadRepo + WriteRepo> UserRepo for T {}

Static dispatch is the default (zero-cost, monomorphized):

fn process(repo: &impl ReadRepo) -> Result<()>

Use dynamic dispatch only when you need heterogeneous collections or type erasure:

fn handlers() -> Vec<Box<dyn Handler>>

Associated types vs generic parameters:

trait Iterator { type Item; }                    // ONE natural choice per impl
trait From<T> { fn from(value: T) -> Self; }     // a type can impl multiple times

Type-State Pattern

Encode state transitions at compile time — invalid transitions don't compile. Put state-specific data inside the state struct, not as Options on a single struct.

pub struct Order<S> { id: OrderId, items: Vec<Item>, state: S }
pub struct Draft;
pub struct Confirmed { confirmed_at: DateTime<Utc> }

impl Order<Draft> {
    pub fn confirm(self) -> Order<Confirmed> { /* move data into the next state */ }
}

Anti-pattern: payment_id: Option<PaymentId> on a single struct + .expect("typestate invariant") at access. Defeats the point — runtime unwraps are exactly what type-state is meant to eliminate. Full worked example in references/newtypes.md.

Error Handling & Guard Clauses

  • anyhow for binaries; thiserror for libraries.
  • Always .context("...") on the ? operator at fallible boundaries.
  • unwrap() ONLY in tests — never in production code. unwrap_or / unwrap_or_default for safe defaults.
  • bail! / ensure! validate at boundaries; let-else, ?, and early return Err(...) keep the happy path unindented and give callers a Result they can handle.
use anyhow::{bail, Context, Result};

fn load_user(path: &Path, id: UserId) -> Result<User> {
    let content = fs::read_to_string(path).context("failed to read user db")?;
    let db: Db = serde_json::from_str(&content).context("failed to parse user db")?;
    let Some(user) = db.users.get(&id) else {
        bail!("user {id} not found");
    };
    Ok(user.clone())
}

When expect is acceptable:

  • Tests (#[cfg(test)], tests/, benches/) — clippy auto-allows via allow-expect-in-tests = true in clippy.toml (paired with the workspace lint expect_used = "warn")
  • Provably infallible: immediately after an is_some()/is_ok() check you just did
  • Process startup (before serving requests) where panic = correct behavior

Library Errors (thiserror)

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]                          // adding a variant stays non-breaking
pub enum LayoutError {
    #[error("cycle detected involving node {0}")]
    Cycle(NodeId),
    #[error("invalid diagram source")]
    Parse(#[from] ParseError),
}
  • #[non_exhaustive] on public error enums — callers can't exhaustively match, so new variants don't break them.
  • Boundary rule: library crates export typed errors; the binary wraps them in anyhow with .context().
  • Parsers: error types carry spans/byte offsets for diagnostics (see references/winnow.md).

UTF-8 / Codepoint Discipline

Never index bytes out of a &str and cast to char in a byte loop (bytes[i] as char) — every high byte (0x80–0xFF) is reinterpreted as a Latin-1 codepoint and re-encoded, silently corrupting Arabic, CJK, and emoji. Push the full codepoint and advance by ch.len_utf8() instead. Truncation: &s[..N] panics on a multi-byte boundary — truncate by chars (s.chars().take(N)), never by bytes.

When writing byte-indexed loops, slicing &str, or converting bytes to chars, read references/utf8.md (the push_char_at pattern, byte-loop safety conditions, and the non-ASCII round-trip property test).

Newtype Wrappers

Private fields with new() validation + accessor methods. Public fields defeat encapsulation.

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PositiveAmount(u64);

impl PositiveAmount {
    pub fn new(value: u64) -> Result<Self> {
        if value == 0 { bail!("amount must be positive"); }
        Ok(Self(value))
    }
    pub fn value(&self) -> u64 { self.0 }
}

#[must_use]   // Result is must_use by default; annotate non-Result returns where ignoring is a bug
pub fn checked_total(items: &[Item]) -> Option<Money>

For families of validated string ids (IssueKey, Workspace, RepoSlug, ...), use the phantom-type Id-over-Spec pattern, not macro_rules! — all shared impls live in one place as plain types and traits, and each alias stays a distinct type. Read references/newtypes.md (includes the serde try_from validation wiring).

Serde Patterns

Default to snake_case wire format — matches Rust struct fields naturally, no rename_all needed, no implicit translation. Use deny_unknown_fields on input types to reject unexpected keys at the boundary. camelCase only when a non-Rust client (typically JS/TS frontend) requires it. For tagged enum events, attribute defaults, and the camelCase exception, read references/serde.md.

Where to Assert — Boundaries vs Invariants

Pick the assertion tool by who is at fault when it fires:

WhereToolReasoning
Constructor (Type::new)bail!/ensure! (returns Err)Caller passed bad data — they need to handle it
Public API entry pointbail!/ensure!Validate untrusted input at the boundary
Inside a private fn, after upstream validationdebug_assert!Either we trust validation, or we have a bug
Pre-/post-condition on a private opdebug_assert!Documents invariant; release skips the cost
Security-critical (auth, bounds, capability)assert! (always-on)Violation in release is worse than crashing
Provably infallible (e.g., is_some() then unwrap)expect("reason proven above")Local proof; explicit message documents it
Return value that must not be ignored#[must_use] on the fnResult is must_use by default; annotate #[must_use] only on non-Result returns where ignoring is a bug

Rule of thumb: at the boundary, the caller is responsible — return Result. Inside, we are responsible — debug_assert!. The only release-time panic is for invariants whose violation is worse than a crash (security, memory safety). Cheap O(1) invariant checks may use assert! (kept in release, effectively free); expensive checks (O(n) scans, conservation sums over collections) use debug_assert!.

pub fn transfer(from: &mut Account, to: &mut Account, amount: u64) -> Result<()> {
    debug_assert!(amount > 0, "amount must be positive");
    debug_assert!(from.id != to.id, "cannot transfer to same account");

    let total_before = from.balance + to.balance;
    from.withdraw(amount)?;
    to.deposit(amount);

    debug_assert_eq!(from.balance + to.balance, total_before, "balance must be conserved");
    Ok(())
}

Concurrency (tokio)

Shared State — Climb to the Simplest Rung

For shared mutable state, climb from simplest to most complex: ownership transfer (channels) → immutable share (Arc) → atomic primitive (AtomicUsize/AtomicBool) → lock (tokio::sync::Mutex/RwLock) → lock-free data structure (dashmap, crossbeam, arc-swap). Stop at the simplest rung that works. AtomicUsize is one type; Mutex<usize> is two types + lock-poisoning + an unwrap — lock-free is simpler, not more complex, when the state is a single cell.

For atomics, memory orderings, thread-local for hot paths, Send/Sync mental model, deadlock checklist, per-rung examples (channels, atomics, locks), and the lock-free crates table, read references/concurrency.md.

Right Primitive for the Job

use tokio::sync::{Mutex, RwLock, Semaphore, mpsc, oneshot};
use tokio::time::timeout;

Mutex<T>              // Exclusive async access
RwLock<T>             // Multiple readers OR one writer
Semaphore             // Limit concurrent operations
mpsc::channel(n)      // Bounded multi-producer, single-consumer
oneshot::channel()    // Single value, single use
timeout(dur, future)  // ALWAYS wrap external calls

// Parallel with join
let (users, orders) = tokio::join!(fetch_users(), fetch_orders());

Channels are always bounded (mpsc::channel(n), never unbounded_channel) — backpressure over unbounded memory growth. CPU-bound work goes to rayon (par_iter) or tokio::task::spawn_blocking, never the async executor. For the JoinSet + Semaphore bounded-concurrency pattern, read references/async-rust.md.

Inside an async runtime, prefer tokio::sync::* over std::sync::* — the std primitives can block the executor if a critical section is long. Short, await-free critical sections on std::sync::Mutex are acceptable when documented.

Async Rust

Most async-Rust complexity is library complexity, not application complexity. Pin, manual Future impls, Box::pin — these exist for runtime/middleware authors. Application code is async fn + .await + tokio::sync::* and that covers ~95% of cases.

Avoid Pin<Box<dyn Future>> in application code — it's the right pattern for tower::Service::Future and similar framework interfaces, suspect anywhere else. The avoidance has a corollary: safely conceal, don't replace. Don't trade Pin for dyn everywhere (lost optimization), or wrap every async fn in indirection, or reflexively reach for Arc<Mutex<...>>. Use language and runtime features that already hide pinning without changing semantics — async fn, impl Future, tokio::spawn, axum::Router. They internally use Pin and expose clean types.

Application pitfalls that bite (with examples in references/async-rust.md):

  1. .await while holding a lock → drop the guard before .await
  2. tokio::spawn requires 'static → clone Arc, move ownership; never borrow
  3. Cancel safety — select!/timeout drop the loser at any .await → put the commit last
  4. block_on inside async deadlocks the runtime → use spawn_blocking for sync work
  5. Arc<Mutex<...>> cargo-cult → check the sharing ladder above first

Async traits: native async fn in trait has been stable since Rust 1.75 — default to it. Reach for #[async_trait] only when you need dyn-compatibility (heterogeneous trait objects), and accept the per-call heap allocation it adds.

Structured concurrency: prefer JoinSet over fire-and-forget tokio::spawn whose handle is dropped (silent error swallowing). JoinSet bounds child lifetimes to the parent's scope; aborting the set aborts in-flight tasks.

For full worked examples (lock-across-await, cancel-safety wrong-then-right, async traits, JoinSet + Semaphore for concurrent iteration, Stream vs Iterator), read references/async-rust.md.

Graceful Shutdown

Pattern: active-request counter + oneshot signal + drain timeout via tokio::select!. For canonical middleware + wiring, read references/graceful-shutdown.md. Never process::exit.

Resilience

Retries with exponential backoff for transient failures; timeouts on every external call; circuit breakers when one dependency can take down others. For canonical retry_with_backoff and circuit-breaker patterns, read references/resilience.md.

Parsers (winnow)

Writing or modifying parsers (winnow combinators, grammar work, text formats): read references/winnow.md FIRST — winnow 1.0's API (&mut input advanced in place, ModalResult, cut_err commitment) diverges hard from nom and from winnow ≤ 0.6, and stale habits don't compile. It also covers zero-copy &'src str ASTs, spans for diagnostics, and insta/proptest/conformance testing for parsers.

Project Structure

  • Always cargo add — never hardcode versions in Cargo.toml.
  • Before adding a dependency, check cargo tree -i <crate> — duplicates cause feature flag conflicts and lock file churn.
  • edition = "2024" for every new package.
  • Pre-commit hooks (cargo-husky): any non-trivial project gets a cargo-husky dev-dep + .cargo-husky/hooks/pre-commit running cargo fmt --check and cargo clippy --workspace --all-targets -- -D warnings. Setup snippet, hook script, and edge cases in references/project-setup.md.

Workspaces (multi-crate)

[workspace]
members = ["crates/*"]
resolver = "3"            # edition-2024 default (Rust 1.84+); virtual workspaces must set it explicitly

[workspace.package]       # version/edition declared once; members inherit via `edition.workspace = true`
edition = "2024"

[workspace.dependencies]  # shared deps declared once; members use `foo = { workspace = true }`
  • Lints live in [workspace.lints.rust] / [workspace.lints.clippy] at the root, with [lints] workspace = true in every member crate.
  • Layering rule: lower crates never depend on higher (parse → layout → render → cli).
  • Each crate exposes a minimal pub surface.

The directory tree and starter dependency lists live in references/project-setup.md.

Build Profiles

Two opposite goals, two profiles. Never opt-level = 1 everywhere — slower to compile than 0, slower at runtime than 3. Dev: your crates at 0, deps at 2, build-override at 0. Release: opt-level = 3 + thin LTO + codegen-units = 1 + panic = "abort". sccache can't cache incremental builds — on a single dev machine, incremental-without-sccache wins; reach for sccache only in CI. Creating a Cargo.toml, tuning compile times, adding SIMD, or writing benchmarks: read references/build-profiles.md.

HTTP Client (reqwest)

ALWAYS set timeouts — connect AND request. A connect timeout alone won't catch a slow body. For canonical client setup with fetch_json helper, retry integration, and error mapping, read references/http-client.md.

Web Framework (Axum)

Routes thin, business logic in services. AppState carries pool + clients. ALWAYS layer TimeoutLayer and TraceLayer. For the canonical handler/state/router pattern with custom error type and IntoResponse, read references/axum.md.

Database (sqlx)

Compile-time-checked queries (query!/query_as!); explicit transactions for multi-statement work; pool sizing based on concurrency, not "more is better". For canonical pool setup, transaction patterns, and migration discipline, read references/sqlx.md.

Logging (tracing)

In network services and long-running daemons, every log line carries a trace_id or correlation_id.

For web services: read X-Correlation-Id from the incoming request, validate it, fall back to a generated 16-char hex id if missing or invalid. Propagate via tracing::Instrument across every async boundary in the request. Write the same id back to the response in X-Correlation-Id so clients can join logs.

Non-service posture: libraries emit events and never install a subscriber; binaries own subscriber installation. CLIs log human-readable diagnostics to stderr — stdout is reserved for pipeable program output.

Naming convention: identifiers and acronyms use Id (not ID) capitalization throughout — header names (X-Correlation-Id, X-Request-Id), type names (UserId, OrderId, PaymentId), field names (trace_id, correlation_id in snake_case for fields).

For canonical setup (correlation-id middleware, fast lock-free trace_id generator, formatter, instrumentation patterns), read references/tracing.md.

Style

  • Fields first, message last: tracing::info!(field = val, "message")
  • % for Display, ? for Debug, bare for primitives / &'static str
  • "-" sentinel for optional fields
  • snake_case fields, lowercase messages
  • skip(self, body, params) in #[instrument]
  • Secrets never logged — even at TRACE level
tracing::info!(version = env!("CARGO_PKG_VERSION"), address = %addr, "starting server");
tracing::info!(tool, duration_ms, "tool completed");
tracing::warn!(from = %prev, to = "OPEN", "circuit breaker state change");

Defense in depth (non-negotiable)

  • Pretty dev formatter, not plain fmt::layer() — dim context, bright content; skeleton and rationale in references/tracing.md.
  • Secrets never reach a log line, masked or not — secrecy::SecretString, explicit masking at the call site, no global redaction layer; rationale in references/tracing.md.
  • Per-HTTP-attempt logging > silent retry middleware — each attempt emits service, method, path, status, duration_ms, attempt; rationale in references/tracing.md.
  • Boot logs emit twice — intent in main.rs before bind, actual (local_addr()) after bind; rationale in references/tracing.md.

Observability (logs + metrics + traces, zero-overhead)

Three pillars, one emission API, one join key. tracing is the single instrumentation surface; the correlation id joins all three pillars so an operator can pivot from a metric spike to the log line to the exact trace.

  • Emit once, consume via layers. Backends (fmt, Loki, OTLP/Langfuse) are subscriber layers attached at boot — never backend calls at call sites, and never custom proc macros: #[instrument] IS the instrumentation macro, and a span no layer wants costs one cached Interest check (~1ns, never constructed).
  • Metrics: the metrics facade (histogram!/counter!) at seams — request/turn boundaries, external-call round-trips, error arms. Exporter (metrics-exporter-prometheus) runs its own listener port; the app's auth surface is untouched. Increments are atomics; scrape is pull-based.
  • Label discipline (non-negotiable): bounded label values only (channel, model, provider, stage, status). Per-user/per-request ids are a cardinality bomb — identity belongs in traces and logs, never in metric labels.
  • LLM/agent work traces to Langfuse via an OTLP layer with gen_ai.* attributes (model, token usage, optionally payloads behind a config flag, redacted). Adopt the service correlation id as the OTel trace id by zero-padding 64→128 bit (W3C-sanctioned) — one shared pad function, so logs and traces carry the same key.
  • Zero-overhead posture: exporters batch on background tasks; the hot path never awaits an exporter and never touches a DB for observability; a misconfigured backend warns and is skipped — it must never block boot or a request.
  • A feature is done when its seams carry spans + metrics. New endpoints, external calls, and error paths land instrumented, not retrofitted.

Dependency Injection

Trait-based injection for testability; struct fields + constructor for wiring; no global service locator. Alias runtime singletons Arc<dyn Trait> as SharedTrait — the alias says "process-wide singleton: clone, don't construct":

pub type SharedTokenVerifier = Arc<dyn TokenVerifier>;
let verifier: SharedTokenVerifier = Arc::new(HsJwtVerifier::new(secret, iss, aud));  // only in main

Constructors live only in main or a build_* helper, landing on AppState as Shared* fields — never via lazy_static / once_cell / LazyLock. For legitimate process-wide constants (a compiled Regex, an interned table), use std::sync::LazyLock — never the once_cell or lazy_static crates. For the full pattern (generic constructor injection, when NOT to alias, concrete Arc<T> singletons, test stubs), read references/di.md.

Testing

Workflow: global TDD doctrine — red, green, vacuous mutation check; regression test lands before the fix.

Behavioral over interaction testing. Verify state and output, not method calls. unwrap() is fine in tests.

TypeWhenExample
RealAlways first choiceReal OrderValidator
FakeIn-memory implInMemoryUserRepo
StubFixed responsesStubPaymentGateway
MockLast resortExternal HTTP API (use WireMock)

For testcontainers (DB tests), property-based testing (proptest), insta snapshot testing, cargo-fuzz targets, and concrete fixture patterns, read references/testing.md.

Integration test layout — consolidate under tests/it/. Every file at the tests/ root compiles to its own binary that statically links the whole crate plus its deps. With heavy deps (an ML runtime, onnxruntime, candle, a vendored C lib), N root files means N fat binaries and N link steps — the usual cause of a slow cargo test and a target/ dir that balloons to tens of GB. Cargo has no flag to merge them; the lean layout is to keep few files at the root and put the suite under tests/it/ as modules of one entry binary (tests/it.rs with #[path = "it/<f>.rs"] mod <f>; per file — #[path] is required because a tests/*.rs file is a crate root and resolves mod to siblings). Put white-box tests in src/ under #[cfg(test)]; reserve tests/ for black-box public-API tests. When setting up a new test suite, or whenever a crate's tests/ root grows past a handful of files, read references/integration-test-layout.md for the mechanics (shared common, feature grouping, include_*! depth) and the behavior-preserving migration recipe.

Lint Configuration

Every Rust project ships clippy.toml + [lints] config aligned to the skill's rules. The skill packages both as assets:

  • assets/clippy.toml — thresholds (function length ≤40, cognitive complexity ≤25, max bool params 1, type complexity 250), disallowed types (std::HashMap → ahash::AHashMap)
  • assets/cargo-lints.toml — lint levels (unwrap_used = deny, await_holding_lock = deny, too_many_arguments = warn, cognitive_complexity = warn, ...); unsafe_code = "forbid" is the asset default — relax to deny + // SAFETY: per-crate only when FFI or measured perf requires it

Copy assets/clippy.toml to the project root; MERGE the lints sections from assets/cargo-lints.toml into Cargo.toml — in a workspace, as [workspace.lints.rust] / [workspace.lints.clippy] tables with [lints] workspace = true in every member. These turn ~10 skill rules into build failures rather than review findings. Test code carve-outs come from clippy.toml's allow-unwrap-in-tests / allow-expect-in-tests / allow-panic-in-tests flags (they cover #[cfg(test)], tests/, and benches/) — no per-file cfg_attr attributes needed.

Before Committing (Self-Review)

Read references/checklist.md before committing or completing a review. Quick top-of-mind:

  • No unwrap() outside tests, ? with .context() everywhere
  • Borrowed where possible (&str not String in params)
  • Timeouts on ALL external calls
  • Bounded channels, Semaphore for concurrency limits
  • Tests verify state/behavior, not interactions
  • Every service log line has trace_id / correlation_id
  • clippy passes with no warnings
  • Serde annotations consistent (rename_all, deny_unknown_fields on input types)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.