guardian-framework 0.1.10 → 0.1.11

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/dist/cli.js CHANGED
@@ -1335,7 +1335,7 @@ __export(exports_package, {
1335
1335
  bin: () => bin,
1336
1336
  author: () => author
1337
1337
  });
1338
- var name = "guardian-framework", version = "0.1.10", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1338
+ var name = "guardian-framework", version = "0.1.11", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1339
1339
  var init_package = __esm(() => {
1340
1340
  exports = {
1341
1341
  ".": {
package/dist/exports.js CHANGED
@@ -995,7 +995,7 @@ __export(exports_package, {
995
995
  bin: () => bin,
996
996
  author: () => author
997
997
  });
998
- var name = "guardian-framework", version = "0.1.10", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
998
+ var name = "guardian-framework", version = "0.1.11", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
999
999
  var init_package = __esm(() => {
1000
1000
  exports = {
1001
1001
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -85,6 +85,7 @@ DO NOT EDIT DIRECTLY - Modify source in .pi/
85
85
  │ └── output-formats.md # Report templates
86
86
 
87
87
  ├── skills/
88
+ │ ├── rust-enterprise-codegen.md
88
89
  │ ├── agents/
89
90
  │ │ ├── architecture-coordinator.md
90
91
  │ │ ├── architecture-validator.md
@@ -415,6 +416,7 @@ When running `guardian-framework generate`, `.pi/` files are transformed:
415
416
  | `AGENTS.md` | `.github/copilot-instructions.md` | YAML frontmatter + canonical header |
416
417
  | `architecture/modules/*.md` | `.claude/architecture/*.md` | Direct copy + canonical header |
417
418
  | `architecture/CHANGELOG.md` | `.claude/architecture/CHANGELOG.md` | Direct copy |
419
+ | `skills/rust-enterprise-codegen.md` | `.pi/skills/rust-enterprise-codegen.md` | Direct copy — full reference patterns |
418
420
  | `skills/agents/*.md` | `.claude/agents/*.md`, `.github/agents/*.agent.md` | Direct copy + YAML frontmatter |
419
421
  | `skills/validators/*.md` | `.opencode/prompts/*.txt` | Convert to .txt, compress |
420
422
  | `context/*.md` | `.claude/context/*.md`, `.opencode/context/*.md` | Direct copy + canonical header |
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-07-03T04:04:20Z",
2
+ "timestamp": "2026-07-03T04:44:32Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {
@@ -8,7 +8,7 @@
8
8
  "failed": 1,
9
9
  "skipped": 12
10
10
  },
11
- "duration_seconds": 0,
11
+ "duration_seconds": 1,
12
12
  "results": [
13
13
  {
14
14
  "name": "check_mr_traceability.sh",
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: rust-codegen
3
+ description: Minimal skill for Rust code generation with DDD + Clean Architecture. References full patterns on demand — never loads inline. Use when implementing Rust modules following enterprise patterns.
4
+ model: inherit
5
+ tools: [Read, Grep]
6
+ ---
7
+
8
+ # Rust Code Generation — DDD + Clean Architecture Patterns
9
+
10
+ > Do NOT load the full reference document. Read specific sections below when needed.
11
+ > Full reference: `.pi/skills/rust-enterprise-codegen.md`
12
+
13
+ ## Quick Reference
14
+
15
+ When implementing, read ONLY the section you need:
16
+
17
+ | When you need... | Read this section from the reference |
18
+ |-----------------|--------------------------------------|
19
+ | Module structure | Section 1 — Clean Architecture 4-layer layout + header template + dependency direction |
20
+ | Aggregate root | Section 2 — DDD aggregate root with command methods, entity encapsulation |
21
+ | Value objects | Section 2 — Immutable value objects with self-validation |
22
+ | Repository pattern | Section 2 — Interface + implementation separation, collection semantics |
23
+ | Domain events | Section 2 (tactical) + Section 8 — Tagged union, serde, correlation IDs |
24
+ | Error types | Section 3 — thiserror enums, root error aggregation, is_retriable(), #[source] |
25
+ | Secret handling | Section 4 — Secret value object with redacted Display |
26
+ | State machines | Section 5 — Typed enum with is_terminal(), transition methods, tracking entity |
27
+ | RAII guards | Section 6 — Budget reservation with Drop auto-release |
28
+ | Async patterns | Section 7 — JoinSet, CancellationToken, select!, bounded channels |
29
+ | Configuration | Section 9 — Multi-source merging (flags > env > file > defaults) |
30
+ | Atomic file ops | Section 10 — write-tmp → fsync → rename pattern |
31
+ | Complex builders | Section 11 — Builder pattern + named constructors |
32
+ | Retry/backoff | Section 12 — BackoffStrategy enum |
33
+ | EventBus | Section 13 — broadcast channel, in-memory log, drain() |
34
+ | Tests | Section 14 — AAA pattern, serde round-trip, proptest, concurrency |
35
+ | Documentation | Section 15 — Module header template, @canonical refs, public API docs |
36
+ | Anti-patterns | Section 16 — what NOT to do (anyhow, unwrap, sync Mutex across await, etc.) |
37
+ | Dependencies | Section 17 — Cargo.toml conventions |
38
+
39
+ ## Command
40
+
41
+ ```
42
+ # Read the section you need (use the Read tool with limit parameter):
43
+ Read file_path=".pi/skills/rust-enterprise-codegen.md" limit=60
44
+ # Or grep for a specific section header:
45
+ Grep pattern="## 7\. Async" path=".pi/skills/rust-enterprise-codegen.md"
46
+ # Then Read with offset/limit targeting the lines you need
47
+ ```
48
+
49
+ ## DDD Layer Contract
50
+
51
+ Each module follows this strict 4-layer structure:
52
+
53
+ | Layer | Purpose | Dependencies | Contains |
54
+ |-------|---------|-------------|----------|
55
+ | `domain/` | Pure business logic | None (serde, chrono, uuid only) | Entities, value objects, aggregates, domain events, repository traits |
56
+ | `application/` | Use case orchestration | `domain/` | Service traits, DTOs, factory interfaces |
57
+ | `infrastructure/` | External adapters | `application/` + `domain/` | Repository implementations, DB clients, HTTP clients |
58
+ | `interfaces/` | API contracts | `application/` | HTTP routes, request/response DTOs, event consumers |
59
+
60
+ **Dependency Rule:** Inner layers never depend on outer layers. `domain/` depends on nothing. `application/` depends on `domain/`. `infrastructure/` depends on `application/`. `interfaces/` depends on `application/`.
61
+
62
+ ## Rules
63
+
64
+ - NEVER read the full reference document into context — read specific sections
65
+ - Target reads with `grep` + line numbers instead
66
+ - Each agent loads only the patterns it needs for its current task
67
+ - ALWAYS follow the 4-layer DDD structure — no `contracts/` wrapper
68
+ - ALWAYS define repository traits in `domain/`, implement in `infrastructure/`
69
+ - ALWAYS use typed error enums (thiserror), never String errors
70
+ - ALWAYS encapsulate aggregate state behind methods
@@ -0,0 +1,1004 @@
1
+ # Rust Enterprise Code Generation — DDD + Clean Architecture
2
+
3
+ > Canonical skill for generating production-grade Rust code.
4
+ > All code MUST follow these patterns. Validators enforce compliance.
5
+ >
6
+ > Source: rigorix-engine (17 frozen-contract modules) + DDD architecture analysis + Clean Architecture principles.
7
+
8
+ ---
9
+
10
+ ## 1. Project Structure — Clean Architecture with DDD
11
+
12
+ Every bounded context follows the same 4-layer structure:
13
+
14
+ ```
15
+ module/
16
+ ├── domain/ # Pure domain entities, value objects, events
17
+ │ ├── mod.rs # Re-exports + module-level documentation
18
+ │ ├── entity.rs # Aggregate roots and entities
19
+ │ ├── value.rs # Value objects
20
+ │ ├── event.rs # Domain event payloads
21
+ │ └── error.rs # Typed error enum (thiserror)
22
+ ├── application/ # Service traits, DTOs, factory interfaces
23
+ │ ├── mod.rs
24
+ │ ├── service.rs # Service trait definitions
25
+ │ ├── factory.rs # Factory trait interfaces
26
+ │ └── dto/ # Input/Output DTOs with validation
27
+ │ └── mod.rs
28
+ ├── infrastructure/ # Repository implementations, external adapters
29
+ │ ├── mod.rs
30
+ │ ├── repository/ # Repository trait definitions
31
+ │ │ ├── mod.rs
32
+ │ │ └── [entity]_repository.rs
33
+ │ └── persistence/ # ORM/database implementations
34
+ └── interfaces/ # API contracts (HTTP, events)
35
+ ├── mod.rs
36
+ └── http/ # REST endpoint contracts
37
+ ├── mod.rs
38
+ ├── routes.rs
39
+ └── dto.rs # Request/Response DTOs
40
+ ```
41
+
42
+ ### Dependency Direction Rule (Inward Dependency)
43
+
44
+ ```
45
+ domain → application → infrastructure → interfaces
46
+ ↑ ↑
47
+ └── interior layers never depend on outer layers
48
+ ```
49
+
50
+ - **domain/** — depends on nothing except serde, chrono, uuid (pure data)
51
+ - **application/** — depends on domain
52
+ - **infrastructure/** — depends on application (implements domain/application traits)
53
+ - **interfaces/** — depends on application (translates HTTP/events to domain calls)
54
+
55
+ ### Module Header Pattern
56
+
57
+ Every `mod.rs` MUST include a canonical reference header:
58
+
59
+ ```rust
60
+ //! Module Purpose — One-line summary of what this module does.
61
+ //!
62
+ //! @canonical .pi/architecture/modules/[module-name].md#[section]
63
+ //! Implements: Contract Freeze — [component names]
64
+ //!
65
+ //! Longer description of the module's purpose, design decisions,
66
+ //! and how it fits into the larger architecture.
67
+ //!
68
+ //! # Architecture
69
+ //!
70
+ //! ```text
71
+ //! module/
72
+ //! ├── domain/ ...
73
+ //! ├── application/ ...
74
+ //! ├── infrastructure/ ...
75
+ //! └── interfaces/ ...
76
+ //! ```
77
+ //!
78
+ //! # Contract (Frozen)
79
+ //! - [List of frozen contract rules]
80
+ //! - No implementation logic beyond constructors and field accessors
81
+ //! - All domain types are serializable (Serialize + Deserialize)
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 2. DDD Tactical Patterns
87
+
88
+ ### Aggregate Root
89
+
90
+ The aggregate root is the entry point for all operations within its boundary. It enforces invariants and coordinates entity state changes.
91
+
92
+ ```rust
93
+ /// Aggregate root for the [Module] bounded context.
94
+ ///
95
+ /// # Contract (Frozen)
96
+ /// - Aggregate roots are the ONLY way to modify entities within their boundary
97
+ /// - Methods return Result<(), Error> instead of panicking
98
+ /// - All mutations go through aggregate methods, never direct field access
99
+ #[derive(Debug, Clone, Serialize, Deserialize)]
100
+ pub struct ModuleAggregate {
101
+ id: Uuid,
102
+ status: ModuleStatus,
103
+ entities: Vec<ChildEntity>,
104
+ created_at: DateTime<Utc>,
105
+ updated_at: DateTime<Utc>,
106
+ }
107
+
108
+ impl ModuleAggregate {
109
+ pub fn new(id: Uuid) -> Self {
110
+ Self {
111
+ id,
112
+ status: ModuleStatus::Pending,
113
+ entities: Vec::new(),
114
+ created_at: Utc::now(),
115
+ updated_at: Utc::now(),
116
+ }
117
+ }
118
+
119
+ /// Execute a state transition — returns events for side effects.
120
+ pub fn execute(&mut self, command: ModuleCommand) -> Result<Vec<DomainEvent>, ModuleError> {
121
+ match command {
122
+ ModuleCommand::Start => self.start(),
123
+ ModuleCommand::Complete { result } => self.complete(result),
124
+ ModuleCommand::Cancel { reason } => self.cancel(reason),
125
+ }
126
+ }
127
+
128
+ fn start(&mut self) -> Result<Vec<DomainEvent>, ModuleError> {
129
+ if !self.status.can_start() {
130
+ return Err(ModuleError::InvalidState {
131
+ current: self.status,
132
+ expected: ModuleStatus::Ready,
133
+ });
134
+ }
135
+ self.status = ModuleStatus::Running;
136
+ self.updated_at = Utc::now();
137
+ Ok(vec![DomainEvent::ModuleStarted { aggregate_id: self.id, timestamp: Utc::now() }])
138
+ }
139
+
140
+ fn complete(&mut self, result: serde_json::Value) -> Result<Vec<DomainEvent>, ModuleError> {
141
+ if !self.status.can_complete() {
142
+ return Err(ModuleError::InvalidState {
143
+ current: self.status,
144
+ expected: ModuleStatus::Running,
145
+ });
146
+ }
147
+ self.status = ModuleStatus::Completed;
148
+ self.updated_at = Utc::now();
149
+ Ok(vec![DomainEvent::ModuleCompleted { aggregate_id: self.id, result, timestamp: Utc::now() }])
150
+ }
151
+
152
+ fn cancel(&mut self, reason: String) -> Result<Vec<DomainEvent>, ModuleError> {
153
+ if self.status.is_terminal() {
154
+ return Err(ModuleError::InvalidState {
155
+ current: self.status,
156
+ expected: ModuleStatus::Pending,
157
+ });
158
+ }
159
+ self.status = ModuleStatus::Cancelled;
160
+ self.updated_at = Utc::now();
161
+ Ok(vec![DomainEvent::ModuleCancelled { aggregate_id: self.id, reason, timestamp: Utc::now() }])
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### Value Object
167
+
168
+ Value objects are immutable, interchangeable, and defined by their attributes, not identity.
169
+
170
+ ```rust
171
+ /// Value object — identified by structural equality, not identity.
172
+ ///
173
+ /// # Contract (Frozen)
174
+ /// - Immutable: all fields are read-only after construction
175
+ /// - Self-validating: constructor validates invariants
176
+ /// - Eq + Hash based on ALL fields
177
+ /// - No setters — create a new instance to change
178
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
179
+ pub struct Money {
180
+ amount: i64, // Stored in smallest currency unit (cents, pennys)
181
+ currency: Currency,
182
+ }
183
+
184
+ impl Money {
185
+ pub fn new(amount: i64, currency: Currency) -> Result<Self, ModuleError> {
186
+ if amount < 0 {
187
+ return Err(ModuleError::ValidationError {
188
+ field: "amount",
189
+ message: "Amount must be non-negative",
190
+ });
191
+ }
192
+ Ok(Self { amount, currency })
193
+ }
194
+
195
+ pub fn amount(&self) -> i64 { self.amount }
196
+ pub fn currency(&self) -> &Currency { &self.currency }
197
+
198
+ pub fn add(&self, other: &Self) -> Result<Self, ModuleError> {
199
+ if self.currency != other.currency {
200
+ return Err(ModuleError::CurrencyMismatch {
201
+ left: self.currency.clone(),
202
+ right: other.currency.clone(),
203
+ });
204
+ }
205
+ Ok(Self { amount: self.amount + other.amount, currency: self.currency.clone() })
206
+ }
207
+ }
208
+ ```
209
+
210
+ ### Repository Pattern
211
+
212
+ Repositories provide collection-like access to aggregates. The interface is defined in `domain/` or `application/`, implemented in `infrastructure/`.
213
+
214
+ ```rust
215
+ /// Repository trait — defined in domain, implemented in infrastructure.
216
+ ///
217
+ /// # Contract (Frozen)
218
+ /// - Interface defined in domain/, implementation in infrastructure/
219
+ /// - Methods return domain types (not ORM entities)
220
+ /// - Repository methods express the ubiquitous language
221
+ pub trait ModuleRepository: Send + Sync {
222
+ fn find_by_id(&self, id: Uuid) -> Result<Option<ModuleAggregate>, ModuleError>;
223
+ fn save(&mut self, aggregate: &ModuleAggregate) -> Result<(), ModuleError>;
224
+ fn delete(&mut self, id: Uuid) -> Result<(), ModuleError>;
225
+ fn find_by_status(&self, status: ModuleStatus) -> Result<Vec<ModuleAggregate>, ModuleError>;
226
+ }
227
+ ```
228
+
229
+ ### Domain Event
230
+
231
+ Domain events capture something meaningful that happened in the domain.
232
+
233
+ ```rust
234
+ /// Domain events for the [Module] bounded context.
235
+ ///
236
+ /// # Contract (Frozen)
237
+ /// - Every event carries aggregate_id and timestamp for correlation
238
+ /// - Serialized as tagged union with `#[serde(tag = "type")]`
239
+ /// - Events are facts — immutable and append-only
240
+ #[derive(Debug, Clone, Serialize, Deserialize)]
241
+ #[serde(tag = "type", rename_all = "snake_case")]
242
+ pub enum DomainEvent {
243
+ ModuleStarted {
244
+ aggregate_id: Uuid,
245
+ timestamp: DateTime<Utc>,
246
+ },
247
+ ModuleCompleted {
248
+ aggregate_id: Uuid,
249
+ result: serde_json::Value,
250
+ timestamp: DateTime<Utc>,
251
+ },
252
+ ModuleCancelled {
253
+ aggregate_id: Uuid,
254
+ reason: String,
255
+ timestamp: DateTime<Utc>,
256
+ },
257
+ }
258
+
259
+ impl DomainEvent {
260
+ /// Canonical snake_case name of this event variant.
261
+ pub fn event_type(&self) -> &'static str {
262
+ match self {
263
+ DomainEvent::ModuleStarted { .. } => "module_started",
264
+ DomainEvent::ModuleCompleted { .. } => "module_completed",
265
+ DomainEvent::ModuleCancelled { .. } => "module_cancelled",
266
+ }
267
+ }
268
+
269
+ /// Extract the common aggregate_id for correlation.
270
+ pub fn aggregate_id(&self) -> &Uuid {
271
+ match self {
272
+ DomainEvent::ModuleStarted { aggregate_id, .. }
273
+ | DomainEvent::ModuleCompleted { aggregate_id, .. }
274
+ | DomainEvent::ModuleCancelled { aggregate_id, .. } => aggregate_id,
275
+ }
276
+ }
277
+ }
278
+ ```
279
+
280
+ ### DDD Rules
281
+
282
+ - ✅ Aggregate roots are the **only** way to modify entities within their boundary
283
+ - ✅ Value objects are **immutable** after construction
284
+ - ✅ Repositories are interfaces in `domain/`, implementations in `infrastructure/`
285
+ - ✅ Domain events are **facts** — never modified after creation
286
+ - ✅ Every aggregate method returns `Result<Vec<DomainEvent>, Error>` for side effects
287
+ - ✅ Ubiquitous language in method and type names (not technology terms)
288
+ - ❌ No anemic domain models (entities with just getters/setters)
289
+ - ❌ No infrastructure concerns leaking into domain
290
+ - ❌ No `pub` fields on aggregates — always encapsulate
291
+ - ❌ No cross-aggregate references — use IDs, not object references
292
+
293
+ ---
294
+
295
+ ## 3. Error Handling — thiserror with Aggregation
296
+
297
+ ### Per-Module Error Enum
298
+
299
+ ```rust
300
+ use thiserror::Error;
301
+
302
+ /// Typed error enum for the [Module] bounded context.
303
+ ///
304
+ /// # Contract (Frozen)
305
+ /// - Every error variant follows the pattern: `PascalCase { fields }`
306
+ /// - `#[error("...")]` Display messages are user-readable
307
+ /// - Implement `is_retriable()` for transient failures
308
+ /// - Derive `Serialize + Deserialize` for API responses
309
+ #[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
310
+ pub enum ModuleError {
311
+ /// Resource not found — include what was requested and what's available.
312
+ #[error("Not found: {id}. Available: {available:?}")]
313
+ NotFound { id: String, available: Vec<String> },
314
+
315
+ /// Invalid state transition attempt.
316
+ #[error("Invalid state transition: {current} → {expected}")]
317
+ InvalidState { current: ModuleStatus, expected: ModuleStatus },
318
+
319
+ /// Duplicate identifier.
320
+ #[error("Duplicate ID: {id}")]
321
+ DuplicateId { id: Uuid },
322
+
323
+ /// Validation failure — field-level error details.
324
+ #[error("Validation failed: {message}")]
325
+ ValidationError { field: &'static str, message: String },
326
+
327
+ /// Operation was cancelled.
328
+ #[error("Operation cancelled")]
329
+ Cancelled,
330
+ }
331
+
332
+ impl ModuleError {
333
+ /// Returns true if the error represents a transient failure.
334
+ pub fn is_retriable(&self) -> bool {
335
+ false // Default: no transient errors unless explicitly marked
336
+ }
337
+ }
338
+ ```
339
+
340
+ ### Root Error Aggregation
341
+
342
+ ```rust
343
+ use thiserror::Error;
344
+
345
+ /// Root error type that aggregates all domain-specific errors via #[from].
346
+ #[derive(Debug, Error)]
347
+ pub enum RootError {
348
+ #[error("DAG error: {0}")]
349
+ Dag(#[from] DagError),
350
+
351
+ #[error("Planning error: {0}")]
352
+ Planning(#[from] PlanningError),
353
+
354
+ #[error("Execution error: {0}")]
355
+ Execution(#[from] ExecutionError),
356
+
357
+ #[error("IO error: {0}")]
358
+ Io(#[from] std::io::Error),
359
+
360
+ #[error("Operation cancelled: {0}")]
361
+ Cancelled(String),
362
+ }
363
+
364
+ impl RootError {
365
+ /// HTTP status code that best represents this error.
366
+ pub fn http_status(&self) -> u16 {
367
+ match self {
368
+ RootError::Dag(_) => 500,
369
+ RootError::Planning(_) => 400,
370
+ RootError::Execution(_) => 500,
371
+ RootError::Io(_) => 500,
372
+ RootError::Cancelled(_) => 499,
373
+ }
374
+ }
375
+
376
+ /// Machine-readable error code.
377
+ pub fn error_code(&self) -> &'static str {
378
+ match self {
379
+ RootError::Dag(_) => "DAG_ERROR",
380
+ RootError::Planning(_) => "PLANNING_ERROR",
381
+ RootError::Execution(_) => "EXECUTION_ERROR",
382
+ RootError::Io(_) => "IO_ERROR",
383
+ RootError::Cancelled(_) => "CANCELLED",
384
+ }
385
+ }
386
+ }
387
+ ```
388
+
389
+ ### Error Source Chains
390
+
391
+ ```rust
392
+ #[derive(Error, Debug)]
393
+ #[error("planning failed: {context}")]
394
+ pub struct PlanningFailure {
395
+ context: String,
396
+ #[source]
397
+ cause: PlanningError,
398
+ }
399
+ ```
400
+
401
+ ### Rules
402
+
403
+ - ✅ Use `thiserror` for ALL library/domain errors
404
+ - ✅ Every error has a descriptive `#[error("...")]` message
405
+ - ✅ Include context in error fields (what was requested, what's available)
406
+ - ✅ Root error aggregates sub-errors via `#[from]` for `?` operator propagation
407
+ - ❌ NEVER use `anyhow` in library code — reserved for binary crates only
408
+ - ❌ NEVER use `.unwrap()` or `.expect()` in production code
409
+ - ❌ NEVER use `String` errors — always typed enums
410
+
411
+ ---
412
+
413
+ ## 4. Secret Handling — Redacted Value Object
414
+
415
+ ```rust
416
+ /// A sensitive value (API key, token) that is redacted in all text output.
417
+ ///
418
+ /// # Security
419
+ /// - Debug/Display show `[REDACTED]` (never leak)
420
+ /// - Only `.expose()` reveals the inner value
421
+ /// - Does NOT derive Serialize — secrets must not be serialized
422
+ #[derive(Clone)]
423
+ pub struct Secret(String);
424
+
425
+ impl Secret {
426
+ pub fn new(value: impl Into<String>) -> Self { Self(value.into()) }
427
+ pub fn expose(&self) -> &str { &self.0 }
428
+ pub fn is_empty(&self) -> bool { self.0.is_empty() }
429
+ }
430
+
431
+ impl fmt::Debug for Secret {
432
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433
+ if self.0.is_empty() { write!(f, "<empty>") }
434
+ else { write!(f, "[REDACTED]") }
435
+ }
436
+ }
437
+
438
+ impl fmt::Display for Secret { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(self, f) } }
439
+ ```
440
+
441
+ ### Rules
442
+ - ✅ Wrap all API keys, tokens, passwords in `Secret`
443
+ - ✅ Load secrets from environment variables, never from config files
444
+ - ❌ Never derive Serialize on types that contain secrets
445
+ - ❌ Never log secrets — `Secret::Debug` is redacted
446
+
447
+ ---
448
+
449
+ ## 5. State Machine Pattern — Typed Enum Lifecycle
450
+
451
+ ```rust
452
+ /// Lifecycle status of a [domain entity].
453
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
454
+ pub enum ModuleStatus {
455
+ Pending,
456
+ Ready,
457
+ Running,
458
+ Completed,
459
+ Failed,
460
+ Cancelled,
461
+ }
462
+
463
+ impl ModuleStatus {
464
+ pub fn is_terminal(&self) -> bool {
465
+ matches!(self, ModuleStatus::Completed | ModuleStatus::Failed | ModuleStatus::Cancelled)
466
+ }
467
+
468
+ pub fn can_start(&self) -> bool { matches!(self, ModuleStatus::Ready) }
469
+ pub fn can_complete(&self) -> bool { matches!(self, ModuleStatus::Running) }
470
+
471
+ pub fn as_str(&self) -> &'static str {
472
+ match self {
473
+ ModuleStatus::Pending => "pending",
474
+ ModuleStatus::Ready => "ready",
475
+ ModuleStatus::Running => "running",
476
+ ModuleStatus::Completed => "completed",
477
+ ModuleStatus::Failed => "failed",
478
+ ModuleStatus::Cancelled => "cancelled",
479
+ }
480
+ }
481
+ }
482
+ ```
483
+
484
+ ### State Tracking Entity
485
+
486
+ ```rust
487
+ pub struct ModuleExecutionState {
488
+ pub module_id: Uuid,
489
+ pub status: ModuleStatus,
490
+ pub retry_attempts: u8,
491
+ pub started_at: Option<DateTime<Utc>>,
492
+ pub completed_at: Option<DateTime<Utc>>,
493
+ pub last_error: Option<String>,
494
+ }
495
+
496
+ impl ModuleExecutionState {
497
+ pub fn new(module_id: Uuid) -> Self { Self { module_id, status: ModuleStatus::Pending, retry_attempts: 0, started_at: None, completed_at: None, last_error: None } }
498
+
499
+ pub fn mark_ready(&mut self) { self.status = ModuleStatus::Ready; self.started_at = None; }
500
+ pub fn mark_running(&mut self) { self.status = ModuleStatus::Running; self.started_at = Some(Utc::now()); }
501
+ pub fn mark_completed(&mut self) { self.status = ModuleStatus::Completed; self.completed_at = Some(Utc::now()); }
502
+ pub fn mark_failed(&mut self, error: String) { self.status = ModuleStatus::Failed; self.last_error = Some(error); }
503
+ pub fn mark_for_retry(&mut self) { self.retry_attempts += 1; self.status = ModuleStatus::Ready; }
504
+ }
505
+ ```
506
+
507
+ ### Rules
508
+ - ✅ State transitions are **methods**, not public field writes
509
+ - ✅ Each transition captures timestamp automatically
510
+ - ✅ `is_terminal()` on every state enum
511
+ - ❌ No direct field mutation from outside the entity
512
+
513
+ ---
514
+
515
+ ## 6. RAII Reservation Pattern — Resource Guard
516
+
517
+ ```rust
518
+ /// RAII guard: reserves budget on creation, auto-returns on Drop.
519
+ pub struct BudgetReservation {
520
+ budget_id: Uuid,
521
+ amount: u64,
522
+ released: bool,
523
+ }
524
+
525
+ impl BudgetReservation {
526
+ pub fn new(budget_id: Uuid, amount: u64) -> Self { Self { budget_id, amount, released: false } }
527
+ pub fn release(mut self) { self.released = true; /* Return budget to pool */ }
528
+ }
529
+
530
+ impl Drop for BudgetReservation {
531
+ fn drop(&mut self) {
532
+ if !self.released {
533
+ tracing::warn!("Budget reservation dropped without release");
534
+ }
535
+ }
536
+ }
537
+ ```
538
+
539
+ ---
540
+
541
+ ## 7. Async Patterns — tokio JoinSet for Parallelism
542
+
543
+ ```rust
544
+ use tokio::task::JoinSet;
545
+
546
+ pub async fn execute_parallel(tasks: Vec<Task>, max_concurrent: u32) -> Result<Vec<TaskResult>, Error> {
547
+ let mut join_set = JoinSet::new();
548
+ let mut results = Vec::new();
549
+ let mut iter = tasks.into_iter();
550
+
551
+ for _ in 0..max_concurrent {
552
+ if let Some(task) = iter.next() { join_set.spawn(execute_task(task)); }
553
+ }
554
+
555
+ while let Some(result) = join_set.join_next().await {
556
+ match result {
557
+ Ok(Ok(output)) => results.push(output),
558
+ Ok(Err(e)) => return Err(e),
559
+ Err(join_error) => return Err(Error::TaskPanicked(join_error.to_string())),
560
+ }
561
+ if let Some(task) = iter.next() { join_set.spawn(execute_task(task)); }
562
+ }
563
+
564
+ Ok(results)
565
+ }
566
+ ```
567
+
568
+ ### Cancellation-Aware Sleep
569
+
570
+ ```rust
571
+ use tokio_util::sync::CancellationToken;
572
+
573
+ pub async fn poll_with_cancellation(cancel: CancellationToken, interval: Duration) -> Result<(), Error> {
574
+ loop {
575
+ tokio::select! {
576
+ _ = tokio::time::sleep(interval) => { /* Do periodic work */ }
577
+ _ = cancel.cancelled() => { return Err(Error::Cancelled("Polling cancelled".into())); }
578
+ }
579
+ }
580
+ }
581
+ ```
582
+
583
+ ### Rules
584
+ - ✅ Use `tokio::sync::mpsc::channel` (bounded) for cross-task communication
585
+ - ✅ Use `tokio::sync::broadcast` for fan-out pub-sub
586
+ - ✅ Use `tokio_util::sync::CancellationToken` for cooperative cancellation
587
+ - ✅ Use `std::sync::Mutex` for short critical sections that don't cross `.await` points
588
+ - ✅ Use `tokio::sync::Mutex` only when the lock must be held across `.await` points
589
+ - ❌ Never hold `std::sync::Mutex` across `.await` points
590
+ - ❌ Never use unbounded channels without explicit justification
591
+ - ❌ Never block with `std::thread::sleep` in async code
592
+
593
+ ---
594
+
595
+ ## 8. Domain Event Pattern — Tagged Union Enum
596
+
597
+ ```rust
598
+ /// All possible events in the bounded context.
599
+ ///
600
+ /// # Contract (Frozen)
601
+ /// - Every variant carries aggregate_id and timestamp for correlation
602
+ /// - Serialized as tagged union with `#[serde(tag = "type")]`
603
+ /// - No implementation logic — pure data
604
+ #[derive(Debug, Clone, Serialize, Deserialize)]
605
+ #[serde(tag = "type", rename_all = "snake_case")]
606
+ pub enum DomainEvent {
607
+ Started { aggregate_id: Uuid, timestamp: DateTime<Utc> },
608
+ Completed { aggregate_id: Uuid, result: serde_json::Value, timestamp: DateTime<Utc> },
609
+ }
610
+
611
+ impl DomainEvent {
612
+ pub fn event_type(&self) -> &'static str {
613
+ match self {
614
+ DomainEvent::Started { .. } => "started",
615
+ DomainEvent::Completed { .. } => "completed",
616
+ }
617
+ }
618
+ pub fn aggregate_id(&self) -> &Uuid {
619
+ match self {
620
+ DomainEvent::Started { aggregate_id, .. }
621
+ | DomainEvent::Completed { aggregate_id, .. } => aggregate_id,
622
+ }
623
+ }
624
+ }
625
+ ```
626
+
627
+ ### Rules
628
+ - ✅ Every event carries `aggregate_id` and `timestamp`
629
+ - ✅ Serialized as tagged union: `#[serde(tag = "type", rename_all = "snake_case")]`
630
+ - ✅ Provide helper methods: `event_type()`, `aggregate_id()`
631
+ - ✅ Write round-trip serde test for every variant
632
+ - ❌ No logic in event types — they are pure data
633
+
634
+ ---
635
+
636
+ ## 9. Configuration Pattern — Multi-Source Merging
637
+
638
+ ```rust
639
+ /// Merge order: CLI flags > Environment > Config file > Defaults
640
+ pub trait Merge {
641
+ fn merge(&mut self, other: Self);
642
+ }
643
+
644
+ pub struct ConfigService;
645
+
646
+ impl ConfigService {
647
+ pub fn load(cli_overrides: CliConfig) -> Result<Config, ConfigError> {
648
+ let mut config = Config::default();
649
+
650
+ // Layer 1: Config file
651
+ if let Some(file) = Self::load_config_file()? { config.merge(file); }
652
+
653
+ // Layer 2: Environment variables (RIGORIX_*)
654
+ config.merge(Self::load_from_env()?);
655
+
656
+ // Layer 3: CLI flags (highest precedence)
657
+ config.merge(cli_overrides);
658
+
659
+ config.validate()?;
660
+ Ok(config)
661
+ }
662
+
663
+ fn load_from_env() -> Result<Config, ConfigError> {
664
+ let mut config = Config::default();
665
+ if let Ok(val) = std::env::var("APP_LOG") { config.log_level = val; }
666
+ if let Ok(val) = std::env::var("APP_API_KEY") { config.api_key = Secret::new(val); }
667
+ Ok(config)
668
+ }
669
+ }
670
+ ```
671
+
672
+ ### Rules
673
+ - ✅ CLI flags override env vars which override config file which override defaults
674
+ - ✅ Secrets loaded ONLY from environment variables
675
+ - ✅ `validate()` runs after merging — fail fast on startup
676
+
677
+ ---
678
+
679
+ ## 10. Atomic File Operations — Write-Rename
680
+
681
+ ```rust
682
+ use std::fs;
683
+ use std::io::Write;
684
+ use std::path::Path;
685
+
686
+ /// Atomic file write: write to tmp → fsync → rename → fsync parent.
687
+ pub fn atomic_write(path: &Path, contents: &str) -> Result<(), IoError> {
688
+ let tmp_path = path.with_extension("tmp");
689
+ let mut file = fs::File::create(&tmp_path)?;
690
+ file.write_all(contents.as_bytes())?;
691
+ file.sync_all()?;
692
+ fs::rename(&tmp_path, path)?;
693
+ if let Some(parent) = path.parent() {
694
+ if let Ok(dir) = fs::File::open(parent) { dir.sync_all()?; }
695
+ }
696
+ Ok(())
697
+ }
698
+
699
+ pub fn clean_orphan_tmp_files(dir: &Path) -> Result<(), IoError> {
700
+ for entry in fs::read_dir(dir)? {
701
+ let entry = entry?;
702
+ if entry.path().extension().map_or(false, |e| e == "tmp") {
703
+ fs::remove_file(entry.path())?;
704
+ }
705
+ }
706
+ Ok(())
707
+ }
708
+ ```
709
+
710
+ ---
711
+
712
+ ## 11. Builder Pattern — Complex Construction
713
+
714
+ ```rust
715
+ #[derive(Debug, Clone, Serialize, Deserialize)]
716
+ pub struct ExecutionPolicy {
717
+ pub max_retries: u8,
718
+ pub retry_on: Vec<FailureType>,
719
+ pub retry_strategy: RetryStrategy,
720
+ pub backoff_ms: u64,
721
+ pub backoff_multiplier: f64,
722
+ pub max_backoff_ms: u64,
723
+ }
724
+
725
+ impl Default for ExecutionPolicy {
726
+ fn default() -> Self {
727
+ Self { max_retries: 3, retry_on: vec![FailureType::Transient], retry_strategy: RetryStrategy::SameOperation, backoff_ms: 100, backoff_multiplier: 2.0, max_backoff_ms: 30_000 }
728
+ }
729
+ }
730
+
731
+ impl ExecutionPolicy {
732
+ pub fn builder() -> ExecutionPolicyBuilder { ExecutionPolicyBuilder::default() }
733
+ pub fn no_retry() -> Self { Self { max_retries: 0, ..Default::default() } }
734
+ pub fn aggressive_retry() -> Self { Self { max_retries: 5, retry_on: vec![FailureType::Transient], backoff_ms: 50, backoff_multiplier: 1.5, ..Default::default() } }
735
+ }
736
+
737
+ #[derive(Default)]
738
+ pub struct ExecutionPolicyBuilder {
739
+ max_retries: u8,
740
+ retry_on: Vec<FailureType>,
741
+ retry_strategy: RetryStrategy,
742
+ backoff_ms: u64,
743
+ backoff_multiplier: f64,
744
+ max_backoff_ms: u64,
745
+ }
746
+
747
+ impl ExecutionPolicyBuilder {
748
+ pub fn with_max_retries(mut self, val: u8) -> Self { self.max_retries = val; self }
749
+ pub fn with_backoff(mut self, base_ms: u64, multiplier: f64, max_ms: u64) -> Self { self.backoff_ms = base_ms; self.backoff_multiplier = multiplier; self.max_backoff_ms = max_ms; self }
750
+ pub fn build(self) -> ExecutionPolicy { ExecutionPolicy { max_retries: self.max_retries, retry_on: self.retry_on, retry_strategy: self.retry_strategy, backoff_ms: self.backoff_ms, backoff_multiplier: self.backoff_multiplier, max_backoff_ms: self.max_backoff_ms } }
751
+ }
752
+ ```
753
+
754
+ ---
755
+
756
+ ## 12. Retry/Backoff Pattern
757
+
758
+ ```rust
759
+ #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
760
+ pub enum BackoffStrategy {
761
+ Fixed { base_delay_ms: u64 },
762
+ Exponential { base_delay_ms: u64, multiplier: f64, max_delay_ms: u64 },
763
+ Linear { base_delay_ms: u64, step_ms: u64, max_delay_ms: u64 },
764
+ Immediate,
765
+ }
766
+
767
+ impl Default for BackoffStrategy {
768
+ fn default() -> Self { Self::Exponential { base_delay_ms: 100, multiplier: 2.0, max_delay_ms: 30_000 } }
769
+ }
770
+
771
+ impl BackoffStrategy {
772
+ /// Compute delay in milliseconds for retry attempt `n` (0-indexed).
773
+ pub fn delay_ms(&self, attempt: u8) -> u64 {
774
+ match self {
775
+ Self::Fixed { base } => *base,
776
+ Self::Exponential { base, mult, max } => (*base as f64 * mult.powi(attempt as i32)) as u64 - max,
777
+ Self::Linear { base, step, max } => (*base + *step * attempt as u64).min(*max),
778
+ Self::Immediate => 0,
779
+ }
780
+ }
781
+ }
782
+ ```
783
+
784
+ ---
785
+
786
+ ## 13. EventBus — Pub-Sub with Broadcast Channel
787
+
788
+ ```rust
789
+ use tokio::sync::broadcast;
790
+
791
+ pub struct EventBus {
792
+ tx: broadcast::Sender<DomainEvent>,
793
+ log: Vec<PersistedEvent>,
794
+ sequence: u64,
795
+ }
796
+
797
+ impl EventBus {
798
+ pub fn new(capacity: usize) -> Self {
799
+ let (tx, _) = broadcast::channel(capacity);
800
+ Self { tx, log: Vec::new(), sequence: 0 }
801
+ }
802
+
803
+ pub fn publish(&mut self, event: DomainEvent) -> Result<(), EventBusError> {
804
+ self.sequence += 1;
805
+ let persisted = PersistedEvent { sequence: self.sequence, event: event.clone() };
806
+ self.log.push(persisted);
807
+ let _ = self.tx.send(event);
808
+ Ok(())
809
+ }
810
+
811
+ pub fn subscribe(&self) -> broadcast::Receiver<DomainEvent> { self.tx.subscribe() }
812
+ pub fn drain(&mut self) -> Vec<PersistedEvent> { self.log.drain(..).collect() }
813
+ }
814
+ ```
815
+
816
+ ---
817
+
818
+ ## 14. Testing Patterns
819
+
820
+ ### Unit Tests — Inline with `#[cfg(test)]`
821
+
822
+ ```rust
823
+ #[cfg(test)]
824
+ mod tests {
825
+ use super::*;
826
+
827
+ // AAA Pattern: Arrange → Act → Assert
828
+ #[test]
829
+ fn test_backoff_delay_exponential() {
830
+ let strategy = BackoffStrategy::Exponential { base_delay_ms: 100, multiplier: 2.0, max_delay_ms: 30_000 };
831
+ assert_eq!(strategy.delay_ms(2), 400); // 100 * 2^2 = 400
832
+ }
833
+
834
+ #[test]
835
+ fn test_status_terminal() {
836
+ assert!(ModuleStatus::Completed.is_terminal());
837
+ assert!(!ModuleStatus::Running.is_terminal());
838
+ }
839
+
840
+ #[test]
841
+ fn test_aggregate_execute_start() {
842
+ let mut agg = ModuleAggregate::new(Uuid::new_v4());
843
+ let events = agg.execute(ModuleCommand::Start).unwrap();
844
+ assert_eq!(agg.status, ModuleStatus::Running);
845
+ assert_eq!(events.len(), 1);
846
+ }
847
+ }
848
+ ```
849
+
850
+ ### Serde Round-Trip Tests — For Every Serialized Type
851
+
852
+ ```rust
853
+ #[test]
854
+ fn test_domain_event_serde_roundtrip() {
855
+ let eid = Uuid::new_v4();
856
+ let event = DomainEvent::ModuleCompleted { aggregate_id: eid, result: serde_json::json!("done"), timestamp: Utc::now() };
857
+ let json = serde_json::to_string(&event).unwrap();
858
+ let deserialized: DomainEvent = serde_json::from_str(&json).unwrap();
859
+ assert_eq!(*deserialized.aggregate_id(), eid);
860
+ }
861
+ ```
862
+
863
+ ### Property-Based Tests
864
+
865
+ ```rust
866
+ #[cfg(test)]
867
+ mod property_tests {
868
+ use super::*;
869
+ use proptest::prelude::*;
870
+
871
+ proptest! {
872
+ #[test]
873
+ fn test_backoff_never_exceeds_max(base in 1..1000u64, mult in 1.0..10.0f64, max in 1000..100_000u64, attempt in 0..10u8) {
874
+ let strategy = BackoffStrategy::Exponential { base_delay_ms: base, multiplier: mult, max_delay_ms: max };
875
+ assert!(strategy.delay_ms(attempt) <= max);
876
+ }
877
+ }
878
+ }
879
+ ```
880
+
881
+ ---
882
+
883
+ ## 15. Documentation Standards
884
+
885
+ ### Module-Level Docs (Every `mod.rs`)
886
+
887
+ ```rust
888
+ //! [Module Name] — One-line purpose.
889
+ //!
890
+ //! @canonical .pi/architecture/modules/[module-name].md
891
+ //! Implements: Contract Freeze — [component list]
892
+ //!
893
+ //! [2-3 paragraph description of what this module does and how it works]
894
+ //!
895
+ //! # Architecture
896
+ //!
897
+ //! [Optional ASCII art or description of sub-module structure]
898
+ //!
899
+ //! # Dependencies
900
+ //!
901
+ //! - Depends on: [other modules]
902
+ //! - Used by: [other modules]
903
+ //!
904
+ //! # Contract (Frozen)
905
+ //!
906
+ //! - [List of frozen contract rules]
907
+ //! - No implementation logic beyond constructors and field accessors
908
+ ```
909
+
910
+ ### Public API Docs
911
+
912
+ ```rust
913
+ /// Description of what this type/function does.
914
+ ///
915
+ /// # Contract (Frozen)
916
+ /// - [Specific contract rules for this type]
917
+ ///
918
+ /// # Examples
919
+ /// ```ignore
920
+ /// let policy = ExecutionPolicy::default();
921
+ /// ```
922
+ ///
923
+ /// # Errors
924
+ /// - Returns `Error::InvalidConfig` if max_retries > 100
925
+ #[derive(Debug, Clone, Serialize, Deserialize)]
926
+ pub struct ExecutionPolicy { ... }
927
+ ```
928
+
929
+ ---
930
+
931
+ ## 16. Anti-Patterns — NEVER DO
932
+
933
+ ```rust
934
+ // ❌ anyhow in library code
935
+ use anyhow::Result; // BAD — use thiserror
936
+
937
+ // ❌ Blocking in async context
938
+ async fn bad() { let data = std::fs::read_to_string("file"); } // BAD — use tokio::fs
939
+
940
+ // ❌ Unbounded channels
941
+ let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); // BAD — no backpressure
942
+
943
+ // ❌ unwrap/expect in production
944
+ let value = result.unwrap(); // BAD — use ? or proper error handling
945
+
946
+ // ❌ std::sync::Mutex held across .await — blocks the runtime thread
947
+ async fn bad() { let guard = data.lock().unwrap(); tokio::time::sleep(...).await; } // BAD
948
+
949
+ // ❌ Direct field mutation of state
950
+ node.status = NodeStatus::Running; // BAD — use transition methods
951
+
952
+ // ❌ Stringly-typed errors
953
+ Err("something went wrong".into()) // BAD — use typed error enums
954
+
955
+ // ❌ Logging secrets
956
+ info!("API key: {}", secret.expose()); // BAD — Secret::Debug is redacted
957
+
958
+ // ❌ Direct thread::sleep in async
959
+ std::thread::sleep(Duration::from_secs(1)); // BAD — use tokio::time::sleep
960
+
961
+ // ❌ Anemic domain models — entities with only getters/setters and no behavior
962
+ pub struct AnemicEntity { pub id: Uuid, pub name: String } // BAD — no domain logic
963
+
964
+ // ❌ Cross-aggregate references by object ref, not ID
965
+ pub struct Order { pub customer: Customer } // BAD — use customer_id: Uuid
966
+
967
+ // ❌ Infrastructure leak in domain
968
+ use sqlx::PgPool; // BAD — domain NEVER imports infrastructure concerns
969
+ ```
970
+
971
+ ---
972
+
973
+ ## 17. Cargo.toml Conventions
974
+
975
+ ```toml
976
+ [package]
977
+ name = "module-name"
978
+ version = "0.1.0"
979
+ edition = "2024"
980
+ description = "One-line description of this crate"
981
+
982
+ [dependencies]
983
+ serde = { version = "1", features = ["derive"] }
984
+ serde_json = "1"
985
+ thiserror = "2"
986
+ tokio = { version = "1", features = ["sync", "time", "macros", "rt"] }
987
+ tokio-util = "0.7"
988
+ uuid = { version = "1", features = ["v4", "serde"] }
989
+ chrono = { version = "0.4", features = ["serde"] }
990
+ tracing = "0.1"
991
+
992
+ [dev-dependencies]
993
+ tempfile = "3"
994
+ proptest = "1"
995
+
996
+ [features]
997
+ live-tests = [] # Flag for tests that hit real APIs
998
+ ```
999
+
1000
+ ---
1001
+
1002
+ *Version: 1.0.0*
1003
+ *Last updated: 2026-07-03*
1004
+ *Source: Guardian DDD patterns + context7 DDD reference (/jkazama/ddd-java, /ardalis/cleanarchitecture)*