create-rigel 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 +21 -0
- package/README.md +156 -0
- package/cli.js +122 -0
- package/package.json +47 -0
- package/templates/express/.claude/CLAUDE.md +114 -0
- package/templates/express/.claude/agents/arch-validator.md +85 -0
- package/templates/express/.claude/agents/db-optimizer.md +72 -0
- package/templates/express/.claude/agents/doc-gardener.md +67 -0
- package/templates/express/.claude/agents/garbage-collector.md +94 -0
- package/templates/express/.claude/agents/gate-checker.md +176 -0
- package/templates/express/.claude/agents/reviewer.md +84 -0
- package/templates/express/.claude/agents/security-auditor.md +82 -0
- package/templates/express/.claude/hooks/post-write.sh +93 -0
- package/templates/express/.claude/rules/api.md +149 -0
- package/templates/express/.claude/rules/architecture.md +78 -0
- package/templates/express/.claude/rules/database.md +104 -0
- package/templates/express/.claude/rules/jobs.md +81 -0
- package/templates/express/.claude/rules/security.md +92 -0
- package/templates/express/.claude/rules/testing.md +97 -0
- package/templates/express/.claude/settings.json +35 -0
- package/templates/express/.claude/skills/00-infra-setup/SKILL.md +479 -0
- package/templates/express/.claude/skills/01-write-roadmap/SKILL.md +72 -0
- package/templates/express/.claude/skills/02-write-spec/SKILL.md +119 -0
- package/templates/express/.claude/skills/03-write-plan/SKILL.md +124 -0
- package/templates/express/.claude/skills/04-build-layer/SKILL.md +216 -0
- package/templates/express/.claude/skills/05-validate-layer/SKILL.md +43 -0
- package/templates/express/.claude/skills/06-push-layer/SKILL.md +59 -0
- package/templates/express/.claude/skills/07-layer-check/SKILL.md +62 -0
- package/templates/express/.claude/skills/08-garbage-collect/SKILL.md +86 -0
- package/templates/express/.claude/skills/09-doc-garden/SKILL.md +64 -0
- package/templates/express/.claude/skills/10-db-optimize/SKILL.md +96 -0
- package/templates/express/.claude/skills/11-load-test/SKILL.md +77 -0
- package/templates/express/.dockerignore +43 -0
- package/templates/express/.env.example +42 -0
- package/templates/express/.github/CODEOWNERS +22 -0
- package/templates/express/.github/dependabot.yml +48 -0
- package/templates/express/.github/pull_request_template.md +58 -0
- package/templates/express/.github/workflows/ci.yml +278 -0
- package/templates/express/.github/workflows/load-test.yml +41 -0
- package/templates/express/.gitleaks.toml +18 -0
- package/templates/express/.lintstagedrc.json +3 -0
- package/templates/express/.nvmrc +1 -0
- package/templates/express/.prettierrc +8 -0
- package/templates/express/.sequelizerc +8 -0
- package/templates/express/AGENTS.md +94 -0
- package/templates/express/ARCHITECTURE.md +161 -0
- package/templates/express/Dockerfile +40 -0
- package/templates/express/Makefile +79 -0
- package/templates/express/QUICKSTART.md +131 -0
- package/templates/express/docker-compose.yml +114 -0
- package/templates/express/docs/PLANS.md +111 -0
- package/templates/express/docs/QUALITY_SCORE.md +66 -0
- package/templates/express/docs/design-docs/core-beliefs.md +89 -0
- package/templates/express/docs/design-docs/decisions/ADR-000-infrastructure.md +70 -0
- package/templates/express/docs/design-docs/decisions/ADR-001-observability.md +82 -0
- package/templates/express/docs/design-docs/decisions/index.md +23 -0
- package/templates/express/docs/design-docs/index.md +11 -0
- package/templates/express/docs/design-docs/observability.md +194 -0
- package/templates/express/docs/design-docs/team-workflow.md +64 -0
- package/templates/express/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/tech-debt-tracker.md +31 -0
- package/templates/express/docs/generated/.gitkeep +0 -0
- package/templates/express/docs/product-specs/ROADMAP.md +49 -0
- package/templates/express/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/express/docs/product-specs/index.md +21 -0
- package/templates/express/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/express/eslint.config.mjs +94 -0
- package/templates/express/gitignore +36 -0
- package/templates/express/infra/monitoring/alloy/config.alloy +59 -0
- package/templates/express/jest.config.ts +61 -0
- package/templates/express/package.json +45 -0
- package/templates/express/scripts/openapi.export.ts +77 -0
- package/templates/express/tests/architecture/isolation.test.ts +51 -0
- package/templates/express/tests/architecture/layers.test.ts +83 -0
- package/templates/express/tests/integration/health.test.ts +40 -0
- package/templates/express/tests/integration/isolation.test.template.ts +88 -0
- package/templates/express/tests/load/smoke.js +46 -0
- package/templates/express/tests/load/soak.js +45 -0
- package/templates/express/tests/load/stress.js +44 -0
- package/templates/express/tests/unit/utils/response.util.test.ts +53 -0
- package/templates/express/tsconfig.build.json +8 -0
- package/templates/express/tsconfig.json +20 -0
- package/templates/fastapi/.claude/CLAUDE.md +122 -0
- package/templates/fastapi/.claude/agents/arch-validator.md +64 -0
- package/templates/fastapi/.claude/agents/db-optimizer.md +59 -0
- package/templates/fastapi/.claude/agents/doc-gardener.md +55 -0
- package/templates/fastapi/.claude/agents/garbage-collector.md +74 -0
- package/templates/fastapi/.claude/agents/gate-checker.md +133 -0
- package/templates/fastapi/.claude/agents/reviewer.md +74 -0
- package/templates/fastapi/.claude/agents/security-auditor.md +69 -0
- package/templates/fastapi/.claude/hooks/post-write.sh +65 -0
- package/templates/fastapi/.claude/rules/api.md +119 -0
- package/templates/fastapi/.claude/rules/architecture.md +84 -0
- package/templates/fastapi/.claude/rules/database.md +153 -0
- package/templates/fastapi/.claude/rules/jobs.md +85 -0
- package/templates/fastapi/.claude/rules/security.md +102 -0
- package/templates/fastapi/.claude/rules/testing.md +207 -0
- package/templates/fastapi/.claude/settings.json +50 -0
- package/templates/fastapi/.claude/skills/00-infra-setup/SKILL.md +311 -0
- package/templates/fastapi/.claude/skills/01-write-roadmap/SKILL.md +61 -0
- package/templates/fastapi/.claude/skills/02-write-spec/SKILL.md +82 -0
- package/templates/fastapi/.claude/skills/03-write-plan/SKILL.md +83 -0
- package/templates/fastapi/.claude/skills/04-build-layer/SKILL.md +170 -0
- package/templates/fastapi/.claude/skills/05-validate-layer/SKILL.md +20 -0
- package/templates/fastapi/.claude/skills/06-push-layer/SKILL.md +31 -0
- package/templates/fastapi/.claude/skills/07-layer-check/SKILL.md +51 -0
- package/templates/fastapi/.claude/skills/08-garbage-collect/SKILL.md +47 -0
- package/templates/fastapi/.claude/skills/09-doc-garden/SKILL.md +9 -0
- package/templates/fastapi/.claude/skills/10-db-optimize/SKILL.md +53 -0
- package/templates/fastapi/.claude/skills/11-load-test/SKILL.md +81 -0
- package/templates/fastapi/.dockerignore +30 -0
- package/templates/fastapi/.github/dependabot.yml +24 -0
- package/templates/fastapi/AGENTS.md +87 -0
- package/templates/fastapi/ARCHITECTURE.md +141 -0
- package/templates/fastapi/Makefile +42 -0
- package/templates/fastapi/README.md +179 -0
- package/templates/fastapi/docs/PLANS.md +26 -0
- package/templates/fastapi/docs/QUALITY_SCORE.md +26 -0
- package/templates/fastapi/docs/design-docs/core-beliefs.md +46 -0
- package/templates/fastapi/docs/design-docs/decisions/ADR-000-infrastructure.md +79 -0
- package/templates/fastapi/docs/design-docs/decisions/index.md +7 -0
- package/templates/fastapi/docs/design-docs/index.md +9 -0
- package/templates/fastapi/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/fastapi/docs/generated/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/ROADMAP.md +49 -0
- package/templates/fastapi/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/index.md +18 -0
- package/templates/fastapi/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/fastapi/gitignore +46 -0
- package/templates/fastapi/scripts/gate.sh +68 -0
- package/templates/nestjs/.claude/CLAUDE.md +121 -0
- package/templates/nestjs/.claude/agents/db-optimizer.md +51 -0
- package/templates/nestjs/.claude/agents/doc-gardener.md +45 -0
- package/templates/nestjs/.claude/agents/garbage-collector.md +50 -0
- package/templates/nestjs/.claude/agents/gate-checker.md +127 -0
- package/templates/nestjs/.claude/agents/reviewer.md +68 -0
- package/templates/nestjs/.claude/agents/security-auditor.md +72 -0
- package/templates/nestjs/.claude/hooks/post-write.sh +91 -0
- package/templates/nestjs/.claude/rules/api.md +87 -0
- package/templates/nestjs/.claude/rules/architecture.md +104 -0
- package/templates/nestjs/.claude/rules/database.md +111 -0
- package/templates/nestjs/.claude/rules/security.md +110 -0
- package/templates/nestjs/.claude/rules/testing.md +145 -0
- package/templates/nestjs/.claude/settings.json +33 -0
- package/templates/nestjs/.claude/skills/00-infra-setup/SKILL.md +222 -0
- package/templates/nestjs/.claude/skills/01-write-spec/SKILL.md +25 -0
- package/templates/nestjs/.claude/skills/02-write-plan/SKILL.md +18 -0
- package/templates/nestjs/.claude/skills/03-build-layer/SKILL.md +242 -0
- package/templates/nestjs/.claude/skills/04-validate-layer/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/05-push-layer/SKILL.md +6 -0
- package/templates/nestjs/.claude/skills/06-layer-check/SKILL.md +12 -0
- package/templates/nestjs/.claude/skills/07-garbage-collect/SKILL.md +2 -0
- package/templates/nestjs/.claude/skills/08-doc-garden/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/09-api-sync/SKILL.md +36 -0
- package/templates/nestjs/.claude/skills/10-load-test/SKILL.md +29 -0
- package/templates/nestjs/.env.example +11 -0
- package/templates/nestjs/AGENTS.md +88 -0
- package/templates/nestjs/ARCHITECTURE.md +192 -0
- package/templates/nestjs/docs/PLANS.md +22 -0
- package/templates/nestjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nestjs/docs/design-docs/core-beliefs.md +18 -0
- package/templates/nestjs/docs/design-docs/decisions/ADR-000-infrastructure.md +58 -0
- package/templates/nestjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nestjs/docs/design-docs/index.md +7 -0
- package/templates/nestjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/tech-debt-tracker.md +16 -0
- package/templates/nestjs/docs/generated/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/index.md +7 -0
- package/templates/nestjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nestjs/gitignore +8 -0
- package/templates/nextjs/.claude/CLAUDE.md +139 -0
- package/templates/nextjs/.claude/agents/arch-validator.md +66 -0
- package/templates/nextjs/.claude/agents/contract-checker.md +78 -0
- package/templates/nextjs/.claude/agents/doc-gardener.md +52 -0
- package/templates/nextjs/.claude/agents/garbage-collector.md +61 -0
- package/templates/nextjs/.claude/agents/gate-checker.md +129 -0
- package/templates/nextjs/.claude/agents/perf-auditor.md +109 -0
- package/templates/nextjs/.claude/agents/reviewer.md +79 -0
- package/templates/nextjs/.claude/agents/security-auditor.md +50 -0
- package/templates/nextjs/.claude/hooks/post-write.sh +121 -0
- package/templates/nextjs/.claude/rules/api-contract.md +166 -0
- package/templates/nextjs/.claude/rules/architecture.md +99 -0
- package/templates/nextjs/.claude/rules/components.md +122 -0
- package/templates/nextjs/.claude/rules/security.md +122 -0
- package/templates/nextjs/.claude/rules/testing.md +187 -0
- package/templates/nextjs/.claude/scripts/infra-setup.sh +550 -0
- package/templates/nextjs/.claude/settings.json +33 -0
- package/templates/nextjs/.claude/skills/00-infra-setup/SKILL.md +471 -0
- package/templates/nextjs/.claude/skills/01-write-roadmap/SKILL.md +77 -0
- package/templates/nextjs/.claude/skills/02-write-spec/SKILL.md +78 -0
- package/templates/nextjs/.claude/skills/03-write-plan/SKILL.md +91 -0
- package/templates/nextjs/.claude/skills/04-build-layer/SKILL.md +141 -0
- package/templates/nextjs/.claude/skills/05-validate-layer/SKILL.md +13 -0
- package/templates/nextjs/.claude/skills/06-push-layer/SKILL.md +20 -0
- package/templates/nextjs/.claude/skills/07-layer-check/SKILL.md +28 -0
- package/templates/nextjs/.claude/skills/08-garbage-collect/SKILL.md +14 -0
- package/templates/nextjs/.claude/skills/09-doc-garden/SKILL.md +12 -0
- package/templates/nextjs/.claude/skills/10-api-sync/SKILL.md +50 -0
- package/templates/nextjs/.claude/skills/11-perf-budget/SKILL.md +51 -0
- package/templates/nextjs/.dockerignore +25 -0
- package/templates/nextjs/.gitattributes +10 -0
- package/templates/nextjs/.github/CODEOWNERS +24 -0
- package/templates/nextjs/.github/dependabot.yml +31 -0
- package/templates/nextjs/.github/pull_request_template.md +59 -0
- package/templates/nextjs/.github/workflows/ci.yml +143 -0
- package/templates/nextjs/.github/workflows/lighthouse.yml +54 -0
- package/templates/nextjs/.github/workflows/load-test.yml +54 -0
- package/templates/nextjs/.husky/pre-commit +1 -0
- package/templates/nextjs/.lintstagedrc.json +4 -0
- package/templates/nextjs/.mcp.json +8 -0
- package/templates/nextjs/.prettierignore +13 -0
- package/templates/nextjs/.prettierrc +9 -0
- package/templates/nextjs/AGENTS.md +88 -0
- package/templates/nextjs/ARCHITECTURE.md +165 -0
- package/templates/nextjs/Dockerfile +38 -0
- package/templates/nextjs/Makefile +50 -0
- package/templates/nextjs/QUICKSTART.md +128 -0
- package/templates/nextjs/docs/PLANS.md +23 -0
- package/templates/nextjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nextjs/docs/design-docs/core-beliefs.md +43 -0
- package/templates/nextjs/docs/design-docs/decisions/ADR-000-infrastructure.md +72 -0
- package/templates/nextjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nextjs/docs/design-docs/index.md +7 -0
- package/templates/nextjs/docs/design-docs/team-workflow.md +66 -0
- package/templates/nextjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/nextjs/docs/generated/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/ROADMAP.md +59 -0
- package/templates/nextjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/index.md +18 -0
- package/templates/nextjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nextjs/eslint.config.mjs +150 -0
- package/templates/nextjs/gitignore +35 -0
- package/templates/nextjs/lighthouserc.js +37 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.ts"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Architecture Rules — Auto-injected on every src/ file edit
|
|
7
|
+
|
|
8
|
+
## Import Order Enforcement
|
|
9
|
+
|
|
10
|
+
Before writing any import, check which layer you are in:
|
|
11
|
+
|
|
12
|
+
| Your file is in | You may import from |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `src/types/` | Nothing |
|
|
15
|
+
| `src/config/` | `src/types/` only |
|
|
16
|
+
| `src/models/` | `src/types/`, `src/config/` |
|
|
17
|
+
| `src/repo/` | `src/types/`, `src/config/`, `src/models/` |
|
|
18
|
+
| `src/services/` | `src/types/`, `src/config/`, `src/repo/` |
|
|
19
|
+
| `src/runtime/` | `src/types/`, `src/config/`, `src/repo/`, `src/services/` |
|
|
20
|
+
| `src/utils/` | Nothing (zero domain imports) |
|
|
21
|
+
| `src/providers/` | `src/types/`, `src/config/` only |
|
|
22
|
+
|
|
23
|
+
**Any import that violates this table is a layer violation. Fix it before proceeding.**
|
|
24
|
+
|
|
25
|
+
## The Checks You Must Pass
|
|
26
|
+
|
|
27
|
+
### Types layer (`src/types/`)
|
|
28
|
+
```typescript
|
|
29
|
+
// ✅ Correct — pure interface, no imports
|
|
30
|
+
export interface Application { id: string; stage: ApplicationStage }
|
|
31
|
+
|
|
32
|
+
// ❌ Wrong — importing another layer
|
|
33
|
+
import { env } from '../config/env' // FORBIDDEN in types
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Config layer (`src/config/`)
|
|
37
|
+
```typescript
|
|
38
|
+
// ✅ Correct
|
|
39
|
+
import { z } from 'zod' // third-party ok
|
|
40
|
+
import type { Application } from '../types' // Types ok
|
|
41
|
+
|
|
42
|
+
// ❌ Wrong
|
|
43
|
+
import { userRepo } from '../repo/user.repo' // FORBIDDEN
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Service layer (`src/services/`)
|
|
47
|
+
```typescript
|
|
48
|
+
// ✅ Correct — takes domain types, returns domain types
|
|
49
|
+
export async function createApplication(userId: string, input: CreateApplicationInput): Promise<Application>
|
|
50
|
+
|
|
51
|
+
// ❌ Wrong — HTTP types in service
|
|
52
|
+
import { Request, Response } from 'express' // FORBIDDEN
|
|
53
|
+
import { HttpException } from '../runtime/...' // FORBIDDEN
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Runtime layer (`src/runtime/`)
|
|
57
|
+
```typescript
|
|
58
|
+
// ✅ Correct handler structure (THIS ORDER — every time)
|
|
59
|
+
router.post('/applications', async (req, res, next) => {
|
|
60
|
+
try {
|
|
61
|
+
const auth = await requireAuth(req) // 1. Auth
|
|
62
|
+
const limited = await rateLimitMiddleware(req, 'USER_API') // 2. Rate limit
|
|
63
|
+
if (limited) return res.status(429).json(limited)
|
|
64
|
+
const body = CreateApplicationSchema.parse(req.body) // 3. Validate
|
|
65
|
+
const result = await applicationService.create(auth.userId, body) // 4. Service
|
|
66
|
+
return res.status(201).json(ok(result, req.requestId)) // 5. Respond
|
|
67
|
+
} catch (err) { next(err) } // 6. Error handler
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// ❌ Wrong — business logic in handler
|
|
71
|
+
router.post('/applications', async (req, res) => {
|
|
72
|
+
if (req.body.stage === 'APPLIED') { ... } // logic belongs in service
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Structural Test
|
|
77
|
+
`tests/architecture/layers.test.ts` imports madge and fails CI if any of these are violated.
|
|
78
|
+
Run it: `npm run test:arch`
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/repo/**/*.ts"
|
|
4
|
+
- "src/models/**/*.ts"
|
|
5
|
+
- "db/migrations/**/*.js"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Database Rules — Auto-injected on repo/model/migration edits
|
|
9
|
+
|
|
10
|
+
## The 5 Checks Before Every Repo Method Ships
|
|
11
|
+
|
|
12
|
+
### 1. Zod Parse on Every Result
|
|
13
|
+
```typescript
|
|
14
|
+
// ✅ REQUIRED
|
|
15
|
+
const raw = await Application.findByPk(id)
|
|
16
|
+
if (!raw) throw new NotFoundError(`Application ${id} not found`)
|
|
17
|
+
return ApplicationSchema.parse(raw.toJSON())
|
|
18
|
+
|
|
19
|
+
// ❌ FORBIDDEN — implicit trust of DB output
|
|
20
|
+
return raw as Application
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. No N+1 — Eager Load Always
|
|
24
|
+
```typescript
|
|
25
|
+
// ✅ Single query
|
|
26
|
+
const apps = await Application.findAll({
|
|
27
|
+
where: { userId },
|
|
28
|
+
include: [
|
|
29
|
+
{ model: Note },
|
|
30
|
+
{ model: Contact },
|
|
31
|
+
{ model: Reminder },
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// ❌ N+1 — DB called inside loop
|
|
36
|
+
const apps = await Application.findAll({ where: { userId } })
|
|
37
|
+
for (const app of apps) {
|
|
38
|
+
app.notes = await Note.findAll({ where: { applicationId: app.id } }) // N queries
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Cursor Pagination on All List Methods
|
|
43
|
+
```typescript
|
|
44
|
+
// ✅ Cursor-based
|
|
45
|
+
async function list(userId: string, cursor?: PageCursor, limit = 20) {
|
|
46
|
+
const where: WhereOptions = { userId }
|
|
47
|
+
if (cursor) {
|
|
48
|
+
where[Op.or] = [
|
|
49
|
+
{ createdAt: { [Op.lt]: cursor.createdAt } },
|
|
50
|
+
{ createdAt: cursor.createdAt, id: { [Op.lt]: cursor.id } },
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
const rows = await Application.findAll({
|
|
54
|
+
where, order: [['createdAt', 'DESC'], ['id', 'DESC']], limit: limit + 1,
|
|
55
|
+
})
|
|
56
|
+
const hasMore = rows.length > limit
|
|
57
|
+
const items = hasMore ? rows.slice(0, limit) : rows
|
|
58
|
+
return {
|
|
59
|
+
items: items.map(r => ApplicationSchema.parse(r.toJSON())),
|
|
60
|
+
nextCursor: hasMore ? { id: items.at(-1)!.id, createdAt: items.at(-1)!.createdAt } : null,
|
|
61
|
+
hasMore,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ❌ FORBIDDEN on tables > 10k rows
|
|
66
|
+
Application.findAll({ offset: page * limit, limit })
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Transactions for Multi-Table Writes
|
|
70
|
+
```typescript
|
|
71
|
+
// ✅ REQUIRED when writing > 1 table
|
|
72
|
+
return sequelize.transaction(async (t) => {
|
|
73
|
+
await Application.update({ stage }, { where: { id }, transaction: t })
|
|
74
|
+
await Note.create({ applicationId: id, content, type: 'SYSTEM' }, { transaction: t })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// No external API calls inside a transaction
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 5. Soft Delete — paranoid: true
|
|
81
|
+
```typescript
|
|
82
|
+
// Model must have paranoid: true
|
|
83
|
+
@Table({ tableName: 'applications', paranoid: true })
|
|
84
|
+
export class Application extends Model { ... }
|
|
85
|
+
|
|
86
|
+
// findAll automatically filters deleted — no manual where needed
|
|
87
|
+
await Application.findAll({ where: { userId } }) // deleted_at IS NULL automatic
|
|
88
|
+
|
|
89
|
+
// Explicit include-deleted (opt-in only)
|
|
90
|
+
await Application.findAll({ where: { userId }, paranoid: false })
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Model Checklist (every new model)
|
|
94
|
+
- [ ] `@Table({ paranoid: true })` — soft delete
|
|
95
|
+
- [ ] `@Default(() => newId())` on id — UUIDv7
|
|
96
|
+
- [ ] `indexes` array defined on `@Table`
|
|
97
|
+
- [ ] FK columns have index
|
|
98
|
+
- [ ] ORDER BY columns have index
|
|
99
|
+
- [ ] Partial index for `WHERE deleted_at IS NULL` queries
|
|
100
|
+
|
|
101
|
+
## Migration Checklist (every new migration)
|
|
102
|
+
- [ ] New indexes use `CONCURRENTLY`
|
|
103
|
+
- [ ] FK constraints defined with explicit `ON DELETE` behaviour
|
|
104
|
+
- [ ] Both `up` and `down` implemented
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/runtime/workers/**/*.ts"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Job Rules — Auto-injected on worker file edits
|
|
7
|
+
|
|
8
|
+
## Worker Checklist (every worker file)
|
|
9
|
+
|
|
10
|
+
### 1. Validate payload with Zod before touching it
|
|
11
|
+
```typescript
|
|
12
|
+
const worker = new Worker(QUEUES.REMINDER_EMAIL, async (job) => {
|
|
13
|
+
// ✅ REQUIRED — validate before processing
|
|
14
|
+
const payload = ReminderEmailJobSchema.parse(job.data)
|
|
15
|
+
|
|
16
|
+
// ❌ FORBIDDEN
|
|
17
|
+
const { reminderId } = job.data // trusting unvalidated external data
|
|
18
|
+
}, { connection: redis })
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Structured logging — start, complete, failed
|
|
22
|
+
```typescript
|
|
23
|
+
const worker = new Worker(QUEUES.REMINDER_EMAIL, async (job) => {
|
|
24
|
+
const payload = ReminderEmailJobSchema.parse(job.data)
|
|
25
|
+
const start = Date.now()
|
|
26
|
+
|
|
27
|
+
logger.info({ event: 'job.start', jobId: job.id, queue: QUEUES.REMINDER_EMAIL })
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await reminderService.sendReminderEmail(payload)
|
|
31
|
+
logger.info({ event: 'job.complete', jobId: job.id, queue: QUEUES.REMINDER_EMAIL, durationMs: Date.now() - start })
|
|
32
|
+
} catch (err) {
|
|
33
|
+
logger.error({ event: 'job.failed', jobId: job.id, queue: QUEUES.REMINDER_EMAIL, err: (err as Error).message })
|
|
34
|
+
throw err // re-throw so BullMQ handles retry
|
|
35
|
+
}
|
|
36
|
+
}, { connection: redis, concurrency: 5 })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 3. Retry config on queue definition
|
|
40
|
+
```typescript
|
|
41
|
+
// ✅ Define retry on the queue
|
|
42
|
+
export const reminderQueue = new Queue(QUEUES.REMINDER_EMAIL, {
|
|
43
|
+
connection: redis,
|
|
44
|
+
defaultJobOptions: {
|
|
45
|
+
attempts: 3,
|
|
46
|
+
backoff: { type: 'exponential', delay: 5000 },
|
|
47
|
+
removeOnComplete: { count: 100 },
|
|
48
|
+
removeOnFail: { count: 500 },
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 4. Metrics
|
|
54
|
+
```typescript
|
|
55
|
+
worker.on('completed', (job) => {
|
|
56
|
+
metrics.increment('jobs.completed', { queue: QUEUES.REMINDER_EMAIL })
|
|
57
|
+
})
|
|
58
|
+
worker.on('failed', (job, err) => {
|
|
59
|
+
metrics.increment('jobs.failed', { queue: QUEUES.REMINDER_EMAIL })
|
|
60
|
+
logger.error({ event: 'job.failed', jobId: job?.id, err: err.message })
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 5. Circuit breaker on external calls inside workers
|
|
65
|
+
```typescript
|
|
66
|
+
// Workers call external APIs (email, SMS, webhooks)
|
|
67
|
+
// Always wrap with circuit breaker + retry
|
|
68
|
+
const result = await withRetry(
|
|
69
|
+
() => emailBreaker.call(() => emailProvider.send(payload)),
|
|
70
|
+
{ maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 10000, jitter: true }
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 6. Graceful shutdown hook
|
|
75
|
+
```typescript
|
|
76
|
+
// Register in src/runtime/server.ts shutdown sequence
|
|
77
|
+
async function shutdown() {
|
|
78
|
+
await reminderWorker.close() // drain in-flight jobs
|
|
79
|
+
await reminderQueue.close()
|
|
80
|
+
}
|
|
81
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/runtime/**/*.ts"
|
|
4
|
+
- "src/repo/**/*.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Security Rules — Auto-injected on runtime and repo file edits
|
|
8
|
+
|
|
9
|
+
## Runtime: Handler Security Checklist
|
|
10
|
+
|
|
11
|
+
Every protected handler MUST follow this exact order:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// 1. Auth FIRST — before anything else
|
|
15
|
+
const auth = await requireAuth(req) // throws UnauthorizedError if invalid
|
|
16
|
+
|
|
17
|
+
// 2. Rate limit
|
|
18
|
+
const limited = await checkRateLimit(req, 'USER_API')
|
|
19
|
+
if (limited) return res.status(429).json(limited)
|
|
20
|
+
|
|
21
|
+
// 3. Input validation — Zod, always
|
|
22
|
+
const body = CreateApplicationSchema.parse(req.body)
|
|
23
|
+
// Never: const body = req.body as CreateApplicationInput
|
|
24
|
+
|
|
25
|
+
// 4. Call service with typed domain inputs
|
|
26
|
+
const result = await applicationService.create(auth.userId, body)
|
|
27
|
+
|
|
28
|
+
// 5. Respond with envelope
|
|
29
|
+
return res.status(201).json(ok(result, req.requestId))
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Repo: Data Boundary Validation
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// ✅ REQUIRED — every query result
|
|
36
|
+
const raw = await Application.findByPk(id)
|
|
37
|
+
if (!raw) throw new NotFoundError(`Application ${id} not found`)
|
|
38
|
+
return ApplicationSchema.parse(raw.toJSON())
|
|
39
|
+
|
|
40
|
+
// ❌ FORBIDDEN — TypeScript lie
|
|
41
|
+
return raw as Application
|
|
42
|
+
return raw.toJSON() as Application
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## SQL Injection Prevention
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// ✅ Safe — Sequelize parameterises automatically
|
|
49
|
+
await Application.findAll({ where: { userId, stage } })
|
|
50
|
+
|
|
51
|
+
// ✅ Safe — named replacements for raw queries
|
|
52
|
+
await sequelize.query(
|
|
53
|
+
'SELECT * FROM applications WHERE user_id = :userId',
|
|
54
|
+
{ replacements: { userId }, type: QueryTypes.SELECT }
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// ❌ BLOCKED by ESLint — string interpolation
|
|
58
|
+
await sequelize.query(`SELECT * FROM applications WHERE user_id = '${userId}'`)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Auth Middleware — Never Inline
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// ✅ Always use the provider
|
|
65
|
+
import { requireAuth } from '../../providers/auth/middleware'
|
|
66
|
+
|
|
67
|
+
// ❌ Never roll your own inline JWT verification
|
|
68
|
+
const token = req.headers.authorization?.split(' ')[1]
|
|
69
|
+
const payload = jwt.verify(token, process.env.JWT_SECRET) // multiple violations
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Error Responses — Never Expose Internals
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// ✅ Correct — sanitised
|
|
76
|
+
return next(new NotFoundError('Application not found'))
|
|
77
|
+
// → errorHandler maps to: { error: { code: 'NOT_FOUND', message: 'Application not found' } }
|
|
78
|
+
|
|
79
|
+
// ❌ Wrong — exposes internals
|
|
80
|
+
res.status(500).json({ error: err.message, stack: err.stack })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Ownership Enforcement in Repo
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// ✅ ALWAYS — user can only access their own resources
|
|
87
|
+
await Application.findOne({ where: { id, userId } }) // userId from auth context
|
|
88
|
+
await Application.findByPk(id, { where: { userId } })
|
|
89
|
+
|
|
90
|
+
// ❌ NEVER — no ownership check
|
|
91
|
+
await Application.findByPk(id) // any user can access any application
|
|
92
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "tests/**/*.ts"
|
|
4
|
+
- "tests/**/*.test.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Testing Rules — Auto-injected on test file edits
|
|
8
|
+
|
|
9
|
+
## Coverage Thresholds (enforced in CI — fail if below)
|
|
10
|
+
|
|
11
|
+
| Layer | Threshold | What to test |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `src/utils/` | **100%** | Every branch, every edge case |
|
|
14
|
+
| `src/services/` | **90%** | Happy path + all error paths + state machine transitions |
|
|
15
|
+
| `src/repo/` | **80%** | Query results, Zod parse, ownership, cursor pagination |
|
|
16
|
+
| `src/runtime/routes/` | **75%** | Auth required, 422 validation, 404 not found |
|
|
17
|
+
| `src/providers/` | **70%** | Core functionality |
|
|
18
|
+
|
|
19
|
+
## Required Test Patterns
|
|
20
|
+
|
|
21
|
+
### Service Tests — error paths are mandatory
|
|
22
|
+
```typescript
|
|
23
|
+
describe('applicationService.transitionStage', () => {
|
|
24
|
+
it('advances stage when transition is valid', async () => { ... })
|
|
25
|
+
it('throws StageTransitionError when transition is invalid', async () => {
|
|
26
|
+
await expect(applicationService.transitionStage(userId, appId, 'ACCEPTED'))
|
|
27
|
+
.rejects.toThrow(StageTransitionError)
|
|
28
|
+
})
|
|
29
|
+
it('throws TerminalStageError when application is already accepted', async () => { ... })
|
|
30
|
+
it('throws NotFoundError when application does not belong to user', async () => { ... })
|
|
31
|
+
it('creates system note in same transaction as stage update', async () => { ... })
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Zod Schema Tests — malformed inputs
|
|
36
|
+
```typescript
|
|
37
|
+
describe('CreateApplicationSchema', () => {
|
|
38
|
+
it('rejects missing company', () => {
|
|
39
|
+
expect(() => CreateApplicationSchema.parse({ role: 'Engineer' })).toThrow()
|
|
40
|
+
})
|
|
41
|
+
it('rejects invalid stage value', () => {
|
|
42
|
+
expect(() => ApplicationSchema.parse({ ...valid, stage: 'PENDING' })).toThrow()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Route Tests — test the HTTP contract
|
|
48
|
+
```typescript
|
|
49
|
+
describe('POST /api/v1/applications', () => {
|
|
50
|
+
it('returns 401 without auth token', async () => {
|
|
51
|
+
const res = await request(app).post('/api/v1/applications').send(validBody)
|
|
52
|
+
expect(res.status).toBe(401)
|
|
53
|
+
})
|
|
54
|
+
it('returns 422 with invalid body', async () => {
|
|
55
|
+
const res = await request(app)
|
|
56
|
+
.post('/api/v1/applications')
|
|
57
|
+
.set('Authorization', `Bearer ${token}`)
|
|
58
|
+
.send({ role: 'Engineer' }) // missing company
|
|
59
|
+
expect(res.status).toBe(422)
|
|
60
|
+
})
|
|
61
|
+
it('returns 201 with standard envelope', async () => {
|
|
62
|
+
const res = await request(app)
|
|
63
|
+
.post('/api/v1/applications')
|
|
64
|
+
.set('Authorization', `Bearer ${token}`)
|
|
65
|
+
.send(validBody)
|
|
66
|
+
expect(res.status).toBe(201)
|
|
67
|
+
expect(res.body).toHaveProperty('data')
|
|
68
|
+
expect(res.body).toHaveProperty('meta.requestId')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Architecture Tests — structural
|
|
74
|
+
```typescript
|
|
75
|
+
// tests/architecture/layers.test.ts
|
|
76
|
+
// Uses madge to check import graph
|
|
77
|
+
it('repo layer does not import from service or runtime', async () => {
|
|
78
|
+
const graph = await madge('src/repo/', { tsConfig: 'tsconfig.json' })
|
|
79
|
+
const deps = graph.obj()
|
|
80
|
+
const violations = Object.entries(deps)
|
|
81
|
+
.filter(([, imports]) => imports.some(i => i.includes('service') || i.includes('runtime')))
|
|
82
|
+
expect(violations).toHaveLength(0)
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Cross-User Isolation Tests — mandatory for owned resources
|
|
87
|
+
|
|
88
|
+
Every repo that scopes queries by `userId` (an owned resource) MUST ship
|
|
89
|
+
`tests/integration/{resource}.isolation.test.ts` — copy `isolation.test.template.ts`.
|
|
90
|
+
It proves user B gets **404 (not 403)** for user A's resource on read/update/delete/list.
|
|
91
|
+
Enforced: `tests/architecture/isolation.test.ts` fails CI if the file is missing.
|
|
92
|
+
|
|
93
|
+
## Test File Naming
|
|
94
|
+
- Unit: `tests/unit/{layer}/{filename}.test.ts`
|
|
95
|
+
- Integration: `tests/integration/{feature}.test.ts`
|
|
96
|
+
- Isolation: `tests/integration/{resource}.isolation.test.ts` (required per owned resource)
|
|
97
|
+
- Architecture: `tests/architecture/layers.test.ts`, `tests/architecture/isolation.test.ts`
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"model": "claude-opus-4-8",
|
|
3
|
+
"permissions": {
|
|
4
|
+
"allow": [
|
|
5
|
+
"Bash(npm *)",
|
|
6
|
+
"Bash(npx *)",
|
|
7
|
+
"Bash(node *)",
|
|
8
|
+
"Bash(git *)",
|
|
9
|
+
"Bash(tsx *)",
|
|
10
|
+
"Bash(bash *)",
|
|
11
|
+
"Bash(sh *)",
|
|
12
|
+
"Bash(cat *)",
|
|
13
|
+
"Bash(ls *)",
|
|
14
|
+
"Bash(grep *)",
|
|
15
|
+
"Bash(find *)",
|
|
16
|
+
"Bash(wc *)",
|
|
17
|
+
"Bash(madge *)",
|
|
18
|
+
"Bash(k6 *)"
|
|
19
|
+
],
|
|
20
|
+
"deny": []
|
|
21
|
+
},
|
|
22
|
+
"hooks": {
|
|
23
|
+
"PostToolUse": [
|
|
24
|
+
{
|
|
25
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "bash .claude/hooks/post-write.sh"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|