AGENT SKILL

rust-code-standards

Rust code standards enforced across all skills. Loaded by other skills, not invoked directly.

适用职业开发工程师
License 许可
MIT
所属仓库
outcomeeng/plugins
最近更新
2026-11-11
Stars
★ 1
Forks
1

01 前言 Front Matter

name
rust-code-standards
description
Rust code standards enforced across all skills. Loaded by other skills, not invoked directly.
allowed-tools
Read
user-invocable
false

03 SKILL.md 正文

<objective> The canonical Rust standards for type usage, ownership, error handling, module boundaries, async design, testing seams, and unsafe code, enforced across implementation, testing, architecture, and review. </objective>

<success_criteria> Rust work follows this standard when compiler, lint, test, and review evidence show the code preserves meaningful type and ownership design, structured errors, explicit module boundaries, justified async and concurrency, testable seams, and documented unsafe invariants, with repo-local overlays applied when present. </success_criteria>

<reference_note> This is a reference skill. Composing Rust skills load these standards explicitly before writing, testing, or auditing. It is not a standalone workflow. </reference_note>

<repo_local_overlay> When another skill loads this reference inside a repository, it must also check for spx/local/rust.md at the repository root. Read that file after this reference if it exists and apply it as repo-local routing to the product's governing specs and decisions. A local overlay supplements skill behavior; it does not declare product truth. </repo_local_overlay>

<standard_family> This standard is the container; read it before any specialized Rust standard.

Work area Specialized standard Purpose
test code /rust-test-standards Rust test filenames, levels, evidence rules, doubles, harnesses, fixtures, and examples
ADRs and architecture docs /rust-architecture-standards Rust architecture decision structure, testability constraints, and architecture review rules

Keep examples in standardizing skills; the standardizing family owns reusable policy and concrete examples. </standard_family>

<tooling_baseline>

Rust code follows the repository's actual toolchain. Unless a repo-local overlay states otherwise:

  • formatting uses rustfmt
  • linting uses clippy
  • compilation and tests run through cargo
  • public APIs and boundaries use explicit types

These standards are enforced by compiler checks, linting, and code review together. Passing one tool is not enough if the code still violates the architectural intent below.

</tooling_baseline>

<type_system>

Use the Rust type system to encode meaning and constraints.

  • prefer newtypes for domain identifiers and validated values
  • use enums instead of stringly-typed state
  • use Option and Result deliberately; do not collapse them into booleans or sentinel values
  • keep public signatures explicit and stable
  • make invalid states unrepresentable when the domain rules are stable
// preferred
pub struct UserId(u64);

pub enum JobStatus {
    Pending,
    Running,
    Failed,
    Complete,
}

// rejected
pub type UserId = u64;
pub type JobStatus = String;

</type_system>

<constant_patterns>

Choose the right Rust construct for grouped constant values:

Pattern When to use
const NAME: Type = value Simple scalar compile-time constant
enum with as_str() or Display Closed set of string values with type safety — prefer over bare string constants
OnceLock<HashMap<K, V>> (std) or Lazy<HashMap<K, V>> (once_cell) Map-like constants with complex initialization
// ✅ preferred: enum for a closed set of string-valued states
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GateStatus {
    Pass,
    Fail,
    Skipped,
}

impl GateStatus {
    pub fn as_str(&self) -> &'static str {
        match self {
            GateStatus::Pass => "pass",
            GateStatus::Fail => "fail",
            GateStatus::Skipped => "skipped",
        }
    }
}

// ✅ preferred: OnceLock for a constant map
use std::collections::HashMap;
use std::sync::OnceLock;

static HEADERS: OnceLock<HashMap<&str, &str>> = OnceLock::new();

fn default_headers() -> &'static HashMap<&'static str, &'static str> {
    HEADERS.get_or_init(|| {
        let mut m = HashMap::new();
        m.insert("Content-Type", "application/json");
        m.insert("Accept", "application/json");
        m
    })
}

// ❌ rejected: scattered bare string constants without semantic structure
pub const STATUS_PASS: &str = "pass";
pub const STATUS_FAIL: &str = "fail";
pub const STATUS_SKIPPED: &str = "skipped";

No re-export of library constants. When production code and tests both need an HTTP status code, both import from the same canonical source (http::StatusCode, framework constants, etc.). Never create product-local aliases.

