rust-coding-skill
Use when editing or reviewing Rust files (*.rs) in this repository.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when editing or reviewing Rust files (*.rs) in this repository.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
Use when ingesting, refreshing, listing, searching, or deleting a local corpus with Blackbird code search and `gh blackbird search --fileset`.
Use when reaching for `gh blackbird` (Blackbird code search) for cross-repo lexical, symbol, or semantic search on GitHub — finding callers, ownership, or how systems work without cloning.
Use when creating a GitHub pull request, or when updating an existing PR's title or body so it matches what the code actually does.
Use when authoring or substantially editing a design doc, architecture doc, or subsystem explanation — the "here's what's there and why" companion to an ADR's terse decision record.
Use when creating a repo-tracked multi-agent planning PR for a large project, especially when phases, living docs, parallel agent prompts, and cross-PR coordination are needed.
Use when about to call a library, crate, or framework API you haven't verified, when a dependency's behavior is surprising, when training-data memory might be stale, or when investigating how a dependency actually behaves.
SOC 직업 분류 기준
| name | rust-coding-skill |
| description | Use when editing or reviewing Rust files (*.rs) in this repository. |
Opinionated Rust style and discipline rules for this codebase. Apply when writing, reviewing, or refactoring Rust.
Template: copied from
tclem/dotfiles/copilot/templates/rust-coding-skill/. Prune sections that do not apply and fill in the "Project-specific extensions" stubs at the bottom.
From most important to least important:
thiserror for typed error enums at module and library boundaries. Name
variants by what the caller should do (ShardNotFound, AuthRequired,
IndexCorrupted), not by where the error came from (DbError, ParseError,
HttpError).anyhow only at the binary boundary — top-level handlers, CLI entry
points, server commands. Never in library code.Box<dyn std::error::Error + Send + Sync> as a return type. It
erases type information, prevents matching on specific failures, and produces
opaque messages with no causal chain. Use a thiserror enum.unwrap() in production code. Use ? to propagate, let-else for
early returns, if let / match for branches, or expect("specific invariant") for truly infallible cases. unwrap() is for tests.is_none() / is_err() followed by unwrap() — use if let or match. The separate check is redundant and easy to desync.Options together: let (Some(a), Some(b)) = (x, y) else { return Err(...); };.panic! only for unrecoverable invariants where the whole application is
broken. Not for localized failures.#[error(transparent)] for wrapped errors that should pass through
unchanged.Option when absence indicates a non-recoverable
violation rather than an expected missing value.JoinHandle or document the lifecycle owner.tokio::task::spawn_blocking for short operations
(the blocking pool is bounded — don't saturate it with multi-second work).
Long-lived workers use a dedicated thread..await. Use channels (tokio::sync::mpsc /
oneshot) or restructure so the critical section finishes before the await.parking_lot::Mutex — common default; short critical sections that never
touch .await.std::sync::Mutex — use when you want poisoning for invariant-critical
state.tokio::sync::Mutex — last resort, only when the lock genuinely must be
held across .await.std::sync::LazyLock for lazy statics (stable since Rust 1.80). Do not
introduce once_cell::Lazy in new code.&self through. Global mutability makes tests
non-deterministic and couples unrelated code paths. Immutable
LazyLock<Regex> / LazyLock<Config> are fine.stream::iter(...).buffer_unordered(N) over unbounded
join_all on dynamic collections.Box::pin locals;
don't #[allow] clippy::large_futures and don't raise thread stack size
as a workaround.Iterator, Future, Display, Debug, Clone, Default, PartialEq,
Hash, Serialize/Deserialize, thiserror::Error) are fine. Real
extension seams where the consumer must be pluggable are also fine —
document why.From/Into for contextual conversions. Prefer named methods:
to_record, into_parts, from_row, as_string. From/Into can't
carry context, can't fail expressively, and produce un-navigable call
sites where the target type is implicit.iter()
method.F: Fn(...) parameters. One callback is often
unavoidable; two or more is a sign the function should be restructured.Static messages with structured fields, never interpolated dynamic data:
// Good
tracing::info!(shard_id = %id, path = %path.display(), "shard indexed");
// Bad
tracing::info!("shard {} indexed at {}", id, path.display());
Static messages are greppable to one emit site; dynamic fields enable aggregation.
Prefer manual error_span! over #[tracing::instrument]. The
attribute is easy to misuse: wrong level by default, captures all args,
hides span lifetime from readers.
Default to error_span!, not info_span!. Span level controls the
minimum filter at which the span is recorded. Under a warn/error
filter, an info_span is dropped, taking the span's fields and parent
context with it — and child error events lose correlation. error_span!
is always present.
Never hold a span.enter() guard across .await. The guard is
thread-local; when a future resumes on a different thread, the span
attaches to the wrong work. Use .instrument(span) instead. In purely
synchronous closures (e.g. inside spawn_blocking), let _g = span.enter(); is fine.
use tracing::Instrument;
async fn do_work(&self, id: Id) -> Result<()> {
let span = tracing::error_span!("do_work", %id);
async {
// body
}
.instrument(span)
.await
}
Avoid mock testing. Depend on real implementations, spin up lightweight versions, or split side-effectful functions into a pure core (takes values) plus a thin wrapper (fetches them).
// Bad — mock the storage trait
let storage = MockStorage::new().expect_get().returns(item);
let result = foo(&storage);
// Good — pure core over real data
let item = Item { /* ... */ };
let result = foo_core(&item);
assert_eq!(actual, expected) — actual first for readable diffs.
unwrap() is fine in tests — not in production.
Tests must run concurrently. Unique test data, temp directories,
serial_test only when truly necessary.
Mark slow or integration tests with #[ignore].
All use statements at the top of the file. Never import items
inline within function bodies.
Prefer top-level imports over fully-qualified paths in expressions and match arms. Long qualified paths add noise.
One use per line, group items from the same module with braces:
// Good
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
// Bad — split same module across lines
use std::collections::HashMap;
use std::collections::HashSet;
// Bad — inline import
fn foo() {
use std::sync::Arc;
}
pub(crate) by default. Only pub when the API is truly public.#[expect(...)] over #[allow(...)] for lint suppression.
#[expect] warns when the suppression becomes unnecessary, so stale
attributes don't accumulate. Apply on individual fields, not whole
structs.let bindings. If a value is used once and
the expression is clear, inline it. let x = foo.clone(); bar(x) is
just bar(foo.clone()).println!("{value:?}") not println!("{:?}", value)...Self::default() in production struct constructors. Explicitly
list all fields so adding a new field causes a compile error at every
construction site. ..Default::default() is fine in tests for brevity._ wildcards. When an enum gains a
variant, a wildcard arm silently swallows it. Exhaustive matches cause
a compile error at every call site, which is what you want._ only for truly unbounded types (integers, strings) or when
the arm genuinely applies to all future variants.full features
unless the repo standardizes on them. Audit with cargo tree; find unused
deps with cargo machete.cargo check, not cargo build.Comments explain why, never what. Engineers can read code.
The training-data default is to doc-comment every function with a summary
of what it does and a bullet list of its behaviors. Override it. Function
names, types, and one go to definition jump already cover that; a
summary comment drifts on the first refactor and adds nothing.
Decision test before writing any comment: "If I delete this, what is lost that can't be recovered by reading the code, types, names, and one navigation jump?" If the answer is "nothing," don't write it.
// Build the request
above let request = build_request(...)).findReferences.Fill in or delete as appropriate for this repo. Anything in this section is repo-local — keep generic guidance above the line.
If this repo has a runtime crate that wraps task spawning, blocking
pools, or rayon (e.g. for tracing-context and request-id propagation),
document the wrappers here and require them over raw tokio::spawn /
tokio::task::spawn_blocking / rayon::spawn.
List anything enforced by clippy.toml or local lint config:
#[tracing::instrument], Runtime::Builder::thread_stack_size,
disallowed types, etc.
If this repo has typed newtype IDs (sessions, workspaces, projects,
etc.), list them and the rule: new code uses the newtype, never raw
String / &str.
For repos that expose Twirp / gRPC / HTTP APIs, document how to pick codes by caller action (transient vs. invariant violation vs. concurrent modification vs. precondition).
Repo-specific rules for connection management, lock discipline, migration file layout, and DML vs. DDL boundaries.
Path construction, separators, canonicalization, branch-to-path encoding, test path normalization — list whatever this repo cares about.
Map source files to bench suites and document the baseline-comparison recipe.