Coding Standards
Status: Current Last updated: 2026-10-20 (commit f7e32e22)
Rust Conventions
- Edition: 2024
- Formatting:
cargo fmtbefore every commit - Linting: clippy is release-time, not per push and not per edit. Stated in full under Rust Conventions below; do not restate it here, because this file already carried the policy twice and the two copies claimed different things.
Error Handling
- No panics for recoverable conditions, use
thiserror/miettefor error types - Use
ErrorSinkfor streamed diagnostics and typedResulterrors for fallible operations; these are different contracts, not competing styles. - Streaming parser APIs return
ParseOutcome<T>(parsed or rejected) with diagnostics in the sink. Whole-document Tree-sitter parsing returnsParseProduct(built or unbuildable) with collected diagnostics. Neither outcome alone certifies validity; see Parser, Model, and API Contracts.
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
- Select the needed Cargo package, target, features and test filter explicitly;
--testsexcludes doctests, while barecargo testcan include them. - Snapshot tests with
instafor complex output comparisons
Generated Files
Never hand-edit generated artifacts:
parser.c: generated fromgrammar.jsgrammar/test/corpus/generated/: generated from specs;manual/is authoredcrates/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.- Use the scoped commands in Developer Verification Checks.
just testselects workspace test binaries with--tests; doctests are separate in that recipe, not inherently excluded by barecargo test. - Clippy is release-time, in
just release-lintand inrelease-lint.ymlon a tag, never per push and never per edit: each pass can require additional compilation. The recipe covers both Cargo workspaces and a feature-off build; its Clippy commands carry-D warnings, so ANY finding is red, not only a violation of the panic family the workspace[lints]table denies; test code relaxes that family via in-source attributes. A finding you intend to keep gets a scoped#[allow]naming the reason. 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: results on stdout through
outln!/out!(thechattercrate’sstdout.rs), neverprintln!/print!, which panic when a consumer such asheadcloses the pipe; the shared writer ends the command with exit status 1 instead.eprintln!for diagnostics;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 or the model’s existing wrapper constructors as
appropriate. Use
Fromonly for infallible conversions; constrained input needsTryFromor a checked constructor. Do not expose mutable/raw constructors or derive deserialization in a way that bypasses admission. DeriveClone, comparison or copying only when compatible with the value’s ownership and evidence role; a consumable capability must not become freely duplicable. Keep constructor, wire and policy tests after introducing a newtype. - 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. Lossy text is not a unique path identity and must not silently become a cache key or hash input. Preserve native path identity, or explicitly admit a validated wire representation appropriate to the cache/protocol.
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
This page last changed: 2026-10-20 (commit f7e32e22). The whole book last changed: 2026-10-27 (commit 5bfee2f9).