Add: windows mvp - transparent bugs not fixed

This commit is contained in:
DaZuo0122
2026-02-12 22:58:33 +08:00
commit 61825f647d
147 changed files with 28498 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target
/dist
/issues/screenshots

112
AGENTS.md Normal file
View 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

File diff suppressed because it is too large Load Diff

35
Cargo.toml Normal file
View 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"] }

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

View 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"

View 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());
}
}

View 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",
] }

File diff suppressed because it is too large Load Diff

View 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"

View 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);
}
}

View 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",
] }

View 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(())
})
}
}
}

View 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

View 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\"");
}
}

View 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"

View 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
View 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
View 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
View 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.

View 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 doesnt 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, doesnt 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 dont 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).

View 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
View 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
View 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.

View 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.

View 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
View 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
View 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.

View 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
View 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
View 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
View 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
View 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())

View 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.

View 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.

View 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`.

View 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"]
}
]
}
```

View 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
```

View 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 |

View 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
View 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 |

View 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 |

View 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 |

View 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
View 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
View 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
View 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 |

View 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 |

View 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

View 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
}
```

View 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
```

View 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
```

View 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 |

View 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 |

View 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 |

View 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 |

View 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 |

View 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"));
}
}
```

View 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")
}
```

View 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-* |

View 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

View 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..."),
}
}
```

View 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(),
)
}
```

View 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
View 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-* |

View 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-* |

View 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

View 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 |

View 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 |

View 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-* |

View 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 |

View 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?"

View 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 |

View 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 |

View 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 交易系统报 E0382trade 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.

View 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 |

View 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
View 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 |

View 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 |

View 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.**

View 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
View 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 |

View 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
```

View 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)
```

View 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

View 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 |

View 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 |

View 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 |

View 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

View 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.

View 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
```

View 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 |

View 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?)
```

View 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

View 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

View 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 }
]
}

View 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

View 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