Coding Standards
Status: Current Last updated: 2026-07-25 22:40 EDT
Rust Conventions
- Edition: 2024
- Formatting:
cargo fmtbefore every commit - Linting: CI owns clippy (single pass, no flags; the workspace
[lints.clippy]table denies only the panic family). Do not run clippy as a local habit; see the clippy policy in the root CLAUDE.md.
Error Handling
- No panics for recoverable conditions, use
thiserror/miettefor error types - Library code uses the
ErrorSinktrait for error reporting, notResult - Use
ParseOutcome<T>in parser code (parsed or rejected)
Logging
- Library crates use
tracing(neverprintln!oreprintln!) - CLI binaries write to stdout (results) and stderr (diagnostics)
- Use appropriate log levels:
error!,warn!,info!,debug!,trace!
Naming
- Follow standard Rust conventions (snake_case for functions, CamelCase for types)
- Conventional Commits for commit messages:
<type>[scope]: <description>- Types:
feat,fix,refactor,test,docs,chore
- Types:
Dependencies
Preferred crates:
clap: CLI argument parsingserde: serializationmiette: user-facing diagnosticsinsta: snapshot testingtracing: structured loggingrayon/crossbeam, concurrencysmallvec: small-buffer optimization
Code Organization
- Keep crate boundaries clean, lower crates should not depend on higher ones
- The model crate should not depend on any parser
- Parsing code should not depend on serialization/transform code
- All CHAT parsing and serialization goes through the AST, never ad-hoc string manipulation
- Treat 10 or more named struct fields as an audit trigger. Wide boundary or
report records can be acceptable, but wide runtime state bags need explicit
review. See
architecture/chat-model/wide-structs.md.
Testing
- Prefer spec-driven tests over hand-written tests for parser behavior
- Use
cargo testfor unit tests (except doctests) - Snapshot tests with
instafor complex output comparisons
Generated Files
Never hand-edit generated artifacts:
parser.c: generated fromgrammar.jsgrammar/test/corpus/: generated from specscrates/talkbank-parser-tests/tests/integration/generated/: generated from specscrates/talkbank-model/src/generated/symbol_sets.rs: generated from symbol registry
Always regenerate from source inputs.
Full Rust Standards Charter (canonical)
Edition and Tooling
- Rust 2024 edition.
cargo fmtbefore committing. Usecargo fmt(not standalonerustfmt) for workspace-consistent formatting.- Prefer
cargo testfor faster parallel-per-test execution. Usecargo test --docfor doctests (they are not part of the normal run those). - CI runs single-pass clippy (
--workspace --all-targets, no flags): the workspace[lints]table denies the panic family in production code; test code relaxes it via in-source attributes. Red means a panic-policy violation, nothing else. See the clippy policy section above.
Error Handling
- No panics for recoverable conditions. Use typed errors
(
thiserror); usemiettefor rich diagnostics where appropriate. - No silent swallowing. Every unexpected condition must be
handled with explicit error reporting, no
.ok(),.unwrap_or_default(), or silent fallbacks that hide bugs.
Output and Logging
- Library crates:
tracingmacros (tracing::info!,tracing::warn!, etc.), neverprintln!/eprintln!. - CLI binaries:
println!/eprintln!for user-facing output;tracingfor debug logging. - Test code:
println!is acceptable (cargo captures it).
Lazy Initialization
LazyLock<Regex>(fromstd::sync) for constant regex patterns. Never callRegex::new()inside functions or loops.OnceLockfor per-instance memoization of runtime-determined values.- Prefer
constwhen possible (even better than lazy). - All lazy init via
std::sync, no external crate dependencies needed.
Type Design
- No boolean blindness. Enums over bools for anything beyond
simple on/off. This is a hard rule.
- Banned: 2+ bool parameters on a function, 2+ related bool
fields on a struct, opposite bool pairs (
foo/no_foo), bool return where meaning is unclear without reading docs. #[derive(Default, clap::ValueEnum)]enum with named variants. For clap CLI args, use#[arg(value_enum)]instead of--flag/--no-flagpairs.- OK as bool:
verbose,force,quiet,dry_run, singleinclude_*/skip_*flags, anything where the parameter name fully communicates whattruemeans.
- Banned: 2+ bool parameters on a function, 2+ related bool
fields on a struct, opposite bool pairs (
BTreeMapfor deterministic JSON in tests and snapshot tests (notHashMap). Ensures consistent, reviewable diffs.- Prefer explicit enums over ambiguous
Optionwhen there are multiple meaningful states.
Newtypes Over Primitives
- No primitive obsession. Domain values must have domain types. Function signatures should be self-documenting through type names, not parameter names.
- Use newtype structs (e.g.,
struct TimestampMs(u64),struct SpeakerId(String)) or theinterned_newtype!/string_newtype!macros fromtalkbank-model. Newtypes should implementDisplay,From/Intofor the underlying type, and deriveClone,Debug,PartialEq,Eqas appropriate. - Scope: Applies to public API boundaries, struct fields, and function signatures. Local variables inside a function body may use bare primitives when the context is unambiguous.
- Parsing boundaries: Parse raw strings into newtypes at the boundary (file I/O, CLI args, IPC). Interior code should never handle raw strings for typed values.
- No ad-hoc format parsing. Use real parsers (JSON:
serde_json, etc.) not regex or string splitting for structured formats. Regex is appropriate only for flat text pattern matching (search, normalization, validation of simple formats).
Integer Discipline
- Distinguish meaning. Not all
usizevalues are interchangeable. Separate:- Index: position into a collection (
UtteranceIndex,GraIndex) - Count: accumulated quantity (
WordCount,UtteranceCount) - Limit: upper bound for iteration or reporting
(
UtteranceLimit,WordLimit) - Threshold: minimum value for inclusion
(
FrequencyThreshold) - ID: opaque identifier (
NodeId,SpeakerIndex)
- Index: position into a collection (
- Non-negative quantities use unsigned types; newtypes enforce domain semantics.
- No bare numeric literals except
0,1, and simple loop bounds. All other numbers must be named constants. Assess whether each constant should be configurable.
Closed-Set Strings and Constants
- Closed sets must be enums. If a string value comes from a
known finite set (tier labels, command names, output formats),
represent it as an
enumwith aFromStrparser andDisplayserializer. UseOther(String)escape hatch only when the set is genuinely extensible. - All remaining string literals must be defined constants. No
scattered
"mor"or"cod"strings, useTierKind::Mororconst DEFAULT_TIER: &str = "cod". - Config defaults: Use
constvalues or enum variants inDefaultimpls, not"string".to_owned()(avoids runtime allocation, makes the default visible at the type level).
File Path Discipline
- File paths use
PathBuf/&Path, neverString. Convert to strings only at display/serialization boundaries via.display()or.to_string_lossy(). - Distinguish base filename (e.g.,
MediaFilenamenewtype, no extension) from full filesystem path (PathBuf). - Use
.display()for user-facing output;.to_string_lossy()only for cache keys or hashing.
Configurability
- Hardcoded thresholds and limits belong in config struct fields with documented defaults.
- If a default is useful to change per-invocation → CLI flag.
- If a default is useful to change per-user → future
defaults.tomlfile (not yet implemented). - Config structs must be constructible in tests without filesystem or network access.
Rustdoc as Primary Documentation
- Types are the primary documentation layer. A reader of crates.io rustdocs should understand the domain by reading type definitions alone.
- Every
pubtype and function must have a doc comment explaining role, ownership, invariants, and CHAT manual references where applicable. - Newtypes must document valid values, units, and meaningful operations.
- Enum variants must document when each variant applies.
File Size Limits
- Recommended: ≤400 lines per file.
- Hard limit: ≤800 lines per file (must be split).
Testability
- No global mutable state. All command state flows through
explicit
Statetypes (theAnalysisCommandtrait pattern). Enforce this going forward. - Config structs must be constructible in tests without filesystem, network, or environment setup.
- Stateful resources (caches, pools, registries) must accept injected dependencies for test control.
Refactoring Triggers
Stop and refactor when you see:
x: i32, y: i32for domain data → use domain structsstart_ms: u64, end_ms: u64→ useTimestampMsnewtype orTimeSpanstructfn foo(lang: &str, speaker: &str, path: &str)→ useLanguageCode,SpeakerId, typed path- Multiple booleans for state → use enum with variants
fn foo(a: bool, b: bool)or--flag/--no-flagpairs → use enum withclap::ValueEnumfn parse() -> Option<T>where failure reason matters → useResult<T, ParseError>match s { "win" => ... }on raw strings → parse toenumat boundary"mor"or"cod"string literals → useTierKind::MororTierKind::Codlimit: usizeormax_X: usize→ use domain-specific newtype (UtteranceLimit,WordLimit)- Bare
0.5or60in logic → named constant or config field - Regex or
split()/find()on XML, JSON, or other structured formats → use a proper parser