@loomfsm/bundle-code 0.1.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/LICENSE +201 -0
- package/agents/acceptance.md +141 -0
- package/agents/api-contract.md +89 -0
- package/agents/architect.md +52 -0
- package/agents/challenger-reviewer.md +104 -0
- package/agents/classifier.md +74 -0
- package/agents/code-analyzer.md +43 -0
- package/agents/context-doc-verifier.md +94 -0
- package/agents/dependency-auditor.md +42 -0
- package/agents/implementer.md +135 -0
- package/agents/logic-reviewer.md +132 -0
- package/agents/migration.md +55 -0
- package/agents/performance.md +95 -0
- package/agents/plan-conformance.md +127 -0
- package/agents/plan-grounding-check.md +106 -0
- package/agents/planner.md +143 -0
- package/agents/playwright.md +68 -0
- package/agents/research.md +52 -0
- package/agents/security.md +88 -0
- package/agents/style-reviewer.md +85 -0
- package/agents/test.md +206 -0
- package/agents/ui-consistency.md +75 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +34 -0
- package/dist/manifest.js.map +1 -0
- package/dist/src/bundle.d.ts +2 -0
- package/dist/src/bundle.js +424 -0
- package/dist/src/bundle.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +14 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/invariants.d.ts +10 -0
- package/dist/src/invariants.js +208 -0
- package/dist/src/invariants.js.map +1 -0
- package/dist/src/policy-resolver.d.ts +2 -0
- package/dist/src/policy-resolver.js +65 -0
- package/dist/src/policy-resolver.js.map +1 -0
- package/dist/src/sandbox-rules.d.ts +2 -0
- package/dist/src/sandbox-rules.js +40 -0
- package/dist/src/sandbox-rules.js.map +1 -0
- package/dist/test/bundle.test.d.ts +1 -0
- package/dist/test/bundle.test.js +289 -0
- package/dist/test/bundle.test.js.map +1 -0
- package/dist/test/sandbox-rules.test.d.ts +1 -0
- package/dist/test/sandbox-rules.test.js +73 -0
- package/dist/test/sandbox-rules.test.js.map +1 -0
- package/knowledge/references/api-design.md +188 -0
- package/knowledge/references/arch-patterns.md +106 -0
- package/knowledge/references/caching.md +190 -0
- package/knowledge/references/concurrency.md +195 -0
- package/knowledge/references/db-postgres.md +153 -0
- package/knowledge/references/e2e-flutter.md +56 -0
- package/knowledge/references/e2e-playwright.md +53 -0
- package/knowledge/references/error-handling.md +208 -0
- package/knowledge/references/next-app-router.md +231 -0
- package/knowledge/references/observability.md +169 -0
- package/knowledge/references/optimization-strategy.md +197 -0
- package/knowledge/references/perf-flutter.md +62 -0
- package/knowledge/references/perf-nestjs.md +59 -0
- package/knowledge/references/perf-python.md +50 -0
- package/knowledge/references/perf-react.md +52 -0
- package/knowledge/references/react19.md +176 -0
- package/knowledge/references/redis.md +175 -0
- package/knowledge/references/security-backend.md +219 -0
- package/knowledge/references/test-flutter.md +65 -0
- package/knowledge/references/test-nestjs.md +82 -0
- package/knowledge/references/test-python.md +76 -0
- package/knowledge/references/test-react.md +66 -0
- package/knowledge/references/test-strategy.md +175 -0
- package/knowledge/references/ui-flutter.md +56 -0
- package/knowledge/references/ui-web.md +51 -0
- package/package.json +34 -0
- package/schemas/agent-feedback.schema.json +80 -0
- package/schemas/category-vocab.json +170 -0
- package/schemas/classifier-output.schema.json +53 -0
- package/schemas/finding.schema.json +92 -0
- package/schemas/pipeline-state.schema.json +238 -0
- package/schemas/reviewer-output.schema.json +62 -0
- package/schemas/state-extension.schema.json +53 -0
- package/schemas/validator-output.schema.json +48 -0
- package/stack-candidates.yaml +248 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
tags: [concurrency, async, parallel, race-condition, atomicity, locks, retry]
|
|
3
|
+
stack_signals: []
|
|
4
|
+
summary: |
|
|
5
|
+
Concurrency design and race-condition reasoning — atomicity is bought, not
|
|
6
|
+
assumed. Patterns for Promise.all, async gather, queues, locks, and shared
|
|
7
|
+
state mutation.
|
|
8
|
+
when_to_load: |
|
|
9
|
+
Task touches async functions, parallel work, queues, locks, atomic
|
|
10
|
+
operations, retry/timeout logic, request handlers under load, background
|
|
11
|
+
jobs, or race-condition-prone state mutations. Diff including Promise.all,
|
|
12
|
+
asyncio.gather, parallel HTTP calls, mutex/lock usage, or read-modify-write
|
|
13
|
+
patterns on shared state also qualifies.
|
|
14
|
+
agent_hints: [challenger-reviewer, logic-reviewer, security]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Concurrency — Senior Stance
|
|
18
|
+
|
|
19
|
+
## When this applies
|
|
20
|
+
Load when task touches: async functions, parallel work, queues, locks, atomic operations, retry/timeout logic, request handlers under load, background jobs, race-condition-prone state mutations. Reviewer (especially Challenger) auto-loads when diff includes `Promise.all`, `asyncio.gather`, parallel HTTP calls, mutex/lock usage, or any read-modify-write pattern on shared state.
|
|
21
|
+
|
|
22
|
+
## Default Stance
|
|
23
|
+
Concurrency bugs hide in dev (single user, single thread) and surface in prod (10k QPS). Default to "is there a race condition here?" before "does this look right?". Atomicity is bought, not assumed. Two operations are atomic only when explicitly composed atomically (transaction, single SQL statement, atomic CPU instruction, single Redis command). Everything else is racy.
|
|
24
|
+
|
|
25
|
+
## Patterns (use these)
|
|
26
|
+
|
|
27
|
+
### Atomic operations only via primitives
|
|
28
|
+
- Database: single statement (`UPDATE … SET n = n + 1`) is atomic. Multi-statement requires a transaction with proper isolation.
|
|
29
|
+
- Redis: single command is atomic. Multi-command needs MULTI or Lua script.
|
|
30
|
+
- In-process: language-level atomics (`atomic.AddInt64`, `Mutex`, `synchronized`, etc.) — never your own flag-and-check loop.
|
|
31
|
+
|
|
32
|
+
### Locking strategies
|
|
33
|
+
- **Optimistic locking** — read with version, write `WHERE version = X`. Retry on failure. Best when contention is low.
|
|
34
|
+
- **Pessimistic locking** — `SELECT … FOR UPDATE`. Blocks others. Best when contention is high but lock duration is short.
|
|
35
|
+
- **Distributed lock** — Redis SET NX EX (single-node) for soft mutex. Postgres advisory lock for hard cross-process mutex. NEVER Redlock for hard mutex (see redis.md).
|
|
36
|
+
|
|
37
|
+
### Idempotency over locking
|
|
38
|
+
For external-facing operations, idempotency keys + DB unique constraints often beat distributed locks:
|
|
39
|
+
- Client sends `Idempotency-Key`. Server records it with response. Replays return cached.
|
|
40
|
+
- DB unique constraint prevents duplicates if multiple workers process the same job.
|
|
41
|
+
- Cheaper than locks; failure mode is "rejected duplicate", not "deadlock".
|
|
42
|
+
|
|
43
|
+
### Backpressure
|
|
44
|
+
Every queue has a bounded size. Every worker pool has a bounded count. When full, callers must back off (429 / drop / slow-path) — NOT pile on. Without backpressure, queue grows unbounded → memory pressure → slow shutdowns → cascading outage.
|
|
45
|
+
|
|
46
|
+
### Timeouts everywhere
|
|
47
|
+
Every external call (HTTP, DB query, Redis, RPC) has an explicit timeout. Default = "wait forever" = ticking time bomb.
|
|
48
|
+
- HTTP client timeouts: connect, read, total. Set all three.
|
|
49
|
+
- DB statement timeout (Postgres `statement_timeout`).
|
|
50
|
+
- Test that the timeout actually fires (chaos engineering, fault injection).
|
|
51
|
+
|
|
52
|
+
### Retry policy with jitter
|
|
53
|
+
On transient failures (5xx, timeout, connection-reset):
|
|
54
|
+
- Exponential backoff: `base * 2^attempt`.
|
|
55
|
+
- Jitter: random 0-base added per attempt. Without jitter, retries from many clients synchronize → thundering herd.
|
|
56
|
+
- Cap attempts: 3-5. Beyond that, escalate (DLQ, alert).
|
|
57
|
+
- Don't retry non-idempotent operations without idempotency keys.
|
|
58
|
+
|
|
59
|
+
### Circuit breakers
|
|
60
|
+
Wrap external dependencies in a circuit breaker:
|
|
61
|
+
- **Closed:** normal traffic.
|
|
62
|
+
- **Open:** fail fast (return cached/error) for N seconds. Set when error rate exceeds threshold.
|
|
63
|
+
- **Half-open:** let one request through; if it succeeds, close.
|
|
64
|
+
Saves the downstream from your retry storm during its outage.
|
|
65
|
+
|
|
66
|
+
### Single-writer principle
|
|
67
|
+
For any piece of state, exactly one component writes; everyone else reads. Multi-writer state without coordination = bug pending.
|
|
68
|
+
- DB row contention → queue + single worker per partition.
|
|
69
|
+
- Cache invalidation → write-through from the same component that owns the source.
|
|
70
|
+
- Filesystem mutation → owned by one process.
|
|
71
|
+
|
|
72
|
+
### Lock ordering to prevent deadlocks
|
|
73
|
+
If you must take multiple locks, always acquire them in the same global order. `lock(A) → lock(B)` everywhere; never `lock(B) → lock(A)` somewhere else.
|
|
74
|
+
|
|
75
|
+
### Read-modify-write must be guarded
|
|
76
|
+
```ts
|
|
77
|
+
// BAD
|
|
78
|
+
const v = await store.get(k);
|
|
79
|
+
await store.put(k, v + 1);
|
|
80
|
+
|
|
81
|
+
// GOOD (atomic)
|
|
82
|
+
await store.increment(k);
|
|
83
|
+
// OR
|
|
84
|
+
await store.casUpdate(k, expectedVersion, v + 1);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Anti-Patterns (DO NOT)
|
|
88
|
+
|
|
89
|
+
### Lock then await external call
|
|
90
|
+
```ts
|
|
91
|
+
mutex.acquire();
|
|
92
|
+
const result = await fetch(externalUrl); // 30s
|
|
93
|
+
mutex.release();
|
|
94
|
+
```
|
|
95
|
+
**Why it bites:** lock held for entire external call duration. Other waiters block 30s. External slowdown = your service slowdown × concurrency.
|
|
96
|
+
**Rule:** do work outside the lock. Lock only the critical section that touches shared state.
|
|
97
|
+
|
|
98
|
+
### Concurrent retries without idempotency
|
|
99
|
+
3 clients retry the same `POST /charge` after a timeout. 3 charges happen.
|
|
100
|
+
**Rule:** idempotency keys are mandatory before allowing retries.
|
|
101
|
+
|
|
102
|
+
### Unbounded `Promise.all` / `asyncio.gather`
|
|
103
|
+
```ts
|
|
104
|
+
const results = await Promise.all(items.map(item => fetchOne(item)));
|
|
105
|
+
```
|
|
106
|
+
With `items.length = 10000`, you fire 10000 concurrent requests. DB connection pool exhausted, target service rate-limited, OOM possible.
|
|
107
|
+
**Rule:** bounded concurrency (`p-limit`, `asyncio.Semaphore`, worker pool with cap).
|
|
108
|
+
|
|
109
|
+
### Sleep-based "wait for" loops
|
|
110
|
+
```ts
|
|
111
|
+
while (!ready()) await sleep(100);
|
|
112
|
+
```
|
|
113
|
+
Polls forever, starves event loop, doesn't scale.
|
|
114
|
+
**Rule:** event-driven (subscribe, await condition, future/promise resolved by notifier).
|
|
115
|
+
|
|
116
|
+
### Shared mutable state across requests
|
|
117
|
+
Module-level counter, cache map, "last user" reference.
|
|
118
|
+
**Rule:** request-scoped state, OR explicit mutex/atomic, OR push to external store (Redis).
|
|
119
|
+
|
|
120
|
+
### Async work fired and not awaited
|
|
121
|
+
```ts
|
|
122
|
+
async function handle(req) {
|
|
123
|
+
doExpensiveWorkInBackground(); // unawaited
|
|
124
|
+
return { ok: true };
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
**Why it bites:** rejection unhandled, lifecycle untied to request, lambda may freeze before completion, no error propagation.
|
|
128
|
+
**Rule:** if work is fire-and-forget, push to a real queue (with at-least-once semantics). Don't rely on event loop background tasks.
|
|
129
|
+
|
|
130
|
+
### Default-no-timeout on HTTP/DB clients
|
|
131
|
+
Many libraries default to no timeout. One slow downstream pins your worker forever.
|
|
132
|
+
**Rule:** explicit timeouts at client construction. Reject implicit defaults.
|
|
133
|
+
|
|
134
|
+
### Retrying on non-idempotent operations
|
|
135
|
+
`POST /charge` failed → retry → second charge.
|
|
136
|
+
**Rule:** retries only when (a) idempotency key in place, OR (b) operation is naturally idempotent (PUT with full state, DELETE).
|
|
137
|
+
|
|
138
|
+
### Distributed transactions (2PC across services)
|
|
139
|
+
"Both services must commit OR both rollback."
|
|
140
|
+
**Why it bites:** 2PC has known failure modes (coordinator crashes), needs participant cooperation, has latency cost. Most "distributed transactions" in the wild are buggy.
|
|
141
|
+
**Rule:** use saga pattern (compensating actions on failure) OR design so eventual consistency is acceptable.
|
|
142
|
+
|
|
143
|
+
### `forEach` with async callback
|
|
144
|
+
```ts
|
|
145
|
+
items.forEach(async (i) => await save(i));
|
|
146
|
+
```
|
|
147
|
+
**Why it bites:** `forEach` doesn't await the promises — they all fire concurrently AND the function returns before completion. You think it's sequential; it isn't.
|
|
148
|
+
**Rule:** `for...of` with await for sequential; `Promise.all` (with concurrency cap) for parallel.
|
|
149
|
+
|
|
150
|
+
### Testing without concurrency
|
|
151
|
+
Tests run single-threaded; bug never reproduces. Then prod has 100 QPS, race condition fires.
|
|
152
|
+
**Rule:** test concurrent paths explicitly (parallel calls in a single test, fault injection).
|
|
153
|
+
|
|
154
|
+
## Decision Framework
|
|
155
|
+
|
|
156
|
+
| Situation | Choice |
|
|
157
|
+
|---|---|
|
|
158
|
+
| Counter incremented from many places | Atomic `INCR` (Redis) or DB `UPDATE … SET n = n + 1` |
|
|
159
|
+
| Read-modify-write | Transaction with `SELECT … FOR UPDATE`, OR optimistic lock with version, OR idempotent rewrite |
|
|
160
|
+
| Process N items in parallel | Bounded concurrency (semaphore, worker pool, `p-limit(N)`) |
|
|
161
|
+
| External call inside critical section | Refactor: do call outside, lock only the shared-state mutation |
|
|
162
|
+
| Retry on transient error | Exponential backoff + jitter + cap attempts |
|
|
163
|
+
| Strong cross-service consistency | Saga pattern with compensating actions; avoid 2PC |
|
|
164
|
+
| Mutex across replicas | Postgres advisory lock or single-node Redis lock; NOT Redlock for safety |
|
|
165
|
+
| Background work after request | Real queue with persistence; not fire-and-forget Promise |
|
|
166
|
+
| Long-poll vs WebSocket vs SSE | SSE for server-to-client streaming; WS for bidirectional; polling only when neither available |
|
|
167
|
+
|
|
168
|
+
## Cost Model
|
|
169
|
+
|
|
170
|
+
| Pattern | Cost when wrong |
|
|
171
|
+
|---|---|
|
|
172
|
+
| Lock around external call | 1 slow downstream → all locked-paths slow → cascading outage |
|
|
173
|
+
| Unbounded parallel calls | DB pool exhaustion, target rate limit, OOM |
|
|
174
|
+
| No timeout on HTTP client | One stuck request pins a worker forever |
|
|
175
|
+
| Retry without jitter | Thundering herd amplifies downstream outage |
|
|
176
|
+
| Read-modify-write race | Last-write-wins data corruption; silent until audit catches it |
|
|
177
|
+
| Fire-and-forget background work | Up to 100% of those calls lost on lambda freeze / process restart |
|
|
178
|
+
| Synchronization via sleep loop | Wastes CPU, scales to ~10 concurrent before degrading |
|
|
179
|
+
|
|
180
|
+
## Red Flags in Diff
|
|
181
|
+
|
|
182
|
+
- `Promise.all(arr.map(...))` where `arr.length` could be > 100 → flag (bounded concurrency needed).
|
|
183
|
+
- New `setTimeout(fn, 0)` / `setImmediate` to "fix race condition" → flag (almost always wrong fix).
|
|
184
|
+
- `await fetch(url)` without explicit timeout option → flag.
|
|
185
|
+
- New retry loop without backoff/jitter/cap → flag.
|
|
186
|
+
- `mutex.acquire()` holding across an `await` of an external call → flag.
|
|
187
|
+
- `forEach(async ...)` → flag (doesn't await).
|
|
188
|
+
- New module-level `let`/`var` mutated in a request handler → flag (shared mutable state).
|
|
189
|
+
- Read-then-write on a row without transaction or version check → flag (race condition).
|
|
190
|
+
- `try { await ... } catch {}` swallowing all errors silently → flag.
|
|
191
|
+
- New background task fired with `void doWork()` or unawaited promise → flag.
|
|
192
|
+
- New external HTTP/DB/Redis client constructed inline per request (not pooled) → flag.
|
|
193
|
+
- "Retry forever" loop without exit condition → flag.
|
|
194
|
+
- Distributed lock implemented as `setIfNotExists` then separate `expire` → flag (race window — see redis.md).
|
|
195
|
+
- 2PC / "atomic across services" claim in plan or code → flag for saga refactor.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
tags: [postgres, sql, database, migrations, query-perf, n-plus-one, indexes, backend]
|
|
3
|
+
stack_signals:
|
|
4
|
+
- project_type: [backend, monorepo]
|
|
5
|
+
summary: |
|
|
6
|
+
PostgreSQL query and migration discipline — EXPLAIN ANALYZE before merging,
|
|
7
|
+
N+1 hunting, index design, migration safety on production-sized tables.
|
|
8
|
+
when_to_load: |
|
|
9
|
+
Task touches SQL files, ORM schema (Prisma *.prisma, TypeORM entities,
|
|
10
|
+
SQLAlchemy models), migrations, raw queries, query builders, or DB
|
|
11
|
+
connection setup. Diff including *.sql, schema changes, or query-shape
|
|
12
|
+
changes qualifies.
|
|
13
|
+
agent_hints: [logic-reviewer, performance, challenger-reviewer]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# PostgreSQL — Senior Stance
|
|
17
|
+
|
|
18
|
+
## When this applies
|
|
19
|
+
Load when the task touches: SQL files, ORM schema (Prisma `*.prisma`, TypeORM entities, SQLAlchemy models), migrations, raw queries, query builders, or DB connection setup. Reviewer auto-loads when diff includes `*.sql`, schema changes, or query-shape changes.
|
|
20
|
+
|
|
21
|
+
## Default Stance
|
|
22
|
+
Treat the DB as the slowest and most expensive component. Every query is until-proven-otherwise a potential N+1, missing index, or full table scan. EXPLAIN before merging anything that's not trivially indexed. Migrations on production-sized tables are operational events, not code changes — they are designed for rollback, then run.
|
|
23
|
+
|
|
24
|
+
## Patterns (use these)
|
|
25
|
+
|
|
26
|
+
### Always run EXPLAIN (ANALYZE) on new queries
|
|
27
|
+
For any query touching > 1 table or > 10K rows. Look for:
|
|
28
|
+
- `Seq Scan` on tables > 10K rows → missing index.
|
|
29
|
+
- `Nested Loop` with high outer cardinality → index missing or wrong.
|
|
30
|
+
- `Filter:` removing > 90% of rows → index doesn't cover the predicate.
|
|
31
|
+
- Hash/Sort spilling to disk → query needs rewriting or work_mem tuning.
|
|
32
|
+
|
|
33
|
+
### Index choice
|
|
34
|
+
- **B-tree** — equality, range, ORDER BY. Default.
|
|
35
|
+
- **Partial index** — `WHERE status = 'active'` predicate that hits 5% of rows. Massive win on storage and write cost.
|
|
36
|
+
- **Composite index** — order matters. Leading column = most selective AND most often filtered.
|
|
37
|
+
- **GIN** — JSONB containment, full-text, array containment.
|
|
38
|
+
- **GiST** — geo, range types, similarity.
|
|
39
|
+
- **BRIN** — append-only timestamp columns on huge tables.
|
|
40
|
+
- Covering index (`INCLUDE`) — when query can be answered from index alone (index-only scan).
|
|
41
|
+
|
|
42
|
+
### Transactions and isolation
|
|
43
|
+
- Default `READ COMMITTED` is fine for most. Don't lower it without thinking.
|
|
44
|
+
- `REPEATABLE READ` for read-modify-write that needs to see consistent snapshot. Note: serialization failures must be retried by the caller.
|
|
45
|
+
- `SERIALIZABLE` for true correctness across rows but cost is real — only when needed.
|
|
46
|
+
- Wrap multi-step writes in a transaction. Always.
|
|
47
|
+
- Avoid long-running transactions: they hold locks AND prevent VACUUM from reclaiming dead tuples → table bloat.
|
|
48
|
+
|
|
49
|
+
### Migration safety on big tables
|
|
50
|
+
- Adding a NOT NULL column with default in PG ≥11 → metadata-only on most recent versions, but **verify on the actual PG version** in use. On older versions: rewrite hits whole table → outage on >10M rows.
|
|
51
|
+
- Adding an index → use `CREATE INDEX CONCURRENTLY` outside transaction. Plain `CREATE INDEX` locks writes for duration.
|
|
52
|
+
- Dropping a column → two-phase: ignore in app first (deploy), then drop in next migration. Never drop and deploy together.
|
|
53
|
+
- Renaming a column → never rename live. Add new, dual-write, backfill, switch reads, drop old. Multiple deploys.
|
|
54
|
+
- Foreign key add on populated table → `NOT VALID` then `VALIDATE CONSTRAINT` separately. Validation is fast read-only check; full add takes write lock.
|
|
55
|
+
|
|
56
|
+
### Connection pooling
|
|
57
|
+
- Use a pool. Limit per-instance connections to (max_connections - reserved) / instance_count.
|
|
58
|
+
- For serverless / function compute → use a connection pooler (PgBouncer in transaction mode, RDS Proxy, Supabase pooler). Direct connections from Lambda = burns through max_connections in seconds.
|
|
59
|
+
- Pool size > 20 per instance is almost always wrong; tune with `pg_stat_activity` not by guessing.
|
|
60
|
+
|
|
61
|
+
### N+1 detection
|
|
62
|
+
- ORM lazy-load in a loop is the canonical case.
|
|
63
|
+
- Fix: explicit `include` / `select_related` / `JOIN`, or batch loader (DataLoader pattern).
|
|
64
|
+
- Cost: 100 rows × 5ms per N+1 query = 500ms latency, instead of one 20ms join.
|
|
65
|
+
|
|
66
|
+
## Anti-Patterns (DO NOT)
|
|
67
|
+
|
|
68
|
+
### `SELECT *` in production code
|
|
69
|
+
**Why it bites:** schema evolves, app pulls bytes it doesn't need (network + memory), index-only scan unreachable, breaking change when adding sensitive column.
|
|
70
|
+
**Rule:** explicit column list. Always.
|
|
71
|
+
|
|
72
|
+
### Implicit casts in WHERE
|
|
73
|
+
`WHERE id = '123'` where `id` is `bigint`. PG may not use the index. Worse on JSONB.
|
|
74
|
+
**Rule:** match types in predicates, especially on indexed columns.
|
|
75
|
+
|
|
76
|
+
### `OFFSET N` for pagination on big tables
|
|
77
|
+
**Why it bites:** OFFSET 10000 = read and discard 10000 rows every page. O(N) per page request. Page 1000 = 10M row scans cumulative.
|
|
78
|
+
**Rule:** keyset pagination — `WHERE id > $last_id ORDER BY id LIMIT 50`. Stable, indexable, scales.
|
|
79
|
+
|
|
80
|
+
### `COUNT(*)` on large filtered tables for "total pages"
|
|
81
|
+
**Why it bites:** scans matching rows. Slow on > 1M rows.
|
|
82
|
+
**Rule:** approximate counts (PG `pg_class.reltuples`), or "show next page exists" instead of "total count", or cached count.
|
|
83
|
+
|
|
84
|
+
### `WHERE col IN (subquery returning millions)`
|
|
85
|
+
**Why it bites:** PG builds hash of millions of rows. May spill to disk.
|
|
86
|
+
**Rule:** rewrite as JOIN or EXISTS, or batch the outer query.
|
|
87
|
+
|
|
88
|
+
### Long-running transaction holding locks
|
|
89
|
+
**Why it bites:** blocks DDL, blocks VACUUM, can deadlock writers, table bloat. Especially in ORMs that auto-open transactions per request and a slow handler keeps it open.
|
|
90
|
+
**Rule:** open transaction at last possible moment, commit at first possible moment. Never wait on external API inside a transaction.
|
|
91
|
+
|
|
92
|
+
### `CREATE INDEX` (without CONCURRENTLY) on prod table > 1M rows
|
|
93
|
+
**Why it bites:** AccessExclusiveLock on the table for the index build duration. Writes block. Outage.
|
|
94
|
+
**Rule:** always `CREATE INDEX CONCURRENTLY` on prod-sized tables. Run outside migration framework if needed.
|
|
95
|
+
|
|
96
|
+
### Foreign key without index on referencing column
|
|
97
|
+
**Why it bites:** every UPDATE/DELETE on parent locks-checks all child rows. Without index → full scan → lock contention.
|
|
98
|
+
**Rule:** always index the FK side. ORMs don't always do this automatically — verify.
|
|
99
|
+
|
|
100
|
+
### `TEXT` for unbounded user input without limit
|
|
101
|
+
**Why it bites:** abuse vector. A single 50MB body kills row size, replication lag, query memory.
|
|
102
|
+
**Rule:** explicit `VARCHAR(N)` or `CHECK (length(col) <= N)`.
|
|
103
|
+
|
|
104
|
+
### JSONB as the schema
|
|
105
|
+
Storing all data in `data jsonb` column to "avoid migrations".
|
|
106
|
+
**Why it bites:** no FK, no constraints, can't index efficiently without GIN per query shape, debugging is harder, query planner can't optimize. JSONB is great for sparse/variant data; not as a substitute for schema.
|
|
107
|
+
**Rule:** structured data → columns. Variant/sparse → JSONB.
|
|
108
|
+
|
|
109
|
+
### Generated SQL with string concatenation
|
|
110
|
+
**Why it bites:** SQL injection, query plan cache miss, parser overhead.
|
|
111
|
+
**Rule:** parameterized queries. Always. ORMs do this; raw `pg.query(\`SELECT ... ${userInput}\`)` does not.
|
|
112
|
+
|
|
113
|
+
### "Soft delete" everywhere via `deleted_at`
|
|
114
|
+
**Why it bites:** every query needs `WHERE deleted_at IS NULL`. Forget once → leak. Indexes need partial (`WHERE deleted_at IS NULL`) or they index dead rows. Joins surface deleted rows unexpectedly.
|
|
115
|
+
**Rule:** soft-delete only what truly needs audit/recovery; hard-delete the rest. If soft-deleted, partial-index everything.
|
|
116
|
+
|
|
117
|
+
## Decision Framework
|
|
118
|
+
|
|
119
|
+
| Situation | Choice | Why |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| New filter column on hot read path | Index. Composite if multiple filters together | Filter at index, not after fetch |
|
|
122
|
+
| Pagination on big table | Keyset, not OFFSET | OFFSET is O(N) per page |
|
|
123
|
+
| Multi-row update inside request handler | Transaction with `SELECT FOR UPDATE` | Prevent concurrent overwrite |
|
|
124
|
+
| Table > 100M rows, time-series | Partition by time (monthly/daily) | Scans target one partition |
|
|
125
|
+
| Adding NOT NULL column to prod | Add nullable → backfill → set NOT NULL | Avoid rewrite lock |
|
|
126
|
+
| Need atomic counter | `UPDATE ... SET n = n + 1 RETURNING n` | Single statement is atomic |
|
|
127
|
+
| Concurrent write contention on row | Queue + single worker, OR row-level lock with retry | Don't lock-spin on hot row |
|
|
128
|
+
| Need consistent read across queries | Single transaction with `REPEATABLE READ` | Snapshot stability |
|
|
129
|
+
|
|
130
|
+
## Cost Model (orders of magnitude)
|
|
131
|
+
|
|
132
|
+
| Operation | Time |
|
|
133
|
+
|---|---|
|
|
134
|
+
| Index lookup (B-tree, cached) | 0.1ms |
|
|
135
|
+
| Sequential scan, 1M rows | 100-500ms |
|
|
136
|
+
| Sequential scan, 100M rows | 10-60s — usually unacceptable |
|
|
137
|
+
| Index-only scan | 2-5x faster than index scan + heap fetch |
|
|
138
|
+
| Single transaction commit (fsync) | 1-10ms |
|
|
139
|
+
| FK check on indexed child | 0.1ms |
|
|
140
|
+
| FK check on un-indexed child, 1M rows | 100ms+ |
|
|
141
|
+
|
|
142
|
+
## Red Flags in Diff
|
|
143
|
+
|
|
144
|
+
- Raw query strings using template literals / `format()` with user-derived input → SQL injection.
|
|
145
|
+
- New filter / sort / join column without corresponding index in same migration → flag.
|
|
146
|
+
- `OFFSET` in pagination on table that may grow > 10K rows → flag.
|
|
147
|
+
- New FK without index on the referencing column → flag.
|
|
148
|
+
- `CREATE INDEX` without `CONCURRENTLY` in prod-targeted migration → flag.
|
|
149
|
+
- ORM call inside loop body → N+1 candidate.
|
|
150
|
+
- Transaction wrapping HTTP/external call → flag (long-running transaction risk).
|
|
151
|
+
- New `DROP COLUMN` / `RENAME` in single migration without staged rollout note → flag.
|
|
152
|
+
- `.findMany()` / `.find()` without `take` / `LIMIT` on potentially-large set → flag.
|
|
153
|
+
- New `JSONB` column where 80% of fields are always-present → flag (probably should be columns).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
tags: [e2e, flutter, integration-test, mobile]
|
|
3
|
+
stack_signals:
|
|
4
|
+
- language: [dart]
|
|
5
|
+
- project_type: [mobile, frontend-app]
|
|
6
|
+
summary: |
|
|
7
|
+
Flutter integration test patterns — IntegrationTestWidgetsFlutterBinding,
|
|
8
|
+
Key-based finders, pumpAndSettle, provider-override mocks.
|
|
9
|
+
when_to_load: |
|
|
10
|
+
Task writes Flutter integration tests, OR project has integration_test/
|
|
11
|
+
directory. Validation step asserts end-to-end behavior on a Flutter app.
|
|
12
|
+
agent_hints: [test, acceptance]
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# E2E: Flutter Integration Tests
|
|
16
|
+
|
|
17
|
+
## Detection
|
|
18
|
+
`integration_test/` directory or `pubspec.yaml` with Flutter
|
|
19
|
+
|
|
20
|
+
## Process
|
|
21
|
+
1. Read existing `integration_test/` files for patterns (test groups, pumping, finders)
|
|
22
|
+
2. Write tests for flows in "Manual Test Steps" section of plan
|
|
23
|
+
3. Run: `flutter test integration_test/` (or specific file)
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
- Use `IntegrationTestWidgetsFlutterBinding.ensureInitialized()`
|
|
27
|
+
- Find widgets via `find.byKey`, `find.byType`, `find.text` — prefer `Key` for stability
|
|
28
|
+
- Use `tester.pumpAndSettle()` after actions, not arbitrary delays
|
|
29
|
+
- Mock backend via dependency injection / provider overrides, not real network
|
|
30
|
+
- Group tests with `group()` per feature
|
|
31
|
+
- Test on at least one platform (Android emulator or iOS simulator)
|
|
32
|
+
|
|
33
|
+
## pumpAndSettle Timeout
|
|
34
|
+
Default timeout is 10 seconds. Increase for screens with long animations:
|
|
35
|
+
```dart
|
|
36
|
+
await tester.pumpAndSettle(const Duration(seconds: 30));
|
|
37
|
+
```
|
|
38
|
+
If pumpAndSettle never settles (infinite animation like a progress indicator), use `pump()` with specific duration instead.
|
|
39
|
+
|
|
40
|
+
## Screenshots
|
|
41
|
+
Capture screenshots during tests for debugging or visual regression:
|
|
42
|
+
```dart
|
|
43
|
+
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
44
|
+
await binding.takeScreenshot('step_name');
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## CI Execution
|
|
48
|
+
- Android: run on emulator started in CI (`flutter emulator --launch`)
|
|
49
|
+
- iOS: run on simulator (`open -a Simulator`)
|
|
50
|
+
- Or use Firebase Test Lab / AWS Device Farm for real devices
|
|
51
|
+
- Integration tests require a running device — cannot run headless like unit tests
|
|
52
|
+
|
|
53
|
+
## Platform Permissions
|
|
54
|
+
- Camera, location, storage permissions need to be pre-granted in test setup
|
|
55
|
+
- Android: use `adb shell pm grant` in CI before running tests
|
|
56
|
+
- iOS: use `simctl privacy` to grant permissions to simulator
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
tags: [e2e, playwright, web, integration-test, frontend]
|
|
3
|
+
stack_signals:
|
|
4
|
+
- language: [typescript, javascript]
|
|
5
|
+
- project_type: [frontend-app, monorepo]
|
|
6
|
+
summary: |
|
|
7
|
+
Playwright E2E patterns — page object usage, getByRole / getByLabel /
|
|
8
|
+
getByText selector preference, test.describe per feature.
|
|
9
|
+
when_to_load: |
|
|
10
|
+
Task writes E2E tests, OR project has Playwright config / e2e directory
|
|
11
|
+
with *.spec.ts. Validation step asserts end-to-end behavior on a web
|
|
12
|
+
stack.
|
|
13
|
+
agent_hints: [test, acceptance]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# E2E: Playwright (Web)
|
|
17
|
+
|
|
18
|
+
## Detection
|
|
19
|
+
`e2e/` or `tests/` with `*.spec.ts` + Playwright config
|
|
20
|
+
|
|
21
|
+
## Process
|
|
22
|
+
1. Read existing Playwright tests for structure (page objects, fixtures, helpers)
|
|
23
|
+
2. Write tests for every flow in "Manual Test Steps" section of plan
|
|
24
|
+
3. Run: command from CLAUDE.md (usually `npm run test:e2e`)
|
|
25
|
+
|
|
26
|
+
## Rules
|
|
27
|
+
- Follow existing page object model if project uses one
|
|
28
|
+
- Use existing fixtures and helpers
|
|
29
|
+
- Prefer: `getByRole`, `getByLabel`, `getByText` over CSS selectors
|
|
30
|
+
- Use `test.describe` blocks per feature
|
|
31
|
+
- No `waitForTimeout` — wait for network/element instead
|
|
32
|
+
- Run against local dev server
|
|
33
|
+
|
|
34
|
+
## Authentication
|
|
35
|
+
- Use `storageState` to save/restore auth session (avoid login on every test)
|
|
36
|
+
- Create a `global-setup.ts` that logs in once and saves state
|
|
37
|
+
- Share state via `test.use({ storageState: 'auth.json' })`
|
|
38
|
+
|
|
39
|
+
## API Interception
|
|
40
|
+
- `page.route('**/api/endpoint', handler)` to mock backend responses
|
|
41
|
+
- Use for: testing error states, offline mode, slow network simulation
|
|
42
|
+
- Prefer: intercept at network level, not mocking the fetch function
|
|
43
|
+
|
|
44
|
+
## Debugging
|
|
45
|
+
- `--headed` flag to see browser during development
|
|
46
|
+
- `--trace on` to capture trace for failed tests
|
|
47
|
+
- Trace viewer: `npx playwright show-trace trace.zip`
|
|
48
|
+
- `page.screenshot()` on failure (configure in `playwright.config.ts`)
|
|
49
|
+
|
|
50
|
+
## Parallelism & Isolation
|
|
51
|
+
- Tests run in parallel by default — each test gets fresh browser context
|
|
52
|
+
- Don't share state between tests (no shared variables, no test ordering)
|
|
53
|
+
- Use `test.describe.serial` only when order truly matters
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
tags: [error-handling, retry, fallback, circuit-breaker, exception, resilience]
|
|
3
|
+
stack_signals: []
|
|
4
|
+
summary: |
|
|
5
|
+
Error-handling design — errors are first-class, fail-fast over
|
|
6
|
+
swallow-and-continue. Patterns for retry, fallback, circuit breakers,
|
|
7
|
+
error envelopes, and dead-letter queues.
|
|
8
|
+
when_to_load: |
|
|
9
|
+
Task touches try/catch blocks, error responses, retry logic, circuit
|
|
10
|
+
breakers, fallback paths, error envelopes, exception types, error logging,
|
|
11
|
+
or dead-letter queues. Diff including new external calls, new HTTP
|
|
12
|
+
handlers, new background jobs, or any change to error-handling code also
|
|
13
|
+
qualifies.
|
|
14
|
+
agent_hints: [logic-reviewer, challenger-reviewer, security]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Error Handling — Senior Stance
|
|
18
|
+
|
|
19
|
+
## When this applies
|
|
20
|
+
Load when task touches: try/catch blocks, error responses, retry logic, circuit breakers, fallback paths, error envelopes, exception types, error logging, dead-letter queues. Reviewer auto-loads when diff includes new external calls, new HTTP handlers, new background jobs, or any change to error-handling code.
|
|
21
|
+
|
|
22
|
+
## Default Stance
|
|
23
|
+
Errors are first-class. Every external call can fail; every input can be malformed; every assumption can be violated. The question is never "what if it fails?" — it's "how does it fail, and what's the right user-facing outcome?". Default to fail-fast and surface (with proper logging) over swallow-and-continue. Resilience comes from explicit policy (retry, fallback, degrade), not from defensive `try/catch` everywhere.
|
|
24
|
+
|
|
25
|
+
## Patterns (use these)
|
|
26
|
+
|
|
27
|
+
### Error categorization (decide once, route consistently)
|
|
28
|
+
- **Validation** (4xx) — caller's fault, no retry, surface to user. Don't log as error (noise).
|
|
29
|
+
- **Authentication / authorization** (401/403) — caller's fault, no retry.
|
|
30
|
+
- **Not found** (404) — caller's fault OR auth-by-existence; never expose internal.
|
|
31
|
+
- **Conflict** (409) — caller's fault (idempotency-key conflict, optimistic-lock fail). May retry with new key.
|
|
32
|
+
- **Rate limit** (429) — caller's fault, retry with backoff after `Retry-After`.
|
|
33
|
+
- **Transient downstream** (502/503/504) — not caller's fault, retry with backoff.
|
|
34
|
+
- **Internal error** (500) — server's fault, alert, do NOT retry blindly (might be deterministic bug).
|
|
35
|
+
|
|
36
|
+
### Retry policy (per category)
|
|
37
|
+
- Idempotent + transient (5xx, timeout, connection-reset): exponential backoff + jitter, cap 3-5 attempts.
|
|
38
|
+
- Non-idempotent: retry only with idempotency key. Otherwise — fail-fast.
|
|
39
|
+
- 4xx (caller's fault): never retry.
|
|
40
|
+
- 429: respect `Retry-After`. If header missing, default backoff.
|
|
41
|
+
|
|
42
|
+
### Circuit breaker
|
|
43
|
+
Wrap each external dependency:
|
|
44
|
+
- **Closed** (normal): pass through.
|
|
45
|
+
- **Open** (when error rate > threshold over window): fail-fast for N seconds.
|
|
46
|
+
- **Half-open**: let one request through; success → close.
|
|
47
|
+
- Saves the downstream from your retry storm during its outage.
|
|
48
|
+
|
|
49
|
+
### Fallback path
|
|
50
|
+
For non-critical features, define a "degraded" answer:
|
|
51
|
+
- Recommendation engine times out → return popular items.
|
|
52
|
+
- Personalization service down → return generic content.
|
|
53
|
+
- Cache miss + DB slow → serve stale cache.
|
|
54
|
+
Document the fallback in code AND in the dashboard so operators see "we're degraded, not broken".
|
|
55
|
+
|
|
56
|
+
### Error envelope (consistent shape)
|
|
57
|
+
For HTTP:
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"error": {
|
|
61
|
+
"code": "VALIDATION_ERROR",
|
|
62
|
+
"message": "Email is required",
|
|
63
|
+
"details": [{ "field": "email", "rule": "required" }],
|
|
64
|
+
"request_id": "req_abc123"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
Same shape for every error. Frontend has one error parser, one error UI.
|
|
69
|
+
|
|
70
|
+
### Typed errors
|
|
71
|
+
Distinguish error categories at the type level:
|
|
72
|
+
- TS: `class ValidationError extends Error`, `class NotFoundError extends Error`, etc.
|
|
73
|
+
- Python: `class ValidationError(BaseException)` hierarchy.
|
|
74
|
+
- Rust/Go: `Result<T, E>` with enum E.
|
|
75
|
+
|
|
76
|
+
Handlers can `instanceof` / pattern-match to decide the right HTTP status and log level.
|
|
77
|
+
|
|
78
|
+
### Fail-fast on unknown state
|
|
79
|
+
If state is corrupt/inconsistent, crash the request (or process) loudly rather than continue with bad data. A loud failure is debuggable; a silently propagating bug is not.
|
|
80
|
+
|
|
81
|
+
### Dead-letter queue (DLQ)
|
|
82
|
+
For background jobs, after retry exhaustion → push to DLQ. Don't drop. Don't loop forever.
|
|
83
|
+
- DLQ size monitored; alert when non-zero growth rate.
|
|
84
|
+
- Operator can inspect, fix, replay.
|
|
85
|
+
|
|
86
|
+
### Error context preservation
|
|
87
|
+
When wrapping/rethrowing:
|
|
88
|
+
- TS: `throw new Error('parse failed', { cause: originalError })` — keeps stack chain.
|
|
89
|
+
- Python: `raise NewError(...) from original` — same.
|
|
90
|
+
- Don't bury the original. Log the chain when surfacing.
|
|
91
|
+
|
|
92
|
+
### Timeouts as deliberate errors
|
|
93
|
+
Every external call has a timeout. Timeout fires → that's a normal error path, not a panic. Handle it: retry (if eligible), fallback, return 503 to caller.
|
|
94
|
+
|
|
95
|
+
## Anti-Patterns (DO NOT)
|
|
96
|
+
|
|
97
|
+
### Empty catch blocks
|
|
98
|
+
```ts
|
|
99
|
+
try { await externalCall(); } catch (e) {}
|
|
100
|
+
```
|
|
101
|
+
**Why it bites:** error swallowed, no log, no metric. Bug invisible until prod incident.
|
|
102
|
+
**Rule:** every catch logs OR rethrows OR has explicit "ignore-because-X" comment with reasoning.
|
|
103
|
+
|
|
104
|
+
### Catch-all at the top of every function
|
|
105
|
+
```ts
|
|
106
|
+
async function handleRequest() {
|
|
107
|
+
try {
|
|
108
|
+
// entire body
|
|
109
|
+
} catch (e) {
|
|
110
|
+
return { error: 'something went wrong' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
**Why it bites:** loses error categorization, no proper status code, no useful logs. Caller can't tell validation error from infrastructure failure.
|
|
115
|
+
**Rule:** centralized error middleware/handler that maps typed errors → HTTP responses. Inner code throws specific error types; top-level translates.
|
|
116
|
+
|
|
117
|
+
### Logging "error" for every catch including expected ones
|
|
118
|
+
Validation failure logs at ERROR level; oncall paged; turns out it's user typo.
|
|
119
|
+
**Rule:** ERROR for unexpected; WARN for expected-but-noteworthy; INFO for normal flow. Validation failures = INFO or DEBUG.
|
|
120
|
+
|
|
121
|
+
### Error message includes stack trace in user-facing response
|
|
122
|
+
`{ "error": "TypeError: cannot read property 'foo' of undefined at ..."}` — leaks internal structure, security risk, terrible UX.
|
|
123
|
+
**Rule:** user gets `code` + safe `message` + `request_id`. Operators look up `request_id` in logs to see the stack.
|
|
124
|
+
|
|
125
|
+
### Retry without idempotency
|
|
126
|
+
Retry storm on `POST /charge` → 3 charges. Real prod incident waiting to happen.
|
|
127
|
+
**Rule:** retry only with idempotency key OR for naturally idempotent ops (PUT full state, DELETE).
|
|
128
|
+
|
|
129
|
+
### Retry without backoff/jitter
|
|
130
|
+
Tight retry loop: target down → 100 clients × 10 retries × 0 delay = 1000 RPS during downstream outage. Outage prolonged.
|
|
131
|
+
**Rule:** exponential backoff + jitter + max attempts. Always.
|
|
132
|
+
|
|
133
|
+
### Generic `Error` for everything
|
|
134
|
+
`throw new Error('user not found')` then catch and `instanceof Error` check. Can't distinguish from any other error.
|
|
135
|
+
**Rule:** typed error classes. `class NotFoundError extends Error`. Handler matches type → status code.
|
|
136
|
+
|
|
137
|
+
### Wrapping every error in a generic envelope, losing original
|
|
138
|
+
```ts
|
|
139
|
+
catch (e) { throw new InternalError('failed') }
|
|
140
|
+
```
|
|
141
|
+
Original cause lost. Debug requires guessing.
|
|
142
|
+
**Rule:** include `cause`/`from`. Preserve the chain.
|
|
143
|
+
|
|
144
|
+
### "Just retry" as the only resilience strategy
|
|
145
|
+
Retries are useful for transient failures, useless for deterministic bugs. Retrying a SQL syntax error 5 times wastes time.
|
|
146
|
+
**Rule:** distinguish transient (retry) from deterministic (fail-fast, alert). Don't retry 4xx, deterministic 5xx, parse errors.
|
|
147
|
+
|
|
148
|
+
### Throw-then-catch as control flow
|
|
149
|
+
Using exceptions for normal branching (e.g., "user not found" as a normal flow path) → exceptions are slow + obscure intent.
|
|
150
|
+
**Rule:** sentinel return values (`null`, `Option`, `Result`) for expected absence. Exceptions for unexpected.
|
|
151
|
+
|
|
152
|
+
### Error logged AND returned to caller
|
|
153
|
+
Same error logged at every layer it bubbles through → 5 log lines per error → log volume × users.
|
|
154
|
+
**Rule:** log once, at the boundary where the error is surfaced. Inner layers rethrow without logging.
|
|
155
|
+
|
|
156
|
+
### Background job retries forever
|
|
157
|
+
No max attempts → poisoned message loops forever, eats workers, blocks queue.
|
|
158
|
+
**Rule:** max attempts. Then DLQ. Then alert.
|
|
159
|
+
|
|
160
|
+
### `process.exit(1)` in library code
|
|
161
|
+
Library kills the host process on error. Caller can't recover.
|
|
162
|
+
**Rule:** library throws; only the application's main loop / signal handler decides whether to exit.
|
|
163
|
+
|
|
164
|
+
## Decision Framework
|
|
165
|
+
|
|
166
|
+
| Failure | Response |
|
|
167
|
+
|---|---|
|
|
168
|
+
| Caller sent invalid input | 4xx with error envelope; log INFO |
|
|
169
|
+
| Caller not authenticated | 401; log INFO |
|
|
170
|
+
| Resource not found | 404; log INFO unless suspicious pattern |
|
|
171
|
+
| Idempotency-key conflict | 409 with previous response; log INFO |
|
|
172
|
+
| Downstream HTTP timeout | retry (idempotent) or 503 (non-idempotent); log WARN |
|
|
173
|
+
| Downstream HTTP 5xx | retry with backoff; log WARN |
|
|
174
|
+
| Downstream rate-limited (429) | respect Retry-After; log WARN |
|
|
175
|
+
| Database connection lost | retry once with new connection; if fail, 503; log ERROR |
|
|
176
|
+
| Validation passes but business rule violated | 422 with specifics; log INFO |
|
|
177
|
+
| Unexpected exception | 500 with generic message + request_id; log ERROR + alert |
|
|
178
|
+
| Background job fails | retry per policy; on exhaustion → DLQ + alert |
|
|
179
|
+
| Critical invariant violated mid-request | log ERROR + abort request (don't return partial bad data) |
|
|
180
|
+
|
|
181
|
+
## Cost Model
|
|
182
|
+
|
|
183
|
+
| Pattern | Cost when wrong |
|
|
184
|
+
|---|---|
|
|
185
|
+
| Empty catch block | Bug invisible; surfaces only as user complaint or incident |
|
|
186
|
+
| Retry without idempotency | Duplicate writes; data corruption; potential financial loss |
|
|
187
|
+
| No circuit breaker on flaky downstream | Your service degrades when downstream does; cascading outage |
|
|
188
|
+
| Generic 500 on validation errors | Frontend shows "something went wrong"; UX suffers; oncall paged falsely |
|
|
189
|
+
| No DLQ on background jobs | Silent data loss; processing gaps invisible |
|
|
190
|
+
| Stack trace in user response | Information leak; security review failure |
|
|
191
|
+
| Excess error logging | Log volume cost; signal-to-noise drops; real errors hidden |
|
|
192
|
+
|
|
193
|
+
## Red Flags in Diff
|
|
194
|
+
|
|
195
|
+
- `try { ... } catch (e) {}` empty catch → flag.
|
|
196
|
+
- `try { ... } catch (e) { console.log(e); }` log-and-continue without rethrow or specific handling → flag (likely swallowing).
|
|
197
|
+
- New external call without timeout option → flag.
|
|
198
|
+
- New retry loop without exponential backoff + jitter + cap → flag.
|
|
199
|
+
- New retry on a non-idempotent op without idempotency key → flag.
|
|
200
|
+
- New catch-all in a request handler returning generic error → flag (use error middleware).
|
|
201
|
+
- `throw new Error('...')` for distinct error categories without typed subclasses → flag.
|
|
202
|
+
- Stack trace / internal type included in error response body → flag (info leak).
|
|
203
|
+
- New background job without DLQ destination on exhaustion → flag.
|
|
204
|
+
- Logging at ERROR level for expected paths (validation, 404 from search) → flag (alert noise).
|
|
205
|
+
- New `process.exit` / `os._exit` / `panic!` outside main entrypoint → flag.
|
|
206
|
+
- Error message constructed by string-concatenating user input → flag (log injection).
|
|
207
|
+
- New error envelope shape that doesn't match the project's existing format → flag (drift).
|
|
208
|
+
- Catching base `Exception` / `Error` and silently mapping to 200 success response → flag immediately.
|