@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.
- package/README.md +34 -3
- package/dist/commands/cardAdd.d.ts +1 -1
- package/dist/commands/cardAdd.d.ts.map +1 -1
- package/dist/commands/cardAdd.js +16 -6
- package/dist/commands/cardAdd.js.map +1 -1
- package/dist/commands/cardDashboard.js +1 -1
- package/dist/commands/cardDashboard.js.map +1 -1
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -314
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hookCommand.d.ts.map +1 -1
- package/dist/commands/hookCommand.js +6 -7
- package/dist/commands/hookCommand.js.map +1 -1
- package/dist/commands/pmCommand.js +1 -1
- package/dist/commands/pmCommand.js.map +1 -1
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +60 -37
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +3 -30
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/commands/tick.js +1 -1
- package/dist/commands/tick.js.map +1 -1
- package/dist/core/checklist.d.ts +22 -0
- package/dist/core/checklist.d.ts.map +1 -0
- package/dist/core/checklist.js +38 -0
- package/dist/core/checklist.js.map +1 -0
- package/dist/core/checklist.test.d.ts +2 -0
- package/dist/core/checklist.test.d.ts.map +1 -0
- package/dist/core/checklist.test.js +74 -0
- package/dist/core/checklist.test.js.map +1 -0
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/config.test.js +7 -4
- package/dist/core/config.test.js.map +1 -1
- package/dist/core/context.d.ts +1 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +197 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +190 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/engines/EventHandler.test.js +3 -3
- package/dist/engines/EventHandler.test.js.map +1 -1
- package/dist/engines/MonitorEngine.js +2 -2
- package/dist/engines/MonitorEngine.js.map +1 -1
- package/dist/engines/SchedulerEngine.js +1 -1
- package/dist/engines/SchedulerEngine.js.map +1 -1
- package/dist/engines/StageEngine.js +3 -3
- package/dist/engines/StageEngine.js.map +1 -1
- package/dist/engines/engine-pipeline-adapter.test.js +2 -2
- package/dist/engines/engine-pipeline-adapter.test.js.map +1 -1
- package/dist/interfaces/TaskBackend.d.ts +3 -1
- package/dist/interfaces/TaskBackend.d.ts.map +1 -1
- package/dist/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/dist/models/types.d.ts +16 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.d.ts +2 -1
- package/dist/providers/MarkdownTaskBackend.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.js +28 -5
- package/dist/providers/MarkdownTaskBackend.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +5 -7
- package/dist/providers/registry.js.map +1 -1
- package/package.json +1 -1
- package/project-template/.claude/hooks/start.sh +44 -0
- package/project-template/.claude/settings.json +1 -1
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- 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 |
|