@coralai/sps-cli 0.41.2 → 0.43.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.
Files changed (168) hide show
  1. package/README.md +34 -3
  2. package/dist/commands/cardAdd.d.ts +1 -1
  3. package/dist/commands/cardAdd.d.ts.map +1 -1
  4. package/dist/commands/cardAdd.js +16 -6
  5. package/dist/commands/cardAdd.js.map +1 -1
  6. package/dist/commands/cardDashboard.js +1 -1
  7. package/dist/commands/cardDashboard.js.map +1 -1
  8. package/dist/commands/doctor.d.ts +9 -0
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +3 -314
  11. package/dist/commands/doctor.js.map +1 -1
  12. package/dist/commands/hookCommand.d.ts.map +1 -1
  13. package/dist/commands/hookCommand.js +6 -7
  14. package/dist/commands/hookCommand.js.map +1 -1
  15. package/dist/commands/pmCommand.js +1 -1
  16. package/dist/commands/pmCommand.js.map +1 -1
  17. package/dist/commands/projectInit.d.ts.map +1 -1
  18. package/dist/commands/projectInit.js +60 -37
  19. package/dist/commands/projectInit.js.map +1 -1
  20. package/dist/commands/setup.d.ts.map +1 -1
  21. package/dist/commands/setup.js +3 -30
  22. package/dist/commands/setup.js.map +1 -1
  23. package/dist/commands/skillCommand.d.ts +2 -0
  24. package/dist/commands/skillCommand.d.ts.map +1 -0
  25. package/dist/commands/skillCommand.js +235 -0
  26. package/dist/commands/skillCommand.js.map +1 -0
  27. package/dist/commands/tick.js +1 -1
  28. package/dist/commands/tick.js.map +1 -1
  29. package/dist/core/checklist.d.ts +22 -0
  30. package/dist/core/checklist.d.ts.map +1 -0
  31. package/dist/core/checklist.js +38 -0
  32. package/dist/core/checklist.js.map +1 -0
  33. package/dist/core/checklist.test.d.ts +2 -0
  34. package/dist/core/checklist.test.d.ts.map +1 -0
  35. package/dist/core/checklist.test.js +74 -0
  36. package/dist/core/checklist.test.js.map +1 -0
  37. package/dist/core/config.d.ts +1 -1
  38. package/dist/core/config.d.ts.map +1 -1
  39. package/dist/core/config.js +1 -1
  40. package/dist/core/config.js.map +1 -1
  41. package/dist/core/config.test.js +7 -4
  42. package/dist/core/config.test.js.map +1 -1
  43. package/dist/core/context.d.ts +1 -1
  44. package/dist/core/context.d.ts.map +1 -1
  45. package/dist/core/skillStore.d.ts +46 -0
  46. package/dist/core/skillStore.d.ts.map +1 -0
  47. package/dist/core/skillStore.js +197 -0
  48. package/dist/core/skillStore.js.map +1 -0
  49. package/dist/core/skillStore.test.d.ts +2 -0
  50. package/dist/core/skillStore.test.d.ts.map +1 -0
  51. package/dist/core/skillStore.test.js +190 -0
  52. package/dist/core/skillStore.test.js.map +1 -0
  53. package/dist/engines/EventHandler.test.js +3 -3
  54. package/dist/engines/EventHandler.test.js.map +1 -1
  55. package/dist/engines/MonitorEngine.js +2 -2
  56. package/dist/engines/MonitorEngine.js.map +1 -1
  57. package/dist/engines/SchedulerEngine.js +1 -1
  58. package/dist/engines/SchedulerEngine.js.map +1 -1
  59. package/dist/engines/StageEngine.js +3 -3
  60. package/dist/engines/StageEngine.js.map +1 -1
  61. package/dist/engines/engine-pipeline-adapter.test.js +2 -2
  62. package/dist/engines/engine-pipeline-adapter.test.js.map +1 -1
  63. package/dist/interfaces/TaskBackend.d.ts +3 -1
  64. package/dist/interfaces/TaskBackend.d.ts.map +1 -1
  65. package/dist/main.js +19 -17
  66. package/dist/main.js.map +1 -1
  67. package/dist/models/types.d.ts +16 -1
  68. package/dist/models/types.d.ts.map +1 -1
  69. package/dist/providers/MarkdownTaskBackend.d.ts +2 -1
  70. package/dist/providers/MarkdownTaskBackend.d.ts.map +1 -1
  71. package/dist/providers/MarkdownTaskBackend.js +28 -5
  72. package/dist/providers/MarkdownTaskBackend.js.map +1 -1
  73. package/dist/providers/registry.d.ts.map +1 -1
  74. package/dist/providers/registry.js +5 -7
  75. package/dist/providers/registry.js.map +1 -1
  76. package/package.json +1 -1
  77. package/project-template/.claude/hooks/start.sh +44 -0
  78. package/project-template/.claude/settings.json +1 -1
  79. package/skills/architecture-decision-records/SKILL.md +207 -0
  80. package/skills/backend/SKILL.md +62 -0
  81. package/skills/backend/references/api-design.md +168 -0
  82. package/skills/backend/references/caching.md +181 -0
  83. package/skills/backend/references/data-access.md +173 -0
  84. package/skills/backend/references/layering.md +181 -0
  85. package/skills/backend/references/observability.md +190 -0
  86. package/skills/backend/references/resilience.md +201 -0
  87. package/skills/backend/references/security.md +186 -0
  88. package/skills/backend-architect/SKILL.md +119 -0
  89. package/skills/code-reviewer/SKILL.md +143 -0
  90. package/skills/coding-standards/SKILL.md +60 -0
  91. package/skills/coding-standards/references/clean-code.md +258 -0
  92. package/skills/coding-standards/references/code-review.md +192 -0
  93. package/skills/coding-standards/references/commits-and-prs.md +226 -0
  94. package/skills/coding-standards/references/error-strategy.md +193 -0
  95. package/skills/coding-standards/references/naming.md +185 -0
  96. package/skills/coding-standards/references/tdd.md +171 -0
  97. package/skills/database/SKILL.md +53 -0
  98. package/skills/database/references/indexing.md +190 -0
  99. package/skills/database/references/migrations.md +199 -0
  100. package/skills/database/references/nosql.md +185 -0
  101. package/skills/database/references/queries.md +295 -0
  102. package/skills/database/references/scaling.md +203 -0
  103. package/skills/database/references/schema.md +191 -0
  104. package/skills/database-optimizer/SKILL.md +168 -0
  105. package/skills/debugging-workflow/SKILL.md +244 -0
  106. package/skills/devops/SKILL.md +55 -0
  107. package/skills/devops/references/ci-cd.md +204 -0
  108. package/skills/devops/references/containers.md +272 -0
  109. package/skills/devops/references/deploy.md +201 -0
  110. package/skills/devops/references/iac.md +252 -0
  111. package/skills/devops/references/observability.md +228 -0
  112. package/skills/devops/references/secrets.md +178 -0
  113. package/skills/devops-automator/SKILL.md +164 -0
  114. package/skills/frontend/SKILL.md +52 -0
  115. package/skills/frontend/references/accessibility.md +222 -0
  116. package/skills/frontend/references/components.md +206 -0
  117. package/skills/frontend/references/performance.md +219 -0
  118. package/skills/frontend/references/routing.md +209 -0
  119. package/skills/frontend/references/state.md +190 -0
  120. package/skills/frontend/references/testing.md +216 -0
  121. package/skills/frontend-developer/SKILL.md +115 -0
  122. package/skills/git-workflow/SKILL.md +355 -0
  123. package/skills/golang/SKILL.md +49 -0
  124. package/skills/golang/references/concurrency.md +284 -0
  125. package/skills/golang/references/errors.md +241 -0
  126. package/skills/golang/references/idioms.md +285 -0
  127. package/skills/golang/references/testing.md +238 -0
  128. package/skills/java/SKILL.md +50 -0
  129. package/skills/java/references/concurrency.md +194 -0
  130. package/skills/java/references/idioms.md +283 -0
  131. package/skills/java/references/testing.md +228 -0
  132. package/skills/kotlin/SKILL.md +47 -0
  133. package/skills/kotlin/references/coroutines.md +240 -0
  134. package/skills/kotlin/references/idioms.md +268 -0
  135. package/skills/kotlin/references/testing.md +219 -0
  136. package/skills/mobile/SKILL.md +50 -0
  137. package/skills/mobile/references/architecture.md +204 -0
  138. package/skills/mobile/references/navigation.md +158 -0
  139. package/skills/mobile/references/performance.md +152 -0
  140. package/skills/mobile/references/platform.md +166 -0
  141. package/skills/mobile/references/state-and-data.md +174 -0
  142. package/skills/python/SKILL.md +51 -0
  143. package/skills/python/THIRD_PARTY.md +14 -0
  144. package/skills/python/references/async.md +218 -0
  145. package/skills/python/references/error-handling.md +254 -0
  146. package/skills/python/references/idioms.md +279 -0
  147. package/skills/python/references/packaging.md +233 -0
  148. package/skills/python/references/testing.md +269 -0
  149. package/skills/python/references/typing.md +292 -0
  150. package/skills/qa-tester/SKILL.md +186 -0
  151. package/skills/rust/SKILL.md +50 -0
  152. package/skills/rust/references/async.md +224 -0
  153. package/skills/rust/references/errors.md +240 -0
  154. package/skills/rust/references/ownership.md +263 -0
  155. package/skills/rust/references/testing.md +274 -0
  156. package/skills/rust/references/traits.md +250 -0
  157. package/skills/security-engineer/SKILL.md +157 -0
  158. package/skills/swift/SKILL.md +48 -0
  159. package/skills/swift/references/concurrency.md +280 -0
  160. package/skills/swift/references/idioms.md +334 -0
  161. package/skills/swift/references/testing.md +229 -0
  162. package/skills/typescript/SKILL.md +51 -0
  163. package/skills/typescript/references/async.md +241 -0
  164. package/skills/typescript/references/errors.md +208 -0
  165. package/skills/typescript/references/idioms.md +246 -0
  166. package/skills/typescript/references/testing.md +225 -0
  167. package/skills/typescript/references/tooling.md +208 -0
  168. package/skills/typescript/references/types.md +259 -0
