forgehive 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +631 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +6265 -0
  4. package/dist/confirm.d.ts +1 -0
  5. package/dist/confirm.js +15 -0
  6. package/dist/fulfillment-catalog.d.ts +10 -0
  7. package/dist/fulfillment-catalog.js +119 -0
  8. package/dist/lookup-table.d.ts +16 -0
  9. package/dist/lookup-table.js +128 -0
  10. package/dist/rollback.d.ts +1 -0
  11. package/dist/rollback.js +9 -0
  12. package/dist/scanner.d.ts +2 -0
  13. package/dist/scanner.js +87 -0
  14. package/dist/types.d.ts +13 -0
  15. package/dist/types.js +1 -0
  16. package/dist/update.d.ts +7 -0
  17. package/dist/update.js +42 -0
  18. package/dist/writer.d.ts +3 -0
  19. package/dist/writer.js +63 -0
  20. package/forgehive/agents/eli.yaml +8 -0
  21. package/forgehive/agents/kai.yaml +8 -0
  22. package/forgehive/agents/nora.yaml +8 -0
  23. package/forgehive/agents/remy.yaml +8 -0
  24. package/forgehive/agents/sam.yaml +8 -0
  25. package/forgehive/agents/suki.yaml +8 -0
  26. package/forgehive/agents/vera.yaml +24 -0
  27. package/forgehive/agents/viktor.yaml +8 -0
  28. package/forgehive/commands/fh-hotfix.md +16 -0
  29. package/forgehive/commands/fh-review.md +16 -0
  30. package/forgehive/commands/fh-ship.md +18 -0
  31. package/forgehive/commands/fh-start-task.md +13 -0
  32. package/forgehive/hooks/guardrails.sh +17 -0
  33. package/forgehive/party/defaults.yaml +21 -0
  34. package/forgehive/skills/INDEX.yaml +64 -0
  35. package/forgehive/skills/expert/.gitkeep +0 -0
  36. package/forgehive/skills/expert/api-design.md +77 -0
  37. package/forgehive/skills/expert/auth-security.md +80 -0
  38. package/forgehive/skills/expert/clean-architecture.md +83 -0
  39. package/forgehive/skills/expert/code-review.md +81 -0
  40. package/forgehive/skills/expert/database-patterns.md +81 -0
  41. package/forgehive/skills/expert/error-handling.md +90 -0
  42. package/forgehive/skills/expert/gdpr-compliance.md +64 -0
  43. package/forgehive/skills/expert/git-conventions.md +84 -0
  44. package/forgehive/skills/expert/monorepo-patterns.md +91 -0
  45. package/forgehive/skills/expert/observability.md +98 -0
  46. package/forgehive/skills/expert/owasp-top10.md +153 -0
  47. package/forgehive/skills/expert/performance-patterns.md +79 -0
  48. package/forgehive/skills/expert/security-checklist.md +70 -0
  49. package/forgehive/skills/expert/testing-strategies.md +74 -0
  50. package/forgehive/skills/expert/typescript-patterns.md +62 -0
  51. package/forgehive/skills/templates/.gitkeep +0 -0
  52. package/forgehive/templates/claude-md.block.md +62 -0
  53. package/package.json +30 -0
