@coralai/sps-cli 0.42.0 → 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/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +40 -53
- package/dist/commands/projectInit.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/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/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/package.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,226 @@
|
|
|
1
|
+
# Commits and Pull Requests
|
|
2
|
+
|
|
3
|
+
Small commits, small PRs, clear messages. The boring hygiene that pays off during incidents.
|
|
4
|
+
|
|
5
|
+
## One logical change per commit
|
|
6
|
+
|
|
7
|
+
A commit should be a coherent unit — reverting it should be safe, and the message should explain *why*.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
# ✅ good commit progression
|
|
11
|
+
1. refactor: extract UserValidator from UserService (no behavior change)
|
|
12
|
+
2. test: add failing test for empty-email case
|
|
13
|
+
3. fix(user): reject empty email in validator
|
|
14
|
+
4. docs: note the new error response in the API doc
|
|
15
|
+
|
|
16
|
+
# ❌ "WIP", "fix stuff", "address review", "final changes final v2"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If you can't describe the commit in one clear sentence, split it.
|
|
20
|
+
|
|
21
|
+
## Commit messages — Conventional Commits
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
<type>(<scope>): <summary>
|
|
25
|
+
|
|
26
|
+
<body: why the change, not what>
|
|
27
|
+
|
|
28
|
+
<footer: tickets, breaking changes, co-authors>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Types
|
|
32
|
+
|
|
33
|
+
| Type | Use for |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `feat` | New user-visible feature |
|
|
36
|
+
| `fix` | Bug fix |
|
|
37
|
+
| `perf` | Performance improvement (behavior unchanged) |
|
|
38
|
+
| `refactor` | Code change with no behavior change |
|
|
39
|
+
| `docs` | Docs only |
|
|
40
|
+
| `test` | Tests only |
|
|
41
|
+
| `build` | Build system, deps, tooling |
|
|
42
|
+
| `ci` | CI config |
|
|
43
|
+
| `chore` | Housekeeping, version bumps |
|
|
44
|
+
| `style` | Formatting only |
|
|
45
|
+
| `revert` | Reverts a previous commit |
|
|
46
|
+
|
|
47
|
+
### Example
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
fix(auth): reject tokens with `alg: none`
|
|
51
|
+
|
|
52
|
+
A crafted JWT with `alg: none` bypassed signature verification on the
|
|
53
|
+
/admin endpoints because we used a default-permissive parser. This locks
|
|
54
|
+
the accepted algorithm set to HS256 and RS256 and rejects everything else
|
|
55
|
+
at parse time.
|
|
56
|
+
|
|
57
|
+
Refs: SEC-412
|
|
58
|
+
Co-Authored-By: Alice <alice@example.com>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Rules:
|
|
62
|
+
- **Summary line ≤ 72 chars**, imperative mood ("add", not "added").
|
|
63
|
+
- **Body wraps at 72**, separated from summary by blank line.
|
|
64
|
+
- Explain *why*; the diff already shows *what*.
|
|
65
|
+
- Reference tickets / incidents.
|
|
66
|
+
|
|
67
|
+
## Subject line quick rules
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
✅ feat(orders): support partial shipments
|
|
71
|
+
✅ fix(api): return 422 instead of 500 on malformed JSON
|
|
72
|
+
✅ perf(search): precompute embedding lookup table
|
|
73
|
+
|
|
74
|
+
❌ Updated file
|
|
75
|
+
❌ More changes
|
|
76
|
+
❌ Fix bug
|
|
77
|
+
❌ Small tweaks
|
|
78
|
+
❌ WIP
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If you find yourself writing "and" in a commit message, you're committing two things. Split.
|
|
82
|
+
|
|
83
|
+
## Atomic commits = safe rollbacks
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
git revert <commit> # cleanly reverses one logical change
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If one commit touches migrations, feature code, and unrelated formatting, you can't revert safely. Splitting pays off the first time something goes wrong.
|
|
90
|
+
|
|
91
|
+
## PR size — keep it reviewable
|
|
92
|
+
|
|
93
|
+
| Diff size | What's reasonable |
|
|
94
|
+
|---|---|
|
|
95
|
+
| < 100 lines | Normal feature increment |
|
|
96
|
+
| 100–400 | Upper bound for typical work |
|
|
97
|
+
| 400+ | Only for: rename, generated code, new directory from a template |
|
|
98
|
+
| 1000+ | You're hiding a mistake |
|
|
99
|
+
|
|
100
|
+
Split by:
|
|
101
|
+
- Behavior change vs. refactor — separate PRs
|
|
102
|
+
- Independent feature pieces — stacked PRs
|
|
103
|
+
- Rename / formatting — always its own PR
|
|
104
|
+
|
|
105
|
+
Reviewers have finite attention. A 2000-line PR gets a rubber stamp, not a review.
|
|
106
|
+
|
|
107
|
+
## Branching
|
|
108
|
+
|
|
109
|
+
Pick one and stick to it.
|
|
110
|
+
|
|
111
|
+
- **Trunk-based** — short-lived branches, merge to main daily. Best for most teams.
|
|
112
|
+
- **GitFlow** — `develop` + `release/*` + `hotfix/*`. Heavier; makes sense for shipped products with long support branches.
|
|
113
|
+
- **Feature branches + PR to main** — pragmatic middle ground.
|
|
114
|
+
|
|
115
|
+
Whichever you pick, the rule holds: short-lived branches. Long-lived branches drift and merge pain compounds.
|
|
116
|
+
|
|
117
|
+
## Branch naming
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
<type>/<short-slug>
|
|
121
|
+
|
|
122
|
+
feat/partial-shipments
|
|
123
|
+
fix/auth-alg-none
|
|
124
|
+
chore/bump-node-20
|
|
125
|
+
refactor/extract-validator
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Include a ticket id if your team tracks them: `fix/SEC-412-alg-none`.
|
|
129
|
+
|
|
130
|
+
## The PR description
|
|
131
|
+
|
|
132
|
+
A good PR description saves the reviewer 10 minutes.
|
|
133
|
+
|
|
134
|
+
```markdown
|
|
135
|
+
## What
|
|
136
|
+
Support partial shipments on orders.
|
|
137
|
+
|
|
138
|
+
## Why
|
|
139
|
+
Warehouse can't always fulfill the full order in one go; today they either
|
|
140
|
+
ship late or cancel. Closes ORDERS-187.
|
|
141
|
+
|
|
142
|
+
## How
|
|
143
|
+
- New `Shipment` aggregate, `Order.shipments: []Shipment`
|
|
144
|
+
- `POST /orders/:id/shipments` creates one
|
|
145
|
+
- Order status derives from shipment state (`partial` if any unshipped items)
|
|
146
|
+
|
|
147
|
+
## Tests
|
|
148
|
+
- Unit: shipment state transitions
|
|
149
|
+
- Integration: POST /shipments, GET /orders/:id reflects partial status
|
|
150
|
+
- Manual: verified on staging with 2-item order, shipped one item, refunded another
|
|
151
|
+
|
|
152
|
+
## Rollout
|
|
153
|
+
- Feature flag: `partial_shipments_enabled` (default off)
|
|
154
|
+
- Migration: adds `shipments` table; safe on empty + large tables (indexed)
|
|
155
|
+
- Rollback: disable flag; table can stay
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
If the team has a PR template, use it — even for "obvious" PRs. Reviewer context is never obvious to the reviewer.
|
|
159
|
+
|
|
160
|
+
## Rebase vs. merge
|
|
161
|
+
|
|
162
|
+
- **Rebase your feature branch on main** before opening a PR to get a clean linear history.
|
|
163
|
+
- **Squash on merge** to main if the PR has noisy intermediate commits; keep it unsquashed if the history is clean and meaningful.
|
|
164
|
+
- **Never rebase a branch other people are working on.** Force-push on a shared branch is how you make enemies.
|
|
165
|
+
|
|
166
|
+
## `.gitignore` — keep secrets and junk out
|
|
167
|
+
|
|
168
|
+
Default entries for any repo:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
# dependencies
|
|
172
|
+
node_modules/
|
|
173
|
+
.venv/
|
|
174
|
+
target/
|
|
175
|
+
|
|
176
|
+
# env / secrets
|
|
177
|
+
.env
|
|
178
|
+
.env.local
|
|
179
|
+
*.pem
|
|
180
|
+
*.key
|
|
181
|
+
|
|
182
|
+
# IDE
|
|
183
|
+
.idea/
|
|
184
|
+
.vscode/
|
|
185
|
+
*.swp
|
|
186
|
+
|
|
187
|
+
# OS
|
|
188
|
+
.DS_Store
|
|
189
|
+
Thumbs.db
|
|
190
|
+
|
|
191
|
+
# build
|
|
192
|
+
dist/
|
|
193
|
+
build/
|
|
194
|
+
*.pyc
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Check before the first commit. Once a secret lands in git history, you must rotate it — `git rm` doesn't remove history.
|
|
198
|
+
|
|
199
|
+
## Commit discipline during review
|
|
200
|
+
|
|
201
|
+
If review asks for changes, commit the change, don't amend silently. The reviewer needs to see the delta.
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
# ✅
|
|
205
|
+
git commit -m "fix(auth): handle empty role list per review"
|
|
206
|
+
git push
|
|
207
|
+
|
|
208
|
+
# ❌
|
|
209
|
+
git commit --amend --no-edit
|
|
210
|
+
git push --force-with-lease # reviewer's already-read diff is now lost
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Squash at merge time if you want a clean final history.
|
|
214
|
+
|
|
215
|
+
## Anti-patterns
|
|
216
|
+
|
|
217
|
+
| Anti-pattern | Fix |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `git add -A` with no check | Review with `git status` / `git diff --staged` first |
|
|
220
|
+
| Committing with `--no-verify` to skip hooks | Fix the hook failure; don't bypass |
|
|
221
|
+
| Force-push to a shared branch | Rebase your own branch only |
|
|
222
|
+
| 5000-line PR "final cleanup" | Split into: rename / format / real change |
|
|
223
|
+
| Empty PR description | Write what / why / how, even for small changes |
|
|
224
|
+
| "WIP" / "fix" commit messages that ship to main | Squash on merge if they're noise |
|
|
225
|
+
| Mixing a dependency bump with a feature | Separate PRs |
|
|
226
|
+
| Merging your own PR without review | Team agrees on when this is OK (e.g., docs only) |
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Error Strategy
|
|
2
|
+
|
|
3
|
+
When to raise, when to return a result, when to log, when to retry. Language-neutral decisions. For syntax (`try/except`, chaining, exception groups), see the language skill.
|
|
4
|
+
|
|
5
|
+
## The question tree
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Is this an expected outcome?
|
|
9
|
+
yes → return a result (Option, Result, ValidationResult, nullable)
|
|
10
|
+
no ↓
|
|
11
|
+
Can this layer recover meaningfully?
|
|
12
|
+
yes → catch specifically, handle, don't re-raise
|
|
13
|
+
no ↓
|
|
14
|
+
Propagate. Let the edge (HTTP handler, CLI, worker) decide.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
One sentence: **expected = data, unexpected = exception, recoverable = catch narrowly, else propagate.**
|
|
18
|
+
|
|
19
|
+
## Expected vs. exceptional
|
|
20
|
+
|
|
21
|
+
| Case | Treatment | Why |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| User input invalid | Return errors to the caller | User mistakes are expected; not exceptional |
|
|
24
|
+
| Item not found on lookup | Return `None` / `Option::None` | Missing is a normal outcome |
|
|
25
|
+
| DB connection dropped | Raise | Infrastructure failure; unexpected |
|
|
26
|
+
| Optimistic-lock version conflict | Raise a domain `ConflictError` | Expected under concurrency; caller decides retry |
|
|
27
|
+
| Programmer bug (null where not-null) | Raise (let it crash) | Hiding this creates a zombie system |
|
|
28
|
+
|
|
29
|
+
The dividing line: if it's part of the normal business flow, return data; if it breaks the flow, raise.
|
|
30
|
+
|
|
31
|
+
## Catch only what you can handle
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
# ❌ swallow-all
|
|
35
|
+
try:
|
|
36
|
+
do_thing()
|
|
37
|
+
except Exception:
|
|
38
|
+
pass # now it looks fine; tomorrow something real is lost
|
|
39
|
+
|
|
40
|
+
# ❌ swallow-and-log (better, still bad if you can't recover)
|
|
41
|
+
try:
|
|
42
|
+
do_thing()
|
|
43
|
+
except Exception as e:
|
|
44
|
+
log.error("oops: %s", e)
|
|
45
|
+
|
|
46
|
+
# ✅ catch the specific type you know how to handle
|
|
47
|
+
try:
|
|
48
|
+
return cache.get(key)
|
|
49
|
+
except CacheUnavailable:
|
|
50
|
+
return source.load(key) # real fallback
|
|
51
|
+
|
|
52
|
+
# ✅ the rest propagates
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Rule: if catching doesn't let you recover, re-raise. The higher layer knows what to do.
|
|
56
|
+
|
|
57
|
+
## Exception hierarchy
|
|
58
|
+
|
|
59
|
+
Per service / module, one base, several subclasses for kinds of failure.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
AppError (base)
|
|
63
|
+
├── ValidationError # 4xx-ish
|
|
64
|
+
├── NotFoundError # 404
|
|
65
|
+
├── ConflictError # 409
|
|
66
|
+
├── AuthError # 401/403
|
|
67
|
+
└── ExternalServiceError # 502/503
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- The edge (HTTP handler) maps `NotFoundError` → 404, `AuthError` → 401, etc. One translation layer, not scattered throughout the app.
|
|
71
|
+
- Never leak framework exceptions (`SQLAlchemyIntegrityError`, `httpx.TimeoutException`) above the adapter. Translate at the adapter boundary into domain errors.
|
|
72
|
+
|
|
73
|
+
## Preserve the cause
|
|
74
|
+
|
|
75
|
+
When re-raising as a different type, chain the original so the traceback still makes sense. Every language has a form of `raise new from original`. Use it.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
try:
|
|
79
|
+
response = http.get(url)
|
|
80
|
+
except NetworkTimeout as e:
|
|
81
|
+
raise ExternalServiceError("pricing down") from e
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Losing the cause makes incidents take twice as long to diagnose.
|
|
85
|
+
|
|
86
|
+
## Result / Option types
|
|
87
|
+
|
|
88
|
+
Some languages (Rust, Kotlin, Swift, Haskell) prefer typed results over exceptions. The calculus is the same, syntax differs:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
# Result-style
|
|
92
|
+
fn find_user(id: UserId) -> Result<User, FindError>
|
|
93
|
+
|
|
94
|
+
match find_user(id):
|
|
95
|
+
Ok(user) -> ...
|
|
96
|
+
Err(FindError::NotFound) -> ...
|
|
97
|
+
Err(FindError::DbDown) -> ...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
In languages without sum types, the idiomatic equivalent is `Optional` for missing + exceptions for failure.
|
|
101
|
+
|
|
102
|
+
Don't fight the language. Use its native style.
|
|
103
|
+
|
|
104
|
+
## Validation — return data, don't raise
|
|
105
|
+
|
|
106
|
+
User-facing validation almost always returns data, not an exception. You want to collect *all* errors, not stop at the first.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
result = validate(input) # returns { ok: false, errors: [...] }
|
|
110
|
+
if not result.ok:
|
|
111
|
+
return 422, result.errors
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Reserve exceptions for truly exceptional validation — "the request doesn't even parse as JSON", for example.
|
|
115
|
+
|
|
116
|
+
## Retries — only at the right layer
|
|
117
|
+
|
|
118
|
+
Retries belong at the boundary that knows the call is retry-safe. Not inside a domain method, not sprinkled at every layer.
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
# ✅
|
|
122
|
+
edge → retrier(timeout=5s, attempts=3) → adapter → network
|
|
123
|
+
|
|
124
|
+
# ❌
|
|
125
|
+
adapter retries, service retries, handler retries
|
|
126
|
+
# → user waits 5× the timeout; retry storms on the dependency
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
See `backend/references/resilience.md` for retry patterns; this skill just says *where* the retry sits.
|
|
130
|
+
|
|
131
|
+
## Logging exceptions
|
|
132
|
+
|
|
133
|
+
Log at the layer that decided not to re-raise. Logging at every level produces log storms and duplicate reports.
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
# Bad — logged three times, same error
|
|
137
|
+
handler: log.error(e); raise
|
|
138
|
+
service: log.error(e); raise
|
|
139
|
+
adapter: log.error(e); raise
|
|
140
|
+
|
|
141
|
+
# Good — log once, at the boundary
|
|
142
|
+
handler: log.exception(...); return 500
|
|
143
|
+
service: (no log, propagate)
|
|
144
|
+
adapter: (no log, propagate)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Include context in the log (request id, user id, inputs), never the secret values.
|
|
148
|
+
|
|
149
|
+
## `finally` — for cleanup only
|
|
150
|
+
|
|
151
|
+
Use `finally` / equivalent for release-the-resource logic (close file, release lock, restore state). Never put business logic in `finally`; it runs even on errors.
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
# ✅
|
|
155
|
+
try:
|
|
156
|
+
lock.acquire()
|
|
157
|
+
do_critical_section()
|
|
158
|
+
finally:
|
|
159
|
+
lock.release()
|
|
160
|
+
|
|
161
|
+
# ❌
|
|
162
|
+
try:
|
|
163
|
+
submit_order()
|
|
164
|
+
finally:
|
|
165
|
+
send_confirmation_email() # sends even if submit failed
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Crash early, crash clearly
|
|
169
|
+
|
|
170
|
+
When an invariant breaks (null where not-null, state impossibility, config missing at boot), fail loudly at the earliest point. Don't try to "keep going".
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
# ✅ at boot
|
|
174
|
+
assert config.db_url, "DB_URL is required"
|
|
175
|
+
|
|
176
|
+
# ❌
|
|
177
|
+
db_url = config.db_url or "localhost" # silently masked; prod points at localhost
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
A service that refuses to start is easier to fix than one that pretends to work.
|
|
181
|
+
|
|
182
|
+
## Anti-patterns
|
|
183
|
+
|
|
184
|
+
| Anti-pattern | Fix |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `try: ... except: pass` | Catch specific, handle or re-raise |
|
|
187
|
+
| Converting exception into `return None` | Caller loses the "why" — expose the error |
|
|
188
|
+
| Re-raising with a new message but no `from` | Traceback becomes useless |
|
|
189
|
+
| Logging and re-raising at every layer | Log once, at the decider |
|
|
190
|
+
| Catching `Exception` at a tight scope | You don't actually know what can happen there |
|
|
191
|
+
| Control flow via exceptions (`raise StopIteration` to break a loop) | Use explicit flow |
|
|
192
|
+
| Different error shapes for similar failures | Pick one base + specific subclasses |
|
|
193
|
+
| Leaking adapter exceptions to the domain | Translate at the boundary |
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Naming
|
|
2
|
+
|
|
3
|
+
Names are the cheapest form of documentation and the most-read part of a codebase.
|
|
4
|
+
|
|
5
|
+
## The test
|
|
6
|
+
|
|
7
|
+
If you can name something clearly, you understand it. If every name you try is bad, the concept is bad.
|
|
8
|
+
|
|
9
|
+
## Principles
|
|
10
|
+
|
|
11
|
+
1. **Intention over implementation.** `active_users`, not `filtered_list`. `is_expired`, not `flag`.
|
|
12
|
+
2. **Domain over mechanics.** `price_in_cents` over `value_int`. Names should read like the business, not the code.
|
|
13
|
+
3. **Consistent vocabulary.** Pick one word per concept. `fetch / retrieve / load / get` → choose one, stick to it. Same for `user / account / customer` — they are different things; don't conflate.
|
|
14
|
+
4. **Searchable.** A two-letter name is unsearchable. `i` in a three-line loop is fine; `i` as a module-level variable is not.
|
|
15
|
+
5. **No encodings.** `strName`, `iCount`, `bFlag` (Hungarian notation) adds noise without signal — languages have types.
|
|
16
|
+
6. **Avoid disinformation.** `user_list` that isn't actually a list is a trap. `account_map` that's a list of pairs is a trap.
|
|
17
|
+
|
|
18
|
+
## Functions — verbs
|
|
19
|
+
|
|
20
|
+
Describe what they do, not how.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
# ✅
|
|
24
|
+
calculate_total(order)
|
|
25
|
+
send_welcome_email(user)
|
|
26
|
+
mark_as_read(notification)
|
|
27
|
+
|
|
28
|
+
# ❌
|
|
29
|
+
process(x) # process what?
|
|
30
|
+
handle_thing(data) # what thing? what handling?
|
|
31
|
+
do_work() # cool
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Booleans — predicates
|
|
35
|
+
|
|
36
|
+
`is_`, `has_`, `can_`, `should_` prefixes read naturally in conditionals.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
if is_active and has_permission(READ) and not is_expired: ...
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Avoid double negatives: `is_not_disabled` is a trap. Flip to `is_enabled`.
|
|
43
|
+
|
|
44
|
+
## Collections — plural nouns
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
users : list of User
|
|
48
|
+
user : single User
|
|
49
|
+
users_by_id : map from id to User
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Don't name the container in the name: `user_list` is redundant when the type is `list[User]`. Reserve `_list` / `_map` suffixes for when you have multiple collections of the same thing and need to disambiguate.
|
|
53
|
+
|
|
54
|
+
## Numbers — include units
|
|
55
|
+
|
|
56
|
+
Units in the name prevent entire categories of bugs.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
# ✅
|
|
60
|
+
timeout_seconds = 30
|
|
61
|
+
price_cents = 2599
|
|
62
|
+
file_size_bytes = 1024
|
|
63
|
+
latency_ms = 87
|
|
64
|
+
|
|
65
|
+
# ❌
|
|
66
|
+
timeout = 30 # seconds? ms?
|
|
67
|
+
price = 25.99 # USD? EUR?
|
|
68
|
+
size = 1024 # bytes? KB? items?
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Mixed-unit bugs (feet vs meters) have crashed spacecraft. They crash billing systems too.
|
|
72
|
+
|
|
73
|
+
## Variables — scope dictates length
|
|
74
|
+
|
|
75
|
+
Short scope → short name. Long scope → longer, more descriptive name.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
# OK in a three-line loop
|
|
79
|
+
for i, x in enumerate(items):
|
|
80
|
+
xs[i] = f(x)
|
|
81
|
+
|
|
82
|
+
# Not OK at module scope
|
|
83
|
+
x = load_all_records() # x? x of what?
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Functions returning booleans
|
|
87
|
+
|
|
88
|
+
Read as a predicate in an `if`.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
# ✅
|
|
92
|
+
if user.can_edit(document): ...
|
|
93
|
+
|
|
94
|
+
# ❌
|
|
95
|
+
if user.edit_permission(document): ... # reads weird
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Async / concurrent
|
|
99
|
+
|
|
100
|
+
Mark functions that do I/O or schedule work.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
async def fetch_user(id): ...
|
|
104
|
+
def fetch_user_async(id): ... # in langs without `async` marker
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Callers need to know they're awaiting something.
|
|
108
|
+
|
|
109
|
+
## Types — nouns, in the singular
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
class User: ... # a user
|
|
113
|
+
class UserRepository: ... # manages users
|
|
114
|
+
class UserNotFoundError: ... # specific, actionable
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Avoiding "magic"
|
|
118
|
+
|
|
119
|
+
Replace literal numbers and strings with named constants when the meaning is non-obvious.
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
# ❌
|
|
123
|
+
if user.age >= 18: ...
|
|
124
|
+
sleep(86400)
|
|
125
|
+
|
|
126
|
+
# ✅
|
|
127
|
+
LEGAL_ADULT_AGE = 18
|
|
128
|
+
ONE_DAY_SECONDS = 86400
|
|
129
|
+
|
|
130
|
+
if user.age >= LEGAL_ADULT_AGE: ...
|
|
131
|
+
sleep(ONE_DAY_SECONDS)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Exception: one-off small numbers that read naturally — `x * 2`, `str[0:10]` — don't need constants.
|
|
135
|
+
|
|
136
|
+
## Events / notifications — past tense, subject-first
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
OrderPlaced, OrderShipped, UserRegistered, PaymentFailed
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Not `PlaceOrder` (that's a command), not `OrderIsShipped` (noise). Events describe things that happened; commands describe things requested.
|
|
143
|
+
|
|
144
|
+
## Files, modules, packages
|
|
145
|
+
|
|
146
|
+
- **Lowercase, hyphen- or underscore-separated**, depending on the language.
|
|
147
|
+
- Name by what's inside, not by mechanics. `user_service.py` > `helpers.py`.
|
|
148
|
+
- Avoid kitchen-sink files: `utils`, `common`, `misc`, `helpers` — they become dumping grounds.
|
|
149
|
+
|
|
150
|
+
## Endpoints, routes
|
|
151
|
+
|
|
152
|
+
- Plural resource nouns: `/users`, `/orders/{id}/items`.
|
|
153
|
+
- Kebab-case, not snake_case or camelCase in paths: `/user-sessions`, not `/userSessions`.
|
|
154
|
+
- No verbs when CRUD fits: `POST /orders`, not `POST /createOrder`.
|
|
155
|
+
|
|
156
|
+
## Feature flags
|
|
157
|
+
|
|
158
|
+
Describe the capability, not the date, not the ticket.
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
# ✅
|
|
162
|
+
checkout_v2_enabled
|
|
163
|
+
redis_session_store_enabled
|
|
164
|
+
|
|
165
|
+
# ❌
|
|
166
|
+
temp_flag_jan_2026
|
|
167
|
+
jira_1234_rollout
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Flags outlive the ticket and the engineer who wrote it. Name for what the flag *does*.
|
|
171
|
+
|
|
172
|
+
## Renaming is cheap; bad names compound
|
|
173
|
+
|
|
174
|
+
IDEs rename safely across projects. If a name is wrong, fix it now. Six months of new code built on top of a confused name will be six months of confusion.
|
|
175
|
+
|
|
176
|
+
## Anti-patterns
|
|
177
|
+
|
|
178
|
+
| Anti-pattern | Fix |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `data`, `info`, `item`, `manager`, `handler` | Say what it actually represents |
|
|
181
|
+
| `foo_list` when it's a set | Use `foo_set` or just `foo` + typed container |
|
|
182
|
+
| Ambiguous pairs: `user` / `account` / `customer` interchangeably | Define the distinction; use the right one |
|
|
183
|
+
| Mixed naming conventions in one file | Follow the language's convention; don't invent your own |
|
|
184
|
+
| Abbreviations that aren't standard | `usr`, `mngr`, `proc` — spell them out |
|
|
185
|
+
| Numbered copies: `process1`, `process2`, `processFinal`, `processFinalV2` | Delete the old ones; one canonical name |
|