// ❌ rejected: product-local re-export
pub const HTTP_OK: u16 = 200;

// ✅ preferred: both production and test code import from the canonical source
use http::StatusCode;
let code = StatusCode::OK;

</constant_patterns>

<ownership_and_borrowing>

Ownership is a design tool, not a compiler obstacle.

  • prefer single ownership by default
  • borrow when the callee does not need ownership
  • introduce Arc, Rc, Mutex, RwLock, or interior mutability only when the use case requires them
  • avoid clone-driven designs that hide unclear data flow
  • document long-lived shared state in architecture-level decisions
// preferred
pub fn render(config: &Config) -> Output { /* ... */ }

// suspicious unless justified
pub fn render(config: Config) -> Output {
    let config = config.clone();
    /* ... */
}

</ownership_and_borrowing>

<error_handling>

Error handling must preserve structure and intent.

  • use typed errors at domain and library boundaries
  • use thiserror for structured public errors when it helps consumers
  • use anyhow for application orchestration when the boundary is not public
  • reserve panic!, unwrap(), and expect() for invariants, tests, or process-fatal startup requirements
  • include enough context for operators and callers to act
#[derive(thiserror::Error, Debug)]
pub enum LoadConfigError {
    #[error("missing config file at {0}")]
    Missing(PathBuf),
    #[error("invalid config format")]
    Invalid(#[source] serde_yaml::Error),
}

</error_handling>

<module_boundaries>

Use modules and visibility to enforce architecture.

  • keep fields private unless external mutation is part of the contract
  • expose constructors and behavior, not arbitrary mutation
  • keep adapters thin and domain logic isolated from transport, storage, and CLI glue
  • prefer traits at true architectural seams; avoid trait abstraction when a concrete type is enough
pub struct Account {
    balance: Money,
}

impl Account {
    pub fn credit(&mut self, amount: Money) {
        self.balance += amount;
    }
}

</module_boundaries>

<async_and_concurrency>

Async and concurrency choices need justification.

  • use async for I/O-bound concurrency
  • use threads or data parallelism for CPU-bound work
  • do not hold locks across .await
  • document Send and Sync assumptions in shared state
  • prefer channels or ownership transfer over unnecessary shared mutable state
// preferred
let item = {
    let mut guard = state.queue.lock().await;
    guard.pop_front()
};
process(item).await;

</async_and_concurrency>

<testing_seams>

Code should support evidence-rich tests without framework mocks.

  • inject external process runners, clocks, and boundary adapters through traits or narrow function parameters
  • use small hand-written doubles when a controlled implementation is needed
  • keep pure logic separable from boundary glue
  • do not make mockall or similar generated mocks the default strategy
pub trait Clock {
    fn now(&self) -> DateTime<Utc>;
}

pub struct Service<C: Clock> {
    clock: C,
}

</testing_seams>

<unsafe_and_ffi>

Unsafe code is a last-mile boundary, not a convenience feature.

  • keep unsafe blocks narrow
  • require SAFETY: comments tied to the actual invariant
  • isolate FFI and raw pointer handling behind safe wrappers where possible
  • prefer MaybeUninit, NonNull, and typed wrappers over ad hoc pointer manipulation
// SAFETY: ptr is non-null, aligned, and valid for len initialized bytes.
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };

</unsafe_and_ffi>

<tool_preferences>

Prefer Rust-native tools and idioms unless a repo-local overlay says otherwise:

  • clap for CLI parsing
  • serde for serialization
  • tracing for structured observability
  • tempfile for tempdir-backed tests
  • assert_cmd for CLI L2 binary tests
  • proptest or quickcheck for property testing
  • trybuild for compile-time contracts

</tool_preferences>

<anti_patterns>

Anti-pattern Why it is rejected
stringly-typed domain states loses invariants and discoverability
clone-heavy data flow hides unclear ownership design
unwrap() in ordinary production paths turns expected failures into crashes
public mutable fields by default breaks encapsulation and invariant control
locks held across .await deadlock and contention risk
generated mocks as the default seam weakens evidence and severs reality-based testing
unsafe used to bypass ownership design replaces clear design with soundness risk

</anti_patterns>

04 同仓库更多 Skills