@@ -0,0 +1,18 @@
1
+ You are running the ForgeHive ship workflow. Follow all steps before declaring ready to merge.
2
+
3
+ ## Pre-Ship Checklist
4
+
5
+ 1. **Tests** — run the test suite from `package.json` scripts
6
+ - If tests fail: stop here, fix before proceeding
7
+ 2. **Diff summary** — `git diff main...HEAD --stat`
8
+ - Show the user what changed
9
+ 3. **Uncommitted work** — `git status`
10
+ - Commit or stash before proceeding
11
+ 4. **Code quality scan**
12
+ - Any `console.log` / `debugger` / TODO in the diff?
13
+ - Any commented-out code?
14
+ - Any hardcoded secrets or API keys?
15
+ 5. **PR draft**
16
+ - Title: under 70 chars, describes the change (not "fix stuff")
17
+ - Body: 2-3 bullets of what changed and why
18
+ 6. **Ask**: "Soll ich den PR erstellen?" — only create if confirmed
@@ -0,0 +1,13 @@
1
+ You are starting a new development task using the ForgeHive workflow.
2
+
3
+ ## Pre-Task Checklist
4
+
5
+ 1. Ask the user: **"Was baust du heute?"** (or "What are you building today?")
6
+ 2. Read `.forgehive/capabilities.yaml` — understand the available stack
7
+ 3. Check `.forgehive/memory/MEMORY.md` — load relevant project context
8
+ 4. Run `fh scan --check` to verify the codebase snapshot is current
9
+ 5. Create a feature branch:
10
+ - Convention from `git-conventions` skill: `feat/<short-description>`, `fix/<issue>`, `chore/<task>`
11
+ - Run: `git checkout -b <branch-name>`
12
+ 6. Summarize: what you're building, which files are likely to change, which capabilities apply
13
+ 7. Check `.forgehive/skills/INDEX.yaml` for relevant expert skills
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ # ForgeHive Guardrails — PreToolUse hook for Bash
3
+ # Receives tool input as JSON on stdin. Exit 1 to block, exit 0 to allow.
4
+
5
+ INPUT=$(cat 2>/dev/null || echo "{}")
6
+ COMMAND=$(node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{process.stdout.write(JSON.parse(d).command||'')}catch{process.stdout.write('')}})" 2>/dev/null <<< "$INPUT" || echo "")
7
+
8
+ DANGEROUS=("git push --force" "git push -f " "git reset --hard" "rm -rf /" "DROP TABLE" "DROP DATABASE")
9
+
10
+ for PATTERN in "${DANGEROUS[@]}"; do
11
+ if echo "$COMMAND" | grep -qi "$PATTERN"; then
12
+ echo "🛡 ForgeHive Guardrail: '$PATTERN' geblockt. Erkläre die Absicht und versuche erneut."
13
+ exit 1
14
+ fi
15
+ done
16
+
17
+ exit 0
@@ -0,0 +1,21 @@
1
+ sets:
2
+ design:
3
+ agents: [suki, viktor]
4
+ trigger: "/design-party"
5
+ description: "UX + Architektur parallel"
6
+ build:
7
+ agents: [viktor, kai, sam]
8
+ trigger: "/party"
9
+ description: "Architektur + Engineering + QA"
10
+ review:
11
+ agents: [kai, sam, eli]
12
+ trigger: "/review-party"
13
+ description: "Code Review + QA + Doku"
14
+ full:
15
+ agents: [nora, eli, remy, suki, viktor, kai, sam]
16
+ trigger: "/full-party"
17
+ description: "Alle Agenten"
18
+ security:
19
+ agents: [vera, sam]
20
+ trigger: "/security-party"
21
+ description: "Security Review + QA"
@@ -0,0 +1,64 @@
1
+ # ForgeHive Skills Index
2
+ # Claude: load only the skills relevant to your current task.
3
+ # Check 'when' field to decide which skills to load.
4
+
5
+ skills:
6
+ typescript-patterns:
7
+ file: expert/typescript-patterns.md
8
+ tags: [typescript, types, generics, decorators, utility-types]
9
+ when: "TypeScript types, generics, utility types, decorators"
10
+
11
+ testing-strategies:
12
+ file: expert/testing-strategies.md
13
+ tags: [testing, tdd, mocking, assertions, coverage, vitest, jest]
14
+ when: "Writing tests, TDD, mocking dependencies, test design"
15
+
16
+ api-design:
17
+ file: expert/api-design.md
18
+ tags: [api, rest, graphql, openapi, versioning, endpoints]
19
+ when: "Designing API endpoints, REST conventions, OpenAPI specs"
20
+
21
+ git-conventions:
22
+ file: expert/git-conventions.md
23
+ tags: [git, commits, branches, pr, conventional-commits, merge]
24
+ when: "Git workflow, commit messages, branch naming, PR process"
25
+
26
+ error-handling:
27
+ file: expert/error-handling.md
28
+ tags: [errors, exceptions, result-types, retry, fallbacks, typescript]
29
+ when: "Error handling, exceptions, Result/Either types, retry logic"
30
+
31
+ security-checklist:
32
+ file: expert/security-checklist.md
33
+ tags: [security, auth, xss, sql-injection, owasp, jwt, csrf]
34
+ when: "Security review, authentication, authorization, input validation"
35
+
36
+ performance-patterns:
37
+ file: expert/performance-patterns.md
38
+ tags: [performance, caching, profiling, optimization, lazy-loading, memoization]
39
+ when: "Performance optimization, caching strategies, profiling"
40
+
41
+ code-review:
42
+ file: expert/code-review.md
43
+ tags: [review, quality, feedback, pr-review, checklist]
44
+ when: "Code review, PR feedback, quality assessment"
45
+
46
+ clean-architecture:
47
+ file: expert/clean-architecture.md
48
+ tags: [architecture, domain, use-cases, clean-arch, ddd, layers]
49
+ when: "Architecture design, layer separation, domain modeling, DDD"
50
+
51
+ database-patterns:
52
+ file: expert/database-patterns.md
53
+ tags: [database, sql, migrations, indexing, orm, postgres, prisma]
54
+ when: "Database design, migrations, query optimization, indexing"
55
+
56
+ monorepo-patterns:
57
+ file: expert/monorepo-patterns.md
58
+ tags: [monorepo, workspaces, turborepo, nx, packages, pnpm]
59
+ when: "Monorepo setup, workspace management, build orchestration"
60
+
61
+ observability:
62
+ file: expert/observability.md
63
+ tags: [logging, metrics, tracing, opentelemetry, monitoring, datadog, sentry]
64
+ when: "Structured logging, metrics (RED/USE), distributed tracing"
File without changes
@@ -0,0 +1,77 @@
1
+ # API Design
2
+
3
+ ## REST Resource Design
4
+
5
+ ```
6
+ GET /users → list users (paginated)
7
+ POST /users → create user
8
+ GET /users/:id → get user
9
+ PATCH /users/:id → partial update
10
+ DELETE /users/:id → delete
11
+
12
+ POST /users/:id/activate → actions as sub-resources
13
+ ```
14
+
15
+ **Naming:** plural nouns for collections, snake_case in JSON, kebab-case in URLs.
16
+
17
+ ## Request / Response Shape
18
+
19
+ ```typescript
20
+ // Response envelope
21
+ interface ApiResponse<T> {
22
+ data: T;
23
+ meta?: { total: number; page: number; limit: number };
24
+ }
25
+
26
+ // Error envelope — always consistent
27
+ interface ApiError {
28
+ error: { code: string; message: string; field?: string };
29
+ }
30
+ ```
31
+
32
+ ## Status Codes
33
+
34
+ | Code | When |
35
+ |---|---|
36
+ | 200 | Successful GET, PATCH |
37
+ | 201 | Successful POST (resource created) |
38
+ | 204 | Successful DELETE (no body) |
39
+ | 400 | Validation error (include field) |
40
+ | 401 | Not authenticated |
41
+ | 403 | Authenticated but not authorized |
42
+ | 404 | Resource not found |
43
+ | 409 | Conflict (duplicate, stale version) |
44
+ | 422 | Semantically invalid (passes schema, fails business rules) |
45
+ | 429 | Rate limited |
46
+ | 500 | Server error (never leak internals) |
47
+
48
+ ## Versioning
49
+
50
+ Prefix: `/v1/users` — never break existing clients, deprecate with sunset headers.
51
+
52
+ ## Pagination
53
+
54
+ ```json
55
+ GET /users?page=2&limit=20
56
+
57
+ {
58
+ "data": [...],
59
+ "meta": { "total": 143, "page": 2, "limit": 20, "hasNext": true }
60
+ }
61
+ ```
62
+
63
+ ## Validation
64
+
65
+ - Validate at the boundary (route handler) — not deep in business logic
66
+ - Return all validation errors at once, not just the first
67
+ - Use JSON Schema or Zod for input validation
68
+
69
+ ## Anti-Patterns
70
+
71
+ | Avoid | Use instead |
72
+ |---|---|
73
+ | Verbs in URLs (`/getUser`) | Nouns + HTTP method |
74
+ | `200` for errors | Correct 4xx/5xx |
75
+ | Leaking stack traces | Structured error codes |
76
+ | Inconsistent field naming | Single convention (snake_case) |
77
+ | Boolean flags in body | Semantic sub-resources or PATCH |
@@ -0,0 +1,80 @@
1
+ # Authentication & Session Security
2
+
3
+ ## Secure Password Handling
4
+
5
+ ```typescript
6
+ import bcrypt from "bcrypt";
7
+
8
+ const SALT_ROUNDS = 12;
9
+
10
+ export async function hashPassword(plain: string): Promise<string> {
11
+ return bcrypt.hash(plain, SALT_ROUNDS);
12
+ }
13
+
14
+ export async function verifyPassword(plain: string, hash: string): Promise<boolean> {
15
+ return bcrypt.compare(plain, hash);
16
+ }
17
+ ```
18
+
19
+ ## JWT Best Practices
20
+
21
+ ```typescript
22
+ import jwt from "jsonwebtoken";
23
+
24
+ const JWT_SECRET = process.env.JWT_SECRET;
25
+ if (!JWT_SECRET || JWT_SECRET.length < 32) {
26
+ throw new Error("JWT_SECRET must be at least 32 characters");
27
+ }
28
+
29
+ export function signToken(userId: string): string {
30
+ return jwt.sign({ userId }, JWT_SECRET!, {
31
+ expiresIn: "15m",
32
+ issuer: "myapp",
33
+ audience: "myapp-client",
34
+ });
35
+ }
36
+
37
+ export function verifyToken(token: string): { userId: string } {
38
+ return jwt.verify(token, JWT_SECRET!, {
39
+ issuer: "myapp",
40
+ audience: "myapp-client",
41
+ }) as { userId: string };
42
+ }
43
+ ```
44
+
45
+ ## Session Security
46
+
47
+ - Use `httpOnly` and `secure` cookie flags
48
+ - Set `SameSite=Strict` or `SameSite=Lax`
49
+ - Rotate session tokens after privilege escalation
50
+ - Implement absolute session timeout (e.g., 8 hours)
51
+ - Implement idle timeout (e.g., 30 minutes)
52
+
53
+ ```typescript
54
+ app.use(session({
55
+ secret: process.env.SESSION_SECRET!,
56
+ resave: false,
57
+ saveUninitialized: false,
58
+ cookie: {
59
+ httpOnly: true,
60
+ secure: process.env.NODE_ENV === "production",
61
+ sameSite: "strict",
62
+ maxAge: 8 * 60 * 60 * 1000, // 8 hours
63
+ },
64
+ }));
65
+ ```
66
+
67
+ ## Multi-Factor Authentication Checklist
68
+
69
+ - [ ] TOTP (Time-based One-Time Password) support
70
+ - [ ] Recovery codes generated and stored (hashed)
71
+ - [ ] Rate limit MFA attempts
72
+ - [ ] MFA bypass audit logging
73
+
74
+ ## Security Checklist
75
+
76
+ - [ ] Passwords hashed with bcrypt/argon2 (not MD5/SHA1)
77
+ - [ ] JWT uses short expiry + refresh token pattern
78
+ - [ ] Session tokens regenerated on login
79
+ - [ ] Brute force protection (rate limiting + account lockout)
80
+ - [ ] Secure password reset flow (time-limited tokens)
@@ -0,0 +1,83 @@
1
+ # Clean Architecture
2
+
3
+ ## Core Principle: Dependency Direction
4
+
5
+ Dependencies point inward. Business logic doesn't know about databases, HTTP, or frameworks.
6
+
7
+ ```
8
+ [HTTP / CLI] → [Use Cases] → [Domain]
9
+ [DB / APIs] → [Use Cases]
10
+
11
+ Interfaces defined here,
12
+ implemented at outer layer
13
+ ```
14
+
15
+ ## Layer Responsibilities
16
+
17
+ **Domain** — pure business logic, no I/O:
18
+ ```typescript
19
+ class Order {
20
+ addItem(product: Product, qty: number): void {
21
+ if (qty <= 0) throw new InvalidQuantityError(qty);
22
+ this.items.push({ product, qty });
23
+ }
24
+ get total(): Money { return this.items.reduce(/* ... */); }
25
+ }
26
+ ```
27
+
28
+ **Use Cases** — orchestrate domain + I/O via interfaces:
29
+ ```typescript
30
+ class PlaceOrderUseCase {
31
+ constructor(
32
+ private readonly orders: OrderRepository, // interface
33
+ private readonly payments: PaymentGateway, // interface
34
+ ) {}
35
+
36
+ async execute(cmd: PlaceOrderCommand): Promise<OrderId> {
37
+ const order = Order.create(cmd);
38
+ const payment = await this.payments.charge(order.total);
39
+ order.markPaid(payment.id);
40
+ return this.orders.save(order);
41
+ }
42
+ }
43
+ ```
44
+
45
+ **Infrastructure** — concrete implementations (DB, HTTP, external APIs).
46
+
47
+ ## File Structure
48
+
49
+ ```
50
+ src/
51
+ domain/ ← no external imports
52
+ order.ts
53
+ user.ts
54
+ use-cases/ ← imports domain only
55
+ place-order.ts
56
+ infrastructure/ ← implements use-case interfaces
57
+ postgres-orders.ts
58
+ stripe-payments.ts
59
+ http/ ← thin: parse, call use case, respond
60
+ orders-controller.ts
61
+ ```
62
+
63
+ ## Boundaries
64
+
65
+ Each boundary should answer:
66
+ - **What does it do?** (single responsibility)
67
+ - **How do you call it?** (clear interface)
68
+ - **What does it depend on?** (explicit, inward)
69
+
70
+ ## When to Separate
71
+
72
+ Three modules that **change together** should probably be one module.
73
+ One module that **changes for different reasons** should be split.
74
+
75
+ ## Anti-Patterns
76
+
77
+ | Avoid | Why |
78
+ |---|---|
79
+ | Business logic in controllers | Hard to test, hard to reuse |
80
+ | Direct DB calls from domain | Domain coupled to infrastructure |
81
+ | God service with 30 methods | No clear responsibility |
82
+ | Anemic domain (only getters/setters) | Logic lives scattered in services |
83
+ | Circular dependencies | Signals wrong boundary |
@@ -0,0 +1,81 @@
1
+ # Code Review
2
+
3
+ ## Reviewer Mindset
4
+
5
+ Your job: find issues the author missed, not rewrite their code. Be direct about problems, respectful about everything else.
6
+
7
+ ## What to Check (Priority Order)
8
+
9
+ ### 1. Correctness
10
+ - Does it do what it claims?
11
+ - Are edge cases handled (null, empty, concurrent)?
12
+ - Are errors handled correctly?
13
+ - Will this break in production?
14
+
15
+ ### 2. Tests
16
+ - Do tests cover the behavior, not just the happy path?
17
+ - Would these tests catch a regression?
18
+ - Are new behaviors tested?
19
+
20
+ ### 3. Design
21
+ - Is the change too large? Should it be split?
22
+ - Does it follow existing patterns?
23
+ - Are there simpler approaches?
24
+ - Are new abstractions justified?
25
+
26
+ ### 4. Security
27
+ - User input validated?
28
+ - Secrets/PII handled correctly?
29
+ - Authorization checked?
30
+
31
+ ### 5. Performance
32
+ - N+1 queries?
33
+ - Unbounded operations on large data?
34
+ - Unnecessary blocking calls?
35
+
36
+ ## Comment Format
37
+
38
+ ```
39
+ // Blocking — must fix before merge
40
+ [BUG] This will NPE when user has no address — add null check at line 42
41
+
42
+ // Important — should fix
43
+ [DESIGN] This 200-line function is hard to test — consider extracting validateAddress()
44
+
45
+ // Minor — optional improvement
46
+ [NIT] Variable name `d` unclear — consider `durationMs`
47
+
48
+ // Question — not blocking
49
+ [Q] Why stringify here instead of using the json field directly?
50
+
51
+ // Positive — acknowledge good work
52
+ [+] Nice — using Result type here instead of throwing makes the caller explicit
53
+ ```
54
+
55
+ ## Self-Review Before Submitting
56
+
57
+ Run through this before opening a PR:
58
+ - [ ] Read your own diff top to bottom
59
+ - [ ] Does the PR description explain WHY, not just what?
60
+ - [ ] Are there debug logs, TODOs, commented-out code to clean up?
61
+ - [ ] Is the PR size reviewable? (< 400 lines is the sweet spot)
62
+ - [ ] Do all tests pass locally?
63
+
64
+ ## PR Size Guide
65
+
66
+ | Lines | Status |
67
+ |---|---|
68
+ | < 100 | Ideal |
69
+ | 100–400 | Acceptable |
70
+ | 400–800 | Ask to split |
71
+ | > 800 | Split — no exceptions |
72
+
73
+ ## Anti-Patterns
74
+
75
+ | Avoid | Why |
76
+ |---|---|
77
+ | "LGTM" without reading | Wastes the review |
78
+ | Bikeshedding on formatting | Use a linter |
79
+ | Rewriting working code | Not your style, their code |
80
+ | Vague comments ("fix this") | Be specific |
81
+ | Approving to avoid conflict | Technical debt |
@@ -0,0 +1,81 @@
1
+ # Database Patterns
2
+
3
+ ## Schema Design
4
+
5
+ **Naming conventions:**
6
+ - Tables: `snake_case`, plural (`users`, `order_items`)
7
+ - Columns: `snake_case` (`created_at`, `user_id`)
8
+ - Primary keys: `id` (UUID or serial)
9
+ - Foreign keys: `<table_singular>_id` (`user_id`, `order_id`)
10
+ - Booleans: `is_` or `has_` prefix (`is_active`, `has_verified_email`)
11
+
12
+ **Always include:**
13
+ ```sql
14
+ CREATE TABLE users (
15
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
16
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
17
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
18
+ );
19
+ ```
20
+
21
+ ## Migrations
22
+
23
+ - One logical change per migration file
24
+ - Migrations are **append-only** — never edit a committed migration
25
+ - Always test `UP` and `DOWN` before merging
26
+ - Non-destructive first: add nullable column → backfill → add NOT NULL constraint
27
+
28
+ ```sql
29
+ -- ✅ Safe migration for adding NOT NULL column
30
+ ALTER TABLE users ADD COLUMN locale TEXT; -- 1. add nullable
31
+ UPDATE users SET locale = 'en' WHERE locale IS NULL; -- 2. backfill
32
+ ALTER TABLE users ALTER COLUMN locale SET NOT NULL; -- 3. constrain
33
+ ```
34
+
35
+ ## Indexing Strategy
36
+
37
+ ```sql
38
+ -- Foreign keys (always index)
39
+ CREATE INDEX idx_orders_user_id ON orders(user_id);
40
+
41
+ -- Common query patterns
42
+ CREATE INDEX idx_users_email ON users(email);
43
+
44
+ -- Composite: put most selective column first
45
+ CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC);
46
+
47
+ -- Partial index for common filtered queries
48
+ CREATE INDEX idx_active_users ON users(email) WHERE is_active = true;
49
+ ```
50
+
51
+ ## Query Patterns
52
+
53
+ ```typescript
54
+ // Batch load instead of N+1
55
+ const userIds = posts.map(p => p.authorId);
56
+ const users = await db.users.findMany({ where: { id: { in: userIds } } });
57
+
58
+ // Cursor pagination (stable, works with inserts)
59
+ const posts = await db.posts.findMany({
60
+ where: { id: { lt: cursor } },
61
+ orderBy: { id: "desc" },
62
+ take: 20,
63
+ });
64
+
65
+ // Use transactions for multi-step mutations
66
+ await db.$transaction(async (tx) => {
67
+ await tx.orders.create({ data: order });
68
+ await tx.inventory.decrement({ where: { id: item.id }, by: qty });
69
+ });
70
+ ```
71
+
72
+ ## Anti-Patterns
73
+
74
+ | Avoid | Why |
75
+ |---|---|
76
+ | `SELECT *` | Pulls unused columns, breaks on schema change |
77
+ | No migrations | Schema drift between environments |
78
+ | Storing arrays as comma-separated strings | Use proper array column or join table |
79
+ | Soft-delete without index | Full table scan on active queries |
80
+ | Logic in triggers | Invisible side effects |
81
+ | ORM for complex reports | Write SQL — it's fine |
@@ -0,0 +1,90 @@
1
+ # Error Handling
2
+
3
+ ## Core Principle: Errors Are Values
4
+
5
+ Handle errors explicitly at call sites. Don't let them propagate silently or get swallowed.
6
+
7
+ ## Error Types
8
+
9
+ ```typescript
10
+ // Domain errors — expected, recoverable
11
+ class UserNotFoundError extends Error {
12
+ readonly code = "USER_NOT_FOUND";
13
+ constructor(readonly userId: string) {
14
+ super(`User ${userId} not found`);
15
+ this.name = "UserNotFoundError";
16
+ }
17
+ }
18
+
19
+ // Infrastructure errors — unexpected, potentially fatal
20
+ // → Let them propagate up to the boundary handler
21
+ ```
22
+
23
+ ## Result Pattern (for fallible operations)
24
+
25
+ ```typescript
26
+ type Result<T, E extends Error = Error> =
27
+ | { ok: true; value: T }
28
+ | { ok: false; error: E };
29
+
30
+ function parseConfig(raw: unknown): Result<Config, ConfigError> {
31
+ if (!isValidConfig(raw)) return { ok: false, error: new ConfigError("invalid") };
32
+ return { ok: true, value: raw as Config };
33
+ }
34
+
35
+ // Usage — explicit at call site
36
+ const result = parseConfig(input);
37
+ if (!result.ok) {
38
+ log.error("bad config", result.error);
39
+ return;
40
+ }
41
+ use(result.value);
42
+ ```
43
+
44
+ ## Boundary Handlers
45
+
46
+ Catch at the outermost layer only:
47
+
48
+ ```typescript
49
+ // HTTP handler
50
+ app.use((err, req, res, next) => {
51
+ if (err instanceof DomainError) {
52
+ res.status(err.httpStatus).json({ error: { code: err.code, message: err.message } });
53
+ } else {
54
+ log.error("unexpected", err);
55
+ res.status(500).json({ error: { code: "INTERNAL", message: "Internal error" } });
56
+ }
57
+ });
58
+ ```
59
+
60
+ ## Async Error Handling
61
+
62
+ ```typescript
63
+ // Always handle rejected promises
64
+ const data = await fetchUser(id).catch(err => {
65
+ if (err instanceof NotFoundError) return null;
66
+ throw err; // re-throw unexpected errors
67
+ });
68
+
69
+ // Top-level: never ignore unhandled rejections
70
+ process.on("unhandledRejection", (err) => {
71
+ log.error("unhandled rejection", err);
72
+ process.exit(1);
73
+ });
74
+ ```
75
+
76
+ ## Logging
77
+
78
+ - Log at the boundary, not at every layer (avoid duplicate logs)
79
+ - Include context: `log.error("fetch failed", { userId, err })`
80
+ - Never log secrets or PII
81
+
82
+ ## Anti-Patterns
83
+
84
+ | Avoid | Use instead |
85
+ |---|---|
86
+ | Empty `catch {}` | At minimum log the error |
87
+ | `catch (e: any)` | `catch (e: unknown)` then narrow |
88
+ | Throwing strings | Throw Error instances |
89
+ | Catching everything | Only catch what you can handle |
90
+ | Error codes as numbers | String codes (searchable) |