red64-cli 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -338
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -13
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/types.d.ts +0 -2
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/HelpScreen.d.ts.map +1 -1
- package/dist/components/screens/HelpScreen.js +0 -2
- package/dist/components/screens/HelpScreen.js.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +5 -8
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +29 -8
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/components/screens/StatusScreen.d.ts.map +1 -1
- package/dist/components/screens/StatusScreen.js +16 -1
- package/dist/components/screens/StatusScreen.js.map +1 -1
- package/dist/services/AgentInvoker.d.ts.map +1 -1
- package/dist/services/AgentInvoker.js +76 -37
- package/dist/services/AgentInvoker.js.map +1 -1
- package/dist/services/ClaudeErrorDetector.d.ts +1 -1
- package/dist/services/ClaudeErrorDetector.d.ts.map +1 -1
- package/dist/services/ClaudeErrorDetector.js +1 -0
- package/dist/services/ClaudeErrorDetector.js.map +1 -1
- package/dist/services/ClaudeHealthCheck.d.ts +7 -0
- package/dist/services/ClaudeHealthCheck.d.ts.map +1 -1
- package/dist/services/ClaudeHealthCheck.js +76 -12
- package/dist/services/ClaudeHealthCheck.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/DockerRunner.js +1 -1
- package/dist/services/DockerRunner.js.map +1 -1
- package/dist/services/PhaseExecutor.d.ts.map +1 -1
- package/dist/services/PhaseExecutor.js +2 -1
- package/dist/services/PhaseExecutor.js.map +1 -1
- package/dist/services/TaskRunner.d.ts.map +1 -1
- package/dist/services/TaskRunner.js +2 -1
- package/dist/services/TaskRunner.js.map +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/stacks/c/code-quality.md +326 -0
- package/framework/stacks/c/coding-style.md +347 -0
- package/framework/stacks/c/conventions.md +513 -0
- package/framework/stacks/c/error-handling.md +350 -0
- package/framework/stacks/c/feedback.md +158 -0
- package/framework/stacks/c/memory-safety.md +408 -0
- package/framework/stacks/c/tech.md +122 -0
- package/framework/stacks/c/testing.md +472 -0
- package/framework/stacks/cpp/code-quality.md +282 -0
- package/framework/stacks/cpp/coding-style.md +363 -0
- package/framework/stacks/cpp/conventions.md +420 -0
- package/framework/stacks/cpp/error-handling.md +264 -0
- package/framework/stacks/cpp/feedback.md +104 -0
- package/framework/stacks/cpp/memory-safety.md +351 -0
- package/framework/stacks/cpp/tech.md +160 -0
- package/framework/stacks/cpp/testing.md +323 -0
- package/framework/stacks/java/code-quality.md +357 -0
- package/framework/stacks/java/coding-style.md +400 -0
- package/framework/stacks/java/conventions.md +437 -0
- package/framework/stacks/java/error-handling.md +408 -0
- package/framework/stacks/java/feedback.md +180 -0
- package/framework/stacks/java/tech.md +126 -0
- package/framework/stacks/java/testing.md +485 -0
- package/framework/stacks/javascript/async-patterns.md +216 -0
- package/framework/stacks/javascript/code-quality.md +182 -0
- package/framework/stacks/javascript/coding-style.md +293 -0
- package/framework/stacks/javascript/conventions.md +268 -0
- package/framework/stacks/javascript/error-handling.md +216 -0
- package/framework/stacks/javascript/feedback.md +80 -0
- package/framework/stacks/javascript/tech.md +114 -0
- package/framework/stacks/javascript/testing.md +209 -0
- package/framework/stacks/loco/code-quality.md +156 -0
- package/framework/stacks/loco/coding-style.md +247 -0
- package/framework/stacks/loco/error-handling.md +225 -0
- package/framework/stacks/loco/feedback.md +35 -0
- package/framework/stacks/loco/loco.md +342 -0
- package/framework/stacks/loco/structure.md +193 -0
- package/framework/stacks/loco/tech.md +129 -0
- package/framework/stacks/loco/testing.md +211 -0
- package/framework/stacks/rust/code-quality.md +370 -0
- package/framework/stacks/rust/coding-style.md +475 -0
- package/framework/stacks/rust/conventions.md +430 -0
- package/framework/stacks/rust/error-handling.md +399 -0
- package/framework/stacks/rust/feedback.md +152 -0
- package/framework/stacks/rust/memory-safety.md +398 -0
- package/framework/stacks/rust/tech.md +121 -0
- package/framework/stacks/rust/testing.md +528 -0
- package/package.json +14 -2
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# Error Handling Patterns
|
|
2
|
+
|
|
3
|
+
Structured error handling for Rust applications with `thiserror` for libraries, `anyhow` for applications, and the `?` operator everywhere.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **`Result<T, E>` everywhere**: No panics in production code, no silent failures
|
|
10
|
+
- **Typed errors for libraries**: Use `thiserror` so callers can match on variants
|
|
11
|
+
- **Ergonomic errors for applications**: Use `anyhow` at the application boundary
|
|
12
|
+
- **Context propagation**: Always add context when crossing abstraction boundaries
|
|
13
|
+
- **Fail fast**: Validate inputs early, return errors immediately with `?`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Error Strategy: `thiserror` vs `anyhow`
|
|
18
|
+
|
|
19
|
+
| Layer | Crate | Purpose |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Library / shared crates | `thiserror` | Custom error enums callers can match on |
|
|
22
|
+
| Application / binary | `anyhow` | Ergonomic error propagation with context |
|
|
23
|
+
| HTTP boundary (Axum) | `thiserror` + `IntoResponse` | Map domain errors to HTTP status codes |
|
|
24
|
+
|
|
25
|
+
```rust
|
|
26
|
+
// GOOD: thiserror for domain/library errors
|
|
27
|
+
#[derive(Debug, thiserror::Error)]
|
|
28
|
+
pub enum UserError {
|
|
29
|
+
#[error("User not found: {0}")]
|
|
30
|
+
NotFound(i64),
|
|
31
|
+
|
|
32
|
+
#[error("Email already registered: {0}")]
|
|
33
|
+
DuplicateEmail(String),
|
|
34
|
+
|
|
35
|
+
#[error("Invalid email format: {0}")]
|
|
36
|
+
InvalidEmail(String),
|
|
37
|
+
|
|
38
|
+
#[error(transparent)]
|
|
39
|
+
Database(#[from] sqlx::Error),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// GOOD: anyhow for application-level code
|
|
43
|
+
use anyhow::{Context, Result};
|
|
44
|
+
|
|
45
|
+
async fn run_migration(pool: &PgPool) -> Result<()> {
|
|
46
|
+
sqlx::migrate!("./migrations")
|
|
47
|
+
.run(pool)
|
|
48
|
+
.await
|
|
49
|
+
.context("Failed to run database migrations")?;
|
|
50
|
+
Ok(())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// BAD: Using anyhow in a library crate
|
|
54
|
+
pub fn parse_config(path: &str) -> anyhow::Result<Config> {
|
|
55
|
+
// Callers cannot match on specific error types
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// BAD: Using thiserror for top-level CLI error handling
|
|
59
|
+
fn main() -> Result<(), AppError> {
|
|
60
|
+
// Unnecessarily verbose for a binary
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Custom Error Enums with `thiserror`
|
|
67
|
+
|
|
68
|
+
### Domain Errors
|
|
69
|
+
|
|
70
|
+
```rust
|
|
71
|
+
use thiserror::Error;
|
|
72
|
+
|
|
73
|
+
#[derive(Debug, Error)]
|
|
74
|
+
pub enum AppError {
|
|
75
|
+
#[error("Resource not found: {resource} ({id})")]
|
|
76
|
+
NotFound {
|
|
77
|
+
resource: &'static str,
|
|
78
|
+
id: String,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
#[error("Conflict: {0}")]
|
|
82
|
+
Conflict(String),
|
|
83
|
+
|
|
84
|
+
#[error("Validation failed: {0}")]
|
|
85
|
+
Validation(String),
|
|
86
|
+
|
|
87
|
+
#[error("Authentication required")]
|
|
88
|
+
Unauthenticated,
|
|
89
|
+
|
|
90
|
+
#[error("Insufficient permissions")]
|
|
91
|
+
Forbidden,
|
|
92
|
+
|
|
93
|
+
#[error("External service error ({service}): {message}")]
|
|
94
|
+
ExternalService {
|
|
95
|
+
service: String,
|
|
96
|
+
message: String,
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
#[error(transparent)]
|
|
100
|
+
Database(#[from] sqlx::Error),
|
|
101
|
+
|
|
102
|
+
#[error(transparent)]
|
|
103
|
+
Internal(#[from] anyhow::Error),
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Axum `IntoResponse` Integration
|
|
108
|
+
|
|
109
|
+
```rust
|
|
110
|
+
use axum::http::StatusCode;
|
|
111
|
+
use axum::response::{IntoResponse, Response};
|
|
112
|
+
use axum::Json;
|
|
113
|
+
use serde_json::json;
|
|
114
|
+
|
|
115
|
+
impl IntoResponse for AppError {
|
|
116
|
+
fn into_response(self) -> Response {
|
|
117
|
+
let (status, code, message) = match &self {
|
|
118
|
+
AppError::NotFound { resource, id } => (
|
|
119
|
+
StatusCode::NOT_FOUND,
|
|
120
|
+
"NOT_FOUND",
|
|
121
|
+
format!("{resource} not found: {id}"),
|
|
122
|
+
),
|
|
123
|
+
AppError::Conflict(msg) => (
|
|
124
|
+
StatusCode::CONFLICT,
|
|
125
|
+
"CONFLICT",
|
|
126
|
+
msg.clone(),
|
|
127
|
+
),
|
|
128
|
+
AppError::Validation(msg) => (
|
|
129
|
+
StatusCode::UNPROCESSABLE_ENTITY,
|
|
130
|
+
"VALIDATION_ERROR",
|
|
131
|
+
msg.clone(),
|
|
132
|
+
),
|
|
133
|
+
AppError::Unauthenticated => (
|
|
134
|
+
StatusCode::UNAUTHORIZED,
|
|
135
|
+
"UNAUTHENTICATED",
|
|
136
|
+
"Authentication required".to_string(),
|
|
137
|
+
),
|
|
138
|
+
AppError::Forbidden => (
|
|
139
|
+
StatusCode::FORBIDDEN,
|
|
140
|
+
"FORBIDDEN",
|
|
141
|
+
"Insufficient permissions".to_string(),
|
|
142
|
+
),
|
|
143
|
+
AppError::ExternalService { service, message } => (
|
|
144
|
+
StatusCode::BAD_GATEWAY,
|
|
145
|
+
"EXTERNAL_SERVICE_ERROR",
|
|
146
|
+
format!("External service error ({service}): {message}"),
|
|
147
|
+
),
|
|
148
|
+
AppError::Database(e) => {
|
|
149
|
+
tracing::error!(error = %e, "Database error");
|
|
150
|
+
(
|
|
151
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
152
|
+
"INTERNAL_ERROR",
|
|
153
|
+
"An internal error occurred".to_string(),
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
AppError::Internal(e) => {
|
|
157
|
+
tracing::error!(error = %e, "Internal error");
|
|
158
|
+
(
|
|
159
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
160
|
+
"INTERNAL_ERROR",
|
|
161
|
+
"An internal error occurred".to_string(),
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
let body = Json(json!({
|
|
167
|
+
"error": {
|
|
168
|
+
"code": code,
|
|
169
|
+
"message": message,
|
|
170
|
+
}
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
(status, body).into_response()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## The `?` Operator
|
|
181
|
+
|
|
182
|
+
### Propagation with `?`
|
|
183
|
+
|
|
184
|
+
```rust
|
|
185
|
+
// GOOD: Clean propagation with ?
|
|
186
|
+
async fn get_user_posts(pool: &PgPool, user_id: i64) -> Result<Vec<Post>, AppError> {
|
|
187
|
+
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
|
|
188
|
+
.fetch_optional(pool)
|
|
189
|
+
.await?
|
|
190
|
+
.ok_or(AppError::NotFound { resource: "User", id: user_id.to_string() })?;
|
|
191
|
+
|
|
192
|
+
let posts = sqlx::query_as!(Post, "SELECT * FROM posts WHERE user_id = $1", user.id)
|
|
193
|
+
.fetch_all(pool)
|
|
194
|
+
.await?;
|
|
195
|
+
|
|
196
|
+
Ok(posts)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// BAD: Manual matching instead of ?
|
|
200
|
+
async fn get_user_posts(pool: &PgPool, user_id: i64) -> Result<Vec<Post>, AppError> {
|
|
201
|
+
let user = match sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
|
|
202
|
+
.fetch_optional(pool)
|
|
203
|
+
.await
|
|
204
|
+
{
|
|
205
|
+
Ok(Some(u)) => u,
|
|
206
|
+
Ok(None) => return Err(AppError::NotFound { resource: "User", id: user_id.to_string() }),
|
|
207
|
+
Err(e) => return Err(AppError::Database(e)),
|
|
208
|
+
};
|
|
209
|
+
// ... more nested matches
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Error Context with `anyhow`
|
|
216
|
+
|
|
217
|
+
### Adding Context
|
|
218
|
+
|
|
219
|
+
```rust
|
|
220
|
+
use anyhow::{Context, Result};
|
|
221
|
+
|
|
222
|
+
// GOOD: Context explains what was happening when the error occurred
|
|
223
|
+
async fn load_config(path: &str) -> Result<Config> {
|
|
224
|
+
let content = std::fs::read_to_string(path)
|
|
225
|
+
.with_context(|| format!("Failed to read config file: {path}"))?;
|
|
226
|
+
|
|
227
|
+
let config: Config = toml::from_str(&content)
|
|
228
|
+
.with_context(|| format!("Failed to parse config file: {path}"))?;
|
|
229
|
+
|
|
230
|
+
Ok(config)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// BAD: No context -- error message is just "No such file or directory"
|
|
234
|
+
async fn load_config(path: &str) -> Result<Config> {
|
|
235
|
+
let content = std::fs::read_to_string(path)?;
|
|
236
|
+
let config: Config = toml::from_str(&content)?;
|
|
237
|
+
Ok(config)
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Context in Async Chains
|
|
242
|
+
|
|
243
|
+
```rust
|
|
244
|
+
// GOOD: Context on each fallible step
|
|
245
|
+
async fn sync_user(pool: &PgPool, external_id: &str) -> Result<User> {
|
|
246
|
+
let external_user = fetch_external_user(external_id)
|
|
247
|
+
.await
|
|
248
|
+
.context("Failed to fetch user from external service")?;
|
|
249
|
+
|
|
250
|
+
let user = upsert_user(pool, &external_user)
|
|
251
|
+
.await
|
|
252
|
+
.context("Failed to upsert user in database")?;
|
|
253
|
+
|
|
254
|
+
send_sync_notification(&user)
|
|
255
|
+
.await
|
|
256
|
+
.context("Failed to send sync notification")?;
|
|
257
|
+
|
|
258
|
+
Ok(user)
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## `From` Implementations
|
|
265
|
+
|
|
266
|
+
### Automatic with `thiserror`
|
|
267
|
+
|
|
268
|
+
```rust
|
|
269
|
+
#[derive(Debug, thiserror::Error)]
|
|
270
|
+
pub enum AppError {
|
|
271
|
+
// #[from] auto-generates From<sqlx::Error> for AppError
|
|
272
|
+
#[error(transparent)]
|
|
273
|
+
Database(#[from] sqlx::Error),
|
|
274
|
+
|
|
275
|
+
// #[from] auto-generates From<serde_json::Error> for AppError
|
|
276
|
+
#[error("Serialization error: {0}")]
|
|
277
|
+
Serialization(#[from] serde_json::Error),
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Now ? works automatically:
|
|
281
|
+
async fn get_user(pool: &PgPool, id: i64) -> Result<User, AppError> {
|
|
282
|
+
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
|
|
283
|
+
.fetch_one(pool)
|
|
284
|
+
.await?; // sqlx::Error -> AppError via From
|
|
285
|
+
Ok(user)
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Manual `From` When Needed
|
|
290
|
+
|
|
291
|
+
```rust
|
|
292
|
+
// When you need custom conversion logic
|
|
293
|
+
impl From<reqwest::Error> for AppError {
|
|
294
|
+
fn from(err: reqwest::Error) -> Self {
|
|
295
|
+
if err.is_timeout() {
|
|
296
|
+
AppError::ExternalService {
|
|
297
|
+
service: "http_client".to_string(),
|
|
298
|
+
message: "Request timed out".to_string(),
|
|
299
|
+
}
|
|
300
|
+
} else if err.is_connect() {
|
|
301
|
+
AppError::ExternalService {
|
|
302
|
+
service: "http_client".to_string(),
|
|
303
|
+
message: "Connection failed".to_string(),
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
AppError::Internal(err.into())
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## No `unwrap()` in Production
|
|
315
|
+
|
|
316
|
+
### Alternatives to `unwrap()`
|
|
317
|
+
|
|
318
|
+
```rust
|
|
319
|
+
// GOOD: Propagate with ?
|
|
320
|
+
let user = get_user(pool, id).await?;
|
|
321
|
+
|
|
322
|
+
// GOOD: Provide default
|
|
323
|
+
let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());
|
|
324
|
+
|
|
325
|
+
// GOOD: Convert to Result with ok_or
|
|
326
|
+
let user = users.get(0).ok_or(AppError::NotFound {
|
|
327
|
+
resource: "User",
|
|
328
|
+
id: "first".to_string(),
|
|
329
|
+
})?;
|
|
330
|
+
|
|
331
|
+
// GOOD: expect() in tests and initialization (with justification)
|
|
332
|
+
let pool = PgPool::connect(&database_url)
|
|
333
|
+
.await
|
|
334
|
+
.expect("DATABASE_URL must point to a valid Postgres instance");
|
|
335
|
+
|
|
336
|
+
// BAD: unwrap in production code
|
|
337
|
+
let user = get_user(pool, id).await.unwrap(); // PANICS if None/Err
|
|
338
|
+
let config = std::fs::read_to_string("config.toml").unwrap(); // PANICS if file missing
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### When `expect()` Is Acceptable
|
|
342
|
+
|
|
343
|
+
| Context | Acceptable? | Reason |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| Tests | Yes | Test failure is the correct behavior |
|
|
346
|
+
| `main()` initialization | Yes | Cannot proceed without config/db |
|
|
347
|
+
| After validation guard | Cautiously | Document why it cannot fail |
|
|
348
|
+
| Production request handling | Never | Panics crash the server |
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Error Logging with `tracing`
|
|
353
|
+
|
|
354
|
+
### Log at the Boundary
|
|
355
|
+
|
|
356
|
+
```rust
|
|
357
|
+
// GOOD: Log once at the boundary, not at every layer
|
|
358
|
+
async fn create_user_handler(
|
|
359
|
+
State(pool): State<PgPool>,
|
|
360
|
+
Json(data): Json<CreateUserRequest>,
|
|
361
|
+
) -> Result<Json<UserResponse>, AppError> {
|
|
362
|
+
let user = create_user(&pool, &data).await.map_err(|e| {
|
|
363
|
+
tracing::error!(
|
|
364
|
+
error = %e,
|
|
365
|
+
email = %data.email,
|
|
366
|
+
"Failed to create user"
|
|
367
|
+
);
|
|
368
|
+
e
|
|
369
|
+
})?;
|
|
370
|
+
Ok(Json(UserResponse::from(user)))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// BAD: Logging at every layer
|
|
374
|
+
async fn create_user(pool: &PgPool, data: &CreateUserRequest) -> Result<User, AppError> {
|
|
375
|
+
let hash = hash_password(&data.password).map_err(|e| {
|
|
376
|
+
tracing::error!("hash failed: {}", e); // Too noisy
|
|
377
|
+
e
|
|
378
|
+
})?;
|
|
379
|
+
// ...
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Anti-Patterns
|
|
386
|
+
|
|
387
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
388
|
+
|---|---|---|
|
|
389
|
+
| `.unwrap()` in handlers | Panics crash the server | Use `?` with proper error types |
|
|
390
|
+
| `Box<dyn Error>` in libraries | Callers cannot match on variants | Use `thiserror` enums |
|
|
391
|
+
| Stringly-typed errors | No programmatic handling | Use typed error enums |
|
|
392
|
+
| Catching and ignoring errors | Hides bugs | Log and propagate or handle explicitly |
|
|
393
|
+
| `anyhow` in library crates | Callers lose type information | Reserve for application code |
|
|
394
|
+
| Error messages with internal details | Security risk in API responses | Return safe messages, log details |
|
|
395
|
+
| Re-implementing `From` that `thiserror` provides | Boilerplate | Use `#[from]` attribute |
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
_Errors are values in Rust. Classify them with `thiserror`, propagate them with `?`, enrich them with `.context()`, and handle them at the boundary._
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Feedback Configuration
|
|
2
|
+
|
|
3
|
+
Project-specific commands for automated feedback during Rust implementation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Test Commands
|
|
8
|
+
|
|
9
|
+
Commands to run tests during implementation. The agent will use these to verify code changes.
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
# Primary test command (REQUIRED)
|
|
13
|
+
test: cargo nextest run
|
|
14
|
+
|
|
15
|
+
# Test with standard cargo test (fallback)
|
|
16
|
+
test_fallback: cargo test
|
|
17
|
+
|
|
18
|
+
# Test with coverage report
|
|
19
|
+
test_coverage: cargo llvm-cov nextest --fail-under-lines 80
|
|
20
|
+
|
|
21
|
+
# Run specific test by name
|
|
22
|
+
test_pattern: cargo nextest run --filter-expr 'test({pattern})'
|
|
23
|
+
|
|
24
|
+
# Run tests for specific crate in workspace
|
|
25
|
+
test_crate: cargo nextest run -p {crate}
|
|
26
|
+
|
|
27
|
+
# Run doctests (nextest does not run these)
|
|
28
|
+
test_doc: cargo test --doc
|
|
29
|
+
|
|
30
|
+
# Run only integration tests
|
|
31
|
+
test_integration: cargo nextest run --filter-expr 'kind(test)'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Linting Commands
|
|
37
|
+
|
|
38
|
+
Commands for code quality checks.
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
# Primary lint command (Clippy with denied warnings)
|
|
42
|
+
lint: cargo clippy --all-targets --all-features -- -D warnings
|
|
43
|
+
|
|
44
|
+
# Lint specific crate
|
|
45
|
+
lint_crate: cargo clippy -p {crate} -- -D warnings
|
|
46
|
+
|
|
47
|
+
# Auto-fix Clippy suggestions
|
|
48
|
+
lint_fix: cargo clippy --fix --allow-dirty
|
|
49
|
+
|
|
50
|
+
# Format check
|
|
51
|
+
format_check: cargo fmt -- --check
|
|
52
|
+
|
|
53
|
+
# Format fix
|
|
54
|
+
format_fix: cargo fmt
|
|
55
|
+
|
|
56
|
+
# Security audit
|
|
57
|
+
audit: cargo audit
|
|
58
|
+
|
|
59
|
+
# Dependency policy check (licenses, advisories, bans)
|
|
60
|
+
deny: cargo deny check
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Build Commands
|
|
66
|
+
|
|
67
|
+
Commands for building the project.
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
# Debug build
|
|
71
|
+
build: cargo build
|
|
72
|
+
|
|
73
|
+
# Release build
|
|
74
|
+
build_release: cargo build --release
|
|
75
|
+
|
|
76
|
+
# Check without building (faster feedback)
|
|
77
|
+
check: cargo check --all-targets --all-features
|
|
78
|
+
|
|
79
|
+
# Build documentation
|
|
80
|
+
doc: cargo doc --no-deps
|
|
81
|
+
|
|
82
|
+
# Clean build artifacts
|
|
83
|
+
clean: cargo clean
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Development Server
|
|
89
|
+
|
|
90
|
+
Commands for starting the development server (Axum application).
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
# Start dev server
|
|
94
|
+
dev_server: cargo run
|
|
95
|
+
|
|
96
|
+
# Start dev server with auto-reload (requires cargo-watch)
|
|
97
|
+
dev_watch: cargo watch -x run
|
|
98
|
+
|
|
99
|
+
# Dev server port
|
|
100
|
+
dev_port: 8080
|
|
101
|
+
|
|
102
|
+
# Dev server base URL
|
|
103
|
+
dev_url: http://localhost:8080
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Database Commands
|
|
109
|
+
|
|
110
|
+
Commands for database operations with SQLx.
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
# Run migrations
|
|
114
|
+
migrate: sqlx migrate run
|
|
115
|
+
|
|
116
|
+
# Create new migration
|
|
117
|
+
migrate_create: sqlx migrate add {name}
|
|
118
|
+
|
|
119
|
+
# Prepare offline query data (for CI without database)
|
|
120
|
+
sqlx_prepare: cargo sqlx prepare
|
|
121
|
+
|
|
122
|
+
# Check offline query data is up to date
|
|
123
|
+
sqlx_check: cargo sqlx prepare --check
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## CI Pipeline Commands
|
|
129
|
+
|
|
130
|
+
Full quality check sequence for continuous integration.
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
# Complete CI check (run in order)
|
|
134
|
+
ci_format: cargo fmt -- --check
|
|
135
|
+
ci_lint: cargo clippy --all-targets --all-features -- -D warnings
|
|
136
|
+
ci_test: cargo nextest run --all-features
|
|
137
|
+
ci_doctest: cargo test --doc
|
|
138
|
+
ci_audit: cargo audit
|
|
139
|
+
ci_deny: cargo deny check
|
|
140
|
+
ci_doc: cargo doc --no-deps
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Notes
|
|
146
|
+
|
|
147
|
+
- Uses `cargo-nextest` as the primary test runner for faster parallel execution
|
|
148
|
+
- Doctests must be run separately with `cargo test --doc` (nextest does not support them)
|
|
149
|
+
- `cargo clippy` with `-D warnings` treats all warnings as errors in CI
|
|
150
|
+
- `cargo audit` checks against the RustSec advisory database
|
|
151
|
+
- `cargo deny` enforces license compliance, advisory checks, and duplicate detection
|
|
152
|
+
- SQLx offline mode (`cargo sqlx prepare`) enables CI builds without a live database
|