一键导入
rust-coding-standards
Rust coding standards, documentation authoring, content workflows, and quality patterns for systemprompt.io development
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Rust coding standards, documentation authoring, content workflows, and quality patterns for systemprompt.io development
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Maintains the living marketing strategy doc at /var/www/html/systemprompt-web/reports/marketing/marketing-strategy-master.md. Never rewrites wholesale — writes diffs. Contains objectives, 30/60/90 targets, channel hypotheses in flight, dead hypotheses, winning tactics, and funnel snapshots. Load marketing-identity first.
Deterministically audit and optimise a published documentation page. Runs a 10-section quality audit, applies 5 rewrite rules for structure, terminology, link health, and completeness. Reads website analytics and GSC data per doc URL, produces a 75-point score delta (100 with analytics), commits changes, and updates the per-doc report. Load identity and brand-voice first.
Deterministically audit and optimise a published feature page on the register ladder: struggling-moment heroes with zero implementation jargon, mechanism evidence relocated to section bodies and dropdowns in the HashiCorp / Stripe / Tailscale register. Runs an 11-section quality audit plus register-ladder checks (hero jargon count, homepage narrative echo, struggling-moment opener, identifier-enumeration, established-not-indie voice) and applies 6 rewrite rules: claim verification, conversion clarity, brand discipline, and Technical-Marketing Synthesis (ten sub-checks: outcome headlines, jargon payoff scoped to sections/dropdowns, numbers with context, feature-to-outcome binding, narrative-vs-reference separation, skeptic test, named surfaces over coined metaphors, dropdown alignment, no Rust internals in narrative, dense sentences no filler). Penalises metaphor-stacking and marketing adjectives. Reads website analytics and GSC data per feature URL, produces a 90-point score delta (115 with analytics), commit
Deterministically audit and optimise a published guide. Runs a 14-section quality audit, applies 7 rewrite rules for value density, brand discipline, search-intent alignment, and CTR. Reads 28-day GSC query data per URL, produces a 100-point score delta across 11 dimensions, commits changes, and updates the per-guide report. Handles guides without GSC data. Load identity and brand-voice first.
Daily CRM and funnel measurement. Pulls GitHub Traffic API for systemprompt-core and systemprompt-template (14d retention, MUST run daily), website analytics via systemprompt CLI, and external feedback signals. Emits 1d/7d/31d funnel deltas and a dated report. Source of truth for every hypothesis metric.
Daily SEO briefing. Analyses content traffic, engagement, and search performance across published guides using internal analytics and Google Search Console. Generates actionable reports with S-### hypotheses. Designed for daily /loop. Load identity first.
| name | rust-coding-standards |
| description | Rust coding standards, documentation authoring, content workflows, and quality patterns for systemprompt.io development |
All code and content standards for systemprompt.io. Follow without exception.
systemprompt.io is a world-class Rust programming brand. Every file must be instantly recognizable as on-brand, world-class idiomatic code.
| Type | Location | Language |
|---|---|---|
| Extensions | extensions/*/src/ | Rust |
| MCP Servers | extensions/mcp/*/src/ | Rust |
| Agents | services/agents/*.yaml | YAML |
| Skills | services/skills/**/*.yaml | YAML |
| Config | services/config/*.yaml | YAML |
Primary language for all extensions, MCP servers, and core logic.
systemprompt_identifiersquery!, query_as!, query_scalar!)thiserror for domain-specific errors, anyhow only at application boundaries (entry/cli, entry/api). Library crates MUST NOT expose anyhow::Error in their public API.tracing -- no println! in library codeasync fn in traits (Rust 1.75+) by default. Use #[async_trait] only when the trait must be dyn-compatible; document the reason on the trait.#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Item not found: {0}")]
NotFound(String),
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
}
HTTP status mapping lives at the entry layer only; domain/* must never reference the HTTP
envelope (ApiError) — Domain → HTTP inverts the dependency rule. The orphan rule forbids
impl From<DomainError> for ApiError in entry/api, so map domain → HTTP via an entry-local
error type that owns the From impls (the oauth/error/ OAuthHttpError and error/
ApiHttpError pattern), then propagate with bare ?. Variant → status is decided once, in one
place; IntoResponse logs exactly once by status class. Reuse the shared
From<RepositoryError|ServiceError> for ApiError for methods that return those.
Banned: inline map_err(|e| ApiError::ctor(format!("…{e}"))) at HTTP call sites — it discards
the typed cause and flattens distinct failure modes into one status (a DB outage becomes a 404).
Also banned: tracing::* inside a map_err that then propagates (the boundary logs once).
map_err stays correct for deliberate variant re-classification (must match, not catch-all) and
for adapting a foreign, variant-less error (http::Error, std::io::Error) at the boundary.
The message is a static event label; every runtime value is a structured field. Interpolating a named value into the message is banned — interpolated messages can't be filtered or aggregated.
tracing::info!(user_id = %user.id, "Created user"); // required
tracing::error!(error = %e, "Operation failed"); // required
tracing::info!("Created user {}", user.id); // banned
The sole exception is a freeform prepared string carrying no named value (tracing::debug!("{summary}")).
| Construct | Resolution |
|---|---|
unsafe | Remove -- forbidden |
unwrap() / expect() | Use ? or ok_or_else(). expect() is permitted only in macro-generated From<String> impls for validated typed IDs and for compile-time-constant Regex::new(); both must be unreachable at runtime. |
panic!() / todo!() / unimplemented!() | Return Result or implement |
map_err(|e| ApiError::ctor(format!("…{e}"))) at an HTTP call site | Map domain → HTTP via an entry-local error type's From impls and propagate with bare ?; the variant decides the status. Exempt: deliberate variant re-classification (match, not catch-all) and foreign variant-less adaptation (http::Error, io::Error). |
dbg!() / println! / eprintln! in libraries | Use tracing. CLI-display layers (infra/logging/services/cli, database/services/display) and build.rs cargo:rerun-if-changed= directives are exempt -- nothing else. |
Interpolating a named value into a tracing message (tracing::info!("started {}", name)) | Emit it as a structured field: tracing::info!(name = %name, "started"). Identifiers, counts, paths, and errors are fields, not message text. |
Inline comments (//) | Delete unless they encode a non-obvious why. WHAT-comments are banned; WHY-comments are required when the code's intent is not derivable from naming. Never narrate "what we just changed" or reference past callers/issues. |
Doc comments (///) | Allowed only at crate / module heads (//!) and on the small set of items where rustdoc adds genuine non-obvious value. NEVER add /// mechanically "on every pub item." A /// line that paraphrases the function's name and signature ("/// Fetch all matching rows.", "/// Errors if zero or more than one row matches.") is a code smell — strip on sight. See "Rustdoc placement" below. |
Raw SQL strings (sqlx::query()) | Use compile-time-verified query! / query_as! / query_scalar!. Allowlist (and ONLY this allowlist) for dynamic SQL: crates/infra/database/src/admin/** and crates/infra/database/src/services/postgres/{introspection,query_executor,transaction,ext}.rs. Anything else is a violation. |
serde_json::Value | Define typed structs with #[derive(Deserialize)] -- see exceptions below |
let _ = <fallible> | Banned for Result/#[must_use] exprs. Use ?, log on error, or .ok() with a tracing::warn! first. Permitted ONLY for: unused trait-arg suppression in default method bodies, OnceCell::set() idempotent init, and writes to a CLI display sink that recursing into tracing would itself be the failure mode. Each exemption must be justified inline as a // Why: ... comment. |
.ok() discarding Result | Add a tracing::warn!(error = %e, "...") BEFORE converting to Option. Carve-outs: std::env::var(...).ok(), HTTP-header to_str().ok(), integer try_from().ok() -- "missing-is-normal" cases only. |
*Manager type names | Use *Service (default), *Handler (HTTP/RPC request handlers only), *Orchestrator (cross-domain workflows only). |
Raw String / &str for entity IDs | Use typed IDs from systemprompt_identifiers constructed via TypedId::new(s) or TypedId::try_new(s)?. From<String> / .into() are forbidden at call sites (the impls exist for serde/Into<T> bounds only). Carve-out: A2A protocol types in models/a2a/protocol/* retain String to match external JSON-RPC spec. |
#[cfg(test)] mod tests | Banned. Tests live in crates/tests/unit/<layer>/<crate>/. |
Backwards-compat shims, Option<T> migration stubs, dual code paths | Land the new code AND delete the old form in the same PR. No deprecation periods inside this repo. |
Imperative SQL in <crate>/schema/*.sql | Schema files are pure declarative target state. Allowed: CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS, CREATE [OR REPLACE] FUNCTION/VIEW/TRIGGER, CREATE TYPE, CREATE EXTENSION IF NOT EXISTS, COMMENT ON. Rejected: ALTER, DROP, top-level DO $$, UPDATE/INSERT/DELETE, TRUNCATE, GRANT, REVOKE. State transitions go in <crate>/schema/migrations/NNN_<name>.sql. Boot-time linter at infra/database/services/schema_linter hard-rejects; pre-merge gate: just lint-schema. See instructions/information/migrations.md. |
SQL string literals or hand-written Migration::new(...) in extension.rs | No SQL as a Rust string. Schema DDL lives in <crate>/schema/<table>.sql, embedded via include_str!(). Migration SQL lives in <crate>/schema/migrations/NNN_<name>.sql, discovered by the crate's build.rs (systemprompt_extension::build::emit_migrations()) and returned by extension_migrations!() — version and name come from the filename, never a hand-listed Vec<Migration>. Pre-merge gate: just lint-extensions. |
Any crate published to crates.io has a different bar than internal application code. The rules below are MANDATORY for shared/*, infra/*, domain/*, app/*, and the systemprompt facade.
| Rule | Required state |
|---|---|
| Crate / module heads | //! on lib.rs (purpose, public surface, feature matrix, error model). //! on every significant pub mod file. No /// walls of paraphrase below that. |
| Public errors | thiserror-derived enum in error.rs; never anyhow::Error in a pub fn signature |
| Feature flags | Documented in lib.rs //! matrix; [package.metadata.docs.rs] all-features = true set in Cargo.toml |
| Examples | At least one runnable example per major feature flag in examples/ (compiled in CI) — the facade's docs.rs landing page should show real usage, not a wall of one-liners |
| Re-exports | The facade may carry a brief /// on each pub use only when the user genuinely cannot tell what the re-export provides from the path; otherwise leave it bare |
| Semver | Breaking changes go through a deliberate major bump, never piggyback on a patch |
cargo doc -D warnings | Must pass — broken intra-doc links are bugs. This is not the same as "every item must have a doc"; do NOT enable #![deny(missing_docs)] workspace-wide. |
Every published crate has a CHANGELOG.md. Entries are written for downstream consumers (the people opening the file on crates.io / docs.rs), not for the team that shipped the code. The reference register is the tokio, sqlx, and serde changelogs — terse, neutral, consumer-facing.
Required structure per release:
## [X.Y.Z] - YYYY-MM-DD
### Breaking
- **Breaking:** `<signature change>`. Migrate by `<one-line action>`.
### Added
- `<one-sentence user-visible addition>`.
### Changed
- `<one-sentence user-visible change>`.
### Fixed
- `<one-sentence user-visible fix>`.
### Removed
- `<one-sentence removal>`. Migrate by `<one-line action>`.
### Breaking, ### Added, ### Changed, ### Fixed, ### Removed, ### Deprecated. No custom sections.**Breaking:**, name the affected symbol, and end with Migrate by ….Banned framing (caught in review, rewritten before tag):
crates/infra/database/src/services/executor.rs:execute_statements_parsed) — link the symbol, don't dump the path.Reviewer test. Read the entry aloud as a downstream maintainer who has never met the team. If any sentence needs team context to parse, rewrite it.
/// exists to answer questions a reader CANNOT answer from the type, the signature, and the function name. Anything that just restates them is noise.
Add /// only when one of these is true — and in every case the comment must encode something that is not present in the code at all. If the fact can be reconstructed by reading the body, the signature, and the names, it is not an exception, no matter how accurately the prose restates it:
Result type (e.g. "returns Err(InvalidGrant) if the code was issued for a different client_id" — yes; "errors if the query matches zero or more than one row" for fetch_one returning Result<Row> — no, the function name says it).Strip on sight (a non-exhaustive list of the smell):
/// Returns the X. over pub fn foo(...) -> X/// Constructs / Builds / Creates an X. over pub fn new() -> X or pub fn build() -> X/// Fetches Y. / /// Inserts Y. / /// Deletes Y. over fn fetch_y / fn insert_y / fn delete_y/// Errors if X. when X is implied by the parameter or return type/// (or //) that narrates the body's own control flow — a doc that walks the reader through which match arm maps where, which branch fires when, or what each step does, when the body below already says exactly that. The length and accuracy of the prose make it worse, not better: it is a second, un-compiled copy of the logic that will rot. Delete it; the code is the documentation.Worked example — the canonical rejected form. This is unacceptable:
/// Translates an upstream failure into the status the *client* should see.
///
/// A malformed request (400/404/422) or rate limit (429) is the caller's to
/// fix, so it passes through; auth, 5xx, timeout, and transport failures are
/// the gateway/provider's problem and collapse to 502/504 without leaking the
/// upstream's credential-level detail.
pub fn map_upstream_error(e: &UpstreamError) -> (StatusCode, String) {
let mapped = match *status {
400 | 404 | 422 => StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_REQUEST),
429 => StatusCode::TOO_MANY_REQUESTS,
408 | 504 => StatusCode::GATEWAY_TIMEOUT,
_ => StatusCode::BAD_GATEWAY,
};
// ...
}
The correct form is the same function with no doc at all: the match is the spec, the name says the intent, the prose is noise. Every clause of that six-line doc is already spelled out in the arms beneath it. Self-documenting code means the reader looks down at the body, not up at a paragraph that will silently drift from it.
Crate / module heads are different. Write a real //! block at the top of lib.rs and at the top of significant mod.rs files. That is where docs.rs gets its landing page; that is where the public surface, layering, feature flags, and error model are explained. A reader who lands on a docs.rs page with a one-paragraph crate-level overview and a clean module index will find what they need; a reader landing on a wall of method paraphrases will not.
Sweeps. If a future audit prompt orders "add /// on every pub item", refuse and apply this rule instead. Rustdoc coverage is not a quality metric; signal is.
Comments are governed by a single principle: the code documents itself unless the why is non-obvious. Concretely:
| Crate type | // inline | /// rustdoc | //! module |
|---|---|---|---|
Production code — all crates (shared/*, infra/*, domain/*, app/*, facade, entry/*) | only when encoding a non-obvious why | only where it adds non-obvious value (see "Rustdoc placement"); never as a per-item paraphrase | high-quality head on lib.rs and every significant pub mod file |
Tests (crates/tests/**) | as needed for test scaffolding | banned | optional |
build.rs | as needed | n/a | n/a |
Entry binaries are held to the same comment bar as libraries — entry is not special. The only thing unique to published crates is publishing hygiene (docs.rs config, semver, feature matrix, examples), not comment style.
A WHY-comment must answer one of: a hidden constraint, a subtle invariant, a workaround for a specific bug, behaviour that would surprise a reader. WHAT-comments ("increments the counter"), narrative comments ("we just refactored this"), and reference-to-history comments ("used by X flow", "added for issue #123") are all banned.
The default is no comment. A WHY-comment is the rare exception, not a routine annotation — reach for one only when omitting it would let a competent reader silently break a real invariant. The bar is genuinely exceptional: if the "why" is derivable from the code, the types, or a well-named function, write none. Prose that re-explains intent at length, paints a consumer-facing picture ("so the UI can tell X apart from Y rather than crying wolf"), or recounts protocol/format trivia belongs in a //! module head if it belongs anywhere — not scattered as inline //. When in doubt, delete it. A file dense with why-comments is a signal the bar has slipped, not that the code is unusually subtle.
Accurate is not sufficient. Truth is the floor, not the bar. A /// that is perfectly correct but merely restates the signature, the type, or the body's control flow is still banned — the test is whether it carries non-derivable signal, not whether it is true. An AI writing this code will be fluent and will produce confident, well-phrased narration for every function; that fluency is exactly the trap. Hold the line: most functions in world-class self-documenting code have no /// and no //, and that is the target state, not a gap to fill.
serde_json::Value erases type information and pushes validation to runtime. Treat every occurrence as a code smell requiring heavy justification.
Always prefer typed structs:
#[derive(Debug, Clone, Deserialize)]
pub struct ToolInput {
pub input: String,
pub tags: Option<Vec<String>>,
}
let args: ToolInput = serde_json::from_value(raw_value)?;
Narrow exceptions (require inline justification comment):
| Exception | Justification |
|---|---|
MCP Tool schema fields (input_schema, output_schema) | Protocol requires serde_json::Map -- external spec boundary |
MCP CallToolResult::structured_content | Protocol defines as Option<serde_json::Value> |
serde_json::json!() for constructing known-shape responses | Acceptable at API boundaries when shape is fixed and outgoing-only |
Core trait signatures requiring Value | Cannot change without core modification |
When you encounter serde_json::Value in existing code:
#[derive(Deserialize)]// JSON: protocol boundary comment// JSON: required by trait contract comment| Metric | Limit | Notes |
|---|---|---|
| Source file length | 300 lines | Treated as a cohesion proxy. Splits must extract genuinely cohesive sub-modules; do not shuffle code into <name>_helpers.rs to silence the rule. |
| Function length | 75 lines | |
| Parameters | 5 | |
| Cognitive complexity | not enforced | clippy::cognitive_complexity is allow at workspace scope -- explicit project decision. |
| 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<()> |
id, uuid, url, jwt, mcp, a2a, api, http, json, sql, ctx, req, res, msg, err, cfg
Every extension requires:
Cargo.toml with systemprompt dependenciessrc/extension.rs implementing Extension traitsrc/error.rs implementing ExtensionError traitschema/ numbered 001_*.sqlEvery MCP server requires:
tracing---
title: "Page Title"
description: "SEO description (150-160 characters)"
slug: "section/page-name"
kind: "guide"
public: true
published_at: "2025-01-27"
after_reading_this:
- "First learning objective (action verb)"
- "Second learning objective"
---
| Kind | Use For |
|---|---|
docs-index | Section index pages |
guide | How-to guides, tutorials |
reference | API, CLI, config reference |
page | Static pages (FAQ, glossary) |
Always use absolute paths from docs root:
[Installation](/documentation/installation)
[Services](/documentation/services)
Every CLI command or code reference MUST include a grounding link:
| Reference Type | Grounding Link Required |
|---|---|
| CLI command | Link to --help output |
| Rust crate | Link to crates.io page |
| Code snippet | Link to GitHub source file |
| Configuration | Link to schema or config file |
services/content/blog/systemprompt infra jobs run publish_pipelinesystemprompt core content verify <slug> --source blogstorage/files/css/extensions/web/src/extension.rs required_assets()just build && systemprompt infra jobs run copy_extension_assets<link rel="stylesheet" href="/css/custom.css">storage/files/js/extensions/web/src/extension.rs required_assets()systemprompt infra jobs run copy_extension_assets<script src="/js/custom.js" defer></script>Before committing any code:
cargo fmt --all
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
just build
just file-size # 0 prod files >300 lines
just check-bans # raw sqlx::query, *Manager, raw String IDs
just deny # license + advisory check
just audit # cargo-audit
The quality.yml CI workflow gates the same checks. Any of them red blocks merge.
Repos stay clean. The default state is main plus the release tags — nothing else.
Branches
wip/*, backup/*, wt/*, worktree-agent-*, visibility-*, dd-coverage-*, or checkpoint branches. Land work on main via a short-lived PR branch, then delete the branch (local + remote) on merge.git push --tags (pushes per-crate cargo-workspaces tags). Push individual release tags: git push origin v<X.Y.Z>.Tags
v<X.Y.Z> for the core repo, bridge-v<X.Y.Z> for the bridge binary, <product>-v<X.Y.Z> for sibling product releases. No checkpoint, wave, milestone, sweep, or pre-* tags.<crate>@<version> tags emitted by cargo-workspaces are noise — prune locally before pushing: git tag -l '*@*' | xargs -r git tag -d.Commit messages
tokio / sqlx: neutral, present tense, consumer-facing. type(scope): summary for the subject.Phase N, wave, attempt, wip, incident narrative, or process commentary in the body.git add <path>), not git add -A. Never lump unrelated work.--no-verify or skip signing. Never git stash — commit + push instead.title under 60 charactersdescription is 150-160 charactersafter_reading_this has 3-5 items with action verbs| Task | Command |
|---|---|
| Format | cargo fmt --all |
| Lint | cargo clippy --workspace --all-targets --all-features -- -D warnings |
| Doc | RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features |
| Test | cargo test --manifest-path crates/tests/Cargo.toml --workspace |
| Build | just build |
| File size gate | just file-size |
| Bespoke ban gate | just check-bans |
| Supply-chain gate | just deny && just audit |
| Publish content | systemprompt infra jobs run publish_pipeline |
| Copy assets | systemprompt infra jobs run copy_extension_assets |
| Verify content | systemprompt core content verify <slug> --source blog |