@@ -0,0 +1,186 @@
1
+ ---
2
+ name: qa-tester
3
+ description: Persona skill — think like a QA engineer. Test the edges, write regression tests from bugs, treat flaky as a bug. Overlay on top of language / end skills. For test patterns, see each language's `testing.md`.
4
+ origin: agency-agents-fork + original (https://github.com/msitarzewski/agency-agents, MIT)
5
+ ---
6
+
7
+ # QA Tester
8
+
9
+ Hunt bugs before users do. Treat every fix as a missed test. This is a **mindset overlay** — for runner-specific patterns, see the language skill's `testing.md`.
10
+
11
+ ## When to load
12
+
13
+ - Writing or reviewing tests
14
+ - Thinking about what edges a feature has
15
+ - Triaging a bug report and reproducing it
16
+ - Reviewing a PR for test coverage
17
+ - Deciding what to include in a release's test pass
18
+
19
+ ## The posture
20
+
21
+ 1. **Tests document behaviour.** Code says what; tests say why and under what conditions.
22
+ 2. **Test the edges first.** The happy path rarely breaks. Empty input, boundary values, concurrent access — that's where bugs live.
23
+ 3. **Every bug is a missing test.** Reproduce first (failing test), then fix.
24
+ 4. **Flaky = broken.** Intermittent failure is a bug in the test or the code. Don't normalize "retry."
25
+ 5. **Measure what the user experiences.** Not implementation internals.
26
+ 6. **Prefer fakes over mocks.** A fake that actually works is cheaper to maintain than a mock setup that grows with every method.
27
+ 7. **Coverage is a floor, not a target.** 100% coverage with weak assertions is still untested.
28
+
29
+ ## The edge catalog — what you always try
30
+
31
+ ### Inputs
32
+
33
+ - Empty string / empty array / null / undefined / missing field.
34
+ - Very long (max + 1).
35
+ - Unicode, emoji, RTL text, zero-width joiners.
36
+ - Whitespace variations (leading, trailing, tabs, newlines).
37
+ - Numbers: 0, -1, max int, min int, float precision.
38
+ - Dates: epoch, far future, timezone boundaries, DST transitions.
39
+ - Case: UPPER, lower, MiXeD.
40
+ - Format violations: invalid JSON, malformed URLs, bad UUIDs.
41
+
42
+ ### States
43
+
44
+ - Fresh user vs. existing user.
45
+ - Empty collection vs. one item vs. many.
46
+ - Pagination: first page, last page, out-of-range page.
47
+ - Session just started vs. about to expire vs. expired.
48
+ - Flag on vs. off vs. transitioning.
49
+
50
+ ### Concurrency
51
+
52
+ - Two users edit the same resource.
53
+ - Double-submit.
54
+ - Slow network + rapid clicks.
55
+ - Cancellation / back button mid-request.
56
+ - Retry after partial failure.
57
+
58
+ ### Permissions
59
+
60
+ - Anonymous / authenticated / admin paths.
61
+ - Cross-tenant: can A see B's data?
62
+ - Revoked session mid-request.
63
+
64
+ ### Failures
65
+
66
+ - DB down / slow.
67
+ - Dependency 500 / 429 / timeout.
68
+ - Queue full.
69
+ - Disk full.
70
+ - Clock skew.
71
+
72
+ ### Environment
73
+
74
+ - Small screen, large screen, landscape.
75
+ - Slow network (3G profile), offline.
76
+ - Browsers: latest + one old + one mobile.
77
+ - Locales: different decimal separators, date formats, text direction.
78
+
79
+ ## The test-writing loop
80
+
81
+ 1. **Define the behaviour** you're testing, in plain words.
82
+ 2. **Write a failing test** that demonstrates it.
83
+ 3. **Make it pass** with the smallest change.
84
+ 4. **Add edge cases** one at a time.
85
+ 5. **Check that failures read clearly** — `"expected active=true, got false"` > `"assertion failed"`.
86
+
87
+ ## Reproducing bugs
88
+
89
+ ```
90
+ Bug report: "Submitting the form sometimes fails"
91
+ ```
92
+
93
+ Don't trust the report. Translate to a test:
94
+
95
+ 1. Reproduce locally. If you can't reproduce, the bug may be the report.
96
+ 2. Minimize the reproduction. One test, one symptom.
97
+ 3. Write the test so it fails on current code.
98
+ 4. Fix the code so the test passes.
99
+ 5. Commit test + fix together.
100
+
101
+ The test is the guarantee that the bug doesn't silently come back.
102
+
103
+ ## The test-pyramid discipline
104
+
105
+ ```
106
+
107
+ │ E2E ← few, critical flows, slow
108
+ │ Integration ← moderate, real deps
109
+ │ Unit ← many, fast, focused
110
+
111
+ ```
112
+
113
+ For every feature:
114
+ - Unit tests for pure logic.
115
+ - Integration tests where interesting edges cross boundaries (DB, queue, HTTP).
116
+ - E2E tests for the few flows where "the button sends you home" really matters.
117
+
118
+ Inverting the pyramid (all-E2E) gives slow CI and brittle tests.
119
+
120
+ ## What makes a test good
121
+
122
+ | Property | Why |
123
+ |---|---|
124
+ | Fast | You run it often. Slow = skipped = useless. |
125
+ | Isolated | No dependency on execution order. |
126
+ | Repeatable | Same outcome every run. No "ran it again, passed." |
127
+ | Self-verifying | Pass/fail is automatic, not "the log looks right." |
128
+ | Behaviour-named | `rejects_empty_email` > `test_1`. |
129
+
130
+ Flaky tests fail these and poison the suite.
131
+
132
+ ## Test review checklist (on someone else's PR)
133
+
134
+ - [ ] Is there a test for the change? For non-trivial changes, no test = push back.
135
+ - [ ] Does the test fail without the fix? (You can mentally run it.)
136
+ - [ ] Are edge cases covered, not just happy path?
137
+ - [ ] Any sleeps, real timeouts, or order-dependent state?
138
+ - [ ] Do error messages point you at the right place?
139
+ - [ ] Do the test names describe behaviour clearly?
140
+ - [ ] Any commented-out or skipped tests?
141
+ - [ ] Any assertions with no signal (`assertTrue(true)`, `toEqual(x, x)`)?
142
+ - [ ] Fixtures are small + scoped; no shared mutable globals.
143
+
144
+ ## What you push back on
145
+
146
+ - **"It's just a small change, no test needed."** It's the small changes that break things silently.
147
+ - **Retry / skip to fix flakiness.** That's hiding the bug; fix the root.
148
+ - **Snapshot tests without review discipline.** Change accepted without reading = loss of signal.
149
+ - **Over-mocking (10 mocks for 20 LOC).** The unit is poorly shaped; refactor.
150
+ - **Coverage for coverage's sake.** Tests that hit lines without asserting behaviour.
151
+ - **UI tests that "look at" rather than "act on"** (assert on CSS classes rather than observable effects).
152
+
153
+ ## What you let go
154
+
155
+ - **Testing framework internals.** They have their own tests.
156
+ - **Testing trivial getters / setters.** No risk, no signal.
157
+ - **Testing the framework's routing / DI.** Integration test, not unit.
158
+ - **100% coverage.** Aim for meaningful coverage, not the number.
159
+
160
+ ## Standard categories of bug you always look for
161
+
162
+ - **Off-by-one** — boundary errors in loops, pagination, limits.
163
+ - **Null / undefined / empty** — every access, every field.
164
+ - **Locale / TZ** — dates, numbers, sorting, text direction.
165
+ - **Race / ordering** — in async code, check concurrent interleavings.
166
+ - **Integer overflow / float precision** — money in float, counters that wrap.
167
+ - **Trust boundary** — input that skipped validation, state that leaked privilege.
168
+ - **Idempotency** — retry of any write.
169
+ - **Resource leaks** — connections, file handles, timers.
170
+
171
+ ## Forbidden patterns
172
+
173
+ - Tests that sleep (real time) to wait for async
174
+ - Test names like `test1`, `test_foo2`
175
+ - `it.only` / `describe.only` committed to main
176
+ - Shared global state not reset between tests
177
+ - Snapshot tests for output containing dates / UUIDs / random IDs (without redaction)
178
+ - Production credentials / real emails in tests
179
+ - Tests that call the real external API in unit suite
180
+ - Disabled tests without a tracking issue and a deletion date
181
+
182
+ ## Pair with
183
+
184
+ - The language's `testing.md` reference for runner specifics.
185
+ - [`coding-standards/references/tdd.md`](../coding-standards/references/tdd.md) — the cycle.
186
+ - [`debugging-workflow`](../debugging-workflow/SKILL.md) — when a test reveals a deeper bug.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: rust
3
+ description: Rust language skill — ownership, traits, errors, async, testing. Pair with end skills (`backend`, `devops`) and with `coding-standards` for cross-language principles.
4
+ origin: original
5
+ ---
6
+
7
+ # Rust
8
+
9
+ Ownership, lifetimes, traits, errors, async. **Language-focused**. Architecture → end skills. General principles (TDD, naming, error strategy) → `coding-standards`.
10
+
11
+ ## When to load
12
+
13
+ - Project primary language is Rust
14
+ - Reviewing Rust code
15
+ - Designing types, traits, error hierarchies
16
+ - Async with `tokio` / `async-std`
17
+
18
+ ## Core principles
19
+
20
+ 1. **Make invalid states unrepresentable.** Use the type system — enums, newtypes, bounds — before runtime checks.
21
+ 2. **`Result` for expected failure, `panic!` only for bugs / invariant violations.**
22
+ 3. **Prefer owned types at API boundaries; `&str` / `&[T]` inside.** Callers decide the allocation policy.
23
+ 4. **Small traits, defined at the call site.** Don't pre-abstract.
24
+ 5. **`#[must_use]` on types that ignore-means-bug** (`Result`, builders, guards).
25
+ 6. **No `unwrap()` / `expect()` outside tests and `main`.** The compiler enforces error handling; don't opt out.
26
+ 7. **`clippy::pedantic` in CI.** Treat warnings as errors.
27
+ 8. **`cargo fmt` and `cargo clippy` — never argue style.**
28
+
29
+ ## How to use references
30
+
31
+ | Reference | When to load |
32
+ |---|---|
33
+ | [`references/ownership.md`](references/ownership.md) | Borrowing, lifetimes, move vs. copy, `Rc`/`Arc`, interior mutability |
34
+ | [`references/errors.md`](references/errors.md) | `Result`, `?`, `thiserror`, `anyhow`, error enums, chaining |
35
+ | [`references/traits.md`](references/traits.md) | Traits, generics, `impl Trait`, associated types, trait objects |
36
+ | [`references/async.md`](references/async.md) | `async fn`, futures, `tokio`, cancellation, `select!`, pinning |
37
+ | [`references/testing.md`](references/testing.md) | `#[test]`, integration tests, `cargo test`, property testing |
38
+
39
+ ## Forbidden patterns (auto-reject)
40
+
41
+ - `unwrap()` / `expect()` in non-test, non-`main` code without a comment explaining why it's unreachable
42
+ - `panic!` as control flow
43
+ - `unsafe` without a `// SAFETY:` comment listing every invariant the caller must uphold
44
+ - `.clone()` in a hot loop when a borrow would suffice
45
+ - `Rc<RefCell<T>>` without a thread-sharing story (single-threaded only; use `Arc<Mutex<T>>` if it crosses threads)
46
+ - Blocking calls inside an `async fn` (`std::thread::sleep`, `std::fs::*` in tokio)
47
+ - `#[allow(clippy::...)]` without a nearby comment justifying it
48
+ - Overlarge error enums (40+ variants); split by module
49
+ - Returning `Vec<String>` from a parse when `&str` views into the input would work
50
+ - Ignoring `#[must_use]` on `Result` (compiler warns; treat as error)
@@ -0,0 +1,224 @@
1
+ # Rust — Async
2
+
3
+ `async fn`, futures, `tokio`, cancellation, `select!`, pinning traps.
4
+
5
+ ## `async fn` basics
6
+
7
+ ```rust
8
+ async fn fetch(url: &str) -> Result<String, reqwest::Error> {
9
+ reqwest::get(url).await?.text().await
10
+ }
11
+ ```
12
+
13
+ `async fn` returns a `Future`. The future does nothing until awaited.
14
+
15
+ ```rust
16
+ // ❌ nothing runs
17
+ let f = fetch("https://x.com");
18
+
19
+ // ✅
20
+ let body = fetch("https://x.com").await?;
21
+ ```
22
+
23
+ ## Runtimes
24
+
25
+ Rust doesn't ship a runtime. Pick one:
26
+
27
+ - **tokio** — de facto standard; multi-threaded; huge ecosystem
28
+ - **async-std** — simpler, fewer features; smaller
29
+ - **smol** — lightweight; good for embedded / minimal binaries
30
+
31
+ Most production code is tokio. Mixing runtimes in one process is painful — pick early.
32
+
33
+ ```rust
34
+ #[tokio::main]
35
+ async fn main() -> anyhow::Result<()> {
36
+ run().await
37
+ }
38
+ ```
39
+
40
+ ## `.await` is a suspension point
41
+
42
+ Every `.await` can be where another task takes over. Anything you hold across `.await` must be `Send` if the runtime moves tasks between threads.
43
+
44
+ ```rust
45
+ // ❌ std::sync::MutexGuard isn't Send; holding across .await breaks
46
+ let g = mu.lock().unwrap();
47
+ do_something().await; // compile error on tokio multi-threaded
48
+ g.push(x);
49
+
50
+ // ✅ tokio::sync::Mutex is async-aware
51
+ let mut g = mu.lock().await;
52
+ do_something().await;
53
+ g.push(x);
54
+
55
+ // ✅ or drop the guard first
56
+ {
57
+ let mut g = mu.lock().unwrap();
58
+ g.push(x);
59
+ }
60
+ do_something().await;
61
+ ```
62
+
63
+ ## Concurrency primitives
64
+
65
+ ### `tokio::spawn` — fire a task
66
+
67
+ ```rust
68
+ let handle = tokio::spawn(async move {
69
+ work().await
70
+ });
71
+ let result = handle.await??; // first ? for JoinError, second for inner Result
72
+ ```
73
+
74
+ `spawn` requires `Send + 'static`. For local-only (non-`Send`) work on the current thread, use `tokio::task::spawn_local` inside a `LocalSet`.
75
+
76
+ ### `join!` — run futures in parallel, wait for all
77
+
78
+ ```rust
79
+ let (u, o) = tokio::join!(get_user(id), get_orders(id));
80
+ ```
81
+
82
+ Same task; no threads involved. All futures must be awaitable concurrently.
83
+
84
+ ### `try_join!` — parallel, short-circuit on first error
85
+
86
+ ```rust
87
+ let (u, o) = tokio::try_join!(get_user(id), get_orders(id))?;
88
+ ```
89
+
90
+ ### `select!` — wait on the first of several
91
+
92
+ ```rust
93
+ tokio::select! {
94
+ r = fetch(url) => handle(r),
95
+ _ = tokio::time::sleep(Duration::from_secs(5)) => timeout(),
96
+ _ = ctx.cancelled() => cancelled(),
97
+ }
98
+ ```
99
+
100
+ `select!` drops the non-winning futures (cancellation). Anything side-effectful in the dropped branch must be cancellation-safe. **Many operations aren't** — databases mid-transaction, partial writes. Use `Pin<Box<...>>` + `futures::future::FutureExt::fuse` or structure with cancellation guards.
101
+
102
+ ## Cancellation
103
+
104
+ In Rust async, cancellation = dropping the future. It happens:
105
+ - When a `tokio::select!` branch loses.
106
+ - When a `spawn`ed `JoinHandle` is aborted.
107
+ - When the caller stops awaiting (e.g., `timeout` fires).
108
+
109
+ **Cancellation-safety** is a property of individual futures. Not all are safe to drop mid-flight. Library docs usually say.
110
+
111
+ Rule: critical writes should complete in a non-cancellable section. `tokio::spawn(async { ... }).await` insulates from caller cancellation (but the spawned task gets its own drop path).
112
+
113
+ ## Timeouts
114
+
115
+ ```rust
116
+ use tokio::time::{timeout, Duration};
117
+
118
+ match timeout(Duration::from_secs(5), fetch(url)).await {
119
+ Ok(Ok(body)) => ...,
120
+ Ok(Err(e)) => return Err(e.into()),
121
+ Err(_) => return Err(anyhow!("timeout")),
122
+ }
123
+ ```
124
+
125
+ Budget timeouts across layers — inner < outer.
126
+
127
+ ## Bounded concurrency
128
+
129
+ ```rust
130
+ use futures::stream::{StreamExt, iter};
131
+
132
+ let results: Vec<_> = iter(urls)
133
+ .map(|u| fetch(u))
134
+ .buffer_unordered(10) // 10 in flight
135
+ .collect()
136
+ .await;
137
+ ```
138
+
139
+ `buffer_unordered` for order-insensitive; `buffered` preserves order.
140
+
141
+ ## Channels
142
+
143
+ | Channel | Use |
144
+ |---|---|
145
+ | `tokio::sync::mpsc` | Multi-producer, single-consumer |
146
+ | `tokio::sync::broadcast` | Multi-producer, multi-consumer (fan-out); bounded, drops if lagging |
147
+ | `tokio::sync::watch` | Single-slot "latest value"; good for config / state updates |
148
+ | `tokio::sync::oneshot` | One-shot; request → response |
149
+
150
+ ```rust
151
+ let (tx, mut rx) = tokio::sync::mpsc::channel(100);
152
+ tokio::spawn(async move {
153
+ while let Some(msg) = rx.recv().await {
154
+ process(msg).await;
155
+ }
156
+ });
157
+ tx.send(msg).await?;
158
+ ```
159
+
160
+ Bounded channels = backpressure. Use unbounded sparingly; unbounded sends can run the receiver out of memory.
161
+
162
+ ## Avoid blocking the executor
163
+
164
+ ```rust
165
+ // ❌ blocks the worker thread — starves other tasks
166
+ std::thread::sleep(Duration::from_secs(1));
167
+ std::fs::read_to_string("big.txt")?;
168
+
169
+ // ✅
170
+ tokio::time::sleep(Duration::from_secs(1)).await;
171
+ tokio::fs::read_to_string("big.txt").await?;
172
+
173
+ // ✅ offload CPU-bound / blocking I/O to a blocking-friendly pool
174
+ tokio::task::spawn_blocking(|| do_cpu_work())
175
+ .await?;
176
+ ```
177
+
178
+ A single blocking call on a worker thread pauses every async task on that worker.
179
+
180
+ ## Pinning — the big word, rare in application code
181
+
182
+ Async state machines live in memory at addresses their own self-references assume are stable. `Pin<P<T>>` is how the language tells you "don't move this".
183
+
184
+ You typically only care if you write your own `Future`. With `async fn` + `tokio`, pinning is handled for you. If you see a compile error about `Unpin`, `Pin::new_unchecked`, or "future cannot be unpinned" — that's when you read up. Day-to-day, ignore.
185
+
186
+ ## Structured concurrency — `JoinSet`
187
+
188
+ ```rust
189
+ use tokio::task::JoinSet;
190
+
191
+ let mut set = JoinSet::new();
192
+ for url in urls {
193
+ set.spawn(fetch(url));
194
+ }
195
+ while let Some(res) = set.join_next().await {
196
+ match res {
197
+ Ok(Ok(body)) => use_body(body),
198
+ Ok(Err(e)) => log::warn!("fetch failed: {e}"),
199
+ Err(e) if e.is_panic() => log::error!("task panicked"),
200
+ Err(_) => {},
201
+ }
202
+ }
203
+ ```
204
+
205
+ Better than loose `spawn` + vector of handles — `JoinSet` cleans up on drop.
206
+
207
+ ## Debugging
208
+
209
+ - **tokio-console** — live task inspector; shows stuck tasks, contention.
210
+ - `RUST_LOG=debug` with `tracing-subscriber` for structured async logs.
211
+ - `#[tokio::main(flavor = "current_thread")]` temporarily for deterministic local repros.
212
+
213
+ ## Anti-patterns
214
+
215
+ | Anti-pattern | Fix |
216
+ |---|---|
217
+ | `std::thread::sleep` / `std::fs::*` in async | Use `tokio::*` equivalents or `spawn_blocking` |
218
+ | Holding `std::sync::MutexGuard` across `.await` | Drop before await or use `tokio::sync::Mutex` |
219
+ | Unbounded channels everywhere | Use bounded; apply backpressure |
220
+ | `async` on functions that do no awaiting | Drop `async`; callers shouldn't need to await |
221
+ | `tokio::spawn` with captured `&` references | Move owned data; tasks are `'static` |
222
+ | `.await` inside a loop that could be parallel | `buffer_unordered` or `JoinSet` |
223
+ | `select!` over non-cancellation-safe futures | Read the docs; wrap in `spawn` if unsafe to drop |
224
+ | Runtimes nested in runtimes (calling `Runtime::new().block_on(...)` inside an async fn) | Don't; `.await` or use `spawn_blocking` |
@@ -0,0 +1,240 @@
1
+ # Rust — Errors
2
+
3
+ `Result`, `?`, `thiserror`, `anyhow`. For general strategy, see `coding-standards/references/error-strategy.md`.
4
+
5
+ ## `Result<T, E>` is the contract
6
+
7
+ Expected failure → `Result`. Bugs / invariant violations → `panic!`.
8
+
9
+ ```rust
10
+ fn parse_port(s: &str) -> Result<u16, ParseIntError> {
11
+ s.parse()
12
+ }
13
+ ```
14
+
15
+ `Result` is `#[must_use]`. The compiler warns if you ignore one. Treat warnings as errors.
16
+
17
+ ## The `?` operator
18
+
19
+ Propagate errors with one character.
20
+
21
+ ```rust
22
+ fn load() -> Result<Config, ConfigError> {
23
+ let raw = std::fs::read_to_string("config.yaml")?; // io::Error -> ConfigError (via From)
24
+ let cfg: Config = serde_yaml::from_str(&raw)?; // serde error -> ConfigError
25
+ Ok(cfg)
26
+ }
27
+ ```
28
+
29
+ `?` uses the `From` trait to convert the inner error to the function's error type. Implement `From<SubErr> for TopErr` to get conversion for free (or use `thiserror`).
30
+
31
+ ## Error enums with `thiserror`
32
+
33
+ For library code where callers need to match on variants.
34
+
35
+ ```rust
36
+ use thiserror::Error;
37
+
38
+ #[derive(Debug, Error)]
39
+ pub enum ConfigError {
40
+ #[error("read config file: {0}")]
41
+ Io(#[from] std::io::Error),
42
+
43
+ #[error("parse yaml: {0}")]
44
+ Parse(#[from] serde_yaml::Error),
45
+
46
+ #[error("missing field {field}")]
47
+ MissingField { field: String },
48
+
49
+ #[error("invalid value for {field}: {reason}")]
50
+ InvalidValue { field: String, reason: String },
51
+ }
52
+ ```
53
+
54
+ `#[from]` auto-generates `From<io::Error> for ConfigError`, so `?` just works.
55
+
56
+ ## `anyhow` for applications
57
+
58
+ For `main`, CLI tools, glue code — anywhere callers only need "it failed, here's why".
59
+
60
+ ```rust
61
+ use anyhow::{Context, Result};
62
+
63
+ fn main() -> Result<()> {
64
+ let cfg = load_config("config.yaml")
65
+ .context("loading application config")?;
66
+ run(cfg).context("running application")?;
67
+ Ok(())
68
+ }
69
+ ```
70
+
71
+ `anyhow::Error` is a type-erased error with a chain. `.context()` adds breadcrumbs. The final message reads like:
72
+
73
+ ```
74
+ Error: running application
75
+
76
+ Caused by:
77
+ 0: loading application config
78
+ 1: parse yaml
79
+ 2: missing field: database.url
80
+ ```
81
+
82
+ **Rule**: libraries → `thiserror`; binaries → `anyhow`. Mixing both is fine — libraries use their typed errors; `anyhow::Error` wraps them at the edge.
83
+
84
+ ## Error sources & chaining
85
+
86
+ Every error has a chain. Walk it when logging:
87
+
88
+ ```rust
89
+ fn log_error(err: &dyn std::error::Error) {
90
+ eprintln!("error: {err}");
91
+ let mut source = err.source();
92
+ while let Some(s) = source {
93
+ eprintln!(" caused by: {s}");
94
+ source = s.source();
95
+ }
96
+ }
97
+ ```
98
+
99
+ `anyhow` formats the chain for you. With `thiserror`, implement `Display` to include context, and let callers walk `.source()`.
100
+
101
+ ## Avoid `Box<dyn Error>` in libraries
102
+
103
+ `Box<dyn Error>` erases your error type. Callers lose the ability to match variants. Fine inside `main` or in examples; avoid in public API.
104
+
105
+ ```rust
106
+ // ❌ in a library
107
+ pub fn load() -> Result<Config, Box<dyn Error>> { ... }
108
+
109
+ // ✅
110
+ pub fn load() -> Result<Config, ConfigError> { ... }
111
+ ```
112
+
113
+ ## `unwrap()` / `expect()` — when OK
114
+
115
+ - **Tests** — panic on unexpected failure is fine.
116
+ - **`main`** — as long as the panic message is actionable.
117
+ - **"Statically known impossible"** — document with `.expect("invariant: X")`.
118
+
119
+ ```rust
120
+ // ✅ truly impossible
121
+ let re = Regex::new(r"^\d+$").expect("hardcoded regex is valid");
122
+
123
+ // ❌ lazy
124
+ let line = input.lines().next().unwrap(); // what if input is empty?
125
+ ```
126
+
127
+ `.expect("msg")` is better than `.unwrap()` — the message tells the next reader why this was assumed safe.
128
+
129
+ ## `match` vs. `if let` vs. `?`
130
+
131
+ ```rust
132
+ // ✅ handle both cases
133
+ match parse(s) {
134
+ Ok(v) => use_v(v),
135
+ Err(e) => log::warn!("parse failed: {e}"),
136
+ }
137
+
138
+ // ✅ one case matters
139
+ if let Ok(v) = parse(s) {
140
+ use_v(v);
141
+ }
142
+
143
+ // ✅ propagate
144
+ let v = parse(s)?;
145
+ ```
146
+
147
+ ## Combining errors with `?` across types
148
+
149
+ If two error types coexist in the function, implement `From` or let `anyhow` absorb both.
150
+
151
+ ```rust
152
+ // Typed approach
153
+ impl From<ReqError> for MyError { ... }
154
+ impl From<ParseError> for MyError { ... }
155
+
156
+ // Anyhow approach
157
+ fn load() -> anyhow::Result<Config> {
158
+ let body = reqwest::blocking::get(url)?.text()?;
159
+ let cfg = serde_yaml::from_str(&body)?;
160
+ Ok(cfg)
161
+ }
162
+ ```
163
+
164
+ ## Retries
165
+
166
+ Retry only idempotent operations. No runtime hides bad idempotency.
167
+
168
+ ```rust
169
+ fn with_retry<F, T, E>(mut f: F) -> Result<T, E>
170
+ where F: FnMut() -> Result<T, E>, E: std::fmt::Debug
171
+ {
172
+ let mut wait = 100;
173
+ for attempt in 0..3 {
174
+ match f() {
175
+ Ok(v) => return Ok(v),
176
+ Err(e) if attempt < 2 => {
177
+ log::warn!("attempt {attempt} failed: {e:?}");
178
+ std::thread::sleep(Duration::from_millis(wait));
179
+ wait *= 2;
180
+ }
181
+ Err(e) => return Err(e),
182
+ }
183
+ }
184
+ unreachable!()
185
+ }
186
+ ```
187
+
188
+ For real use, bring a crate (`backoff`, `tokio-retry`). See `backend/references/resilience.md` for when.
189
+
190
+ ## Domain-specific error granularity
191
+
192
+ Keep error enums focused. One giant `AppError` with 40 variants is worse than five module-level error types.
193
+
194
+ ```rust
195
+ // Module a/error.rs
196
+ pub enum Error { /* a-specific */ }
197
+
198
+ // Module b/error.rs
199
+ pub enum Error { /* b-specific */ }
200
+
201
+ // crate top-level (for public API or for anyhow consumers)
202
+ pub enum Error {
203
+ #[error(transparent)] A(#[from] a::Error),
204
+ #[error(transparent)] B(#[from] b::Error),
205
+ }
206
+ ```
207
+
208
+ `#[error(transparent)]` + `#[from]` makes the outer enum forward the inner's message cleanly.
209
+
210
+ ## Panic → catch at the edge
211
+
212
+ Panics in spawned threads or tasks need to be caught or your server silently degrades.
213
+
214
+ ```rust
215
+ // Axum / tower middleware example — pseudo
216
+ tokio::task::spawn(async move {
217
+ match tokio::task::spawn(async { handle(req).await }).await {
218
+ Ok(Ok(resp)) => resp,
219
+ Ok(Err(e)) => to_http_response(e),
220
+ Err(join_err) if join_err.is_panic() => {
221
+ log::error!("handler panicked");
222
+ http_500()
223
+ }
224
+ Err(_) => http_500(),
225
+ }
226
+ });
227
+ ```
228
+
229
+ ## Anti-patterns
230
+
231
+ | Anti-pattern | Fix |
232
+ |---|---|
233
+ | `unwrap()` in library code | Return `Result`, let the caller decide |
234
+ | `panic!("bad input")` on user input | Return a validation error |
235
+ | Stringly-typed errors (`Err("failed".to_string())`) | Typed enum |
236
+ | One `Error::Other(String)` variant used for everything | Split by actual failure mode |
237
+ | Catching every error with `?` and ignoring the chain in logs | Walk `.source()` or use `anyhow` formatting |
238
+ | `Box<dyn Error>` in public lib API | `thiserror`-generated enum |
239
+ | Using `anyhow` inside library code that users might match on | Define a typed error |
240
+ | Silent `.ok()` / `.unwrap_or_default()` that swallows meaningful failures | Handle or propagate |