ワンクリックで
rust-standards
Complete Rust coding, linting, testing, architecture, and layer boundary standards for systemprompt.io development
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Complete Rust coding, linting, testing, architecture, and layer boundary standards for systemprompt.io development
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | Rust Standards |
| description | Complete Rust coding, linting, testing, architecture, and layer boundary standards for systemprompt.io development |
systemprompt.io is a world-class Rust programming brand. Every Rust file must be instantly recognizable as idiomatic Rust as Steve Klabnik would write it.
Run cargo clippy --workspace -- -D warnings and cargo fmt --all after changes.
Prefer iterator chains, combinators, and pattern matching over imperative control flow.
let name = request.name.as_deref().map(str::trim);
let value = opt.unwrap_or_else(|| compute_default());
let result = input.ok_or_else(|| Error::Missing)?;
let valid_items: Vec<_> = items
.iter()
.filter(|item| item.is_active())
.map(|item| item.to_dto())
.collect();
| Anti-Pattern | Idiomatic |
|---|---|
if let Some(x) = opt { x } else { default } | opt.unwrap_or(default) |
match opt { Some(x) => Some(f(x)), None => None } | opt.map(f) |
if condition { Some(x) } else { None } | condition.then(|| x) |
Nested if let / match | Combine with and_then, map, ok_or |
Manual loops building Vec | Iterator chains with collect() |
| Metric | Limit |
|---|---|
| Source file length | 300 lines |
| Cognitive complexity | 15 |
| Function length | 75 lines |
| Parameters | 5 |
| Construct | Resolution |
|---|---|
unsafe | Remove - forbidden in this codebase |
unwrap() | Use ?, ok_or_else(), or expect() with message |
unwrap_or_default() | Fail explicitly - never use fuzzy defaults |
panic!() / todo!() / unimplemented!() | Return Result or implement |
Inline comments (//) | Delete - code documents itself through naming |
Doc comments (///, //!) | Delete - no rustdoc (rare //! module docs excepted) |
| TODO/FIXME/HACK comments | Fix immediately or don't write |
Tests in source files (#[cfg(test)]) | Move to crates/tests/ |
Raw env::var() | Use Config::init() / AppContext |
| Magic numbers/strings | Use constants or enums |
| Commented-out code | Delete - git has history |
All identifier fields use wrappers from systemprompt_identifiers:
use systemprompt_identifiers::{TaskId, UserId};
pub struct Task { pub id: TaskId, pub user_id: UserId }
Available: SessionId, UserId, AgentId, TaskId, ContextId, TraceId, ClientId, AgentName, AiToolCallId, McpExecutionId, SkillId, SourceId, CategoryId, ArtifactId.
All logging via tracing. No println! in library code.
Request-scoped (handlers, services):
let _guard = req_ctx.span().enter();
tracing::info!(user_id = %user.id, "Created user");
System/background (schedulers, startup):
let _guard = SystemSpan::new("scheduler").enter();
tracing::info!("Running cleanup job");
Adding context mid-request:
let span = req_ctx.span();
span.record_task_id(&task_id);
let _guard = span.enter();
Use structured fields: tracing::info!(user_id = %id, "msg") not tracing::info!("msg {}", id).
Services NEVER execute queries directly. All SQL in repositories using SQLX macros:
pub async fn find_by_email(&self, email: &str) -> Result<Option<User>, sqlx::Error> {
sqlx::query_as!(User, "SELECT id, email, name FROM users WHERE email = $1", email)
.fetch_optional(&**self.pool)
.await
}
| Allowed | Forbidden |
|---|---|
sqlx::query!() | sqlx::query() |
sqlx::query_as!() | sqlx::query_as() |
sqlx::query_scalar!() | sqlx::query_scalar() |
The ! suffix enables compile-time verification.
Reference Pattern (repositories):
impl UserRepository {
pub fn new(db: &DbPool) -> Result<Self> {
Ok(Self { pool: db.pool_arc()? })
}
}
Owned Pattern (services/composites):
impl TaskService {
pub const fn new(db_pool: DbPool) -> Self {
Self { db_pool }
}
}
| Pattern | Parameter Name |
|---|---|
| Reference | db: &DbPool |
| Owned | db_pool: DbPool |
Use domain-specific errors with thiserror. anyhow only at application boundaries:
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("User not found: {0}")]
NotFound(String),
#[error("Database error: {0}")]
Database(#[from] DatabaseError),
}
Log errors once at handling boundary, not at every propagation point.
| Layer | Type |
|---|---|
| Rust | DateTime<Utc> |
| PostgreSQL | TIMESTAMPTZ |
Never use NaiveDateTime or TIMESTAMP.
Required for types with 3+ fields OR mixed required/optional fields.
impl AiRequest {
pub fn builder(messages: Vec<AiMessage>, provider: &str, model: &str,
max_tokens: u32, ctx: RequestContext) -> AiRequestBuilder {
AiRequestBuilder::new(messages, provider, model, max_tokens, ctx)
}
}
let request = AiRequest::builder(messages, "gemini", "gemini-2.5-flash", 8192, ctx)
.with_sampling(params)
.with_tools(tools)
.build();
| Rule | Description |
|---|---|
Required fields in new() | All non-optional fields as constructor parameters |
Optional fields via with_*() | Each optional field gets a builder method |
build() consumes builder | Returns final struct |
No Default for complex types | Explicit construction prevents invalid states |
| Prefix | Returns |
|---|---|
get_ | Result<T> - fails if missing |
find_ | Result<Option<T>> - may not exist |
list_ | Result<Vec<T>> |
create_ | Result<T> or Result<Id> |
update_ | Result<T> or Result<()> |
delete_ | Result<()> |
is_ / has_ | bool |
| Type | Name |
|---|---|
| Database pool | db_pool |
| Repository | {noun}_repository |
| Service | {noun}_service |
Allowed: id, uuid, url, jwt, mcp, a2a, api, http, json, sql, ctx, req, res, msg, err, cfg
These patterns silently swallow errors, making debugging impossible:
| Pattern | Resolution |
|---|---|
.ok() on Result | Use ? or map_err() to propagate with context |
let _ = result | Handle error explicitly or use ? |
match { Err(_) => default } | Propagate error or log with tracing::error! |
filter_map(|e| e.ok()) | Log failures before filtering |
Error log then Ok() | Propagate the error after logging |
Acceptable .ok() usage:
if let Err(e) = operation().await {
cleanup().await.ok();
return Err(e);
}
serde_json::from_str(s).map_err(|e| {
tracing::warn!(error = %e, "Parse failed");
e
}).ok()
Detection commands:
rg '\.ok\(\)' --type rust -g '!*test*'
rg 'let _ =' --type rust -g '!*test*'
rg 'unwrap_or_default\(\)' --type rust -g '!*test*'
Events from agent/worker processes must go through HTTP webhook to API process:
Agent Process -> HTTP POST /webhook -> API Process -> CONTEXT_BROADCASTER -> SSE clients
Use BroadcastClient trait:
create_webhook_broadcaster(token) - for agent servicescreate_local_broadcaster() - for API routes (same process)Reference: This document extends the Rust Standards above. All code must meet world-class idiomatic Rust standards.
cargo clippy --workspace -- -D warnings
cargo fmt --all
Zero tolerance for warnings. All clippy lints are enforced at the workspace level.
All crates inherit from [workspace.lints.clippy] in root Cargo.toml. No per-crate overrides except test crates.
| Group | Level | Purpose |
|---|---|---|
all | deny | Core correctness lints |
pedantic | deny | Stricter code quality |
nursery | warn | Experimental quality lints |
cargo | warn | Cargo manifest quality |
perf | warn | Performance suggestions |
suspicious | deny | Potentially incorrect code |
These lints cause compilation to fail:
| Lint | Rationale |
|---|---|
unwrap_used | Use ?, ok_or_else(), or expect() with context |
panic | Return Result, never panic in library code |
unimplemented | Implement or return Result::Err |
todo | Complete implementation before merge |
too_many_arguments | Refactor into builder pattern or config struct |
dbg_macro | Remove debug macros before merge |
exit | Only allowed in CLI entry points with justification |
rc_mutex | Use Arc<Mutex<T>> or redesign |
| Lint | Action Required |
|---|---|
cognitive_complexity | Refactor function, extract helpers |
too_many_lines | Split into smaller functions (limit: 75 lines) |
type_complexity | Create type alias |
expect_used | Prefer ? operator, use expect only with clear message |
inefficient_to_string | Use to_owned() or avoid allocation |
unnecessary_wraps | Remove unnecessary Result/Option wrapper |
unused_async | Remove async or add async operation |
if_not_else | Invert condition for clarity |
redundant_else | Remove unnecessary else after early return |
manual_let_else | Use let ... else { } pattern |
match_bool | Use if/else instead |
option_if_let_else | Use combinators: map_or, map_or_else |
needless_pass_by_value | Take &T instead of T |
items_after_statements | Move items before statements |
semicolon_if_nothing_returned | Add semicolon for clarity |
or_fun_call | Use unwrap_or_else instead of unwrap_or with fn call |
redundant_clone | Remove unnecessary .clone() |
unnecessary_to_owned | Avoid .to_owned() when reference suffices |
implicit_clone | Make cloning explicit |
large_futures | Box large futures |
match_wild_err_arm | Handle specific error variants |
print_stdout | Use tracing for output |
print_stderr | Use tracing for errors |
empty_structs_with_brackets | Remove {} from unit structs |
rest_pat_in_fully_bound_structs | Bind all fields explicitly |
clone_on_ref_ptr | Use Arc::clone(&x) for clarity |
separated_literal_suffix | Use 100_u32 not 100u32 |
try_err | Use ? operator instead of return Err |
These are allowed at workspace level due to practical constraints:
| Lint | Reason |
|---|---|
cargo_common_metadata | Not publishing to crates.io |
multiple_crate_versions | Transitive dependency conflicts |
return_self_not_must_use | Builder pattern returns |
must_use_candidate | Too many false positives |
trivially_copy_pass_by_ref | Consistency over micro-optimization |
cast_possible_truncation | TUI coordinate math requires casts |
cast_sign_loss | TUI coordinate math requires casts |
cast_precision_loss | Progress bar calculations |
cast_possible_wrap | TUI coordinate math |
format_push_string | String building patterns |
uninlined_format_args | Readability preference |
result_unit_err | Legacy error patterns |
missing_docs_in_private_items | No rustdoc requirement |
module_name_repetitions | Domain naming clarity |
missing_errors_doc | No rustdoc requirement |
missing_panics_doc | No rustdoc requirement |
derive_partial_eq_without_eq | Generated code compatibility |
Default: FORBIDDEN
Inline #[allow(clippy::...)] attributes are prohibited except for documented exceptions.
Only these categories may qualify for exceptions:
| Category | Example |
|---|---|
| Serde/Serialization | Empty structs for JSON {} |
| FFI/External Constraints | Generic closures in async contexts |
| Fundamental Type Design | Capability flags requiring many bools |
| CLI Entry Points | Intentional exit() for fatal errors |
Test crates (crates/tests/**) may have relaxed lints:
[lints.clippy]
expect_used = "allow"
unwrap_used = "allow"
panic = "allow"
This does NOT apply to:
src/ directories| Lint | Level |
|---|---|
unsafe_code | forbid |
missing_debug_implementations | warn |
missing_copy_implementations | warn |
trivial_casts | warn |
trivial_numeric_casts | warn |
unused_import_braces | warn |
unused_qualifications | warn |
variant_size_differences | warn |
Before every commit:
cargo clippy --workspace -- -D warnings passescargo fmt --all applied#[allow(...)] without exception documentationcognitive_complexity warnings addressed or justifiedtodo!(), unimplemented!(), or panic!() in library codeAnalyze test coverage in this crate, identify gaps, fix broken tests, and implement missing test coverage.
Before beginning test work, understand:
ALL tests MUST be in separate crates -- NEVER inline, ALWAYS in the separate test workspace.
| Pattern | Status |
|---|---|
#[cfg(test)] mod tests { } in source | FORBIDDEN |
crates/domain/users/tests/ | FORBIDDEN |
crates/tests/unit/domain/users/ | REQUIRED |
crates/tests/
+-- Cargo.toml # Separate workspace manifest
+-- unit/ # Unit tests mirroring source structure
| +-- shared/
| | +-- models/
| | +-- traits/
| | +-- identifiers/
| | +-- client/
| +-- infra/
| | +-- database/
| | +-- logging/
| | +-- config/
| | +-- cloud/
| | +-- loader/
| | +-- events/
| +-- domain/
| | +-- users/
| | +-- oauth/
| | +-- files/
| | +-- analytics/
| | +-- content/
| | +-- ai/
| | +-- mcp/
| | +-- agent/
| +-- app/
| | +-- runtime/
| | +-- generator/
| +-- entry/
| +-- api/
+-- integration/ # Integration tests by feature
+-- extension/
For each layer, list all source crates and their key modules:
ls -la crates/shared/
ls -la crates/infra/
ls -la crates/domain/
ls -la crates/app/
ls -la crates/entry/
Map existing test coverage:
ls -la crates/tests/unit/
ls -la crates/tests/integration/
For each source crate, determine:
cargo test --manifest-path crates/tests/Cargo.toml --workspace 2>&1
cargo install cargo-llvm-cov
cargo llvm-cov --manifest-path crates/tests/Cargo.toml --workspace --html
cargo llvm-cov --manifest-path crates/tests/Cargo.toml --workspace
use systemprompt_{crate}::{Module, function_to_test};
#[test]
fn function_name_with_valid_input_returns_expected() {
let input = create_valid_input();
let result = function_to_test(input);
assert_eq!(result, expected_output);
}
#[test]
fn function_name_with_invalid_input_returns_error() {
let input = create_invalid_input();
let result = function_to_test(input);
assert!(result.is_err());
}
use systemprompt_{crate}::{AsyncService};
#[tokio::test]
async fn async_operation_completes_successfully() {
let service = AsyncService::new();
let result = service.operation().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn integration_test_name() -> Result<()> {
// Phase 1: Setup
let ctx = TestContext::new().await?;
let unique_id = ctx.fingerprint().to_string();
// Phase 2: Action
let response = ctx.make_request("/endpoint").await?;
assert!(response.status().is_success());
// Phase 3: Wait
wait_for_async_processing().await;
// Phase 4: Query
let rows = ctx.db.fetch_all(&query, &[&unique_id]).await?;
// Phase 5: Assert
assert!(!rows.is_empty());
// Phase 6: Cleanup
let mut cleanup = TestCleanup::new(ctx.db.clone());
cleanup.track_fingerprint(unique_id);
cleanup.cleanup_all().await?;
// Phase 7: Log
println!("Test passed");
Ok(())
}
For each missing test crate:
Create Cargo.toml:
[package]
name = "systemprompt-{name}-tests"
version.workspace = true
edition.workspace = true
publish = false
[dependencies]
systemprompt-{name} = { path = "../../../../{layer}/{name}" }
[dev-dependencies]
tokio = { workspace = true, features = ["test-util", "macros"] }
Create src/lib.rs with module declarations
Add to workspace members in crates/tests/Cargo.toml
After completing test work, update {crate_path}/status.md:
# {Crate Name} Status
## Test Status
| Metric | Value |
|--------|-------|
| Test crate | `crates/tests/unit/{layer}/{name}/` |
| Tests passing | X/Y |
| Coverage | Z% |
| Last verified | YYYY-MM-DD |
## Test Commands
cargo test --manifest-path crates/tests/Cargo.toml -p systemprompt-{name}-tests
cargo llvm-cov --manifest-path crates/tests/Cargo.toml -p systemprompt-{name}-tests
Before completing:
cargo build --manifest-path crates/tests/Cargo.toml --workspace)cargo test --manifest-path crates/tests/Cargo.toml --workspace)grep -r "#[cfg(test)]" crates/shared crates/infra crates/domain crates/app crates/entry)systemprompt-{name}-tests)status.md updated with current test statecrates/
shared/ # Pure types, zero internal dependencies
infra/ # Stateless infrastructure utilities
domain/ # Bounded contexts with SQL + repos + services
app/ # Orchestration, no business logic
entry/ # Entry points (binaries, public APIs)
systemprompt/ # Facade: Public API for external consumers (crates.io)
Entry (api, cli) -> App (runtime, scheduler, generator)
|
Domain (agent, ai, mcp, oauth, users, files, content, analytics, templates)
|
Infra (database, events, security, config, logging, loader, cloud)
|
Shared (models, traits, identifiers, extension, provider-contracts, client)
crates/shared/)Pure types with zero dependencies on other systemprompt crates (except within shared/).
| Crate | Purpose |
|---|---|
provider-contracts/ | Provider trait contracts (LlmProvider, ToolProvider, Job, ComponentRenderer, etc.) |
identifiers/ | Typed IDs (UserId, TaskId, etc.) |
models/ | Domain models, API types, configuration structs |
traits/ | Infrastructure trait definitions (DomainConfig, ConfigProvider, DatabaseHandle) |
template-provider/ | Template loading and rendering abstractions |
client/ | HTTP client for external API access |
extension/ | Extension framework for user customization |
crates/infra/)Stateless utilities providing cross-cutting concerns. Can depend on shared/ only.
| Crate | Purpose |
|---|---|
database/ | SQLx abstraction, connection pooling, base repository trait |
events/ | Event bus, broadcasters, SSE infrastructure |
security/ | JWT validation, token extraction, cookie handling |
config/ | Configuration loading, environment handling |
logging/ | Tracing setup, log sinks, database layer |
cloud/ | Cloud API client, tenant management |
crates/domain/)Full bounded contexts. Each crate owns its database tables, repositories, and services. Can depend on shared/ and infra/.
| Crate | Bounded Context | Key Entities |
|---|---|---|
users/ | User identity | User, Role |
oauth/ | Authentication | Token, Client, Grant, Session |
files/ | File storage | File, FileMetadata |
analytics/ | Metrics & tracking | Session, Event, Metric |
content/ | Content management | Content, Category, Tag |
ai/ | LLM integration | Request, Response, Provider |
mcp/ | MCP protocol | Server, Tool, Deployment |
agent/ | A2A protocol | Agent, Task, Context, Skill |
Required domain crate structure:
domain/{name}/
Cargo.toml
schema/ # SQL schema files
src/
lib.rs # Public API
extension.rs # Extension trait implementation
error.rs # Domain-specific errors
models/ # Domain models
repository/ # Data access layer
services/ # Business logic
crates/app/)Orchestration without business logic. Can depend on shared/, infra/, domain/.
| Crate | Purpose |
|---|---|
scheduler/ | Job scheduling, cron execution |
generator/ | Static site generation |
runtime/ | StartupValidator, AppContext, lifecycle management |
crates/entry/)Entry points that wire the application together. Can depend on all layers.
| Crate | Purpose |
|---|---|
cli/ | Command-line interface |
api/ | HTTP gateway, route handlers, middleware |
tui/ | Terminal UI |
systemprompt/)Public API for external consumers. Re-exports only, no new code.
Using a repository from another module is the correct pattern for cross-module data access:
pub mod repositoryDependencies are acceptable when:
Example: agent -> mcp is correct because agent orchestrates MCP tools.
Do NOT add traits just for the sake of abstraction:
All code must use config profiles -- no environment variable fallbacks:
Config::from_profile() is the only way to build configurationDomainConfig traitConfigExtensionTyped trait--force bypassWhen spawning subprocesses (agents, MCP servers), config and secrets MUST be passed explicitly:
Required env vars for ALL subprocesses:
SYSTEMPROMPT_PROFILE -- Path to profile.yamlJWT_SECRET -- JWT signing secret (passed directly, no file discovery)DATABASE_URL -- Database connection stringRules:
JWT_SECRET env var over file loadingif let Ok(...) patterns for secrets -- fail loudly if missingModules are defined in Rust code at crates/infra/loader/src/modules/. Each module uses include_str!() to embed SQL schemas at compile time.
Extensions register jobs, schemas, and routes via inventory macros. These are static initializers that only execute if the crate is linked into the final binary.
Key rule: Core's CLI binary does NOT link extension crates. Products must own the binary.
use my_product as _; // Forces linkage
#[tokio::main]
async fn main() -> anyhow::Result<()> {
systemprompt_cli::run().await
}
| Layer | Cannot Depend On |
|---|---|
| Shared | Any systemprompt crate (except within shared/) |
| Infra | domain/, app/, entry/ |
| Domain | Other domain crates, app/, entry/ |
| App | entry/ |
Domain crates using another domain's public API is acceptable when:
crates/domain/agent/)Depends on:
systemprompt-oauth -- Authenticationsystemprompt-users -- User lookupsystemprompt-logging -- Loggingsystemprompt-database -- Database poolsystemprompt-mcp -- Tool orchestration (legitimate downward dependency)crates/entry/api/)Depends on: All domain and app crates (entry layer wires everything)
crates/app/scheduler/)Depends on: Domain crates via Job trait abstractions
Unified startup validation via DomainConfig trait and StartupValidator:
pub trait DomainConfig: Send + Sync {
fn domain_id(&self) -> &'static str;
fn load(&mut self, config: &Config) -> Result<(), DomainConfigError>;
fn validate(&self) -> Result<ValidationReport, DomainConfigError>;
}
Each domain defines its own error types. Use thiserror for derivation. Convert at boundaries using From implementations.
Services receive dependencies through constructors, not global state:
// GOOD: Explicit dependencies
pub fn new(db: DbPool, config: &AiConfig) -> Self
// BAD: Service locator
pub fn new(app_context: &AppContext) -> Self // Hides true dependencies
# Check for forbidden dependencies
grep "systemprompt-agent" crates/infra/*/Cargo.toml # Should be empty
grep "systemprompt-ai" crates/infra/*/Cargo.toml # Should be empty
# Verify domain isolation
grep "systemprompt-" crates/domain/*/Cargo.toml | grep -v "systemprompt-models\|systemprompt-traits\|systemprompt-identifiers\|systemprompt-database\|systemprompt-events"
Search the web for information using the WebSearch tool
Demonstrates governance blocking when plaintext secrets are detected in tool inputs
A simple demonstration skill that searches the web for information
Demonstration skill that attempts to use a plaintext secret, designed to be blocked by governance hooks
List, message, configure, and restart agents via the systemprompt CLI
View traffic, costs, agent stats, and bot detection via the systemprompt CLI