Add: windows mvp - transparent bugs not fixed
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
/dist
|
||||
/issues/screenshots
|
||||
112
AGENTS.md
Normal file
112
AGENTS.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Rust Skills - Agent Instructions
|
||||
|
||||
> For OpenAI Codex and compatible agents
|
||||
|
||||
## Default Project Settings
|
||||
|
||||
When creating Rust projects or Cargo.toml files, ALWAYS use:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
all = "warn"
|
||||
pedantic = "warn"
|
||||
```
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
### 1. Question Routing
|
||||
Route Rust questions to appropriate skills:
|
||||
- Ownership/borrowing → m01-ownership
|
||||
- Smart pointers → m02-resource
|
||||
- Error handling → m06-error-handling
|
||||
- Concurrency → m07-concurrency
|
||||
- Unsafe code → unsafe-checker
|
||||
|
||||
### 2. Code Style
|
||||
Follow Rust coding guidelines:
|
||||
- Use snake_case for variables and functions
|
||||
- Use PascalCase for types and traits
|
||||
- Use SCREAMING_SNAKE_CASE for constants
|
||||
- Max line length: 100 characters
|
||||
- Use `?` operator instead of `unwrap()` in library code
|
||||
|
||||
### 3. Error Handling
|
||||
```rust
|
||||
// Good: Use Result with context
|
||||
fn read_config() -> Result<Config, ConfigError> {
|
||||
let content = std::fs::read_to_string("config.toml")
|
||||
.map_err(|e| ConfigError::Io(e))?;
|
||||
toml::from_str(&content)
|
||||
.map_err(|e| ConfigError::Parse(e))
|
||||
}
|
||||
|
||||
// Avoid: unwrap() in library code
|
||||
fn read_config() -> Config {
|
||||
let content = std::fs::read_to_string("config.toml").unwrap(); // Bad
|
||||
toml::from_str(&content).unwrap() // Bad
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Unsafe Code
|
||||
Every `unsafe` block MUST have a `// SAFETY:` comment:
|
||||
```rust
|
||||
// SAFETY: We checked that index < len above, so this is in bounds
|
||||
unsafe { slice.get_unchecked(index) }
|
||||
```
|
||||
|
||||
### 5. Common Error Fixes
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| E0382 | Use of moved value | Clone, borrow, or use reference |
|
||||
| E0597 | Lifetime too short | Extend lifetime or restructure |
|
||||
| E0502 | Borrow conflict | Split borrows or use RefCell |
|
||||
| E0499 | Multiple mut borrows | Restructure to single mut borrow |
|
||||
| E0277 | Missing trait impl | Add trait bound or implement trait |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Ownership
|
||||
- Each value has one owner
|
||||
- Borrowing: `&T` (shared) or `&mut T` (exclusive)
|
||||
- Lifetimes: `'a` annotations for references
|
||||
|
||||
### Smart Pointers
|
||||
- `Box<T>`: Heap allocation
|
||||
- `Rc<T>`: Reference counting (single-threaded)
|
||||
- `Arc<T>`: Atomic reference counting (thread-safe)
|
||||
- `RefCell<T>`: Interior mutability
|
||||
|
||||
### Concurrency
|
||||
- `Send`: Safe to transfer between threads
|
||||
- `Sync`: Safe to share references between threads
|
||||
- `Mutex<T>`: Mutual exclusion
|
||||
- `RwLock<T>`: Reader-writer lock
|
||||
|
||||
### Async
|
||||
```rust
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let handle = tokio::spawn(async {
|
||||
// async work
|
||||
});
|
||||
handle.await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
## Skill Files
|
||||
|
||||
For detailed guidance, see:
|
||||
- `skills/rust-router/SKILL.md` - Question routing
|
||||
- `skills/coding-guidelines/SKILL.md` - Code style rules
|
||||
- `skills/unsafe-checker/SKILL.md` - Unsafe code review
|
||||
- `skills/m01-ownership/SKILL.md` - Ownership concepts
|
||||
- `skills/m06-error-handling/SKILL.md` - Error patterns
|
||||
- `skills/m07-concurrency/SKILL.md` - Concurrency patterns
|
||||
5190
Cargo.lock
generated
Normal file
5190
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
Normal file
35
Cargo.toml
Normal file
@@ -0,0 +1,35 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/sprimo-app",
|
||||
"crates/sprimo-api",
|
||||
"crates/sprimo-config",
|
||||
"crates/sprimo-platform",
|
||||
"crates/sprimo-protocol",
|
||||
"crates/sprimo-sprite",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
version = "0.1.0"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
all = "warn"
|
||||
pedantic = "warn"
|
||||
|
||||
[workspace.dependencies]
|
||||
axum = "0.8.4"
|
||||
bevy = { version = "0.14.2", default-features = true }
|
||||
directories = "5.0.1"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "signal", "net", "sync"] }
|
||||
toml = "0.8.23"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "fmt"] }
|
||||
uuid = { version = "1.18.1", features = ["serde", "v4", "v7"] }
|
||||
BIN
assets/sprite-packs/default/atlas.png
Normal file
BIN
assets/sprite-packs/default/atlas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 404 B |
35
assets/sprite-packs/default/manifest.json
Normal file
35
assets/sprite-packs/default/manifest.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "default",
|
||||
"version": "1",
|
||||
"image": "sprite.png",
|
||||
"frame_width": 512,
|
||||
"frame_height": 512,
|
||||
"animations": [
|
||||
{
|
||||
"name": "idle",
|
||||
"fps": 6,
|
||||
"frames": [0, 1]
|
||||
},
|
||||
{
|
||||
"name": "active",
|
||||
"fps": 10,
|
||||
"frames": [1, 0]
|
||||
},
|
||||
{
|
||||
"name": "success",
|
||||
"fps": 10,
|
||||
"frames": [0, 1, 0],
|
||||
"one_shot": true
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"fps": 8,
|
||||
"frames": [1, 0, 1],
|
||||
"one_shot": true
|
||||
}
|
||||
],
|
||||
"anchor": {
|
||||
"x": 0.5,
|
||||
"y": 1.0
|
||||
}
|
||||
}
|
||||
BIN
assets/sprite-packs/default/sprite.png
Normal file
BIN
assets/sprite-packs/default/sprite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 MiB |
20
crates/sprimo-api/Cargo.toml
Normal file
20
crates/sprimo-api/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "sprimo-api"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
sprimo-protocol = { path = "../sprimo-protocol" }
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tower = "0.5.2"
|
||||
405
crates/sprimo-api/src/lib.rs
Normal file
405
crates/sprimo-api/src/lib.rs
Normal file
@@ -0,0 +1,405 @@
|
||||
use axum::body::Bytes;
|
||||
use axum::extract::{Json, State};
|
||||
use axum::http::{header, HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
use sprimo_protocol::v1::{
|
||||
CommandEnvelope, ErrorResponse, FrontendStateSnapshot, HealthResponse,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
use thiserror::Error;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::mpsc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApiConfig {
|
||||
pub bind_addr: SocketAddr,
|
||||
pub auth_token: String,
|
||||
pub app_version: String,
|
||||
pub app_build: String,
|
||||
pub dedupe_capacity: usize,
|
||||
pub dedupe_ttl: Duration,
|
||||
}
|
||||
|
||||
impl ApiConfig {
|
||||
#[must_use]
|
||||
pub fn default_with_token(auth_token: String) -> Self {
|
||||
Self {
|
||||
bind_addr: SocketAddr::from(([127, 0, 0, 1], 32_145)),
|
||||
auth_token,
|
||||
app_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
app_build: "dev".to_string(),
|
||||
dedupe_capacity: 5_000,
|
||||
dedupe_ttl: Duration::from_secs(600),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApiState {
|
||||
start_at: Instant,
|
||||
auth_token: String,
|
||||
app_version: String,
|
||||
app_build: String,
|
||||
dedupe_capacity: usize,
|
||||
dedupe_ttl: Duration,
|
||||
recent_ids: Mutex<HashMap<Uuid, Instant>>,
|
||||
snapshot: Arc<RwLock<FrontendStateSnapshot>>,
|
||||
command_tx: mpsc::Sender<CommandEnvelope>,
|
||||
}
|
||||
|
||||
impl ApiState {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
config: ApiConfig,
|
||||
snapshot: Arc<RwLock<FrontendStateSnapshot>>,
|
||||
command_tx: mpsc::Sender<CommandEnvelope>,
|
||||
) -> Self {
|
||||
Self {
|
||||
start_at: Instant::now(),
|
||||
auth_token: config.auth_token,
|
||||
app_version: config.app_version,
|
||||
app_build: config.app_build,
|
||||
dedupe_capacity: config.dedupe_capacity,
|
||||
dedupe_ttl: config.dedupe_ttl,
|
||||
recent_ids: Mutex::new(HashMap::new()),
|
||||
snapshot,
|
||||
command_tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ApiServerError {
|
||||
#[error("bind failed: {0}")]
|
||||
Bind(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub fn app_router(state: Arc<ApiState>) -> Router {
|
||||
Router::new()
|
||||
.route("/v1/health", get(handle_health))
|
||||
.route("/v1/state", get(handle_state))
|
||||
.route("/v1/command", post(handle_command))
|
||||
.route("/v1/commands", post(handle_commands))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
pub async fn run_server(config: ApiConfig, state: Arc<ApiState>) -> Result<(), ApiServerError> {
|
||||
let listener = TcpListener::bind(config.bind_addr).await?;
|
||||
axum::serve(listener, app_router(state))
|
||||
.await
|
||||
.map_err(ApiServerError::Bind)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_health(State(state): State<Arc<ApiState>>) -> Json<HealthResponse> {
|
||||
let snapshot = state
|
||||
.snapshot
|
||||
.read()
|
||||
.expect("frontend snapshot lock poisoned");
|
||||
Json(HealthResponse {
|
||||
version: state.app_version.clone(),
|
||||
build: state.app_build.clone(),
|
||||
uptime_seconds: state.start_at.elapsed().as_secs(),
|
||||
active_sprite_pack: snapshot.active_sprite_pack.clone(),
|
||||
capabilities: snapshot.capabilities.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_state(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<FrontendStateSnapshot>, ApiError> {
|
||||
require_auth(&headers, &state)?;
|
||||
let snapshot = state
|
||||
.snapshot
|
||||
.read()
|
||||
.map_err(|_| ApiError::Internal("snapshot lock poisoned".to_string()))?
|
||||
.clone();
|
||||
Ok(Json(snapshot))
|
||||
}
|
||||
|
||||
async fn handle_command(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
require_auth(&headers, &state)?;
|
||||
let command: CommandEnvelope =
|
||||
serde_json::from_slice(&body).map_err(|e| ApiError::BadJson(e.to_string()))?;
|
||||
enqueue_if_new(&state, command).await?;
|
||||
Ok(StatusCode::ACCEPTED)
|
||||
}
|
||||
|
||||
async fn handle_commands(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
require_auth(&headers, &state)?;
|
||||
let commands: Vec<CommandEnvelope> =
|
||||
serde_json::from_slice(&body).map_err(|e| ApiError::BadJson(e.to_string()))?;
|
||||
for command in commands {
|
||||
enqueue_if_new(&state, command).await?;
|
||||
}
|
||||
Ok(StatusCode::ACCEPTED)
|
||||
}
|
||||
|
||||
async fn enqueue_if_new(state: &ApiState, command: CommandEnvelope) -> Result<(), ApiError> {
|
||||
let now = Instant::now();
|
||||
{
|
||||
let mut ids = state
|
||||
.recent_ids
|
||||
.lock()
|
||||
.map_err(|_| ApiError::Internal("dedupe lock poisoned".to_string()))?;
|
||||
|
||||
ids.retain(|_, seen_at| now.duration_since(*seen_at) < state.dedupe_ttl);
|
||||
if ids.contains_key(&command.id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if ids.len() >= state.dedupe_capacity {
|
||||
let oldest = ids
|
||||
.iter()
|
||||
.min_by_key(|(_, seen_at)| *seen_at)
|
||||
.map(|(id, _)| *id);
|
||||
if let Some(oldest) = oldest {
|
||||
ids.remove(&oldest);
|
||||
}
|
||||
}
|
||||
|
||||
ids.insert(command.id, now);
|
||||
}
|
||||
|
||||
state
|
||||
.command_tx
|
||||
.send(command)
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("command receiver dropped".to_string()))
|
||||
}
|
||||
|
||||
fn require_auth(headers: &HeaderMap, state: &ApiState) -> Result<(), ApiError> {
|
||||
let raw = headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.ok_or(ApiError::Unauthorized)?;
|
||||
let expected = format!("Bearer {}", state.auth_token);
|
||||
if raw == expected {
|
||||
return Ok(());
|
||||
}
|
||||
Err(ApiError::Unauthorized)
|
||||
}
|
||||
|
||||
enum ApiError {
|
||||
Unauthorized,
|
||||
BadJson(String),
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
Self::Unauthorized => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ErrorResponse {
|
||||
code: "unauthorized".to_string(),
|
||||
message: "missing or invalid bearer token".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Self::BadJson(message) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
code: "bad_json".to_string(),
|
||||
message,
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Self::Internal(message) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
code: "internal".to_string(),
|
||||
message,
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{app_router, ApiConfig, ApiState};
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use sprimo_protocol::v1::{
|
||||
CapabilityFlags, CommandEnvelope, FrontendCommand, FrontendStateSnapshot,
|
||||
};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::sync::mpsc;
|
||||
use tower::ServiceExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn build_state() -> (Arc<ApiState>, mpsc::Receiver<CommandEnvelope>) {
|
||||
let snapshot =
|
||||
FrontendStateSnapshot::idle(CapabilityFlags::default());
|
||||
let snapshot = Arc::new(RwLock::new(snapshot));
|
||||
let (tx, rx) = mpsc::channel(8);
|
||||
(
|
||||
Arc::new(ApiState::new(
|
||||
ApiConfig::default_with_token("token".to_string()),
|
||||
snapshot,
|
||||
tx,
|
||||
)),
|
||||
rx,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn health_does_not_require_auth() {
|
||||
let (state, _) = build_state();
|
||||
let app = app_router(state);
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/v1/health")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_requires_auth() {
|
||||
let (state, _) = build_state();
|
||||
let app = app_router(state);
|
||||
let command = CommandEnvelope {
|
||||
id: Uuid::new_v4(),
|
||||
ts_ms: 1,
|
||||
command: FrontendCommand::Toast {
|
||||
text: "hi".to_string(),
|
||||
ttl_ms: None,
|
||||
},
|
||||
};
|
||||
let body = serde_json::to_vec(&command).expect("json");
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/command")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_accepts_with_auth() {
|
||||
let (state, mut rx) = build_state();
|
||||
let app = app_router(state);
|
||||
let command = CommandEnvelope {
|
||||
id: Uuid::new_v4(),
|
||||
ts_ms: 1,
|
||||
command: FrontendCommand::Toast {
|
||||
text: "hi".to_string(),
|
||||
ttl_ms: None,
|
||||
},
|
||||
};
|
||||
let body = serde_json::to_vec(&command).expect("json");
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/command")
|
||||
.header("content-type", "application/json")
|
||||
.header("authorization", "Bearer token")
|
||||
.body(Body::from(body))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::ACCEPTED);
|
||||
let received = rx.recv().await.expect("forwarded command");
|
||||
assert_eq!(received.id, command.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn malformed_json_returns_bad_request() {
|
||||
let (state, _) = build_state();
|
||||
let app = app_router(state);
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/command")
|
||||
.header("content-type", "application/json")
|
||||
.header("authorization", "Bearer token")
|
||||
.body(Body::from("{ not-json }"))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn duplicate_command_is_deduped() {
|
||||
let (state, mut rx) = build_state();
|
||||
let app = app_router(state);
|
||||
let command = CommandEnvelope {
|
||||
id: Uuid::new_v4(),
|
||||
ts_ms: 1,
|
||||
command: FrontendCommand::Toast {
|
||||
text: "hi".to_string(),
|
||||
ttl_ms: None,
|
||||
},
|
||||
};
|
||||
let body = serde_json::to_vec(&command).expect("json");
|
||||
|
||||
let first = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/command")
|
||||
.header("content-type", "application/json")
|
||||
.header("authorization", "Bearer token")
|
||||
.body(Body::from(body.clone()))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(first.status(), StatusCode::ACCEPTED);
|
||||
|
||||
let second = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/command")
|
||||
.header("content-type", "application/json")
|
||||
.header("authorization", "Bearer token")
|
||||
.body(Body::from(body))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(second.status(), StatusCode::ACCEPTED);
|
||||
|
||||
let one = rx.recv().await.expect("first command");
|
||||
assert_eq!(one.id, command.id);
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
}
|
||||
29
crates/sprimo-app/Cargo.toml
Normal file
29
crates/sprimo-app/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "sprimo-app"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
bevy.workspace = true
|
||||
image = { version = "0.25.9", default-features = false, features = ["png"] }
|
||||
raw-window-handle = "0.6.2"
|
||||
sprimo-api = { path = "../sprimo-api" }
|
||||
sprimo-config = { path = "../sprimo-config" }
|
||||
sprimo-platform = { path = "../sprimo-platform" }
|
||||
sprimo-protocol = { path = "../sprimo-protocol" }
|
||||
sprimo-sprite = { path = "../sprimo-sprite" }
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.58.0", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
1295
crates/sprimo-app/src/main.rs
Normal file
1295
crates/sprimo-app/src/main.rs
Normal file
File diff suppressed because it is too large
Load Diff
18
crates/sprimo-config/Cargo.toml
Normal file
18
crates/sprimo-config/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "sprimo-config"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
directories.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.18.0"
|
||||
206
crates/sprimo-config/src/lib.rs
Normal file
206
crates/sprimo-config/src/lib.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use directories::ProjectDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("no supported configuration directory for this platform")]
|
||||
MissingProjectDir,
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("invalid config file: {0}")]
|
||||
Parse(#[from] toml::de::Error),
|
||||
#[error("could not encode config: {0}")]
|
||||
Encode(#[from] toml::ser::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct AppConfig {
|
||||
pub window: WindowConfig,
|
||||
pub animation: AnimationConfig,
|
||||
pub sprite: SpriteConfig,
|
||||
pub api: ApiConfig,
|
||||
pub logging: LoggingConfig,
|
||||
pub controls: ControlsConfig,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
window: WindowConfig::default(),
|
||||
animation: AnimationConfig::default(),
|
||||
sprite: SpriteConfig::default(),
|
||||
api: ApiConfig::default(),
|
||||
logging: LoggingConfig::default(),
|
||||
controls: ControlsConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct WindowConfig {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub monitor_id: Option<String>,
|
||||
pub scale: f32,
|
||||
pub always_on_top: bool,
|
||||
pub click_through: bool,
|
||||
pub visible: bool,
|
||||
}
|
||||
|
||||
impl Default for WindowConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
x: 200.0,
|
||||
y: 200.0,
|
||||
monitor_id: None,
|
||||
scale: 1.0,
|
||||
always_on_top: true,
|
||||
click_through: false,
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct AnimationConfig {
|
||||
pub fps: u16,
|
||||
pub idle_timeout_ms: u64,
|
||||
}
|
||||
|
||||
impl Default for AnimationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fps: 30,
|
||||
idle_timeout_ms: 3_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct SpriteConfig {
|
||||
pub selected_pack: String,
|
||||
pub sprite_packs_dir: String,
|
||||
}
|
||||
|
||||
impl Default for SpriteConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
selected_pack: "default".to_string(),
|
||||
sprite_packs_dir: "sprite-packs".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct ApiConfig {
|
||||
pub port: u16,
|
||||
pub auth_token: String,
|
||||
}
|
||||
|
||||
impl Default for ApiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: 32_145,
|
||||
auth_token: Uuid::new_v4().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct LoggingConfig {
|
||||
pub level: String,
|
||||
}
|
||||
|
||||
impl Default for LoggingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: "info".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct ControlsConfig {
|
||||
pub hotkey_enabled: bool,
|
||||
pub recovery_hotkey: String,
|
||||
}
|
||||
|
||||
impl Default for ControlsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hotkey_enabled: true,
|
||||
recovery_hotkey: "Ctrl+Alt+P".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn config_path(app_name: &str) -> Result<PathBuf, ConfigError> {
|
||||
let dirs =
|
||||
ProjectDirs::from("", "", app_name).ok_or(ConfigError::MissingProjectDir)?;
|
||||
Ok(dirs.config_dir().join("config.toml"))
|
||||
}
|
||||
|
||||
pub fn load_or_create(app_name: &str) -> Result<(PathBuf, AppConfig), ConfigError> {
|
||||
let path = config_path(app_name)?;
|
||||
load_or_create_at(&path)
|
||||
}
|
||||
|
||||
pub fn load_or_create_at(path: &Path) -> Result<(PathBuf, AppConfig), ConfigError> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
if !path.exists() {
|
||||
let cfg = AppConfig::default();
|
||||
save(path, &cfg)?;
|
||||
return Ok((path.to_path_buf(), cfg));
|
||||
}
|
||||
|
||||
let raw = fs::read_to_string(path)?;
|
||||
let cfg = toml::from_str::<AppConfig>(&raw)?;
|
||||
Ok((path.to_path_buf(), cfg))
|
||||
}
|
||||
|
||||
pub fn save(path: &Path, config: &AppConfig) -> Result<(), ConfigError> {
|
||||
let encoded = toml::to_string_pretty(config)?;
|
||||
fs::write(path, encoded)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{load_or_create_at, save, AppConfig};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn bootstrap_writes_default_config() {
|
||||
let temp = TempDir::new().expect("tempdir");
|
||||
let path = temp.path().join("config.toml");
|
||||
let (_, config) = load_or_create_at(&path).expect("load or create");
|
||||
assert_eq!(config.api.port, 32_145);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_roundtrip() {
|
||||
let temp = TempDir::new().expect("tempdir");
|
||||
let path = temp.path().join("config.toml");
|
||||
let mut config = AppConfig::default();
|
||||
config.window.x = 42.0;
|
||||
|
||||
save(&path, &config).expect("save");
|
||||
let (_, loaded) = load_or_create_at(&path).expect("reload");
|
||||
assert!((loaded.window.x - 42.0).abs() < f32::EPSILON);
|
||||
}
|
||||
}
|
||||
18
crates/sprimo-platform/Cargo.toml
Normal file
18
crates/sprimo-platform/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "sprimo-platform"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
sprimo-protocol = { path = "../sprimo-protocol" }
|
||||
thiserror.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.58.0", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
252
crates/sprimo-platform/src/lib.rs
Normal file
252
crates/sprimo-platform/src/lib.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use sprimo_protocol::v1::CapabilityFlags;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PlatformError {
|
||||
#[error("operation unsupported on this platform")]
|
||||
Unsupported,
|
||||
#[error("platform operation failed: {0}")]
|
||||
Message(String),
|
||||
}
|
||||
|
||||
pub trait PlatformAdapter: Send + Sync {
|
||||
fn capabilities(&self) -> CapabilityFlags;
|
||||
fn attach_window_handle(&self, handle: isize) -> Result<(), PlatformError>;
|
||||
fn set_click_through(&self, enabled: bool) -> Result<(), PlatformError>;
|
||||
fn set_always_on_top(&self, enabled: bool) -> Result<(), PlatformError>;
|
||||
fn set_visible(&self, visible: bool) -> Result<(), PlatformError>;
|
||||
fn set_window_position(&self, x: f32, y: f32) -> Result<(), PlatformError>;
|
||||
}
|
||||
|
||||
pub fn create_adapter() -> Box<dyn PlatformAdapter> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
return Box::new(windows::WindowsAdapter::default());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Box::new(NoopAdapter::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NoopAdapter {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl PlatformAdapter for NoopAdapter {
|
||||
fn capabilities(&self) -> CapabilityFlags {
|
||||
CapabilityFlags::default()
|
||||
}
|
||||
|
||||
fn attach_window_handle(&self, _handle: isize) -> Result<(), PlatformError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_click_through(&self, _enabled: bool) -> Result<(), PlatformError> {
|
||||
Err(PlatformError::Unsupported)
|
||||
}
|
||||
|
||||
fn set_always_on_top(&self, _enabled: bool) -> Result<(), PlatformError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_visible(&self, _visible: bool) -> Result<(), PlatformError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_window_position(&self, _x: f32, _y: f32) -> Result<(), PlatformError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[allow(unsafe_code)]
|
||||
mod windows {
|
||||
use crate::{PlatformAdapter, PlatformError};
|
||||
use sprimo_protocol::v1::CapabilityFlags;
|
||||
use std::ffi::c_void;
|
||||
use std::sync::Mutex;
|
||||
use windows::Win32::Foundation::{COLORREF, GetLastError, HWND};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetWindowLongPtrW, SetLayeredWindowAttributes, SetWindowLongPtrW, SetWindowPos,
|
||||
ShowWindow, GWL_EXSTYLE, HWND_NOTOPMOST, HWND_TOPMOST, LWA_COLORKEY, SW_HIDE, SW_SHOW,
|
||||
SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WINDOW_EX_STYLE, WS_EX_LAYERED,
|
||||
WS_EX_TRANSPARENT,
|
||||
};
|
||||
|
||||
const COLOR_KEY_RGB: [u8; 3] = [255, 0, 255];
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WindowsAdapter {
|
||||
hwnd: Mutex<Option<isize>>,
|
||||
}
|
||||
|
||||
impl WindowsAdapter {
|
||||
fn enable_color_key_transparency(hwnd: HWND) -> Result<(), PlatformError> {
|
||||
// SAFETY: `hwnd` comes from a real native window handle attached during startup.
|
||||
let current_bits = unsafe { GetWindowLongPtrW(hwnd, GWL_EXSTYLE) };
|
||||
let mut style = WINDOW_EX_STYLE(current_bits as u32);
|
||||
style |= WS_EX_LAYERED;
|
||||
|
||||
// SAFETY: We pass the same valid window handle and write a computed style bitmask.
|
||||
let previous = unsafe { SetWindowLongPtrW(hwnd, GWL_EXSTYLE, style.0 as isize) };
|
||||
if previous == 0 {
|
||||
// SAFETY: Calling `GetLastError` immediately after failed Win32 API is valid.
|
||||
let code = unsafe { GetLastError().0 };
|
||||
if code != 0 {
|
||||
return Err(PlatformError::Message(format!(
|
||||
"SetWindowLongPtrW(layered) failed: {}",
|
||||
code
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let key = COLORREF(
|
||||
u32::from(COLOR_KEY_RGB[0])
|
||||
| (u32::from(COLOR_KEY_RGB[1]) << 8)
|
||||
| (u32::from(COLOR_KEY_RGB[2]) << 16),
|
||||
);
|
||||
// SAFETY: `hwnd` is valid and colorkey transparency is configured on a layered window.
|
||||
let result = unsafe { SetLayeredWindowAttributes(hwnd, key, 0, LWA_COLORKEY) };
|
||||
if let Err(err) = result {
|
||||
// SAFETY: Calling `GetLastError` immediately after failed Win32 API is valid.
|
||||
let code = unsafe { GetLastError().0 };
|
||||
return Err(PlatformError::Message(format!(
|
||||
"SetLayeredWindowAttributes(colorkey) failed: {} ({})",
|
||||
code, err
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_click_through_style(
|
||||
hwnd: HWND,
|
||||
click_through: Option<bool>,
|
||||
) -> Result<(), PlatformError> {
|
||||
// SAFETY: `hwnd` comes from a real native window handle attached during startup.
|
||||
let current_bits = unsafe { GetWindowLongPtrW(hwnd, GWL_EXSTYLE) };
|
||||
let mut style = WINDOW_EX_STYLE(current_bits as u32);
|
||||
if let Some(enabled) = click_through {
|
||||
if enabled {
|
||||
style |= WS_EX_TRANSPARENT;
|
||||
} else {
|
||||
style &= !WS_EX_TRANSPARENT;
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: We pass the same valid window handle and write a computed style bitmask.
|
||||
let previous = unsafe { SetWindowLongPtrW(hwnd, GWL_EXSTYLE, style.0 as isize) };
|
||||
if previous == 0 {
|
||||
// SAFETY: Calling `GetLastError` immediately after failed Win32 API is valid.
|
||||
let code = unsafe { GetLastError().0 };
|
||||
if code != 0 {
|
||||
return Err(PlatformError::Message(format!(
|
||||
"SetWindowLongPtrW failed: {}",
|
||||
code
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn with_hwnd<F>(&self, callback: F) -> Result<(), PlatformError>
|
||||
where
|
||||
F: FnOnce(HWND) -> Result<(), PlatformError>,
|
||||
{
|
||||
let guard = self
|
||||
.hwnd
|
||||
.lock()
|
||||
.map_err(|_| PlatformError::Message("window handle lock poisoned".to_string()))?;
|
||||
let hwnd = guard.ok_or_else(|| {
|
||||
PlatformError::Message("window handle not attached".to_string())
|
||||
})?;
|
||||
callback(HWND(hwnd as *mut c_void))
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformAdapter for WindowsAdapter {
|
||||
fn capabilities(&self) -> CapabilityFlags {
|
||||
CapabilityFlags {
|
||||
supports_click_through: true,
|
||||
supports_transparency: true,
|
||||
supports_tray: false,
|
||||
supports_global_hotkey: true,
|
||||
supports_skip_taskbar: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_window_handle(&self, handle: isize) -> Result<(), PlatformError> {
|
||||
{
|
||||
let mut guard = self.hwnd.lock().map_err(|_| {
|
||||
PlatformError::Message("window handle lock poisoned".to_string())
|
||||
})?;
|
||||
*guard = Some(handle);
|
||||
}
|
||||
self.with_hwnd(Self::enable_color_key_transparency)
|
||||
}
|
||||
|
||||
fn set_click_through(&self, enabled: bool) -> Result<(), PlatformError> {
|
||||
self.with_hwnd(|hwnd| Self::apply_click_through_style(hwnd, Some(enabled)))
|
||||
}
|
||||
|
||||
fn set_always_on_top(&self, enabled: bool) -> Result<(), PlatformError> {
|
||||
self.with_hwnd(|hwnd| {
|
||||
let insert_after = if enabled { HWND_TOPMOST } else { HWND_NOTOPMOST };
|
||||
// SAFETY: `hwnd` is valid and flags request only z-order update.
|
||||
let result = unsafe {
|
||||
SetWindowPos(hwnd, insert_after, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)
|
||||
};
|
||||
if let Err(err) = result {
|
||||
// SAFETY: Calling `GetLastError` immediately after failed Win32 API is valid.
|
||||
let code = unsafe { GetLastError().0 };
|
||||
return Err(PlatformError::Message(format!(
|
||||
"SetWindowPos(topmost) failed: {} ({})",
|
||||
code, err
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn set_visible(&self, visible: bool) -> Result<(), PlatformError> {
|
||||
self.with_hwnd(|hwnd| {
|
||||
let command = if visible { SW_SHOW } else { SW_HIDE };
|
||||
// SAFETY: `hwnd` is valid and `ShowWindow` is safe with these standard commands.
|
||||
unsafe {
|
||||
let _ = ShowWindow(hwnd, command);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn set_window_position(&self, x: f32, y: f32) -> Result<(), PlatformError> {
|
||||
self.with_hwnd(|hwnd| {
|
||||
// SAFETY: `hwnd` is valid and this call updates only position.
|
||||
let result = unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
HWND(std::ptr::null_mut()),
|
||||
x.round() as i32,
|
||||
y.round() as i32,
|
||||
0,
|
||||
0,
|
||||
SWP_NOSIZE | SWP_NOZORDER,
|
||||
)
|
||||
};
|
||||
if let Err(err) = result {
|
||||
// SAFETY: Calling `GetLastError` immediately after failed Win32 API is valid.
|
||||
let code = unsafe { GetLastError().0 };
|
||||
return Err(PlatformError::Message(format!(
|
||||
"SetWindowPos(move) failed: {} ({})",
|
||||
code, err
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
15
crates/sprimo-protocol/Cargo.toml
Normal file
15
crates/sprimo-protocol/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "sprimo-protocol"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
173
crates/sprimo-protocol/src/lib.rs
Normal file
173
crates/sprimo-protocol/src/lib.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
pub mod v1 {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FrontendState {
|
||||
Idle,
|
||||
Active,
|
||||
Success,
|
||||
Error,
|
||||
Dragging,
|
||||
Hidden,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct AnimationPriority(pub u8);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CommandEnvelope {
|
||||
pub id: Uuid,
|
||||
pub ts_ms: i64,
|
||||
pub command: FrontendCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
|
||||
pub enum FrontendCommand {
|
||||
SetState {
|
||||
state: FrontendState,
|
||||
ttl_ms: Option<u64>,
|
||||
},
|
||||
PlayAnimation {
|
||||
name: String,
|
||||
priority: AnimationPriority,
|
||||
duration_ms: Option<u64>,
|
||||
interrupt: Option<bool>,
|
||||
},
|
||||
SetSpritePack {
|
||||
pack_id_or_path: String,
|
||||
},
|
||||
SetTransform {
|
||||
x: Option<f32>,
|
||||
y: Option<f32>,
|
||||
anchor: Option<String>,
|
||||
scale: Option<f32>,
|
||||
opacity: Option<f32>,
|
||||
},
|
||||
SetFlags {
|
||||
click_through: Option<bool>,
|
||||
always_on_top: Option<bool>,
|
||||
visible: Option<bool>,
|
||||
},
|
||||
Toast {
|
||||
text: String,
|
||||
ttl_ms: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CapabilityFlags {
|
||||
pub supports_click_through: bool,
|
||||
pub supports_transparency: bool,
|
||||
pub supports_tray: bool,
|
||||
pub supports_global_hotkey: bool,
|
||||
pub supports_skip_taskbar: bool,
|
||||
}
|
||||
|
||||
impl Default for CapabilityFlags {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
supports_click_through: false,
|
||||
supports_transparency: true,
|
||||
supports_tray: false,
|
||||
supports_global_hotkey: false,
|
||||
supports_skip_taskbar: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OverlayFlags {
|
||||
pub click_through: bool,
|
||||
pub always_on_top: bool,
|
||||
pub visible: bool,
|
||||
}
|
||||
|
||||
impl Default for OverlayFlags {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
click_through: false,
|
||||
always_on_top: true,
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FrontendStateSnapshot {
|
||||
pub state: FrontendState,
|
||||
pub current_animation: String,
|
||||
pub flags: OverlayFlags,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub scale: f32,
|
||||
pub active_sprite_pack: String,
|
||||
pub capabilities: CapabilityFlags,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_error: Option<String>,
|
||||
}
|
||||
|
||||
impl FrontendStateSnapshot {
|
||||
#[must_use]
|
||||
pub fn idle(capabilities: CapabilityFlags) -> Self {
|
||||
Self {
|
||||
state: FrontendState::Idle,
|
||||
current_animation: "idle".to_string(),
|
||||
flags: OverlayFlags::default(),
|
||||
x: 200.0,
|
||||
y: 200.0,
|
||||
scale: 1.0,
|
||||
active_sprite_pack: "default".to_string(),
|
||||
capabilities,
|
||||
last_error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct HealthResponse {
|
||||
pub version: String,
|
||||
pub build: String,
|
||||
pub uptime_seconds: u64,
|
||||
pub active_sprite_pack: String,
|
||||
pub capabilities: CapabilityFlags,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ErrorResponse {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::v1::{AnimationPriority, CommandEnvelope, FrontendCommand, FrontendState};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn command_envelope_roundtrip() {
|
||||
let payload = CommandEnvelope {
|
||||
id: Uuid::nil(),
|
||||
ts_ms: 1_737_000_000_000,
|
||||
command: FrontendCommand::PlayAnimation {
|
||||
name: "dance".to_string(),
|
||||
priority: AnimationPriority(9),
|
||||
duration_ms: Some(1_200),
|
||||
interrupt: Some(true),
|
||||
},
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_string(&payload).expect("serialize");
|
||||
let decoded: CommandEnvelope = serde_json::from_str(&encoded).expect("deserialize");
|
||||
assert_eq!(decoded, payload);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_serializes_as_snake_case() {
|
||||
let encoded = serde_json::to_string(&FrontendState::Hidden).expect("serialize");
|
||||
assert_eq!(encoded, "\"hidden\"");
|
||||
}
|
||||
}
|
||||
16
crates/sprimo-sprite/Cargo.toml
Normal file
16
crates/sprimo-sprite/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "sprimo-sprite"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.18.0"
|
||||
123
crates/sprimo-sprite/src/lib.rs
Normal file
123
crates/sprimo-sprite/src/lib.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
const MANIFEST_FILE: &str = "manifest.json";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SpriteError {
|
||||
#[error("sprite pack not found: {0}")]
|
||||
PackNotFound(String),
|
||||
#[error("manifest not found: {0}")]
|
||||
ManifestNotFound(PathBuf),
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("invalid manifest: {0}")]
|
||||
Parse(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpritePackManifest {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
pub image: String,
|
||||
pub frame_width: u32,
|
||||
pub frame_height: u32,
|
||||
pub animations: Vec<AnimationDefinition>,
|
||||
pub anchor: AnchorPoint,
|
||||
#[serde(default)]
|
||||
pub chroma_key_color: Option<String>,
|
||||
#[serde(default)]
|
||||
pub chroma_key_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AnimationDefinition {
|
||||
pub name: String,
|
||||
pub fps: u16,
|
||||
pub frames: Vec<u32>,
|
||||
pub one_shot: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AnchorPoint {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
pub fn load_manifest(pack_dir: &Path) -> Result<SpritePackManifest, SpriteError> {
|
||||
let manifest_path = pack_dir.join(MANIFEST_FILE);
|
||||
if !manifest_path.exists() {
|
||||
return Err(SpriteError::ManifestNotFound(manifest_path));
|
||||
}
|
||||
|
||||
let raw = fs::read_to_string(&manifest_path)?;
|
||||
let manifest = serde_json::from_str::<SpritePackManifest>(&raw)?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
pub fn resolve_pack_path(root: &Path, pack_id_or_path: &str) -> Result<PathBuf, SpriteError> {
|
||||
let direct = PathBuf::from(pack_id_or_path);
|
||||
if direct.exists() {
|
||||
return Ok(direct);
|
||||
}
|
||||
|
||||
let candidate = root.join(pack_id_or_path);
|
||||
if candidate.exists() {
|
||||
return Ok(candidate);
|
||||
}
|
||||
|
||||
Err(SpriteError::PackNotFound(pack_id_or_path.to_string()))
|
||||
}
|
||||
|
||||
pub fn load_selected_or_default(
|
||||
root: &Path,
|
||||
selected_pack: &str,
|
||||
default_pack: &str,
|
||||
) -> Result<SpritePackManifest, SpriteError> {
|
||||
let selected = resolve_pack_path(root, selected_pack)
|
||||
.and_then(|path| load_manifest(&path));
|
||||
if let Ok(manifest) = selected {
|
||||
return Ok(manifest);
|
||||
}
|
||||
|
||||
let fallback_path = resolve_pack_path(root, default_pack)?;
|
||||
load_manifest(&fallback_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{load_selected_or_default, SpritePackManifest};
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn write_manifest(dir: &std::path::Path, id: &str) {
|
||||
let manifest = SpritePackManifest {
|
||||
id: id.to_string(),
|
||||
version: "1".to_string(),
|
||||
image: "atlas.png".to_string(),
|
||||
frame_width: 64,
|
||||
frame_height: 64,
|
||||
animations: vec![],
|
||||
anchor: super::AnchorPoint { x: 0.5, y: 1.0 },
|
||||
chroma_key_color: None,
|
||||
chroma_key_enabled: None,
|
||||
};
|
||||
let encoded = serde_json::to_string_pretty(&manifest).expect("manifest encode");
|
||||
fs::write(dir.join("manifest.json"), encoded).expect("manifest write");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_to_default_pack() {
|
||||
let temp = TempDir::new().expect("tempdir");
|
||||
let root = temp.path();
|
||||
let default_dir = root.join("default");
|
||||
fs::create_dir_all(&default_dir).expect("mkdir");
|
||||
write_manifest(&default_dir, "default");
|
||||
|
||||
let manifest =
|
||||
load_selected_or_default(root, "missing", "default").expect("fallback");
|
||||
assert_eq!(manifest.id, "default");
|
||||
}
|
||||
}
|
||||
80
docs/API_SPEC.md
Normal file
80
docs/API_SPEC.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Sprimo Frontend API Specification (v1)
|
||||
|
||||
Base URL: `http://127.0.0.1:<port>`
|
||||
|
||||
Auth: `Authorization: Bearer <token>` required on all endpoints except `/v1/health`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `GET /v1/health`
|
||||
|
||||
- Auth: none
|
||||
- Response `200`:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"build": "dev",
|
||||
"uptime_seconds": 12,
|
||||
"active_sprite_pack": "default",
|
||||
"capabilities": {
|
||||
"supports_click_through": true,
|
||||
"supports_transparency": true,
|
||||
"supports_tray": true,
|
||||
"supports_global_hotkey": true,
|
||||
"supports_skip_taskbar": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /v1/state`
|
||||
|
||||
- Auth: required
|
||||
- Response `200`: current frontend snapshot.
|
||||
- Response `401`: missing/invalid token.
|
||||
- Includes optional `last_error` diagnostic field for latest runtime command-application error.
|
||||
|
||||
### `POST /v1/command`
|
||||
|
||||
- Auth: required
|
||||
- Body: one command envelope.
|
||||
- Response `202`: accepted.
|
||||
- Response `400`: malformed JSON.
|
||||
- Response `401`: missing/invalid token.
|
||||
- Runtime note for `SetSpritePack`:
|
||||
- successful load switches sprite atlas immediately.
|
||||
- failed load keeps current in-memory sprite/animation unchanged and reports the failure via `/v1/state.last_error`.
|
||||
|
||||
### `POST /v1/commands`
|
||||
|
||||
- Auth: required
|
||||
- Body: ordered array of command envelopes.
|
||||
- Response `202`: accepted.
|
||||
- Response `400`: malformed JSON.
|
||||
- Response `401`: missing/invalid token.
|
||||
- Commands are applied in-order by the frontend runtime after transport acceptance.
|
||||
|
||||
## Command Envelope
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "2d95f8a8-65cc-4309-8f76-4881949d7f4b",
|
||||
"ts_ms": 1737000000000,
|
||||
"command": {
|
||||
"type": "set_state",
|
||||
"payload": {
|
||||
"state": "active",
|
||||
"ttl_ms": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "unauthorized",
|
||||
"message": "missing or invalid bearer token"
|
||||
}
|
||||
```
|
||||
43
docs/ARCHITECTURE.md
Normal file
43
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Sprimo Frontend Architecture
|
||||
|
||||
## Workspace Layout
|
||||
|
||||
- `crates/sprimo-app`: process entrypoint and runtime wiring.
|
||||
- `crates/sprimo-api`: axum-based localhost control server.
|
||||
- `crates/sprimo-config`: config schema, path resolution, persistence.
|
||||
- `crates/sprimo-platform`: platform abstraction for overlay operations.
|
||||
- `crates/sprimo-protocol`: shared API/state/command protocol types.
|
||||
- `crates/sprimo-sprite`: sprite pack manifest loading and fallback logic.
|
||||
|
||||
## Runtime Data Flow
|
||||
|
||||
1. `sprimo-app` loads or creates `config.toml`.
|
||||
2. App builds initial `FrontendStateSnapshot`.
|
||||
3. App starts `sprimo-api` on a Tokio runtime.
|
||||
4. API authenticates commands and deduplicates IDs.
|
||||
5. Commands are bridged from Tokio channel to Bevy main-thread systems.
|
||||
6. Bevy systems apply commands to sprite state, window/platform operations, and config persistence.
|
||||
7. Shared snapshot is exposed by API via `/v1/state` and `/v1/health`.
|
||||
|
||||
## Sprite Reload Semantics
|
||||
|
||||
- `SetSpritePack` uses a two-phase flow:
|
||||
1. Validate and build candidate pack runtime (manifest + clips + atlas layout + texture handle).
|
||||
2. Commit atomically only on success.
|
||||
- On reload failure, current pack remains active and snapshot `last_error` is updated.
|
||||
|
||||
## Threading Model
|
||||
|
||||
- Main task: startup + shutdown signal.
|
||||
- API task: axum server.
|
||||
- Bridge task: forwards API commands into Bevy ingest channel.
|
||||
- Bevy main thread: rendering, animation, command application, and window behavior.
|
||||
- Optional hotkey thread (Windows): registers global hotkey and pushes recovery events.
|
||||
- Snapshot is shared via `Arc<RwLock<_>>`.
|
||||
|
||||
## Design Constraints
|
||||
|
||||
- Bind API to localhost only.
|
||||
- Token auth by default for mutating and state endpoints.
|
||||
- Keep protocol crate renderer-agnostic for future backend/frontend reuse.
|
||||
- Keep platform crate isolated for per-OS implementations.
|
||||
45
docs/CONFIG_REFERENCE.md
Normal file
45
docs/CONFIG_REFERENCE.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Sprimo Config Reference
|
||||
|
||||
File location:
|
||||
|
||||
- Windows: `%APPDATA%/sprimo/config.toml`
|
||||
- macOS: `~/Library/Application Support/sprimo/config.toml`
|
||||
- Linux: `~/.config/sprimo/config.toml`
|
||||
|
||||
## Schema
|
||||
|
||||
```toml
|
||||
[window]
|
||||
x = 200.0
|
||||
y = 200.0
|
||||
monitor_id = ""
|
||||
scale = 1.0
|
||||
always_on_top = true
|
||||
click_through = false
|
||||
visible = true
|
||||
|
||||
[animation]
|
||||
fps = 30
|
||||
idle_timeout_ms = 3000
|
||||
|
||||
[sprite]
|
||||
selected_pack = "default"
|
||||
sprite_packs_dir = "sprite-packs"
|
||||
|
||||
[api]
|
||||
port = 32145
|
||||
auth_token = "generated-uuid-token"
|
||||
|
||||
[logging]
|
||||
level = "info"
|
||||
|
||||
[controls]
|
||||
hotkey_enabled = true
|
||||
recovery_hotkey = "Ctrl+Alt+P"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `auth_token` is generated on first run if config does not exist.
|
||||
- `window.x`, `window.y`, `window.scale`, and flag fields are persisted after matching commands.
|
||||
- On Windows, `recovery_hotkey` provides click-through recovery even when the window is non-interactive.
|
||||
497
docs/FRONTEND_REQUIREMENTS.md
Normal file
497
docs/FRONTEND_REQUIREMENTS.md
Normal file
@@ -0,0 +1,497 @@
|
||||
## Frontend Requirements Document — **sprimo-frontend**
|
||||
|
||||
**Document type:** Product + Engineering requirements (frontend scope only)
|
||||
**Version:** v0.1
|
||||
**Date:** 2026-02-12
|
||||
**Scope:** Bevy-based overlay renderer + local control server + minimal UI.
|
||||
**Out of scope:** Backend detection/rules engine (assumed external), cloud services.
|
||||
|
||||
---
|
||||
|
||||
# 1. Overview
|
||||
|
||||
The frontend is a **desktop overlay pet renderer** implemented in Rust (Bevy). It presents an animated character in a **transparent, borderless window** that can be **always-on-top** and optionally **click-through**. It receives control instructions from a local backend process via **localhost REST API**, applies them to its animation/state machine, and persists user preferences locally.
|
||||
|
||||
The frontend must be able to run standalone (idle animation) even if the backend is not running.
|
||||
|
||||
---
|
||||
|
||||
# 2. Goals
|
||||
|
||||
> Note: Windows and Linux should be first-hand support, macOS support is optional.
|
||||
|
||||
1. **Render a cute animated character overlay** with smooth sprite animation.
|
||||
2. Provide a **stable command interface** (REST) for backend control.
|
||||
3. Offer **essential user controls** (tray/menu + hotkeys optional) to avoid “locking” the pet in click-through mode.
|
||||
4. Persist **window position, scale, sprite pack choice, and flags**.
|
||||
5. Be **cross-platform** (Windows/macOS/Linux) with documented degradations, especially on Linux Wayland.
|
||||
|
||||
---
|
||||
|
||||
# 3. Non-Goals (Frontend v0.1)
|
||||
|
||||
* Tool activity detection (backend responsibility).
|
||||
* Online sprite gallery, accounts, telemetry.
|
||||
* Complex rigged animation (Live2D/3D). Sprite atlas only.
|
||||
* Full settings UI panel (tray + config file is enough for v0.1).
|
||||
|
||||
---
|
||||
|
||||
# 4. User Stories
|
||||
|
||||
## Core
|
||||
|
||||
* As a user, I can see the pet on my desktop immediately after launch.
|
||||
* As a user, I can drag the pet to a preferred location.
|
||||
* As a user, I can toggle click-through so the pet doesn’t block my mouse.
|
||||
* As a user, I can toggle always-on-top so the pet stays visible.
|
||||
* As a user, I can change the character (sprite pack).
|
||||
|
||||
## Backend control
|
||||
|
||||
* As a backend process, I can instruct the frontend to play specific animations and switch states via REST.
|
||||
* As a backend process, I can update position/scale/visibility.
|
||||
* As a backend process, I can query frontend health/version.
|
||||
|
||||
## Safety
|
||||
|
||||
* As a user, I can always recover control of the pet even if click-through is enabled (hotkey/tray item).
|
||||
|
||||
---
|
||||
|
||||
# 5. Functional Requirements
|
||||
|
||||
## 5.1 Window & Overlay Behavior
|
||||
|
||||
### FR-FW-1 Borderless + transparent
|
||||
|
||||
* Window must be **borderless** and **no title bar**.
|
||||
* Background must be **transparent** so only the pet is visible.
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* Screenshot shows only pet pixels; underlying desktop visible.
|
||||
* No OS window chrome visible.
|
||||
|
||||
### FR-FW-2 Always-on-top
|
||||
|
||||
* Support always-on-top toggle.
|
||||
* Default: ON (configurable).
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* When ON, window stays above normal windows.
|
||||
|
||||
### FR-FW-3 Click-through (mouse pass-through)
|
||||
|
||||
* Support enabling/disabling click-through:
|
||||
|
||||
* ON: mouse events pass to windows underneath.
|
||||
* OFF: pet receives mouse input (drag, context menu).
|
||||
* Must provide a **failsafe** mechanism to disable click-through without clicking the pet.
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* With click-through enabled, user can click apps behind pet.
|
||||
* User can disable click-through via tray or hotkey reliably.
|
||||
|
||||
### FR-FW-4 Dragging & anchoring
|
||||
|
||||
* When click-through is OFF, user can drag the pet.
|
||||
* Dragging updates persisted position in config.
|
||||
* Optional: snapping to screen edges.
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* Drag works smoothly, without major jitter.
|
||||
* Relaunch restores position.
|
||||
|
||||
### FR-FW-5 Multi-monitor + DPI
|
||||
|
||||
* Window positioning must respect:
|
||||
|
||||
* multiple monitors
|
||||
* per-monitor DPI scaling
|
||||
* Position stored in a device-independent way where possible.
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* Pet stays at expected corner after moving between monitors.
|
||||
|
||||
### FR-FW-6 Taskbar/Dock visibility
|
||||
|
||||
* Prefer not showing a taskbar entry for the overlay window (where supported).
|
||||
* If not feasible cross-platform, document behavior.
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* On Windows/macOS: overlay window ideally hidden from taskbar/dock.
|
||||
* If not implemented, doesn’t break core usage.
|
||||
|
||||
### Platform notes (requirements)
|
||||
|
||||
* **Windows:** click-through uses extended window styles (WS_EX_TRANSPARENT / layered), always-on-top via SetWindowPos.
|
||||
* **macOS:** NSWindow level + ignoresMouseEvents.
|
||||
* **Linux:** best effort:
|
||||
|
||||
* X11: possible with shape/input region.
|
||||
* Wayland: click-through may be unavailable; document limitation.
|
||||
|
||||
---
|
||||
|
||||
## 5.2 Rendering & Animation
|
||||
|
||||
### FR-ANI-1 Sprite pack format
|
||||
|
||||
Frontend must load sprite packs from local disk with:
|
||||
|
||||
* `manifest.json` (required)
|
||||
* atlas image(s) (PNG; required)
|
||||
* optional metadata (author, license, preview)
|
||||
|
||||
**Manifest must define:**
|
||||
|
||||
* sprite sheet dimensions and frame rects (or grid)
|
||||
* animations: name → ordered frame list + fps
|
||||
* anchor point (pivot) for positioning
|
||||
* optional hitbox (for drag region)
|
||||
* optional offsets per frame (advanced)
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* Default bundled pack loads with no config.
|
||||
* Invalid packs fail gracefully with error message/log and fallback to default pack.
|
||||
|
||||
### FR-ANI-2 Animation playback
|
||||
|
||||
* Play loop animations (idle/dance).
|
||||
* Support one-shot animations (celebrate/error) with:
|
||||
|
||||
* duration or “until frames complete”
|
||||
* return-to-previous-state behavior
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* Switching animations is immediate and consistent.
|
||||
|
||||
### FR-ANI-3 FPS control
|
||||
|
||||
* Configurable target FPS for animation updates (e.g., 30/60).
|
||||
* Rendering should remain stable.
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* Low CPU usage when idle animation running.
|
||||
|
||||
### FR-ANI-4 Layering
|
||||
|
||||
* Single character is mandatory for v0.1.
|
||||
* Optional: accessory layers (future).
|
||||
|
||||
---
|
||||
|
||||
## 5.3 State Machine & Command Application
|
||||
|
||||
### FR-STM-1 Core states
|
||||
|
||||
Frontend supports at minimum:
|
||||
|
||||
* `Idle`
|
||||
* `Active`
|
||||
* `Success`
|
||||
* `Error`
|
||||
* `Dragging` (internal)
|
||||
* `Hidden` (window hidden but process alive)
|
||||
|
||||
Each state maps to a default animation (configurable by sprite pack):
|
||||
|
||||
* Idle → idle animation
|
||||
* Active → typing/dance
|
||||
* Success → celebrate then return to Active/Idle
|
||||
* Error → upset then return
|
||||
|
||||
### FR-STM-2 Transition rules
|
||||
|
||||
* Backend commands can:
|
||||
|
||||
* set state directly
|
||||
* request specific animation with priority
|
||||
* Frontend must implement:
|
||||
|
||||
* debouncing/cooldown (avoid flicker)
|
||||
* priority arbitration (e.g., Error overrides Active)
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* Repeated Active commands don’t restart animation constantly.
|
||||
* Error state overrides Active for N seconds.
|
||||
|
||||
### FR-STM-3 Local autonomy (no backend)
|
||||
|
||||
* If backend is absent:
|
||||
|
||||
* frontend stays in Idle with periodic idle animation
|
||||
* user controls still function
|
||||
* If backend connects later, commands apply immediately.
|
||||
|
||||
---
|
||||
|
||||
## 5.4 REST Control Server (Frontend-hosted)
|
||||
|
||||
### FR-API-1 Local binding only
|
||||
|
||||
* HTTP server must bind to `127.0.0.1` by default.
|
||||
* Port configurable; recommended default fixed port (e.g., 32145).
|
||||
|
||||
### FR-API-2 Authentication token
|
||||
|
||||
* Frontend generates a random token on first run.
|
||||
* Stored in local config directory.
|
||||
* Backend must provide `Authorization: Bearer <token>` for all non-health endpoints.
|
||||
* Health endpoint may be unauthenticated (optional).
|
||||
|
||||
### FR-API-3 Endpoints (v0.1)
|
||||
|
||||
**Required:**
|
||||
|
||||
* `GET /v1/health`
|
||||
|
||||
* returns: version, build, uptime, active_sprite_pack
|
||||
* `POST /v1/command`
|
||||
|
||||
* accept a single command
|
||||
* `POST /v1/commands`
|
||||
|
||||
* accept batch of commands
|
||||
* `GET /v1/state` (debug)
|
||||
|
||||
* current state, current animation, flags, position/scale
|
||||
|
||||
**Command types required:**
|
||||
|
||||
* `SetState { state, ttl_ms? }`
|
||||
* `PlayAnimation { name, priority, duration_ms?, interrupt? }`
|
||||
* `SetSpritePack { pack_id_or_path }`
|
||||
* `SetTransform { x?, y?, anchor?, scale?, opacity? }`
|
||||
* `SetFlags { click_through?, always_on_top?, visible? }`
|
||||
* `Toast { text, ttl_ms? }` (optional but recommended)
|
||||
|
||||
### FR-API-4 Idempotency & dedupe
|
||||
|
||||
* Commands include:
|
||||
|
||||
* `id` (ULID/UUID)
|
||||
* `ts_ms`
|
||||
* Frontend must ignore duplicate command IDs for a short window (e.g., last 5k IDs or last 10 minutes).
|
||||
|
||||
### FR-API-5 Error handling
|
||||
|
||||
* Invalid commands return 400 with error details.
|
||||
* Auth failure returns 401.
|
||||
* Server never crashes due to malformed input.
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* Fuzzing basic JSON payload does not crash.
|
||||
|
||||
---
|
||||
|
||||
## 5.5 User Controls
|
||||
|
||||
### FR-CTL-1 Tray/menu bar controls (preferred)
|
||||
|
||||
Provide tray/menu bar items:
|
||||
|
||||
* Show/Hide
|
||||
* Toggle Click-through
|
||||
* Toggle Always-on-top
|
||||
* Sprite Pack selection (at least “Default” + “Open sprite folder…”)
|
||||
* Reload sprite packs
|
||||
* Quit
|
||||
|
||||
If tray is too hard on Linux in v0.1, provide a fallback (hotkey + config).
|
||||
|
||||
### FR-CTL-2 Hotkeys (failsafe)
|
||||
|
||||
At minimum one global hotkey:
|
||||
|
||||
* Toggle click-through OR “enter interactive mode”
|
||||
|
||||
Example default:
|
||||
|
||||
* `Ctrl+Alt+P` (Windows/Linux), `Cmd+Option+P` (macOS)
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* User can recover control even if pet is click-through and cannot be clicked.
|
||||
|
||||
### FR-CTL-3 Context menu (optional)
|
||||
|
||||
Right click pet (when interactive) to open a minimal menu.
|
||||
|
||||
---
|
||||
|
||||
## 5.6 Persistence & Configuration
|
||||
|
||||
### FR-CFG-1 Config storage
|
||||
|
||||
* Store config in OS-appropriate directory:
|
||||
|
||||
* Windows: `%APPDATA%/sprimo/config.toml`
|
||||
* macOS: `~/Library/Application Support/sprimo/config.toml`
|
||||
* Linux: `~/.config/sprimo/config.toml`
|
||||
|
||||
### FR-CFG-2 Config fields
|
||||
|
||||
* window:
|
||||
|
||||
* position (x,y) + monitor id (best-effort)
|
||||
* scale
|
||||
* always_on_top
|
||||
* click_through
|
||||
* visible
|
||||
* animation:
|
||||
|
||||
* fps
|
||||
* idle_timeout_ms
|
||||
* sprite:
|
||||
|
||||
* selected_pack
|
||||
* sprite_packs_dir
|
||||
* api:
|
||||
|
||||
* port
|
||||
* auth_token
|
||||
* logging:
|
||||
|
||||
* level
|
||||
|
||||
### FR-CFG-3 Live reload (nice-to-have)
|
||||
|
||||
* v0.1: reload on tray action (“Reload config”).
|
||||
* v0.2+: auto reload.
|
||||
|
||||
---
|
||||
|
||||
## 5.7 Logging & Diagnostics
|
||||
|
||||
### FR-LOG-1 Logging
|
||||
|
||||
* Log to file + console (configurable).
|
||||
* Include:
|
||||
|
||||
* API requests summary
|
||||
* command application
|
||||
* sprite pack load errors
|
||||
* platform overlay operations outcomes
|
||||
|
||||
### FR-LOG-2 Diagnostics endpoint (optional)
|
||||
|
||||
* `GET /v1/diag` returns recent errors and platform capability flags.
|
||||
|
||||
---
|
||||
|
||||
# 6. Non-Functional Requirements
|
||||
|
||||
## Performance
|
||||
|
||||
* Idle: < 2% CPU on typical dev laptop (target).
|
||||
* Memory: should not grow unbounded; texture caching bounded.
|
||||
|
||||
## Reliability
|
||||
|
||||
* Should run for 8 hours without crash under command load.
|
||||
* If REST server fails to bind port, show clear error and fallback behavior.
|
||||
|
||||
## Security
|
||||
|
||||
* Localhost only binding.
|
||||
* Token auth by default.
|
||||
* No file system access beyond sprite/config/log directories.
|
||||
|
||||
## Compatibility
|
||||
|
||||
* Windows 10/11
|
||||
* macOS 12+ recommended
|
||||
* Linux:
|
||||
|
||||
* X11 supported
|
||||
* Wayland: documented limitations, still runs
|
||||
|
||||
---
|
||||
|
||||
# 7. Platform Capability Matrix (deliverable)
|
||||
|
||||
Frontend must expose in logs (and optionally `/v1/health`) capability flags:
|
||||
|
||||
* `supports_click_through`
|
||||
* `supports_transparency`
|
||||
* `supports_tray`
|
||||
* `supports_global_hotkey`
|
||||
* `supports_skip_taskbar`
|
||||
|
||||
Example:
|
||||
|
||||
* Windows: all true
|
||||
* macOS: all true
|
||||
* Linux X11: most true
|
||||
* Linux Wayland: click-through likely false, skip-taskbar variable
|
||||
|
||||
---
|
||||
|
||||
# 8. Acceptance Test Plan (v0.1)
|
||||
|
||||
## Window
|
||||
|
||||
1. Launch: window appears borderless & transparent.
|
||||
2. Drag: with click-through OFF, drag updates position; restart restores.
|
||||
3. Click-through: toggle via hotkey; pet becomes non-interactive; toggle back works.
|
||||
4. Always-on-top: verify staying above typical apps.
|
||||
|
||||
## Animation
|
||||
|
||||
1. Default pack: idle animation loops.
|
||||
2. Command: `PlayAnimation("dance")` plays immediately.
|
||||
3. One-shot: `Success` plays then returns to previous.
|
||||
|
||||
## API
|
||||
|
||||
1. Health returns correct version/build.
|
||||
2. Auth required for commands; invalid token rejected.
|
||||
3. Batch endpoint applies multiple commands in order.
|
||||
4. Malformed JSON returns 400, app remains running.
|
||||
|
||||
## Persistence
|
||||
|
||||
1. Settings persist across restart.
|
||||
2. Missing sprite pack falls back to default.
|
||||
|
||||
---
|
||||
|
||||
# 9. Implementation Constraints / Suggested Stack (not mandatory but recommended)
|
||||
|
||||
* Renderer/engine: **Bevy 2D**
|
||||
* REST server: **axum + tokio** in background thread
|
||||
* Shared protocol: `protocol` crate with `serde` structs
|
||||
* Platform overlay: separate crate/module with per-OS implementations:
|
||||
|
||||
* Windows: `windows` crate (Win32 APIs)
|
||||
* macOS: `objc2`/`cocoa` bindings
|
||||
* Linux X11: `x11rb` (best effort)
|
||||
* Config: `toml`
|
||||
* IDs: `ulid` or `uuid`
|
||||
|
||||
---
|
||||
|
||||
# 10. Open Questions (Frontend)
|
||||
|
||||
1. Do we require Linux Wayland support beyond “runs but limited overlay features”?
|
||||
2. Do we want multiple pets (multiple windows) in v0.1 or later?
|
||||
3. Do we embed a default sprite pack (license?), or ship an empty skeleton?
|
||||
|
||||
---
|
||||
|
||||
If you want, I can turn this into:
|
||||
|
||||
* a `docs/FRONTEND_REQUIREMENTS.md`
|
||||
* plus a concrete **REST API spec** (OpenAPI-like) and a **sprite-pack manifest schema** with examples (JSON).
|
||||
26
docs/IMPLEMENTATION_STATUS.md
Normal file
26
docs/IMPLEMENTATION_STATUS.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Sprimo Frontend Implementation Status
|
||||
|
||||
Date: 2026-02-12
|
||||
|
||||
## MVP Vertical Slice
|
||||
|
||||
| Area | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| Workspace split | Implemented | `sprimo-app`, `sprimo-api`, `sprimo-config`, `sprimo-platform`, `sprimo-protocol`, `sprimo-sprite` |
|
||||
| Local REST server | Implemented | `/v1/health`, `/v1/state`, `/v1/command`, `/v1/commands`; localhost bind and bearer auth |
|
||||
| Command/state pipeline | Implemented | Command queue, state snapshot updates, transient state TTL rollback |
|
||||
| Config persistence | Implemented | `config.toml` bootstrap/load/save with generated token |
|
||||
| Sprite pack contract | Implemented | `manifest.json` loader and selected->default fallback |
|
||||
| Platform abstraction | Implemented | Windows adapter now applies click-through/top-most/visibility/position using Win32 APIs |
|
||||
| Overlay rendering | Implemented (MVP) | Bevy runtime with transparent undecorated window, sprite playback, command bridge |
|
||||
| Global failsafe | Implemented (Windows) | Global recovery hotkey `Ctrl+Alt+P` disables click-through and forces visibility |
|
||||
| Embedded default pack | Implemented | Bundled under `assets/sprite-packs/default/` using `sprite.png` (8x7, 512x512 frames) |
|
||||
| Build/package automation | Implemented (Windows) | `justfile` and `scripts/package_windows.py` generate portable ZIP + SHA256 |
|
||||
| QA/documentation workflow | Implemented | `docs/QA_WORKFLOW.md`, issue/evidence templates, and `scripts/qa_validate.py` with `just qa-validate` |
|
||||
|
||||
## Next Major Gaps
|
||||
|
||||
1. Tray/menu controls are still not implemented.
|
||||
2. Linux/macOS overlay behavior remains best-effort with no-op platform adapter.
|
||||
3. `/v1/state` diagnostics are minimal; error history is not persisted beyond latest runtime error.
|
||||
4. Issue 1 runtime after-fix screenshot evidence is still pending before closure.
|
||||
62
docs/ISSUE_TEMPLATE.md
Normal file
62
docs/ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Issue Template (`issues/issueN.md`)
|
||||
|
||||
## Title
|
||||
|
||||
## Severity
|
||||
|
||||
## Environment
|
||||
|
||||
- OS:
|
||||
- App version/build:
|
||||
- Renderer/backend details:
|
||||
|
||||
## Summary
|
||||
|
||||
## Reproduction Steps
|
||||
|
||||
1.
|
||||
2.
|
||||
|
||||
## Expected Result
|
||||
|
||||
## Actual Result
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
## Fix Plan
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
## Verification
|
||||
|
||||
### Commands Run
|
||||
|
||||
- [ ] `cargo check --workspace`
|
||||
- [ ] `cargo test --workspace`
|
||||
- [ ] `just qa-validate`
|
||||
|
||||
### Visual Checklist
|
||||
|
||||
- [ ] Before screenshot(s): `issues/screenshots/issueN-before-YYYYMMDD-HHMMSS.png`
|
||||
- [ ] After screenshot(s): `issues/screenshots/issueN-after-YYYYMMDD-HHMMSS.png`
|
||||
|
||||
### Result
|
||||
|
||||
- Status:
|
||||
- Notes:
|
||||
|
||||
## Status History
|
||||
|
||||
- `YYYY-MM-DD HH:MM` - `<actor>` - `Reported` - note
|
||||
|
||||
## Closure
|
||||
|
||||
- Current Status: `Reported`
|
||||
- Close Date:
|
||||
- Owner:
|
||||
- Linked PR/commit:
|
||||
|
||||
## Original Report (if migrated)
|
||||
|
||||
Paste the initial report verbatim when migrating legacy issues.
|
||||
|
||||
41
docs/MVP_ACCEPTANCE.md
Normal file
41
docs/MVP_ACCEPTANCE.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# MVP Acceptance Checklist
|
||||
|
||||
## API
|
||||
|
||||
- [x] `GET /v1/health` returns version/build/uptime and active sprite pack.
|
||||
- [x] `GET /v1/state` requires bearer token.
|
||||
- [x] `POST /v1/command` requires bearer token and returns `202` for valid command.
|
||||
- [x] `POST /v1/commands` accepts batch in-order.
|
||||
- [x] malformed JSON returns `400`, server remains alive.
|
||||
|
||||
## Command Pipeline
|
||||
|
||||
- [x] duplicate command IDs are ignored within dedupe window.
|
||||
- [x] `SetState` updates state and default animation mapping.
|
||||
- [x] transient state with `ttl_ms` returns to durable state.
|
||||
- [x] `SetTransform` persists x/y/scale.
|
||||
- [x] `SetFlags` persists click-through/always-on-top/visible.
|
||||
|
||||
## Config
|
||||
|
||||
- [x] first run bootstraps `config.toml`.
|
||||
- [x] `auth_token` generated and reused across restarts.
|
||||
|
||||
## Sprite
|
||||
|
||||
- [x] selected sprite pack loads when present.
|
||||
- [x] missing selected pack falls back to `default`.
|
||||
- [x] runtime `SetSpritePack` successfully switches pack without restart.
|
||||
- [x] failed runtime `SetSpritePack` keeps current visuals and reports error in state snapshot.
|
||||
|
||||
## Platform
|
||||
|
||||
- [x] capability flags are exposed in `/v1/health`.
|
||||
- [x] non-supported platform operations do not crash the process.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `cargo test --workspace` passes.
|
||||
- API auth coverage exists in `crates/sprimo-api/src/lib.rs` tests.
|
||||
- Config bootstrap and roundtrip coverage exists in `crates/sprimo-config/src/lib.rs` tests.
|
||||
- Sprite fallback coverage exists in `crates/sprimo-sprite/src/lib.rs` tests.
|
||||
17
docs/PLATFORM_CAPABILITY_MATRIX.md
Normal file
17
docs/PLATFORM_CAPABILITY_MATRIX.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Platform Capability Matrix (MVP)
|
||||
|
||||
Date: 2026-02-12
|
||||
|
||||
| Capability | Windows | Linux X11 | Linux Wayland | macOS |
|
||||
|------------|---------|-----------|---------------|-------|
|
||||
| `supports_click_through` | true (implemented) | false (current) | false | false (current) |
|
||||
| `supports_transparency` | true | true | true | true |
|
||||
| `supports_tray` | false (current) | false (current) | false (current) | false (current) |
|
||||
| `supports_global_hotkey` | true (implemented) | false (current) | false (current) | false (current) |
|
||||
| `supports_skip_taskbar` | true (target) | false (current) | false (current) | false (current) |
|
||||
|
||||
## Notes
|
||||
|
||||
- Current code applies real Win32 operations for click-through, visibility, top-most, and positioning.
|
||||
- Non-Windows targets currently use a no-op adapter with conservative flags.
|
||||
- Wayland limitations remain an expected degradation in v0.1.
|
||||
38
docs/QA_EVIDENCE_TEMPLATE.md
Normal file
38
docs/QA_EVIDENCE_TEMPLATE.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# QA Evidence Template
|
||||
|
||||
Use this block inside `issues/issueN.md` under `## Verification`.
|
||||
|
||||
```md
|
||||
### Commands Run
|
||||
|
||||
- [ ] `cargo check --workspace`
|
||||
- [ ] `cargo test --workspace`
|
||||
- [ ] `just qa-validate`
|
||||
|
||||
### Command Output Summary
|
||||
|
||||
- `cargo check --workspace`: pass/fail, key notes
|
||||
- `cargo test --workspace`: pass/fail, key notes
|
||||
- `just qa-validate`: pass/fail, key notes
|
||||
|
||||
### Screenshots
|
||||
|
||||
- Before:
|
||||
- `issues/screenshots/issueN-before-YYYYMMDD-HHMMSS.png`
|
||||
- After:
|
||||
- `issues/screenshots/issueN-after-YYYYMMDD-HHMMSS.png`
|
||||
|
||||
### Visual Assertions
|
||||
|
||||
- [ ] Overlay is transparent where expected
|
||||
- [ ] Window size/placement behavior matches expected result
|
||||
- [ ] Sprite/background alpha behavior matches expected result
|
||||
|
||||
### Reviewer Record
|
||||
|
||||
- Date:
|
||||
- Verified by:
|
||||
- Final result:
|
||||
- Notes:
|
||||
```
|
||||
|
||||
85
docs/QA_WORKFLOW.md
Normal file
85
docs/QA_WORKFLOW.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Sprimo QA and Documentation Workflow
|
||||
|
||||
## Purpose
|
||||
|
||||
This workflow defines how bug reports, runtime evidence, and documentation updates are
|
||||
maintained so every issue is auditable from first report to closure.
|
||||
|
||||
## Issue Lifecycle
|
||||
|
||||
All issues use a single file: `issues/issueN.md`.
|
||||
|
||||
Allowed lifecycle states:
|
||||
|
||||
1. `Reported`
|
||||
2. `Triaged`
|
||||
3. `In Progress`
|
||||
4. `Fix Implemented`
|
||||
5. `Verification Passed`
|
||||
6. `Closed`
|
||||
|
||||
Each state transition must include:
|
||||
|
||||
- local timestamp (`YYYY-MM-DD HH:MM`)
|
||||
- actor
|
||||
- short note
|
||||
- evidence links when available
|
||||
|
||||
## Evidence Requirements
|
||||
|
||||
Screenshots are stored under `issues/screenshots/`.
|
||||
|
||||
Naming convention:
|
||||
|
||||
- before: `issueN-before-YYYYMMDD-HHMMSS.png`
|
||||
- after: `issueN-after-YYYYMMDD-HHMMSS.png`
|
||||
- optional checkpoint suffix: `-step1`, `-step2`
|
||||
|
||||
For UI/runtime behavior bugs:
|
||||
|
||||
- at least one before screenshot is required at report/triage time
|
||||
- at least one after screenshot is required before `Verification Passed` or `Closed`
|
||||
|
||||
Legacy reports may keep `issues/screenshots/issueN.png` as before evidence.
|
||||
|
||||
## Verification Gate
|
||||
|
||||
Before marking an issue `Closed`, all conditions must be met:
|
||||
|
||||
1. Relevant checks/tests are recorded:
|
||||
- `cargo check --workspace`
|
||||
- `cargo test --workspace` (or documented rationale for targeted tests)
|
||||
2. Visual checklist is completed with before/after screenshot references.
|
||||
3. Docs are synchronized:
|
||||
- `issues/issueN.md` lifecycle and verification sections updated
|
||||
- impacted files under `docs/` updated when behavior/spec/status changed
|
||||
|
||||
## Documentation Sync Rules
|
||||
|
||||
Update these files when applicable:
|
||||
|
||||
- `docs/IMPLEMENTATION_STATUS.md` for milestone-level implementation status
|
||||
- `docs/RELEASE_TESTING.md` when release/manual QA steps change
|
||||
- `docs/API_SPEC.md`, `docs/CONFIG_REFERENCE.md`, or other contracts if behavior changed
|
||||
|
||||
## Command Checklist
|
||||
|
||||
Minimum command set for fix validation:
|
||||
|
||||
```powershell
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
just qa-validate
|
||||
```
|
||||
|
||||
For runtime behavior issues, include screenshot capture paths in the issue file.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
An issue is done only when:
|
||||
|
||||
- lifecycle state is `Closed`
|
||||
- verification gate passed
|
||||
- evidence links resolve to files in repository
|
||||
- `just qa-validate` passes
|
||||
|
||||
72
docs/RELEASE_TESTING.md
Normal file
72
docs/RELEASE_TESTING.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Release Packaging and Behavior Testing (Windows)
|
||||
|
||||
## Artifact Layout
|
||||
|
||||
Current release package type: portable ZIP.
|
||||
|
||||
Expected contents:
|
||||
|
||||
- `sprimo-app.exe`
|
||||
- `assets/sprite-packs/default/manifest.json`
|
||||
- `assets/sprite-packs/default/sprite.png`
|
||||
- `README.txt`
|
||||
|
||||
Generated outputs:
|
||||
|
||||
- `dist/sprimo-windows-x64-v<version>.zip`
|
||||
- `dist/sprimo-windows-x64-v<version>.zip.sha256`
|
||||
|
||||
## Commands
|
||||
|
||||
Use `just` for command entry:
|
||||
|
||||
```powershell
|
||||
just check
|
||||
just test
|
||||
just build-release
|
||||
just package-win
|
||||
just smoke-win
|
||||
```
|
||||
|
||||
`just package-win` calls `scripts/package_windows.py package`.
|
||||
`just smoke-win` calls `scripts/package_windows.py smoke`.
|
||||
|
||||
## Behavior Test Checklist (Packaged App)
|
||||
|
||||
Run tests from an unpacked ZIP folder, not from the workspace run.
|
||||
|
||||
1. Launch `sprimo-app.exe`; verify default sprite renders.
|
||||
2. Verify no terminal window appears when launching release build by double-click.
|
||||
3. Verify global hotkey recovery (`Ctrl+Alt+P`) forces interactive mode.
|
||||
4. Verify click-through and always-on-top toggles via API commands.
|
||||
5. Verify `/v1/health` and `/v1/state` behavior with auth.
|
||||
6. Verify `SetSpritePack`:
|
||||
- valid pack switches runtime visuals
|
||||
- invalid pack keeps current visuals and sets `last_error`
|
||||
7. Restart app and verify persisted config behavior.
|
||||
8. Confirm overlay background is transparent (desktop visible behind non-sprite pixels).
|
||||
9. Confirm no magenta matte remains around sprite in default pack.
|
||||
|
||||
## Test Log Template
|
||||
|
||||
- Date:
|
||||
- Artifact:
|
||||
- Commit:
|
||||
- Tester:
|
||||
- Result:
|
||||
- Notes:
|
||||
|
||||
## Issue Evidence Gate
|
||||
|
||||
Before release sign-off for a bug fix:
|
||||
|
||||
1. Link the issue file (`issues/issueN.md`) in the test log.
|
||||
2. Ensure before/after screenshot evidence is referenced from the issue:
|
||||
- before: `issues/screenshots/issueN-before-YYYYMMDD-HHMMSS.png`
|
||||
- after: `issues/screenshots/issueN-after-YYYYMMDD-HHMMSS.png`
|
||||
3. Record verification commands and outcomes in the issue:
|
||||
- `cargo check --workspace`
|
||||
- `cargo test --workspace`
|
||||
- `just qa-validate`
|
||||
|
||||
Legacy issues may reference `issues/screenshots/issueN.png` as before evidence.
|
||||
66
docs/SPRITE_PACK_SCHEMA.md
Normal file
66
docs/SPRITE_PACK_SCHEMA.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Sprite Pack Manifest Schema (MVP)
|
||||
|
||||
Path: `<pack_dir>/manifest.json`
|
||||
|
||||
## Required Fields
|
||||
|
||||
- `id` (string): unique sprite pack ID.
|
||||
- `version` (string): schema/pack version label.
|
||||
- `image` (string): atlas image filename, e.g. `atlas.png`.
|
||||
- `frame_width` (u32): frame width in pixels.
|
||||
- `frame_height` (u32): frame height in pixels.
|
||||
- `animations` (array): list of animation definitions.
|
||||
- `anchor` (object): sprite anchor/pivot.
|
||||
|
||||
## Animation Definition
|
||||
|
||||
- `name` (string)
|
||||
- `fps` (u16)
|
||||
- `frames` (array of u32 frame indices)
|
||||
- `one_shot` (optional bool)
|
||||
|
||||
## Anchor Object
|
||||
|
||||
- `x` (f32)
|
||||
- `y` (f32)
|
||||
|
||||
## Example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "default",
|
||||
"version": "1",
|
||||
"image": "atlas.png",
|
||||
"frame_width": 64,
|
||||
"frame_height": 64,
|
||||
"animations": [
|
||||
{ "name": "idle", "fps": 8, "frames": [0, 1, 2, 3] },
|
||||
{ "name": "success", "fps": 12, "frames": [20, 21, 22], "one_shot": true }
|
||||
],
|
||||
"anchor": { "x": 0.5, "y": 1.0 }
|
||||
}
|
||||
```
|
||||
|
||||
## Runtime Fallback Behavior
|
||||
|
||||
- The selected pack is attempted first.
|
||||
- If selected pack is missing/invalid, frontend falls back to `default`.
|
||||
- If `default` is missing/invalid, loading fails with error.
|
||||
|
||||
## Alpha Handling
|
||||
|
||||
- If the source image already has an alpha channel, runtime uses it directly.
|
||||
- If the source image is RGB-only, runtime can apply chroma-key conversion:
|
||||
- default key color: `#FF00FF`
|
||||
- matching pixels become transparent.
|
||||
- Manifest optional fields:
|
||||
- `chroma_key_color` (string `#RRGGBB`)
|
||||
- `chroma_key_enabled` (bool)
|
||||
|
||||
## Grid Derivation Rule
|
||||
|
||||
- Runtime derives atlas grid from image dimensions:
|
||||
- `columns = image_width / frame_width`
|
||||
- `rows = image_height / frame_height`
|
||||
- Image dimensions must be divisible by frame dimensions.
|
||||
- Every animation frame index must be `< columns * rows`.
|
||||
235
issues/issue1.md
Normal file
235
issues/issue1.md
Normal file
@@ -0,0 +1,235 @@
|
||||
## Title
|
||||
|
||||
Windows overlay renders as large opaque window; background not transparent;
|
||||
sprite shows magenta matte; window is not pet-sized.
|
||||
|
||||
## Severity
|
||||
|
||||
P0
|
||||
|
||||
## Environment
|
||||
|
||||
- OS: Windows
|
||||
- App: `sprimo-app` frontend runtime
|
||||
- Reported on: 2026-02-12
|
||||
- Evidence screenshot: `issues/screenshots/issue1.png`
|
||||
|
||||
## Summary
|
||||
|
||||
Current runtime output violates overlay requirements:
|
||||
|
||||
- opaque dark background is visible behind the pet
|
||||
- sprite keeps magenta matte background
|
||||
- window footprint is larger than expected pet footprint
|
||||
|
||||
## Reproduction Steps
|
||||
|
||||
1. Launch the frontend executable on Windows.
|
||||
2. Observe the initial overlay window at startup.
|
||||
|
||||
## Expected Result
|
||||
|
||||
- Window appears borderless and transparent.
|
||||
- Only pet pixels are visible.
|
||||
- Sprite background uses alpha (no magenta matte).
|
||||
- Window size is constrained to sprite bounds plus small margin.
|
||||
|
||||
## Actual Result
|
||||
|
||||
- Window background appears opaque dark gray.
|
||||
- Sprite contains magenta matte rectangle.
|
||||
- Window appears larger than a pet-sized overlay.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
Current findings from repository inspection:
|
||||
|
||||
- The default sprite is RGB (`Format24bppRgb`) and depends on runtime chroma-key conversion.
|
||||
- Window and clear-color transparency settings were configured, but layered-window attributes
|
||||
were not explicitly applied during window-handle attachment.
|
||||
- `SetTransform.x/y` was applied to both window coordinates and sprite world translation, causing
|
||||
visible offset within the client area and making the window appear larger than pet bounds.
|
||||
- Chroma-key conversion used exact color matching only, which is fragile for near-magenta pixels.
|
||||
|
||||
## Fix Plan
|
||||
|
||||
1. Reproduce with runtime logs enabled and capture an explicit before screenshot.
|
||||
2. Validate chroma-key conversion path against loaded texture content.
|
||||
3. Validate Windows composition and layered style behavior with attached HWND.
|
||||
4. Apply targeted renderer/platform fixes.
|
||||
5. Capture after screenshot and rerun verification checklist.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Implemented code changes:
|
||||
|
||||
1. `crates/sprimo-app/src/main.rs`
|
||||
- Sprite now spawns centered at `(0, 0, 0)`; persisted `x/y` remains window placement only.
|
||||
- `SetTransform.x/y` no longer mutates sprite world translation.
|
||||
- Chroma-key conversion now:
|
||||
- uses tolerance matching around `#FF00FF`,
|
||||
- preserves non-key alpha values,
|
||||
- forces chroma-key when an alpha-bearing image is fully opaque but key-colored in large areas.
|
||||
- Added regression tests for tolerant keying, alpha preservation, and force-key detection.
|
||||
|
||||
2. `crates/sprimo-platform/src/lib.rs`
|
||||
- Windows adapter now applies layered style + `SetLayeredWindowAttributes` when window handle is
|
||||
attached.
|
||||
- Click-through toggles now reuse the layered-style application path to keep transparency setup
|
||||
stable.
|
||||
|
||||
3. Additional Windows window configuration hardening (`crates/sprimo-app/src/main.rs`)
|
||||
- Primary window now explicitly sets `composite_alpha_mode = PreMultiplied`.
|
||||
- Primary window now starts with a small explicit resolution (`544x544`) to avoid large default
|
||||
client area before runtime pack sizing applies.
|
||||
|
||||
4. Follow-up rollback + keying adjustment after new screenshot review
|
||||
- Removed explicit `composite_alpha_mode` override to return control to Bevy/WGPU default.
|
||||
- Removed forced layered alpha attributes from Windows adapter to avoid opaque black composition.
|
||||
- Increased chroma-key tolerance to better remove magenta fringe around sprite edges.
|
||||
|
||||
5. Windows compositor fallback for persistent opaque background
|
||||
- Added Windows color-key transparency fallback:
|
||||
- sets `WS_EX_LAYERED` on attached HWND
|
||||
- applies `SetLayeredWindowAttributes(..., LWA_COLORKEY)` with key color `#01FE03`
|
||||
- Switched Windows clear color to the same key color so non-sprite background can be cut out by
|
||||
the OS compositor even when swapchain alpha blending is not honored.
|
||||
|
||||
6. Windows mode switch to avoid swapchain-alpha path conflicts
|
||||
- On Windows, primary window now disables Bevy transparent swapchain mode
|
||||
(`transparent = false`) and relies on layered color-key composition only.
|
||||
- Non-Windows behavior remains unchanged (`transparent = true` + alpha clear).
|
||||
|
||||
7. DWM composition strategy after transparent color-key regression
|
||||
- Removed Windows color-key compositor strategy (it hid sprite on tested path).
|
||||
- Added DWM composition setup on HWND attach:
|
||||
- `DwmIsCompositionEnabled`
|
||||
- `DwmExtendFrameIntoClientArea` with full glass margins
|
||||
- `DwmEnableBlurBehindWindow`
|
||||
- Restored Windows render path to alpha clear + `transparent = true`.
|
||||
|
||||
8. Renderer alpha mode correction pass
|
||||
- Removed DWM composition overrides to avoid conflicting transparency mechanisms.
|
||||
- Forced Bevy window `CompositeAlphaMode::PostMultiplied` on Windows to align with transparent
|
||||
swapchain blending expectations.
|
||||
- Kept native alpha clear color render path.
|
||||
|
||||
9. Hardware-safe Windows fallback after runtime panic
|
||||
- Runtime log showed wgpu surface supports alpha modes `[Opaque]` only on this machine.
|
||||
- Switched Windows path to opaque swapchain (`transparent = false`) plus Win32 layered
|
||||
color-key transparency (`WS_EX_LAYERED + LWA_COLORKEY`).
|
||||
- Aligned Windows clear color to the same key color (`#FF00FF`) and removed crash-causing
|
||||
post-multiplied alpha request.
|
||||
|
||||
10. Color-key visibility fix for opaque presentation path
|
||||
- In Windows colorkey mode, keyed pixels are now emitted with alpha `255` (not `0`) so key RGB
|
||||
survives the presentation path and can be matched by `LWA_COLORKEY`.
|
||||
- Updated chroma-key unit tests for platform-specific keyed-alpha expectations.
|
||||
|
||||
## Verification
|
||||
|
||||
### Commands Run
|
||||
|
||||
- [x] `cargo check --workspace`
|
||||
- [x] `cargo test --workspace`
|
||||
- [x] `just qa-validate`
|
||||
|
||||
### Screenshots
|
||||
|
||||
- Before:
|
||||
- `issues/screenshots/issue1.png` (legacy baseline)
|
||||
- After:
|
||||
- pending (capture required before `Verification Passed`/`Closed`)
|
||||
|
||||
### Command Output Summary
|
||||
|
||||
- `cargo check --workspace`: pass
|
||||
- `cargo test --workspace`: pass
|
||||
- `just qa-validate`: pass
|
||||
- `cargo check -p sprimo-app -p sprimo-platform`: pass
|
||||
- `cargo test -p sprimo-app`: pass
|
||||
- dist runtime binary refreshed from latest debug build: done
|
||||
- Follow-up rebuild + dist binary refresh after screenshot-guided rollback: done
|
||||
- Windows color-key fallback build + dist binary refresh: done
|
||||
- Windows transparent-mode switch build + dist binary refresh: done
|
||||
- DWM composition fallback build + dist binary refresh: done
|
||||
- Post-multiplied alpha mode build + dist binary refresh: done
|
||||
- Opaque+colorkey fallback build + dist binary refresh: done
|
||||
- Keyed-alpha colorkey build + dist binary refresh: done
|
||||
|
||||
### Result
|
||||
|
||||
- Current Status: `Fix Implemented`
|
||||
- Notes: code fix implemented and validated by tests; runtime after screenshot still pending.
|
||||
|
||||
## Status History
|
||||
|
||||
- `2026-02-12 20:15` - reporter - `Reported` - initial bug report with screenshot.
|
||||
- `2026-02-12 20:35` - codex - `Triaged` - validated screenshot symptoms and repository context.
|
||||
- `2026-02-12 20:50` - codex - `Reported` - lifecycle file normalized; fix not yet applied.
|
||||
- `2026-02-12 21:20` - codex - `In Progress` - implemented window/transparency/chroma-key fixes.
|
||||
- `2026-02-12 21:30` - codex - `Fix Implemented` - workspace checks/tests and QA validator passed.
|
||||
- `2026-02-12 22:10` - codex - `In Progress` - added explicit Windows composite alpha mode and startup resolution.
|
||||
- `2026-02-12 22:15` - codex - `Fix Implemented` - app/platform targeted checks passed and dist binary refreshed.
|
||||
- `2026-02-12 22:35` - codex - `In Progress` - analyzed new after screenshot showing black opaque background and magenta fringe.
|
||||
- `2026-02-12 22:45` - codex - `Fix Implemented` - rolled back layered alpha/composite override and tightened chroma-key edge removal.
|
||||
- `2026-02-12 22:55` - codex - `In Progress` - analyzed latest screenshot still showing opaque black background.
|
||||
- `2026-02-12 23:05` - codex - `Fix Implemented` - added Windows color-key compositor fallback and aligned clear color.
|
||||
- `2026-02-12 23:15` - codex - `In Progress` - analyzed latest screenshot showing color-key path still ineffective with swapchain transparency.
|
||||
- `2026-02-12 23:22` - codex - `Fix Implemented` - switched Windows to non-transparent swapchain mode and kept layered color-key compositor path.
|
||||
- `2026-02-12 23:30` - codex - `In Progress` - analyzed screenshot where window became effectively empty/over-transparent.
|
||||
- `2026-02-12 23:42` - codex - `Fix Implemented` - removed color-key strategy and switched to DWM composition setup.
|
||||
- `2026-02-12 23:55` - codex - `In Progress` - analyzed screenshot where black background persisted under DWM strategy.
|
||||
- `2026-02-13 00:05` - codex - `Fix Implemented` - removed DWM overrides and forced post-multiplied alpha mode on Windows.
|
||||
- `2026-02-13 00:35` - codex - `In Progress` - reproduced startup panic from runtime log (`alpha modes: [Opaque]`).
|
||||
- `2026-02-13 00:45` - codex - `Fix Implemented` - replaced transparent swapchain path with Windows opaque+colorkey fallback; startup panic no longer reproduced.
|
||||
- `2026-02-13 00:55` - codex - `In Progress` - analyzed persistent black background under opaque+colorkey path.
|
||||
- `2026-02-13 01:05` - codex - `Fix Implemented` - changed keyed-pixel alpha to 255 in Windows colorkey mode and updated tests.
|
||||
|
||||
## Closure
|
||||
|
||||
- Current Status: `Fix Implemented`
|
||||
- Close Date:
|
||||
- Owner:
|
||||
- Linked PR/commit:
|
||||
|
||||
## Original Report (2026-02-12, normalized from legacy encoding)
|
||||
|
||||
### Bug Report / Debugging Issue (Windows)
|
||||
|
||||
**Title:** Windows overlay renders as large opaque window; background not transparent; sprite
|
||||
shows magenta matte; window not pet-sized.
|
||||
|
||||
**Severity:** P0 (core functionality broken; not an overlay pet)
|
||||
|
||||
**Component:** Frontend (Bevy renderer / windowing / transparency pipeline)
|
||||
|
||||
**Summary:** The app shows a large opaque dark-gray window with a character sprite on top. The
|
||||
sprite includes a magenta rectangular matte. This violates transparent borderless overlay
|
||||
requirements.
|
||||
|
||||
**Evidence from screenshot (`issues/screenshots/issue1.png`):**
|
||||
|
||||
1. Opaque background fills most of the window.
|
||||
2. Window appears much larger than pet-only bounds.
|
||||
3. Magenta matte appears behind the sprite.
|
||||
|
||||
**Expected:**
|
||||
|
||||
- Borderless transparent overlay.
|
||||
- Pet-only visible pixels.
|
||||
- No magenta matte.
|
||||
|
||||
**Actual:**
|
||||
|
||||
- Opaque dark window.
|
||||
- Magenta sprite background.
|
||||
- Regular app-window behavior.
|
||||
|
||||
**Likely causes in original report:**
|
||||
|
||||
1. Missing OS-level transparent/layered window behavior.
|
||||
2. Non-transparent clear color or camera clear.
|
||||
3. Sprite asset alpha issue (RGB or wrong export).
|
||||
4. Missing chroma-key discard path.
|
||||
5. Missing pet-sized window sizing logic.
|
||||
21
justfile
Normal file
21
justfile
Normal file
@@ -0,0 +1,21 @@
|
||||
set shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||
|
||||
python := "python"
|
||||
|
||||
check:
|
||||
cargo check --workspace
|
||||
|
||||
test:
|
||||
cargo test --workspace
|
||||
|
||||
build-release:
|
||||
cargo build --release -p sprimo-app
|
||||
|
||||
package-win:
|
||||
{{python}} scripts/package_windows.py package
|
||||
|
||||
smoke-win:
|
||||
{{python}} scripts/package_windows.py smoke
|
||||
|
||||
qa-validate:
|
||||
{{python}} scripts/qa_validate.py
|
||||
197
scripts/package_windows.py
Normal file
197
scripts/package_windows.py
Normal file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and package a portable Windows ZIP for sprimo-app."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DIST = ROOT / "dist"
|
||||
BIN_REL = ROOT / "target" / "release" / "sprimo-app.exe"
|
||||
ASSETS_REL = ROOT / "assets"
|
||||
|
||||
|
||||
class PackagingError(RuntimeError):
|
||||
"""Raised when packaging preconditions are not met."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageLayout:
|
||||
version: str
|
||||
zip_path: Path
|
||||
checksum_path: Path
|
||||
|
||||
|
||||
def run(cmd: list[str], cwd: Path = ROOT) -> None:
|
||||
result = subprocess.run(cmd, cwd=cwd, check=False)
|
||||
if result.returncode != 0:
|
||||
raise PackagingError(f"command failed ({result.returncode}): {' '.join(cmd)}")
|
||||
|
||||
|
||||
def read_version() -> str:
|
||||
cmd = ["cargo", "metadata", "--format-version", "1", "--no-deps"]
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
cwd=ROOT,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise PackagingError(f"failed to read cargo metadata: {proc.stderr.strip()}")
|
||||
|
||||
data = json.loads(proc.stdout)
|
||||
root_pkg = data.get("workspace_root")
|
||||
if not root_pkg:
|
||||
raise PackagingError("workspace metadata missing workspace_root")
|
||||
|
||||
# Prefer workspace package version.
|
||||
manifest_path = ROOT / "Cargo.toml"
|
||||
manifest = manifest_path.read_text(encoding="utf-8")
|
||||
marker = "version = "
|
||||
for line in manifest.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith(marker):
|
||||
return stripped.split("=", 1)[1].strip().strip('"')
|
||||
|
||||
# Fallback to first package version from metadata.
|
||||
packages = data.get("packages", [])
|
||||
if packages:
|
||||
return packages[0]["version"]
|
||||
raise PackagingError("could not determine version")
|
||||
|
||||
|
||||
def ensure_release_binary() -> Path:
|
||||
if not BIN_REL.exists():
|
||||
run(["cargo", "build", "--release", "-p", "sprimo-app"])
|
||||
if not BIN_REL.exists():
|
||||
raise PackagingError(f"release binary missing: {BIN_REL}")
|
||||
return BIN_REL
|
||||
|
||||
|
||||
def ensure_assets() -> None:
|
||||
required = [
|
||||
ASSETS_REL / "sprite-packs" / "default" / "manifest.json",
|
||||
ASSETS_REL / "sprite-packs" / "default" / "sprite.png",
|
||||
]
|
||||
missing = [path for path in required if not path.exists()]
|
||||
if missing:
|
||||
joined = ", ".join(str(path) for path in missing)
|
||||
raise PackagingError(f"required assets missing: {joined}")
|
||||
|
||||
|
||||
def iter_stage_files(stage_root: Path) -> Iterable[Path]:
|
||||
for path in stage_root.rglob("*"):
|
||||
if path.is_file():
|
||||
yield path
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
while True:
|
||||
chunk = handle.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def package() -> PackageLayout:
|
||||
version = read_version()
|
||||
ensure_assets()
|
||||
binary = ensure_release_binary()
|
||||
|
||||
DIST.mkdir(parents=True, exist_ok=True)
|
||||
artifact_name = f"sprimo-windows-x64-v{version}"
|
||||
zip_path = DIST / f"{artifact_name}.zip"
|
||||
checksum_path = DIST / f"{artifact_name}.zip.sha256"
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="sprimo-package-") as temp_dir:
|
||||
stage = Path(temp_dir) / artifact_name
|
||||
stage.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
shutil.copy2(binary, stage / "sprimo-app.exe")
|
||||
shutil.copytree(ASSETS_REL, stage / "assets", dirs_exist_ok=True)
|
||||
|
||||
readme = stage / "README.txt"
|
||||
readme.write_text(
|
||||
"Sprimo portable package\n"
|
||||
"Run: sprimo-app.exe\n"
|
||||
"Assets are expected at ./assets relative to the executable.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
archive_base = DIST / artifact_name
|
||||
if zip_path.exists():
|
||||
zip_path.unlink()
|
||||
if checksum_path.exists():
|
||||
checksum_path.unlink()
|
||||
|
||||
shutil.make_archive(str(archive_base), "zip", root_dir=stage.parent, base_dir=stage.name)
|
||||
|
||||
checksum = sha256_file(zip_path)
|
||||
checksum_path.write_text(f"{checksum} {zip_path.name}\n", encoding="utf-8")
|
||||
return PackageLayout(version=version, zip_path=zip_path, checksum_path=checksum_path)
|
||||
|
||||
|
||||
def smoke() -> None:
|
||||
layout = package()
|
||||
print(f"package created: {layout.zip_path}")
|
||||
print(f"checksum file: {layout.checksum_path}")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="sprimo-smoke-") as temp_dir:
|
||||
unzip_dir = Path(temp_dir) / "unpacked"
|
||||
shutil.unpack_archive(layout.zip_path, unzip_dir)
|
||||
root_candidates = [p for p in unzip_dir.iterdir() if p.is_dir()]
|
||||
if not root_candidates:
|
||||
raise PackagingError("smoke failed: package root directory missing")
|
||||
pkg_root = root_candidates[0]
|
||||
|
||||
required = [
|
||||
pkg_root / "sprimo-app.exe",
|
||||
pkg_root / "assets" / "sprite-packs" / "default" / "manifest.json",
|
||||
pkg_root / "assets" / "sprite-packs" / "default" / "sprite.png",
|
||||
]
|
||||
missing = [path for path in required if not path.exists()]
|
||||
if missing:
|
||||
joined = ", ".join(str(path) for path in missing)
|
||||
raise PackagingError(f"smoke failed: expected files missing: {joined}")
|
||||
|
||||
print("smoke check passed: package structure is valid")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Sprimo Windows packaging helper")
|
||||
parser.add_argument("command", choices=["package", "smoke"], help="action to execute")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
try:
|
||||
if args.command == "package":
|
||||
layout = package()
|
||||
print(f"created: {layout.zip_path}")
|
||||
print(f"sha256: {layout.checksum_path}")
|
||||
else:
|
||||
smoke()
|
||||
return 0
|
||||
except PackagingError as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
127
scripts/qa_validate.py
Normal file
127
scripts/qa_validate.py
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate issue QA workflow artifacts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ISSUES_DIR = ROOT / "issues"
|
||||
|
||||
REQUIRED_SECTIONS = (
|
||||
"Title",
|
||||
"Severity",
|
||||
"Environment",
|
||||
"Summary",
|
||||
"Reproduction Steps",
|
||||
"Expected Result",
|
||||
"Actual Result",
|
||||
"Root Cause Analysis",
|
||||
"Fix Plan",
|
||||
"Implementation Notes",
|
||||
"Verification",
|
||||
"Status History",
|
||||
"Closure",
|
||||
)
|
||||
|
||||
CLOSED_STATES = {"Verification Passed", "Closed"}
|
||||
|
||||
|
||||
def issue_files() -> list[Path]:
|
||||
return sorted(ISSUES_DIR.glob("issue[0-9]*.md"))
|
||||
|
||||
|
||||
def has_section(content: str, name: str) -> bool:
|
||||
pattern = rf"(?m)^##\s+{re.escape(name)}\s*$"
|
||||
return bool(re.search(pattern, content))
|
||||
|
||||
|
||||
def current_status(content: str) -> str | None:
|
||||
match = re.search(r"(?m)^- Current Status:\s*`?([^`\n]+)`?\s*$", content)
|
||||
return match.group(1).strip() if match else None
|
||||
|
||||
|
||||
def screenshot_refs(content: str) -> list[str]:
|
||||
pattern = r"issues/screenshots/[A-Za-z0-9._-]+\.png"
|
||||
return sorted(set(re.findall(pattern, content)))
|
||||
|
||||
|
||||
def has_before_ref(refs: list[str], issue_num: str) -> bool:
|
||||
legacy = f"issues/screenshots/issue{issue_num}.png"
|
||||
return any("-before-" in ref for ref in refs) or legacy in refs
|
||||
|
||||
|
||||
def has_after_ref(refs: list[str]) -> bool:
|
||||
return any("-after-" in ref for ref in refs)
|
||||
|
||||
|
||||
def command_check_present(content: str) -> bool:
|
||||
required = (
|
||||
"`cargo check --workspace`",
|
||||
"`cargo test --workspace`",
|
||||
"`just qa-validate`",
|
||||
)
|
||||
return all(token in content for token in required)
|
||||
|
||||
|
||||
def verify_issue(path: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
content = path.read_text(encoding="utf-8")
|
||||
issue_num_match = re.search(r"issue([0-9]+)\.md$", path.name)
|
||||
issue_num = issue_num_match.group(1) if issue_num_match else "?"
|
||||
|
||||
for section in REQUIRED_SECTIONS:
|
||||
if not has_section(content, section):
|
||||
errors.append(f"{path}: missing section '## {section}'")
|
||||
|
||||
refs = screenshot_refs(content)
|
||||
for ref in refs:
|
||||
if not (ROOT / ref).exists():
|
||||
errors.append(f"{path}: missing screenshot file: {ref}")
|
||||
|
||||
if not has_before_ref(refs, issue_num):
|
||||
errors.append(
|
||||
f"{path}: missing before screenshot reference "
|
||||
"(use issueN-before-...png or legacy issueN.png)"
|
||||
)
|
||||
|
||||
status = current_status(content)
|
||||
if status in CLOSED_STATES and not has_after_ref(refs):
|
||||
errors.append(
|
||||
f"{path}: status '{status}' requires at least one after screenshot reference"
|
||||
)
|
||||
|
||||
if status in CLOSED_STATES and not command_check_present(content):
|
||||
errors.append(
|
||||
f"{path}: status '{status}' requires command checklist entries for "
|
||||
"cargo check, cargo test, and just qa-validate"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
files = issue_files()
|
||||
if not files:
|
||||
print("error: no issue files found under issues/", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
failures: list[str] = []
|
||||
for path in files:
|
||||
failures.extend(verify_issue(path))
|
||||
|
||||
if failures:
|
||||
for failure in failures:
|
||||
print(f"error: {failure}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"qa validation passed ({len(files)} issue file(s) checked)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
95
skills/coding-guidelines/SKILL.md
Normal file
95
skills/coding-guidelines/SKILL.md
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: coding-guidelines
|
||||
description: "Use when asking about Rust code style or best practices. Keywords: naming, formatting, comment, clippy, rustfmt, lint, code style, best practice, P.NAM, G.FMT, code review, naming convention, variable naming, function naming, type naming, 命名规范, 代码风格, 格式化, 最佳实践, 代码审查, 怎么命名"
|
||||
source: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Rust Coding Guidelines (50 Core Rules)
|
||||
|
||||
## Naming (Rust-Specific)
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| No `get_` prefix | `fn name()` not `fn get_name()` |
|
||||
| Iterator convention | `iter()` / `iter_mut()` / `into_iter()` |
|
||||
| Conversion naming | `as_` (cheap &), `to_` (expensive), `into_` (ownership) |
|
||||
| Static var prefix | `G_CONFIG` for `static`, no prefix for `const` |
|
||||
|
||||
## Data Types
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Use newtypes | `struct Email(String)` for domain semantics |
|
||||
| Prefer slice patterns | `if let [first, .., last] = slice` |
|
||||
| Pre-allocate | `Vec::with_capacity()`, `String::with_capacity()` |
|
||||
| Avoid Vec abuse | Use arrays for fixed sizes |
|
||||
|
||||
## Strings
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Prefer bytes | `s.bytes()` over `s.chars()` when ASCII |
|
||||
| Use `Cow<str>` | When might modify borrowed data |
|
||||
| Use `format!` | Over string concatenation with `+` |
|
||||
| Avoid nested iteration | `contains()` on string is O(n*m) |
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Use `?` propagation | Not `try!()` macro |
|
||||
| `expect()` over `unwrap()` | When value guaranteed |
|
||||
| Assertions for invariants | `assert!` at function entry |
|
||||
|
||||
## Memory
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Meaningful lifetimes | `'src`, `'ctx` not just `'a` |
|
||||
| `try_borrow()` for RefCell | Avoid panic |
|
||||
| Shadowing for transformation | `let x = x.parse()?` |
|
||||
|
||||
## Concurrency
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Identify lock ordering | Prevent deadlocks |
|
||||
| Atomics for primitives | Not Mutex for bool/usize |
|
||||
| Choose memory order carefully | Relaxed/Acquire/Release/SeqCst |
|
||||
|
||||
## Async
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Sync for CPU-bound | Async is for I/O |
|
||||
| Don't hold locks across await | Use scoped guards |
|
||||
|
||||
## Macros
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Avoid unless necessary | Prefer functions/generics |
|
||||
| Follow Rust syntax | Macro input should look like Rust |
|
||||
|
||||
## Deprecated → Better
|
||||
|
||||
| Deprecated | Better | Since |
|
||||
|------------|--------|-------|
|
||||
| `lazy_static!` | `std::sync::OnceLock` | 1.70 |
|
||||
| `once_cell::Lazy` | `std::sync::LazyLock` | 1.80 |
|
||||
| `std::sync::mpsc` | `crossbeam::channel` | - |
|
||||
| `std::sync::Mutex` | `parking_lot::Mutex` | - |
|
||||
| `failure`/`error-chain` | `thiserror`/`anyhow` | - |
|
||||
| `try!()` | `?` operator | 2018 |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```
|
||||
Naming: snake_case (fn/var), CamelCase (type), SCREAMING_CASE (const)
|
||||
Format: rustfmt (just use it)
|
||||
Docs: /// for public items, //! for module docs
|
||||
Lint: #![warn(clippy::all)]
|
||||
```
|
||||
|
||||
Claude knows Rust conventions well. These are the non-obvious Rust-specific rules.
|
||||
16
skills/coding-guidelines/clippy-lints/_index.md
Normal file
16
skills/coding-guidelines/clippy-lints/_index.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Clippy Lint → Rule Mapping
|
||||
|
||||
| Clippy Lint | Category | Fix |
|
||||
|-------------|----------|-----|
|
||||
| `unwrap_used` | Error | Use `?` or `expect()` |
|
||||
| `needless_clone` | Perf | Use reference |
|
||||
| `await_holding_lock` | Async | Scope guard before await |
|
||||
| `linkedlist` | Perf | Use Vec/VecDeque |
|
||||
| `wildcard_imports` | Style | Explicit imports |
|
||||
| `missing_safety_doc` | Safety | Add `# Safety` doc |
|
||||
| `undocumented_unsafe_blocks` | Safety | Add `// SAFETY:` |
|
||||
| `transmute_ptr_to_ptr` | Safety | Use `pointer::cast()` |
|
||||
| `large_stack_arrays` | Mem | Use Vec or Box |
|
||||
| `too_many_arguments` | Design | Use struct params |
|
||||
|
||||
For unsafe-related lints → see `unsafe-checker` skill.
|
||||
6
skills/coding-guidelines/index/rules-index.md
Normal file
6
skills/coding-guidelines/index/rules-index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Complete Rules Reference
|
||||
|
||||
For the full 500+ rules, see:
|
||||
- Source: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/
|
||||
|
||||
Core rules are in `../SKILL.md`.
|
||||
49
skills/core-actionbook/SKILL.md
Normal file
49
skills/core-actionbook/SKILL.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: core-actionbook
|
||||
# Internal tool - no description to prevent auto-triggering
|
||||
# Used by: rust-learner agents for pre-computed selectors
|
||||
---
|
||||
|
||||
# Actionbook
|
||||
|
||||
Pre-computed action manuals for browser automation. Agents receive structured page information instead of parsing entire HTML.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **search_actions** - Search by keyword, returns URL-based action IDs with content previews
|
||||
2. **get_action_by_id** - Get full action manual with page details, DOM structure, and element selectors
|
||||
3. **Execute** - Use returned selectors with your browser automation tool
|
||||
|
||||
## MCP Tools
|
||||
|
||||
- `search_actions` - Search by keyword. Returns: URL-based action IDs, content previews, relevance scores
|
||||
- `get_action_by_id` - Get full action details. Returns: action content, page element selectors (CSS/XPath), element types, allowed methods (click, type, extract), document metadata
|
||||
|
||||
### Parameters
|
||||
|
||||
**search_actions**:
|
||||
- `query` (required): Search keyword (e.g., "airbnb search", "google login")
|
||||
- `type`: `vector` | `fulltext` | `hybrid` (default)
|
||||
- `limit`: Max results (default: 5)
|
||||
- `sourceIds`: Filter by source IDs (comma-separated)
|
||||
- `minScore`: Minimum relevance score (0-1)
|
||||
|
||||
**get_action_by_id**:
|
||||
- `id` (required): URL-based action ID (e.g., `example.com/page`)
|
||||
|
||||
## Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Airbnb Search",
|
||||
"url": "www.airbnb.com/search",
|
||||
"elements": [
|
||||
{
|
||||
"name": "location_input",
|
||||
"selector": "input[data-testid='structured-search-input-field-query']",
|
||||
"type": "textbox",
|
||||
"methods": ["type", "fill"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
115
skills/core-agent-browser/SKILL.md
Normal file
115
skills/core-agent-browser/SKILL.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
name: core-agent-browser
|
||||
# Internal tool - no description to prevent auto-triggering
|
||||
# Used by: rust-learner, docs-researcher, crate-researcher agents
|
||||
---
|
||||
|
||||
# Browser Automation with agent-browser
|
||||
|
||||
## Priority Note
|
||||
|
||||
For fetching Rust/crate information, use this priority order:
|
||||
1. **rust-learner skill** - Orchestrates actionbook + browser-fetcher
|
||||
2. **actionbook MCP** - Pre-computed selectors for known sites
|
||||
3. **agent-browser CLI** - Direct browser automation (last resort)
|
||||
|
||||
Use agent-browser directly only when:
|
||||
- actionbook has no pre-computed selectors for the target site
|
||||
- You need interactive browser testing/automation
|
||||
- You need screenshots or form filling
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to page
|
||||
agent-browser snapshot -i # Get interactive elements with refs
|
||||
agent-browser click @e1 # Click element by ref
|
||||
agent-browser fill @e2 "text" # Fill input by ref
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
## Core workflow
|
||||
|
||||
1. Navigate: `agent-browser open <url>`
|
||||
2. Snapshot: `agent-browser snapshot -i` (returns elements with refs like `@e1`, `@e2`)
|
||||
3. Interact using refs from the snapshot
|
||||
4. Re-snapshot after navigation or significant DOM changes
|
||||
|
||||
## Commands
|
||||
|
||||
### Navigation
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
### Snapshot (page analysis)
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
```
|
||||
|
||||
### Interactions (use @refs from snapshot)
|
||||
```bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scrollintoview @e1 # Scroll element into view
|
||||
```
|
||||
|
||||
### Get information
|
||||
```bash
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get value @e1 # Get input value
|
||||
agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
```
|
||||
|
||||
### Screenshots
|
||||
```bash
|
||||
agent-browser screenshot # Screenshot to stdout
|
||||
agent-browser screenshot path.png # Save to file
|
||||
agent-browser screenshot --full # Full page
|
||||
```
|
||||
|
||||
### Wait
|
||||
```bash
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait --text "Success" # Wait for text
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
```
|
||||
|
||||
### Semantic locators (alternative to refs)
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
```
|
||||
|
||||
## Example: Form submission
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/form
|
||||
agent-browser snapshot -i
|
||||
# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3]
|
||||
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser snapshot -i # Check result
|
||||
```
|
||||
216
skills/core-dynamic-skills/SKILL.md
Normal file
216
skills/core-dynamic-skills/SKILL.md
Normal file
@@ -0,0 +1,216 @@
|
||||
---
|
||||
name: core-dynamic-skills
|
||||
# Command-based tool - no description to prevent auto-triggering
|
||||
# Triggered by: /sync-crate-skills, /clean-crate-skills, /update-crate-skill
|
||||
argument-hint: "[--force] | <crate_name>"
|
||||
context: fork
|
||||
agent: general-purpose
|
||||
---
|
||||
|
||||
# Dynamic Skills Manager
|
||||
|
||||
> **Version:** 2.1.0 | **Last Updated:** 2025-01-27
|
||||
|
||||
Orchestrates on-demand generation of crate-specific skills based on project dependencies.
|
||||
|
||||
## Concept
|
||||
|
||||
Dynamic skills are:
|
||||
- Generated locally at `~/.claude/skills/`
|
||||
- Based on Cargo.toml dependencies
|
||||
- Created using llms.txt from docs.rs
|
||||
- Versioned and updatable
|
||||
- Not committed to the rust-skills repository
|
||||
|
||||
## Trigger Scenarios
|
||||
|
||||
### Prompt-on-Open
|
||||
|
||||
When entering a directory with Cargo.toml:
|
||||
1. Detect Cargo.toml (single or workspace)
|
||||
2. Parse dependencies list
|
||||
3. Check which crates are missing skills
|
||||
4. If missing: "Found X dependencies without skills. Sync now?"
|
||||
5. If confirmed: run `/sync-crate-skills`
|
||||
|
||||
### Manual Commands
|
||||
|
||||
- `/sync-crate-skills` - Sync all dependencies
|
||||
- `/clean-crate-skills [crate]` - Remove skills
|
||||
- `/update-crate-skill <crate>` - Update specific skill
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check if agent and command infrastructure is available.**
|
||||
|
||||
Try to read: `../../agents/` directory
|
||||
Check if `/create-llms-for-skills` and `/create-skills-via-llms` commands work.
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install)
|
||||
|
||||
**When full plugin infrastructure is available:**
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Cargo.toml
|
||||
↓
|
||||
Parse dependencies
|
||||
↓
|
||||
For each crate:
|
||||
├─ Check ~/.claude/skills/{crate}/
|
||||
├─ If missing: Check actionbook for llms.txt
|
||||
│ ├─ Found: /create-skills-via-llms
|
||||
│ └─ Not found: /create-llms-for-skills first
|
||||
└─ Load skill
|
||||
```
|
||||
|
||||
### Workflow Priority
|
||||
|
||||
1. **actionbook MCP** - Check for pre-generated llms.txt
|
||||
2. **/create-llms-for-skills** - Generate llms.txt from docs.rs
|
||||
3. **/create-skills-via-llms** - Create skills from llms.txt
|
||||
|
||||
### Sync Command
|
||||
|
||||
```bash
|
||||
/sync-crate-skills [--force]
|
||||
```
|
||||
|
||||
1. Parse Cargo.toml for dependencies
|
||||
2. For each dependency:
|
||||
- Check if skill exists at `~/.claude/skills/{crate}/`
|
||||
- If missing (or --force): generate skill
|
||||
3. Report results
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install)
|
||||
|
||||
**When agent/command infrastructure is NOT available, execute manually:**
|
||||
|
||||
### Step 1: Parse Cargo.toml
|
||||
|
||||
```bash
|
||||
# Read dependencies
|
||||
cat Cargo.toml | grep -A 100 '\[dependencies\]' | grep -E '^[a-zA-Z]'
|
||||
```
|
||||
|
||||
Or use Read tool to parse Cargo.toml and extract:
|
||||
- `[dependencies]` section
|
||||
- `[dev-dependencies]` section (optional)
|
||||
- Workspace members (if workspace project)
|
||||
|
||||
### Step 2: Check Existing Skills
|
||||
|
||||
```bash
|
||||
# List existing skills
|
||||
ls ~/.claude/skills/
|
||||
```
|
||||
|
||||
Compare with dependencies to find missing skills.
|
||||
|
||||
### Step 3: Generate Missing Skills
|
||||
|
||||
For each missing crate:
|
||||
|
||||
```bash
|
||||
# 1. Fetch crate documentation
|
||||
agent-browser open "https://docs.rs/{crate}/latest/{crate}/"
|
||||
agent-browser get text ".docblock"
|
||||
# Save content
|
||||
|
||||
# 2. Create skill directory
|
||||
mkdir -p ~/.claude/skills/{crate}
|
||||
mkdir -p ~/.claude/skills/{crate}/references
|
||||
|
||||
# 3. Create SKILL.md
|
||||
# Use template from rust-skill-creator inline mode
|
||||
|
||||
# 4. Create reference files for key modules
|
||||
agent-browser open "https://docs.rs/{crate}/latest/{crate}/{module}/"
|
||||
agent-browser get text ".docblock"
|
||||
# Save to ~/.claude/skills/{crate}/references/{module}.md
|
||||
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**WebFetch fallback:**
|
||||
```
|
||||
WebFetch("https://docs.rs/{crate}/latest/{crate}/", "Extract API documentation overview, key types, and usage examples")
|
||||
```
|
||||
|
||||
### Step 4: Workspace Support
|
||||
|
||||
For Cargo workspace projects:
|
||||
|
||||
```bash
|
||||
# 1. Parse root Cargo.toml for workspace members
|
||||
cat Cargo.toml | grep -A 10 '\[workspace\]'
|
||||
|
||||
# 2. For each member, parse their Cargo.toml
|
||||
for member in members; do
|
||||
cat ${member}/Cargo.toml | grep -A 100 '\[dependencies\]'
|
||||
done
|
||||
|
||||
# 3. Aggregate and deduplicate dependencies
|
||||
# 4. Generate skills for missing crates
|
||||
```
|
||||
|
||||
### Clean Command (Inline)
|
||||
|
||||
```bash
|
||||
# Clean specific crate
|
||||
rm -rf ~/.claude/skills/{crate_name}
|
||||
|
||||
# Clean all generated skills
|
||||
rm -rf ~/.claude/skills/*
|
||||
```
|
||||
|
||||
### Update Command (Inline)
|
||||
|
||||
```bash
|
||||
# Remove old skill
|
||||
rm -rf ~/.claude/skills/{crate_name}
|
||||
|
||||
# Re-generate (same as sync for single crate)
|
||||
# Follow Step 3 above for the specific crate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Local Skills Directory
|
||||
|
||||
```
|
||||
~/.claude/skills/
|
||||
├── tokio/
|
||||
│ ├── SKILL.md
|
||||
│ └── references/
|
||||
├── serde/
|
||||
│ ├── SKILL.md
|
||||
│ └── references/
|
||||
└── axum/
|
||||
├── SKILL.md
|
||||
└── references/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/sync-crate-skills` - Main sync command
|
||||
- `/clean-crate-skills` - Cleanup command
|
||||
- `/update-crate-skill` - Update command
|
||||
- `/create-llms-for-skills` - Generate llms.txt (Agent Mode only)
|
||||
- `/create-skills-via-llms` - Create skills from llms.txt (Agent Mode only)
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Commands not found | Skills-only install | Use inline mode |
|
||||
| Cargo.toml not found | Not in Rust project | Navigate to project root |
|
||||
| docs.rs unavailable | Network issue | Retry or skip crate |
|
||||
| Permission denied | Directory issue | Check ~/.claude/skills/ permissions |
|
||||
249
skills/core-fix-skill-docs/SKILL.md
Normal file
249
skills/core-fix-skill-docs/SKILL.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
name: core-fix-skill-docs
|
||||
# Internal maintenance tool - no description to prevent auto-triggering
|
||||
# Triggered by: /fix-skill-docs command
|
||||
argument-hint: "[crate_name] [--check-only]"
|
||||
context: fork
|
||||
agent: general-purpose
|
||||
---
|
||||
|
||||
# Fix Skill Documentation
|
||||
|
||||
> **Version:** 2.1.0 | **Last Updated:** 2025-01-27
|
||||
|
||||
Check and fix missing reference files in dynamic skills.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/fix-skill-docs [crate_name] [--check-only] [--remove-invalid]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `crate_name`: Specific crate to check (optional, defaults to all)
|
||||
- `--check-only`: Only report issues, don't fix
|
||||
- `--remove-invalid`: Remove invalid references instead of creating files
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check if agent infrastructure is available.**
|
||||
|
||||
This skill can run in two modes:
|
||||
- **Agent Mode**: Uses background agents for documentation fetching
|
||||
- **Inline Mode**: Executes directly using agent-browser CLI or WebFetch
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install)
|
||||
|
||||
**When agent infrastructure is available, use background agents for fetching:**
|
||||
|
||||
### Instructions
|
||||
|
||||
#### 1. Scan Skills Directory
|
||||
|
||||
```bash
|
||||
# If crate_name provided
|
||||
skill_dir=~/.claude/skills/{crate_name}
|
||||
|
||||
# Otherwise scan all
|
||||
for dir in ~/.claude/skills/*/; do
|
||||
# Process each skill
|
||||
done
|
||||
```
|
||||
|
||||
#### 2. Parse SKILL.md for References
|
||||
|
||||
Extract referenced files from Documentation section:
|
||||
|
||||
```markdown
|
||||
## Documentation
|
||||
- `./references/file1.md` - Description
|
||||
```
|
||||
|
||||
#### 3. Check File Existence
|
||||
|
||||
```bash
|
||||
if [ ! -f "{skill_dir}/references/{filename}" ]; then
|
||||
echo "MISSING: {filename}"
|
||||
fi
|
||||
```
|
||||
|
||||
#### 4. Report Status
|
||||
|
||||
```
|
||||
=== {crate_name} ===
|
||||
SKILL.md: OK
|
||||
references/:
|
||||
- sync.md: OK
|
||||
- runtime.md: MISSING
|
||||
|
||||
Action needed: 1 file missing
|
||||
```
|
||||
|
||||
#### 5. Fix Missing Files (Agent Mode)
|
||||
|
||||
Launch background agent to fetch documentation:
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
run_in_background: true,
|
||||
prompt: "Fetch documentation for {crate_name}/{module} from docs.rs.
|
||||
Use agent-browser CLI to navigate to https://docs.rs/{crate_name}/latest/{crate_name}/{module}/
|
||||
Extract the main documentation and save to ~/.claude/skills/{crate_name}/references/{module}.md"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install)
|
||||
|
||||
**When agent infrastructure is NOT available, execute directly:**
|
||||
|
||||
### Step 1: Scan Skills Directory
|
||||
|
||||
```bash
|
||||
# List all skills
|
||||
ls ~/.claude/skills/
|
||||
|
||||
# Or check specific skill
|
||||
ls ~/.claude/skills/{crate_name}/
|
||||
```
|
||||
|
||||
### Step 2: Parse SKILL.md for References
|
||||
|
||||
Read SKILL.md and extract all `./references/*.md` patterns:
|
||||
|
||||
```bash
|
||||
# Using Read tool
|
||||
Read("~/.claude/skills/{crate_name}/SKILL.md")
|
||||
|
||||
# Look for lines like:
|
||||
# - `./references/sync.md` - Sync primitives
|
||||
# - `./references/runtime.md` - Runtime configuration
|
||||
```
|
||||
|
||||
### Step 3: Check File Existence
|
||||
|
||||
```bash
|
||||
# Check each referenced file
|
||||
for ref in references; do
|
||||
if [ ! -f "~/.claude/skills/{crate_name}/references/${ref}.md" ]; then
|
||||
echo "MISSING: ${ref}.md"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Step 4: Report Status
|
||||
|
||||
Output format:
|
||||
```
|
||||
=== {crate_name} ===
|
||||
SKILL.md: OK
|
||||
references/:
|
||||
- sync.md: OK
|
||||
- runtime.md: MISSING
|
||||
|
||||
Action needed: 1 file missing
|
||||
```
|
||||
|
||||
### Step 5: Fix Missing Files (Inline)
|
||||
|
||||
For each missing file:
|
||||
|
||||
**Using agent-browser CLI:**
|
||||
```bash
|
||||
agent-browser open "https://docs.rs/{crate_name}/latest/{crate_name}/{module}/"
|
||||
agent-browser get text ".docblock"
|
||||
# Save output to ~/.claude/skills/{crate_name}/references/{module}.md
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Using WebFetch fallback:**
|
||||
```
|
||||
WebFetch("https://docs.rs/{crate_name}/latest/{crate_name}/{module}/",
|
||||
"Extract the main documentation content for this module")
|
||||
```
|
||||
|
||||
Then write the content:
|
||||
```bash
|
||||
Write("~/.claude/skills/{crate_name}/references/{module}.md", <fetched_content>)
|
||||
```
|
||||
|
||||
### Step 6: Update SKILL.md (if --remove-invalid)
|
||||
|
||||
If `--remove-invalid` flag is set and file cannot be fetched:
|
||||
|
||||
```bash
|
||||
# Read current SKILL.md
|
||||
Read("~/.claude/skills/{crate_name}/SKILL.md")
|
||||
|
||||
# Remove the invalid reference line
|
||||
Edit("~/.claude/skills/{crate_name}/SKILL.md",
|
||||
old_string="- `./references/{invalid_file}.md` - Description",
|
||||
new_string="")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Priority
|
||||
|
||||
1. **agent-browser CLI** - Primary tool for fetching documentation
|
||||
2. **WebFetch** - Fallback if agent-browser unavailable
|
||||
3. **Edit SKILL.md** - For removing invalid references (--remove-invalid only)
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Check All Skills (--check-only)
|
||||
|
||||
```bash
|
||||
/fix-skill-docs --check-only
|
||||
|
||||
# Output:
|
||||
=== tokio ===
|
||||
SKILL.md: OK
|
||||
references/:
|
||||
- sync.md: OK
|
||||
- runtime.md: MISSING
|
||||
- task.md: OK
|
||||
|
||||
=== serde ===
|
||||
SKILL.md: OK
|
||||
references/:
|
||||
- derive.md: OK
|
||||
|
||||
Summary: 1 file missing in 1 skill
|
||||
```
|
||||
|
||||
### Fix Specific Crate
|
||||
|
||||
```bash
|
||||
/fix-skill-docs tokio
|
||||
|
||||
# Fetches missing runtime.md from docs.rs
|
||||
# Reports success
|
||||
```
|
||||
|
||||
### Remove Invalid References
|
||||
|
||||
```bash
|
||||
/fix-skill-docs tokio --remove-invalid
|
||||
|
||||
# If runtime.md cannot be fetched:
|
||||
# Removes reference from SKILL.md instead
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Agent not available | Skills-only install | Use inline mode |
|
||||
| Skills directory empty | No skills installed | Run /sync-crate-skills first |
|
||||
| docs.rs unavailable | Network issue | Retry or use --remove-invalid |
|
||||
| Permission denied | Directory issue | Check ~/.claude/skills/ permissions |
|
||||
| Invalid SKILL.md format | Corrupted skill | Re-generate skill |
|
||||
161
skills/domain-cli/SKILL.md
Normal file
161
skills/domain-cli/SKILL.md
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
name: domain-cli
|
||||
description: "Use when building CLI tools. Keywords: CLI, command line, terminal, clap, structopt, argument parsing, subcommand, interactive, TUI, ratatui, crossterm, indicatif, progress bar, colored output, shell completion, config file, environment variable, 命令行, 终端应用, 参数解析"
|
||||
globs: ["**/Cargo.toml"]
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# CLI Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| User ergonomics | Clear help, errors | clap derive macros |
|
||||
| Config precedence | CLI > env > file | Layered config loading |
|
||||
| Exit codes | Non-zero on error | Proper Result handling |
|
||||
| Stdout/stderr | Data vs errors | eprintln! for errors |
|
||||
| Interruptible | Handle Ctrl+C | Signal handling |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### User Communication
|
||||
|
||||
```
|
||||
RULE: Errors to stderr, data to stdout
|
||||
WHY: Pipeable output, scriptability
|
||||
RUST: eprintln! for errors, println! for data
|
||||
```
|
||||
|
||||
### Configuration Priority
|
||||
|
||||
```
|
||||
RULE: CLI args > env vars > config file > defaults
|
||||
WHY: User expectation, override capability
|
||||
RUST: Layered config with clap + figment/config
|
||||
```
|
||||
|
||||
### Exit Codes
|
||||
|
||||
```
|
||||
RULE: Return non-zero on any error
|
||||
WHY: Script integration, automation
|
||||
RUST: main() -> Result<(), Error> or explicit exit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need argument parsing"
|
||||
↓ m05-type-driven: Derive structs for args
|
||||
↓ clap: #[derive(Parser)]
|
||||
|
||||
"Need config layering"
|
||||
↓ m09-domain: Config as domain object
|
||||
↓ figment/config: Layer sources
|
||||
|
||||
"Need progress display"
|
||||
↓ m12-lifecycle: Progress bar as RAII
|
||||
↓ indicatif: ProgressBar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| Argument parsing | clap |
|
||||
| Interactive prompts | dialoguer |
|
||||
| Progress bars | indicatif |
|
||||
| Colored output | colored |
|
||||
| Terminal UI | ratatui |
|
||||
| Terminal control | crossterm |
|
||||
| Console utilities | console |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Args struct | Type-safe args | `#[derive(Parser)]` |
|
||||
| Subcommands | Command hierarchy | `#[derive(Subcommand)]` |
|
||||
| Config layers | Override precedence | CLI > env > file |
|
||||
| Progress | User feedback | `ProgressBar::new(len)` |
|
||||
|
||||
## Code Pattern: CLI Structure
|
||||
|
||||
```rust
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "myapp", about = "My CLI tool")]
|
||||
struct Cli {
|
||||
/// Enable verbose output
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Initialize a new project
|
||||
Init { name: String },
|
||||
/// Run the application
|
||||
Run {
|
||||
#[arg(short, long)]
|
||||
port: Option<u16>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Commands::Init { name } => init_project(&name)?,
|
||||
Commands::Run { port } => run_server(port.unwrap_or(8080))?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Errors to stdout | Breaks piping | eprintln! |
|
||||
| No help text | Poor UX | #[arg(help = "...")] |
|
||||
| Panic on error | Bad exit code | Result + proper handling |
|
||||
| No progress for long ops | User uncertainty | indicatif |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Type-safe args | Derive macros | clap Parser |
|
||||
| Error handling | Result propagation | anyhow + exit codes |
|
||||
| User feedback | Progress RAII | indicatif ProgressBar |
|
||||
| Config precedence | Builder pattern | Layered sources |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Error handling | m06-error-handling |
|
||||
| Type-driven args | m05-type-driven |
|
||||
| Progress lifecycle | m12-lifecycle |
|
||||
| Async CLI | m07-concurrency |
|
||||
166
skills/domain-cloud-native/SKILL.md
Normal file
166
skills/domain-cloud-native/SKILL.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: domain-cloud-native
|
||||
description: "Use when building cloud-native apps. Keywords: kubernetes, k8s, docker, container, grpc, tonic, microservice, service mesh, observability, tracing, metrics, health check, cloud, deployment, 云原生, 微服务, 容器"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Cloud-Native Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| 12-Factor | Config from env | Environment-based config |
|
||||
| Observability | Metrics + traces | tracing + opentelemetry |
|
||||
| Health checks | Liveness/readiness | Dedicated endpoints |
|
||||
| Graceful shutdown | Clean termination | Signal handling |
|
||||
| Horizontal scale | Stateless design | No local state |
|
||||
| Container-friendly | Small binaries | Release optimization |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### Stateless Design
|
||||
|
||||
```
|
||||
RULE: No local persistent state
|
||||
WHY: Pods can be killed/rescheduled anytime
|
||||
RUST: External state (Redis, DB), no static mut
|
||||
```
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
```
|
||||
RULE: Handle SIGTERM, drain connections
|
||||
WHY: Zero-downtime deployments
|
||||
RUST: tokio::signal + graceful shutdown
|
||||
```
|
||||
|
||||
### Observability
|
||||
|
||||
```
|
||||
RULE: Every request must be traceable
|
||||
WHY: Debugging distributed systems
|
||||
RUST: tracing spans, opentelemetry export
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need distributed tracing"
|
||||
↓ m12-lifecycle: Span lifecycle
|
||||
↓ tracing + opentelemetry
|
||||
|
||||
"Need graceful shutdown"
|
||||
↓ m07-concurrency: Signal handling
|
||||
↓ m12-lifecycle: Connection draining
|
||||
|
||||
"Need health checks"
|
||||
↓ domain-web: HTTP endpoints
|
||||
↓ m06-error-handling: Health status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| gRPC | tonic |
|
||||
| Kubernetes | kube, kube-runtime |
|
||||
| Docker | bollard |
|
||||
| Tracing | tracing, opentelemetry |
|
||||
| Metrics | prometheus, metrics |
|
||||
| Config | config, figment |
|
||||
| Health | HTTP endpoints |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| gRPC services | Service mesh | tonic + tower |
|
||||
| K8s operators | Custom resources | kube-runtime Controller |
|
||||
| Observability | Debugging | tracing + OTEL |
|
||||
| Health checks | Orchestration | `/health`, `/ready` |
|
||||
| Config | 12-factor | Env vars + secrets |
|
||||
|
||||
## Code Pattern: Graceful Shutdown
|
||||
|
||||
```rust
|
||||
use tokio::signal;
|
||||
|
||||
async fn run_server() -> anyhow::Result<()> {
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/ready", get(ready));
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
signal::ctrl_c().await.expect("failed to listen for ctrl+c");
|
||||
tracing::info!("shutdown signal received");
|
||||
}
|
||||
```
|
||||
|
||||
## Health Check Pattern
|
||||
|
||||
```rust
|
||||
async fn health() -> StatusCode {
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
async fn ready(State(db): State<Arc<DbPool>>) -> StatusCode {
|
||||
match db.ping().await {
|
||||
Ok(_) => StatusCode::OK,
|
||||
Err(_) => StatusCode::SERVICE_UNAVAILABLE,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Local file state | Not stateless | External storage |
|
||||
| No SIGTERM handling | Hard kills | Graceful shutdown |
|
||||
| No tracing | Can't debug | tracing spans |
|
||||
| Static config | Not 12-factor | Env vars |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Stateless | External state | Arc<Client> for external |
|
||||
| Graceful shutdown | Signal handling | tokio::signal |
|
||||
| Tracing | Span lifecycle | tracing + OTEL |
|
||||
| Health checks | HTTP endpoints | Dedicated routes |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Async patterns | m07-concurrency |
|
||||
| HTTP endpoints | domain-web |
|
||||
| Error handling | m13-domain-error |
|
||||
| Resource lifecycle | m12-lifecycle |
|
||||
178
skills/domain-embedded/SKILL.md
Normal file
178
skills/domain-embedded/SKILL.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
name: domain-embedded
|
||||
description: "Use when developing embedded/no_std Rust. Keywords: embedded, no_std, microcontroller, MCU, ARM, RISC-V, bare metal, firmware, HAL, PAC, RTIC, embassy, interrupt, DMA, peripheral, GPIO, SPI, I2C, UART, embedded-hal, cortex-m, esp32, stm32, nrf, 嵌入式, 单片机, 固件, 裸机"
|
||||
globs: ["**/Cargo.toml", "**/.cargo/config.toml"]
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
## Project Context (Auto-Injected)
|
||||
|
||||
**Target configuration:**
|
||||
!`cat .cargo/config.toml 2>/dev/null || echo "No .cargo/config.toml found"`
|
||||
|
||||
---
|
||||
|
||||
# Embedded Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| No heap | Stack allocation | heapless, no Box/Vec |
|
||||
| No std | Core only | #![no_std] |
|
||||
| Real-time | Predictable timing | No dynamic alloc |
|
||||
| Resource limited | Minimal memory | Static buffers |
|
||||
| Hardware safety | Safe peripheral access | HAL + ownership |
|
||||
| Interrupt safe | No blocking in ISR | Atomic, critical sections |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### No Dynamic Allocation
|
||||
|
||||
```
|
||||
RULE: Cannot use heap (no allocator)
|
||||
WHY: Deterministic memory, no OOM
|
||||
RUST: heapless::Vec<T, N>, arrays
|
||||
```
|
||||
|
||||
### Interrupt Safety
|
||||
|
||||
```
|
||||
RULE: Shared state must be interrupt-safe
|
||||
WHY: ISR can preempt at any time
|
||||
RUST: Mutex<RefCell<T>> + critical section
|
||||
```
|
||||
|
||||
### Hardware Ownership
|
||||
|
||||
```
|
||||
RULE: Peripherals must have clear ownership
|
||||
WHY: Prevent conflicting access
|
||||
RUST: HAL takes ownership, singletons
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need no_std compatible data structures"
|
||||
↓ m02-resource: heapless collections
|
||||
↓ Static sizing: heapless::Vec<T, N>
|
||||
|
||||
"Need interrupt-safe state"
|
||||
↓ m03-mutability: Mutex<RefCell<Option<T>>>
|
||||
↓ m07-concurrency: Critical sections
|
||||
|
||||
"Need peripheral ownership"
|
||||
↓ m01-ownership: Singleton pattern
|
||||
↓ m12-lifecycle: RAII for hardware
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer Stack
|
||||
|
||||
| Layer | Examples | Purpose |
|
||||
|-------|----------|---------|
|
||||
| PAC | stm32f4, esp32c3 | Register access |
|
||||
| HAL | stm32f4xx-hal | Hardware abstraction |
|
||||
| Framework | RTIC, Embassy | Concurrency |
|
||||
| Traits | embedded-hal | Portable drivers |
|
||||
|
||||
## Framework Comparison
|
||||
|
||||
| Framework | Style | Best For |
|
||||
|-----------|-------|----------|
|
||||
| RTIC | Priority-based | Interrupt-driven apps |
|
||||
| Embassy | Async | Complex state machines |
|
||||
| Bare metal | Manual | Simple apps |
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| Runtime (ARM) | cortex-m-rt |
|
||||
| Panic handler | panic-halt, panic-probe |
|
||||
| Collections | heapless |
|
||||
| HAL traits | embedded-hal |
|
||||
| Logging | defmt |
|
||||
| Flash/debug | probe-run |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| no_std setup | Bare metal | `#![no_std]` + `#![no_main]` |
|
||||
| Entry point | Startup | `#[entry]` or embassy |
|
||||
| Static state | ISR access | `Mutex<RefCell<Option<T>>>` |
|
||||
| Fixed buffers | No heap | `heapless::Vec<T, N>` |
|
||||
|
||||
## Code Pattern: Static Peripheral
|
||||
|
||||
```rust
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use cortex_m::interrupt::{self, Mutex};
|
||||
use core::cell::RefCell;
|
||||
|
||||
static LED: Mutex<RefCell<Option<Led>>> = Mutex::new(RefCell::new(None));
|
||||
|
||||
#[entry]
|
||||
fn main() -> ! {
|
||||
let dp = pac::Peripherals::take().unwrap();
|
||||
let led = Led::new(dp.GPIOA);
|
||||
|
||||
interrupt::free(|cs| {
|
||||
LED.borrow(cs).replace(Some(led));
|
||||
});
|
||||
|
||||
loop {
|
||||
interrupt::free(|cs| {
|
||||
if let Some(led) = LED.borrow(cs).borrow_mut().as_mut() {
|
||||
led.toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Using Vec | Heap allocation | heapless::Vec |
|
||||
| No critical section | Race with ISR | Mutex + interrupt::free |
|
||||
| Blocking in ISR | Missed interrupts | Defer to main loop |
|
||||
| Unsafe peripheral | Hardware conflict | HAL ownership |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| No heap | Static collections | heapless::Vec<T, N> |
|
||||
| ISR safety | Critical sections | Mutex<RefCell<T>> |
|
||||
| Hardware ownership | Singleton | take().unwrap() |
|
||||
| no_std | Core-only | #![no_std], #![no_main] |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Static memory | m02-resource |
|
||||
| Interior mutability | m03-mutability |
|
||||
| Interrupt patterns | m07-concurrency |
|
||||
| Unsafe for hardware | unsafe-checker |
|
||||
146
skills/domain-fintech/SKILL.md
Normal file
146
skills/domain-fintech/SKILL.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
name: domain-fintech
|
||||
description: "Use when building fintech apps. Keywords: fintech, trading, decimal, currency, financial, money, transaction, ledger, payment, exchange rate, precision, rounding, accounting, 金融, 交易系统, 货币, 支付"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# FinTech Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| Audit trail | Immutable records | Arc<T>, no mutation |
|
||||
| Precision | No floating point | rust_decimal |
|
||||
| Consistency | Transaction boundaries | Clear ownership |
|
||||
| Compliance | Complete logging | Structured tracing |
|
||||
| Reproducibility | Deterministic execution | No race conditions |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### Financial Precision
|
||||
|
||||
```
|
||||
RULE: Never use f64 for money
|
||||
WHY: Floating point loses precision
|
||||
RUST: Use rust_decimal::Decimal
|
||||
```
|
||||
|
||||
### Audit Requirements
|
||||
|
||||
```
|
||||
RULE: All transactions must be immutable and traceable
|
||||
WHY: Regulatory compliance, dispute resolution
|
||||
RUST: Arc<T> for sharing, event sourcing pattern
|
||||
```
|
||||
|
||||
### Consistency
|
||||
|
||||
```
|
||||
RULE: Money can't disappear or appear
|
||||
WHY: Double-entry accounting principles
|
||||
RUST: Transaction types with validated totals
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need immutable transaction records"
|
||||
↓ m09-domain: Model as Value Objects
|
||||
↓ m01-ownership: Use Arc for shared immutable data
|
||||
|
||||
"Need precise decimal math"
|
||||
↓ m05-type-driven: Newtype for Currency/Amount
|
||||
↓ rust_decimal: Use Decimal type
|
||||
|
||||
"Need transaction boundaries"
|
||||
↓ m12-lifecycle: RAII for transaction scope
|
||||
↓ m09-domain: Aggregate boundaries
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| Decimal math | rust_decimal |
|
||||
| Date/time | chrono, time |
|
||||
| UUID | uuid |
|
||||
| Serialization | serde |
|
||||
| Validation | validator |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Currency newtype | Type safety | `struct Amount(Decimal);` |
|
||||
| Transaction | Atomic operations | Event sourcing |
|
||||
| Audit log | Traceability | Structured logging with trace IDs |
|
||||
| Ledger | Double-entry | Debit/credit balance |
|
||||
|
||||
## Code Pattern: Currency Type
|
||||
|
||||
```rust
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Amount {
|
||||
value: Decimal,
|
||||
currency: Currency,
|
||||
}
|
||||
|
||||
impl Amount {
|
||||
pub fn new(value: Decimal, currency: Currency) -> Self {
|
||||
Self { value, currency }
|
||||
}
|
||||
|
||||
pub fn add(&self, other: &Amount) -> Result<Amount, CurrencyMismatch> {
|
||||
if self.currency != other.currency {
|
||||
return Err(CurrencyMismatch);
|
||||
}
|
||||
Ok(Amount::new(self.value + other.value, self.currency))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Using f64 | Precision loss | rust_decimal |
|
||||
| Mutable transaction | Audit trail broken | Immutable + events |
|
||||
| String for amount | No validation | Validated newtype |
|
||||
| Silent overflow | Money disappears | Checked arithmetic |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Immutable records | Event sourcing | Arc<T>, Clone |
|
||||
| Transaction scope | Aggregate | Owned children |
|
||||
| Precision | Value Object | rust_decimal newtype |
|
||||
| Thread-safe sharing | Shared immutable | Arc (not Rc) |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Value Object design | m09-domain |
|
||||
| Ownership for immutable | m01-ownership |
|
||||
| Arc for sharing | m02-resource |
|
||||
| Error handling | m13-domain-error |
|
||||
168
skills/domain-iot/SKILL.md
Normal file
168
skills/domain-iot/SKILL.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: domain-iot
|
||||
description: "Use when building IoT apps. Keywords: IoT, Internet of Things, sensor, MQTT, device, edge computing, telemetry, actuator, smart home, gateway, protocol, 物联网, 传感器, 边缘计算, 智能家居"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# IoT Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| Unreliable network | Offline-first | Local buffering |
|
||||
| Power constraints | Efficient code | Sleep modes, minimal alloc |
|
||||
| Resource limits | Small footprint | no_std where needed |
|
||||
| Security | Encrypted comms | TLS, signed firmware |
|
||||
| Reliability | Self-recovery | Watchdog, error handling |
|
||||
| OTA updates | Safe upgrades | Rollback capability |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### Network Unreliability
|
||||
|
||||
```
|
||||
RULE: Network can fail at any time
|
||||
WHY: Wireless, remote locations
|
||||
RUST: Local queue, retry with backoff
|
||||
```
|
||||
|
||||
### Power Management
|
||||
|
||||
```
|
||||
RULE: Minimize power consumption
|
||||
WHY: Battery life, energy costs
|
||||
RUST: Sleep modes, efficient algorithms
|
||||
```
|
||||
|
||||
### Device Security
|
||||
|
||||
```
|
||||
RULE: All communication encrypted
|
||||
WHY: Physical access possible
|
||||
RUST: TLS, signed messages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need offline-first design"
|
||||
↓ m12-lifecycle: Local buffer with persistence
|
||||
↓ m13-domain-error: Retry with backoff
|
||||
|
||||
"Need power efficiency"
|
||||
↓ domain-embedded: no_std patterns
|
||||
↓ m10-performance: Minimal allocations
|
||||
|
||||
"Need reliable messaging"
|
||||
↓ m07-concurrency: Async with timeout
|
||||
↓ MQTT: QoS levels
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Comparison
|
||||
|
||||
| Environment | Stack | Crates |
|
||||
|-------------|-------|--------|
|
||||
| Linux gateway | tokio + std | rumqttc, reqwest |
|
||||
| MCU device | embassy + no_std | embedded-hal |
|
||||
| Hybrid | Split workloads | Both |
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| MQTT (std) | rumqttc, paho-mqtt |
|
||||
| Embedded | embedded-hal, embassy |
|
||||
| Async (std) | tokio |
|
||||
| Async (no_std) | embassy |
|
||||
| Logging (no_std) | defmt |
|
||||
| Logging (std) | tracing |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Pub/Sub | Device comms | MQTT topics |
|
||||
| Edge compute | Local processing | Filter before upload |
|
||||
| OTA updates | Firmware upgrade | Signed + rollback |
|
||||
| Power mgmt | Battery life | Sleep + wake events |
|
||||
| Store & forward | Network reliability | Local queue |
|
||||
|
||||
## Code Pattern: MQTT Client
|
||||
|
||||
```rust
|
||||
use rumqttc::{AsyncClient, MqttOptions, QoS};
|
||||
|
||||
async fn run_mqtt() -> anyhow::Result<()> {
|
||||
let mut options = MqttOptions::new("device-1", "broker.example.com", 1883);
|
||||
options.set_keep_alive(Duration::from_secs(30));
|
||||
|
||||
let (client, mut eventloop) = AsyncClient::new(options, 10);
|
||||
|
||||
// Subscribe to commands
|
||||
client.subscribe("devices/device-1/commands", QoS::AtLeastOnce).await?;
|
||||
|
||||
// Publish telemetry
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let data = read_sensor().await;
|
||||
client.publish("devices/device-1/telemetry", QoS::AtLeastOnce, false, data).await.ok();
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Process events
|
||||
loop {
|
||||
match eventloop.poll().await {
|
||||
Ok(event) => handle_event(event).await,
|
||||
Err(e) => {
|
||||
tracing::error!("MQTT error: {}", e);
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| No retry logic | Lost data | Exponential backoff |
|
||||
| Always-on radio | Battery drain | Sleep between sends |
|
||||
| Unencrypted MQTT | Security risk | TLS |
|
||||
| No local buffer | Network outage = data loss | Persist locally |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Offline-first | Store & forward | Local queue + flush |
|
||||
| Power efficiency | Sleep patterns | Timer-based wake |
|
||||
| Network reliability | Retry | tokio-retry, backoff |
|
||||
| Security | TLS | rustls, native-tls |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Embedded patterns | domain-embedded |
|
||||
| Async patterns | m07-concurrency |
|
||||
| Error recovery | m13-domain-error |
|
||||
| Performance | m10-performance |
|
||||
181
skills/domain-ml/SKILL.md
Normal file
181
skills/domain-ml/SKILL.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
name: domain-ml
|
||||
description: "Use when building ML/AI apps in Rust. Keywords: machine learning, ML, AI, tensor, model, inference, neural network, deep learning, training, prediction, ndarray, tch-rs, burn, candle, 机器学习, 人工智能, 模型推理"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Machine Learning Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| Large data | Efficient memory | Zero-copy, streaming |
|
||||
| GPU acceleration | CUDA/Metal support | candle, tch-rs |
|
||||
| Model portability | Standard formats | ONNX |
|
||||
| Batch processing | Throughput over latency | Batched inference |
|
||||
| Numerical precision | Float handling | ndarray, careful f32/f64 |
|
||||
| Reproducibility | Deterministic | Seeded random, versioning |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### Memory Efficiency
|
||||
|
||||
```
|
||||
RULE: Avoid copying large tensors
|
||||
WHY: Memory bandwidth is bottleneck
|
||||
RUST: References, views, in-place ops
|
||||
```
|
||||
|
||||
### GPU Utilization
|
||||
|
||||
```
|
||||
RULE: Batch operations for GPU efficiency
|
||||
WHY: GPU overhead per kernel launch
|
||||
RUST: Batch sizes, async data loading
|
||||
```
|
||||
|
||||
### Model Portability
|
||||
|
||||
```
|
||||
RULE: Use standard model formats
|
||||
WHY: Train in Python, deploy in Rust
|
||||
RUST: ONNX via tract or candle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need efficient data pipelines"
|
||||
↓ m10-performance: Streaming, batching
|
||||
↓ polars: Lazy evaluation
|
||||
|
||||
"Need GPU inference"
|
||||
↓ m07-concurrency: Async data loading
|
||||
↓ candle/tch-rs: CUDA backend
|
||||
|
||||
"Need model loading"
|
||||
↓ m12-lifecycle: Lazy init, caching
|
||||
↓ tract: ONNX runtime
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Case → Framework
|
||||
|
||||
| Use Case | Recommended | Why |
|
||||
|----------|-------------|-----|
|
||||
| Inference only | tract (ONNX) | Lightweight, portable |
|
||||
| Training + inference | candle, burn | Pure Rust, GPU |
|
||||
| PyTorch models | tch-rs | Direct bindings |
|
||||
| Data pipelines | polars | Fast, lazy eval |
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| Tensors | ndarray |
|
||||
| ONNX inference | tract |
|
||||
| ML framework | candle, burn |
|
||||
| PyTorch bindings | tch-rs |
|
||||
| Data processing | polars |
|
||||
| Embeddings | fastembed |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Model loading | Once, reuse | `OnceLock<Model>` |
|
||||
| Batching | Throughput | Collect then process |
|
||||
| Streaming | Large data | Iterator-based |
|
||||
| GPU async | Parallelism | Data loading parallel to compute |
|
||||
|
||||
## Code Pattern: Inference Server
|
||||
|
||||
```rust
|
||||
use std::sync::OnceLock;
|
||||
use tract_onnx::prelude::*;
|
||||
|
||||
static MODEL: OnceLock<SimplePlan<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>> = OnceLock::new();
|
||||
|
||||
fn get_model() -> &'static SimplePlan<...> {
|
||||
MODEL.get_or_init(|| {
|
||||
tract_onnx::onnx()
|
||||
.model_for_path("model.onnx")
|
||||
.unwrap()
|
||||
.into_optimized()
|
||||
.unwrap()
|
||||
.into_runnable()
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
async fn predict(input: Vec<f32>) -> anyhow::Result<Vec<f32>> {
|
||||
let model = get_model();
|
||||
let input = tract_ndarray::arr1(&input).into_shape((1, input.len()))?;
|
||||
let result = model.run(tvec!(input.into()))?;
|
||||
Ok(result[0].to_array_view::<f32>()?.iter().copied().collect())
|
||||
}
|
||||
```
|
||||
|
||||
## Code Pattern: Batched Inference
|
||||
|
||||
```rust
|
||||
async fn batch_predict(inputs: Vec<Vec<f32>>, batch_size: usize) -> Vec<Vec<f32>> {
|
||||
let mut results = Vec::with_capacity(inputs.len());
|
||||
|
||||
for batch in inputs.chunks(batch_size) {
|
||||
// Stack inputs into batch tensor
|
||||
let batch_tensor = stack_inputs(batch);
|
||||
|
||||
// Run inference on batch
|
||||
let batch_output = model.run(batch_tensor).await;
|
||||
|
||||
// Unstack results
|
||||
results.extend(unstack_outputs(batch_output));
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Clone tensors | Memory waste | Use views |
|
||||
| Single inference | GPU underutilized | Batch processing |
|
||||
| Load model per request | Slow | Singleton pattern |
|
||||
| Sync data loading | GPU idle | Async pipeline |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Memory efficiency | Zero-copy | ndarray views |
|
||||
| Model singleton | Lazy init | OnceLock<Model> |
|
||||
| Batch processing | Chunked iteration | chunks() + parallel |
|
||||
| GPU async | Concurrent loading | tokio::spawn + GPU |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Performance | m10-performance |
|
||||
| Lazy initialization | m12-lifecycle |
|
||||
| Async patterns | m07-concurrency |
|
||||
| Memory efficiency | m01-ownership |
|
||||
156
skills/domain-web/SKILL.md
Normal file
156
skills/domain-web/SKILL.md
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
name: domain-web
|
||||
description: "Use when building web services. Keywords: web server, HTTP, REST API, GraphQL, WebSocket, axum, actix, warp, rocket, tower, hyper, reqwest, middleware, router, handler, extractor, state management, authentication, authorization, JWT, session, cookie, CORS, rate limiting, web 开发, HTTP 服务, API 设计, 中间件, 路由"
|
||||
globs: ["**/Cargo.toml"]
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Web Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| Stateless HTTP | No request-local globals | State in extractors |
|
||||
| Concurrency | Handle many connections | Async, Send + Sync |
|
||||
| Latency SLA | Fast response | Efficient ownership |
|
||||
| Security | Input validation | Type-safe extractors |
|
||||
| Observability | Request tracing | tracing + tower layers |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### Async by Default
|
||||
|
||||
```
|
||||
RULE: Web handlers must not block
|
||||
WHY: Block one task = block many requests
|
||||
RUST: async/await, spawn_blocking for CPU work
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
```
|
||||
RULE: Shared state must be thread-safe
|
||||
WHY: Handlers run on any thread
|
||||
RUST: Arc<T>, Arc<RwLock<T>> for mutable
|
||||
```
|
||||
|
||||
### Request Lifecycle
|
||||
|
||||
```
|
||||
RULE: Resources live only for request duration
|
||||
WHY: Memory management, no leaks
|
||||
RUST: Extractors, proper ownership
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need shared application state"
|
||||
↓ m07-concurrency: Use Arc for thread-safe sharing
|
||||
↓ m02-resource: Arc<RwLock<T>> for mutable state
|
||||
|
||||
"Need request validation"
|
||||
↓ m05-type-driven: Validated extractors
|
||||
↓ m06-error-handling: IntoResponse for errors
|
||||
|
||||
"Need middleware stack"
|
||||
↓ m12-lifecycle: Tower layers
|
||||
↓ m04-zero-cost: Trait-based composition
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Framework Comparison
|
||||
|
||||
| Framework | Style | Best For |
|
||||
|-----------|-------|----------|
|
||||
| axum | Functional, tower | Modern APIs |
|
||||
| actix-web | Actor-based | High performance |
|
||||
| warp | Filter composition | Composable APIs |
|
||||
| rocket | Macro-driven | Rapid development |
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| HTTP server | axum, actix-web |
|
||||
| HTTP client | reqwest |
|
||||
| JSON | serde_json |
|
||||
| Auth/JWT | jsonwebtoken |
|
||||
| Session | tower-sessions |
|
||||
| Database | sqlx, diesel |
|
||||
| Middleware | tower |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Extractors | Request parsing | `State(db)`, `Json(payload)` |
|
||||
| Error response | Unified errors | `impl IntoResponse` |
|
||||
| Middleware | Cross-cutting | Tower layers |
|
||||
| Shared state | App config | `Arc<AppState>` |
|
||||
|
||||
## Code Pattern: Axum Handler
|
||||
|
||||
```rust
|
||||
async fn handler(
|
||||
State(db): State<Arc<DbPool>>,
|
||||
Json(payload): Json<CreateUser>,
|
||||
) -> Result<Json<User>, AppError> {
|
||||
let user = db.create_user(&payload).await?;
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
// Error handling
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match self {
|
||||
Self::NotFound => (StatusCode::NOT_FOUND, "Not found"),
|
||||
Self::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error"),
|
||||
};
|
||||
(status, Json(json!({"error": message}))).into_response()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Blocking in handler | Latency spike | spawn_blocking |
|
||||
| Rc in state | Not Send + Sync | Use Arc |
|
||||
| No validation | Security risk | Type-safe extractors |
|
||||
| No error response | Bad UX | IntoResponse impl |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Async handlers | Async/await | tokio runtime |
|
||||
| Thread-safe state | Shared state | Arc<T>, Arc<RwLock<T>> |
|
||||
| Request lifecycle | Extractors | Ownership via From<Request> |
|
||||
| Middleware | Tower layers | Trait-based composition |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Async patterns | m07-concurrency |
|
||||
| State management | m02-resource |
|
||||
| Error handling | m06-error-handling |
|
||||
| Middleware design | m12-lifecycle |
|
||||
134
skills/m01-ownership/SKILL.md
Normal file
134
skills/m01-ownership/SKILL.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
name: m01-ownership
|
||||
description: "CRITICAL: Use for ownership/borrow/lifetime issues. Triggers: E0382, E0597, E0506, E0507, E0515, E0716, E0106, value moved, borrowed value does not live long enough, cannot move out of, use of moved value, ownership, borrow, lifetime, 'a, 'static, move, clone, Copy, 所有权, 借用, 生命周期"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Ownership & Lifetimes
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Who should own this data, and for how long?**
|
||||
|
||||
Before fixing ownership errors, understand the data's role:
|
||||
- Is it shared or exclusive?
|
||||
- Is it short-lived or long-lived?
|
||||
- Is it transformed or just read?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Error | Don't Just Say | Ask Instead |
|
||||
|-------|----------------|-------------|
|
||||
| E0382 | "Clone it" | Who should own this data? |
|
||||
| E0597 | "Extend lifetime" | Is the scope boundary correct? |
|
||||
| E0506 | "End borrow first" | Should mutation happen elsewhere? |
|
||||
| E0507 | "Clone before move" | Why are we moving from a reference? |
|
||||
| E0515 | "Return owned" | Should caller own the data? |
|
||||
| E0716 | "Bind to variable" | Why is this temporary? |
|
||||
| E0106 | "Add 'a" | What is the actual lifetime relationship? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before fixing an ownership error, ask:
|
||||
|
||||
1. **What is this data's domain role?**
|
||||
- Entity (unique identity) → owned
|
||||
- Value Object (interchangeable) → clone/copy OK
|
||||
- Temporary (computation result) → maybe restructure
|
||||
|
||||
2. **Is the ownership design intentional?**
|
||||
- By design → work within constraints
|
||||
- Accidental → consider redesign
|
||||
|
||||
3. **Fix symptom or redesign?**
|
||||
- If Strike 3 (3rd attempt) → escalate to Layer 2
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When errors persist, trace to design layer:
|
||||
|
||||
```
|
||||
E0382 (moved value)
|
||||
↑ Ask: What design choice led to this ownership pattern?
|
||||
↑ Check: m09-domain (is this Entity or Value Object?)
|
||||
↑ Check: domain-* (what constraints apply?)
|
||||
```
|
||||
|
||||
| Persistent Error | Trace To | Question |
|
||||
|-----------------|----------|----------|
|
||||
| E0382 repeated | m02-resource | Should use Arc/Rc for sharing? |
|
||||
| E0597 repeated | m09-domain | Is scope boundary at right place? |
|
||||
| E0506/E0507 | m03-mutability | Should use interior mutability? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design decisions to implementation:
|
||||
|
||||
```
|
||||
"Data needs to be shared immutably"
|
||||
↓ Use: Arc<T> (multi-thread) or Rc<T> (single-thread)
|
||||
|
||||
"Data needs exclusive ownership"
|
||||
↓ Use: move semantics, take ownership
|
||||
|
||||
"Data is read-only view"
|
||||
↓ Use: &T (immutable borrow)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Ownership | Cost | Use When |
|
||||
|---------|-----------|------|----------|
|
||||
| Move | Transfer | Zero | Caller doesn't need data |
|
||||
| `&T` | Borrow | Zero | Read-only access |
|
||||
| `&mut T` | Exclusive borrow | Zero | Need to modify |
|
||||
| `clone()` | Duplicate | Alloc + copy | Actually need a copy |
|
||||
| `Rc<T>` | Shared (single) | Ref count | Single-thread sharing |
|
||||
| `Arc<T>` | Shared (multi) | Atomic ref count | Multi-thread sharing |
|
||||
| `Cow<T>` | Clone-on-write | Alloc if mutated | Might modify |
|
||||
|
||||
## Error Code Reference
|
||||
|
||||
| Error | Cause | Quick Fix |
|
||||
|-------|-------|-----------|
|
||||
| E0382 | Value moved | Clone, reference, or redesign ownership |
|
||||
| E0597 | Reference outlives owner | Extend owner scope or restructure |
|
||||
| E0506 | Assign while borrowed | End borrow before mutation |
|
||||
| E0507 | Move out of borrowed | Clone or use reference |
|
||||
| E0515 | Return local reference | Return owned value |
|
||||
| E0716 | Temporary dropped | Bind to variable |
|
||||
| E0106 | Missing lifetime | Add `'a` annotation |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| `.clone()` everywhere | Hides design issues | Design ownership properly |
|
||||
| Fight borrow checker | Increases complexity | Work with the compiler |
|
||||
| `'static` for everything | Restricts flexibility | Use appropriate lifetimes |
|
||||
| Leak with `Box::leak` | Memory leak | Proper lifetime design |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Need smart pointers | m02-resource |
|
||||
| Need interior mutability | m03-mutability |
|
||||
| Data is domain entity | m09-domain |
|
||||
| Learning ownership concepts | m14-mental-model |
|
||||
222
skills/m01-ownership/comparison.md
Normal file
222
skills/m01-ownership/comparison.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# Ownership: Comparison with Other Languages
|
||||
|
||||
## Rust vs C++
|
||||
|
||||
### Memory Management
|
||||
|
||||
| Aspect | Rust | C++ |
|
||||
|--------|------|-----|
|
||||
| Default | Move semantics | Copy semantics (pre-C++11) |
|
||||
| Move | `let b = a;` (a invalidated) | `auto b = std::move(a);` (a valid but unspecified) |
|
||||
| Copy | `let b = a.clone();` | `auto b = a;` |
|
||||
| Safety | Compile-time enforcement | Runtime responsibility |
|
||||
|
||||
### Rust Move vs C++ Move
|
||||
|
||||
```rust
|
||||
// Rust: after move, 'a' is INVALID
|
||||
let a = String::from("hello");
|
||||
let b = a; // a moved
|
||||
// println!("{}", a); // COMPILE ERROR
|
||||
|
||||
// Equivalent in C++:
|
||||
// std::string a = "hello";
|
||||
// std::string b = std::move(a);
|
||||
// std::cout << a; // UNDEFINED (compiles but buggy)
|
||||
```
|
||||
|
||||
### Smart Pointers
|
||||
|
||||
| Rust | C++ | Purpose |
|
||||
|------|-----|---------|
|
||||
| `Box<T>` | `std::unique_ptr<T>` | Unique ownership |
|
||||
| `Rc<T>` | `std::shared_ptr<T>` | Shared ownership |
|
||||
| `Arc<T>` | `std::shared_ptr<T>` + atomic | Thread-safe shared |
|
||||
| `RefCell<T>` | (manual runtime checks) | Interior mutability |
|
||||
|
||||
---
|
||||
|
||||
## Rust vs Go
|
||||
|
||||
### Memory Model
|
||||
|
||||
| Aspect | Rust | Go |
|
||||
|--------|------|-----|
|
||||
| Memory | Stack + heap, explicit | GC manages all |
|
||||
| Ownership | Enforced at compile-time | None (GC handles) |
|
||||
| Null | `Option<T>` | `nil` for pointers |
|
||||
| Concurrency | `Send`/`Sync` traits | Channels (less strict) |
|
||||
|
||||
### Sharing Data
|
||||
|
||||
```rust
|
||||
// Rust: explicit about sharing
|
||||
use std::sync::Arc;
|
||||
let data = Arc::new(vec![1, 2, 3]);
|
||||
let data_clone = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
println!("{:?}", data_clone);
|
||||
});
|
||||
|
||||
// Go: implicit sharing
|
||||
// data := []int{1, 2, 3}
|
||||
// go func() {
|
||||
// fmt.Println(data) // potential race condition
|
||||
// }()
|
||||
```
|
||||
|
||||
### Why No GC in Rust
|
||||
|
||||
1. **Deterministic destruction**: Resources freed exactly when scope ends
|
||||
2. **Zero-cost**: No GC pauses or overhead
|
||||
3. **Embeddable**: Works in OS kernels, embedded systems
|
||||
4. **Predictable latency**: Critical for real-time systems
|
||||
|
||||
---
|
||||
|
||||
## Rust vs Java/C#
|
||||
|
||||
### Reference Semantics
|
||||
|
||||
| Aspect | Rust | Java/C# |
|
||||
|--------|------|---------|
|
||||
| Objects | Owned by default | Reference by default |
|
||||
| Null | `Option<T>` | `null` (nullable) |
|
||||
| Immutability | Default | Must use `final`/`readonly` |
|
||||
| Copy | Explicit `.clone()` | Reference copy (shallow) |
|
||||
|
||||
### Comparison
|
||||
|
||||
```rust
|
||||
// Rust: clear ownership
|
||||
fn process(data: Vec<i32>) { // takes ownership
|
||||
// data is ours, will be freed at end
|
||||
}
|
||||
|
||||
let numbers = vec![1, 2, 3];
|
||||
process(numbers);
|
||||
// numbers is invalid here
|
||||
|
||||
// Java: ambiguous ownership
|
||||
// void process(List<Integer> data) {
|
||||
// // Who owns data? Caller? Callee? Both?
|
||||
// // Can caller still use it?
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rust vs Python
|
||||
|
||||
### Memory Model
|
||||
|
||||
| Aspect | Rust | Python |
|
||||
|--------|------|--------|
|
||||
| Typing | Static, compile-time | Dynamic, runtime |
|
||||
| Memory | Ownership-based | Reference counting + GC |
|
||||
| Mutability | Default immutable | Default mutable |
|
||||
| Performance | Native, zero-cost | Interpreted, higher overhead |
|
||||
|
||||
### Common Pattern Translation
|
||||
|
||||
```rust
|
||||
// Rust: borrowing iteration
|
||||
let items = vec!["a", "b", "c"];
|
||||
for item in &items {
|
||||
println!("{}", item);
|
||||
}
|
||||
// items still usable
|
||||
|
||||
// Python: iteration doesn't consume
|
||||
// items = ["a", "b", "c"]
|
||||
// for item in items:
|
||||
// print(item)
|
||||
// items still usable (different reason - ref counting)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unique Rust Concepts
|
||||
|
||||
### Concepts Other Languages Lack
|
||||
|
||||
1. **Borrow Checker**: No other mainstream language has compile-time borrow checking
|
||||
2. **Lifetimes**: Explicit annotation of reference validity
|
||||
3. **Move by Default**: Values move, not copy
|
||||
4. **No Null**: `Option<T>` instead of null pointers
|
||||
5. **Affine Types**: Values can be used at most once
|
||||
|
||||
### Learning Curve Areas
|
||||
|
||||
| Concept | Coming From | Key Insight |
|
||||
|---------|-------------|-------------|
|
||||
| Ownership | GC languages | Think about who "owns" data |
|
||||
| Borrowing | C/C++ | Like references but checked |
|
||||
| Lifetimes | Any | Explicit scope of validity |
|
||||
| Move | C++ | Move is default, not copy |
|
||||
|
||||
---
|
||||
|
||||
## Mental Model Shifts
|
||||
|
||||
### From GC Languages (Java, Go, Python)
|
||||
|
||||
```
|
||||
Before: "Memory just works, GC handles it"
|
||||
After: "I explicitly decide who owns data and when it's freed"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- Think about ownership at design time
|
||||
- Returning references requires lifetime thinking
|
||||
- No more `null` - use `Option<T>`
|
||||
|
||||
### From C/C++
|
||||
|
||||
```
|
||||
Before: "I manually manage memory and hope I get it right"
|
||||
After: "Compiler enforces correctness, I fight the borrow checker"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- Trust the compiler's errors
|
||||
- Move is the default (unlike C++ copy)
|
||||
- Smart pointers are idiomatic, not overhead
|
||||
|
||||
### From Functional Languages (Haskell, ML)
|
||||
|
||||
```
|
||||
Before: "Everything is immutable, copying is fine"
|
||||
After: "Mutability is explicit, ownership prevents aliasing"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- Mutability is safe because of ownership rules
|
||||
- No persistent data structures needed (usually)
|
||||
- Performance characteristics are explicit
|
||||
|
||||
---
|
||||
|
||||
## Performance Trade-offs
|
||||
|
||||
| Language | Memory Overhead | Latency | Throughput |
|
||||
|----------|-----------------|---------|------------|
|
||||
| Rust | Minimal (no GC) | Predictable | Excellent |
|
||||
| C++ | Minimal | Predictable | Excellent |
|
||||
| Go | GC overhead | GC pauses | Good |
|
||||
| Java | GC overhead | GC pauses | Good |
|
||||
| Python | High (ref counting + GC) | Variable | Lower |
|
||||
|
||||
### When Rust Ownership Wins
|
||||
|
||||
1. **Real-time systems**: No GC pauses
|
||||
2. **Embedded**: No runtime overhead
|
||||
3. **High-performance**: Zero-cost abstractions
|
||||
4. **Concurrent**: Data races prevented at compile time
|
||||
|
||||
### When GC Might Be Preferable
|
||||
|
||||
1. **Rapid prototyping**: Less mental overhead
|
||||
2. **Complex object graphs**: Cycles are tricky in Rust
|
||||
3. **GUI applications**: Object lifetimes are dynamic
|
||||
4. **Small programs**: Overhead doesn't matter
|
||||
339
skills/m01-ownership/examples/best-practices.md
Normal file
339
skills/m01-ownership/examples/best-practices.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# Ownership Best Practices
|
||||
|
||||
## API Design Patterns
|
||||
|
||||
### 1. Prefer Borrowing Over Ownership
|
||||
|
||||
```rust
|
||||
// BAD: takes ownership unnecessarily
|
||||
fn print_name(name: String) {
|
||||
println!("Name: {}", name);
|
||||
}
|
||||
|
||||
// GOOD: borrows instead
|
||||
fn print_name(name: &str) {
|
||||
println!("Name: {}", name);
|
||||
}
|
||||
|
||||
// Caller benefits:
|
||||
let name = String::from("Alice");
|
||||
print_name(&name); // can reuse name
|
||||
print_name(&name); // still valid
|
||||
```
|
||||
|
||||
### 2. Return Owned Values from Constructors
|
||||
|
||||
```rust
|
||||
// GOOD: return owned value
|
||||
impl User {
|
||||
fn new(name: &str) -> Self {
|
||||
User {
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: accept Into<String> for flexibility
|
||||
impl User {
|
||||
fn new(name: impl Into<String>) -> Self {
|
||||
User {
|
||||
name: name.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
let u1 = User::new("Alice"); // &str
|
||||
let u2 = User::new(String::from("Bob")); // String
|
||||
```
|
||||
|
||||
### 3. Use AsRef for Generic Borrowing
|
||||
|
||||
```rust
|
||||
// GOOD: accepts both &str and String
|
||||
fn process<S: AsRef<str>>(input: S) {
|
||||
let s = input.as_ref();
|
||||
println!("{}", s);
|
||||
}
|
||||
|
||||
process("literal"); // &str
|
||||
process(String::from("owned")); // String
|
||||
process(&String::from("ref")); // &String
|
||||
```
|
||||
|
||||
### 4. Cow for Clone-on-Write
|
||||
|
||||
```rust
|
||||
use std::borrow::Cow;
|
||||
|
||||
// Return borrowed when possible, owned when needed
|
||||
fn maybe_modify(s: &str, uppercase: bool) -> Cow<'_, str> {
|
||||
if uppercase {
|
||||
Cow::Owned(s.to_uppercase()) // allocates
|
||||
} else {
|
||||
Cow::Borrowed(s) // zero-cost
|
||||
}
|
||||
}
|
||||
|
||||
let input = "hello";
|
||||
let result = maybe_modify(input, false);
|
||||
// result is borrowed, no allocation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Struct Design Patterns
|
||||
|
||||
### 1. Owned Fields vs References
|
||||
|
||||
```rust
|
||||
// Use owned fields for most cases
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
// Use references only when lifetime is clear
|
||||
struct UserView<'a> {
|
||||
name: &'a str,
|
||||
email: &'a str,
|
||||
}
|
||||
|
||||
// Pattern: owned data + view for efficiency
|
||||
impl User {
|
||||
fn view(&self) -> UserView<'_> {
|
||||
UserView {
|
||||
name: &self.name,
|
||||
email: &self.email,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Builder Pattern with Ownership
|
||||
|
||||
```rust
|
||||
#[derive(Default)]
|
||||
struct RequestBuilder {
|
||||
url: Option<String>,
|
||||
method: Option<String>,
|
||||
body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl RequestBuilder {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// Take self by value for chaining
|
||||
fn url(mut self, url: impl Into<String>) -> Self {
|
||||
self.url = Some(url.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn method(mut self, method: impl Into<String>) -> Self {
|
||||
self.method = Some(method.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn build(self) -> Result<Request, Error> {
|
||||
Ok(Request {
|
||||
url: self.url.ok_or(Error::MissingUrl)?,
|
||||
method: self.method.unwrap_or_else(|| "GET".to_string()),
|
||||
body: self.body.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
let req = RequestBuilder::new()
|
||||
.url("https://example.com")
|
||||
.method("POST")
|
||||
.build()?;
|
||||
```
|
||||
|
||||
### 3. Interior Mutability When Needed
|
||||
|
||||
```rust
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
// Shared mutable state in single-threaded context
|
||||
struct Counter {
|
||||
value: Rc<RefCell<u32>>,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
fn new() -> Self {
|
||||
Counter {
|
||||
value: Rc::new(RefCell::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn increment(&self) {
|
||||
*self.value.borrow_mut() += 1;
|
||||
}
|
||||
|
||||
fn get(&self) -> u32 {
|
||||
*self.value.borrow()
|
||||
}
|
||||
|
||||
fn clone_handle(&self) -> Self {
|
||||
Counter {
|
||||
value: Rc::clone(&self.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collection Patterns
|
||||
|
||||
### 1. Efficient Iteration
|
||||
|
||||
```rust
|
||||
let items = vec![1, 2, 3, 4, 5];
|
||||
|
||||
// Iterate by reference (no move)
|
||||
for item in &items {
|
||||
println!("{}", item);
|
||||
}
|
||||
|
||||
// Iterate by mutable reference
|
||||
for item in &mut items.clone() {
|
||||
*item *= 2;
|
||||
}
|
||||
|
||||
// Consume with into_iter when done
|
||||
let sum: i32 = items.into_iter().sum();
|
||||
```
|
||||
|
||||
### 2. Collecting Results
|
||||
|
||||
```rust
|
||||
// Collect into owned collection
|
||||
let strings: Vec<String> = (0..5)
|
||||
.map(|i| format!("item_{}", i))
|
||||
.collect();
|
||||
|
||||
// Collect references
|
||||
let refs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
// Collect with transformation
|
||||
let result: Result<Vec<i32>, _> = ["1", "2", "3"]
|
||||
.iter()
|
||||
.map(|s| s.parse::<i32>())
|
||||
.collect();
|
||||
```
|
||||
|
||||
### 3. Entry API for Maps
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut map: HashMap<String, Vec<i32>> = HashMap::new();
|
||||
|
||||
// Efficient: don't search twice
|
||||
map.entry("key".to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(42);
|
||||
|
||||
// With entry modification
|
||||
map.entry("key".to_string())
|
||||
.and_modify(|v| v.push(43))
|
||||
.or_insert_with(|| vec![43]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling with Ownership
|
||||
|
||||
### 1. Preserve Context in Errors
|
||||
|
||||
```rust
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ParseError {
|
||||
input: String, // owns the problematic input
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Failed to parse '{}': {}", self.input, self.message)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(input: &str) -> Result<i32, ParseError> {
|
||||
input.parse().map_err(|_| ParseError {
|
||||
input: input.to_string(), // clone for error context
|
||||
message: "not a valid integer".to_string(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Ownership in Result Chains
|
||||
|
||||
```rust
|
||||
fn process_data(path: &str) -> Result<ProcessedData, Error> {
|
||||
let content = std::fs::read_to_string(path)?; // owned String
|
||||
let parsed = parse_content(&content)?; // borrow
|
||||
let processed = transform(parsed)?; // ownership moves
|
||||
Ok(processed) // return owned
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### 1. Avoid Unnecessary Clones
|
||||
|
||||
```rust
|
||||
// BAD: cloning just to compare
|
||||
fn contains_item(items: &[String], target: &str) -> bool {
|
||||
items.iter().any(|s| s.clone() == target) // unnecessary clone
|
||||
}
|
||||
|
||||
// GOOD: compare references
|
||||
fn contains_item(items: &[String], target: &str) -> bool {
|
||||
items.iter().any(|s| s == target) // String implements PartialEq<str>
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Slices for Flexibility
|
||||
|
||||
```rust
|
||||
// BAD: requires Vec
|
||||
fn sum(numbers: &Vec<i32>) -> i32 {
|
||||
numbers.iter().sum()
|
||||
}
|
||||
|
||||
// GOOD: accepts any slice
|
||||
fn sum(numbers: &[i32]) -> i32 {
|
||||
numbers.iter().sum()
|
||||
}
|
||||
|
||||
// Now works with:
|
||||
sum(&vec![1, 2, 3]); // Vec
|
||||
sum(&[1, 2, 3]); // array
|
||||
sum(&array[1..3]); // slice
|
||||
```
|
||||
|
||||
### 3. In-Place Mutation
|
||||
|
||||
```rust
|
||||
// BAD: allocates new String
|
||||
fn make_uppercase(s: &str) -> String {
|
||||
s.to_uppercase()
|
||||
}
|
||||
|
||||
// GOOD when you own the data: mutate in place
|
||||
fn make_uppercase(mut s: String) -> String {
|
||||
s.make_ascii_uppercase(); // in-place for ASCII
|
||||
s
|
||||
}
|
||||
```
|
||||
265
skills/m01-ownership/patterns/common-errors.md
Normal file
265
skills/m01-ownership/patterns/common-errors.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Common Ownership Errors & Fixes
|
||||
|
||||
## E0382: Use of Moved Value
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
let s = String::from("hello");
|
||||
let s2 = s; // s moved here
|
||||
println!("{}", s); // ERROR: value borrowed after move
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Clone (if ownership not needed)**
|
||||
```rust
|
||||
let s = String::from("hello");
|
||||
let s2 = s.clone(); // s is cloned
|
||||
println!("{}", s); // OK: s still valid
|
||||
```
|
||||
|
||||
**Option 2: Borrow (if modification not needed)**
|
||||
```rust
|
||||
let s = String::from("hello");
|
||||
let s2 = &s; // borrow, not move
|
||||
println!("{}", s); // OK
|
||||
println!("{}", s2); // OK
|
||||
```
|
||||
|
||||
**Option 3: Use Rc/Arc (for shared ownership)**
|
||||
```rust
|
||||
use std::rc::Rc;
|
||||
let s = Rc::new(String::from("hello"));
|
||||
let s2 = Rc::clone(&s); // shared ownership
|
||||
println!("{}", s); // OK
|
||||
println!("{}", s2); // OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0597: Borrowed Value Does Not Live Long Enough
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
fn get_str() -> &str {
|
||||
let s = String::from("hello");
|
||||
&s // ERROR: s dropped here, but reference returned
|
||||
}
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Return owned value**
|
||||
```rust
|
||||
fn get_str() -> String {
|
||||
String::from("hello") // return owned value
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Use 'static lifetime**
|
||||
```rust
|
||||
fn get_str() -> &'static str {
|
||||
"hello" // string literal has 'static lifetime
|
||||
}
|
||||
```
|
||||
|
||||
**Option 3: Accept reference parameter**
|
||||
```rust
|
||||
fn get_str<'a>(s: &'a str) -> &'a str {
|
||||
s // return reference with same lifetime as input
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0499: Cannot Borrow as Mutable More Than Once
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
let mut s = String::from("hello");
|
||||
let r1 = &mut s;
|
||||
let r2 = &mut s; // ERROR: second mutable borrow
|
||||
println!("{}, {}", r1, r2);
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Sequential borrows**
|
||||
```rust
|
||||
let mut s = String::from("hello");
|
||||
{
|
||||
let r1 = &mut s;
|
||||
r1.push_str(" world");
|
||||
} // r1 goes out of scope
|
||||
let r2 = &mut s; // OK: r1 no longer exists
|
||||
```
|
||||
|
||||
**Option 2: Use RefCell for interior mutability**
|
||||
```rust
|
||||
use std::cell::RefCell;
|
||||
let s = RefCell::new(String::from("hello"));
|
||||
let mut r1 = s.borrow_mut();
|
||||
// drop r1 before borrowing again
|
||||
drop(r1);
|
||||
let mut r2 = s.borrow_mut();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0502: Cannot Borrow as Mutable While Immutable Borrow Exists
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
let mut v = vec![1, 2, 3];
|
||||
let first = &v[0]; // immutable borrow
|
||||
v.push(4); // ERROR: mutable borrow while immutable exists
|
||||
println!("{}", first);
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Finish using immutable borrow first**
|
||||
```rust
|
||||
let mut v = vec![1, 2, 3];
|
||||
let first = v[0]; // copy value, not borrow
|
||||
v.push(4); // OK
|
||||
println!("{}", first); // OK: using copied value
|
||||
```
|
||||
|
||||
**Option 2: Clone before mutating**
|
||||
```rust
|
||||
let mut v = vec![1, 2, 3];
|
||||
let first = v[0].clone(); // if T: Clone
|
||||
v.push(4);
|
||||
println!("{}", first);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0507: Cannot Move Out of Borrowed Content
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
fn take_string(s: &String) {
|
||||
let moved = *s; // ERROR: cannot move out of borrowed content
|
||||
}
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Clone**
|
||||
```rust
|
||||
fn take_string(s: &String) {
|
||||
let cloned = s.clone();
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Take ownership in function signature**
|
||||
```rust
|
||||
fn take_string(s: String) { // take ownership
|
||||
let moved = s;
|
||||
}
|
||||
```
|
||||
|
||||
**Option 3: Use mem::take for Option/Default types**
|
||||
```rust
|
||||
fn take_from_option(opt: &mut Option<String>) -> Option<String> {
|
||||
std::mem::take(opt) // replaces with None, returns owned value
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0515: Return Local Reference
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
fn create_string() -> &String {
|
||||
let s = String::from("hello");
|
||||
&s // ERROR: cannot return reference to local variable
|
||||
}
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Return owned value**
|
||||
```rust
|
||||
fn create_string() -> String {
|
||||
String::from("hello")
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Use static/const**
|
||||
```rust
|
||||
fn get_static_str() -> &'static str {
|
||||
"hello"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0716: Temporary Value Dropped While Borrowed
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
let r: &str = &String::from("hello"); // ERROR: temporary dropped
|
||||
println!("{}", r);
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Bind to variable first**
|
||||
```rust
|
||||
let s = String::from("hello");
|
||||
let r: &str = &s;
|
||||
println!("{}", r);
|
||||
```
|
||||
|
||||
**Option 2: Use let binding with reference**
|
||||
```rust
|
||||
let r: &str = {
|
||||
let s = String::from("hello");
|
||||
// s.as_str() // ERROR: still temporary
|
||||
Box::leak(s.into_boxed_str()) // extreme: leak for 'static
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Loop Ownership Issues
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
let strings = vec![String::from("a"), String::from("b")];
|
||||
for s in strings {
|
||||
println!("{}", s);
|
||||
}
|
||||
// ERROR: strings moved into loop
|
||||
println!("{:?}", strings);
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Iterate by reference**
|
||||
```rust
|
||||
let strings = vec![String::from("a"), String::from("b")];
|
||||
for s in &strings {
|
||||
println!("{}", s);
|
||||
}
|
||||
println!("{:?}", strings); // OK
|
||||
```
|
||||
|
||||
**Option 2: Use iter()**
|
||||
```rust
|
||||
for s in strings.iter() {
|
||||
println!("{}", s);
|
||||
}
|
||||
```
|
||||
|
||||
**Option 3: Clone if needed**
|
||||
```rust
|
||||
for s in strings.clone() {
|
||||
// consumes cloned vec
|
||||
}
|
||||
println!("{:?}", strings); // original still available
|
||||
```
|
||||
229
skills/m01-ownership/patterns/lifetime-patterns.md
Normal file
229
skills/m01-ownership/patterns/lifetime-patterns.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Lifetime Patterns
|
||||
|
||||
## Basic Lifetime Annotation
|
||||
|
||||
### When Required
|
||||
```rust
|
||||
// ERROR: missing lifetime specifier
|
||||
fn longest(x: &str, y: &str) -> &str {
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
|
||||
// FIX: explicit lifetime
|
||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
```
|
||||
|
||||
### Lifetime Elision Rules
|
||||
1. Each input reference gets its own lifetime
|
||||
2. If one input lifetime, output uses same
|
||||
3. If `&self` or `&mut self`, output uses self's lifetime
|
||||
|
||||
```rust
|
||||
// These are equivalent (elision applies):
|
||||
fn first_word(s: &str) -> &str { ... }
|
||||
fn first_word<'a>(s: &'a str) -> &'a str { ... }
|
||||
|
||||
// Method with self (elision applies):
|
||||
impl MyStruct {
|
||||
fn get_ref(&self) -> &str { ... }
|
||||
// Equivalent to:
|
||||
fn get_ref<'a>(&'a self) -> &'a str { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Struct Lifetimes
|
||||
|
||||
### Struct Holding References
|
||||
```rust
|
||||
// Struct must declare lifetime for references
|
||||
struct Excerpt<'a> {
|
||||
part: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Excerpt<'a> {
|
||||
fn level(&self) -> i32 { 3 }
|
||||
|
||||
// Return reference tied to self's lifetime
|
||||
fn get_part(&self) -> &str {
|
||||
self.part
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Lifetimes in Struct
|
||||
```rust
|
||||
struct Multi<'a, 'b> {
|
||||
x: &'a str,
|
||||
y: &'b str,
|
||||
}
|
||||
|
||||
// Use when references may have different lifetimes
|
||||
fn make_multi<'a, 'b>(x: &'a str, y: &'b str) -> Multi<'a, 'b> {
|
||||
Multi { x, y }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 'static Lifetime
|
||||
|
||||
### When to Use
|
||||
```rust
|
||||
// String literals are 'static
|
||||
let s: &'static str = "hello";
|
||||
|
||||
// Owned data can be leaked to 'static
|
||||
let leaked: &'static str = Box::leak(String::from("hello").into_boxed_str());
|
||||
|
||||
// Thread spawn requires 'static or move
|
||||
std::thread::spawn(move || {
|
||||
// closure owns data, satisfies 'static
|
||||
});
|
||||
```
|
||||
|
||||
### Avoid Overusing 'static
|
||||
```rust
|
||||
// BAD: requires 'static unnecessarily
|
||||
fn process(s: &'static str) { ... }
|
||||
|
||||
// GOOD: use generic lifetime
|
||||
fn process<'a>(s: &'a str) { ... }
|
||||
// or
|
||||
fn process(s: &str) { ... } // lifetime elision
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Higher-Ranked Trait Bounds (HRTB)
|
||||
|
||||
### for<'a> Syntax
|
||||
```rust
|
||||
// Function that works with any lifetime
|
||||
fn apply_to_ref<F>(f: F)
|
||||
where
|
||||
F: for<'a> Fn(&'a str) -> &'a str,
|
||||
{
|
||||
let s = String::from("hello");
|
||||
let result = f(&s);
|
||||
println!("{}", result);
|
||||
}
|
||||
```
|
||||
|
||||
### Common Use: Closure Bounds
|
||||
```rust
|
||||
// Closure that borrows any lifetime
|
||||
fn filter_refs<F>(items: &[&str], pred: F) -> Vec<&str>
|
||||
where
|
||||
F: for<'a> Fn(&'a str) -> bool,
|
||||
{
|
||||
items.iter().copied().filter(|s| pred(s)).collect()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lifetime Bounds
|
||||
|
||||
### 'a: 'b (Outlives)
|
||||
```rust
|
||||
// 'a must live at least as long as 'b
|
||||
fn coerce<'a, 'b>(x: &'a str) -> &'b str
|
||||
where
|
||||
'a: 'b,
|
||||
{
|
||||
x
|
||||
}
|
||||
```
|
||||
|
||||
### T: 'a (Type Outlives Lifetime)
|
||||
```rust
|
||||
// T must live at least as long as 'a
|
||||
struct Wrapper<'a, T: 'a> {
|
||||
value: &'a T,
|
||||
}
|
||||
|
||||
// Common pattern with trait objects
|
||||
fn use_trait<'a, T: MyTrait + 'a>(t: &'a T) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Lifetime Mistakes
|
||||
|
||||
### Mistake 1: Returning Reference to Local
|
||||
```rust
|
||||
// WRONG
|
||||
fn dangle() -> &String {
|
||||
let s = String::from("hello");
|
||||
&s // s dropped, reference invalid
|
||||
}
|
||||
|
||||
// RIGHT
|
||||
fn no_dangle() -> String {
|
||||
String::from("hello")
|
||||
}
|
||||
```
|
||||
|
||||
### Mistake 2: Conflicting Lifetimes
|
||||
```rust
|
||||
// WRONG: might return reference to y which has shorter lifetime
|
||||
fn wrong<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
|
||||
y // ERROR: 'b might not live as long as 'a
|
||||
}
|
||||
|
||||
// RIGHT: use same lifetime or add bound
|
||||
fn right<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
y // OK: both have lifetime 'a
|
||||
}
|
||||
```
|
||||
|
||||
### Mistake 3: Struct Outlives Reference
|
||||
```rust
|
||||
// WRONG: s might outlive the string it references
|
||||
let r;
|
||||
{
|
||||
let s = String::from("hello");
|
||||
r = Excerpt { part: &s }; // ERROR
|
||||
}
|
||||
println!("{}", r.part); // s already dropped
|
||||
|
||||
// RIGHT: ensure source outlives struct
|
||||
let s = String::from("hello");
|
||||
let r = Excerpt { part: &s };
|
||||
println!("{}", r.part); // OK: s still in scope
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Subtyping and Variance
|
||||
|
||||
### Covariance
|
||||
```rust
|
||||
// &'a T is covariant in 'a
|
||||
// Can use &'long where &'short expected
|
||||
fn example<'short, 'long: 'short>(long_ref: &'long str) {
|
||||
let short_ref: &'short str = long_ref; // OK: covariance
|
||||
}
|
||||
```
|
||||
|
||||
### Invariance
|
||||
```rust
|
||||
// &'a mut T is invariant in 'a
|
||||
fn example<'a, 'b>(x: &'a mut &'b str, y: &'b str) {
|
||||
*x = y; // ERROR if 'a and 'b are different
|
||||
}
|
||||
```
|
||||
|
||||
### Practical Impact
|
||||
```rust
|
||||
// This works due to covariance
|
||||
fn accept_any<'a>(s: &'a str) { ... }
|
||||
|
||||
let s = String::from("hello");
|
||||
let long_lived: &str = &s;
|
||||
accept_any(long_lived); // 'long coerces to 'short
|
||||
```
|
||||
159
skills/m02-resource/SKILL.md
Normal file
159
skills/m02-resource/SKILL.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: m02-resource
|
||||
description: "CRITICAL: Use for smart pointers and resource management. Triggers: Box, Rc, Arc, Weak, RefCell, Cell, smart pointer, heap allocation, reference counting, RAII, Drop, should I use Box or Rc, when to use Arc vs Rc, 智能指针, 引用计数, 堆分配"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Resource Management
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**What ownership pattern does this resource need?**
|
||||
|
||||
Before choosing a smart pointer, understand:
|
||||
- Is ownership single or shared?
|
||||
- Is access single-threaded or multi-threaded?
|
||||
- Are there potential cycles?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Error | Don't Just Say | Ask Instead |
|
||||
|-------|----------------|-------------|
|
||||
| "Need heap allocation" | "Use Box" | Why can't this be on stack? |
|
||||
| Rc memory leak | "Use Weak" | Is the cycle necessary in design? |
|
||||
| RefCell panic | "Use try_borrow" | Is runtime check the right approach? |
|
||||
| Arc overhead complaint | "Accept it" | Is multi-thread access actually needed? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before choosing a smart pointer:
|
||||
|
||||
1. **What's the ownership model?**
|
||||
- Single owner → Box or owned value
|
||||
- Shared ownership → Rc/Arc
|
||||
- Weak reference → Weak
|
||||
|
||||
2. **What's the thread context?**
|
||||
- Single-thread → Rc, Cell, RefCell
|
||||
- Multi-thread → Arc, Mutex, RwLock
|
||||
|
||||
3. **Are there cycles?**
|
||||
- Yes → One direction must be Weak
|
||||
- No → Regular Rc/Arc is fine
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When pointer choice is unclear, trace to design:
|
||||
|
||||
```
|
||||
"Should I use Arc or Rc?"
|
||||
↑ Ask: Is this data shared across threads?
|
||||
↑ Check: m07-concurrency (thread model)
|
||||
↑ Check: domain-* (performance constraints)
|
||||
```
|
||||
|
||||
| Situation | Trace To | Question |
|
||||
|-----------|----------|----------|
|
||||
| Rc vs Arc confusion | m07-concurrency | What's the concurrency model? |
|
||||
| RefCell panics | m03-mutability | Is interior mutability right here? |
|
||||
| Memory leaks | m12-lifecycle | Where should cleanup happen? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Need single-owner heap data"
|
||||
↓ Use: Box<T>
|
||||
|
||||
"Need shared immutable data (single-thread)"
|
||||
↓ Use: Rc<T>
|
||||
|
||||
"Need shared immutable data (multi-thread)"
|
||||
↓ Use: Arc<T>
|
||||
|
||||
"Need to break reference cycle"
|
||||
↓ Use: Weak<T>
|
||||
|
||||
"Need shared mutable data"
|
||||
↓ Single-thread: Rc<RefCell<T>>
|
||||
↓ Multi-thread: Arc<Mutex<T>> or Arc<RwLock<T>>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Type | Ownership | Thread-Safe | Use When |
|
||||
|------|-----------|-------------|----------|
|
||||
| `Box<T>` | Single | Yes | Heap allocation, recursive types |
|
||||
| `Rc<T>` | Shared | No | Single-thread shared ownership |
|
||||
| `Arc<T>` | Shared | Yes | Multi-thread shared ownership |
|
||||
| `Weak<T>` | Weak ref | Same as Rc/Arc | Break reference cycles |
|
||||
| `Cell<T>` | Single | No | Interior mutability (Copy types) |
|
||||
| `RefCell<T>` | Single | No | Interior mutability (runtime check) |
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```
|
||||
Need heap allocation?
|
||||
├─ Yes → Single owner?
|
||||
│ ├─ Yes → Box<T>
|
||||
│ └─ No → Multi-thread?
|
||||
│ ├─ Yes → Arc<T>
|
||||
│ └─ No → Rc<T>
|
||||
└─ No → Stack allocation (default)
|
||||
|
||||
Have reference cycles?
|
||||
├─ Yes → Use Weak for one direction
|
||||
└─ No → Regular Rc/Arc
|
||||
|
||||
Need interior mutability?
|
||||
├─ Yes → Thread-safe needed?
|
||||
│ ├─ Yes → Mutex<T> or RwLock<T>
|
||||
│ └─ No → T: Copy? → Cell<T> : RefCell<T>
|
||||
└─ No → Use &mut T
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Problem | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Rc cycle leak | Mutual strong refs | Use Weak for one direction |
|
||||
| RefCell panic | Borrow conflict at runtime | Use try_borrow or restructure |
|
||||
| Arc overhead | Atomic ops in hot path | Consider Rc if single-threaded |
|
||||
| Box unnecessary | Data fits on stack | Remove Box |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Arc everywhere | Unnecessary atomic overhead | Use Rc for single-thread |
|
||||
| RefCell everywhere | Runtime panics | Design clear ownership |
|
||||
| Box for small types | Unnecessary allocation | Stack allocation |
|
||||
| Ignore Weak for cycles | Memory leaks | Design parent-child with Weak |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Ownership errors | m01-ownership |
|
||||
| Interior mutability details | m03-mutability |
|
||||
| Multi-thread context | m07-concurrency |
|
||||
| Resource lifecycle | m12-lifecycle |
|
||||
153
skills/m03-mutability/SKILL.md
Normal file
153
skills/m03-mutability/SKILL.md
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
name: m03-mutability
|
||||
description: "CRITICAL: Use for mutability issues. Triggers: E0596, E0499, E0502, cannot borrow as mutable, already borrowed as immutable, mut, &mut, interior mutability, Cell, RefCell, Mutex, RwLock, 可变性, 内部可变性, 借用冲突"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Mutability
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Why does this data need to change, and who can change it?**
|
||||
|
||||
Before adding interior mutability, understand:
|
||||
- Is mutation essential or accidental complexity?
|
||||
- Who should control mutation?
|
||||
- Is the mutation pattern safe?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Error | Don't Just Say | Ask Instead |
|
||||
|-------|----------------|-------------|
|
||||
| E0596 | "Add mut" | Should this really be mutable? |
|
||||
| E0499 | "Split borrows" | Is the data structure right? |
|
||||
| E0502 | "Separate scopes" | Why do we need both borrows? |
|
||||
| RefCell panic | "Use try_borrow" | Is runtime check appropriate? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before adding mutability:
|
||||
|
||||
1. **Is mutation necessary?**
|
||||
- Maybe transform → return new value
|
||||
- Maybe builder → construct immutably
|
||||
|
||||
2. **Who controls mutation?**
|
||||
- External caller → `&mut T`
|
||||
- Internal logic → interior mutability
|
||||
- Concurrent access → synchronized mutability
|
||||
|
||||
3. **What's the thread context?**
|
||||
- Single-thread → Cell/RefCell
|
||||
- Multi-thread → Mutex/RwLock/Atomic
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When mutability conflicts persist:
|
||||
|
||||
```
|
||||
E0499/E0502 (borrow conflicts)
|
||||
↑ Ask: Is the data structure designed correctly?
|
||||
↑ Check: m09-domain (should data be split?)
|
||||
↑ Check: m07-concurrency (is async involved?)
|
||||
```
|
||||
|
||||
| Persistent Error | Trace To | Question |
|
||||
|-----------------|----------|----------|
|
||||
| Repeated borrow conflicts | m09-domain | Should data be restructured? |
|
||||
| RefCell in async | m07-concurrency | Is Send/Sync needed? |
|
||||
| Mutex deadlocks | m07-concurrency | Is the lock design right? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Need mutable access from &self"
|
||||
↓ T: Copy → Cell<T>
|
||||
↓ T: !Copy → RefCell<T>
|
||||
|
||||
"Need thread-safe mutation"
|
||||
↓ Simple counters → AtomicXxx
|
||||
↓ Complex data → Mutex<T> or RwLock<T>
|
||||
|
||||
"Need shared mutable state"
|
||||
↓ Single-thread: Rc<RefCell<T>>
|
||||
↓ Multi-thread: Arc<Mutex<T>>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Borrow Rules
|
||||
|
||||
```
|
||||
At any time, you can have EITHER:
|
||||
├─ Multiple &T (immutable borrows)
|
||||
└─ OR one &mut T (mutable borrow)
|
||||
|
||||
Never both simultaneously.
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Thread-Safe | Runtime Cost | Use When |
|
||||
|---------|-------------|--------------|----------|
|
||||
| `&mut T` | N/A | Zero | Exclusive mutable access |
|
||||
| `Cell<T>` | No | Zero | Copy types, no refs needed |
|
||||
| `RefCell<T>` | No | Runtime check | Non-Copy, need runtime borrow |
|
||||
| `Mutex<T>` | Yes | Lock contention | Thread-safe mutation |
|
||||
| `RwLock<T>` | Yes | Lock contention | Many readers, few writers |
|
||||
| `Atomic*` | Yes | Minimal | Simple types (bool, usize) |
|
||||
|
||||
## Error Code Reference
|
||||
|
||||
| Error | Cause | Quick Fix |
|
||||
|-------|-------|-----------|
|
||||
| E0596 | Borrowing immutable as mutable | Add `mut` or redesign |
|
||||
| E0499 | Multiple mutable borrows | Restructure code flow |
|
||||
| E0502 | &mut while & exists | Separate borrow scopes |
|
||||
|
||||
---
|
||||
|
||||
## Interior Mutability Decision
|
||||
|
||||
| Scenario | Choose |
|
||||
|----------|--------|
|
||||
| T: Copy, single-thread | `Cell<T>` |
|
||||
| T: !Copy, single-thread | `RefCell<T>` |
|
||||
| T: Copy, multi-thread | `AtomicXxx` |
|
||||
| T: !Copy, multi-thread | `Mutex<T>` or `RwLock<T>` |
|
||||
| Read-heavy, multi-thread | `RwLock<T>` |
|
||||
| Simple flags/counters | `AtomicBool`, `AtomicUsize` |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| RefCell everywhere | Runtime panics | Clear ownership design |
|
||||
| Mutex for single-thread | Unnecessary overhead | RefCell |
|
||||
| Ignore RefCell panic | Hard to debug | Handle or restructure |
|
||||
| Lock inside hot loop | Performance killer | Batch operations |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Smart pointer choice | m02-resource |
|
||||
| Thread safety | m07-concurrency |
|
||||
| Data structure design | m09-domain |
|
||||
| Anti-patterns | m15-anti-pattern |
|
||||
165
skills/m04-zero-cost/SKILL.md
Normal file
165
skills/m04-zero-cost/SKILL.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
name: m04-zero-cost
|
||||
description: "CRITICAL: Use for generics, traits, zero-cost abstraction. Triggers: E0277, E0308, E0599, generic, trait, impl, dyn, where, monomorphization, static dispatch, dynamic dispatch, impl Trait, trait bound not satisfied, 泛型, 特征, 零成本抽象, 单态化"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Zero-Cost Abstraction
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Do we need compile-time or runtime polymorphism?**
|
||||
|
||||
Before choosing between generics and trait objects:
|
||||
- Is the type known at compile time?
|
||||
- Is a heterogeneous collection needed?
|
||||
- What's the performance priority?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Error | Don't Just Say | Ask Instead |
|
||||
|-------|----------------|-------------|
|
||||
| E0277 | "Add trait bound" | Is this abstraction at the right level? |
|
||||
| E0308 | "Fix the type" | Should types be unified or distinct? |
|
||||
| E0599 | "Import the trait" | Is the trait the right abstraction? |
|
||||
| E0038 | "Make object-safe" | Do we really need dynamic dispatch? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before adding trait bounds:
|
||||
|
||||
1. **What abstraction is needed?**
|
||||
- Same behavior, different types → trait
|
||||
- Different behavior, same type → enum
|
||||
- No abstraction needed → concrete type
|
||||
|
||||
2. **When is type known?**
|
||||
- Compile time → generics (static dispatch)
|
||||
- Runtime → trait objects (dynamic dispatch)
|
||||
|
||||
3. **What's the trade-off priority?**
|
||||
- Performance → generics
|
||||
- Compile time → trait objects
|
||||
- Flexibility → depends
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When type system fights back:
|
||||
|
||||
```
|
||||
E0277 (trait bound not satisfied)
|
||||
↑ Ask: Is the abstraction level correct?
|
||||
↑ Check: m09-domain (what behavior is being abstracted?)
|
||||
↑ Check: m05-type-driven (should use newtype?)
|
||||
```
|
||||
|
||||
| Persistent Error | Trace To | Question |
|
||||
|-----------------|----------|----------|
|
||||
| Complex trait bounds | m09-domain | Is the abstraction right? |
|
||||
| Object safety issues | m05-type-driven | Can typestate help? |
|
||||
| Type explosion | m10-performance | Accept dyn overhead? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Need to abstract over types with same behavior"
|
||||
↓ Types known at compile time → impl Trait or generics
|
||||
↓ Types determined at runtime → dyn Trait
|
||||
|
||||
"Need collection of different types"
|
||||
↓ Closed set → enum
|
||||
↓ Open set → Vec<Box<dyn Trait>>
|
||||
|
||||
"Need to return different types"
|
||||
↓ Same type → impl Trait
|
||||
↓ Different types → Box<dyn Trait>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Dispatch | Code Size | Runtime Cost |
|
||||
|---------|----------|-----------|--------------|
|
||||
| `fn foo<T: Trait>()` | Static | +bloat | Zero |
|
||||
| `fn foo(x: &dyn Trait)` | Dynamic | Minimal | vtable lookup |
|
||||
| `impl Trait` return | Static | +bloat | Zero |
|
||||
| `Box<dyn Trait>` | Dynamic | Minimal | Allocation + vtable |
|
||||
|
||||
## Syntax Comparison
|
||||
|
||||
```rust
|
||||
// Static dispatch - type known at compile time
|
||||
fn process(x: impl Display) { } // argument position
|
||||
fn process<T: Display>(x: T) { } // explicit generic
|
||||
fn get() -> impl Display { } // return position
|
||||
|
||||
// Dynamic dispatch - type determined at runtime
|
||||
fn process(x: &dyn Display) { } // reference
|
||||
fn process(x: Box<dyn Display>) { } // owned
|
||||
```
|
||||
|
||||
## Error Code Reference
|
||||
|
||||
| Error | Cause | Quick Fix |
|
||||
|-------|-------|-----------|
|
||||
| E0277 | Type doesn't impl trait | Add impl or change bound |
|
||||
| E0308 | Type mismatch | Check generic params |
|
||||
| E0599 | No method found | Import trait with `use` |
|
||||
| E0038 | Trait not object-safe | Use generics or redesign |
|
||||
|
||||
---
|
||||
|
||||
## Decision Guide
|
||||
|
||||
| Scenario | Choose | Why |
|
||||
|----------|--------|-----|
|
||||
| Performance critical | Generics | Zero runtime cost |
|
||||
| Heterogeneous collection | `dyn Trait` | Different types at runtime |
|
||||
| Plugin architecture | `dyn Trait` | Unknown types at compile |
|
||||
| Reduce compile time | `dyn Trait` | Less monomorphization |
|
||||
| Small, known type set | `enum` | No indirection |
|
||||
|
||||
---
|
||||
|
||||
## Object Safety
|
||||
|
||||
A trait is object-safe if it:
|
||||
- Doesn't have `Self: Sized` bound
|
||||
- Doesn't return `Self`
|
||||
- Doesn't have generic methods
|
||||
- Uses `where Self: Sized` for non-object-safe methods
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Over-generic everything | Compile time, complexity | Concrete types when possible |
|
||||
| `dyn` for known types | Unnecessary indirection | Generics |
|
||||
| Complex trait hierarchies | Hard to understand | Simpler design |
|
||||
| Ignore object safety | Limits flexibility | Plan for dyn if needed |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Type-driven design | m05-type-driven |
|
||||
| Domain abstraction | m09-domain |
|
||||
| Performance concerns | m10-performance |
|
||||
| Send/Sync bounds | m07-concurrency |
|
||||
175
skills/m05-type-driven/SKILL.md
Normal file
175
skills/m05-type-driven/SKILL.md
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
name: m05-type-driven
|
||||
description: "CRITICAL: Use for type-driven design. Triggers: type state, PhantomData, newtype, marker trait, builder pattern, make invalid states unrepresentable, compile-time validation, sealed trait, ZST, 类型状态, 新类型模式, 类型驱动设计"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Type-Driven Design
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**How can the type system prevent invalid states?**
|
||||
|
||||
Before reaching for runtime checks:
|
||||
- Can the compiler catch this error?
|
||||
- Can invalid states be unrepresentable?
|
||||
- Can the type encode the invariant?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Pattern | Don't Just Say | Ask Instead |
|
||||
|---------|----------------|-------------|
|
||||
| Primitive obsession | "It's just a string" | What does this value represent? |
|
||||
| Boolean flags | "Add an is_valid flag" | Can states be types? |
|
||||
| Optional everywhere | "Check for None" | Is absence really possible? |
|
||||
| Validation at runtime | "Return Err if invalid" | Can we validate at construction? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before adding runtime validation:
|
||||
|
||||
1. **Can the type encode the constraint?**
|
||||
- Numeric range → bounded types or newtypes
|
||||
- Valid states → type state pattern
|
||||
- Semantic meaning → newtype
|
||||
|
||||
2. **When is validation possible?**
|
||||
- At construction → validated newtype
|
||||
- At state transition → type state
|
||||
- Only at runtime → Result with clear error
|
||||
|
||||
3. **Who needs to know the invariant?**
|
||||
- Compiler → type-level encoding
|
||||
- API users → clear type signatures
|
||||
- Runtime only → documentation
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When type design is unclear:
|
||||
|
||||
```
|
||||
"Need to validate email format"
|
||||
↑ Ask: Is this a domain value object?
|
||||
↑ Check: m09-domain (Email as Value Object)
|
||||
↑ Check: domain-* (validation requirements)
|
||||
```
|
||||
|
||||
| Situation | Trace To | Question |
|
||||
|-----------|----------|----------|
|
||||
| What types to create | m09-domain | What's the domain model? |
|
||||
| State machine design | m09-domain | What are valid transitions? |
|
||||
| Marker trait usage | m04-zero-cost | Static or dynamic dispatch? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Need type-safe wrapper for primitives"
|
||||
↓ Newtype: struct UserId(u64);
|
||||
|
||||
"Need compile-time state validation"
|
||||
↓ Type State: Connection<Connected>
|
||||
|
||||
"Need to track phantom type parameters"
|
||||
↓ PhantomData: PhantomData<T>
|
||||
|
||||
"Need capability markers"
|
||||
↓ Marker Trait: trait Validated {}
|
||||
|
||||
"Need gradual construction"
|
||||
↓ Builder: Builder::new().field(x).build()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| Newtype | Type safety | `struct UserId(u64);` |
|
||||
| Type State | State machine | `Connection<Connected>` |
|
||||
| PhantomData | Variance/lifetime | `PhantomData<&'a T>` |
|
||||
| Marker Trait | Capability flag | `trait Validated {}` |
|
||||
| Builder | Gradual construction | `Builder::new().name("x").build()` |
|
||||
| Sealed Trait | Prevent external impl | `mod private { pub trait Sealed {} }` |
|
||||
|
||||
## Pattern Examples
|
||||
|
||||
### Newtype
|
||||
|
||||
```rust
|
||||
struct Email(String); // Not just any string
|
||||
|
||||
impl Email {
|
||||
pub fn new(s: &str) -> Result<Self, ValidationError> {
|
||||
// Validate once, trust forever
|
||||
validate_email(s)?;
|
||||
Ok(Self(s.to_string()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Type State
|
||||
|
||||
```rust
|
||||
struct Connection<State>(TcpStream, PhantomData<State>);
|
||||
|
||||
struct Disconnected;
|
||||
struct Connected;
|
||||
struct Authenticated;
|
||||
|
||||
impl Connection<Disconnected> {
|
||||
fn connect(self) -> Connection<Connected> { ... }
|
||||
}
|
||||
|
||||
impl Connection<Connected> {
|
||||
fn authenticate(self) -> Connection<Authenticated> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decision Guide
|
||||
|
||||
| Need | Pattern |
|
||||
|------|---------|
|
||||
| Type safety for primitives | Newtype |
|
||||
| Compile-time state validation | Type State |
|
||||
| Lifetime/variance markers | PhantomData |
|
||||
| Capability flags | Marker Trait |
|
||||
| Gradual construction | Builder |
|
||||
| Closed set of impls | Sealed Trait |
|
||||
| Zero-sized type marker | ZST struct |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Boolean flags for states | Runtime errors | Type state |
|
||||
| String for semantic types | No type safety | Newtype |
|
||||
| Option for uninitialized | Unclear invariant | Builder |
|
||||
| Public fields with invariants | Invariant violation | Private + validated new() |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Domain modeling | m09-domain |
|
||||
| Trait design | m04-zero-cost |
|
||||
| Error handling in constructors | m06-error-handling |
|
||||
| Anti-patterns | m15-anti-pattern |
|
||||
166
skills/m06-error-handling/SKILL.md
Normal file
166
skills/m06-error-handling/SKILL.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: m06-error-handling
|
||||
description: "CRITICAL: Use for error handling. Triggers: Result, Option, Error, ?, unwrap, expect, panic, anyhow, thiserror, when to panic vs return Result, custom error, error propagation, 错误处理, Result 用法, 什么时候用 panic"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Error Handling
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Is this failure expected or a bug?**
|
||||
|
||||
Before choosing error handling strategy:
|
||||
- Can this fail in normal operation?
|
||||
- Who should handle this failure?
|
||||
- What context does the caller need?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Pattern | Don't Just Say | Ask Instead |
|
||||
|---------|----------------|-------------|
|
||||
| unwrap panics | "Use ?" | Is None/Err actually possible here? |
|
||||
| Type mismatch on ? | "Use anyhow" | Are error types designed correctly? |
|
||||
| Lost error context | "Add .context()" | What does the caller need to know? |
|
||||
| Too many error variants | "Use Box<dyn Error>" | Is error granularity right? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before handling an error:
|
||||
|
||||
1. **What kind of failure is this?**
|
||||
- Expected → Result<T, E>
|
||||
- Absence normal → Option<T>
|
||||
- Bug/invariant → panic!
|
||||
- Unrecoverable → panic!
|
||||
|
||||
2. **Who handles this?**
|
||||
- Caller → propagate with ?
|
||||
- Current function → match/if-let
|
||||
- User → friendly error message
|
||||
- Programmer → panic with message
|
||||
|
||||
3. **What context is needed?**
|
||||
- Type of error → thiserror variants
|
||||
- Call chain → anyhow::Context
|
||||
- Debug info → anyhow or tracing
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When error strategy is unclear:
|
||||
|
||||
```
|
||||
"Should I return Result or Option?"
|
||||
↑ Ask: Is absence/failure normal or exceptional?
|
||||
↑ Check: m09-domain (what does domain say?)
|
||||
↑ Check: domain-* (error handling requirements)
|
||||
```
|
||||
|
||||
| Situation | Trace To | Question |
|
||||
|-----------|----------|----------|
|
||||
| Too many unwraps | m09-domain | Is the data model right? |
|
||||
| Error context design | m13-domain-error | What recovery is needed? |
|
||||
| Library vs app errors | m11-ecosystem | Who are the consumers? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Expected failure, library code"
|
||||
↓ Use: thiserror for typed errors
|
||||
|
||||
"Expected failure, application code"
|
||||
↓ Use: anyhow for ergonomic errors
|
||||
|
||||
"Absence is normal (find, get, lookup)"
|
||||
↓ Use: Option<T>
|
||||
|
||||
"Bug or invariant violation"
|
||||
↓ Use: panic!, assert!, unreachable!
|
||||
|
||||
"Need to propagate with context"
|
||||
↓ Use: .context("what was happening")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | When | Example |
|
||||
|---------|------|---------|
|
||||
| `Result<T, E>` | Recoverable error | `fn read() -> Result<String, io::Error>` |
|
||||
| `Option<T>` | Absence is normal | `fn find() -> Option<&Item>` |
|
||||
| `?` | Propagate error | `let data = file.read()?;` |
|
||||
| `unwrap()` | Dev/test only | `config.get("key").unwrap()` |
|
||||
| `expect()` | Invariant holds | `env.get("HOME").expect("HOME set")` |
|
||||
| `panic!` | Unrecoverable | `panic!("critical failure")` |
|
||||
|
||||
## Library vs Application
|
||||
|
||||
| Context | Error Crate | Why |
|
||||
|---------|-------------|-----|
|
||||
| Library | `thiserror` | Typed errors for consumers |
|
||||
| Application | `anyhow` | Ergonomic error handling |
|
||||
| Mixed | Both | thiserror at boundaries, anyhow internally |
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```
|
||||
Is failure expected?
|
||||
├─ Yes → Is absence the only "failure"?
|
||||
│ ├─ Yes → Option<T>
|
||||
│ └─ No → Result<T, E>
|
||||
│ ├─ Library → thiserror
|
||||
│ └─ Application → anyhow
|
||||
└─ No → Is it a bug?
|
||||
├─ Yes → panic!, assert!
|
||||
└─ No → Consider if really unrecoverable
|
||||
|
||||
Use ? → Need context?
|
||||
├─ Yes → .context("message")
|
||||
└─ No → Plain ?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `unwrap()` panic | Unhandled None/Err | Use `?` or match |
|
||||
| Type mismatch | Different error types | Use `anyhow` or `From` |
|
||||
| Lost context | `?` without context | Add `.context()` |
|
||||
| `cannot use ?` | Missing Result return | Return `Result<(), E>` |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| `.unwrap()` everywhere | Panics in production | `.expect("reason")` or `?` |
|
||||
| Ignore errors silently | Bugs hidden | Handle or propagate |
|
||||
| `panic!` for expected errors | Bad UX, no recovery | Result |
|
||||
| Box<dyn Error> everywhere | Lost type info | thiserror |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Domain error strategy | m13-domain-error |
|
||||
| Crate boundaries | m11-ecosystem |
|
||||
| Type-safe errors | m05-type-driven |
|
||||
| Mental models | m14-mental-model |
|
||||
332
skills/m06-error-handling/examples/library-vs-app.md
Normal file
332
skills/m06-error-handling/examples/library-vs-app.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# Error Handling: Library vs Application
|
||||
|
||||
## Library Error Design
|
||||
|
||||
### Principles
|
||||
1. **Define specific error types** - Don't use `anyhow` in libraries
|
||||
2. **Implement std::error::Error** - For compatibility
|
||||
3. **Provide error variants** - Let users match on errors
|
||||
4. **Include source errors** - Enable error chains
|
||||
5. **Be `Send + Sync`** - For async compatibility
|
||||
|
||||
### Example: Library Error Type
|
||||
```rust
|
||||
// lib.rs
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DatabaseError {
|
||||
#[error("connection failed: {host}:{port}")]
|
||||
ConnectionFailed {
|
||||
host: String,
|
||||
port: u16,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("query failed: {query}")]
|
||||
QueryFailed {
|
||||
query: String,
|
||||
#[source]
|
||||
source: SqlError,
|
||||
},
|
||||
|
||||
#[error("record not found: {table}.{id}")]
|
||||
NotFound { table: String, id: String },
|
||||
|
||||
#[error("constraint violation: {0}")]
|
||||
ConstraintViolation(String),
|
||||
}
|
||||
|
||||
// Public Result alias
|
||||
pub type Result<T> = std::result::Result<T, DatabaseError>;
|
||||
|
||||
// Library functions
|
||||
pub fn connect(host: &str, port: u16) -> Result<Connection> {
|
||||
// ...
|
||||
}
|
||||
|
||||
pub fn query(conn: &Connection, sql: &str) -> Result<Rows> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Library Usage of Errors
|
||||
```rust
|
||||
impl Database {
|
||||
pub fn get_user(&self, id: &str) -> Result<User> {
|
||||
let rows = self.query(&format!("SELECT * FROM users WHERE id = '{}'", id))?;
|
||||
|
||||
rows.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| DatabaseError::NotFound {
|
||||
table: "users".to_string(),
|
||||
id: id.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Application Error Design
|
||||
|
||||
### Principles
|
||||
1. **Use anyhow for convenience** - Or custom unified error
|
||||
2. **Add context liberally** - Help debugging
|
||||
3. **Log at boundaries** - Don't log in libraries
|
||||
4. **Convert to user-friendly messages** - For display
|
||||
|
||||
### Example: Application Error Handling
|
||||
```rust
|
||||
// main.rs
|
||||
use anyhow::{Context, Result};
|
||||
use tracing::{error, info};
|
||||
|
||||
async fn run_server() -> Result<()> {
|
||||
let config = load_config()
|
||||
.context("failed to load configuration")?;
|
||||
|
||||
let db = Database::connect(&config.db_url)
|
||||
.await
|
||||
.context("failed to connect to database")?;
|
||||
|
||||
let server = Server::new(config.port)
|
||||
.context("failed to create server")?;
|
||||
|
||||
info!("Server starting on port {}", config.port);
|
||||
|
||||
server.run(db).await
|
||||
.context("server error")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::init();
|
||||
|
||||
if let Err(e) = run_server().await {
|
||||
error!("Application error: {:#}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Converting Library Errors
|
||||
```rust
|
||||
use mylib::DatabaseError;
|
||||
|
||||
async fn get_user_handler(id: &str) -> Result<Response> {
|
||||
match db.get_user(id).await {
|
||||
Ok(user) => Ok(Response::json(user)),
|
||||
|
||||
Err(DatabaseError::NotFound { .. }) => {
|
||||
Ok(Response::not_found("User not found"))
|
||||
}
|
||||
|
||||
Err(DatabaseError::ConnectionFailed { .. }) => {
|
||||
error!("Database connection failed");
|
||||
Ok(Response::internal_error("Service unavailable"))
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
error!("Database error: {}", e);
|
||||
Err(e.into()) // Convert to anyhow::Error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ - Use anyhow or unified error │
|
||||
│ - Add context at boundaries │
|
||||
│ - Log errors │
|
||||
│ - Convert to user messages │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
│ calls
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Service Layer │
|
||||
│ - Map between error types │
|
||||
│ - Add business context │
|
||||
│ - Handle recoverable errors │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
│ calls
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Library Layer │
|
||||
│ - Define specific error types │
|
||||
│ - Use thiserror │
|
||||
│ - Include source errors │
|
||||
│ - No logging │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### HTTP API Error Response
|
||||
```rust
|
||||
use axum::{response::IntoResponse, http::StatusCode};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
code: String,
|
||||
}
|
||||
|
||||
enum AppError {
|
||||
NotFound(String),
|
||||
BadRequest(String),
|
||||
Internal(anyhow::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let (status, error, code) = match self {
|
||||
AppError::NotFound(msg) => {
|
||||
(StatusCode::NOT_FOUND, msg, "NOT_FOUND")
|
||||
}
|
||||
AppError::BadRequest(msg) => {
|
||||
(StatusCode::BAD_REQUEST, msg, "BAD_REQUEST")
|
||||
}
|
||||
AppError::Internal(e) => {
|
||||
tracing::error!("Internal error: {:#}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal server error".to_string(),
|
||||
"INTERNAL_ERROR",
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let body = ErrorResponse {
|
||||
error,
|
||||
code: code.to_string(),
|
||||
};
|
||||
|
||||
(status, axum::Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CLI Error Handling
|
||||
```rust
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(short, long)]
|
||||
config: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run() {
|
||||
eprintln!("Error: {:#}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let config = std::fs::read_to_string(&args.config)
|
||||
.context(format!("Failed to read config file: {}", args.config))?;
|
||||
|
||||
let parsed: Config = toml::from_str(&config)
|
||||
.context("Failed to parse config file")?;
|
||||
|
||||
process(parsed)?;
|
||||
|
||||
println!("Done!");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Error Handling
|
||||
|
||||
### Testing Error Cases
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_not_found_error() {
|
||||
let result = db.get_user("nonexistent");
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(DatabaseError::NotFound { table, id })
|
||||
if table == "users" && id == "nonexistent"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_message() {
|
||||
let err = DatabaseError::NotFound {
|
||||
table: "users".to_string(),
|
||||
id: "123".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(err.to_string(), "record not found: users.123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_chain() {
|
||||
let io_err = std::io::Error::new(
|
||||
std::io::ErrorKind::ConnectionRefused,
|
||||
"connection refused"
|
||||
);
|
||||
|
||||
let err = DatabaseError::ConnectionFailed {
|
||||
host: "localhost".to_string(),
|
||||
port: 5432,
|
||||
source: io_err,
|
||||
};
|
||||
|
||||
// Check source is preserved
|
||||
assert!(err.source().is_some());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing with anyhow
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_with_context() -> anyhow::Result<()> {
|
||||
let result = process("valid input")?;
|
||||
assert_eq!(result, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_context() {
|
||||
let err = process("invalid")
|
||||
.context("processing failed")
|
||||
.unwrap_err();
|
||||
|
||||
// Check error chain contains expected text
|
||||
let chain = format!("{:#}", err);
|
||||
assert!(chain.contains("processing failed"));
|
||||
}
|
||||
}
|
||||
```
|
||||
404
skills/m06-error-handling/patterns/error-patterns.md
Normal file
404
skills/m06-error-handling/patterns/error-patterns.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# Error Handling Patterns
|
||||
|
||||
## The ? Operator
|
||||
|
||||
### Basic Usage
|
||||
```rust
|
||||
fn read_config() -> Result<Config, io::Error> {
|
||||
let content = std::fs::read_to_string("config.toml")?;
|
||||
let config: Config = toml::from_str(&content)?; // needs From impl
|
||||
Ok(config)
|
||||
}
|
||||
```
|
||||
|
||||
### With Different Error Types
|
||||
```rust
|
||||
use std::error::Error;
|
||||
|
||||
// Box<dyn Error> for quick prototyping
|
||||
fn process() -> Result<(), Box<dyn Error>> {
|
||||
let file = std::fs::read_to_string("data.txt")?;
|
||||
let num: i32 = file.trim().parse()?; // different error type
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Conversion with From
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
enum MyError {
|
||||
Io(std::io::Error),
|
||||
Parse(std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for MyError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
MyError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::ParseIntError> for MyError {
|
||||
fn from(err: std::num::ParseIntError) -> Self {
|
||||
MyError::Parse(err)
|
||||
}
|
||||
}
|
||||
|
||||
fn process() -> Result<i32, MyError> {
|
||||
let content = std::fs::read_to_string("num.txt")?; // auto-converts
|
||||
let num: i32 = content.trim().parse()?; // auto-converts
|
||||
Ok(num)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Type Design
|
||||
|
||||
### Simple Enum Error
|
||||
```rust
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ConfigError {
|
||||
NotFound,
|
||||
InvalidFormat,
|
||||
MissingField(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ConfigError::NotFound => write!(f, "configuration file not found"),
|
||||
ConfigError::InvalidFormat => write!(f, "invalid configuration format"),
|
||||
ConfigError::MissingField(field) => write!(f, "missing field: {}", field),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
```
|
||||
|
||||
### Error with Source (Wrapping)
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub struct AppError {
|
||||
kind: AppErrorKind,
|
||||
source: Option<Box<dyn std::error::Error + Send + Sync>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AppErrorKind {
|
||||
Config,
|
||||
Database,
|
||||
Network,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.kind {
|
||||
AppErrorKind::Config => write!(f, "configuration error"),
|
||||
AppErrorKind::Database => write!(f, "database error"),
|
||||
AppErrorKind::Network => write!(f, "network error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AppError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.source.as_ref().map(|e| e.as_ref() as _)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using thiserror
|
||||
|
||||
### Basic Usage
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DataError {
|
||||
#[error("file not found: {path}")]
|
||||
NotFound { path: String },
|
||||
|
||||
#[error("invalid data format")]
|
||||
InvalidFormat,
|
||||
|
||||
#[error("IO error")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("parse error: {0}")]
|
||||
Parse(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
// Usage
|
||||
fn load_data(path: &str) -> Result<Data, DataError> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(|_| DataError::NotFound { path: path.to_string() })?;
|
||||
let num: i32 = content.trim().parse()?; // auto-converts with #[from]
|
||||
Ok(Data { value: num })
|
||||
}
|
||||
```
|
||||
|
||||
### Transparent Wrapper
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error(transparent)]
|
||||
pub struct MyError(#[from] InnerError);
|
||||
|
||||
// Useful for newtype error wrappers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using anyhow
|
||||
|
||||
### For Applications
|
||||
```rust
|
||||
use anyhow::{Context, Result, bail, ensure};
|
||||
|
||||
fn process_file(path: &str) -> Result<Data> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.context("failed to read config file")?;
|
||||
|
||||
ensure!(!content.is_empty(), "config file is empty");
|
||||
|
||||
let data: Data = serde_json::from_str(&content)
|
||||
.context("failed to parse JSON")?;
|
||||
|
||||
if data.version < 1 {
|
||||
bail!("unsupported config version: {}", data.version);
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let data = process_file("config.json")
|
||||
.context("failed to load configuration")?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Error Chain
|
||||
```rust
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn deep_function() -> Result<()> {
|
||||
std::fs::read_to_string("missing.txt")
|
||||
.context("failed to read file")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn middle_function() -> Result<()> {
|
||||
deep_function()
|
||||
.context("failed in deep function")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn top_function() -> Result<()> {
|
||||
middle_function()
|
||||
.context("failed in middle function")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Error output shows full chain:
|
||||
// Error: failed in middle function
|
||||
// Caused by:
|
||||
// 0: failed in deep function
|
||||
// 1: failed to read file
|
||||
// 2: No such file or directory (os error 2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Option Handling
|
||||
|
||||
### Converting Option to Result
|
||||
```rust
|
||||
fn find_user(id: u32) -> Option<User> { ... }
|
||||
|
||||
// Using ok_or for static error
|
||||
fn get_user(id: u32) -> Result<User, &'static str> {
|
||||
find_user(id).ok_or("user not found")
|
||||
}
|
||||
|
||||
// Using ok_or_else for dynamic error
|
||||
fn get_user(id: u32) -> Result<User, String> {
|
||||
find_user(id).ok_or_else(|| format!("user {} not found", id))
|
||||
}
|
||||
```
|
||||
|
||||
### Chaining Options
|
||||
```rust
|
||||
fn get_nested_value(data: &Data) -> Option<&str> {
|
||||
data.config
|
||||
.as_ref()?
|
||||
.nested
|
||||
.as_ref()?
|
||||
.value
|
||||
.as_deref()
|
||||
}
|
||||
|
||||
// Equivalent with and_then
|
||||
fn get_nested_value(data: &Data) -> Option<&str> {
|
||||
data.config
|
||||
.as_ref()
|
||||
.and_then(|c| c.nested.as_ref())
|
||||
.and_then(|n| n.value.as_deref())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Result Combinators
|
||||
|
||||
### map and map_err
|
||||
```rust
|
||||
fn parse_port(s: &str) -> Result<u16, ParseError> {
|
||||
s.parse::<u16>()
|
||||
.map_err(|e| ParseError::InvalidPort(e))
|
||||
}
|
||||
|
||||
fn get_url(config: &Config) -> Result<String, Error> {
|
||||
config.url()
|
||||
.map(|u| format!("https://{}", u))
|
||||
}
|
||||
```
|
||||
|
||||
### and_then (flatMap)
|
||||
```rust
|
||||
fn validate_and_save(input: &str) -> Result<(), Error> {
|
||||
validate(input)
|
||||
.and_then(|valid| save(valid))
|
||||
.and_then(|saved| notify(saved))
|
||||
}
|
||||
```
|
||||
|
||||
### unwrap_or and unwrap_or_else
|
||||
```rust
|
||||
// Default value
|
||||
let port = config.port().unwrap_or(8080);
|
||||
|
||||
// Computed default
|
||||
let port = config.port().unwrap_or_else(|| find_free_port());
|
||||
|
||||
// Default for Result
|
||||
let data = load_data().unwrap_or_default();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Early Return vs Combinators
|
||||
|
||||
### Early Return Style
|
||||
```rust
|
||||
fn process(input: &str) -> Result<Output, Error> {
|
||||
let step1 = validate(input)?;
|
||||
if !step1.is_valid {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
|
||||
let step2 = transform(step1)?;
|
||||
let step3 = save(step2)?;
|
||||
|
||||
Ok(step3)
|
||||
}
|
||||
```
|
||||
|
||||
### Combinator Style
|
||||
```rust
|
||||
fn process(input: &str) -> Result<Output, Error> {
|
||||
validate(input)
|
||||
.and_then(|s| {
|
||||
if s.is_valid {
|
||||
Ok(s)
|
||||
} else {
|
||||
Err(Error::Invalid)
|
||||
}
|
||||
})
|
||||
.and_then(transform)
|
||||
.and_then(save)
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use Which
|
||||
|
||||
| Style | Best For |
|
||||
|-------|----------|
|
||||
| Early return (`?`) | Most cases, clearer flow |
|
||||
| Combinators | Functional pipelines, one-liners |
|
||||
| Match | Complex branching on errors |
|
||||
|
||||
---
|
||||
|
||||
## Panic vs Result
|
||||
|
||||
### When to Panic
|
||||
```rust
|
||||
// 1. Unrecoverable programmer error
|
||||
fn get_config() -> &'static Config {
|
||||
CONFIG.get().expect("config must be initialized")
|
||||
}
|
||||
|
||||
// 2. In tests
|
||||
#[test]
|
||||
fn test_parsing() {
|
||||
let result = parse("valid").unwrap(); // OK in tests
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
// 3. Prototype/examples
|
||||
fn main() {
|
||||
let data = load().unwrap(); // OK for quick examples
|
||||
}
|
||||
```
|
||||
|
||||
### When to Return Result
|
||||
```rust
|
||||
// 1. Any I/O operation
|
||||
fn read_file(path: &str) -> Result<String, io::Error>
|
||||
|
||||
// 2. User input validation
|
||||
fn parse_port(s: &str) -> Result<u16, ParseError>
|
||||
|
||||
// 3. Network operations
|
||||
async fn fetch(url: &str) -> Result<Response, Error>
|
||||
|
||||
// 4. Anything that can fail at runtime
|
||||
fn connect(addr: &str) -> Result<Connection, Error>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Context Best Practices
|
||||
|
||||
### Add Context at Boundaries
|
||||
```rust
|
||||
fn load_user_config(user_id: u64) -> Result<Config, Error> {
|
||||
let path = format!("/home/{}/config.toml", user_id);
|
||||
|
||||
std::fs::read_to_string(&path)
|
||||
.context(format!("failed to read config for user {}", user_id))?
|
||||
// NOT: .context("failed to read file") // too generic
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Include Relevant Data
|
||||
```rust
|
||||
// Good: includes the problematic value
|
||||
fn parse_age(s: &str) -> Result<u8, Error> {
|
||||
s.parse()
|
||||
.context(format!("invalid age value: '{}'", s))
|
||||
}
|
||||
|
||||
// Bad: no context about what failed
|
||||
fn parse_age(s: &str) -> Result<u8, Error> {
|
||||
s.parse()
|
||||
.context("parse error")
|
||||
}
|
||||
```
|
||||
222
skills/m07-concurrency/SKILL.md
Normal file
222
skills/m07-concurrency/SKILL.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
name: m07-concurrency
|
||||
description: "CRITICAL: Use for concurrency/async. Triggers: E0277 Send Sync, cannot be sent between threads, thread, spawn, channel, mpsc, Mutex, RwLock, Atomic, async, await, Future, tokio, deadlock, race condition, 并发, 线程, 异步, 死锁"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Concurrency
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Is this CPU-bound or I/O-bound, and what's the sharing model?**
|
||||
|
||||
Before choosing concurrency primitives:
|
||||
- What's the workload type?
|
||||
- What data needs to be shared?
|
||||
- What's the thread safety requirement?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Error | Don't Just Say | Ask Instead |
|
||||
|-------|----------------|-------------|
|
||||
| E0277 Send | "Add Send bound" | Should this type cross threads? |
|
||||
| E0277 Sync | "Wrap in Mutex" | Is shared access really needed? |
|
||||
| Future not Send | "Use spawn_local" | Is async the right choice? |
|
||||
| Deadlock | "Reorder locks" | Is the locking design correct? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before adding concurrency:
|
||||
|
||||
1. **What's the workload?**
|
||||
- CPU-bound → threads (std::thread, rayon)
|
||||
- I/O-bound → async (tokio, async-std)
|
||||
- Mixed → hybrid approach
|
||||
|
||||
2. **What's the sharing model?**
|
||||
- No sharing → message passing (channels)
|
||||
- Immutable sharing → Arc<T>
|
||||
- Mutable sharing → Arc<Mutex<T>> or Arc<RwLock<T>>
|
||||
|
||||
3. **What are the Send/Sync requirements?**
|
||||
- Cross-thread ownership → Send
|
||||
- Cross-thread references → Sync
|
||||
- Single-thread async → spawn_local
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑ (MANDATORY)
|
||||
|
||||
**CRITICAL**: Don't just fix the error. Trace UP to find domain constraints.
|
||||
|
||||
### Domain Detection Table
|
||||
|
||||
| Context Keywords | Load Domain Skill | Key Constraint |
|
||||
|-----------------|-------------------|----------------|
|
||||
| Web API, HTTP, axum, actix, handler | **domain-web** | Handlers run on any thread |
|
||||
| 交易, 支付, trading, payment | **domain-fintech** | Audit + thread safety |
|
||||
| gRPC, kubernetes, microservice | **domain-cloud-native** | Distributed tracing |
|
||||
| CLI, terminal, clap | **domain-cli** | Usually single-thread OK |
|
||||
|
||||
### Example: Web API + Rc Error
|
||||
|
||||
```
|
||||
"Rc cannot be sent between threads" in Web API context
|
||||
↑ DETECT: "Web API" → Load domain-web
|
||||
↑ FIND: domain-web says "Shared state must be thread-safe"
|
||||
↑ FIND: domain-web says "Rc in state" is Common Mistake
|
||||
↓ DESIGN: Use Arc<T> with State extractor
|
||||
↓ IMPL: axum::extract::State<Arc<AppConfig>>
|
||||
```
|
||||
|
||||
### Generic Trace
|
||||
|
||||
```
|
||||
"Send not satisfied for my type"
|
||||
↑ Ask: What domain is this? Load domain-* skill
|
||||
↑ Ask: Does this type need to cross thread boundaries?
|
||||
↑ Check: m09-domain (is the data model correct?)
|
||||
```
|
||||
|
||||
| Situation | Trace To | Question |
|
||||
|-----------|----------|----------|
|
||||
| Send/Sync in Web | **domain-web** | What's the state management pattern? |
|
||||
| Send/Sync in CLI | **domain-cli** | Is multi-thread really needed? |
|
||||
| Mutex vs channels | m09-domain | Shared state or message passing? |
|
||||
| Async vs threads | m10-performance | What's the workload profile? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Need parallelism for CPU work"
|
||||
↓ Use: std::thread or rayon
|
||||
|
||||
"Need concurrency for I/O"
|
||||
↓ Use: async/await with tokio
|
||||
|
||||
"Need to share immutable data across threads"
|
||||
↓ Use: Arc<T>
|
||||
|
||||
"Need to share mutable data across threads"
|
||||
↓ Use: Arc<Mutex<T>> or Arc<RwLock<T>>
|
||||
↓ Or: channels for message passing
|
||||
|
||||
"Need simple atomic operations"
|
||||
↓ Use: AtomicBool, AtomicUsize, etc.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Send/Sync Markers
|
||||
|
||||
| Marker | Meaning | Example |
|
||||
|--------|---------|---------|
|
||||
| `Send` | Can transfer ownership between threads | Most types |
|
||||
| `Sync` | Can share references between threads | `Arc<T>` |
|
||||
| `!Send` | Must stay on one thread | `Rc<T>` |
|
||||
| `!Sync` | No shared refs across threads | `RefCell<T>` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Thread-Safe | Blocking | Use When |
|
||||
|---------|-------------|----------|----------|
|
||||
| `std::thread` | Yes | Yes | CPU-bound parallelism |
|
||||
| `async/await` | Yes | No | I/O-bound concurrency |
|
||||
| `Mutex<T>` | Yes | Yes | Shared mutable state |
|
||||
| `RwLock<T>` | Yes | Yes | Read-heavy shared state |
|
||||
| `mpsc::channel` | Yes | Optional | Message passing |
|
||||
| `Arc<Mutex<T>>` | Yes | Yes | Shared mutable across threads |
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```
|
||||
What type of work?
|
||||
├─ CPU-bound → std::thread or rayon
|
||||
├─ I/O-bound → async/await
|
||||
└─ Mixed → hybrid (spawn_blocking)
|
||||
|
||||
Need to share data?
|
||||
├─ No → message passing (channels)
|
||||
├─ Immutable → Arc<T>
|
||||
└─ Mutable →
|
||||
├─ Read-heavy → Arc<RwLock<T>>
|
||||
└─ Write-heavy → Arc<Mutex<T>>
|
||||
└─ Simple counter → AtomicUsize
|
||||
|
||||
Async context?
|
||||
├─ Type is Send → tokio::spawn
|
||||
├─ Type is !Send → spawn_local
|
||||
└─ Blocking code → spawn_blocking
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| E0277 `Send` not satisfied | Non-Send in async | Use Arc or spawn_local |
|
||||
| E0277 `Sync` not satisfied | Non-Sync shared | Wrap with Mutex |
|
||||
| Deadlock | Lock ordering | Consistent lock order |
|
||||
| `future is not Send` | Non-Send across await | Drop before await |
|
||||
| `MutexGuard` across await | Guard held during suspend | Scope guard properly |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Arc<Mutex<T>> everywhere | Contention, complexity | Message passing |
|
||||
| thread::sleep in async | Blocks executor | tokio::time::sleep |
|
||||
| Holding locks across await | Blocks other tasks | Scope locks tightly |
|
||||
| Ignoring deadlock risk | Hard to debug | Lock ordering, try_lock |
|
||||
|
||||
---
|
||||
|
||||
## Async-Specific Patterns
|
||||
|
||||
### Avoid MutexGuard Across Await
|
||||
|
||||
```rust
|
||||
// Bad: guard held across await
|
||||
let guard = mutex.lock().await;
|
||||
do_async().await; // guard still held!
|
||||
|
||||
// Good: scope the lock
|
||||
{
|
||||
let guard = mutex.lock().await;
|
||||
// use guard
|
||||
} // guard dropped
|
||||
do_async().await;
|
||||
```
|
||||
|
||||
### Non-Send Types in Async
|
||||
|
||||
```rust
|
||||
// Rc is !Send, can't cross await in spawned task
|
||||
// Option 1: use Arc instead
|
||||
// Option 2: use spawn_local (single-thread runtime)
|
||||
// Option 3: ensure Rc is dropped before .await
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Smart pointer choice | m02-resource |
|
||||
| Interior mutability | m03-mutability |
|
||||
| Performance tuning | m10-performance |
|
||||
| Domain concurrency needs | domain-* |
|
||||
312
skills/m07-concurrency/comparison.md
Normal file
312
skills/m07-concurrency/comparison.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# Concurrency: Comparison with Other Languages
|
||||
|
||||
## Rust vs Go
|
||||
|
||||
### Concurrency Model
|
||||
|
||||
| Aspect | Rust | Go |
|
||||
|--------|------|-----|
|
||||
| Model | Ownership + Send/Sync | CSP (Communicating Sequential Processes) |
|
||||
| Primitives | Arc, Mutex, channels | goroutines, channels |
|
||||
| Safety | Compile-time | Runtime (race detector) |
|
||||
| Async | async/await + runtime | Built-in scheduler |
|
||||
|
||||
### Goroutines vs Rust Tasks
|
||||
|
||||
```rust
|
||||
// Rust: explicit about thread safety
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
let data = Arc::new(Mutex::new(vec![]));
|
||||
let data_clone = Arc::clone(&data);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut guard = data_clone.lock().await;
|
||||
guard.push(1); // Safe: Mutex protects access
|
||||
});
|
||||
|
||||
// Go: implicit sharing (potential race)
|
||||
// data := []int{}
|
||||
// go func() {
|
||||
// data = append(data, 1) // RACE CONDITION!
|
||||
// }()
|
||||
```
|
||||
|
||||
### Channel Comparison
|
||||
|
||||
```rust
|
||||
// Rust: typed channels with ownership
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<String>(100);
|
||||
|
||||
tokio::spawn(async move {
|
||||
tx.send("hello".to_string()).await.unwrap();
|
||||
// tx is moved, can't be used elsewhere
|
||||
});
|
||||
|
||||
// Go: channels are more flexible but less safe
|
||||
// ch := make(chan string, 100)
|
||||
// go func() {
|
||||
// ch <- "hello"
|
||||
// // ch can still be used anywhere
|
||||
// }()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rust vs Java
|
||||
|
||||
### Thread Safety Model
|
||||
|
||||
| Aspect | Rust | Java |
|
||||
|--------|------|------|
|
||||
| Safety | Compile-time (Send/Sync) | Runtime (synchronized, volatile) |
|
||||
| Null | No null (Option) | NullPointerException risk |
|
||||
| Locks | RAII (drop releases) | try-finally or try-with-resources |
|
||||
| Memory | No GC | GC with stop-the-world |
|
||||
|
||||
### Synchronization Comparison
|
||||
|
||||
```rust
|
||||
// Rust: lock is tied to data
|
||||
use std::sync::Mutex;
|
||||
|
||||
let data = Mutex::new(vec![1, 2, 3]);
|
||||
{
|
||||
let mut guard = data.lock().unwrap();
|
||||
guard.push(4);
|
||||
} // lock released automatically
|
||||
|
||||
// Java: lock and data are separate
|
||||
// List<Integer> data = new ArrayList<>();
|
||||
// synchronized(data) {
|
||||
// data.add(4);
|
||||
// } // easy to forget synchronization elsewhere
|
||||
```
|
||||
|
||||
### Thread Pool Comparison
|
||||
|
||||
```rust
|
||||
// Rust: rayon for data parallelism
|
||||
use rayon::prelude::*;
|
||||
|
||||
let sum: i32 = (0..1000)
|
||||
.into_par_iter()
|
||||
.map(|x| x * x)
|
||||
.sum();
|
||||
|
||||
// Java: Stream API
|
||||
// int sum = IntStream.range(0, 1000)
|
||||
// .parallel()
|
||||
// .map(x -> x * x)
|
||||
// .sum();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rust vs C++
|
||||
|
||||
### Safety Guarantees
|
||||
|
||||
| Aspect | Rust | C++ |
|
||||
|--------|------|-----|
|
||||
| Data races | Prevented at compile-time | Undefined behavior |
|
||||
| Deadlocks | Not prevented (same as C++) | Not prevented |
|
||||
| Thread safety | Send/Sync traits | Convention only |
|
||||
| Memory ordering | Explicit Ordering enum | memory_order enum |
|
||||
|
||||
### Atomic Comparison
|
||||
|
||||
```rust
|
||||
// Rust: clear memory ordering
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
let counter = AtomicI32::new(0);
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
let value = counter.load(Ordering::Acquire);
|
||||
|
||||
// C++: similar but without safety
|
||||
// std::atomic<int> counter{0};
|
||||
// counter.fetch_add(1, std::memory_order_seq_cst);
|
||||
// int value = counter.load(std::memory_order_acquire);
|
||||
```
|
||||
|
||||
### Mutex Comparison
|
||||
|
||||
```rust
|
||||
// Rust: data protected by Mutex
|
||||
use std::sync::Mutex;
|
||||
|
||||
struct SafeCounter {
|
||||
count: Mutex<i32>, // Mutex contains the data
|
||||
}
|
||||
|
||||
impl SafeCounter {
|
||||
fn increment(&self) {
|
||||
*self.count.lock().unwrap() += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// C++: mutex separate from data (error-prone)
|
||||
// class Counter {
|
||||
// std::mutex mtx;
|
||||
// int count; // NOT protected by type system
|
||||
// public:
|
||||
// void increment() {
|
||||
// std::lock_guard<std::mutex> lock(mtx);
|
||||
// count++;
|
||||
// }
|
||||
// void unsafe_increment() {
|
||||
// count++; // Compiles! But wrong.
|
||||
// }
|
||||
// };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Models Comparison
|
||||
|
||||
| Language | Model | Runtime |
|
||||
|----------|-------|---------|
|
||||
| Rust | async/await, zero-cost | tokio, async-std (bring your own) |
|
||||
| Go | goroutines | Built-in scheduler |
|
||||
| JavaScript | async/await, Promises | Event loop (single-threaded) |
|
||||
| Python | async/await | asyncio (single-threaded) |
|
||||
| Java | CompletableFuture, Virtual Threads | ForkJoinPool, Loom |
|
||||
|
||||
### Rust vs JavaScript Async
|
||||
|
||||
```rust
|
||||
// Rust: async requires explicit runtime, can use multiple threads
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let results = tokio::join!(
|
||||
fetch("url1"), // runs concurrently
|
||||
fetch("url2"),
|
||||
);
|
||||
}
|
||||
|
||||
// JavaScript: single-threaded event loop
|
||||
// async function main() {
|
||||
// const results = await Promise.all([
|
||||
// fetch("url1"),
|
||||
// fetch("url2"),
|
||||
// ]);
|
||||
// }
|
||||
```
|
||||
|
||||
### Rust vs Python Async
|
||||
|
||||
```rust
|
||||
// Rust: true parallelism possible
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
let handles: Vec<_> = urls
|
||||
.into_iter()
|
||||
.map(|url| tokio::spawn(fetch(url))) // spawns on thread pool
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Python: asyncio is single-threaded (use ProcessPoolExecutor for CPU)
|
||||
# async def main():
|
||||
# tasks = [asyncio.create_task(fetch(url)) for url in urls]
|
||||
# await asyncio.gather(*tasks) # all on same thread
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Send and Sync: Rust's Unique Feature
|
||||
|
||||
No other mainstream language has compile-time thread safety markers:
|
||||
|
||||
| Trait | Meaning | Auto-impl |
|
||||
|-------|---------|-----------|
|
||||
| `Send` | Safe to transfer between threads | Most types |
|
||||
| `Sync` | Safe to share `&T` between threads | Types with thread-safe `&` |
|
||||
| `!Send` | Must stay on one thread | Rc, raw pointers |
|
||||
| `!Sync` | References can't be shared | RefCell, Cell |
|
||||
|
||||
### Why This Matters
|
||||
|
||||
```rust
|
||||
// Rust PREVENTS this at compile time:
|
||||
use std::rc::Rc;
|
||||
|
||||
let rc = Rc::new(42);
|
||||
std::thread::spawn(move || {
|
||||
println!("{}", rc); // ERROR: Rc is not Send
|
||||
});
|
||||
|
||||
// In other languages, this would be a runtime bug:
|
||||
// - Go: race detector might catch it
|
||||
// - Java: undefined behavior
|
||||
// - Python: GIL usually saves you
|
||||
// - C++: undefined behavior
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Aspect | Rust | Go | Java | C++ |
|
||||
|--------|------|-----|------|-----|
|
||||
| Thread overhead | System threads or M:N | M:N (goroutines) | System or virtual | System threads |
|
||||
| Context switch | OS-level or cooperative | Cheap (goroutines) | OS-level | OS-level |
|
||||
| Memory | Predictable (no GC) | GC pauses | GC pauses | Predictable |
|
||||
| Async overhead | Zero-cost futures | Runtime overhead | Boxing overhead | Depends |
|
||||
|
||||
### When to Use What
|
||||
|
||||
| Scenario | Best Choice |
|
||||
|----------|-------------|
|
||||
| CPU-bound parallelism | Rust (rayon), C++ |
|
||||
| I/O-bound concurrency | Rust (tokio), Go, Node.js |
|
||||
| Low latency required | Rust, C++ |
|
||||
| Rapid development | Go, Python |
|
||||
| Complex concurrent state | Rust (compile-time safety) |
|
||||
|
||||
---
|
||||
|
||||
## Mental Model Shifts
|
||||
|
||||
### From Go
|
||||
|
||||
```
|
||||
Before: "Just use goroutines and channels"
|
||||
After: "Explicitly declare what can be shared and how"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- `Arc<Mutex<T>>` instead of implicit sharing
|
||||
- Compiler enforces thread safety
|
||||
- Async needs explicit runtime
|
||||
|
||||
### From Java
|
||||
|
||||
```
|
||||
Before: "synchronized everywhere, hope for the best"
|
||||
After: "Types encode thread safety, compiler enforces"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- No need for synchronized keyword
|
||||
- Mutex contains data, not separate
|
||||
- No GC pauses in critical sections
|
||||
|
||||
### From C++
|
||||
|
||||
```
|
||||
Before: "Be careful, read the docs, use sanitizers"
|
||||
After: "Compiler catches data races, trust the type system"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- Send/Sync replace convention
|
||||
- RAII locks are mandatory, not optional
|
||||
- Much harder to write incorrect concurrent code
|
||||
396
skills/m07-concurrency/examples/thread-patterns.md
Normal file
396
skills/m07-concurrency/examples/thread-patterns.md
Normal file
@@ -0,0 +1,396 @@
|
||||
# Thread-Based Concurrency Patterns
|
||||
|
||||
## Thread Spawning Best Practices
|
||||
|
||||
### Basic Thread Spawn
|
||||
```rust
|
||||
use std::thread;
|
||||
|
||||
fn main() {
|
||||
let handle = thread::spawn(|| {
|
||||
println!("Hello from thread!");
|
||||
42 // return value
|
||||
});
|
||||
|
||||
let result = handle.join().unwrap();
|
||||
println!("Thread returned: {}", result);
|
||||
}
|
||||
```
|
||||
|
||||
### Named Threads for Debugging
|
||||
```rust
|
||||
use std::thread;
|
||||
|
||||
let builder = thread::Builder::new()
|
||||
.name("worker-1".to_string())
|
||||
.stack_size(32 * 1024); // 32KB stack
|
||||
|
||||
let handle = builder.spawn(|| {
|
||||
println!("Thread name: {:?}", thread::current().name());
|
||||
}).unwrap();
|
||||
```
|
||||
|
||||
### Scoped Threads (No 'static Required)
|
||||
```rust
|
||||
use std::thread;
|
||||
|
||||
fn process_data(data: &[u32]) -> Vec<u32> {
|
||||
thread::scope(|s| {
|
||||
let handles: Vec<_> = data
|
||||
.chunks(2)
|
||||
.map(|chunk| {
|
||||
s.spawn(|| {
|
||||
chunk.iter().map(|x| x * 2).collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
handles
|
||||
.into_iter()
|
||||
.flat_map(|h| h.join().unwrap())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let data = vec![1, 2, 3, 4, 5, 6];
|
||||
let result = process_data(&data); // No 'static needed!
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shared State Patterns
|
||||
|
||||
### Arc + Mutex (Read-Write)
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
fn shared_counter() {
|
||||
let counter = Arc::new(Mutex::new(0));
|
||||
let mut handles = vec![];
|
||||
|
||||
for _ in 0..10 {
|
||||
let counter = Arc::clone(&counter);
|
||||
let handle = thread::spawn(move || {
|
||||
let mut num = counter.lock().unwrap();
|
||||
*num += 1;
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
println!("Result: {}", *counter.lock().unwrap());
|
||||
}
|
||||
```
|
||||
|
||||
### Arc + RwLock (Read-Heavy)
|
||||
```rust
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
|
||||
fn read_heavy_cache() {
|
||||
let cache = Arc::new(RwLock::new(vec![1, 2, 3]));
|
||||
|
||||
// Many readers
|
||||
for i in 0..5 {
|
||||
let cache = Arc::clone(&cache);
|
||||
thread::spawn(move || {
|
||||
let data = cache.read().unwrap();
|
||||
println!("Reader {}: {:?}", i, *data);
|
||||
});
|
||||
}
|
||||
|
||||
// Occasional writer
|
||||
{
|
||||
let cache = Arc::clone(&cache);
|
||||
thread::spawn(move || {
|
||||
let mut data = cache.write().unwrap();
|
||||
data.push(4);
|
||||
println!("Writer: added element");
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Atomic for Simple Types
|
||||
```rust
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
fn atomic_counter() {
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
let mut handles = vec![];
|
||||
|
||||
for _ in 0..10 {
|
||||
let counter = Arc::clone(&counter);
|
||||
handles.push(thread::spawn(move || {
|
||||
for _ in 0..1000 {
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
println!("Result: {}", counter.load(Ordering::SeqCst));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Channel Patterns
|
||||
|
||||
### MPSC Channel
|
||||
```rust
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
fn producer_consumer() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
// Multiple producers
|
||||
for i in 0..3 {
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
for j in 0..5 {
|
||||
tx.send(format!("msg {}-{}", i, j)).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
drop(tx); // Drop original sender
|
||||
|
||||
// Single consumer
|
||||
for received in rx {
|
||||
println!("Got: {}", received);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sync Channel (Bounded)
|
||||
```rust
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
fn bounded_channel() {
|
||||
let (tx, rx) = mpsc::sync_channel(2); // buffer size 2
|
||||
|
||||
thread::spawn(move || {
|
||||
for i in 0..5 {
|
||||
println!("Sending {}", i);
|
||||
tx.send(i).unwrap(); // blocks if buffer full
|
||||
println!("Sent {}", i);
|
||||
}
|
||||
});
|
||||
|
||||
thread::sleep(std::time::Duration::from_millis(500));
|
||||
for received in rx {
|
||||
println!("Received: {}", received);
|
||||
thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Thread Pool Patterns
|
||||
|
||||
### Using rayon for Parallel Iteration
|
||||
```rust
|
||||
use rayon::prelude::*;
|
||||
|
||||
fn parallel_map() {
|
||||
let numbers: Vec<i32> = (0..1000).collect();
|
||||
|
||||
let squares: Vec<i32> = numbers
|
||||
.par_iter() // parallel iterator
|
||||
.map(|x| x * x)
|
||||
.collect();
|
||||
|
||||
println!("Processed {} items", squares.len());
|
||||
}
|
||||
|
||||
fn parallel_filter_map() {
|
||||
let data: Vec<String> = get_data();
|
||||
|
||||
let results: Vec<_> = data
|
||||
.par_iter()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| expensive_process(s))
|
||||
.collect();
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Thread Pool with crossbeam
|
||||
```rust
|
||||
use crossbeam::channel;
|
||||
use std::thread;
|
||||
|
||||
fn custom_pool(num_workers: usize) {
|
||||
let (tx, rx) = channel::bounded::<Box<dyn FnOnce() + Send>>(100);
|
||||
|
||||
// Spawn workers
|
||||
let workers: Vec<_> = (0..num_workers)
|
||||
.map(|_| {
|
||||
let rx = rx.clone();
|
||||
thread::spawn(move || {
|
||||
while let Ok(task) = rx.recv() {
|
||||
task();
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Submit tasks
|
||||
for i in 0..100 {
|
||||
tx.send(Box::new(move || {
|
||||
println!("Processing task {}", i);
|
||||
})).unwrap();
|
||||
}
|
||||
|
||||
drop(tx); // Close channel
|
||||
|
||||
for worker in workers {
|
||||
worker.join().unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Synchronization Primitives
|
||||
|
||||
### Barrier (Wait for All)
|
||||
```rust
|
||||
use std::sync::{Arc, Barrier};
|
||||
use std::thread;
|
||||
|
||||
fn barrier_example() {
|
||||
let barrier = Arc::new(Barrier::new(3));
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..3 {
|
||||
let barrier = Arc::clone(&barrier);
|
||||
handles.push(thread::spawn(move || {
|
||||
println!("Thread {} starting", i);
|
||||
thread::sleep(std::time::Duration::from_millis(i as u64 * 100));
|
||||
|
||||
barrier.wait(); // All threads wait here
|
||||
|
||||
println!("Thread {} after barrier", i);
|
||||
}));
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Condvar (Condition Variable)
|
||||
```rust
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::thread;
|
||||
|
||||
fn condvar_example() {
|
||||
let pair = Arc::new((Mutex::new(false), Condvar::new()));
|
||||
let pair_clone = Arc::clone(&pair);
|
||||
|
||||
// Waiter thread
|
||||
let waiter = thread::spawn(move || {
|
||||
let (lock, cvar) = &*pair_clone;
|
||||
let mut started = lock.lock().unwrap();
|
||||
while !*started {
|
||||
started = cvar.wait(started).unwrap();
|
||||
}
|
||||
println!("Waiter: condition met!");
|
||||
});
|
||||
|
||||
// Notifier
|
||||
thread::sleep(std::time::Duration::from_millis(100));
|
||||
let (lock, cvar) = &*pair;
|
||||
{
|
||||
let mut started = lock.lock().unwrap();
|
||||
*started = true;
|
||||
}
|
||||
cvar.notify_one();
|
||||
|
||||
waiter.join().unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Once (One-Time Initialization)
|
||||
```rust
|
||||
use std::sync::Once;
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
static mut CONFIG: Option<Config> = None;
|
||||
|
||||
fn get_config() -> &'static Config {
|
||||
INIT.call_once(|| {
|
||||
unsafe {
|
||||
CONFIG = Some(load_config());
|
||||
}
|
||||
});
|
||||
unsafe { CONFIG.as_ref().unwrap() }
|
||||
}
|
||||
|
||||
// Better: use once_cell or lazy_static
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static CONFIG: Lazy<Config> = Lazy::new(|| {
|
||||
load_config()
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling in Threads
|
||||
|
||||
### Handling Panics
|
||||
```rust
|
||||
use std::thread;
|
||||
|
||||
fn handle_panic() {
|
||||
let handle = thread::spawn(|| {
|
||||
panic!("Thread panicked!");
|
||||
});
|
||||
|
||||
match handle.join() {
|
||||
Ok(_) => println!("Thread completed successfully"),
|
||||
Err(e) => {
|
||||
if let Some(s) = e.downcast_ref::<&str>() {
|
||||
println!("Thread panicked with: {}", s);
|
||||
} else if let Some(s) = e.downcast_ref::<String>() {
|
||||
println!("Thread panicked with: {}", s);
|
||||
} else {
|
||||
println!("Thread panicked with unknown error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Catching Panics
|
||||
```rust
|
||||
use std::panic;
|
||||
|
||||
fn catch_panic() {
|
||||
let result = panic::catch_unwind(|| {
|
||||
risky_operation()
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(value) => println!("Success: {:?}", value),
|
||||
Err(_) => println!("Operation panicked, continuing..."),
|
||||
}
|
||||
}
|
||||
```
|
||||
409
skills/m07-concurrency/patterns/async-patterns.md
Normal file
409
skills/m07-concurrency/patterns/async-patterns.md
Normal file
@@ -0,0 +1,409 @@
|
||||
# Async Patterns in Rust
|
||||
|
||||
## Task Spawning
|
||||
|
||||
### Basic Spawn
|
||||
```rust
|
||||
use tokio::task;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Spawn a task that runs concurrently
|
||||
let handle = task::spawn(async {
|
||||
expensive_computation().await
|
||||
});
|
||||
|
||||
// Do other work while task runs
|
||||
other_work().await;
|
||||
|
||||
// Wait for result
|
||||
let result = handle.await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Spawn with Shared State
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
async fn process_with_state() {
|
||||
let state = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
let handles: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
let state = Arc::clone(&state);
|
||||
tokio::spawn(async move {
|
||||
let mut guard = state.lock().await;
|
||||
guard.push(i);
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Wait for all tasks
|
||||
for handle in handles {
|
||||
handle.await.unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Select Pattern
|
||||
|
||||
### Racing Multiple Futures
|
||||
```rust
|
||||
use tokio::select;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
async fn first_response() {
|
||||
select! {
|
||||
result = fetch_from_server_a() => {
|
||||
println!("A responded first: {:?}", result);
|
||||
}
|
||||
result = fetch_from_server_b() => {
|
||||
println!("B responded first: {:?}", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Select with Timeout
|
||||
```rust
|
||||
use tokio::time::timeout;
|
||||
|
||||
async fn with_timeout() -> Result<Data, Error> {
|
||||
select! {
|
||||
result = fetch_data() => result,
|
||||
_ = sleep(Duration::from_secs(5)) => {
|
||||
Err(Error::Timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Or use timeout directly
|
||||
async fn with_timeout2() -> Result<Data, Error> {
|
||||
timeout(Duration::from_secs(5), fetch_data())
|
||||
.await
|
||||
.map_err(|_| Error::Timeout)?
|
||||
}
|
||||
```
|
||||
|
||||
### Select with Channel
|
||||
```rust
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
async fn process_messages(mut rx: mpsc::Receiver<Message>) {
|
||||
loop {
|
||||
select! {
|
||||
Some(msg) = rx.recv() => {
|
||||
handle_message(msg).await;
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
println!("Shutting down...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Channel Patterns
|
||||
|
||||
### MPSC (Multi-Producer, Single-Consumer)
|
||||
```rust
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
async fn producer_consumer() {
|
||||
let (tx, mut rx) = mpsc::channel(100);
|
||||
|
||||
// Spawn producers
|
||||
for i in 0..3 {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
tx.send(format!("Message from {}", i)).await.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
// Drop original sender so channel closes
|
||||
drop(tx);
|
||||
|
||||
// Consume
|
||||
while let Some(msg) = rx.recv().await {
|
||||
println!("Received: {}", msg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Oneshot (Single-Shot Response)
|
||||
```rust
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
async fn request_response() {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let result = compute_something().await;
|
||||
tx.send(result).unwrap();
|
||||
});
|
||||
|
||||
// Wait for response
|
||||
let response = rx.await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Broadcast (Multi-Consumer)
|
||||
```rust
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
async fn pub_sub() {
|
||||
let (tx, _) = broadcast::channel(16);
|
||||
|
||||
// Subscribe multiple consumers
|
||||
let mut rx1 = tx.subscribe();
|
||||
let mut rx2 = tx.subscribe();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(msg) = rx1.recv().await {
|
||||
println!("Consumer 1: {}", msg);
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(msg) = rx2.recv().await {
|
||||
println!("Consumer 2: {}", msg);
|
||||
}
|
||||
});
|
||||
|
||||
// Publish
|
||||
tx.send("Hello").unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Watch (Single Latest Value)
|
||||
```rust
|
||||
use tokio::sync::watch;
|
||||
|
||||
async fn config_updates() {
|
||||
let (tx, mut rx) = watch::channel(Config::default());
|
||||
|
||||
// Consumer watches for changes
|
||||
tokio::spawn(async move {
|
||||
while rx.changed().await.is_ok() {
|
||||
let config = rx.borrow();
|
||||
apply_config(&config);
|
||||
}
|
||||
});
|
||||
|
||||
// Update config
|
||||
tx.send(Config::new()).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structured Concurrency
|
||||
|
||||
### JoinSet for Task Groups
|
||||
```rust
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
async fn parallel_fetch(urls: Vec<String>) -> Vec<Result<Response, Error>> {
|
||||
let mut set = JoinSet::new();
|
||||
|
||||
for url in urls {
|
||||
set.spawn(async move {
|
||||
fetch(&url).await
|
||||
});
|
||||
}
|
||||
|
||||
let mut results = vec![];
|
||||
while let Some(res) = set.join_next().await {
|
||||
results.push(res.unwrap());
|
||||
}
|
||||
results
|
||||
}
|
||||
```
|
||||
|
||||
### Scoped Tasks (no 'static)
|
||||
```rust
|
||||
// Using tokio-scoped or async-scoped crate
|
||||
use async_scoped::TokioScope;
|
||||
|
||||
async fn scoped_example(data: &[u32]) {
|
||||
let results = TokioScope::scope_and_block(|scope| {
|
||||
for item in data {
|
||||
scope.spawn(async move {
|
||||
process(item).await
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cancellation Patterns
|
||||
|
||||
### Using CancellationToken
|
||||
```rust
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
async fn cancellable_task(token: CancellationToken) {
|
||||
loop {
|
||||
select! {
|
||||
_ = token.cancelled() => {
|
||||
println!("Task cancelled");
|
||||
break;
|
||||
}
|
||||
_ = do_work() => {
|
||||
// Continue working
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn main_with_cancellation() {
|
||||
let token = CancellationToken::new();
|
||||
let task_token = token.clone();
|
||||
|
||||
let handle = tokio::spawn(cancellable_task(task_token));
|
||||
|
||||
// Cancel after some condition
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
token.cancel();
|
||||
|
||||
handle.await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Graceful Shutdown
|
||||
```rust
|
||||
async fn serve_with_shutdown(shutdown: impl Future) {
|
||||
let server = TcpListener::bind("0.0.0.0:8080").await.unwrap();
|
||||
|
||||
loop {
|
||||
select! {
|
||||
Ok((socket, _)) = server.accept() => {
|
||||
tokio::spawn(handle_connection(socket));
|
||||
}
|
||||
_ = &mut shutdown => {
|
||||
println!("Shutting down...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c().await.unwrap();
|
||||
};
|
||||
|
||||
serve_with_shutdown(ctrl_c).await;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backpressure Patterns
|
||||
|
||||
### Bounded Channels
|
||||
```rust
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
async fn with_backpressure() {
|
||||
// Buffer of 10 - producers will wait if full
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
|
||||
let producer = tokio::spawn(async move {
|
||||
for i in 0..1000 {
|
||||
// This will wait if channel is full
|
||||
tx.send(i).await.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let consumer = tokio::spawn(async move {
|
||||
while let Some(item) = rx.recv().await {
|
||||
// Slow consumer
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
process(item);
|
||||
}
|
||||
});
|
||||
|
||||
let _ = tokio::join!(producer, consumer);
|
||||
}
|
||||
```
|
||||
|
||||
### Semaphore for Rate Limiting
|
||||
```rust
|
||||
use tokio::sync::Semaphore;
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn rate_limited_requests(urls: Vec<String>) {
|
||||
let semaphore = Arc::new(Semaphore::new(10)); // max 10 concurrent
|
||||
|
||||
let handles: Vec<_> = urls
|
||||
.into_iter()
|
||||
.map(|url| {
|
||||
let sem = Arc::clone(&semaphore);
|
||||
tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
fetch(&url).await
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.await.unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling in Async
|
||||
|
||||
### Propagating Errors
|
||||
```rust
|
||||
async fn fetch_and_parse(url: &str) -> Result<Data, Error> {
|
||||
let response = fetch(url).await?;
|
||||
let data = parse(response).await?;
|
||||
Ok(data)
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Task Panics
|
||||
```rust
|
||||
async fn robust_spawn() {
|
||||
let handle = tokio::spawn(async {
|
||||
risky_operation().await
|
||||
});
|
||||
|
||||
match handle.await {
|
||||
Ok(result) => println!("Success: {:?}", result),
|
||||
Err(e) if e.is_panic() => {
|
||||
println!("Task panicked: {:?}", e);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Task cancelled: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Try-Join for Multiple Results
|
||||
```rust
|
||||
use tokio::try_join;
|
||||
|
||||
async fn fetch_all() -> Result<(A, B, C), Error> {
|
||||
// All must succeed, or first error returned
|
||||
try_join!(
|
||||
fetch_a(),
|
||||
fetch_b(),
|
||||
fetch_c(),
|
||||
)
|
||||
}
|
||||
```
|
||||
331
skills/m07-concurrency/patterns/common-errors.md
Normal file
331
skills/m07-concurrency/patterns/common-errors.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# Common Concurrency Errors & Fixes
|
||||
|
||||
## E0277: Cannot Send Between Threads
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
use std::rc::Rc;
|
||||
|
||||
let data = Rc::new(42);
|
||||
std::thread::spawn(move || {
|
||||
println!("{}", data); // ERROR: Rc<i32> cannot be sent between threads
|
||||
});
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Use Arc instead**
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
|
||||
let data = Arc::new(42);
|
||||
let data_clone = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
println!("{}", data_clone); // OK: Arc is Send
|
||||
});
|
||||
```
|
||||
|
||||
**Option 2: Move owned data**
|
||||
```rust
|
||||
let data = 42; // i32 is Copy and Send
|
||||
std::thread::spawn(move || {
|
||||
println!("{}", data); // OK
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0277: Cannot Share Between Threads (Not Sync)
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
|
||||
let data = Arc::new(RefCell::new(42));
|
||||
// ERROR: RefCell is not Sync
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Use Mutex for thread-safe interior mutability**
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let data = Arc::new(Mutex::new(42));
|
||||
let data_clone = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
let mut guard = data_clone.lock().unwrap();
|
||||
*guard += 1;
|
||||
});
|
||||
```
|
||||
|
||||
**Option 2: Use RwLock for read-heavy workloads**
|
||||
```rust
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
let data = Arc::new(RwLock::new(42));
|
||||
let data_clone = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
let guard = data_clone.read().unwrap();
|
||||
println!("{}", *guard);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deadlock Patterns
|
||||
|
||||
### Pattern 1: Lock Ordering Deadlock
|
||||
```rust
|
||||
// DANGER: potential deadlock
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let a = Arc::new(Mutex::new(1));
|
||||
let b = Arc::new(Mutex::new(2));
|
||||
|
||||
// Thread 1: locks a then b
|
||||
let a1 = Arc::clone(&a);
|
||||
let b1 = Arc::clone(&b);
|
||||
std::thread::spawn(move || {
|
||||
let _a = a1.lock().unwrap();
|
||||
let _b = b1.lock().unwrap(); // waits for b
|
||||
});
|
||||
|
||||
// Thread 2: locks b then a (opposite order!)
|
||||
let a2 = Arc::clone(&a);
|
||||
let b2 = Arc::clone(&b);
|
||||
std::thread::spawn(move || {
|
||||
let _b = b2.lock().unwrap();
|
||||
let _a = a2.lock().unwrap(); // waits for a - DEADLOCK
|
||||
});
|
||||
```
|
||||
|
||||
### Fix: Consistent Lock Ordering
|
||||
```rust
|
||||
// SAFE: always lock in same order (a before b)
|
||||
std::thread::spawn(move || {
|
||||
let _a = a1.lock().unwrap();
|
||||
let _b = b1.lock().unwrap();
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let _a = a2.lock().unwrap(); // same order
|
||||
let _b = b2.lock().unwrap();
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Self-Deadlock
|
||||
```rust
|
||||
// DANGER: locking same mutex twice
|
||||
let m = Mutex::new(42);
|
||||
let _g1 = m.lock().unwrap();
|
||||
let _g2 = m.lock().unwrap(); // DEADLOCK on std::Mutex
|
||||
|
||||
// FIX: use parking_lot::ReentrantMutex if needed
|
||||
// or restructure code to avoid double locking
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mutex Guard Across Await
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
use std::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
|
||||
async fn bad_async() {
|
||||
let m = Mutex::new(42);
|
||||
let guard = m.lock().unwrap();
|
||||
sleep(Duration::from_secs(1)).await; // WARNING: guard held across await
|
||||
println!("{}", *guard);
|
||||
}
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Scope the lock**
|
||||
```rust
|
||||
async fn good_async() {
|
||||
let m = Mutex::new(42);
|
||||
let value = {
|
||||
let guard = m.lock().unwrap();
|
||||
*guard // copy value
|
||||
}; // guard dropped here
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
println!("{}", value);
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Use tokio::sync::Mutex**
|
||||
```rust
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
async fn good_async() {
|
||||
let m = Mutex::new(42);
|
||||
let guard = m.lock().await; // async lock
|
||||
sleep(Duration::from_secs(1)).await; // OK with tokio::Mutex
|
||||
println!("{}", *guard);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Race Prevention
|
||||
|
||||
### Pattern: Missing Synchronization
|
||||
```rust
|
||||
// This WON'T compile - Rust prevents data races
|
||||
use std::sync::Arc;
|
||||
|
||||
let data = Arc::new(0);
|
||||
let d1 = Arc::clone(&data);
|
||||
let d2 = Arc::clone(&data);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// *d1 += 1; // ERROR: cannot mutate through Arc
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// *d2 += 1; // ERROR: cannot mutate through Arc
|
||||
});
|
||||
```
|
||||
|
||||
### Fix: Add Synchronization
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
// Option 1: Mutex
|
||||
let data = Arc::new(Mutex::new(0));
|
||||
let d1 = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
*d1.lock().unwrap() += 1;
|
||||
});
|
||||
|
||||
// Option 2: Atomic (for simple types)
|
||||
let data = Arc::new(AtomicI32::new(0));
|
||||
let d1 = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
d1.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Channel Errors
|
||||
|
||||
### Disconnected Channel
|
||||
```rust
|
||||
use std::sync::mpsc;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
drop(tx); // sender dropped
|
||||
match rx.recv() {
|
||||
Ok(v) => println!("{}", v),
|
||||
Err(_) => println!("channel disconnected"), // this happens
|
||||
}
|
||||
```
|
||||
|
||||
### Fix: Handle Disconnection
|
||||
```rust
|
||||
// Use try_recv for non-blocking
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(msg) => handle(msg),
|
||||
Err(TryRecvError::Empty) => continue,
|
||||
Err(TryRecvError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Or iterate (stops on disconnect)
|
||||
for msg in rx {
|
||||
handle(msg);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Common Errors
|
||||
|
||||
### Forgetting to Spawn
|
||||
```rust
|
||||
// WRONG: future not polled
|
||||
async fn fetch_data() -> Result<Data, Error> { ... }
|
||||
|
||||
fn process() {
|
||||
fetch_data(); // does nothing! returns Future that's dropped
|
||||
}
|
||||
|
||||
// RIGHT: await or spawn
|
||||
async fn process() {
|
||||
let data = fetch_data().await; // awaited
|
||||
}
|
||||
|
||||
fn process_sync() {
|
||||
tokio::spawn(fetch_data()); // spawned
|
||||
}
|
||||
```
|
||||
|
||||
### Blocking in Async Context
|
||||
```rust
|
||||
// WRONG: blocks the executor
|
||||
async fn bad() {
|
||||
std::thread::sleep(Duration::from_secs(1)); // blocks!
|
||||
std::fs::read_to_string("file.txt").unwrap(); // blocks!
|
||||
}
|
||||
|
||||
// RIGHT: use async versions
|
||||
async fn good() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
tokio::fs::read_to_string("file.txt").await.unwrap();
|
||||
}
|
||||
|
||||
// Or spawn_blocking for CPU-bound work
|
||||
async fn compute() {
|
||||
let result = tokio::task::spawn_blocking(|| {
|
||||
heavy_computation() // OK to block here
|
||||
}).await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Thread Panic Handling
|
||||
|
||||
### Unhandled Panic
|
||||
```rust
|
||||
let handle = std::thread::spawn(|| {
|
||||
panic!("oops");
|
||||
});
|
||||
|
||||
// Main thread continues, might miss the error
|
||||
handle.join().unwrap(); // panics here
|
||||
```
|
||||
|
||||
### Proper Error Handling
|
||||
```rust
|
||||
let handle = std::thread::spawn(|| {
|
||||
panic!("oops");
|
||||
});
|
||||
|
||||
match handle.join() {
|
||||
Ok(result) => println!("Success: {:?}", result),
|
||||
Err(e) => println!("Thread panicked: {:?}", e),
|
||||
}
|
||||
|
||||
// For async: use catch_unwind
|
||||
use std::panic;
|
||||
|
||||
async fn safe_task() {
|
||||
let result = panic::catch_unwind(|| {
|
||||
risky_operation()
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(v) => use_value(v),
|
||||
Err(_) => log_error("task panicked"),
|
||||
}
|
||||
}
|
||||
```
|
||||
174
skills/m09-domain/SKILL.md
Normal file
174
skills/m09-domain/SKILL.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
name: m09-domain
|
||||
description: "CRITICAL: Use for domain modeling. Triggers: domain model, DDD, domain-driven design, entity, value object, aggregate, repository pattern, business rules, validation, invariant, 领域模型, 领域驱动设计, 业务规则"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Domain Modeling
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**What is this concept's role in the domain?**
|
||||
|
||||
Before modeling in code, understand:
|
||||
- Is it an Entity (identity matters) or Value Object (interchangeable)?
|
||||
- What invariants must be maintained?
|
||||
- Where are the aggregate boundaries?
|
||||
|
||||
---
|
||||
|
||||
## Domain Concept → Rust Pattern
|
||||
|
||||
| Domain Concept | Rust Pattern | Ownership Implication |
|
||||
|----------------|--------------|----------------------|
|
||||
| Entity | struct + Id | Owned, unique identity |
|
||||
| Value Object | struct + Clone/Copy | Shareable, immutable |
|
||||
| Aggregate Root | struct owns children | Clear ownership tree |
|
||||
| Repository | trait | Abstracts persistence |
|
||||
| Domain Event | enum | Captures state changes |
|
||||
| Service | impl block / free fn | Stateless operations |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before creating a domain type:
|
||||
|
||||
1. **What's the concept's identity?**
|
||||
- Needs unique identity → Entity (Id field)
|
||||
- Interchangeable by value → Value Object (Clone/Copy)
|
||||
|
||||
2. **What invariants must hold?**
|
||||
- Always valid → private fields + validated constructor
|
||||
- Transition rules → type state pattern
|
||||
|
||||
3. **Who owns this data?**
|
||||
- Single owner (parent) → owned field
|
||||
- Shared reference → Arc/Rc
|
||||
- Weak reference → Weak
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To domain constraints (Layer 3):
|
||||
|
||||
```
|
||||
"How should I model a Transaction?"
|
||||
↑ Ask: What domain rules govern transactions?
|
||||
↑ Check: domain-fintech (audit, precision requirements)
|
||||
↑ Check: Business stakeholders (what invariants?)
|
||||
```
|
||||
|
||||
| Design Question | Trace To | Ask |
|
||||
|-----------------|----------|-----|
|
||||
| Entity vs Value Object | domain-* | What makes two instances "the same"? |
|
||||
| Aggregate boundaries | domain-* | What must be consistent together? |
|
||||
| Validation rules | domain-* | What business rules apply? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Model as Entity"
|
||||
↓ m01-ownership: Owned, unique
|
||||
↓ m05-type-driven: Newtype for Id
|
||||
|
||||
"Model as Value Object"
|
||||
↓ m01-ownership: Clone/Copy OK
|
||||
↓ m05-type-driven: Validate at construction
|
||||
|
||||
"Model as Aggregate"
|
||||
↓ m01-ownership: Parent owns children
|
||||
↓ m02-resource: Consider Rc for shared within aggregate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| DDD Concept | Rust Pattern | Example |
|
||||
|-------------|--------------|---------|
|
||||
| Value Object | Newtype | `struct Email(String);` |
|
||||
| Entity | Struct + ID | `struct User { id: UserId, ... }` |
|
||||
| Aggregate | Module boundary | `mod order { ... }` |
|
||||
| Repository | Trait | `trait UserRepo { fn find(...) }` |
|
||||
| Domain Event | Enum | `enum OrderEvent { Created, ... }` |
|
||||
|
||||
## Pattern Templates
|
||||
|
||||
### Value Object
|
||||
|
||||
```rust
|
||||
struct Email(String);
|
||||
|
||||
impl Email {
|
||||
pub fn new(s: &str) -> Result<Self, ValidationError> {
|
||||
validate_email(s)?;
|
||||
Ok(Self(s.to_string()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Entity
|
||||
|
||||
```rust
|
||||
struct UserId(Uuid);
|
||||
|
||||
struct User {
|
||||
id: UserId,
|
||||
email: Email,
|
||||
// ... other fields
|
||||
}
|
||||
|
||||
impl PartialEq for User {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id // Identity equality
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Aggregate
|
||||
|
||||
```rust
|
||||
mod order {
|
||||
pub struct Order {
|
||||
id: OrderId,
|
||||
items: Vec<OrderItem>, // Owned children
|
||||
// ...
|
||||
}
|
||||
|
||||
impl Order {
|
||||
pub fn add_item(&mut self, item: OrderItem) {
|
||||
// Enforce aggregate invariants
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Why Wrong | Better |
|
||||
|---------|-----------|--------|
|
||||
| Primitive obsession | No type safety | Newtype wrappers |
|
||||
| Public fields with invariants | Invariants violated | Private + accessor |
|
||||
| Leaked aggregate internals | Broken encapsulation | Methods on root |
|
||||
| String for semantic types | No validation | Validated newtype |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Type-driven implementation | m05-type-driven |
|
||||
| Ownership for aggregates | m01-ownership |
|
||||
| Domain error handling | m13-domain-error |
|
||||
| Specific domain rules | domain-* |
|
||||
157
skills/m10-performance/SKILL.md
Normal file
157
skills/m10-performance/SKILL.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
name: m10-performance
|
||||
description: "CRITICAL: Use for performance optimization. Triggers: performance, optimization, benchmark, profiling, flamegraph, criterion, slow, fast, allocation, cache, SIMD, make it faster, 性能优化, 基准测试"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Performance Optimization
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**What's the bottleneck, and is optimization worth it?**
|
||||
|
||||
Before optimizing:
|
||||
- Have you measured? (Don't guess)
|
||||
- What's the acceptable performance?
|
||||
- Will optimization add complexity?
|
||||
|
||||
---
|
||||
|
||||
## Performance Decision → Implementation
|
||||
|
||||
| Goal | Design Choice | Implementation |
|
||||
|------|---------------|----------------|
|
||||
| Reduce allocations | Pre-allocate, reuse | `with_capacity`, object pools |
|
||||
| Improve cache | Contiguous data | `Vec`, `SmallVec` |
|
||||
| Parallelize | Data parallelism | `rayon`, threads |
|
||||
| Avoid copies | Zero-copy | References, `Cow<T>` |
|
||||
| Reduce indirection | Inline data | `smallvec`, arrays |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before optimizing:
|
||||
|
||||
1. **Have you measured?**
|
||||
- Profile first → flamegraph, perf
|
||||
- Benchmark → criterion, cargo bench
|
||||
- Identify actual hotspots
|
||||
|
||||
2. **What's the priority?**
|
||||
- Algorithm (10x-1000x improvement)
|
||||
- Data structure (2x-10x)
|
||||
- Allocation (2x-5x)
|
||||
- Cache (1.5x-3x)
|
||||
|
||||
3. **What's the trade-off?**
|
||||
- Complexity vs speed
|
||||
- Memory vs CPU
|
||||
- Latency vs throughput
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To domain constraints (Layer 3):
|
||||
|
||||
```
|
||||
"How fast does this need to be?"
|
||||
↑ Ask: What's the performance SLA?
|
||||
↑ Check: domain-* (latency requirements)
|
||||
↑ Check: Business requirements (acceptable response time)
|
||||
```
|
||||
|
||||
| Question | Trace To | Ask |
|
||||
|----------|----------|-----|
|
||||
| Latency requirements | domain-* | What's acceptable response time? |
|
||||
| Throughput needs | domain-* | How many requests per second? |
|
||||
| Memory constraints | domain-* | What's the memory budget? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Need to reduce allocations"
|
||||
↓ m01-ownership: Use references, avoid clone
|
||||
↓ m02-resource: Pre-allocate with_capacity
|
||||
|
||||
"Need to parallelize"
|
||||
↓ m07-concurrency: Choose rayon or threads
|
||||
↓ m07-concurrency: Consider async for I/O-bound
|
||||
|
||||
"Need cache efficiency"
|
||||
↓ Data layout: Prefer Vec over HashMap when possible
|
||||
↓ Access patterns: Sequential over random access
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `cargo bench` | Micro-benchmarks |
|
||||
| `criterion` | Statistical benchmarks |
|
||||
| `perf` / `flamegraph` | CPU profiling |
|
||||
| `heaptrack` | Allocation tracking |
|
||||
| `valgrind` / `cachegrind` | Cache analysis |
|
||||
|
||||
## Optimization Priority
|
||||
|
||||
```
|
||||
1. Algorithm choice (10x - 1000x)
|
||||
2. Data structure (2x - 10x)
|
||||
3. Allocation reduction (2x - 5x)
|
||||
4. Cache optimization (1.5x - 3x)
|
||||
5. SIMD/Parallelism (2x - 8x)
|
||||
```
|
||||
|
||||
## Common Techniques
|
||||
|
||||
| Technique | When | How |
|
||||
|-----------|------|-----|
|
||||
| Pre-allocation | Known size | `Vec::with_capacity(n)` |
|
||||
| Avoid cloning | Hot paths | Use references or `Cow<T>` |
|
||||
| Batch operations | Many small ops | Collect then process |
|
||||
| SmallVec | Usually small | `smallvec::SmallVec<[T; N]>` |
|
||||
| Inline buffers | Fixed-size data | Arrays over Vec |
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Why Wrong | Better |
|
||||
|---------|-----------|--------|
|
||||
| Optimize without profiling | Wrong target | Profile first |
|
||||
| Benchmark in debug mode | Meaningless | Always `--release` |
|
||||
| Use LinkedList | Cache unfriendly | `Vec` or `VecDeque` |
|
||||
| Hidden `.clone()` | Unnecessary allocs | Use references |
|
||||
| Premature optimization | Wasted effort | Make it work first |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Clone to avoid lifetimes | Performance cost | Proper ownership |
|
||||
| Box everything | Indirection cost | Stack when possible |
|
||||
| HashMap for small sets | Overhead | Vec with linear search |
|
||||
| String concat in loop | O(n^2) | `String::with_capacity` or `format!` |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Reducing clones | m01-ownership |
|
||||
| Concurrency options | m07-concurrency |
|
||||
| Smart pointer choice | m02-resource |
|
||||
| Domain requirements | domain-* |
|
||||
365
skills/m10-performance/patterns/optimization-guide.md
Normal file
365
skills/m10-performance/patterns/optimization-guide.md
Normal file
@@ -0,0 +1,365 @@
|
||||
# Rust Performance Optimization Guide
|
||||
|
||||
## Profiling First
|
||||
|
||||
### Tools
|
||||
```bash
|
||||
# CPU profiling
|
||||
cargo install flamegraph
|
||||
cargo flamegraph --bin myapp
|
||||
|
||||
# Memory profiling
|
||||
cargo install cargo-instruments # macOS
|
||||
heaptrack ./target/release/myapp # Linux
|
||||
|
||||
# Benchmarking
|
||||
cargo bench # with criterion
|
||||
|
||||
# Cache analysis
|
||||
valgrind --tool=cachegrind ./target/release/myapp
|
||||
```
|
||||
|
||||
### Criterion Benchmarks
|
||||
```rust
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
fn benchmark_parse(c: &mut Criterion) {
|
||||
let input = "test data".repeat(1000);
|
||||
|
||||
c.bench_function("parse_v1", |b| {
|
||||
b.iter(|| parse_v1(&input))
|
||||
});
|
||||
|
||||
c.bench_function("parse_v2", |b| {
|
||||
b.iter(|| parse_v2(&input))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, benchmark_parse);
|
||||
criterion_main!(benches);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Optimizations
|
||||
|
||||
### 1. Avoid Unnecessary Allocations
|
||||
|
||||
```rust
|
||||
// BAD: allocates on every call
|
||||
fn to_uppercase(s: &str) -> String {
|
||||
s.to_uppercase()
|
||||
}
|
||||
|
||||
// GOOD: return Cow, allocate only if needed
|
||||
use std::borrow::Cow;
|
||||
|
||||
fn to_uppercase(s: &str) -> Cow<'_, str> {
|
||||
if s.chars().all(|c| c.is_uppercase()) {
|
||||
Cow::Borrowed(s)
|
||||
} else {
|
||||
Cow::Owned(s.to_uppercase())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Reuse Allocations
|
||||
|
||||
```rust
|
||||
// BAD: creates new Vec each iteration
|
||||
for item in items {
|
||||
let mut buffer = Vec::new();
|
||||
process(&mut buffer, item);
|
||||
}
|
||||
|
||||
// GOOD: reuse buffer
|
||||
let mut buffer = Vec::new();
|
||||
for item in items {
|
||||
buffer.clear();
|
||||
process(&mut buffer, item);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Appropriate Collections
|
||||
|
||||
| Need | Collection | Notes |
|
||||
|------|------------|-------|
|
||||
| Sequential access | `Vec<T>` | Best cache locality |
|
||||
| Random access by key | `HashMap<K, V>` | O(1) lookup |
|
||||
| Ordered keys | `BTreeMap<K, V>` | O(log n) lookup |
|
||||
| Small sets (<20) | `Vec<T>` + linear search | Lower overhead |
|
||||
| FIFO queue | `VecDeque<T>` | O(1) push/pop both ends |
|
||||
|
||||
### 4. Pre-allocate Capacity
|
||||
|
||||
```rust
|
||||
// BAD: many reallocations
|
||||
let mut v = Vec::new();
|
||||
for i in 0..10000 {
|
||||
v.push(i);
|
||||
}
|
||||
|
||||
// GOOD: single allocation
|
||||
let mut v = Vec::with_capacity(10000);
|
||||
for i in 0..10000 {
|
||||
v.push(i);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## String Optimization
|
||||
|
||||
### Avoid String Concatenation in Loops
|
||||
|
||||
```rust
|
||||
// BAD: O(n²) allocations
|
||||
let mut result = String::new();
|
||||
for s in strings {
|
||||
result = result + &s;
|
||||
}
|
||||
|
||||
// GOOD: O(n) with push_str
|
||||
let mut result = String::new();
|
||||
for s in strings {
|
||||
result.push_str(&s);
|
||||
}
|
||||
|
||||
// BETTER: pre-calculate capacity
|
||||
let total_len: usize = strings.iter().map(|s| s.len()).sum();
|
||||
let mut result = String::with_capacity(total_len);
|
||||
for s in strings {
|
||||
result.push_str(&s);
|
||||
}
|
||||
|
||||
// BEST: use join for simple cases
|
||||
let result = strings.join("");
|
||||
```
|
||||
|
||||
### Use &str When Possible
|
||||
|
||||
```rust
|
||||
// BAD: requires allocation
|
||||
fn greet(name: String) {
|
||||
println!("Hello, {}", name);
|
||||
}
|
||||
|
||||
// GOOD: borrows, no allocation
|
||||
fn greet(name: &str) {
|
||||
println!("Hello, {}", name);
|
||||
}
|
||||
|
||||
// Works with both:
|
||||
greet("world"); // &str
|
||||
greet(&String::from("world")); // &String coerces to &str
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Iterator Optimization
|
||||
|
||||
### Use Iterators Over Indexing
|
||||
|
||||
```rust
|
||||
// BAD: bounds checking on each access
|
||||
let mut sum = 0;
|
||||
for i in 0..vec.len() {
|
||||
sum += vec[i];
|
||||
}
|
||||
|
||||
// GOOD: no bounds checking
|
||||
let sum: i32 = vec.iter().sum();
|
||||
|
||||
// GOOD: when index needed
|
||||
for (i, item) in vec.iter().enumerate() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Evaluation
|
||||
|
||||
```rust
|
||||
// Iterators are lazy - computation happens at collect
|
||||
let result: Vec<_> = data
|
||||
.iter()
|
||||
.filter(|x| x.is_valid())
|
||||
.map(|x| x.process())
|
||||
.take(10) // stop after 10 items
|
||||
.collect();
|
||||
```
|
||||
|
||||
### Avoid Collecting When Not Needed
|
||||
|
||||
```rust
|
||||
// BAD: unnecessary intermediate allocation
|
||||
let filtered: Vec<_> = items.iter().filter(|x| x.valid).collect();
|
||||
let count = filtered.len();
|
||||
|
||||
// GOOD: no allocation
|
||||
let count = items.iter().filter(|x| x.valid).count();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parallelism with Rayon
|
||||
|
||||
```rust
|
||||
use rayon::prelude::*;
|
||||
|
||||
// Sequential
|
||||
let sum: i32 = (0..1_000_000).map(|x| x * x).sum();
|
||||
|
||||
// Parallel (automatic work stealing)
|
||||
let sum: i32 = (0..1_000_000).into_par_iter().map(|x| x * x).sum();
|
||||
|
||||
// Parallel with custom chunk size
|
||||
let results: Vec<_> = data
|
||||
.par_chunks(1000)
|
||||
.map(|chunk| process_chunk(chunk))
|
||||
.collect();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Layout
|
||||
|
||||
### Use Appropriate Integer Sizes
|
||||
|
||||
```rust
|
||||
// If values are small, use smaller types
|
||||
struct Item {
|
||||
count: u8, // 0-255, not u64
|
||||
flags: u8, // small enum
|
||||
id: u32, // if 4 billion is enough
|
||||
}
|
||||
```
|
||||
|
||||
### Pack Structs Efficiently
|
||||
|
||||
```rust
|
||||
// BAD: 24 bytes due to padding
|
||||
struct Bad {
|
||||
a: u8, // 1 byte + 7 padding
|
||||
b: u64, // 8 bytes
|
||||
c: u8, // 1 byte + 7 padding
|
||||
}
|
||||
|
||||
// GOOD: 16 bytes (or use #[repr(packed)])
|
||||
struct Good {
|
||||
b: u64, // 8 bytes
|
||||
a: u8, // 1 byte
|
||||
c: u8, // 1 byte + 6 padding
|
||||
}
|
||||
```
|
||||
|
||||
### Box Large Values
|
||||
|
||||
```rust
|
||||
// Large enum variants waste space
|
||||
enum Message {
|
||||
Quit,
|
||||
Data([u8; 10000]), // all variants are 10000+ bytes
|
||||
}
|
||||
|
||||
// Better: box the large variant
|
||||
enum Message {
|
||||
Quit,
|
||||
Data(Box<[u8; 10000]>), // variants are pointer-sized
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Performance
|
||||
|
||||
### Avoid Blocking in Async
|
||||
|
||||
```rust
|
||||
// BAD: blocks the executor
|
||||
async fn bad() {
|
||||
std::thread::sleep(Duration::from_secs(1)); // blocking!
|
||||
std::fs::read_to_string("file.txt").unwrap(); // blocking!
|
||||
}
|
||||
|
||||
// GOOD: use async versions
|
||||
async fn good() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
tokio::fs::read_to_string("file.txt").await.unwrap();
|
||||
}
|
||||
|
||||
// For CPU work: spawn_blocking
|
||||
async fn compute() -> i32 {
|
||||
tokio::task::spawn_blocking(|| {
|
||||
heavy_computation()
|
||||
}).await.unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
### Buffer Async I/O
|
||||
|
||||
```rust
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
// BAD: many small reads
|
||||
async fn bad(file: File) {
|
||||
let mut byte = [0u8];
|
||||
while file.read(&mut byte).await.unwrap() > 0 {
|
||||
process(byte[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: buffered reading
|
||||
async fn good(file: File) {
|
||||
let reader = BufReader::new(file);
|
||||
let mut lines = reader.lines();
|
||||
while let Some(line) = lines.next_line().await.unwrap() {
|
||||
process(&line);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Release Build Optimization
|
||||
|
||||
### Cargo.toml Settings
|
||||
|
||||
```toml
|
||||
[profile.release]
|
||||
lto = true # Link-time optimization
|
||||
codegen-units = 1 # Single codegen unit (slower compile, faster code)
|
||||
panic = "abort" # Smaller binary, no unwinding
|
||||
strip = true # Strip symbols
|
||||
|
||||
[profile.release-fast]
|
||||
inherits = "release"
|
||||
opt-level = 3 # Maximum optimization
|
||||
|
||||
[profile.release-small]
|
||||
inherits = "release"
|
||||
opt-level = "s" # Optimize for size
|
||||
```
|
||||
|
||||
### Compile-Time Assertions
|
||||
|
||||
```rust
|
||||
// Zero runtime cost
|
||||
const _: () = assert!(std::mem::size_of::<MyStruct>() <= 64);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before optimizing:
|
||||
- [ ] Profile to find actual bottlenecks
|
||||
- [ ] Have benchmarks to measure improvement
|
||||
- [ ] Consider if optimization is worth complexity
|
||||
|
||||
Common wins:
|
||||
- [ ] Reduce allocations (Cow, reuse buffers)
|
||||
- [ ] Use appropriate collections
|
||||
- [ ] Pre-allocate with_capacity
|
||||
- [ ] Use iterators instead of indexing
|
||||
- [ ] Enable LTO for release builds
|
||||
- [ ] Use rayon for parallel workloads
|
||||
162
skills/m11-ecosystem/SKILL.md
Normal file
162
skills/m11-ecosystem/SKILL.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
name: m11-ecosystem
|
||||
description: "Use when integrating crates or ecosystem questions. Keywords: E0425, E0433, E0603, crate, cargo, dependency, feature flag, workspace, which crate to use, using external C libraries, creating Python extensions, PyO3, wasm, WebAssembly, bindgen, cbindgen, napi-rs, cannot find, private, crate recommendation, best crate for, Cargo.toml, features, crate 推荐, 依赖管理, 特性标志, 工作空间, Python 绑定"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
## Current Dependencies (Auto-Injected)
|
||||
|
||||
!`grep -A 100 '^\[dependencies\]' Cargo.toml 2>/dev/null | head -30 || echo "No Cargo.toml found"`
|
||||
|
||||
---
|
||||
|
||||
# Ecosystem Integration
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**What's the right crate for this job, and how should it integrate?**
|
||||
|
||||
Before adding dependencies:
|
||||
- Is there a standard solution?
|
||||
- What's the maintenance status?
|
||||
- What's the API stability?
|
||||
|
||||
---
|
||||
|
||||
## Integration Decision → Implementation
|
||||
|
||||
| Need | Choice | Crates |
|
||||
|------|--------|--------|
|
||||
| Serialization | Derive-based | serde, serde_json |
|
||||
| Async runtime | tokio or async-std | tokio (most popular) |
|
||||
| HTTP client | Ergonomic | reqwest |
|
||||
| HTTP server | Modern | axum, actix-web |
|
||||
| Database | SQL or ORM | sqlx, diesel |
|
||||
| CLI parsing | Derive-based | clap |
|
||||
| Error handling | App vs lib | anyhow, thiserror |
|
||||
| Logging | Facade | tracing, log |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before adding a dependency:
|
||||
|
||||
1. **Is it well-maintained?**
|
||||
- Recent commits?
|
||||
- Active issue response?
|
||||
- Breaking changes frequency?
|
||||
|
||||
2. **What's the scope?**
|
||||
- Do you need the full crate or just a feature?
|
||||
- Can feature flags reduce bloat?
|
||||
|
||||
3. **How does it integrate?**
|
||||
- Trait-based or concrete types?
|
||||
- Sync or async?
|
||||
- What bounds does it require?
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To domain constraints (Layer 3):
|
||||
|
||||
```
|
||||
"Which HTTP framework should I use?"
|
||||
↑ Ask: What are the performance requirements?
|
||||
↑ Check: domain-web (latency, throughput needs)
|
||||
↑ Check: Team expertise (familiarity with framework)
|
||||
```
|
||||
|
||||
| Question | Trace To | Ask |
|
||||
|----------|----------|-----|
|
||||
| Framework choice | domain-* | What constraints matter? |
|
||||
| Library vs build | domain-* | What's the deployment model? |
|
||||
| API design | domain-* | Who are the consumers? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Integrate external crate"
|
||||
↓ m04-zero-cost: Trait bounds and generics
|
||||
↓ m06-error-handling: Error type compatibility
|
||||
|
||||
"FFI integration"
|
||||
↓ unsafe-checker: Safety requirements
|
||||
↓ m12-lifecycle: Resource cleanup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Language Interop
|
||||
|
||||
| Integration | Crate/Tool | Use Case |
|
||||
|-------------|------------|----------|
|
||||
| C/C++ → Rust | `bindgen` | Auto-generate bindings |
|
||||
| Rust → C | `cbindgen` | Export C headers |
|
||||
| Python ↔ Rust | `pyo3` | Python extensions |
|
||||
| Node.js ↔ Rust | `napi-rs` | Node addons |
|
||||
| WebAssembly | `wasm-bindgen` | Browser/WASI |
|
||||
|
||||
### Cargo Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| `[features]` | Optional functionality |
|
||||
| `default = [...]` | Default features |
|
||||
| `feature = "serde"` | Conditional deps |
|
||||
| `[workspace]` | Multi-crate projects |
|
||||
|
||||
## Error Code Reference
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| E0433 | Can't find crate | Add to Cargo.toml |
|
||||
| E0603 | Private item | Check crate docs |
|
||||
| Feature not enabled | Optional feature | Enable in `features` |
|
||||
| Version conflict | Incompatible deps | `cargo update` or pin |
|
||||
| Duplicate types | Different crate versions | Unify in workspace |
|
||||
|
||||
---
|
||||
|
||||
## Crate Selection Criteria
|
||||
|
||||
| Criterion | Good Sign | Warning Sign |
|
||||
|-----------|-----------|--------------|
|
||||
| Maintenance | Recent commits | Years inactive |
|
||||
| Community | Active issues/PRs | No response |
|
||||
| Documentation | Examples, API docs | Minimal docs |
|
||||
| Stability | Semantic versioning | Frequent breaking |
|
||||
| Dependencies | Minimal, well-known | Heavy, obscure |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| `extern crate` | Outdated (2018+) | Just `use` |
|
||||
| `#[macro_use]` | Global pollution | Explicit import |
|
||||
| Wildcard deps `*` | Unpredictable | Specific versions |
|
||||
| Too many deps | Supply chain risk | Evaluate necessity |
|
||||
| Vendoring everything | Maintenance burden | Trust crates.io |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Error type design | m06-error-handling |
|
||||
| Trait integration | m04-zero-cost |
|
||||
| FFI safety | unsafe-checker |
|
||||
| Resource management | m12-lifecycle |
|
||||
177
skills/m12-lifecycle/SKILL.md
Normal file
177
skills/m12-lifecycle/SKILL.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: m12-lifecycle
|
||||
description: "Use when designing resource lifecycles. Keywords: RAII, Drop, resource lifecycle, connection pool, lazy initialization, connection pool design, resource cleanup patterns, cleanup, scope, OnceCell, Lazy, once_cell, OnceLock, transaction, session management, when is Drop called, cleanup on error, guard pattern, scope guard, 资源生命周期, 连接池, 惰性初始化, 资源清理, RAII 模式"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Resource Lifecycle
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**When should this resource be created, used, and cleaned up?**
|
||||
|
||||
Before implementing lifecycle:
|
||||
- What's the resource's scope?
|
||||
- Who owns the cleanup responsibility?
|
||||
- What happens on error?
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle Pattern → Implementation
|
||||
|
||||
| Pattern | When | Implementation |
|
||||
|---------|------|----------------|
|
||||
| RAII | Auto cleanup | `Drop` trait |
|
||||
| Lazy init | Deferred creation | `OnceLock`, `LazyLock` |
|
||||
| Pool | Reuse expensive resources | `r2d2`, `deadpool` |
|
||||
| Guard | Scoped access | `MutexGuard` pattern |
|
||||
| Scope | Transaction boundary | Custom struct + Drop |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before designing lifecycle:
|
||||
|
||||
1. **What's the resource cost?**
|
||||
- Cheap → create per use
|
||||
- Expensive → pool or cache
|
||||
- Global → lazy singleton
|
||||
|
||||
2. **What's the scope?**
|
||||
- Function-local → stack allocation
|
||||
- Request-scoped → passed or extracted
|
||||
- Application-wide → static or Arc
|
||||
|
||||
3. **What about errors?**
|
||||
- Cleanup must happen → Drop
|
||||
- Cleanup is optional → explicit close
|
||||
- Cleanup can fail → Result from close
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To domain constraints (Layer 3):
|
||||
|
||||
```
|
||||
"How should I manage database connections?"
|
||||
↑ Ask: What's the connection cost?
|
||||
↑ Check: domain-* (latency requirements)
|
||||
↑ Check: Infrastructure (connection limits)
|
||||
```
|
||||
|
||||
| Question | Trace To | Ask |
|
||||
|----------|----------|-----|
|
||||
| Connection pooling | domain-* | What's acceptable latency? |
|
||||
| Resource limits | domain-* | What are infra constraints? |
|
||||
| Transaction scope | domain-* | What must be atomic? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Need automatic cleanup"
|
||||
↓ m02-resource: Implement Drop
|
||||
↓ m01-ownership: Clear owner for cleanup
|
||||
|
||||
"Need lazy initialization"
|
||||
↓ m03-mutability: OnceLock for thread-safe
|
||||
↓ m07-concurrency: LazyLock for sync
|
||||
|
||||
"Need connection pool"
|
||||
↓ m07-concurrency: Thread-safe pool
|
||||
↓ m02-resource: Arc for sharing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Type | Use Case |
|
||||
|---------|------|----------|
|
||||
| RAII | `Drop` trait | Auto cleanup on scope exit |
|
||||
| Lazy Init | `OnceLock`, `LazyLock` | Deferred initialization |
|
||||
| Pool | `r2d2`, `deadpool` | Connection reuse |
|
||||
| Guard | `MutexGuard` | Scoped lock release |
|
||||
| Scope | Custom struct | Transaction boundaries |
|
||||
|
||||
## Lifecycle Events
|
||||
|
||||
| Event | Rust Mechanism |
|
||||
|-------|----------------|
|
||||
| Creation | `new()`, `Default` |
|
||||
| Lazy Init | `OnceLock::get_or_init` |
|
||||
| Usage | `&self`, `&mut self` |
|
||||
| Cleanup | `Drop::drop()` |
|
||||
|
||||
## Pattern Templates
|
||||
|
||||
### RAII Guard
|
||||
|
||||
```rust
|
||||
struct FileGuard {
|
||||
path: PathBuf,
|
||||
_handle: File,
|
||||
}
|
||||
|
||||
impl Drop for FileGuard {
|
||||
fn drop(&mut self) {
|
||||
// Cleanup: remove temp file
|
||||
let _ = std::fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Singleton
|
||||
|
||||
```rust
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static CONFIG: OnceLock<Config> = OnceLock::new();
|
||||
|
||||
fn get_config() -> &'static Config {
|
||||
CONFIG.get_or_init(|| {
|
||||
Config::load().expect("config required")
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| Resource leak | Forgot Drop | Implement Drop or RAII wrapper |
|
||||
| Double free | Manual memory | Let Rust handle |
|
||||
| Use after drop | Dangling reference | Check lifetimes |
|
||||
| E0509 move out of Drop | Moving owned field | `Option::take()` |
|
||||
| Pool exhaustion | Not returned | Ensure Drop returns |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Manual cleanup | Easy to forget | RAII/Drop |
|
||||
| `lazy_static!` | External dep | `std::sync::OnceLock` |
|
||||
| Global mutable state | Thread unsafety | `OnceLock` or proper sync |
|
||||
| Forget to close | Resource leak | Drop impl |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Smart pointers | m02-resource |
|
||||
| Thread-safe init | m07-concurrency |
|
||||
| Domain scopes | m09-domain |
|
||||
| Error in cleanup | m06-error-handling |
|
||||
180
skills/m13-domain-error/SKILL.md
Normal file
180
skills/m13-domain-error/SKILL.md
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
name: m13-domain-error
|
||||
description: "Use when designing domain error handling. Keywords: domain error, error categorization, recovery strategy, retry, fallback, domain error hierarchy, user-facing vs internal errors, error code design, circuit breaker, graceful degradation, resilience, error context, backoff, retry with backoff, error recovery, transient vs permanent error, 领域错误, 错误分类, 恢复策略, 重试, 熔断器, 优雅降级"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Domain Error Strategy
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Who needs to handle this error, and how should they recover?**
|
||||
|
||||
Before designing error types:
|
||||
- Is this user-facing or internal?
|
||||
- Is recovery possible?
|
||||
- What context is needed for debugging?
|
||||
|
||||
---
|
||||
|
||||
## Error Categorization
|
||||
|
||||
| Error Type | Audience | Recovery | Example |
|
||||
|------------|----------|----------|---------|
|
||||
| User-facing | End users | Guide action | `InvalidEmail`, `NotFound` |
|
||||
| Internal | Developers | Debug info | `DatabaseError`, `ParseError` |
|
||||
| System | Ops/SRE | Monitor/alert | `ConnectionTimeout`, `RateLimited` |
|
||||
| Transient | Automation | Retry | `NetworkError`, `ServiceUnavailable` |
|
||||
| Permanent | Human | Investigate | `ConfigInvalid`, `DataCorrupted` |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before designing error types:
|
||||
|
||||
1. **Who sees this error?**
|
||||
- End user → friendly message, actionable
|
||||
- Developer → detailed, debuggable
|
||||
- Ops → structured, alertable
|
||||
|
||||
2. **Can we recover?**
|
||||
- Transient → retry with backoff
|
||||
- Degradable → fallback value
|
||||
- Permanent → fail fast, alert
|
||||
|
||||
3. **What context is needed?**
|
||||
- Call chain → anyhow::Context
|
||||
- Request ID → structured logging
|
||||
- Input data → error payload
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To domain constraints (Layer 3):
|
||||
|
||||
```
|
||||
"How should I handle payment failures?"
|
||||
↑ Ask: What are the business rules for retries?
|
||||
↑ Check: domain-fintech (transaction requirements)
|
||||
↑ Check: SLA (availability requirements)
|
||||
```
|
||||
|
||||
| Question | Trace To | Ask |
|
||||
|----------|----------|-----|
|
||||
| Retry policy | domain-* | What's acceptable latency for retry? |
|
||||
| User experience | domain-* | What message should users see? |
|
||||
| Compliance | domain-* | What must be logged for audit? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Need typed errors"
|
||||
↓ m06-error-handling: thiserror for library
|
||||
↓ m04-zero-cost: Error enum design
|
||||
|
||||
"Need error context"
|
||||
↓ m06-error-handling: anyhow::Context
|
||||
↓ Logging: tracing with fields
|
||||
|
||||
"Need retry logic"
|
||||
↓ m07-concurrency: async retry patterns
|
||||
↓ Crates: tokio-retry, backoff
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Recovery Pattern | When | Implementation |
|
||||
|------------------|------|----------------|
|
||||
| Retry | Transient failures | exponential backoff |
|
||||
| Fallback | Degraded mode | cached/default value |
|
||||
| Circuit Breaker | Cascading failures | failsafe-rs |
|
||||
| Timeout | Slow operations | `tokio::time::timeout` |
|
||||
| Bulkhead | Isolation | separate thread pools |
|
||||
|
||||
## Error Hierarchy
|
||||
|
||||
```rust
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum AppError {
|
||||
// User-facing
|
||||
#[error("Invalid input: {0}")]
|
||||
Validation(String),
|
||||
|
||||
// Transient (retryable)
|
||||
#[error("Service temporarily unavailable")]
|
||||
ServiceUnavailable(#[source] reqwest::Error),
|
||||
|
||||
// Internal (log details, show generic)
|
||||
#[error("Internal error")]
|
||||
Internal(#[source] anyhow::Error),
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn is_retryable(&self) -> bool {
|
||||
matches!(self, Self::ServiceUnavailable(_))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Retry Pattern
|
||||
|
||||
```rust
|
||||
use tokio_retry::{Retry, strategy::ExponentialBackoff};
|
||||
|
||||
async fn with_retry<F, T, E>(f: F) -> Result<T, E>
|
||||
where
|
||||
F: Fn() -> impl Future<Output = Result<T, E>>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
let strategy = ExponentialBackoff::from_millis(100)
|
||||
.max_delay(Duration::from_secs(10))
|
||||
.take(5);
|
||||
|
||||
Retry::spawn(strategy, || f()).await
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Why Wrong | Better |
|
||||
|---------|-----------|--------|
|
||||
| Same error for all | No actionability | Categorize by audience |
|
||||
| Retry everything | Wasted resources | Only transient errors |
|
||||
| Infinite retry | DoS self | Max attempts + backoff |
|
||||
| Expose internal errors | Security risk | User-friendly messages |
|
||||
| No context | Hard to debug | .context() everywhere |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| String errors | No structure | thiserror types |
|
||||
| panic! for recoverable | Bad UX | Result with context |
|
||||
| Ignore errors | Silent failures | Log or propagate |
|
||||
| Box<dyn Error> everywhere | Lost type info | thiserror |
|
||||
| Error in happy path | Performance | Early validation |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Error handling basics | m06-error-handling |
|
||||
| Retry implementation | m07-concurrency |
|
||||
| Domain modeling | m09-domain |
|
||||
| User-facing APIs | domain-* |
|
||||
177
skills/m14-mental-model/SKILL.md
Normal file
177
skills/m14-mental-model/SKILL.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: m14-mental-model
|
||||
description: "Use when learning Rust concepts. Keywords: mental model, how to think about ownership, understanding borrow checker, visualizing memory layout, analogy, misconception, explaining ownership, why does Rust, help me understand, confused about, learning Rust, explain like I'm, ELI5, intuition for, coming from Java, coming from Python, 心智模型, 如何理解所有权, 学习 Rust, Rust 入门, 为什么 Rust"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Mental Models
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**What's the right way to think about this Rust concept?**
|
||||
|
||||
When learning or explaining Rust:
|
||||
- What's the correct mental model?
|
||||
- What misconceptions should be avoided?
|
||||
- What analogies help understanding?
|
||||
|
||||
---
|
||||
|
||||
## Key Mental Models
|
||||
|
||||
| Concept | Mental Model | Analogy |
|
||||
|---------|--------------|---------|
|
||||
| Ownership | Unique key | Only one person has the house key |
|
||||
| Move | Key handover | Giving away your key |
|
||||
| `&T` | Lending for reading | Lending a book |
|
||||
| `&mut T` | Exclusive editing | Only you can edit the doc |
|
||||
| Lifetime `'a` | Valid scope | "Ticket valid until..." |
|
||||
| `Box<T>` | Heap pointer | Remote control to TV |
|
||||
| `Rc<T>` | Shared ownership | Multiple remotes, last turns off |
|
||||
| `Arc<T>` | Thread-safe Rc | Remotes from any room |
|
||||
|
||||
---
|
||||
|
||||
## Coming From Other Languages
|
||||
|
||||
| From | Key Shift |
|
||||
|------|-----------|
|
||||
| Java/C# | Values are owned, not references by default |
|
||||
| C/C++ | Compiler enforces safety rules |
|
||||
| Python/Go | No GC, deterministic destruction |
|
||||
| Functional | Mutability is safe via ownership |
|
||||
| JavaScript | No null, use Option instead |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
When confused about Rust:
|
||||
|
||||
1. **What's the ownership model?**
|
||||
- Who owns this data?
|
||||
- How long does it live?
|
||||
- Who can access it?
|
||||
|
||||
2. **What guarantee is Rust providing?**
|
||||
- No data races
|
||||
- No dangling pointers
|
||||
- No use-after-free
|
||||
|
||||
3. **What's the compiler telling me?**
|
||||
- Error = violation of safety rule
|
||||
- Solution = work with the rules
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To design understanding (Layer 2):
|
||||
|
||||
```
|
||||
"Why can't I do X in Rust?"
|
||||
↑ Ask: What safety guarantee would be violated?
|
||||
↑ Check: m01-m07 for the rule being enforced
|
||||
↑ Ask: What's the intended design pattern?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"I understand the concept, now how do I implement?"
|
||||
↓ m01-ownership: Ownership patterns
|
||||
↓ m02-resource: Smart pointer choice
|
||||
↓ m07-concurrency: Thread safety
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Misconceptions
|
||||
|
||||
| Error | Wrong Model | Correct Model |
|
||||
|-------|-------------|---------------|
|
||||
| E0382 use after move | GC cleans up | Ownership = unique key transfer |
|
||||
| E0502 borrow conflict | Multiple writers OK | Only one writer at a time |
|
||||
| E0499 multiple mut borrows | Aliased mutation | Exclusive access for mutation |
|
||||
| E0106 missing lifetime | Ignoring scope | References have validity scope |
|
||||
| E0507 cannot move from `&T` | Implicit clone | References don't own data |
|
||||
|
||||
## Deprecated Thinking
|
||||
|
||||
| Deprecated | Better |
|
||||
|------------|--------|
|
||||
| "Rust is like C++" | Different ownership model |
|
||||
| "Lifetimes are GC" | Compile-time validity scope |
|
||||
| "Clone solves everything" | Restructure ownership |
|
||||
| "Fight the borrow checker" | Work with the compiler |
|
||||
| "`unsafe` to avoid rules" | Understand safe patterns first |
|
||||
|
||||
---
|
||||
|
||||
## Ownership Visualization
|
||||
|
||||
```
|
||||
Stack Heap
|
||||
+----------------+ +----------------+
|
||||
| main() | | |
|
||||
| s1 ─────────────────────> │ "hello" |
|
||||
| | | |
|
||||
| fn takes(s) { | | |
|
||||
| s2 (moved) ─────────────> │ "hello" |
|
||||
| } | | (s1 invalid) |
|
||||
+----------------+ +----------------+
|
||||
|
||||
After move: s1 is no longer valid
|
||||
```
|
||||
|
||||
## Reference Visualization
|
||||
|
||||
```
|
||||
+----------------+
|
||||
| data: String |────────────> "hello"
|
||||
+----------------+
|
||||
↑
|
||||
│ &data (immutable borrow)
|
||||
│
|
||||
+------+------+
|
||||
| reader1 reader2 (multiple OK)
|
||||
+------+------+
|
||||
|
||||
+----------------+
|
||||
| data: String |────────────> "hello"
|
||||
+----------------+
|
||||
↑
|
||||
│ &mut data (mutable borrow)
|
||||
│
|
||||
+------+
|
||||
| writer (only one)
|
||||
+------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Learning Path
|
||||
|
||||
| Stage | Focus | Skills |
|
||||
|-------|-------|--------|
|
||||
| Beginner | Ownership basics | m01-ownership, m14-mental-model |
|
||||
| Intermediate | Smart pointers, error handling | m02, m06 |
|
||||
| Advanced | Concurrency, unsafe | m07, unsafe-checker |
|
||||
| Expert | Design patterns | m09-m15, domain-* |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Ownership errors | m01-ownership |
|
||||
| Smart pointers | m02-resource |
|
||||
| Concurrency | m07-concurrency |
|
||||
| Anti-patterns | m15-anti-pattern |
|
||||
286
skills/m14-mental-model/patterns/thinking-in-rust.md
Normal file
286
skills/m14-mental-model/patterns/thinking-in-rust.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# Thinking in Rust: Mental Models
|
||||
|
||||
## Core Mental Models
|
||||
|
||||
### 1. Ownership as Resource Management
|
||||
|
||||
```
|
||||
Traditional: "Who has a pointer to this data?"
|
||||
Rust: "Who OWNS this data and is responsible for freeing it?"
|
||||
```
|
||||
|
||||
Key insight: Every value has exactly one owner. When the owner goes out of scope, the value is dropped.
|
||||
|
||||
```rust
|
||||
{
|
||||
let s = String::from("hello"); // s owns the String
|
||||
// use s...
|
||||
} // s goes out of scope, String is dropped (memory freed)
|
||||
```
|
||||
|
||||
### 2. Borrowing as Temporary Access
|
||||
|
||||
```
|
||||
Traditional: "I'll just read from this pointer"
|
||||
Rust: "I'm borrowing this value, owner still responsible for it"
|
||||
```
|
||||
|
||||
Key insight: Borrows are like library books - you can read them, but must return them.
|
||||
|
||||
```rust
|
||||
fn print_length(s: &String) { // borrows s
|
||||
println!("{}", s.len());
|
||||
} // borrow ends, caller still owns s
|
||||
|
||||
let my_string = String::from("hello");
|
||||
print_length(&my_string); // lend to function
|
||||
println!("{}", my_string); // still have it
|
||||
```
|
||||
|
||||
### 3. Lifetimes as Validity Scopes
|
||||
|
||||
```
|
||||
Traditional: "Hope this pointer is still valid"
|
||||
Rust: "Compiler tracks exactly how long references are valid"
|
||||
```
|
||||
|
||||
Key insight: A reference can't outlive the data it points to.
|
||||
|
||||
```rust
|
||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
// 'a means: the returned reference is valid as long as BOTH inputs are valid
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shifting Perspectives
|
||||
|
||||
### From "Everything is a Reference" (Java/C#)
|
||||
|
||||
Java mental model:
|
||||
```java
|
||||
// Everything is implicitly a reference
|
||||
User user = new User("Alice"); // user is a reference
|
||||
List<User> users = new ArrayList<>();
|
||||
users.add(user); // shares the reference
|
||||
user.setName("Bob"); // affects the list too!
|
||||
```
|
||||
|
||||
Rust mental model:
|
||||
```rust
|
||||
// Values are owned, sharing is explicit
|
||||
let user = User::new("Alice"); // user is owned
|
||||
let mut users = vec![];
|
||||
users.push(user); // user moved into vec, can't use user anymore
|
||||
// user.set_name("Bob"); // ERROR: user was moved
|
||||
|
||||
// If you need sharing:
|
||||
use std::rc::Rc;
|
||||
let user = Rc::new(User::new("Alice"));
|
||||
let user2 = Rc::clone(&user); // explicit shared ownership
|
||||
```
|
||||
|
||||
### From "Manual Memory Management" (C/C++)
|
||||
|
||||
C mental model:
|
||||
```c
|
||||
char* s = malloc(100);
|
||||
// ... must remember to free(s) ...
|
||||
// ... what if we return early? ...
|
||||
// ... what if an exception occurs? ...
|
||||
free(s);
|
||||
```
|
||||
|
||||
Rust mental model:
|
||||
```rust
|
||||
let s = String::with_capacity(100);
|
||||
// ... use s ...
|
||||
// No need to free - Rust drops s automatically when scope ends
|
||||
// Even with early returns, panics, or any control flow
|
||||
```
|
||||
|
||||
### From "Garbage Collection" (Go/Python)
|
||||
|
||||
GC mental model:
|
||||
```python
|
||||
# Create objects, GC will figure it out
|
||||
users = []
|
||||
for name in names:
|
||||
users.append(User(name))
|
||||
# GC runs sometime later, when it feels like it
|
||||
```
|
||||
|
||||
Rust mental model:
|
||||
```rust
|
||||
let users: Vec<User> = names
|
||||
.iter()
|
||||
.map(|name| User::new(name))
|
||||
.collect();
|
||||
// Memory is freed EXACTLY when users goes out of scope
|
||||
// Deterministic, no GC pauses, no unpredictable memory usage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Questions to Ask
|
||||
|
||||
### When Designing Functions
|
||||
|
||||
1. **Does this function need to own the data, or just read it?**
|
||||
- Need to keep it: take ownership (`fn process(data: Vec<T>)`)
|
||||
- Just reading: borrow (`fn process(data: &[T])`)
|
||||
- Need to modify: mutable borrow (`fn process(data: &mut Vec<T>)`)
|
||||
|
||||
2. **Does the return value contain references to inputs?**
|
||||
- Yes: need lifetime annotations
|
||||
- No: lifetime elision usually works
|
||||
|
||||
### When Designing Structs
|
||||
|
||||
1. **Should this struct own its data or reference it?**
|
||||
- Long-lived, independent: own (`name: String`)
|
||||
- Short-lived view: reference (`name: &'a str`)
|
||||
|
||||
2. **Do multiple parts need to access the same data?**
|
||||
- Single-threaded: `Rc<T>` or `Rc<RefCell<T>>`
|
||||
- Multi-threaded: `Arc<T>` or `Arc<Mutex<T>>`
|
||||
|
||||
### When Hitting Borrow Checker Errors
|
||||
|
||||
1. **Am I trying to use a value after moving it?**
|
||||
- Clone it, borrow it, or restructure the code
|
||||
|
||||
2. **Am I trying to have multiple mutable references?**
|
||||
- Scope the mutations, use interior mutability, or redesign
|
||||
|
||||
3. **Does a reference outlive its source?**
|
||||
- Return owned data instead, or use `'static`
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### The Clone Escape Hatch
|
||||
|
||||
When fighting the borrow checker, `.clone()` often works:
|
||||
|
||||
```rust
|
||||
// Can't do this - double borrow
|
||||
let mut map = HashMap::new();
|
||||
for key in map.keys() {
|
||||
map.insert(key.clone(), process(key)); // ERROR: map borrowed twice
|
||||
}
|
||||
|
||||
// Clone to escape
|
||||
let keys: Vec<_> = map.keys().cloned().collect();
|
||||
for key in keys {
|
||||
map.insert(key.clone(), process(&key)); // OK
|
||||
}
|
||||
```
|
||||
|
||||
But ask: "Is there a better design?" Often, restructuring is better than cloning.
|
||||
|
||||
### The "Make It Own" Pattern
|
||||
|
||||
When lifetimes get complex, make the struct own its data:
|
||||
|
||||
```rust
|
||||
// Complex: struct with references
|
||||
struct Parser<'a> {
|
||||
input: &'a str,
|
||||
current: &'a str,
|
||||
}
|
||||
|
||||
// Simpler: struct owns data
|
||||
struct Parser {
|
||||
input: String,
|
||||
position: usize,
|
||||
}
|
||||
```
|
||||
|
||||
### The "Split the Borrow" Pattern
|
||||
|
||||
```rust
|
||||
struct Data {
|
||||
field_a: Vec<i32>,
|
||||
field_b: Vec<i32>,
|
||||
}
|
||||
|
||||
// Can't borrow self mutably twice
|
||||
fn process(&mut self) {
|
||||
// for a in &self.field_a {
|
||||
// self.field_b.push(*a); // ERROR
|
||||
// }
|
||||
|
||||
// Split the borrow
|
||||
let Data { field_a, field_b } = self;
|
||||
for a in field_a.iter() {
|
||||
field_b.push(*a); // OK: separate borrows
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Rust Way
|
||||
|
||||
### Embrace the Type System
|
||||
|
||||
```rust
|
||||
// Don't: stringly-typed
|
||||
fn connect(host: &str, port: &str) { ... }
|
||||
connect("8080", "localhost"); // oops, wrong order
|
||||
|
||||
// Do: strongly-typed
|
||||
struct Host(String);
|
||||
struct Port(u16);
|
||||
fn connect(host: Host, port: Port) { ... }
|
||||
// connect(Port(8080), Host("localhost".into())); // compile error!
|
||||
```
|
||||
|
||||
### Make Invalid States Unrepresentable
|
||||
|
||||
```rust
|
||||
// Don't: runtime checks
|
||||
struct Connection {
|
||||
socket: Option<Socket>,
|
||||
connected: bool,
|
||||
}
|
||||
|
||||
// Do: types enforce states
|
||||
enum Connection {
|
||||
Disconnected,
|
||||
Connected { socket: Socket },
|
||||
}
|
||||
```
|
||||
|
||||
### Let the Compiler Guide You
|
||||
|
||||
```rust
|
||||
// Start with what you want
|
||||
fn process(data: ???) -> ???
|
||||
|
||||
// Let compiler errors tell you:
|
||||
// - What types are needed
|
||||
// - What lifetimes are needed
|
||||
// - What bounds are needed
|
||||
|
||||
// The error messages are documentation!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary: The Rust Mental Model
|
||||
|
||||
1. **Values have owners** - exactly one at a time
|
||||
2. **Borrowing is lending** - temporary access, owner retains responsibility
|
||||
3. **Lifetimes are scopes** - compiler tracks validity
|
||||
4. **Types encode constraints** - use them to prevent bugs
|
||||
5. **The compiler is your friend** - work with it, not against it
|
||||
|
||||
When stuck:
|
||||
- Clone to make progress
|
||||
- Restructure to own instead of borrow
|
||||
- Ask: "What is the compiler trying to tell me?"
|
||||
160
skills/m15-anti-pattern/SKILL.md
Normal file
160
skills/m15-anti-pattern/SKILL.md
Normal file
@@ -0,0 +1,160 @@
|
||||
---
|
||||
name: m15-anti-pattern
|
||||
description: "Use when reviewing code for anti-patterns. Keywords: anti-pattern, common mistake, pitfall, code smell, bad practice, code review, is this an anti-pattern, better way to do this, common mistake to avoid, why is this bad, idiomatic way, beginner mistake, fighting borrow checker, clone everywhere, unwrap in production, should I refactor, 反模式, 常见错误, 代码异味, 最佳实践, 地道写法"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Anti-Patterns
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Is this pattern hiding a design problem?**
|
||||
|
||||
When reviewing code:
|
||||
- Is this solving the symptom or the cause?
|
||||
- Is there a more idiomatic approach?
|
||||
- Does this fight or flow with Rust?
|
||||
|
||||
---
|
||||
|
||||
## Anti-Pattern → Better Pattern
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| `.clone()` everywhere | Hides ownership issues | Proper references or ownership |
|
||||
| `.unwrap()` in production | Runtime panics | `?`, `expect`, or handling |
|
||||
| `Rc` when single owner | Unnecessary overhead | Simple ownership |
|
||||
| `unsafe` for convenience | UB risk | Find safe pattern |
|
||||
| OOP via `Deref` | Misleading API | Composition, traits |
|
||||
| Giant match arms | Unmaintainable | Extract to methods |
|
||||
| `String` everywhere | Allocation waste | `&str`, `Cow<str>` |
|
||||
| Ignoring `#[must_use]` | Lost errors | Handle or `let _ =` |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
When seeing suspicious code:
|
||||
|
||||
1. **Is this symptom or cause?**
|
||||
- Clone to avoid borrow? → Ownership design issue
|
||||
- Unwrap "because it won't fail"? → Unhandled case
|
||||
|
||||
2. **What would idiomatic code look like?**
|
||||
- References instead of clones
|
||||
- Iterators instead of index loops
|
||||
- Pattern matching instead of flags
|
||||
|
||||
3. **Does this fight Rust?**
|
||||
- Fighting borrow checker → restructure
|
||||
- Excessive unsafe → find safe pattern
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To design understanding:
|
||||
|
||||
```
|
||||
"Why does my code have so many clones?"
|
||||
↑ Ask: Is the ownership model correct?
|
||||
↑ Check: m09-domain (data flow design)
|
||||
↑ Check: m01-ownership (reference patterns)
|
||||
```
|
||||
|
||||
| Anti-Pattern | Trace To | Question |
|
||||
|--------------|----------|----------|
|
||||
| Clone everywhere | m01-ownership | Who should own this data? |
|
||||
| Unwrap everywhere | m06-error-handling | What's the error strategy? |
|
||||
| Rc everywhere | m09-domain | Is ownership clear? |
|
||||
| Fighting lifetimes | m09-domain | Should data structure change? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Replace clone with proper ownership"
|
||||
↓ m01-ownership: Reference patterns
|
||||
↓ m02-resource: Smart pointer if needed
|
||||
|
||||
"Replace unwrap with proper handling"
|
||||
↓ m06-error-handling: ? operator
|
||||
↓ m06-error-handling: expect with message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Top 5 Beginner Mistakes
|
||||
|
||||
| Rank | Mistake | Fix |
|
||||
|------|---------|-----|
|
||||
| 1 | Clone to escape borrow checker | Use references |
|
||||
| 2 | Unwrap in production | Propagate with `?` |
|
||||
| 3 | String for everything | Use `&str` |
|
||||
| 4 | Index loops | Use iterators |
|
||||
| 5 | Fighting lifetimes | Restructure to own data |
|
||||
|
||||
## Code Smell → Refactoring
|
||||
|
||||
| Smell | Indicates | Refactoring |
|
||||
|-------|-----------|-------------|
|
||||
| Many `.clone()` | Ownership unclear | Clarify data flow |
|
||||
| Many `.unwrap()` | Error handling missing | Add proper handling |
|
||||
| Many `pub` fields | Encapsulation broken | Private + accessors |
|
||||
| Deep nesting | Complex logic | Extract methods |
|
||||
| Long functions | Multiple responsibilities | Split |
|
||||
| Giant enums | Missing abstraction | Trait + types |
|
||||
|
||||
---
|
||||
|
||||
## Common Error Patterns
|
||||
|
||||
| Error | Anti-Pattern Cause | Fix |
|
||||
|-------|-------------------|-----|
|
||||
| E0382 use after move | Cloning vs ownership | Proper references |
|
||||
| Panic in production | Unwrap everywhere | ?, matching |
|
||||
| Slow performance | String for all text | &str, Cow |
|
||||
| Borrow checker fights | Wrong structure | Restructure |
|
||||
| Memory bloat | Rc/Arc everywhere | Simple ownership |
|
||||
|
||||
---
|
||||
|
||||
## Deprecated → Better
|
||||
|
||||
| Deprecated | Better |
|
||||
|------------|--------|
|
||||
| Index-based loops | `.iter()`, `.enumerate()` |
|
||||
| `collect::<Vec<_>>()` then iterate | Chain iterators |
|
||||
| Manual unsafe cell | `Cell`, `RefCell` |
|
||||
| `mem::transmute` for casts | `as` or `TryFrom` |
|
||||
| Custom linked list | `Vec`, `VecDeque` |
|
||||
| `lazy_static!` | `std::sync::OnceLock` |
|
||||
|
||||
---
|
||||
|
||||
## Quick Review Checklist
|
||||
|
||||
- [ ] No `.clone()` without justification
|
||||
- [ ] No `.unwrap()` in library code
|
||||
- [ ] No `pub` fields with invariants
|
||||
- [ ] No index loops when iterator works
|
||||
- [ ] No `String` where `&str` suffices
|
||||
- [ ] No ignored `#[must_use]` warnings
|
||||
- [ ] No `unsafe` without SAFETY comment
|
||||
- [ ] No giant functions (>50 lines)
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Ownership patterns | m01-ownership |
|
||||
| Error handling | m06-error-handling |
|
||||
| Mental models | m14-mental-model |
|
||||
| Performance | m10-performance |
|
||||
421
skills/m15-anti-pattern/patterns/common-mistakes.md
Normal file
421
skills/m15-anti-pattern/patterns/common-mistakes.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# Common Rust Anti-Patterns & Mistakes
|
||||
|
||||
## Ownership Anti-Patterns
|
||||
|
||||
### 1. Clone Everything
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: clone to avoid borrow checker
|
||||
fn process(data: Vec<String>) {
|
||||
for item in data.clone() { // unnecessary clone
|
||||
println!("{}", item);
|
||||
}
|
||||
use_data(data);
|
||||
}
|
||||
|
||||
// BETTER: borrow when you don't need ownership
|
||||
fn process(data: Vec<String>) {
|
||||
for item in &data { // borrow instead
|
||||
println!("{}", item);
|
||||
}
|
||||
use_data(data);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Unnecessary Box
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: boxing everything
|
||||
fn get_value() -> Box<String> {
|
||||
Box::new(String::from("hello"))
|
||||
}
|
||||
|
||||
// BETTER: return value directly
|
||||
fn get_value() -> String {
|
||||
String::from("hello")
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Holding References Too Long
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: borrow prevents mutation
|
||||
let mut data = vec![1, 2, 3];
|
||||
let first = &data[0];
|
||||
data.push(4); // ERROR: data is borrowed
|
||||
println!("{}", first);
|
||||
|
||||
// BETTER: scope the borrow
|
||||
let mut data = vec![1, 2, 3];
|
||||
let first = data[0]; // copy the value
|
||||
data.push(4); // OK
|
||||
println!("{}", first);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Anti-Patterns
|
||||
|
||||
### 4. Unwrap Everywhere
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: crashes on error
|
||||
fn process_file(path: &str) {
|
||||
let content = std::fs::read_to_string(path).unwrap();
|
||||
let config: Config = toml::from_str(&content).unwrap();
|
||||
}
|
||||
|
||||
// BETTER: propagate errors
|
||||
fn process_file(path: &str) -> Result<Config, Error> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let config: Config = toml::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Ignoring Errors
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: silent failure
|
||||
let _ = file.write_all(data);
|
||||
|
||||
// BETTER: handle or propagate
|
||||
file.write_all(data)?;
|
||||
// or at minimum, log the error
|
||||
if let Err(e) = file.write_all(data) {
|
||||
eprintln!("Warning: failed to write: {}", e);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Panic in Library Code
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: library panics
|
||||
pub fn parse(input: &str) -> Data {
|
||||
if input.is_empty() {
|
||||
panic!("input cannot be empty");
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// BETTER: return Result
|
||||
pub fn parse(input: &str) -> Result<Data, ParseError> {
|
||||
if input.is_empty() {
|
||||
return Err(ParseError::EmptyInput);
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## String Anti-Patterns
|
||||
|
||||
### 7. String Instead of &str
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: forces allocation
|
||||
fn greet(name: String) {
|
||||
println!("Hello, {}", name);
|
||||
}
|
||||
|
||||
greet("world".to_string()); // allocation
|
||||
|
||||
// BETTER: accept &str
|
||||
fn greet(name: &str) {
|
||||
println!("Hello, {}", name);
|
||||
}
|
||||
|
||||
greet("world"); // no allocation
|
||||
```
|
||||
|
||||
### 8. Format for Simple Concatenation
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: format overhead
|
||||
let greeting = format!("{}{}", "Hello, ", name);
|
||||
|
||||
// BETTER for simple cases: push_str
|
||||
let mut greeting = String::from("Hello, ");
|
||||
greeting.push_str(name);
|
||||
|
||||
// Or use + for String + &str
|
||||
let greeting = String::from("Hello, ") + name;
|
||||
```
|
||||
|
||||
### 9. Repeated String Operations
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: O(n²) allocations
|
||||
let mut result = String::new();
|
||||
for word in words {
|
||||
result = result + word + " ";
|
||||
}
|
||||
|
||||
// BETTER: join
|
||||
let result = words.join(" ");
|
||||
|
||||
// Or with_capacity + push_str
|
||||
let mut result = String::with_capacity(total_len);
|
||||
for word in words {
|
||||
result.push_str(word);
|
||||
result.push(' ');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collection Anti-Patterns
|
||||
|
||||
### 10. Index Instead of Iterator
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: bounds checking overhead
|
||||
for i in 0..vec.len() {
|
||||
process(vec[i]);
|
||||
}
|
||||
|
||||
// BETTER: iterator
|
||||
for item in &vec {
|
||||
process(item);
|
||||
}
|
||||
```
|
||||
|
||||
### 11. Collect Then Iterate
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: unnecessary allocation
|
||||
let filtered: Vec<_> = items.iter().filter(|x| x.valid).collect();
|
||||
for item in filtered {
|
||||
process(item);
|
||||
}
|
||||
|
||||
// BETTER: chain iterators
|
||||
for item in items.iter().filter(|x| x.valid) {
|
||||
process(item);
|
||||
}
|
||||
```
|
||||
|
||||
### 12. Wrong Collection Type
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: Vec for frequent membership checks
|
||||
let allowed: Vec<&str> = vec!["a", "b", "c"];
|
||||
if allowed.contains(&input) { ... } // O(n)
|
||||
|
||||
// BETTER: HashSet for membership
|
||||
use std::collections::HashSet;
|
||||
let allowed: HashSet<&str> = ["a", "b", "c"].into();
|
||||
if allowed.contains(input) { ... } // O(1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Concurrency Anti-Patterns
|
||||
|
||||
### 13. Mutex for Read-Heavy Data
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: Mutex when mostly reading
|
||||
let data = Arc::new(Mutex::new(config));
|
||||
// All readers block each other
|
||||
|
||||
// BETTER: RwLock for read-heavy workloads
|
||||
let data = Arc::new(RwLock::new(config));
|
||||
// Multiple readers can proceed in parallel
|
||||
```
|
||||
|
||||
### 14. Holding Lock Across Await
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: lock held across await
|
||||
async fn bad() {
|
||||
let guard = mutex.lock().unwrap();
|
||||
some_async_op().await; // lock held!
|
||||
use(guard);
|
||||
}
|
||||
|
||||
// BETTER: scope the lock
|
||||
async fn good() {
|
||||
let value = {
|
||||
let guard = mutex.lock().unwrap();
|
||||
guard.clone()
|
||||
}; // lock released
|
||||
some_async_op().await;
|
||||
use(value);
|
||||
}
|
||||
```
|
||||
|
||||
### 15. Blocking in Async
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: blocking call in async
|
||||
async fn bad() {
|
||||
std::thread::sleep(Duration::from_secs(1)); // blocks executor!
|
||||
}
|
||||
|
||||
// BETTER: async sleep
|
||||
async fn good() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
// For CPU work: spawn_blocking
|
||||
async fn compute() {
|
||||
tokio::task::spawn_blocking(|| heavy_work()).await
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type System Anti-Patterns
|
||||
|
||||
### 16. Stringly Typed
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: strings for everything
|
||||
fn connect(host: &str, port: &str, timeout: &str) { ... }
|
||||
connect("8080", "localhost", "30"); // wrong order!
|
||||
|
||||
// BETTER: strong types
|
||||
struct Host(String);
|
||||
struct Port(u16);
|
||||
struct Timeout(Duration);
|
||||
|
||||
fn connect(host: Host, port: Port, timeout: Timeout) { ... }
|
||||
```
|
||||
|
||||
### 17. Boolean Parameters
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: what does true mean?
|
||||
fn fetch(url: &str, use_cache: bool, validate_ssl: bool) { ... }
|
||||
fetch("https://...", true, false); // unclear
|
||||
|
||||
// BETTER: builder or named parameters
|
||||
struct FetchOptions {
|
||||
use_cache: bool,
|
||||
validate_ssl: bool,
|
||||
}
|
||||
|
||||
fn fetch(url: &str, options: FetchOptions) { ... }
|
||||
fetch("https://...", FetchOptions {
|
||||
use_cache: true,
|
||||
validate_ssl: false,
|
||||
});
|
||||
```
|
||||
|
||||
### 18. Option<Option<T>>
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: nested Option
|
||||
fn find(id: u32) -> Option<Option<User>> { ... }
|
||||
// What does None vs Some(None) mean?
|
||||
|
||||
// BETTER: use Result or custom enum
|
||||
enum FindResult {
|
||||
Found(User),
|
||||
NotFound,
|
||||
Error(String),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Design Anti-Patterns
|
||||
|
||||
### 19. Taking Ownership Unnecessarily
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: takes ownership but doesn't need it
|
||||
fn validate(config: Config) -> bool {
|
||||
config.timeout > 0 && config.retries >= 0
|
||||
}
|
||||
|
||||
// BETTER: borrow
|
||||
fn validate(config: &Config) -> bool {
|
||||
config.timeout > 0 && config.retries >= 0
|
||||
}
|
||||
```
|
||||
|
||||
### 20. Returning References to Temporaries
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: impossible lifetime
|
||||
fn get_default() -> &str {
|
||||
let s = String::from("default");
|
||||
&s // ERROR: s is dropped
|
||||
}
|
||||
|
||||
// BETTER: return owned
|
||||
fn get_default() -> String {
|
||||
String::from("default")
|
||||
}
|
||||
|
||||
// Or return static
|
||||
fn get_default() -> &'static str {
|
||||
"default"
|
||||
}
|
||||
```
|
||||
|
||||
### 21. Overly Generic Functions
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: complex generics for simple function
|
||||
fn process<T, U, V>(input: T) -> V
|
||||
where
|
||||
T: Into<U>,
|
||||
U: AsRef<str> + Clone,
|
||||
V: From<String>,
|
||||
{ ... }
|
||||
|
||||
// BETTER: concrete types if generics not needed
|
||||
fn process(input: &str) -> String { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Macro Anti-Patterns
|
||||
|
||||
### 22. Macro When Function Works
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: macro for simple operation
|
||||
macro_rules! add {
|
||||
($a:expr, $b:expr) => { $a + $b };
|
||||
}
|
||||
|
||||
// BETTER: just use a function
|
||||
fn add(a: i32, b: i32) -> i32 { a + b }
|
||||
```
|
||||
|
||||
### 23. Complex Macro Without Tests
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: complex macro with no tests
|
||||
macro_rules! define_api {
|
||||
// ... 100 lines of macro code ...
|
||||
}
|
||||
|
||||
// BETTER: test macro outputs
|
||||
#[test]
|
||||
fn test_macro_expansion() {
|
||||
// Use cargo-expand or trybuild
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Anti-Pattern | Better Alternative |
|
||||
|--------------|-------------------|
|
||||
| Clone everywhere | Borrow when possible |
|
||||
| Unwrap everywhere | Propagate with `?` |
|
||||
| `String` parameters | `&str` parameters |
|
||||
| Index loops | Iterator loops |
|
||||
| Collect then process | Chain iterators |
|
||||
| Mutex for reads | RwLock for read-heavy |
|
||||
| Lock across await | Scope the lock |
|
||||
| Blocking in async | spawn_blocking |
|
||||
| Stringly typed | Strong types |
|
||||
| Boolean params | Builders or enums |
|
||||
352
skills/meta-cognition-parallel/SKILL.md
Normal file
352
skills/meta-cognition-parallel/SKILL.md
Normal file
@@ -0,0 +1,352 @@
|
||||
---
|
||||
name: meta-cognition-parallel
|
||||
description: "EXPERIMENTAL: Three-layer parallel meta-cognition analysis. Triggers on: /meta-parallel, 三层分析, parallel analysis, 并行元认知"
|
||||
argument-hint: "<rust_question>"
|
||||
---
|
||||
|
||||
# Meta-Cognition Parallel Analysis (Experimental)
|
||||
|
||||
> **Status:** Experimental | **Version:** 0.2.0 | **Last Updated:** 2025-01-27
|
||||
>
|
||||
> This skill tests parallel three-layer cognitive analysis.
|
||||
|
||||
## Concept
|
||||
|
||||
Instead of sequential analysis, this skill launches three parallel analyzers - one for each cognitive layer - then synthesizes their results.
|
||||
|
||||
```
|
||||
User Question
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ meta-cognition-parallel │
|
||||
│ (Coordinator) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
├─── Layer 1 ──► Language Mechanics ──► L1 Result
|
||||
│
|
||||
├─── Layer 2 ──► Design Choices ──► L2 Result
|
||||
│ ├── Parallel (Agent Mode)
|
||||
│ │ or Sequential (Inline)
|
||||
└─── Layer 3 ──► Domain Constraints ──► L3 Result
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Cross-Layer Synthesis │
|
||||
│ (In main context with all results) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Domain-Correct Architectural Solution
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/meta-parallel <your Rust question>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
/meta-parallel 我的交易系统报 E0382 错误,应该用 clone 吗?
|
||||
```
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check agent file availability first to determine execution mode.**
|
||||
|
||||
Try to read layer analyzer files:
|
||||
- `../../agents/layer1-analyzer.md`
|
||||
- `../../agents/layer2-analyzer.md`
|
||||
- `../../agents/layer3-analyzer.md`
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install) - Parallel Execution
|
||||
|
||||
**When all layer analyzer files exist at `../../agents/`:**
|
||||
|
||||
### Step 1: Parse User Query
|
||||
|
||||
Extract from `$ARGUMENTS`:
|
||||
- The original question
|
||||
- Any code snippets
|
||||
- Domain hints (trading, web, embedded, etc.)
|
||||
|
||||
### Step 2: Launch Three Parallel Agents
|
||||
|
||||
**CRITICAL: Launch all three Tasks in a SINGLE message to enable parallel execution.**
|
||||
|
||||
```
|
||||
Read agent files, then launch in parallel:
|
||||
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
run_in_background: true,
|
||||
prompt: <content of ../../agents/layer1-analyzer.md>
|
||||
+ "\n\n## User Query\n" + $ARGUMENTS
|
||||
)
|
||||
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
run_in_background: true,
|
||||
prompt: <content of ../../agents/layer2-analyzer.md>
|
||||
+ "\n\n## User Query\n" + $ARGUMENTS
|
||||
)
|
||||
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
run_in_background: true,
|
||||
prompt: <content of ../../agents/layer3-analyzer.md>
|
||||
+ "\n\n## User Query\n" + $ARGUMENTS
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3: Collect Results
|
||||
|
||||
Wait for all three agents to complete. Each returns structured analysis.
|
||||
|
||||
### Step 4: Cross-Layer Synthesis
|
||||
|
||||
With all three results, perform synthesis per template below.
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install) - Sequential Execution
|
||||
|
||||
**When layer analyzer files are NOT available, execute analysis directly:**
|
||||
|
||||
### Step 1: Parse User Query
|
||||
|
||||
Same as Agent Mode - extract question, code, and domain hints from `$ARGUMENTS`.
|
||||
|
||||
### Step 2: Execute Layer 1 - Language Mechanics
|
||||
|
||||
Analyze the Rust language mechanics involved:
|
||||
|
||||
```markdown
|
||||
## Layer 1: Language Mechanics
|
||||
|
||||
**Error/Pattern Identified:**
|
||||
- Error code: E0XXX (if applicable)
|
||||
- Pattern: ownership/borrowing/lifetime/etc.
|
||||
|
||||
**Root Cause:**
|
||||
[Explain why this error occurs in terms of Rust's ownership model]
|
||||
|
||||
**Language-Level Solutions:**
|
||||
1. [Solution 1]: description
|
||||
2. [Solution 2]: description
|
||||
|
||||
**Confidence:** HIGH | MEDIUM | LOW
|
||||
**Reasoning:** [Why this confidence level]
|
||||
```
|
||||
|
||||
**Focus areas:**
|
||||
- Ownership rules (move, copy, borrow)
|
||||
- Lifetime annotations
|
||||
- Borrowing rules (shared vs mutable)
|
||||
- Error codes and their meanings
|
||||
|
||||
### Step 3: Execute Layer 2 - Design Choices
|
||||
|
||||
Analyze the design patterns and trade-offs:
|
||||
|
||||
```markdown
|
||||
## Layer 2: Design Choices
|
||||
|
||||
**Design Pattern Context:**
|
||||
- Current approach: [What pattern is being used]
|
||||
- Problem: [Why it conflicts with Rust's rules]
|
||||
|
||||
**Design Alternatives:**
|
||||
| Pattern | Pros | Cons | When to Use |
|
||||
|---------|------|------|-------------|
|
||||
| Pattern A | ... | ... | ... |
|
||||
| Pattern B | ... | ... | ... |
|
||||
|
||||
**Recommended Pattern:**
|
||||
[Which pattern fits best and why]
|
||||
|
||||
**Confidence:** HIGH | MEDIUM | LOW
|
||||
**Reasoning:** [Why this confidence level]
|
||||
```
|
||||
|
||||
**Focus areas:**
|
||||
- Smart pointer choices (Box, Rc, Arc)
|
||||
- Interior mutability patterns (Cell, RefCell, Mutex)
|
||||
- Ownership transfer vs sharing
|
||||
- Cloning vs references
|
||||
|
||||
### Step 4: Execute Layer 3 - Domain Constraints
|
||||
|
||||
Analyze domain-specific requirements:
|
||||
|
||||
```markdown
|
||||
## Layer 3: Domain Constraints
|
||||
|
||||
**Domain Identified:** [trading/fintech | web | CLI | embedded | etc.]
|
||||
|
||||
**Domain-Specific Requirements:**
|
||||
- [ ] Performance: [requirements]
|
||||
- [ ] Safety: [requirements]
|
||||
- [ ] Concurrency: [requirements]
|
||||
- [ ] Auditability: [requirements]
|
||||
|
||||
**Domain Best Practices:**
|
||||
1. [Best practice 1]
|
||||
2. [Best practice 2]
|
||||
|
||||
**Constraints on Solution:**
|
||||
- MUST: [hard requirements]
|
||||
- SHOULD: [soft requirements]
|
||||
- AVOID: [anti-patterns for this domain]
|
||||
|
||||
**Confidence:** HIGH | MEDIUM | LOW
|
||||
**Reasoning:** [Why this confidence level]
|
||||
```
|
||||
|
||||
**Focus areas:**
|
||||
- Industry requirements (FinTech regulations, web scalability, etc.)
|
||||
- Performance constraints
|
||||
- Safety and correctness requirements
|
||||
- Common patterns in the domain
|
||||
|
||||
### Step 5: Cross-Layer Synthesis
|
||||
|
||||
Combine all three layers:
|
||||
|
||||
```markdown
|
||||
## Cross-Layer Synthesis
|
||||
|
||||
### Layer Results Summary
|
||||
|
||||
| Layer | Key Finding | Confidence |
|
||||
|-------|-------------|------------|
|
||||
| L1 (Mechanics) | [Summary] | [Level] |
|
||||
| L2 (Design) | [Summary] | [Level] |
|
||||
| L3 (Domain) | [Summary] | [Level] |
|
||||
|
||||
### Cross-Layer Reasoning
|
||||
|
||||
1. **L3 → L2:** [How domain constraints affect design choice]
|
||||
2. **L2 → L1:** [How design choice determines mechanism]
|
||||
3. **L1 ← L3:** [Direct domain impact on language features]
|
||||
|
||||
### Synthesized Recommendation
|
||||
|
||||
**Problem:** [Restated with full context]
|
||||
|
||||
**Solution:** [Domain-correct architectural solution]
|
||||
|
||||
**Rationale:**
|
||||
- Domain requires: [L3 constraint]
|
||||
- Design pattern: [L2 pattern]
|
||||
- Mechanism: [L1 implementation]
|
||||
|
||||
### Confidence Assessment
|
||||
|
||||
- **Overall:** HIGH | MEDIUM | LOW
|
||||
- **Limiting Factor:** [Which layer had lowest confidence]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output Template
|
||||
|
||||
Both modes produce the same output format:
|
||||
|
||||
```markdown
|
||||
# Three-Layer Meta-Cognition Analysis
|
||||
|
||||
> Query: [User's question]
|
||||
|
||||
---
|
||||
|
||||
## Layer 1: Language Mechanics
|
||||
[L1 analysis result]
|
||||
|
||||
---
|
||||
|
||||
## Layer 2: Design Choices
|
||||
[L2 analysis result]
|
||||
|
||||
---
|
||||
|
||||
## Layer 3: Domain Constraints
|
||||
[L3 analysis result]
|
||||
|
||||
---
|
||||
|
||||
## Cross-Layer Synthesis
|
||||
|
||||
### Reasoning Chain
|
||||
```
|
||||
L3 Domain: [Constraint]
|
||||
↓ implies
|
||||
L2 Design: [Pattern]
|
||||
↓ implemented via
|
||||
L1 Mechanism: [Feature]
|
||||
```
|
||||
|
||||
### Final Recommendation
|
||||
|
||||
**Do:** [Recommended approach]
|
||||
|
||||
**Don't:** [What to avoid]
|
||||
|
||||
**Code Pattern:**
|
||||
```rust
|
||||
// Recommended implementation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Analysis performed by meta-cognition-parallel v0.2.0 (experimental)*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Test 1: Trading System E0382
|
||||
```
|
||||
/meta-parallel 交易系统报 E0382,trade record 被 move 了
|
||||
```
|
||||
|
||||
Expected: L3 identifies FinTech constraints → L2 suggests shared immutable → L1 recommends Arc<T>
|
||||
|
||||
### Test 2: Web API Concurrency
|
||||
```
|
||||
/meta-parallel Web API 中多个 handler 需要共享数据库连接池
|
||||
```
|
||||
|
||||
Expected: L3 identifies Web constraints → L2 suggests connection pooling → L1 recommends Arc<Pool>
|
||||
|
||||
### Test 3: CLI Tool Config
|
||||
```
|
||||
/meta-parallel CLI 工具如何处理配置文件和命令行参数的优先级
|
||||
```
|
||||
|
||||
Expected: L3 identifies CLI constraints → L2 suggests config precedence pattern → L1 recommends builder pattern
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Agent files not found | Skills-only install | Use inline mode (sequential) |
|
||||
| Agent timeout | Complex analysis | Wait longer or use inline mode |
|
||||
| Incomplete layer result | Agent issue | Fill in with inline analysis |
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Agent Mode:** Parallel execution, faster but requires plugin install
|
||||
- **Inline Mode:** Sequential execution, slower but works everywhere
|
||||
- Cross-layer synthesis quality depends on result structure
|
||||
- May have higher latency than simple single-layer analysis
|
||||
|
||||
## Feedback
|
||||
|
||||
This is experimental. Please report issues and suggestions to improve the three-layer analysis approach.
|
||||
206
skills/rust-call-graph/SKILL.md
Normal file
206
skills/rust-call-graph/SKILL.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
name: rust-call-graph
|
||||
description: "Visualize Rust function call graphs using LSP. Triggers on: /call-graph, call hierarchy, who calls, what calls, 调用图, 调用关系, 谁调用了, 调用了谁"
|
||||
argument-hint: "<function_name> [--depth N] [--direction in|out|both]"
|
||||
allowed-tools: ["LSP", "Read", "Glob"]
|
||||
---
|
||||
|
||||
# Rust Call Graph
|
||||
|
||||
Visualize function call relationships using LSP call hierarchy.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-call-graph <function_name> [--depth N] [--direction in|out|both]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--depth N`: How many levels to traverse (default: 3)
|
||||
- `--direction`: `in` (callers), `out` (callees), `both`
|
||||
|
||||
**Examples:**
|
||||
- `/rust-call-graph process_request` - Show both callers and callees
|
||||
- `/rust-call-graph handle_error --direction in` - Show only callers
|
||||
- `/rust-call-graph main --direction out --depth 5` - Deep callee analysis
|
||||
|
||||
## LSP Operations
|
||||
|
||||
### 1. Prepare Call Hierarchy
|
||||
|
||||
Get the call hierarchy item for a function.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "prepareCallHierarchy",
|
||||
filePath: "src/handler.rs",
|
||||
line: 45,
|
||||
character: 8
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Incoming Calls (Who calls this?)
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "incomingCalls",
|
||||
filePath: "src/handler.rs",
|
||||
line: 45,
|
||||
character: 8
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Outgoing Calls (What does this call?)
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "outgoingCalls",
|
||||
filePath: "src/handler.rs",
|
||||
line: 45,
|
||||
character: 8
|
||||
)
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
User: "Show call graph for process_request"
|
||||
│
|
||||
▼
|
||||
[1] Find function location
|
||||
LSP(workspaceSymbol) or Grep
|
||||
│
|
||||
▼
|
||||
[2] Prepare call hierarchy
|
||||
LSP(prepareCallHierarchy)
|
||||
│
|
||||
▼
|
||||
[3] Get incoming calls (callers)
|
||||
LSP(incomingCalls)
|
||||
│
|
||||
▼
|
||||
[4] Get outgoing calls (callees)
|
||||
LSP(outgoingCalls)
|
||||
│
|
||||
▼
|
||||
[5] Recursively expand to depth N
|
||||
│
|
||||
▼
|
||||
[6] Generate ASCII visualization
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
### Incoming Calls (Who calls this?)
|
||||
|
||||
```
|
||||
## Callers of `process_request`
|
||||
|
||||
main
|
||||
└── run_server
|
||||
└── handle_connection
|
||||
└── process_request ◄── YOU ARE HERE
|
||||
```
|
||||
|
||||
### Outgoing Calls (What does this call?)
|
||||
|
||||
```
|
||||
## Callees of `process_request`
|
||||
|
||||
process_request ◄── YOU ARE HERE
|
||||
├── parse_headers
|
||||
│ └── validate_header
|
||||
├── authenticate
|
||||
│ ├── check_token
|
||||
│ └── load_user
|
||||
├── execute_handler
|
||||
│ └── [dynamic dispatch]
|
||||
└── send_response
|
||||
└── serialize_body
|
||||
```
|
||||
|
||||
### Bidirectional (Both)
|
||||
|
||||
```
|
||||
## Call Graph for `process_request`
|
||||
|
||||
┌─────────────────┐
|
||||
│ main │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ run_server │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│handle_connection│
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌───────▼───────┐ ┌───────▼───────┐ ┌───────▼───────┐
|
||||
│ parse_headers │ │ authenticate │ │send_response │
|
||||
└───────────────┘ └───────┬───────┘ └───────────────┘
|
||||
│
|
||||
┌───────┴───────┐
|
||||
│ │
|
||||
┌──────▼──────┐ ┌──────▼──────┐
|
||||
│ check_token │ │ load_user │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## Analysis Insights
|
||||
|
||||
After generating the call graph, provide insights:
|
||||
|
||||
```
|
||||
## Analysis
|
||||
|
||||
**Entry Points:** main, test_process_request
|
||||
**Leaf Functions:** validate_header, serialize_body
|
||||
**Hot Path:** main → run_server → handle_connection → process_request
|
||||
**Complexity:** 12 functions, 3 levels deep
|
||||
|
||||
**Potential Issues:**
|
||||
- `authenticate` has high fan-out (4 callees)
|
||||
- `process_request` is called from 3 places (consider if this is intentional)
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
| User Says | Direction | Use Case |
|
||||
|-----------|-----------|----------|
|
||||
| "Who calls X?" | incoming | Impact analysis |
|
||||
| "What does X call?" | outgoing | Understanding implementation |
|
||||
| "Show call graph" | both | Full picture |
|
||||
| "Trace from main to X" | outgoing | Execution path |
|
||||
|
||||
## Visualization Options
|
||||
|
||||
| Style | Best For |
|
||||
|-------|----------|
|
||||
| Tree (default) | Simple hierarchies |
|
||||
| Box diagram | Complex relationships |
|
||||
| Flat list | Many connections |
|
||||
| Mermaid | Export to docs |
|
||||
|
||||
### Mermaid Export
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
main --> run_server
|
||||
run_server --> handle_connection
|
||||
handle_connection --> process_request
|
||||
process_request --> parse_headers
|
||||
process_request --> authenticate
|
||||
process_request --> send_response
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Find definition | rust-code-navigator |
|
||||
| Project structure | rust-symbol-analyzer |
|
||||
| Trait implementations | rust-trait-explorer |
|
||||
| Safe refactoring | rust-refactor-helper |
|
||||
159
skills/rust-code-navigator/SKILL.md
Normal file
159
skills/rust-code-navigator/SKILL.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: rust-code-navigator
|
||||
description: "Navigate Rust code using LSP. Triggers on: /navigate, go to definition, find references, where is defined, 跳转定义, 查找引用, 定义在哪, 谁用了这个"
|
||||
argument-hint: "<symbol> [in file.rs:line]"
|
||||
allowed-tools: ["LSP", "Read", "Glob"]
|
||||
---
|
||||
|
||||
# Rust Code Navigator
|
||||
|
||||
Navigate large Rust codebases efficiently using Language Server Protocol.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-code-navigator <symbol> [in file.rs:line]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- `/rust-code-navigator parse_config` - Find definition of parse_config
|
||||
- `/rust-code-navigator MyStruct in src/lib.rs:42` - Navigate from specific location
|
||||
|
||||
## LSP Operations
|
||||
|
||||
### 1. Go to Definition
|
||||
|
||||
Find where a symbol is defined.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "goToDefinition",
|
||||
filePath: "src/main.rs",
|
||||
line: 25,
|
||||
character: 10
|
||||
)
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- User asks "where is X defined?"
|
||||
- User wants to understand a type/function
|
||||
- Ctrl+click equivalent
|
||||
|
||||
### 2. Find References
|
||||
|
||||
Find all usages of a symbol.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "findReferences",
|
||||
filePath: "src/lib.rs",
|
||||
line: 15,
|
||||
character: 8
|
||||
)
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- User asks "who uses X?"
|
||||
- Before refactoring/renaming
|
||||
- Understanding impact of changes
|
||||
|
||||
### 3. Hover Information
|
||||
|
||||
Get type and documentation for a symbol.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "hover",
|
||||
filePath: "src/main.rs",
|
||||
line: 30,
|
||||
character: 15
|
||||
)
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- User asks "what type is X?"
|
||||
- User wants documentation
|
||||
- Quick type checking
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
User: "Where is the Config struct defined?"
|
||||
│
|
||||
▼
|
||||
[1] Search for "Config" in workspace
|
||||
LSP(operation: "workspaceSymbol", ...)
|
||||
│
|
||||
▼
|
||||
[2] If multiple results, ask user to clarify
|
||||
│
|
||||
▼
|
||||
[3] Go to definition
|
||||
LSP(operation: "goToDefinition", ...)
|
||||
│
|
||||
▼
|
||||
[4] Show file path and context
|
||||
Read surrounding code for context
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
### Definition Found
|
||||
|
||||
```
|
||||
## Config (struct)
|
||||
|
||||
**Defined in:** `src/config.rs:15`
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub name: String,
|
||||
pub port: u16,
|
||||
pub debug: bool,
|
||||
}
|
||||
```
|
||||
|
||||
**Documentation:** Configuration for the application server.
|
||||
```
|
||||
|
||||
### References Found
|
||||
|
||||
```
|
||||
## References to `Config` (5 found)
|
||||
|
||||
| Location | Context |
|
||||
|----------|---------|
|
||||
| src/main.rs:10 | `let config = Config::load()?;` |
|
||||
| src/server.rs:25 | `fn new(config: Config) -> Self` |
|
||||
| src/server.rs:42 | `self.config.port` |
|
||||
| src/tests.rs:15 | `Config::default()` |
|
||||
| src/cli.rs:8 | `config: Option<Config>` |
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
| User Says | LSP Operation |
|
||||
|-----------|---------------|
|
||||
| "Where is X defined?" | goToDefinition |
|
||||
| "Who uses X?" | findReferences |
|
||||
| "What type is X?" | hover |
|
||||
| "Find all structs" | workspaceSymbol |
|
||||
| "What's in this file?" | documentSymbol |
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| "No LSP server" | rust-analyzer not running | Suggest: `rustup component add rust-analyzer` |
|
||||
| "Symbol not found" | Typo or not in scope | Search with workspaceSymbol first |
|
||||
| "Multiple definitions" | Generics or macros | Show all and let user choose |
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Call relationships | rust-call-graph |
|
||||
| Project structure | rust-symbol-analyzer |
|
||||
| Trait implementations | rust-trait-explorer |
|
||||
| Safe refactoring | rust-refactor-helper |
|
||||
233
skills/rust-daily/SKILL.md
Normal file
233
skills/rust-daily/SKILL.md
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
name: rust-daily
|
||||
description: |
|
||||
CRITICAL: Use for Rust news and daily/weekly/monthly reports. Triggers on:
|
||||
rust news, rust daily, rust weekly, TWIR, rust blog,
|
||||
Rust 日报, Rust 周报, Rust 新闻, Rust 动态
|
||||
argument-hint: "[today|week|month]"
|
||||
context: fork
|
||||
agent: Explore
|
||||
---
|
||||
|
||||
# Rust Daily Report
|
||||
|
||||
> **Version:** 2.1.0 | **Last Updated:** 2025-01-27
|
||||
|
||||
Fetch Rust community updates, filtered by time range.
|
||||
|
||||
## Data Sources
|
||||
|
||||
| Category | Sources |
|
||||
|----------|---------|
|
||||
| Ecosystem | Reddit r/rust, This Week in Rust |
|
||||
| Official | blog.rust-lang.org, Inside Rust |
|
||||
| Foundation | rustfoundation.org (news, blog, events) |
|
||||
|
||||
## Parameters
|
||||
|
||||
- `time_range`: day | week | month (default: week)
|
||||
- `category`: all | ecosystem | official | foundation
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check agent file availability first to determine execution mode.**
|
||||
|
||||
Try to read: `../../agents/rust-daily-reporter.md`
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install)
|
||||
|
||||
**When `../../agents/rust-daily-reporter.md` exists:**
|
||||
|
||||
### Workflow
|
||||
|
||||
```
|
||||
1. Read: ../../agents/rust-daily-reporter.md
|
||||
2. Task(subagent_type: "general-purpose", run_in_background: false, prompt: <agent content>)
|
||||
3. Wait for result
|
||||
4. Format and present to user
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install)
|
||||
|
||||
**When agent file is NOT available, execute each source directly:**
|
||||
|
||||
### 1. Reddit r/rust
|
||||
|
||||
```bash
|
||||
# Using agent-browser CLI
|
||||
agent-browser open "https://www.reddit.com/r/rust/hot/"
|
||||
agent-browser get text ".Post" --limit 10
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Or with WebFetch fallback:**
|
||||
```
|
||||
WebFetch("https://www.reddit.com/r/rust/hot/", "Extract top 10 posts with scores and titles")
|
||||
```
|
||||
|
||||
**Parse output into:**
|
||||
| Score | Title | Link |
|
||||
|-------|-------|------|
|
||||
|
||||
### 2. This Week in Rust
|
||||
|
||||
```bash
|
||||
# Check actionbook first
|
||||
mcp__actionbook__search_actions("this week in rust")
|
||||
mcp__actionbook__get_action_by_id(<action_id>)
|
||||
|
||||
# Then fetch
|
||||
agent-browser open "https://this-week-in-rust.org/"
|
||||
agent-browser get text "<selector_from_actionbook>"
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Parse output into:**
|
||||
- Issue #{number} ({date}): highlights
|
||||
|
||||
### 3. Rust Blog (Official)
|
||||
|
||||
```bash
|
||||
agent-browser open "https://blog.rust-lang.org/"
|
||||
agent-browser get text "article" --limit 5
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Or with WebFetch fallback:**
|
||||
```
|
||||
WebFetch("https://blog.rust-lang.org/", "Extract latest 5 blog posts with dates and titles")
|
||||
```
|
||||
|
||||
**Parse output into:**
|
||||
| Date | Title | Summary |
|
||||
|------|-------|---------|
|
||||
|
||||
### 4. Inside Rust
|
||||
|
||||
```bash
|
||||
agent-browser open "https://blog.rust-lang.org/inside-rust/"
|
||||
agent-browser get text "article" --limit 3
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Or with WebFetch fallback:**
|
||||
```
|
||||
WebFetch("https://blog.rust-lang.org/inside-rust/", "Extract latest 3 posts with dates and titles")
|
||||
```
|
||||
|
||||
### 5. Rust Foundation
|
||||
|
||||
```bash
|
||||
# News
|
||||
agent-browser open "https://rustfoundation.org/media/category/news/"
|
||||
agent-browser get text "article" --limit 3
|
||||
agent-browser close
|
||||
|
||||
# Blog
|
||||
agent-browser open "https://rustfoundation.org/media/category/blog/"
|
||||
agent-browser get text "article" --limit 3
|
||||
agent-browser close
|
||||
|
||||
# Events
|
||||
agent-browser open "https://rustfoundation.org/events/"
|
||||
agent-browser get text "article" --limit 3
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
### Time Filtering
|
||||
|
||||
After fetching all sources, filter by time range:
|
||||
|
||||
| Range | Filter |
|
||||
|-------|--------|
|
||||
| day | Last 24 hours |
|
||||
| week | Last 7 days |
|
||||
| month | Last 30 days |
|
||||
|
||||
### Combining Results
|
||||
|
||||
After fetching all sources, combine into the output format below.
|
||||
|
||||
---
|
||||
|
||||
## Tool Chain Priority
|
||||
|
||||
Both modes use the same tool chain order:
|
||||
|
||||
1. **actionbook MCP** - Check for cached/pre-fetched content first
|
||||
```
|
||||
mcp__actionbook__search_actions("rust news {date}")
|
||||
mcp__actionbook__search_actions("this week in rust")
|
||||
mcp__actionbook__search_actions("rust blog")
|
||||
```
|
||||
|
||||
2. **agent-browser CLI** - For dynamic web content
|
||||
```bash
|
||||
agent-browser open "<url>"
|
||||
agent-browser get text "<selector>"
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
3. **WebFetch** - Fallback if agent-browser unavailable
|
||||
|
||||
| Source | Primary Tool | Fallback |
|
||||
|--------|--------------|----------|
|
||||
| Reddit | agent-browser | WebFetch |
|
||||
| TWIR | actionbook → agent-browser | WebFetch |
|
||||
| Rust Blog | actionbook → WebFetch | - |
|
||||
| Foundation | actionbook → WebFetch | - |
|
||||
|
||||
**DO NOT use:**
|
||||
- Chrome MCP directly
|
||||
- WebSearch for fetching news pages
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# Rust {Weekly|Daily|Monthly} Report
|
||||
|
||||
**Time Range:** {start} - {end}
|
||||
|
||||
## Ecosystem
|
||||
|
||||
### Reddit r/rust
|
||||
| Score | Title | Link |
|
||||
|-------|-------|------|
|
||||
| {score} | {title} | [link]({url}) |
|
||||
|
||||
### This Week in Rust
|
||||
- Issue #{number} ({date}): highlights
|
||||
|
||||
## Official
|
||||
| Date | Title | Summary |
|
||||
|------|-------|---------|
|
||||
| {date} | {title} | {summary} |
|
||||
|
||||
## Foundation
|
||||
| Date | Title | Summary |
|
||||
|------|-------|---------|
|
||||
| {date} | {title} | {summary} |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
- Each source should have at least 1 result, otherwise mark "No updates"
|
||||
- On fetch failure, retry with alternative tool
|
||||
- Report reason if all tools fail for a source
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Agent file not found | Skills-only install | Use inline mode |
|
||||
| agent-browser unavailable | CLI not installed | Use WebFetch |
|
||||
| Site timeout | Network issues | Retry once, then skip source |
|
||||
| Empty results | Selector mismatch | Report and use fallback |
|
||||
114
skills/rust-deps-visualizer/SKILL.md
Normal file
114
skills/rust-deps-visualizer/SKILL.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: rust-deps-visualizer
|
||||
description: "Visualize Rust project dependencies as ASCII art. Triggers on: /deps-viz, dependency graph, show dependencies, visualize deps, 依赖图, 依赖可视化, 显示依赖"
|
||||
argument-hint: "[--depth N] [--features]"
|
||||
allowed-tools: ["Bash", "Read", "Glob"]
|
||||
---
|
||||
|
||||
# Rust Dependencies Visualizer
|
||||
|
||||
Generate ASCII art visualizations of your Rust project's dependency tree.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-deps-visualizer [--depth N] [--features]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--depth N`: Limit tree depth (default: 3)
|
||||
- `--features`: Show feature flags
|
||||
|
||||
## Output Format
|
||||
|
||||
### Simple Tree (Default)
|
||||
|
||||
```
|
||||
my-project v0.1.0
|
||||
├── tokio v1.49.0
|
||||
│ ├── pin-project-lite v0.2.x
|
||||
│ └── bytes v1.x
|
||||
├── serde v1.0.x
|
||||
│ └── serde_derive v1.0.x
|
||||
└── anyhow v1.x
|
||||
```
|
||||
|
||||
### Feature-Aware Tree
|
||||
|
||||
```
|
||||
my-project v0.1.0
|
||||
├── tokio v1.49.0 [rt, rt-multi-thread, macros, fs, io-util]
|
||||
│ ├── pin-project-lite v0.2.x
|
||||
│ └── bytes v1.x
|
||||
├── serde v1.0.x [derive]
|
||||
│ └── serde_derive v1.0.x (proc-macro)
|
||||
└── anyhow v1.x [std]
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
**Step 1:** Parse Cargo.toml for direct dependencies
|
||||
|
||||
```bash
|
||||
cargo metadata --format-version=1 --no-deps 2>/dev/null
|
||||
```
|
||||
|
||||
**Step 2:** Get full dependency tree
|
||||
|
||||
```bash
|
||||
cargo tree --depth=${DEPTH:-3} ${FEATURES:+--features} 2>/dev/null
|
||||
```
|
||||
|
||||
**Step 3:** Format as ASCII art tree
|
||||
|
||||
Use these box-drawing characters:
|
||||
- `├──` for middle items
|
||||
- `└──` for last items
|
||||
- `│ ` for continuation lines
|
||||
|
||||
## Visual Enhancements
|
||||
|
||||
### Dependency Categories
|
||||
|
||||
```
|
||||
my-project v0.1.0
|
||||
│
|
||||
├─[Runtime]─────────────────────
|
||||
│ ├── tokio v1.49.0
|
||||
│ └── async-trait v0.1.x
|
||||
│
|
||||
├─[Serialization]───────────────
|
||||
│ ├── serde v1.0.x
|
||||
│ └── serde_json v1.x
|
||||
│
|
||||
└─[Development]─────────────────
|
||||
├── criterion v0.5.x
|
||||
└── proptest v1.x
|
||||
```
|
||||
|
||||
### Size Visualization (Optional)
|
||||
|
||||
```
|
||||
my-project v0.1.0
|
||||
├── tokio v1.49.0 ████████████ 2.1 MB
|
||||
├── serde v1.0.x ███████ 1.2 MB
|
||||
├── regex v1.x █████ 890 KB
|
||||
└── anyhow v1.x ██ 120 KB
|
||||
─────────────────
|
||||
Total: 4.3 MB
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Check for Cargo.toml in current directory
|
||||
2. Run `cargo tree` with specified options
|
||||
3. Parse output and generate ASCII visualization
|
||||
4. Optionally categorize by purpose (runtime, dev, build)
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Crate selection advice | m11-ecosystem |
|
||||
| Workspace management | m11-ecosystem |
|
||||
| Feature flag decisions | m11-ecosystem |
|
||||
310
skills/rust-learner/SKILL.md
Normal file
310
skills/rust-learner/SKILL.md
Normal file
@@ -0,0 +1,310 @@
|
||||
---
|
||||
name: rust-learner
|
||||
description: "Use when asking about Rust versions or crate info. Keywords: latest version, what's new, changelog, Rust 1.x, Rust release, stable, nightly, crate info, crates.io, lib.rs, docs.rs, API documentation, crate features, dependencies, which crate, what version, Rust edition, edition 2021, edition 2024, cargo add, cargo update, 最新版本, 版本号, 稳定版, 最新, 哪个版本, crate 信息, 文档, 依赖, Rust 版本, 新特性, 有什么特性"
|
||||
allowed-tools: ["Task", "Read", "Glob", "mcp__actionbook__*", "Bash"]
|
||||
---
|
||||
|
||||
# Rust Learner
|
||||
|
||||
> **Version:** 2.1.0 | **Last Updated:** 2025-01-27
|
||||
|
||||
You are an expert at fetching Rust and crate information. Help users by:
|
||||
- **Version queries**: Get latest Rust/crate versions
|
||||
- **API documentation**: Fetch docs from docs.rs
|
||||
- **Changelog**: Get Rust version features from releases.rs
|
||||
|
||||
**Primary skill for fetching Rust/crate information.**
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check agent file availability first to determine execution mode.**
|
||||
|
||||
Try to read the agent file for your query type. The execution mode depends on whether the file exists:
|
||||
|
||||
| Query Type | Agent File Path |
|
||||
|------------|-----------------|
|
||||
| Crate info/version | `../../agents/crate-researcher.md` |
|
||||
| Rust version features | `../../agents/rust-changelog.md` |
|
||||
| Std library docs | `../../agents/std-docs-researcher.md` |
|
||||
| Third-party crate docs | `../../agents/docs-researcher.md` |
|
||||
| Clippy lints | `../../agents/clippy-researcher.md` |
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install)
|
||||
|
||||
**When agent files exist at `../../agents/`:**
|
||||
|
||||
### Workflow
|
||||
|
||||
1. Read the appropriate agent file (relative to this skill)
|
||||
2. Launch Task with `run_in_background: true`
|
||||
3. Continue with other work or wait for completion
|
||||
4. Summarize results to user
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
run_in_background: true,
|
||||
prompt: <read from ../../agents/*.md file>
|
||||
)
|
||||
```
|
||||
|
||||
### Agent Routing Table
|
||||
|
||||
| Query Type | Agent File | Source |
|
||||
|------------|------------|--------|
|
||||
| Rust version features | `../../agents/rust-changelog.md` | releases.rs |
|
||||
| Crate info/version | `../../agents/crate-researcher.md` | lib.rs, crates.io |
|
||||
| **Std library docs** (Send, Sync, Arc, etc.) | `../../agents/std-docs-researcher.md` | doc.rust-lang.org |
|
||||
| Third-party crate docs (tokio, serde, etc.) | `../../agents/docs-researcher.md` | docs.rs |
|
||||
| Clippy lints | `../../agents/clippy-researcher.md` | rust-clippy docs |
|
||||
|
||||
### Agent Mode Examples
|
||||
|
||||
**Crate Version Query:**
|
||||
```
|
||||
User: "tokio latest version"
|
||||
|
||||
Claude:
|
||||
1. Read ../../agents/crate-researcher.md
|
||||
2. Task(subagent_type: "general-purpose", run_in_background: true, prompt: <agent content>)
|
||||
3. Wait for agent
|
||||
4. Summarize results
|
||||
```
|
||||
|
||||
**Rust Changelog Query:**
|
||||
```
|
||||
User: "What's new in Rust 1.85?"
|
||||
|
||||
Claude:
|
||||
1. Read ../../agents/rust-changelog.md
|
||||
2. Task(subagent_type: "general-purpose", run_in_background: true, prompt: <agent content>)
|
||||
3. Wait for agent
|
||||
4. Summarize features
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install)
|
||||
|
||||
**When agent files are NOT available, execute directly using these steps:**
|
||||
|
||||
### Crate Info Query
|
||||
|
||||
```
|
||||
1. actionbook: mcp__actionbook__search_actions("lib.rs crate info")
|
||||
2. Get action details: mcp__actionbook__get_action_by_id(<action_id>)
|
||||
3. agent-browser CLI (or WebFetch fallback):
|
||||
- open "https://lib.rs/crates/{crate_name}"
|
||||
- get text using selector from actionbook
|
||||
- close
|
||||
4. Parse and format output
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```markdown
|
||||
## {Crate Name}
|
||||
|
||||
**Version:** {latest}
|
||||
**Description:** {description}
|
||||
|
||||
**Features:**
|
||||
- `feature1`: description
|
||||
|
||||
**Links:**
|
||||
- [docs.rs](https://docs.rs/{crate}) | [crates.io](https://crates.io/crates/{crate}) | [repo]({repo_url})
|
||||
```
|
||||
|
||||
### Rust Version Query
|
||||
|
||||
```
|
||||
1. actionbook: mcp__actionbook__search_actions("releases.rs rust changelog")
|
||||
2. Get action details for selectors
|
||||
3. agent-browser CLI (or WebFetch fallback):
|
||||
- open "https://releases.rs/docs/1.{version}.0/"
|
||||
- get text using selector from actionbook
|
||||
- close
|
||||
4. Parse and format output
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```markdown
|
||||
## Rust 1.{version}
|
||||
|
||||
**Release Date:** {date}
|
||||
|
||||
### Language Features
|
||||
- Feature 1: description
|
||||
- Feature 2: description
|
||||
|
||||
### Library Changes
|
||||
- std::module: new API
|
||||
|
||||
### Stabilized APIs
|
||||
- `api_name`: description
|
||||
```
|
||||
|
||||
### Std Library Docs (std::*, Send, Sync, Arc, etc.)
|
||||
|
||||
```
|
||||
1. Construct URL: "https://doc.rust-lang.org/std/{path}/"
|
||||
- Traits: std/{module}/trait.{Name}.html
|
||||
- Structs: std/{module}/struct.{Name}.html
|
||||
- Modules: std/{module}/index.html
|
||||
2. agent-browser CLI (or WebFetch fallback):
|
||||
- open <url>
|
||||
- get text "main .docblock"
|
||||
- close
|
||||
3. Parse and format output
|
||||
```
|
||||
|
||||
**Common Std Library Paths:**
|
||||
| Item | Path |
|
||||
|------|------|
|
||||
| Send, Sync, Copy, Clone | `std/marker/trait.{Name}.html` |
|
||||
| Arc, Mutex, RwLock | `std/sync/struct.{Name}.html` |
|
||||
| Rc, Weak | `std/rc/struct.{Name}.html` |
|
||||
| RefCell, Cell | `std/cell/struct.{Name}.html` |
|
||||
| Box | `std/boxed/struct.Box.html` |
|
||||
| Vec | `std/vec/struct.Vec.html` |
|
||||
| String | `std/string/struct.String.html` |
|
||||
|
||||
**Output Format:**
|
||||
```markdown
|
||||
## std::{path}::{Name}
|
||||
|
||||
**Signature:**
|
||||
```rust
|
||||
{signature}
|
||||
```
|
||||
|
||||
**Description:**
|
||||
{description}
|
||||
|
||||
**Examples:**
|
||||
```rust
|
||||
{example_code}
|
||||
```
|
||||
```
|
||||
|
||||
### Third-Party Crate Docs (tokio, serde, etc.)
|
||||
|
||||
```
|
||||
1. Construct URL: "https://docs.rs/{crate}/latest/{crate}/{path}"
|
||||
2. agent-browser CLI (or WebFetch fallback):
|
||||
- open <url>
|
||||
- get text ".docblock"
|
||||
- close
|
||||
3. Parse and format output
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```markdown
|
||||
## {crate}::{path}
|
||||
|
||||
**Signature:**
|
||||
```rust
|
||||
{signature}
|
||||
```
|
||||
|
||||
**Description:**
|
||||
{description}
|
||||
|
||||
**Examples:**
|
||||
```rust
|
||||
{example_code}
|
||||
```
|
||||
```
|
||||
|
||||
### Clippy Lints
|
||||
|
||||
```
|
||||
1. agent-browser CLI (or WebFetch fallback):
|
||||
- open "https://rust-lang.github.io/rust-clippy/stable/"
|
||||
- search for lint name in page
|
||||
- get text ".lint-doc" for matching lint
|
||||
- close
|
||||
2. Parse and format output
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```markdown
|
||||
## Clippy Lint: {lint_name}
|
||||
|
||||
**Level:** {warn|deny|allow}
|
||||
**Category:** {category}
|
||||
|
||||
**Description:**
|
||||
{what_it_checks}
|
||||
|
||||
**Example (Bad):**
|
||||
```rust
|
||||
{bad_code}
|
||||
```
|
||||
|
||||
**Example (Good):**
|
||||
```rust
|
||||
{good_code}
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Chain Priority
|
||||
|
||||
Both modes use the same tool chain order:
|
||||
|
||||
1. **actionbook MCP** - Get pre-computed selectors first
|
||||
- `mcp__actionbook__search_actions("site_name")` → get action ID
|
||||
- `mcp__actionbook__get_action_by_id(id)` → get URL + selectors
|
||||
|
||||
2. **agent-browser CLI** - Primary execution tool
|
||||
```bash
|
||||
agent-browser open <url>
|
||||
agent-browser get text <selector_from_actionbook>
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
3. **WebFetch** - Last resort only if agent-browser unavailable
|
||||
|
||||
### Fallback Principle (CRITICAL)
|
||||
|
||||
```
|
||||
actionbook → agent-browser → WebFetch (only if agent-browser unavailable)
|
||||
```
|
||||
|
||||
**DO NOT:**
|
||||
- Skip agent-browser because it's slower
|
||||
- Use WebFetch as primary when agent-browser is available
|
||||
- Block on WebFetch without trying agent-browser first
|
||||
|
||||
---
|
||||
|
||||
## Deprecated Patterns
|
||||
|
||||
| Deprecated | Use Instead | Reason |
|
||||
|------------|-------------|--------|
|
||||
| WebSearch for crate info | Task + agent or inline mode | Structured data |
|
||||
| Direct WebFetch | actionbook + agent-browser | Pre-computed selectors |
|
||||
| Guessing version numbers | Always fetch from source | Prevents misinformation |
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Agent file not found | Skills-only install | Use inline mode |
|
||||
| actionbook unavailable | MCP not configured | Fall back to WebFetch |
|
||||
| agent-browser not found | CLI not installed | Fall back to WebFetch |
|
||||
| Agent timeout | Site slow/down | Retry or inform user |
|
||||
| Empty results | Selector mismatch | Report and use WebFetch fallback |
|
||||
|
||||
## Proactive Triggering
|
||||
|
||||
This skill triggers AUTOMATICALLY when:
|
||||
- Any Rust crate name mentioned (tokio, serde, axum, sqlx, etc.)
|
||||
- Questions about "latest", "new", "version", "changelog"
|
||||
- API documentation requests
|
||||
- Dependency/feature questions
|
||||
|
||||
**DO NOT use WebSearch for Rust crate info. Use agents or inline mode instead.**
|
||||
273
skills/rust-refactor-helper/SKILL.md
Normal file
273
skills/rust-refactor-helper/SKILL.md
Normal file
@@ -0,0 +1,273 @@
|
||||
---
|
||||
name: rust-refactor-helper
|
||||
description: "Safe Rust refactoring with LSP analysis. Triggers on: /refactor, rename symbol, move function, extract, 重构, 重命名, 提取函数, 安全重构"
|
||||
argument-hint: "<action> <target> [--dry-run]"
|
||||
allowed-tools: ["LSP", "Read", "Glob", "Grep", "Edit"]
|
||||
---
|
||||
|
||||
# Rust Refactor Helper
|
||||
|
||||
Perform safe refactoring with comprehensive impact analysis.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-refactor-helper <action> <target> [--dry-run]
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
- `rename <old> <new>` - Rename symbol
|
||||
- `extract-fn <selection>` - Extract to function
|
||||
- `inline <fn>` - Inline function
|
||||
- `move <symbol> <dest>` - Move to module
|
||||
|
||||
**Examples:**
|
||||
- `/rust-refactor-helper rename parse_config load_config`
|
||||
- `/rust-refactor-helper extract-fn src/main.rs:20-35`
|
||||
- `/rust-refactor-helper move UserService src/services/`
|
||||
|
||||
## LSP Operations Used
|
||||
|
||||
### Pre-Refactor Analysis
|
||||
|
||||
```
|
||||
# Find all references before renaming
|
||||
LSP(
|
||||
operation: "findReferences",
|
||||
filePath: "src/lib.rs",
|
||||
line: 25,
|
||||
character: 8
|
||||
)
|
||||
|
||||
# Get symbol info
|
||||
LSP(
|
||||
operation: "hover",
|
||||
filePath: "src/lib.rs",
|
||||
line: 25,
|
||||
character: 8
|
||||
)
|
||||
|
||||
# Check call hierarchy for move operations
|
||||
LSP(
|
||||
operation: "incomingCalls",
|
||||
filePath: "src/lib.rs",
|
||||
line: 25,
|
||||
character: 8
|
||||
)
|
||||
```
|
||||
|
||||
## Refactoring Workflows
|
||||
|
||||
### 1. Rename Symbol
|
||||
|
||||
```
|
||||
User: "Rename parse_config to load_config"
|
||||
│
|
||||
▼
|
||||
[1] Find symbol definition
|
||||
LSP(goToDefinition)
|
||||
│
|
||||
▼
|
||||
[2] Find ALL references
|
||||
LSP(findReferences)
|
||||
│
|
||||
▼
|
||||
[3] Categorize by file
|
||||
│
|
||||
▼
|
||||
[4] Check for conflicts
|
||||
- Is 'load_config' already used?
|
||||
- Are there macro-generated uses?
|
||||
│
|
||||
▼
|
||||
[5] Show impact analysis (--dry-run)
|
||||
│
|
||||
▼
|
||||
[6] Apply changes with Edit tool
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
## Rename: parse_config → load_config
|
||||
|
||||
### Impact Analysis
|
||||
|
||||
**Definition:** src/config.rs:25
|
||||
**References found:** 8
|
||||
|
||||
| File | Line | Context | Change |
|
||||
|------|------|---------|--------|
|
||||
| src/config.rs | 25 | `pub fn parse_config(` | Definition |
|
||||
| src/config.rs | 45 | `parse_config(path)?` | Call |
|
||||
| src/main.rs | 12 | `config::parse_config` | Import |
|
||||
| src/main.rs | 30 | `let cfg = parse_config(` | Call |
|
||||
| src/lib.rs | 8 | `pub use config::parse_config` | Re-export |
|
||||
| tests/config_test.rs | 15 | `parse_config("test.toml")` | Test |
|
||||
| tests/config_test.rs | 25 | `parse_config("")` | Test |
|
||||
| docs/api.md | 42 | `parse_config` | Documentation |
|
||||
|
||||
### Potential Issues
|
||||
|
||||
⚠️ **Documentation reference:** docs/api.md:42 may need manual update
|
||||
⚠️ **Re-export:** src/lib.rs:8 - public API change
|
||||
|
||||
### Proceed?
|
||||
- [x] --dry-run (preview only)
|
||||
- [ ] Apply changes
|
||||
```
|
||||
|
||||
### 2. Extract Function
|
||||
|
||||
```
|
||||
User: "Extract lines 20-35 in main.rs to a function"
|
||||
│
|
||||
▼
|
||||
[1] Read the selected code block
|
||||
│
|
||||
▼
|
||||
[2] Analyze variables
|
||||
- Which are inputs? (used but not defined in block)
|
||||
- Which are outputs? (defined and used after block)
|
||||
- Which are local? (defined and used only in block)
|
||||
│
|
||||
▼
|
||||
[3] Determine function signature
|
||||
│
|
||||
▼
|
||||
[4] Check for early returns, loops, etc.
|
||||
│
|
||||
▼
|
||||
[5] Generate extracted function
|
||||
│
|
||||
▼
|
||||
[6] Replace original code with call
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
## Extract Function: src/main.rs:20-35
|
||||
|
||||
### Selected Code
|
||||
```rust
|
||||
let file = File::open(&path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let config: Config = toml::from_str(&contents)?;
|
||||
validate_config(&config)?;
|
||||
```
|
||||
|
||||
### Analysis
|
||||
|
||||
**Inputs:** path: &Path
|
||||
**Outputs:** config: Config
|
||||
**Side Effects:** File I/O, may return error
|
||||
|
||||
### Extracted Function
|
||||
|
||||
```rust
|
||||
fn load_and_validate_config(path: &Path) -> Result<Config> {
|
||||
let file = File::open(path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let config: Config = toml::from_str(&contents)?;
|
||||
validate_config(&config)?;
|
||||
Ok(config)
|
||||
}
|
||||
```
|
||||
|
||||
### Replacement
|
||||
|
||||
```rust
|
||||
let config = load_and_validate_config(&path)?;
|
||||
```
|
||||
```
|
||||
|
||||
### 3. Move Symbol
|
||||
|
||||
```
|
||||
User: "Move UserService to src/services/"
|
||||
│
|
||||
▼
|
||||
[1] Find symbol and all its dependencies
|
||||
│
|
||||
▼
|
||||
[2] Find all references (callers)
|
||||
LSP(findReferences)
|
||||
│
|
||||
▼
|
||||
[3] Analyze import changes needed
|
||||
│
|
||||
▼
|
||||
[4] Check for circular dependencies
|
||||
│
|
||||
▼
|
||||
[5] Generate move plan
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
## Move: UserService → src/services/user.rs
|
||||
|
||||
### Current Location
|
||||
src/handlers/auth.rs:50-120
|
||||
|
||||
### Dependencies (will be moved together)
|
||||
- struct UserService (50-80)
|
||||
- impl UserService (82-120)
|
||||
- const DEFAULT_TIMEOUT (48)
|
||||
|
||||
### Import Changes Required
|
||||
|
||||
| File | Current | New |
|
||||
|------|---------|-----|
|
||||
| src/main.rs | `use handlers::auth::UserService` | `use services::user::UserService` |
|
||||
| src/handlers/api.rs | `use super::auth::UserService` | `use crate::services::user::UserService` |
|
||||
| tests/auth_test.rs | `use crate::handlers::auth::UserService` | `use crate::services::user::UserService` |
|
||||
|
||||
### New File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── services/
|
||||
│ ├── mod.rs (NEW - add `pub mod user;`)
|
||||
│ └── user.rs (NEW - UserService moved here)
|
||||
├── handlers/
|
||||
│ └── auth.rs (UserService removed)
|
||||
```
|
||||
|
||||
### Circular Dependency Check
|
||||
✅ No circular dependencies detected
|
||||
```
|
||||
|
||||
## Safety Checks
|
||||
|
||||
| Check | Purpose |
|
||||
|-------|---------|
|
||||
| Reference completeness | Ensure all uses are found |
|
||||
| Name conflicts | Detect existing symbols with same name |
|
||||
| Visibility changes | Warn if pub/private scope changes |
|
||||
| Macro-generated code | Warn about code in macros |
|
||||
| Documentation | Flag doc comments mentioning symbol |
|
||||
| Test coverage | Show affected tests |
|
||||
|
||||
## Dry Run Mode
|
||||
|
||||
Always use `--dry-run` first to preview changes:
|
||||
|
||||
```
|
||||
/rust-refactor-helper rename old_name new_name --dry-run
|
||||
```
|
||||
|
||||
This shows all changes without applying them.
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Navigate to symbol | rust-code-navigator |
|
||||
| Understand call flow | rust-call-graph |
|
||||
| Project structure | rust-symbol-analyzer |
|
||||
| Trait implementations | rust-trait-explorer |
|
||||
239
skills/rust-router/SKILL.md
Normal file
239
skills/rust-router/SKILL.md
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
name: rust-router
|
||||
description: "CRITICAL: Use for ALL Rust questions including errors, design, and coding.
|
||||
HIGHEST PRIORITY for: 比较, 对比, compare, vs, versus, 区别, difference, 最佳实践, best practice,
|
||||
tokio vs, async-std vs, 比较 tokio, 比较 async,
|
||||
Triggers on: Rust, cargo, rustc, crate, Cargo.toml,
|
||||
意图分析, 问题分析, 语义分析, analyze intent, question analysis,
|
||||
compile error, borrow error, lifetime error, ownership error, type error, trait error,
|
||||
value moved, cannot borrow, does not live long enough, mismatched types, not satisfied,
|
||||
E0382, E0597, E0277, E0308, E0499, E0502, E0596,
|
||||
async, await, Send, Sync, tokio, concurrency, error handling,
|
||||
编译错误, compile error, 所有权, ownership, 借用, borrow, 生命周期, lifetime, 类型错误, type error,
|
||||
异步, async, 并发, concurrency, 错误处理, error handling,
|
||||
问题, problem, question, 怎么用, how to use, 如何, how to, 为什么, why,
|
||||
什么是, what is, 帮我写, help me write, 实现, implement, 解释, explain"
|
||||
globs: ["**/Cargo.toml", "**/*.rs"]
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# Rust Question Router
|
||||
|
||||
> **Version:** 2.0.0 | **Last Updated:** 2025-01-22
|
||||
>
|
||||
> **v2.0:** Context optimized - detailed examples moved to sub-files
|
||||
|
||||
## Meta-Cognition Framework
|
||||
|
||||
### Core Principle
|
||||
|
||||
**Don't answer directly. Trace through the cognitive layers first.**
|
||||
|
||||
```
|
||||
Layer 3: Domain Constraints (WHY)
|
||||
├── Business rules, regulatory requirements
|
||||
├── domain-fintech, domain-web, domain-cli, etc.
|
||||
└── "Why is it designed this way?"
|
||||
|
||||
Layer 2: Design Choices (WHAT)
|
||||
├── Architecture patterns, DDD concepts
|
||||
├── m09-m15 skills
|
||||
└── "What pattern should I use?"
|
||||
|
||||
Layer 1: Language Mechanics (HOW)
|
||||
├── Ownership, borrowing, lifetimes, traits
|
||||
├── m01-m07 skills
|
||||
└── "How do I implement this in Rust?"
|
||||
```
|
||||
|
||||
### Routing by Entry Point
|
||||
|
||||
| User Signal | Entry Layer | Direction | First Skill |
|
||||
|-------------|-------------|-----------|-------------|
|
||||
| E0xxx error | Layer 1 | Trace UP ↑ | m01-m07 |
|
||||
| Compile error | Layer 1 | Trace UP ↑ | Error table below |
|
||||
| "How to design..." | Layer 2 | Check L3, then DOWN ↓ | m09-domain |
|
||||
| "Building [domain] app" | Layer 3 | Trace DOWN ↓ | domain-* |
|
||||
| "Best practice..." | Layer 2 | Both directions | m09-m15 |
|
||||
| Performance issue | Layer 1 → 2 | UP then DOWN | m10-performance |
|
||||
|
||||
### CRITICAL: Dual-Skill Loading
|
||||
|
||||
**When domain keywords are present, you MUST load BOTH skills:**
|
||||
|
||||
| Domain Keywords | L1 Skill | L3 Skill |
|
||||
|-----------------|----------|----------|
|
||||
| Web API, HTTP, axum, handler | m07-concurrency | **domain-web** |
|
||||
| 交易, 支付, trading, payment | m01-ownership | **domain-fintech** |
|
||||
| CLI, terminal, clap | m07-concurrency | **domain-cli** |
|
||||
| kubernetes, grpc, microservice | m07-concurrency | **domain-cloud-native** |
|
||||
| embedded, no_std, MCU | m02-resource | **domain-embedded** |
|
||||
|
||||
---
|
||||
|
||||
## INSTRUCTIONS FOR CLAUDE
|
||||
|
||||
### CRITICAL: Negotiation Protocol Trigger
|
||||
|
||||
**BEFORE answering, check if negotiation is required:**
|
||||
|
||||
| Query Contains | Action |
|
||||
|----------------|--------|
|
||||
| "比较", "对比", "compare", "vs", "versus" | **MUST use negotiation** |
|
||||
| "最佳实践", "best practice" | **MUST use negotiation** |
|
||||
| Domain + error (e.g., "交易系统 E0382") | **MUST use negotiation** |
|
||||
| Ambiguous scope (e.g., "tokio 性能") | **SHOULD use negotiation** |
|
||||
|
||||
**When negotiation is required, include:**
|
||||
|
||||
```markdown
|
||||
## Negotiation Analysis
|
||||
|
||||
**Query Type:** [Comparative | Cross-domain | Synthesis | Ambiguous]
|
||||
**Negotiation:** Enabled
|
||||
|
||||
### Source: [Agent/Skill Name]
|
||||
**Confidence:** HIGH | MEDIUM | LOW | UNCERTAIN
|
||||
**Gaps:** [What's missing]
|
||||
|
||||
## Synthesized Answer
|
||||
[Answer]
|
||||
|
||||
**Overall Confidence:** [Level]
|
||||
**Disclosed Gaps:** [Gaps user should know]
|
||||
```
|
||||
|
||||
> **详细协议见:** `patterns/negotiation.md`
|
||||
|
||||
---
|
||||
|
||||
### Default Project Settings
|
||||
|
||||
When creating new Rust projects or Cargo.toml files, ALWAYS use:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
edition = "2024" # ALWAYS use latest stable edition
|
||||
rust-version = "1.85"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
all = "warn"
|
||||
pedantic = "warn"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer 1 Skills (Language Mechanics)
|
||||
|
||||
| Pattern | Route To |
|
||||
|---------|----------|
|
||||
| move, borrow, lifetime, E0382, E0597 | m01-ownership |
|
||||
| Box, Rc, Arc, RefCell, Cell | m02-resource |
|
||||
| mut, interior mutability, E0499, E0502, E0596 | m03-mutability |
|
||||
| generic, trait, inline, monomorphization | m04-zero-cost |
|
||||
| type state, phantom, newtype | m05-type-driven |
|
||||
| Result, Error, panic, ?, anyhow, thiserror | m06-error-handling |
|
||||
| Send, Sync, thread, async, channel | m07-concurrency |
|
||||
| unsafe, FFI, extern, raw pointer, transmute | **unsafe-checker** |
|
||||
|
||||
## Layer 2 Skills (Design Choices)
|
||||
|
||||
| Pattern | Route To |
|
||||
|---------|----------|
|
||||
| domain model, business logic | m09-domain |
|
||||
| performance, optimization, benchmark | m10-performance |
|
||||
| integration, interop, bindings | m11-ecosystem |
|
||||
| resource lifecycle, RAII, Drop | m12-lifecycle |
|
||||
| domain error, recovery strategy | m13-domain-error |
|
||||
| mental model, how to think | m14-mental-model |
|
||||
| anti-pattern, common mistake, pitfall | m15-anti-pattern |
|
||||
|
||||
## Layer 3 Skills (Domain Constraints)
|
||||
|
||||
| Domain Keywords | Route To |
|
||||
|-----------------|----------|
|
||||
| fintech, trading, decimal, currency | domain-fintech |
|
||||
| ml, tensor, model, inference | domain-ml |
|
||||
| kubernetes, docker, grpc, microservice | domain-cloud-native |
|
||||
| embedded, sensor, mqtt, iot | domain-iot |
|
||||
| web server, HTTP, REST, axum, actix | domain-web |
|
||||
| CLI, command line, clap, terminal | domain-cli |
|
||||
| no_std, microcontroller, firmware | domain-embedded |
|
||||
|
||||
---
|
||||
|
||||
## Error Code Routing
|
||||
|
||||
| Error Code | Route To | Common Cause |
|
||||
|------------|----------|--------------|
|
||||
| E0382 | m01-ownership | Use of moved value |
|
||||
| E0597 | m01-ownership | Lifetime too short |
|
||||
| E0506 | m01-ownership | Cannot assign to borrowed |
|
||||
| E0507 | m01-ownership | Cannot move out of borrowed |
|
||||
| E0515 | m01-ownership | Return local reference |
|
||||
| E0716 | m01-ownership | Temporary value dropped |
|
||||
| E0106 | m01-ownership | Missing lifetime specifier |
|
||||
| E0596 | m03-mutability | Cannot borrow as mutable |
|
||||
| E0499 | m03-mutability | Multiple mutable borrows |
|
||||
| E0502 | m03-mutability | Borrow conflict |
|
||||
| E0277 | m04/m07 | Trait bound not satisfied |
|
||||
| E0308 | m04-zero-cost | Type mismatch |
|
||||
| E0599 | m04-zero-cost | No method found |
|
||||
| E0038 | m04-zero-cost | Trait not object-safe |
|
||||
| E0433 | m11-ecosystem | Cannot find crate/module |
|
||||
|
||||
---
|
||||
|
||||
## Functional Routing Table
|
||||
|
||||
| Pattern | Route To | Action |
|
||||
|---------|----------|--------|
|
||||
| latest version, what's new | **rust-learner** | Use agents |
|
||||
| API, docs, documentation | **docs-researcher** | Use agent |
|
||||
| code style, naming, clippy | **coding-guidelines** | Read skill |
|
||||
| unsafe code, FFI | **unsafe-checker** | Read skill |
|
||||
| code review | **os-checker** | See `integrations/os-checker.md` |
|
||||
|
||||
---
|
||||
|
||||
## Priority Order
|
||||
|
||||
1. **Identify cognitive layer** (L1/L2/L3)
|
||||
2. **Load entry skill** (m0x/m1x/domain)
|
||||
3. **Trace through layers** (UP or DOWN)
|
||||
4. **Cross-reference skills** as indicated in "Trace" sections
|
||||
5. **Answer with reasoning chain**
|
||||
|
||||
### Keyword Conflict Resolution
|
||||
|
||||
| Keyword | Resolution |
|
||||
|---------|------------|
|
||||
| `unsafe` | **unsafe-checker** (more specific than m11) |
|
||||
| `error` | **m06** for general, **m13** for domain-specific |
|
||||
| `RAII` | **m12** for design, **m01** for implementation |
|
||||
| `crate` | **rust-learner** for version, **m11** for integration |
|
||||
| `tokio` | **tokio-*** for API, **m07** for concepts |
|
||||
|
||||
**Priority Hierarchy:**
|
||||
|
||||
```
|
||||
1. Error codes (E0xxx) → Direct lookup, highest priority
|
||||
2. Negotiation triggers (compare, vs, best practice) → Enable negotiation
|
||||
3. Domain keywords + error → Load BOTH domain + error skills
|
||||
4. Specific crate keywords → Route to crate-specific skill if exists
|
||||
5. General concept keywords → Route to meta-question skill
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sub-Files Reference
|
||||
|
||||
| File | Content |
|
||||
|------|---------|
|
||||
| `patterns/negotiation.md` | Negotiation protocol details |
|
||||
| `examples/workflow.md` | Workflow examples |
|
||||
| `integrations/os-checker.md` | OS-Checker integration |
|
||||
85
skills/rust-router/examples/workflow.md
Normal file
85
skills/rust-router/examples/workflow.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Workflow Examples
|
||||
|
||||
> rust-router 的工作流程示例
|
||||
|
||||
## Example 1: Error Code with Domain Context
|
||||
|
||||
```
|
||||
User: "Why am I getting E0382 in my trading system?"
|
||||
|
||||
Analysis:
|
||||
1. Entry: Layer 1 (E0382 = ownership/move error)
|
||||
2. Load: m01-ownership skill
|
||||
3. Context: "trading system" → domain-fintech
|
||||
|
||||
Trace UP ↑:
|
||||
- E0382 in trading context
|
||||
- Check domain-fintech: "immutable audit records"
|
||||
- Finding: Trading data should be shared, not moved
|
||||
|
||||
Response:
|
||||
"E0382 indicates a value was moved when still needed.
|
||||
In a trading system (domain-fintech), transaction records
|
||||
should be immutable and shareable for audit purposes.
|
||||
|
||||
Instead of cloning, consider:
|
||||
- Arc<TradeRecord> for shared immutable access
|
||||
- This aligns with financial audit requirements
|
||||
|
||||
See: m01-ownership (Trace Up section),
|
||||
domain-fintech (Audit Requirements)"
|
||||
```
|
||||
|
||||
## Example 2: Design Question
|
||||
|
||||
```
|
||||
User: "How should I handle user authentication?"
|
||||
|
||||
1. Entry: Layer 2 (design question)
|
||||
2. Trace UP to Layer 3: domain-web constraints
|
||||
3. Load: domain-web skill (security, stateless HTTP)
|
||||
4. Trace DOWN: m06-error-handling, m07-concurrency
|
||||
5. Answer: JWT with proper error types, async handlers
|
||||
```
|
||||
|
||||
## Example 3: Comparative Query
|
||||
|
||||
```
|
||||
User: "Compare tokio and async-std"
|
||||
|
||||
1. Detect: "compare" → Enable negotiation
|
||||
2. Load both runtime knowledge sources
|
||||
3. Assess confidence for each
|
||||
4. Synthesize with disclosed gaps
|
||||
5. Answer: Structured comparison table
|
||||
```
|
||||
|
||||
## Example 4: Multi-Layer Trace
|
||||
|
||||
```
|
||||
User: "My web API reports Rc cannot be sent between threads"
|
||||
|
||||
1. Entry: Layer 1 (Send/Sync error)
|
||||
2. Load: m07-concurrency
|
||||
3. Detect: "web API" → domain-web
|
||||
4. Dual-skill loading:
|
||||
- m07: Explain Send/Sync bounds
|
||||
- domain-web: Web state management patterns
|
||||
5. Answer: Use Arc instead of Rc, or move to thread-local
|
||||
```
|
||||
|
||||
## Example 5: Intent Analysis Request
|
||||
|
||||
```
|
||||
User: "Analyze this question: How do I share state in actix-web?"
|
||||
|
||||
Analysis Steps:
|
||||
1. Extract Keywords: share, state, actix-web
|
||||
2. Identify Entry Layer: Layer 1 (sharing = concurrency) + Layer 3 (actix-web = web)
|
||||
3. Map to Skills: m07-concurrency, domain-web
|
||||
4. Report:
|
||||
- Layer 1: Concurrency (state sharing mechanisms)
|
||||
- Layer 3: Web domain (HTTP handler patterns)
|
||||
- Suggested trace: L1 → L3
|
||||
5. Invoke: m07-concurrency first, then domain-web
|
||||
```
|
||||
56
skills/rust-router/integrations/os-checker.md
Normal file
56
skills/rust-router/integrations/os-checker.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# OS-Checker Integration
|
||||
|
||||
> 代码审查和安全审计工具集成
|
||||
|
||||
## Available Commands
|
||||
|
||||
| Use Case | Command | Tools |
|
||||
|----------|---------|-------|
|
||||
| Daily check | `/rust-review` | clippy |
|
||||
| Security audit | `/audit security` | cargo audit, geiger |
|
||||
| Unsafe audit | `/audit safety` | miri, rudra |
|
||||
| Concurrency audit | `/audit concurrency` | lockbud |
|
||||
| Full audit | `/audit full` | all os-checker tools |
|
||||
|
||||
## When to Suggest OS-Checker
|
||||
|
||||
| User Intent | Suggest |
|
||||
|-------------|---------|
|
||||
| Code review request | `/rust-review` |
|
||||
| Security concerns | `/audit security` |
|
||||
| Unsafe code review | `/audit safety` |
|
||||
| Deadlock/race concerns | `/audit concurrency` |
|
||||
| Pre-release check | `/audit full` |
|
||||
|
||||
## Tool Descriptions
|
||||
|
||||
### clippy
|
||||
Standard Rust linter for code style and common mistakes.
|
||||
|
||||
### cargo audit
|
||||
Security vulnerability scanner for dependencies.
|
||||
|
||||
### geiger
|
||||
Counts unsafe code usage in dependencies.
|
||||
|
||||
### miri
|
||||
Interprets MIR to detect undefined behavior.
|
||||
|
||||
### rudra
|
||||
Memory safety bug detector.
|
||||
|
||||
### lockbud
|
||||
Deadlock and concurrency bug detector.
|
||||
|
||||
## Integration Flow
|
||||
|
||||
```
|
||||
User: "Review my unsafe code"
|
||||
│
|
||||
▼
|
||||
Router detects: unsafe + review
|
||||
│
|
||||
├── Load: unsafe-checker skill (for manual review)
|
||||
│
|
||||
└── Suggest: `/audit safety` (for automated check)
|
||||
```
|
||||
154
skills/rust-router/patterns/negotiation.md
Normal file
154
skills/rust-router/patterns/negotiation.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# Negotiation Protocol
|
||||
|
||||
> 比较查询和跨领域问题的处理协议
|
||||
|
||||
## When to Enable Negotiation
|
||||
|
||||
For complex queries requiring structured agent responses, enable negotiation mode.
|
||||
|
||||
| Query Pattern | Enable Negotiation | Reason |
|
||||
|---------------|-------------------|--------|
|
||||
| Single error code lookup | No | Direct answer |
|
||||
| Single crate version | No | Direct lookup |
|
||||
| "Compare X and Y" | **Yes** | Multi-faceted |
|
||||
| Domain + error | **Yes** | Cross-layer context |
|
||||
| "Best practices for..." | **Yes** | Requires synthesis |
|
||||
| Ambiguous scope | **Yes** | Needs clarification |
|
||||
| Multi-crate question | **Yes** | Multiple sources |
|
||||
|
||||
## Negotiation Decision Flow
|
||||
|
||||
```
|
||||
Query Received
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ Is query single-lookup? │
|
||||
│ (version, error code, def) │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
├── Yes → Direct dispatch (no negotiation)
|
||||
│
|
||||
▼ No
|
||||
┌─────────────────────────────┐
|
||||
│ Does query require: │
|
||||
│ - Comparison? │
|
||||
│ - Cross-domain context? │
|
||||
│ - Synthesis/aggregation? │
|
||||
│ - Multiple sources? │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
├── Yes → Dispatch with negotiation: true
|
||||
│
|
||||
▼ No
|
||||
┌─────────────────────────────┐
|
||||
│ Is scope ambiguous? │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
├── Yes → Dispatch with negotiation: true
|
||||
│
|
||||
▼ No
|
||||
└── Direct dispatch (no negotiation)
|
||||
```
|
||||
|
||||
## Negotiation Dispatch
|
||||
|
||||
When dispatching with negotiation:
|
||||
|
||||
```
|
||||
1. Set `negotiation: true`
|
||||
2. Include original query context
|
||||
3. Expect structured response:
|
||||
- Findings
|
||||
- Confidence (HIGH/MEDIUM/LOW/UNCERTAIN)
|
||||
- Gaps identified
|
||||
- Context questions (if any)
|
||||
4. Evaluate response against original intent
|
||||
```
|
||||
|
||||
## Orchestrator Evaluation
|
||||
|
||||
After receiving negotiation response:
|
||||
|
||||
| Confidence | Intent Coverage | Action |
|
||||
|------------|-----------------|--------|
|
||||
| HIGH | Complete | Synthesize answer |
|
||||
| HIGH | Partial | May need supplementary query |
|
||||
| MEDIUM | Complete | Accept with disclosed gaps |
|
||||
| MEDIUM | Partial | Refine with context |
|
||||
| LOW | Any | Refine or try alternative |
|
||||
| UNCERTAIN | Any | Try alternative or escalate |
|
||||
|
||||
## Refinement Loop
|
||||
|
||||
If response insufficient:
|
||||
|
||||
```
|
||||
Round 1: Initial query
|
||||
│
|
||||
▼ (LOW confidence or gaps block intent)
|
||||
Round 2: Refined query with:
|
||||
- Answers to agent's context questions
|
||||
- Narrowed scope
|
||||
│
|
||||
▼ (still insufficient)
|
||||
Round 3: Final attempt with:
|
||||
- Alternative agent/source
|
||||
- Maximum context provided
|
||||
│
|
||||
▼ (still insufficient)
|
||||
Synthesize best-effort answer with disclosed gaps
|
||||
```
|
||||
|
||||
## Integration with 3-Strike Rule
|
||||
|
||||
Negotiation follows the 3-Strike escalation:
|
||||
|
||||
```
|
||||
Strike 1: Initial query returns LOW confidence
|
||||
→ Refine with more context
|
||||
|
||||
Strike 2: Refined query still LOW
|
||||
→ Try alternative agent/source
|
||||
|
||||
Strike 3: Still insufficient
|
||||
→ Synthesize best-effort answer
|
||||
→ Report gaps to user explicitly
|
||||
```
|
||||
|
||||
See `_meta/error-protocol.md` for full escalation rules.
|
||||
|
||||
## Negotiation Routing Examples
|
||||
|
||||
**Example 1: No Negotiation Needed**
|
||||
```
|
||||
Query: "What is tokio's latest version?"
|
||||
Analysis: Single lookup
|
||||
Action: Direct dispatch to crate-researcher
|
||||
```
|
||||
|
||||
**Example 2: Negotiation Required**
|
||||
```
|
||||
Query: "Compare tokio and async-std for a web server"
|
||||
Analysis: Comparative + domain context
|
||||
Action: Dispatch with negotiation: true
|
||||
Expected: Structured responses from both runtime lookups
|
||||
Evaluation: Check if web-server specific data found
|
||||
```
|
||||
|
||||
**Example 3: Cross-Domain Negotiation**
|
||||
```
|
||||
Query: "E0382 in my trading system"
|
||||
Analysis: Error code + domain context
|
||||
Action:
|
||||
- Dispatch m01-ownership (standard - error is defined)
|
||||
- Dispatch domain-fintech (negotiation: true - domain context)
|
||||
Synthesis: Combine error explanation with domain-appropriate fix
|
||||
```
|
||||
|
||||
## Related Documents
|
||||
|
||||
- `_meta/negotiation-protocol.md` - Full protocol specification
|
||||
- `_meta/negotiation-templates.md` - Response templates
|
||||
- `_meta/error-protocol.md` - 3-Strike escalation
|
||||
- `agents/_negotiation/response-format.md` - Agent response format
|
||||
265
skills/rust-skill-creator/SKILL.md
Normal file
265
skills/rust-skill-creator/SKILL.md
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
name: rust-skill-creator
|
||||
description: "Use when creating skills for Rust crates or std library documentation. Keywords: create rust skill, create crate skill, create std skill, 创建 rust skill, 创建 crate skill, 创建 std skill, 动态 rust skill, 动态 crate skill, skill for tokio, skill for serde, skill for axum, generate rust skill, rust 技能, crate 技能, 从文档创建skill, from docs create skill"
|
||||
argument-hint: "<crate_name|std::module>"
|
||||
context: fork
|
||||
agent: general-purpose
|
||||
---
|
||||
|
||||
# Rust Skill Creator
|
||||
|
||||
> **Version:** 2.1.0 | **Last Updated:** 2025-01-27
|
||||
>
|
||||
> Create dynamic skills for Rust crates and std library documentation.
|
||||
|
||||
## When to Use
|
||||
|
||||
This skill handles requests to create skills for:
|
||||
- Third-party crates (tokio, serde, axum, etc.)
|
||||
- Rust standard library (std::sync, std::marker, etc.)
|
||||
- Any Rust documentation URL
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check if related commands/skills are available.**
|
||||
|
||||
This skill relies on:
|
||||
- `/create-llms-for-skills` command
|
||||
- `/create-skills-via-llms` command
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install)
|
||||
|
||||
**When the commands above are available (full plugin installation):**
|
||||
|
||||
### Workflow
|
||||
|
||||
#### 1. Identify the Target
|
||||
|
||||
| User Request | Target Type | URL Pattern |
|
||||
|--------------|-------------|-------------|
|
||||
| "create tokio skill" | Third-party crate | `docs.rs/tokio/latest/tokio/` |
|
||||
| "create Send trait skill" | Std library | `doc.rust-lang.org/std/marker/trait.Send.html` |
|
||||
| "create skill from URL" + URL | Custom URL | User-provided URL |
|
||||
|
||||
#### 2. Execute the Command
|
||||
|
||||
Use the `/create-llms-for-skills` command:
|
||||
|
||||
```
|
||||
/create-llms-for-skills <url> [requirements]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# For third-party crate
|
||||
/create-llms-for-skills https://docs.rs/tokio/latest/tokio/
|
||||
|
||||
# For std library
|
||||
/create-llms-for-skills https://doc.rust-lang.org/std/marker/trait.Send.html
|
||||
|
||||
# With specific requirements
|
||||
/create-llms-for-skills https://docs.rs/axum/latest/axum/ "Focus on routing and extractors"
|
||||
```
|
||||
|
||||
#### 3. Follow-up with Skill Creation
|
||||
|
||||
After llms.txt is generated, use:
|
||||
|
||||
```
|
||||
/create-skills-via-llms <crate_name> <llms_path> [version]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install)
|
||||
|
||||
**When the commands above are NOT available, create skills manually:**
|
||||
|
||||
### Step 1: Identify Target and Construct URL
|
||||
|
||||
| Target | URL Template |
|
||||
|--------|--------------|
|
||||
| Crate overview | `https://docs.rs/{crate}/latest/{crate}/` |
|
||||
| Crate module | `https://docs.rs/{crate}/latest/{crate}/{module}/` |
|
||||
| Std trait | `https://doc.rust-lang.org/std/{module}/trait.{Name}.html` |
|
||||
| Std struct | `https://doc.rust-lang.org/std/{module}/struct.{Name}.html` |
|
||||
| Std module | `https://doc.rust-lang.org/std/{module}/index.html` |
|
||||
|
||||
### Step 2: Fetch Documentation
|
||||
|
||||
```bash
|
||||
# Using agent-browser CLI
|
||||
agent-browser open "<documentation_url>"
|
||||
agent-browser get text ".docblock"
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Or with WebFetch fallback:**
|
||||
```
|
||||
WebFetch("<documentation_url>", "Extract API documentation including types, functions, and examples")
|
||||
```
|
||||
|
||||
### Step 3: Create Skill Directory
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.claude/skills/{crate_name}
|
||||
mkdir -p ~/.claude/skills/{crate_name}/references
|
||||
```
|
||||
|
||||
### Step 4: Generate SKILL.md
|
||||
|
||||
Create `~/.claude/skills/{crate_name}/SKILL.md` with this template:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {crate_name}
|
||||
description: "Documentation for {crate_name} crate. Keywords: {keywords}"
|
||||
---
|
||||
|
||||
# {Crate Name}
|
||||
|
||||
> **Version:** {version} | **Source:** docs.rs
|
||||
|
||||
## Overview
|
||||
|
||||
{Brief description from documentation}
|
||||
|
||||
## Key Types
|
||||
|
||||
### {Type1}
|
||||
{Description and usage}
|
||||
|
||||
### {Type2}
|
||||
{Description and usage}
|
||||
|
||||
## Common Patterns
|
||||
|
||||
{Usage patterns extracted from documentation}
|
||||
|
||||
## Examples
|
||||
|
||||
```rust
|
||||
{Example code from documentation}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- `./references/overview.md` - Main overview
|
||||
- `./references/{module}.md` - Module documentation
|
||||
|
||||
## Links
|
||||
|
||||
- [docs.rs](https://docs.rs/{crate})
|
||||
- [crates.io](https://crates.io/crates/{crate})
|
||||
```
|
||||
|
||||
### Step 5: Generate Reference Files
|
||||
|
||||
For each major module or type, create a reference file:
|
||||
|
||||
```bash
|
||||
# Fetch and save module documentation
|
||||
agent-browser open "https://docs.rs/{crate}/latest/{crate}/{module}/"
|
||||
agent-browser get text ".docblock" > ~/.claude/skills/{crate_name}/references/{module}.md
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
### Step 6: Verify Skill
|
||||
|
||||
```bash
|
||||
# Check skill structure
|
||||
ls -la ~/.claude/skills/{crate_name}/
|
||||
cat ~/.claude/skills/{crate_name}/SKILL.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## URL Construction Helper
|
||||
|
||||
| Target | URL Template |
|
||||
|--------|--------------|
|
||||
| Crate overview | `https://docs.rs/{crate}/latest/{crate}/` |
|
||||
| Crate module | `https://docs.rs/{crate}/latest/{crate}/{module}/` |
|
||||
| Std trait | `https://doc.rust-lang.org/std/{module}/trait.{Name}.html` |
|
||||
| Std struct | `https://doc.rust-lang.org/std/{module}/struct.{Name}.html` |
|
||||
| Std module | `https://doc.rust-lang.org/std/{module}/index.html` |
|
||||
|
||||
## Common Std Library Paths
|
||||
|
||||
| Item | Path |
|
||||
|------|------|
|
||||
| Send, Sync, Copy, Clone | `std/marker/trait.{Name}.html` |
|
||||
| Arc, Mutex, RwLock | `std/sync/struct.{Name}.html` |
|
||||
| Rc, Weak | `std/rc/struct.{Name}.html` |
|
||||
| RefCell, Cell | `std/cell/struct.{Name}.html` |
|
||||
| Box | `std/boxed/struct.Box.html` |
|
||||
| Vec | `std/vec/struct.Vec.html` |
|
||||
| String | `std/string/struct.String.html` |
|
||||
| Option | `std/option/enum.Option.html` |
|
||||
| Result | `std/result/enum.Result.html` |
|
||||
|
||||
---
|
||||
|
||||
## Example Interactions
|
||||
|
||||
### Example 1: Create Crate Skill (Agent Mode)
|
||||
|
||||
```
|
||||
User: "Create a dynamic skill for tokio"
|
||||
|
||||
Claude:
|
||||
1. Identify: Third-party crate "tokio"
|
||||
2. Execute: /create-llms-for-skills https://docs.rs/tokio/latest/tokio/
|
||||
3. Wait for llms.txt generation
|
||||
4. Execute: /create-skills-via-llms tokio ~/tmp/{timestamp}-tokio-llms.txt
|
||||
```
|
||||
|
||||
### Example 2: Create Crate Skill (Inline Mode)
|
||||
|
||||
```
|
||||
User: "Create a dynamic skill for tokio"
|
||||
|
||||
Claude:
|
||||
1. Identify: Third-party crate "tokio"
|
||||
2. Fetch: agent-browser open "https://docs.rs/tokio/latest/tokio/"
|
||||
3. Extract documentation
|
||||
4. Create: ~/.claude/skills/tokio/SKILL.md
|
||||
5. Create: ~/.claude/skills/tokio/references/
|
||||
6. Save reference files for key modules (sync, task, runtime, etc.)
|
||||
```
|
||||
|
||||
### Example 3: Create Std Library Skill
|
||||
|
||||
```
|
||||
User: "Create a skill for Send and Sync traits"
|
||||
|
||||
Claude:
|
||||
1. Identify: Std library traits
|
||||
2. (Agent Mode) Execute: /create-llms-for-skills https://doc.rust-lang.org/std/marker/trait.Send.html https://doc.rust-lang.org/std/marker/trait.Sync.html
|
||||
(Inline Mode) Fetch each URL, create skill manually
|
||||
3. Complete skill creation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DO NOT
|
||||
|
||||
- Use `best-skill-creator` for Rust-related skill creation
|
||||
- Guess documentation URLs without verification
|
||||
- Skip documentation fetching step
|
||||
|
||||
## Output Location
|
||||
|
||||
All generated skills are saved to: `~/.claude/skills/`
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Commands not found | Skills-only install | Use inline mode |
|
||||
| URL not found | Invalid crate/module | Verify crate exists on crates.io |
|
||||
| Empty documentation | API changed | Use alternative selectors |
|
||||
| Permission denied | Directory issue | Check ~/.claude/skills/ permissions |
|
||||
222
skills/rust-symbol-analyzer/SKILL.md
Normal file
222
skills/rust-symbol-analyzer/SKILL.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
name: rust-symbol-analyzer
|
||||
description: "Analyze Rust project structure using LSP symbols. Triggers on: /symbols, project structure, list structs, list traits, list functions, 符号分析, 项目结构, 列出所有, 有哪些struct"
|
||||
argument-hint: "[file.rs] [--type struct|trait|fn|mod]"
|
||||
allowed-tools: ["LSP", "Read", "Glob"]
|
||||
---
|
||||
|
||||
# Rust Symbol Analyzer
|
||||
|
||||
Analyze project structure by examining symbols across your Rust codebase.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-symbol-analyzer [file.rs] [--type struct|trait|fn|mod]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- `/rust-symbol-analyzer` - Analyze entire project
|
||||
- `/rust-symbol-analyzer src/lib.rs` - Analyze single file
|
||||
- `/rust-symbol-analyzer --type trait` - List all traits in project
|
||||
|
||||
## LSP Operations
|
||||
|
||||
### 1. Document Symbols (Single File)
|
||||
|
||||
Get all symbols in a file with their hierarchy.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "documentSymbol",
|
||||
filePath: "src/lib.rs",
|
||||
line: 1,
|
||||
character: 1
|
||||
)
|
||||
```
|
||||
|
||||
**Returns:** Nested structure of modules, structs, functions, etc.
|
||||
|
||||
### 2. Workspace Symbols (Entire Project)
|
||||
|
||||
Search for symbols across the workspace.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "workspaceSymbol",
|
||||
filePath: "src/lib.rs",
|
||||
line: 1,
|
||||
character: 1
|
||||
)
|
||||
```
|
||||
|
||||
**Note:** Query is implicit in the operation context.
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
User: "What's the structure of this project?"
|
||||
│
|
||||
▼
|
||||
[1] Find all Rust files
|
||||
Glob("**/*.rs")
|
||||
│
|
||||
▼
|
||||
[2] Get symbols from each key file
|
||||
LSP(documentSymbol) for lib.rs, main.rs
|
||||
│
|
||||
▼
|
||||
[3] Categorize by type
|
||||
│
|
||||
▼
|
||||
[4] Generate structure visualization
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
### Project Overview
|
||||
|
||||
```
|
||||
## Project Structure: my-project
|
||||
|
||||
### Modules
|
||||
├── src/
|
||||
│ ├── lib.rs (root)
|
||||
│ ├── config/
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── parser.rs
|
||||
│ ├── handlers/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── auth.rs
|
||||
│ │ └── api.rs
|
||||
│ └── models/
|
||||
│ ├── mod.rs
|
||||
│ ├── user.rs
|
||||
│ └── order.rs
|
||||
└── tests/
|
||||
└── integration.rs
|
||||
```
|
||||
|
||||
### By Symbol Type
|
||||
|
||||
```
|
||||
## Symbols by Type
|
||||
|
||||
### Structs (12)
|
||||
| Name | Location | Fields | Derives |
|
||||
|------|----------|--------|---------|
|
||||
| Config | src/config.rs:10 | 5 | Debug, Clone |
|
||||
| User | src/models/user.rs:8 | 4 | Debug, Serialize |
|
||||
| Order | src/models/order.rs:15 | 6 | Debug, Serialize |
|
||||
| ... | | | |
|
||||
|
||||
### Traits (4)
|
||||
| Name | Location | Methods | Implementors |
|
||||
|------|----------|---------|--------------|
|
||||
| Handler | src/handlers/mod.rs:5 | 3 | AuthHandler, ApiHandler |
|
||||
| Repository | src/db/mod.rs:12 | 5 | UserRepo, OrderRepo |
|
||||
| ... | | | |
|
||||
|
||||
### Functions (25)
|
||||
| Name | Location | Visibility | Async |
|
||||
|------|----------|------------|-------|
|
||||
| main | src/main.rs:10 | pub | yes |
|
||||
| parse_config | src/config.rs:45 | pub | no |
|
||||
| ... | | | |
|
||||
|
||||
### Enums (6)
|
||||
| Name | Location | Variants |
|
||||
|------|----------|----------|
|
||||
| Error | src/error.rs:5 | 8 |
|
||||
| Status | src/models/order.rs:5 | 4 |
|
||||
| ... | | |
|
||||
```
|
||||
|
||||
### Single File Analysis
|
||||
|
||||
```
|
||||
## src/handlers/auth.rs
|
||||
|
||||
### Symbols Hierarchy
|
||||
|
||||
mod auth
|
||||
├── struct AuthHandler
|
||||
│ ├── field: config: Config
|
||||
│ ├── field: db: Pool
|
||||
│ └── impl AuthHandler
|
||||
│ ├── fn new(config, db) -> Self
|
||||
│ ├── fn authenticate(&self, token) -> Result<User>
|
||||
│ └── fn refresh_token(&self, user) -> Result<Token>
|
||||
├── struct Token
|
||||
│ ├── field: value: String
|
||||
│ └── field: expires: DateTime
|
||||
├── enum AuthError
|
||||
│ ├── InvalidToken
|
||||
│ ├── Expired
|
||||
│ └── Unauthorized
|
||||
└── impl Handler for AuthHandler
|
||||
├── fn handle(&self, req) -> Response
|
||||
└── fn name(&self) -> &str
|
||||
```
|
||||
|
||||
## Analysis Features
|
||||
|
||||
### Complexity Metrics
|
||||
|
||||
```
|
||||
## Complexity Analysis
|
||||
|
||||
| File | Structs | Functions | Lines | Complexity |
|
||||
|------|---------|-----------|-------|------------|
|
||||
| src/handlers/auth.rs | 2 | 8 | 150 | Medium |
|
||||
| src/models/user.rs | 3 | 12 | 200 | High |
|
||||
| src/config.rs | 1 | 3 | 50 | Low |
|
||||
|
||||
**Hotspots:** Files with high complexity that may need refactoring
|
||||
- src/handlers/api.rs (15 functions, 300 lines)
|
||||
```
|
||||
|
||||
### Dependency Analysis
|
||||
|
||||
```
|
||||
## Internal Dependencies
|
||||
|
||||
auth.rs
|
||||
├── imports from: config.rs, models/user.rs, db/mod.rs
|
||||
└── imported by: main.rs, handlers/mod.rs
|
||||
|
||||
user.rs
|
||||
├── imports from: (none - leaf module)
|
||||
└── imported by: auth.rs, api.rs, tests/
|
||||
```
|
||||
|
||||
## Symbol Types
|
||||
|
||||
| Type | Icon | LSP Kind |
|
||||
|------|------|----------|
|
||||
| Module | 📦 | Module |
|
||||
| Struct | 🏗️ | Struct |
|
||||
| Enum | 🔢 | Enum |
|
||||
| Trait | 📜 | Interface |
|
||||
| Function | ⚡ | Function |
|
||||
| Method | 🔧 | Method |
|
||||
| Constant | 🔒 | Constant |
|
||||
| Field | 📎 | Field |
|
||||
|
||||
## Common Queries
|
||||
|
||||
| User Says | Analysis |
|
||||
|-----------|----------|
|
||||
| "What structs are in this project?" | workspaceSymbol + filter |
|
||||
| "Show me src/lib.rs structure" | documentSymbol |
|
||||
| "Find all async functions" | workspaceSymbol + async filter |
|
||||
| "List public API" | documentSymbol + pub filter |
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Navigate to symbol | rust-code-navigator |
|
||||
| Call relationships | rust-call-graph |
|
||||
| Trait implementations | rust-trait-explorer |
|
||||
| Safe refactoring | rust-refactor-helper |
|
||||
248
skills/rust-trait-explorer/SKILL.md
Normal file
248
skills/rust-trait-explorer/SKILL.md
Normal file
@@ -0,0 +1,248 @@
|
||||
---
|
||||
name: rust-trait-explorer
|
||||
description: "Explore Rust trait implementations using LSP. Triggers on: /trait-impl, find implementations, who implements, trait 实现, 谁实现了, 实现了哪些trait"
|
||||
argument-hint: "<TraitName|StructName>"
|
||||
allowed-tools: ["LSP", "Read", "Glob", "Grep"]
|
||||
---
|
||||
|
||||
# Rust Trait Explorer
|
||||
|
||||
Discover trait implementations and understand polymorphic designs.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-trait-explorer <TraitName|StructName>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- `/rust-trait-explorer Handler` - Find all implementors of Handler trait
|
||||
- `/rust-trait-explorer MyStruct` - Find all traits implemented by MyStruct
|
||||
|
||||
## LSP Operations
|
||||
|
||||
### Go to Implementation
|
||||
|
||||
Find all implementations of a trait.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "goToImplementation",
|
||||
filePath: "src/traits.rs",
|
||||
line: 10,
|
||||
character: 11
|
||||
)
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- Trait name is known
|
||||
- Want to find all implementors
|
||||
- Understanding polymorphic code
|
||||
|
||||
## Workflow
|
||||
|
||||
### Find Trait Implementors
|
||||
|
||||
```
|
||||
User: "Who implements the Handler trait?"
|
||||
│
|
||||
▼
|
||||
[1] Find trait definition
|
||||
LSP(goToDefinition) or workspaceSymbol
|
||||
│
|
||||
▼
|
||||
[2] Get implementations
|
||||
LSP(goToImplementation)
|
||||
│
|
||||
▼
|
||||
[3] For each impl, get details
|
||||
LSP(documentSymbol) for methods
|
||||
│
|
||||
▼
|
||||
[4] Generate implementation map
|
||||
```
|
||||
|
||||
### Find Traits for a Type
|
||||
|
||||
```
|
||||
User: "What traits does MyStruct implement?"
|
||||
│
|
||||
▼
|
||||
[1] Find struct definition
|
||||
│
|
||||
▼
|
||||
[2] Search for "impl * for MyStruct"
|
||||
Grep pattern matching
|
||||
│
|
||||
▼
|
||||
[3] Get trait details for each
|
||||
│
|
||||
▼
|
||||
[4] Generate trait list
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
### Trait Implementors
|
||||
|
||||
```
|
||||
## Implementations of `Handler`
|
||||
|
||||
**Trait defined at:** src/traits.rs:15
|
||||
|
||||
```rust
|
||||
pub trait Handler {
|
||||
fn handle(&self, request: Request) -> Response;
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
```
|
||||
|
||||
### Implementors (4)
|
||||
|
||||
| Type | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| AuthHandler | src/handlers/auth.rs:20 | Handles authentication |
|
||||
| ApiHandler | src/handlers/api.rs:15 | REST API endpoints |
|
||||
| WebSocketHandler | src/handlers/ws.rs:10 | WebSocket connections |
|
||||
| MockHandler | tests/mocks.rs:5 | Test mock |
|
||||
|
||||
### Implementation Details
|
||||
|
||||
#### AuthHandler
|
||||
```rust
|
||||
impl Handler for AuthHandler {
|
||||
fn handle(&self, request: Request) -> Response {
|
||||
// Authentication logic
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"auth"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ApiHandler
|
||||
```rust
|
||||
impl Handler for ApiHandler {
|
||||
fn handle(&self, request: Request) -> Response {
|
||||
// API routing logic
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"api"
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### Traits for a Type
|
||||
|
||||
```
|
||||
## Traits implemented by `User`
|
||||
|
||||
**Struct defined at:** src/models/user.rs:10
|
||||
|
||||
### Standard Library Traits
|
||||
| Trait | Derived/Manual | Notes |
|
||||
|-------|----------------|-------|
|
||||
| Debug | #[derive] | Auto-generated |
|
||||
| Clone | #[derive] | Auto-generated |
|
||||
| Default | manual | Custom defaults |
|
||||
| Display | manual | User-friendly output |
|
||||
|
||||
### Serde Traits
|
||||
| Trait | Location |
|
||||
|-------|----------|
|
||||
| Serialize | #[derive] |
|
||||
| Deserialize | #[derive] |
|
||||
|
||||
### Project Traits
|
||||
| Trait | Location | Methods |
|
||||
|-------|----------|---------|
|
||||
| Entity | src/db/entity.rs:30 | id(), created_at() |
|
||||
| Validatable | src/validation.rs:15 | validate() |
|
||||
|
||||
### Implementation Hierarchy
|
||||
|
||||
```
|
||||
User
|
||||
├── derive
|
||||
│ ├── Debug
|
||||
│ ├── Clone
|
||||
│ ├── Serialize
|
||||
│ └── Deserialize
|
||||
└── impl
|
||||
├── Default (src/models/user.rs:50)
|
||||
├── Display (src/models/user.rs:60)
|
||||
├── Entity (src/models/user.rs:70)
|
||||
└── Validatable (src/models/user.rs:85)
|
||||
```
|
||||
```
|
||||
|
||||
## Trait Hierarchy Visualization
|
||||
|
||||
```
|
||||
## Trait Hierarchy
|
||||
|
||||
┌─────────────┐
|
||||
│ Error │ (std)
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
│ │ │
|
||||
┌───────▼───────┐ ┌──▼──┐ ┌───────▼───────┐
|
||||
│ AppError │ │ ... │ │ DbError │
|
||||
└───────┬───────┘ └─────┘ └───────┬───────┘
|
||||
│ │
|
||||
┌───────▼───────┐ ┌───────▼───────┐
|
||||
│ AuthError │ │ QueryError │
|
||||
└───────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
## Analysis Features
|
||||
|
||||
### Coverage Check
|
||||
|
||||
```
|
||||
## Trait Implementation Coverage
|
||||
|
||||
Trait: Handler (3 required methods)
|
||||
|
||||
| Implementor | handle() | name() | priority() | Complete |
|
||||
|-------------|----------|--------|------------|----------|
|
||||
| AuthHandler | ✅ | ✅ | ✅ | Yes |
|
||||
| ApiHandler | ✅ | ✅ | ❌ default | Yes |
|
||||
| MockHandler | ✅ | ✅ | ✅ | Yes |
|
||||
```
|
||||
|
||||
### Blanket Implementations
|
||||
|
||||
```
|
||||
## Blanket Implementations
|
||||
|
||||
The following blanket impls may apply to your types:
|
||||
|
||||
| Trait | Blanket Impl | Applies To |
|
||||
|-------|--------------|------------|
|
||||
| From<T> | `impl<T> From<T> for T` | All types |
|
||||
| Into<U> | `impl<T, U> Into<U> for T where U: From<T>` | Types with From |
|
||||
| ToString | `impl<T: Display> ToString for T` | Types with Display |
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
| User Says | Action |
|
||||
|-----------|--------|
|
||||
| "Who implements X?" | goToImplementation on trait |
|
||||
| "What traits does Y impl?" | Grep for `impl * for Y` |
|
||||
| "Show trait hierarchy" | Find super-traits recursively |
|
||||
| "Is X: Send + Sync?" | Check std trait impls |
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Navigate to impl | rust-code-navigator |
|
||||
| Call relationships | rust-call-graph |
|
||||
| Project structure | rust-symbol-analyzer |
|
||||
| Safe refactoring | rust-refactor-helper |
|
||||
136
skills/unsafe-checker/AGENTS.md
Normal file
136
skills/unsafe-checker/AGENTS.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Unsafe Checker - Quick Reference
|
||||
|
||||
**Auto-generated from rules/**
|
||||
|
||||
## Rule Summary by Section
|
||||
|
||||
### General Principles (3 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| general-01 | P | Do Not Abuse Unsafe to Escape Compiler Safety Checks |
|
||||
| general-02 | P | Do Not Blindly Use Unsafe for Performance |
|
||||
| general-03 | G | Do Not Create Aliases for Types/Methods Named "Unsafe" |
|
||||
|
||||
### Safety Abstraction (11 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| safety-01 | P | Be Aware of Memory Safety Issues from Panics |
|
||||
| safety-02 | P | Unsafe Code Authors Must Verify Safety Invariants |
|
||||
| safety-03 | P | Do Not Expose Uninitialized Memory in Public APIs |
|
||||
| safety-04 | P | Avoid Double-Free from Panic Safety Issues |
|
||||
| safety-05 | P | Consider Safety When Manually Implementing Auto Traits |
|
||||
| safety-06 | P | Do Not Expose Raw Pointers in Public APIs |
|
||||
| safety-07 | P | Provide Unsafe Counterparts for Performance Alongside Safe Methods |
|
||||
| safety-08 | P | Mutable Return from Immutable Parameter is Wrong |
|
||||
| safety-09 | P | Add SAFETY Comment Before Any Unsafe Block |
|
||||
| safety-10 | G | Add Safety Section in Docs for Public Unsafe Functions |
|
||||
| safety-11 | G | Use assert! Instead of debug_assert! in Unsafe Functions |
|
||||
|
||||
### Raw Pointers (6 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| ptr-01 | P | Do Not Share Raw Pointers Across Threads |
|
||||
| ptr-02 | P | Prefer NonNull<T> Over *mut T |
|
||||
| ptr-03 | P | Use PhantomData<T> for Variance and Ownership |
|
||||
| ptr-04 | G | Do Not Dereference Pointers Cast to Misaligned Types |
|
||||
| ptr-05 | G | Do Not Manually Convert Immutable Pointer to Mutable |
|
||||
| ptr-06 | G | Prefer pointer::cast Over `as` for Pointer Casting |
|
||||
|
||||
### Union (2 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| union-01 | P | Avoid Union Except for C Interop |
|
||||
| union-02 | P | Do Not Use Union Variants Across Different Lifetimes |
|
||||
|
||||
### Memory Layout (6 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| mem-01 | P | Choose Appropriate Data Layout for Struct/Tuple/Enum |
|
||||
| mem-02 | P | Do Not Modify Memory Variables of Other Processes |
|
||||
| mem-03 | P | Do Not Let String/Vec Auto-Drop Other Process's Memory |
|
||||
| mem-04 | P | Prefer Reentrant Versions of C-API or Syscalls |
|
||||
| mem-05 | P | Use Third-Party Crates for Bitfields |
|
||||
| mem-06 | G | Use MaybeUninit<T> for Uninitialized Memory |
|
||||
|
||||
### FFI (18 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| ffi-01 | P | Avoid Passing Strings Directly to C |
|
||||
| ffi-02 | P | Read Documentation Carefully for std::ffi Types |
|
||||
| ffi-03 | P | Implement Drop for Wrapped C Pointers |
|
||||
| ffi-04 | P | Handle Panics When Crossing FFI Boundaries |
|
||||
| ffi-05 | P | Use Portable Type Aliases from std or libc |
|
||||
| ffi-06 | P | Ensure C-ABI String Compatibility |
|
||||
| ffi-07 | P | Do Not Implement Drop for Types Passed to External Code |
|
||||
| ffi-08 | P | Handle Errors Properly in FFI |
|
||||
| ffi-09 | P | Use References Instead of Raw Pointers in Safe Wrappers |
|
||||
| ffi-10 | P | Exported Functions Must Be Thread-Safe |
|
||||
| ffi-11 | P | Be Careful with repr(packed) Field References |
|
||||
| ffi-12 | P | Document Invariant Assumptions for C Parameters |
|
||||
| ffi-13 | P | Ensure Consistent Data Layout for Custom Types |
|
||||
| ffi-14 | P | Types in FFI Should Have Stable Layout |
|
||||
| ffi-15 | P | Validate Non-Robust External Values |
|
||||
| ffi-16 | P | Separate Data and Code for Closures to C |
|
||||
| ffi-17 | P | Use Opaque Types Instead of c_void |
|
||||
| ffi-18 | P | Avoid Passing Trait Objects to C |
|
||||
|
||||
### I/O Safety (1 rule)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| io-01 | P | Ensure I/O Safety When Using Raw Handles |
|
||||
|
||||
## Clippy Lint Mapping
|
||||
|
||||
| Clippy Lint | Rule | Category |
|
||||
|-------------|------|----------|
|
||||
| `undocumented_unsafe_blocks` | safety-09 | SAFETY comments |
|
||||
| `missing_safety_doc` | safety-10 | Safety docs |
|
||||
| `panic_in_result_fn` | safety-01, ffi-04 | Panic safety |
|
||||
| `non_send_fields_in_send_ty` | safety-05 | Send/Sync |
|
||||
| `uninit_assumed_init` | safety-03 | Initialization |
|
||||
| `uninit_vec` | mem-06 | Initialization |
|
||||
| `mut_from_ref` | safety-08 | Aliasing |
|
||||
| `cast_ptr_alignment` | ptr-04 | Alignment |
|
||||
| `cast_ref_to_mut` | ptr-05 | Aliasing |
|
||||
| `ptr_as_ptr` | ptr-06 | Pointer casting |
|
||||
| `unaligned_references` | ffi-11 | Packed structs |
|
||||
| `debug_assert_with_mut_call` | safety-11 | Assertions |
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
```
|
||||
Writing unsafe code?
|
||||
│
|
||||
├─ FFI with C?
|
||||
│ └─ See ffi-* rules
|
||||
│
|
||||
├─ Raw pointers?
|
||||
│ └─ See ptr-* rules
|
||||
│
|
||||
├─ Manual Send/Sync?
|
||||
│ └─ See safety-05
|
||||
│
|
||||
├─ MaybeUninit/uninitialized?
|
||||
│ └─ See safety-03, mem-06
|
||||
│
|
||||
└─ Performance optimization?
|
||||
└─ See general-02, safety-07
|
||||
```
|
||||
|
||||
## Essential Checklist
|
||||
|
||||
Before every unsafe block:
|
||||
- [ ] SAFETY comment present
|
||||
- [ ] Invariants documented
|
||||
- [ ] Pointer validity checked
|
||||
- [ ] Aliasing rules followed
|
||||
- [ ] Panic safety considered
|
||||
- [ ] Tested with Miri
|
||||
|
||||
## Resources
|
||||
|
||||
- `checklists/before-unsafe.md` - Pre-writing checklist
|
||||
- `checklists/review-unsafe.md` - Code review checklist
|
||||
- `checklists/common-pitfalls.md` - Common bugs and fixes
|
||||
- `examples/safe-abstraction.md` - Safe wrapper patterns
|
||||
- `examples/ffi-patterns.md` - FFI best practices
|
||||
86
skills/unsafe-checker/SKILL.md
Normal file
86
skills/unsafe-checker/SKILL.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: unsafe-checker
|
||||
description: "CRITICAL: Use for unsafe Rust code review and FFI. Triggers on: unsafe, raw pointer, FFI, extern, transmute, *mut, *const, union, #[repr(C)], libc, std::ffi, MaybeUninit, NonNull, SAFETY comment, soundness, undefined behavior, UB, safe wrapper, memory layout, bindgen, cbindgen, CString, CStr, 安全抽象, 裸指针, 外部函数接口, 内存布局, 不安全代码, FFI 绑定, 未定义行为"
|
||||
globs: ["**/*.rs"]
|
||||
allowed-tools: ["Read", "Grep", "Glob"]
|
||||
---
|
||||
|
||||
Display the following ASCII art exactly as shown. Do not modify spaces or line breaks:
|
||||
```text
|
||||
⚠️ **Unsafe Rust Checker Loaded**
|
||||
|
||||
* ^ *
|
||||
/◉\_~^~_/◉\
|
||||
⚡/ o \⚡
|
||||
'_ _'
|
||||
/ '-----' \
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Unsafe Rust Checker
|
||||
|
||||
## When Unsafe is Valid
|
||||
|
||||
| Use Case | Example |
|
||||
|----------|---------|
|
||||
| FFI | Calling C functions |
|
||||
| Low-level abstractions | Implementing `Vec`, `Arc` |
|
||||
| Performance | Measured bottleneck with safe alternative too slow |
|
||||
|
||||
**NOT valid:** Escaping borrow checker without understanding why.
|
||||
|
||||
## Required Documentation
|
||||
|
||||
```rust
|
||||
// SAFETY: <why this is safe>
|
||||
unsafe { ... }
|
||||
|
||||
/// # Safety
|
||||
/// <caller requirements>
|
||||
pub unsafe fn dangerous() { ... }
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Operation | Safety Requirements |
|
||||
|-----------|---------------------|
|
||||
| `*ptr` deref | Valid, aligned, initialized |
|
||||
| `&*ptr` | + No aliasing violations |
|
||||
| `transmute` | Same size, valid bit pattern |
|
||||
| `extern "C"` | Correct signature, ABI |
|
||||
| `static mut` | Synchronization guaranteed |
|
||||
| `impl Send/Sync` | Actually thread-safe |
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Fix |
|
||||
|-------|-----|
|
||||
| Null pointer deref | Check for null before deref |
|
||||
| Use after free | Ensure lifetime validity |
|
||||
| Data race | Add proper synchronization |
|
||||
| Alignment violation | Use `#[repr(C)]`, check alignment |
|
||||
| Invalid bit pattern | Use `MaybeUninit` |
|
||||
| Missing SAFETY comment | Add `// SAFETY:` |
|
||||
|
||||
## Deprecated → Better
|
||||
|
||||
| Deprecated | Use Instead |
|
||||
|------------|-------------|
|
||||
| `mem::uninitialized()` | `MaybeUninit<T>` |
|
||||
| `mem::zeroed()` for refs | `MaybeUninit<T>` |
|
||||
| Raw pointer arithmetic | `NonNull<T>`, `ptr::add` |
|
||||
| `CString::new().unwrap().as_ptr()` | Store `CString` first |
|
||||
| `static mut` | `AtomicT` or `Mutex` |
|
||||
| Manual extern | `bindgen` |
|
||||
|
||||
## FFI Crates
|
||||
|
||||
| Direction | Crate |
|
||||
|-----------|-------|
|
||||
| C → Rust | bindgen |
|
||||
| Rust → C | cbindgen |
|
||||
| Python | PyO3 |
|
||||
| Node.js | napi-rs |
|
||||
|
||||
Claude knows unsafe Rust. Focus on SAFETY comments and soundness.
|
||||
115
skills/unsafe-checker/checklists/before-unsafe.md
Normal file
115
skills/unsafe-checker/checklists/before-unsafe.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Checklist: Before Writing Unsafe Code
|
||||
|
||||
Use this checklist before writing any `unsafe` block or `unsafe fn`.
|
||||
|
||||
## 1. Do You Really Need Unsafe?
|
||||
|
||||
- [ ] Have you tried all safe alternatives?
|
||||
- [ ] Can you restructure the code to satisfy the borrow checker?
|
||||
- [ ] Would interior mutability (`Cell`, `RefCell`, `Mutex`) solve the problem?
|
||||
- [ ] Is there a safe crate that already does this?
|
||||
- [ ] Is the performance gain (if any) worth the safety risk?
|
||||
|
||||
**If you answered "no" to all, proceed with unsafe.**
|
||||
|
||||
## 2. What Unsafe Operation Do You Need?
|
||||
|
||||
Identify which specific unsafe operation you're performing:
|
||||
|
||||
- [ ] Dereferencing a raw pointer (`*const T`, `*mut T`)
|
||||
- [ ] Calling an `unsafe` function
|
||||
- [ ] Accessing a mutable static variable
|
||||
- [ ] Implementing an unsafe trait (`Send`, `Sync`, etc.)
|
||||
- [ ] Accessing fields of a `union`
|
||||
- [ ] Using `extern "C"` functions (FFI)
|
||||
|
||||
## 3. Safety Invariants
|
||||
|
||||
For each unsafe operation, document the invariants:
|
||||
|
||||
### For Pointer Dereference:
|
||||
- [ ] Is the pointer non-null?
|
||||
- [ ] Is the pointer properly aligned for the type?
|
||||
- [ ] Does the pointer point to valid, initialized memory?
|
||||
- [ ] Is the memory not being mutated by other code?
|
||||
- [ ] Will the memory remain valid for the entire duration of use?
|
||||
|
||||
### For Mutable Aliasing:
|
||||
- [ ] Are you creating multiple mutable references to the same memory?
|
||||
- [ ] Is there any possibility of aliasing `&mut` and `&`?
|
||||
- [ ] Have you verified no other code can access this memory?
|
||||
|
||||
### For FFI:
|
||||
- [ ] Is the function signature correct (types, ABI)?
|
||||
- [ ] Are you handling potential null pointers?
|
||||
- [ ] Are you handling potential panics (catch_unwind)?
|
||||
- [ ] Is memory ownership clear (who allocates, who frees)?
|
||||
|
||||
### For Send/Sync:
|
||||
- [ ] Is concurrent access properly synchronized?
|
||||
- [ ] Are there any data races possible?
|
||||
- [ ] Does the type truly satisfy the trait requirements?
|
||||
|
||||
## 4. Panic Safety
|
||||
|
||||
- [ ] What happens if this code panics at any line?
|
||||
- [ ] Are data structures left in a valid state on panic?
|
||||
- [ ] Do you need a panic guard for cleanup?
|
||||
- [ ] Could a destructor see invalid state?
|
||||
|
||||
## 5. Documentation
|
||||
|
||||
- [ ] Have you written a `// SAFETY:` comment explaining:
|
||||
- What invariants must hold?
|
||||
- Why those invariants are upheld here?
|
||||
|
||||
- [ ] For `unsafe fn`, have you written `# Safety` docs explaining:
|
||||
- What the caller must guarantee?
|
||||
- What happens if requirements are violated?
|
||||
|
||||
## 6. Testing and Verification
|
||||
|
||||
- [ ] Can you add debug assertions to verify invariants?
|
||||
- [ ] Have you tested with Miri (`cargo miri test`)?
|
||||
- [ ] Have you tested with address sanitizer (`RUSTFLAGS="-Zsanitizer=address"`)?
|
||||
- [ ] Have you considered fuzzing the unsafe code?
|
||||
|
||||
## Quick Reference: Common SAFETY Comments
|
||||
|
||||
```rust
|
||||
// SAFETY: We checked that index < len above, so this is in bounds.
|
||||
|
||||
// SAFETY: The pointer was created from a valid reference and hasn't been invalidated.
|
||||
|
||||
// SAFETY: We hold the lock, guaranteeing exclusive access.
|
||||
|
||||
// SAFETY: The type is #[repr(C)] and all fields are initialized.
|
||||
|
||||
// SAFETY: Caller guarantees the pointer is non-null and properly aligned.
|
||||
```
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```
|
||||
Need unsafe?
|
||||
|
|
||||
v
|
||||
Can you use safe Rust? --Yes--> Don't use unsafe
|
||||
|
|
||||
No
|
||||
v
|
||||
Can you use existing safe abstraction? --Yes--> Use it (std, crates)
|
||||
|
|
||||
No
|
||||
v
|
||||
Document all invariants
|
||||
|
|
||||
v
|
||||
Add SAFETY comments
|
||||
|
|
||||
v
|
||||
Write the unsafe code
|
||||
|
|
||||
v
|
||||
Test with Miri
|
||||
```
|
||||
253
skills/unsafe-checker/checklists/common-pitfalls.md
Normal file
253
skills/unsafe-checker/checklists/common-pitfalls.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# Common Unsafe Pitfalls and Fixes
|
||||
|
||||
A reference of frequently encountered unsafe bugs and how to fix them.
|
||||
|
||||
## Pitfall 1: Dangling Pointer from Local
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() -> *const i32 {
|
||||
let x = 42;
|
||||
&x as *const i32 // Dangling after return!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good() -> Box<i32> {
|
||||
Box::new(42) // Heap allocation lives beyond function
|
||||
}
|
||||
|
||||
// Or return the value itself
|
||||
fn better() -> i32 {
|
||||
42
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 2: CString Lifetime
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() -> *const c_char {
|
||||
let s = CString::new("hello").unwrap();
|
||||
s.as_ptr() // Dangling! CString dropped
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good(s: &CString) -> *const c_char {
|
||||
s.as_ptr() // Caller keeps CString alive
|
||||
}
|
||||
|
||||
// Or take ownership
|
||||
fn also_good(s: CString) -> *const c_char {
|
||||
s.into_raw() // Caller must free with CString::from_raw
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 3: Vec set_len with Uninitialized Data
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() -> Vec<String> {
|
||||
let mut v = Vec::with_capacity(10);
|
||||
unsafe { v.set_len(10); } // Strings are uninitialized!
|
||||
v
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good() -> Vec<String> {
|
||||
let mut v = Vec::with_capacity(10);
|
||||
for _ in 0..10 {
|
||||
v.push(String::new());
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
// Or use resize
|
||||
fn also_good() -> Vec<String> {
|
||||
let mut v = Vec::new();
|
||||
v.resize(10, String::new());
|
||||
v
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 4: Reference to Packed Field
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
#[repr(packed)]
|
||||
struct Packed { a: u8, b: u32 }
|
||||
|
||||
fn bad(p: &Packed) -> &u32 {
|
||||
&p.b // UB: misaligned reference!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good(p: &Packed) -> u32 {
|
||||
unsafe { std::ptr::addr_of!(p.b).read_unaligned() }
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 5: Mutable Aliasing Through Raw Pointers
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() {
|
||||
let mut x = 42;
|
||||
let ptr1 = &mut x as *mut i32;
|
||||
let ptr2 = &mut x as *mut i32; // Already have ptr1!
|
||||
unsafe {
|
||||
*ptr1 = 1;
|
||||
*ptr2 = 2; // Aliasing mutable pointers!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good() {
|
||||
let mut x = 42;
|
||||
let ptr = &mut x as *mut i32;
|
||||
unsafe {
|
||||
*ptr = 1;
|
||||
*ptr = 2; // Same pointer, sequential access
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 6: Transmute to Wrong Size
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() {
|
||||
let x: u32 = 42;
|
||||
let y: u64 = unsafe { std::mem::transmute(x) }; // UB: size mismatch!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good() {
|
||||
let x: u32 = 42;
|
||||
let y: u64 = x as u64; // Use conversion
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 7: Invalid Enum Discriminant
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
#[repr(u8)]
|
||||
enum Status { A = 0, B = 1, C = 2 }
|
||||
|
||||
fn bad(raw: u8) -> Status {
|
||||
unsafe { std::mem::transmute(raw) } // UB if raw > 2!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good(raw: u8) -> Option<Status> {
|
||||
match raw {
|
||||
0 => Some(Status::A),
|
||||
1 => Some(Status::B),
|
||||
2 => Some(Status::C),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 8: FFI Panic Unwinding
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
#[no_mangle]
|
||||
extern "C" fn callback(x: i32) -> i32 {
|
||||
if x < 0 {
|
||||
panic!("negative!"); // UB: unwinding across FFI!
|
||||
}
|
||||
x * 2
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
#[no_mangle]
|
||||
extern "C" fn callback(x: i32) -> i32 {
|
||||
std::panic::catch_unwind(|| {
|
||||
if x < 0 {
|
||||
panic!("negative!");
|
||||
}
|
||||
x * 2
|
||||
}).unwrap_or(-1) // Return error code on panic
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 9: Double Free from Clone + into_raw
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
struct Handle(*mut c_void);
|
||||
|
||||
impl Clone for Handle {
|
||||
fn clone(&self) -> Self {
|
||||
Handle(self.0) // Both now "own" same pointer!
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Handle {
|
||||
fn drop(&mut self) {
|
||||
unsafe { free(self.0); } // Double free when both drop!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
struct Handle(*mut c_void);
|
||||
|
||||
// Don't implement Clone, or implement proper reference counting
|
||||
impl Handle {
|
||||
fn clone_ptr(&self) -> *mut c_void {
|
||||
self.0 // Return raw pointer, no ownership
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 10: Forget Doesn't Run Destructors
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() {
|
||||
let guard = lock.lock();
|
||||
std::mem::forget(guard); // Lock never released!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good() {
|
||||
let guard = lock.lock();
|
||||
// Let guard drop naturally
|
||||
// or explicitly: drop(guard);
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference Table
|
||||
|
||||
| Pitfall | Detection | Fix |
|
||||
|---------|-----------|-----|
|
||||
| Dangling pointer | Miri | Extend lifetime or heap allocate |
|
||||
| Uninitialized read | Miri | Use MaybeUninit properly |
|
||||
| Misaligned access | Miri, UBsan | read_unaligned, copy by value |
|
||||
| Data race | TSan | Use atomics or mutex |
|
||||
| Double free | ASan | Track ownership carefully |
|
||||
| Invalid enum | Manual review | Use TryFrom |
|
||||
| FFI panic | Testing | catch_unwind |
|
||||
| Type confusion | Miri | Match types exactly |
|
||||
113
skills/unsafe-checker/checklists/review-unsafe.md
Normal file
113
skills/unsafe-checker/checklists/review-unsafe.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Checklist: Reviewing Unsafe Code
|
||||
|
||||
Use this checklist when reviewing code containing `unsafe`.
|
||||
|
||||
## 1. Surface-Level Checks
|
||||
|
||||
- [ ] Does every `unsafe` block have a `// SAFETY:` comment?
|
||||
- [ ] Does every `unsafe fn` have `# Safety` documentation?
|
||||
- [ ] Are the safety comments specific and verifiable, not vague?
|
||||
- [ ] Is the unsafe code minimized (smallest possible unsafe block)?
|
||||
|
||||
## 2. Pointer Validity
|
||||
|
||||
For each pointer dereference:
|
||||
|
||||
- [ ] **Non-null**: Is null checked before dereference?
|
||||
- [ ] **Aligned**: Is alignment verified or guaranteed by construction?
|
||||
- [ ] **Valid**: Does the pointer point to allocated memory?
|
||||
- [ ] **Initialized**: Is the memory initialized before reading?
|
||||
- [ ] **Lifetime**: Is the memory valid for the entire use duration?
|
||||
- [ ] **Unique**: For `&mut`, is there only one mutable reference?
|
||||
|
||||
## 3. Memory Safety
|
||||
|
||||
- [ ] **No aliasing**: Are `&` and `&mut` never created to the same memory simultaneously?
|
||||
- [ ] **No use-after-free**: Is memory not accessed after deallocation?
|
||||
- [ ] **No double-free**: Is memory freed exactly once?
|
||||
- [ ] **No data races**: Is concurrent access properly synchronized?
|
||||
- [ ] **Bounds checked**: Are array/slice accesses in bounds?
|
||||
|
||||
## 4. Type Safety
|
||||
|
||||
- [ ] **Transmute**: Are transmuted types actually compatible?
|
||||
- [ ] **Repr**: Do FFI types have `#[repr(C)]`?
|
||||
- [ ] **Enum values**: Are enum discriminants validated from external sources?
|
||||
- [ ] **Unions**: Is the correct union field accessed?
|
||||
|
||||
## 5. Panic Safety
|
||||
|
||||
- [ ] What state is the program in if this code panics?
|
||||
- [ ] Are partially constructed objects properly cleaned up?
|
||||
- [ ] Do Drop implementations see valid state?
|
||||
- [ ] Is there a panic guard if needed?
|
||||
|
||||
## 6. FFI-Specific Checks
|
||||
|
||||
- [ ] **Types**: Do Rust types match C types exactly?
|
||||
- [ ] **Strings**: Are strings properly null-terminated?
|
||||
- [ ] **Ownership**: Is it clear who owns/frees memory?
|
||||
- [ ] **Thread safety**: Are callbacks thread-safe?
|
||||
- [ ] **Panic boundary**: Are panics caught before crossing FFI?
|
||||
- [ ] **Error handling**: Are C-style errors properly handled?
|
||||
|
||||
## 7. Concurrency Checks
|
||||
|
||||
- [ ] **Send/Sync**: Are manual implementations actually sound?
|
||||
- [ ] **Atomics**: Are memory orderings correct?
|
||||
- [ ] **Locks**: Is there potential for deadlock?
|
||||
- [ ] **Data races**: Is all shared mutable state synchronized?
|
||||
|
||||
## 8. Red Flags (Require Extra Scrutiny)
|
||||
|
||||
| Pattern | Concern |
|
||||
|---------|---------|
|
||||
| `transmute` | Type compatibility, provenance |
|
||||
| `as` on pointers | Alignment, type punning |
|
||||
| `static mut` | Data races |
|
||||
| `*const T as *mut T` | Aliasing violation |
|
||||
| Manual `Send`/`Sync` | Thread safety |
|
||||
| `assume_init` | Initialization |
|
||||
| `set_len` on Vec | Uninitialized memory |
|
||||
| `from_raw_parts` | Lifetime, validity |
|
||||
| `offset`/`add`/`sub` | Out of bounds |
|
||||
| FFI callbacks | Panic safety |
|
||||
|
||||
## 9. Verification Questions
|
||||
|
||||
Ask the author:
|
||||
- "What would happen if [X invariant] was violated?"
|
||||
- "How do you know [pointer/reference] is valid here?"
|
||||
- "What if this panics at [specific line]?"
|
||||
- "Who is responsible for freeing this memory?"
|
||||
|
||||
## 10. Testing Requirements
|
||||
|
||||
- [ ] Has this been tested with Miri?
|
||||
- [ ] Are there unit tests covering edge cases?
|
||||
- [ ] Are there tests for error conditions?
|
||||
- [ ] Has concurrent code been tested under stress?
|
||||
|
||||
## Review Severity Guide
|
||||
|
||||
| Severity | Requires |
|
||||
|----------|----------|
|
||||
| `transmute` | Two reviewers, Miri test |
|
||||
| Manual `Send`/`Sync` | Thread safety expert review |
|
||||
| FFI | Documentation of C interface |
|
||||
| `static mut` | Justification for not using atomic/mutex |
|
||||
| Pointer arithmetic | Bounds proof |
|
||||
|
||||
## Sample Review Comments
|
||||
|
||||
```
|
||||
// Good SAFETY comment ✓
|
||||
// SAFETY: index was checked to be < len on line 42
|
||||
|
||||
// Needs improvement ✗
|
||||
// SAFETY: This is safe because we know it works
|
||||
|
||||
// Missing information ✗
|
||||
// SAFETY: ptr is valid
|
||||
// (Why is it valid? How do we know?)
|
||||
```
|
||||
353
skills/unsafe-checker/examples/ffi-patterns.md
Normal file
353
skills/unsafe-checker/examples/ffi-patterns.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# FFI Best Practices and Patterns
|
||||
|
||||
Examples of safe and idiomatic Rust-C interoperability.
|
||||
|
||||
## Pattern 1: Basic FFI Wrapper
|
||||
|
||||
```rust
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::{c_char, c_int, c_void};
|
||||
use std::ptr::NonNull;
|
||||
|
||||
// Raw C API
|
||||
mod ffi {
|
||||
use super::*;
|
||||
|
||||
extern "C" {
|
||||
pub fn lib_create(name: *const c_char) -> *mut c_void;
|
||||
pub fn lib_destroy(handle: *mut c_void);
|
||||
pub fn lib_process(handle: *mut c_void, data: *const u8, len: usize) -> c_int;
|
||||
pub fn lib_get_error() -> *const c_char;
|
||||
}
|
||||
}
|
||||
|
||||
// Safe Rust wrapper
|
||||
pub struct Library {
|
||||
handle: NonNull<c_void>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LibraryError(String);
|
||||
|
||||
impl Library {
|
||||
pub fn new(name: &str) -> Result<Self, LibraryError> {
|
||||
let c_name = CString::new(name).map_err(|_| LibraryError("invalid name".into()))?;
|
||||
|
||||
let handle = unsafe { ffi::lib_create(c_name.as_ptr()) };
|
||||
|
||||
NonNull::new(handle)
|
||||
.map(|handle| Self { handle })
|
||||
.ok_or_else(|| Self::last_error())
|
||||
}
|
||||
|
||||
pub fn process(&self, data: &[u8]) -> Result<(), LibraryError> {
|
||||
let result = unsafe {
|
||||
ffi::lib_process(self.handle.as_ptr(), data.as_ptr(), data.len())
|
||||
};
|
||||
|
||||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Self::last_error())
|
||||
}
|
||||
}
|
||||
|
||||
fn last_error() -> LibraryError {
|
||||
let ptr = unsafe { ffi::lib_get_error() };
|
||||
if ptr.is_null() {
|
||||
LibraryError("unknown error".into())
|
||||
} else {
|
||||
let msg = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
LibraryError(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Library {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::lib_destroy(self.handle.as_ptr()); }
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent accidental copies
|
||||
impl !Clone for Library {}
|
||||
```
|
||||
|
||||
## Pattern 2: Callback Registration
|
||||
|
||||
```rust
|
||||
use std::os::raw::{c_int, c_void};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
|
||||
type CCallback = extern "C" fn(value: c_int, user_data: *mut c_void) -> c_int;
|
||||
|
||||
extern "C" {
|
||||
fn register_callback(cb: CCallback, user_data: *mut c_void);
|
||||
fn unregister_callback();
|
||||
}
|
||||
|
||||
/// Safely register a Rust closure as a C callback.
|
||||
pub struct CallbackGuard<F> {
|
||||
_closure: Box<F>,
|
||||
}
|
||||
|
||||
impl<F: FnMut(i32) -> i32 + 'static> CallbackGuard<F> {
|
||||
pub fn register(closure: F) -> Self {
|
||||
let boxed = Box::new(closure);
|
||||
let user_data = Box::into_raw(boxed) as *mut c_void;
|
||||
|
||||
extern "C" fn trampoline<F: FnMut(i32) -> i32>(
|
||||
value: c_int,
|
||||
user_data: *mut c_void,
|
||||
) -> c_int {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let closure = unsafe { &mut *(user_data as *mut F) };
|
||||
closure(value as i32) as c_int
|
||||
}));
|
||||
result.unwrap_or(-1)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
register_callback(trampoline::<F>, user_data);
|
||||
}
|
||||
|
||||
Self {
|
||||
// SAFETY: We just created this box and need to keep it alive
|
||||
_closure: unsafe { Box::from_raw(user_data as *mut F) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Drop for CallbackGuard<F> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { unregister_callback(); }
|
||||
// Box in _closure is dropped automatically
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
fn example() {
|
||||
let multiplier = 2;
|
||||
let _guard = CallbackGuard::register(move |x| x * multiplier);
|
||||
// Callback is active until _guard is dropped
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 3: Opaque Handle Types
|
||||
|
||||
```rust
|
||||
use std::marker::PhantomData;
|
||||
|
||||
// Opaque type markers - prevents mixing up handles
|
||||
#[repr(C)]
|
||||
pub struct DatabaseHandle {
|
||||
_data: [u8; 0],
|
||||
_marker: PhantomData<(*mut u8, std::marker::PhantomPinned)>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ConnectionHandle {
|
||||
_data: [u8; 0],
|
||||
_marker: PhantomData<(*mut u8, std::marker::PhantomPinned)>,
|
||||
}
|
||||
|
||||
mod ffi {
|
||||
use super::*;
|
||||
|
||||
extern "C" {
|
||||
pub fn db_open(path: *const c_char) -> *mut DatabaseHandle;
|
||||
pub fn db_close(db: *mut DatabaseHandle);
|
||||
pub fn db_connect(db: *mut DatabaseHandle) -> *mut ConnectionHandle;
|
||||
pub fn conn_close(conn: *mut ConnectionHandle);
|
||||
pub fn conn_query(conn: *mut ConnectionHandle, sql: *const c_char) -> c_int;
|
||||
}
|
||||
}
|
||||
|
||||
// Type-safe wrappers
|
||||
pub struct Database {
|
||||
handle: NonNull<DatabaseHandle>,
|
||||
}
|
||||
|
||||
pub struct Connection<'db> {
|
||||
handle: NonNull<ConnectionHandle>,
|
||||
_db: PhantomData<&'db Database>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn open(path: &str) -> Result<Self, ()> {
|
||||
let c_path = CString::new(path).map_err(|_| ())?;
|
||||
let handle = unsafe { ffi::db_open(c_path.as_ptr()) };
|
||||
NonNull::new(handle).map(|h| Self { handle: h }).ok_or(())
|
||||
}
|
||||
|
||||
pub fn connect(&self) -> Result<Connection<'_>, ()> {
|
||||
let handle = unsafe { ffi::db_connect(self.handle.as_ptr()) };
|
||||
NonNull::new(handle)
|
||||
.map(|h| Connection { handle: h, _db: PhantomData })
|
||||
.ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Database {
|
||||
fn drop(&mut self) {
|
||||
// All Connections must be dropped first (enforced by lifetime)
|
||||
unsafe { ffi::db_close(self.handle.as_ptr()); }
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection<'_> {
|
||||
pub fn query(&self, sql: &str) -> Result<(), ()> {
|
||||
let c_sql = CString::new(sql).map_err(|_| ())?;
|
||||
let result = unsafe { ffi::conn_query(self.handle.as_ptr(), c_sql.as_ptr()) };
|
||||
if result == 0 { Ok(()) } else { Err(()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Connection<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::conn_close(self.handle.as_ptr()); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 4: Error Handling Across FFI
|
||||
|
||||
```rust
|
||||
use std::os::raw::c_int;
|
||||
|
||||
// Error codes for C
|
||||
pub const SUCCESS: c_int = 0;
|
||||
pub const ERR_NULL_PTR: c_int = 1;
|
||||
pub const ERR_INVALID_UTF8: c_int = 2;
|
||||
pub const ERR_IO: c_int = 3;
|
||||
pub const ERR_PANIC: c_int = -1;
|
||||
|
||||
// Thread-local error storage
|
||||
thread_local! {
|
||||
static LAST_ERROR: std::cell::RefCell<Option<Box<dyn std::error::Error>>> =
|
||||
std::cell::RefCell::new(None);
|
||||
}
|
||||
|
||||
fn set_last_error<E: std::error::Error + 'static>(err: E) {
|
||||
LAST_ERROR.with(|e| {
|
||||
*e.borrow_mut() = Some(Box::new(err));
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the last error message. Caller must free with `free_string`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_last_error() -> *mut c_char {
|
||||
LAST_ERROR.with(|e| {
|
||||
e.borrow()
|
||||
.as_ref()
|
||||
.map(|err| {
|
||||
CString::new(err.to_string())
|
||||
.unwrap_or_else(|_| CString::new("error").unwrap())
|
||||
.into_raw()
|
||||
})
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
})
|
||||
}
|
||||
|
||||
/// Free a string returned by this library.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn free_string(s: *mut c_char) {
|
||||
if !s.is_null() {
|
||||
// SAFETY: String was created by CString::into_raw
|
||||
unsafe { drop(CString::from_raw(s)); }
|
||||
}
|
||||
}
|
||||
|
||||
/// Example function with proper error handling.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn do_operation(data: *const u8, len: usize) -> c_int {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| -> Result<(), c_int> {
|
||||
if data.is_null() {
|
||||
return Err(ERR_NULL_PTR);
|
||||
}
|
||||
|
||||
let slice = unsafe { std::slice::from_raw_parts(data, len) };
|
||||
|
||||
std::str::from_utf8(slice)
|
||||
.map_err(|e| {
|
||||
set_last_error(e);
|
||||
ERR_INVALID_UTF8
|
||||
})?;
|
||||
|
||||
// Do actual work...
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => SUCCESS,
|
||||
Ok(Err(code)) => code,
|
||||
Err(_) => ERR_PANIC,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 5: Struct with C Layout
|
||||
|
||||
```rust
|
||||
use std::os::raw::{c_char, c_int};
|
||||
|
||||
/// A C-compatible configuration struct.
|
||||
#[repr(C)]
|
||||
pub struct Config {
|
||||
pub version: c_int,
|
||||
pub flags: u32,
|
||||
pub name: [c_char; 64],
|
||||
pub name_len: usize,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(version: i32, flags: u32, name: &str) -> Option<Self> {
|
||||
if name.len() >= 64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut config = Self {
|
||||
version: version as c_int,
|
||||
flags,
|
||||
name: [0; 64],
|
||||
name_len: name.len(),
|
||||
};
|
||||
|
||||
// Copy name bytes
|
||||
for (i, byte) in name.bytes().enumerate() {
|
||||
config.name[i] = byte as c_char;
|
||||
}
|
||||
|
||||
Some(config)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
let bytes = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
self.name.as_ptr() as *const u8,
|
||||
self.name_len,
|
||||
)
|
||||
};
|
||||
// SAFETY: We only store valid UTF-8 in new()
|
||||
unsafe { std::str::from_utf8_unchecked(bytes) }
|
||||
}
|
||||
}
|
||||
|
||||
// Verify layout at compile time
|
||||
const _: () = {
|
||||
assert!(std::mem::size_of::<Config>() == 80); // 4 + 4 + 64 + 8
|
||||
assert!(std::mem::align_of::<Config>() == 8);
|
||||
};
|
||||
```
|
||||
|
||||
## Key FFI Guidelines
|
||||
|
||||
1. **Always use `#[repr(C)]`** for types crossing FFI
|
||||
2. **Handle null pointers** at the boundary
|
||||
3. **Catch panics** before returning to C
|
||||
4. **Document ownership** clearly
|
||||
5. **Use opaque types** for type safety
|
||||
6. **Keep unsafe minimal** and well-documented
|
||||
272
skills/unsafe-checker/examples/safe-abstraction.md
Normal file
272
skills/unsafe-checker/examples/safe-abstraction.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Safe Abstraction Examples
|
||||
|
||||
Examples of building safe APIs on top of unsafe code.
|
||||
|
||||
## Example 1: Simple Wrapper with Bounds Check
|
||||
|
||||
```rust
|
||||
/// A slice wrapper that provides unchecked access internally
|
||||
/// but safe access externally.
|
||||
pub struct SafeSlice<'a, T> {
|
||||
ptr: *const T,
|
||||
len: usize,
|
||||
_marker: std::marker::PhantomData<&'a T>,
|
||||
}
|
||||
|
||||
impl<'a, T> SafeSlice<'a, T> {
|
||||
/// Creates a SafeSlice from a regular slice.
|
||||
pub fn new(slice: &'a [T]) -> Self {
|
||||
Self {
|
||||
ptr: slice.as_ptr(),
|
||||
len: slice.len(),
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe get - returns Option.
|
||||
pub fn get(&self, index: usize) -> Option<&T> {
|
||||
if index < self.len {
|
||||
// SAFETY: We just verified index < len
|
||||
Some(unsafe { &*self.ptr.add(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Unsafe get - caller must ensure bounds.
|
||||
///
|
||||
/// # Safety
|
||||
/// `index` must be less than `self.len()`.
|
||||
pub unsafe fn get_unchecked(&self, index: usize) -> &T {
|
||||
debug_assert!(index < self.len);
|
||||
&*self.ptr.add(index)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example 2: Resource Wrapper with Drop
|
||||
|
||||
```rust
|
||||
use std::ptr::NonNull;
|
||||
|
||||
/// Safe wrapper around a C-allocated buffer.
|
||||
pub struct CBuffer {
|
||||
ptr: NonNull<u8>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn c_alloc(size: usize) -> *mut u8;
|
||||
fn c_free(ptr: *mut u8);
|
||||
}
|
||||
|
||||
impl CBuffer {
|
||||
/// Creates a new buffer. Returns None if allocation fails.
|
||||
pub fn new(size: usize) -> Option<Self> {
|
||||
let ptr = unsafe { c_alloc(size) };
|
||||
NonNull::new(ptr).map(|ptr| Self { ptr, len: size })
|
||||
}
|
||||
|
||||
/// Returns a slice view of the buffer.
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
// SAFETY: ptr is valid for len bytes (from c_alloc contract)
|
||||
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
|
||||
}
|
||||
|
||||
/// Returns a mutable slice view.
|
||||
pub fn as_mut_slice(&mut self) -> &mut [u8] {
|
||||
// SAFETY: We have &mut self, so exclusive access
|
||||
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CBuffer {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: ptr was allocated by c_alloc and not yet freed
|
||||
unsafe { c_free(self.ptr.as_ptr()); }
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent double-free
|
||||
impl !Clone for CBuffer {}
|
||||
|
||||
// Safe to send between threads (assuming c_alloc is thread-safe)
|
||||
unsafe impl Send for CBuffer {}
|
||||
```
|
||||
|
||||
## Example 3: Interior Mutability with UnsafeCell
|
||||
|
||||
```rust
|
||||
use std::cell::UnsafeCell;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// A simple spinlock demonstrating safe abstraction over UnsafeCell.
|
||||
pub struct SpinLock<T> {
|
||||
locked: AtomicBool,
|
||||
data: UnsafeCell<T>,
|
||||
}
|
||||
|
||||
pub struct SpinLockGuard<'a, T> {
|
||||
lock: &'a SpinLock<T>,
|
||||
}
|
||||
|
||||
impl<T> SpinLock<T> {
|
||||
pub const fn new(data: T) -> Self {
|
||||
Self {
|
||||
locked: AtomicBool::new(false),
|
||||
data: UnsafeCell::new(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lock(&self) -> SpinLockGuard<'_, T> {
|
||||
// Spin until we acquire the lock
|
||||
while self.locked.compare_exchange_weak(
|
||||
false,
|
||||
true,
|
||||
Ordering::Acquire,
|
||||
Ordering::Relaxed,
|
||||
).is_err() {
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
SpinLockGuard { lock: self }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for SpinLockGuard<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T {
|
||||
// SAFETY: We hold the lock, so we have exclusive access
|
||||
unsafe { &*self.lock.data.get() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::DerefMut for SpinLockGuard<'_, T> {
|
||||
fn deref_mut(&mut self) -> &mut T {
|
||||
// SAFETY: We hold the lock, so we have exclusive access
|
||||
unsafe { &mut *self.lock.data.get() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for SpinLockGuard<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
self.lock.locked.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: The lock ensures only one thread accesses data at a time
|
||||
unsafe impl<T: Send> Sync for SpinLock<T> {}
|
||||
unsafe impl<T: Send> Send for SpinLock<T> {}
|
||||
```
|
||||
|
||||
## Example 4: Iterator with Lifetime Tracking
|
||||
|
||||
```rust
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// An iterator over raw pointer range with proper lifetime tracking.
|
||||
pub struct PtrIter<'a, T> {
|
||||
current: *const T,
|
||||
end: *const T,
|
||||
_marker: PhantomData<&'a T>,
|
||||
}
|
||||
|
||||
impl<'a, T> PtrIter<'a, T> {
|
||||
/// Creates an iterator from a slice.
|
||||
pub fn new(slice: &'a [T]) -> Self {
|
||||
let ptr = slice.as_ptr();
|
||||
Self {
|
||||
current: ptr,
|
||||
// SAFETY: Adding len to slice pointer is always valid
|
||||
end: unsafe { ptr.add(slice.len()) },
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for PtrIter<'a, T> {
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current == self.end {
|
||||
None
|
||||
} else {
|
||||
// SAFETY:
|
||||
// - current < end (checked above)
|
||||
// - PhantomData<&'a T> ensures the data lives for 'a
|
||||
let item = unsafe { &*self.current };
|
||||
self.current = unsafe { self.current.add(1) };
|
||||
Some(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example 5: Builder Pattern with Delayed Initialization
|
||||
|
||||
```rust
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
/// A builder that collects exactly N items, then produces an array.
|
||||
pub struct ArrayBuilder<T, const N: usize> {
|
||||
data: [MaybeUninit<T>; N],
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<T, const N: usize> ArrayBuilder<T, N> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
// SAFETY: MaybeUninit doesn't require initialization
|
||||
data: unsafe { MaybeUninit::uninit().assume_init() },
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, value: T) -> Result<(), T> {
|
||||
if self.count >= N {
|
||||
return Err(value);
|
||||
}
|
||||
self.data[self.count].write(value);
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build(self) -> Option<[T; N]> {
|
||||
if self.count != N {
|
||||
return None;
|
||||
}
|
||||
|
||||
// SAFETY: All N elements have been initialized
|
||||
let result = unsafe {
|
||||
// Prevent drop of self.data (we're moving out)
|
||||
let data = std::ptr::read(&self.data);
|
||||
std::mem::forget(self);
|
||||
// Transmute MaybeUninit array to initialized array
|
||||
std::mem::transmute_copy::<[MaybeUninit<T>; N], [T; N]>(&data)
|
||||
};
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> Drop for ArrayBuilder<T, N> {
|
||||
fn drop(&mut self) {
|
||||
// Drop only initialized elements
|
||||
for i in 0..self.count {
|
||||
// SAFETY: Elements 0..count are initialized
|
||||
unsafe { self.data[i].assume_init_drop(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
1. **Encapsulation**: Hide unsafe behind safe public API
|
||||
2. **Invariant maintenance**: Use private fields to maintain invariants
|
||||
3. **PhantomData**: Track lifetimes and ownership for pointers
|
||||
4. **RAII**: Use Drop for cleanup
|
||||
5. **Type state**: Use types to encode valid states
|
||||
17
skills/unsafe-checker/metadata.json
Normal file
17
skills/unsafe-checker/metadata.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "unsafe-checker",
|
||||
"version": "1.0.0",
|
||||
"description": "Unsafe Rust code review and safety abstraction checker",
|
||||
"source": "https://github.com/Rust-Coding-Guidelines/rust-coding-guidelines-zh",
|
||||
"lastUpdated": "2026-01-16",
|
||||
"ruleCount": 47,
|
||||
"sections": [
|
||||
{ "id": "general", "name": "General Principles", "count": 3 },
|
||||
{ "id": "safety", "name": "Safety Abstraction", "count": 11 },
|
||||
{ "id": "ptr", "name": "Raw Pointers", "count": 6 },
|
||||
{ "id": "union", "name": "Union", "count": 2 },
|
||||
{ "id": "mem", "name": "Memory Layout", "count": 6 },
|
||||
{ "id": "ffi", "name": "FFI", "count": 18 },
|
||||
{ "id": "io", "name": "I/O Safety", "count": 1 }
|
||||
]
|
||||
}
|
||||
77
skills/unsafe-checker/rules/_sections.md
Normal file
77
skills/unsafe-checker/rules/_sections.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Unsafe Checker - Section Definitions
|
||||
|
||||
## Section Overview
|
||||
|
||||
| # | Section | Prefix | Level | Count | Impact |
|
||||
|---|---------|--------|-------|-------|--------|
|
||||
| 1 | General Principles | `general-` | CRITICAL | 3 | Foundational unsafe usage guidance |
|
||||
| 2 | Safety Abstraction | `safety-` | CRITICAL | 11 | Building sound safe APIs |
|
||||
| 3 | Raw Pointers | `ptr-` | HIGH | 6 | Pointer manipulation safety |
|
||||
| 4 | Union | `union-` | HIGH | 2 | Union type safety |
|
||||
| 5 | Memory Layout | `mem-` | HIGH | 6 | Data representation correctness |
|
||||
| 6 | FFI | `ffi-` | CRITICAL | 18 | C interoperability safety |
|
||||
| 7 | I/O Safety | `io-` | MEDIUM | 1 | Handle/resource safety |
|
||||
|
||||
## Section Details
|
||||
|
||||
### 1. General Principles (`general-`)
|
||||
|
||||
**Focus**: When and why to use unsafe
|
||||
|
||||
- P.UNS.01: Don't abuse unsafe to escape borrow checker
|
||||
- P.UNS.02: Don't use unsafe blindly for performance
|
||||
- G.UNS.01: Don't create aliases for "unsafe" named items
|
||||
|
||||
### 2. Safety Abstraction (`safety-`)
|
||||
|
||||
**Focus**: Building sound safe abstractions over unsafe code
|
||||
|
||||
Key invariants:
|
||||
- Panic safety
|
||||
- Memory initialization
|
||||
- Send/Sync correctness
|
||||
- API soundness
|
||||
|
||||
### 3. Raw Pointers (`ptr-`)
|
||||
|
||||
**Focus**: Safe pointer manipulation patterns
|
||||
|
||||
- Aliasing rules
|
||||
- Alignment requirements
|
||||
- Null/dangling prevention
|
||||
- Type casting
|
||||
|
||||
### 4. Union (`union-`)
|
||||
|
||||
**Focus**: Safe union usage (primarily for C interop)
|
||||
|
||||
- Initialization rules
|
||||
- Lifetime considerations
|
||||
- Type punning dangers
|
||||
|
||||
### 5. Memory Layout (`mem-`)
|
||||
|
||||
**Focus**: Correct data representation
|
||||
|
||||
- `#[repr(C)]` usage
|
||||
- Alignment and padding
|
||||
- Uninitialized memory
|
||||
- Cross-process memory
|
||||
|
||||
### 6. FFI (`ffi-`)
|
||||
|
||||
**Focus**: Safe C interoperability
|
||||
|
||||
Subcategories:
|
||||
- String handling (CString, CStr)
|
||||
- Type compatibility
|
||||
- Error handling across FFI
|
||||
- Thread safety
|
||||
- Resource management
|
||||
|
||||
### 7. I/O Safety (`io-`)
|
||||
|
||||
**Focus**: Handle and resource ownership
|
||||
|
||||
- Raw file descriptor safety
|
||||
- Handle validity guarantees
|
||||
53
skills/unsafe-checker/rules/_template.md
Normal file
53
skills/unsafe-checker/rules/_template.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Rule Template
|
||||
|
||||
Use this template for all unsafe-checker rules.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
---
|
||||
id: {prefix}-{number}
|
||||
original_id: P.UNS.XXX.YY or G.UNS.XXX.YY
|
||||
level: P|G
|
||||
impact: CRITICAL|HIGH|MEDIUM
|
||||
clippy: <clippy_lint_name> (if applicable)
|
||||
---
|
||||
|
||||
# {Rule Title}
|
||||
|
||||
## Summary
|
||||
|
||||
One-sentence description of what this rule requires.
|
||||
|
||||
## Rationale
|
||||
|
||||
Why this rule matters for safety/soundness.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Description of the anti-pattern
|
||||
<code that violates the rule>
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Description of the correct pattern
|
||||
<code that follows the rule>
|
||||
```
|
||||
|
||||
## Common Violations
|
||||
|
||||
1. Violation pattern 1
|
||||
2. Violation pattern 2
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Check item 1
|
||||
- [ ] Check item 2
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `{other-rule-id}`: Brief description
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user