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.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/cli.js +122 -0
  4. package/package.json +47 -0
  5. package/templates/express/.claude/CLAUDE.md +114 -0
  6. package/templates/express/.claude/agents/arch-validator.md +85 -0
  7. package/templates/express/.claude/agents/db-optimizer.md +72 -0
  8. package/templates/express/.claude/agents/doc-gardener.md +67 -0
  9. package/templates/express/.claude/agents/garbage-collector.md +94 -0
  10. package/templates/express/.claude/agents/gate-checker.md +176 -0
  11. package/templates/express/.claude/agents/reviewer.md +84 -0
  12. package/templates/express/.claude/agents/security-auditor.md +82 -0
  13. package/templates/express/.claude/hooks/post-write.sh +93 -0
  14. package/templates/express/.claude/rules/api.md +149 -0
  15. package/templates/express/.claude/rules/architecture.md +78 -0
  16. package/templates/express/.claude/rules/database.md +104 -0
  17. package/templates/express/.claude/rules/jobs.md +81 -0
  18. package/templates/express/.claude/rules/security.md +92 -0
  19. package/templates/express/.claude/rules/testing.md +97 -0
  20. package/templates/express/.claude/settings.json +35 -0
  21. package/templates/express/.claude/skills/00-infra-setup/SKILL.md +479 -0
  22. package/templates/express/.claude/skills/01-write-roadmap/SKILL.md +72 -0
  23. package/templates/express/.claude/skills/02-write-spec/SKILL.md +119 -0
  24. package/templates/express/.claude/skills/03-write-plan/SKILL.md +124 -0
  25. package/templates/express/.claude/skills/04-build-layer/SKILL.md +216 -0
  26. package/templates/express/.claude/skills/05-validate-layer/SKILL.md +43 -0
  27. package/templates/express/.claude/skills/06-push-layer/SKILL.md +59 -0
  28. package/templates/express/.claude/skills/07-layer-check/SKILL.md +62 -0
  29. package/templates/express/.claude/skills/08-garbage-collect/SKILL.md +86 -0
  30. package/templates/express/.claude/skills/09-doc-garden/SKILL.md +64 -0
  31. package/templates/express/.claude/skills/10-db-optimize/SKILL.md +96 -0
  32. package/templates/express/.claude/skills/11-load-test/SKILL.md +77 -0
  33. package/templates/express/.dockerignore +43 -0
  34. package/templates/express/.env.example +42 -0
  35. package/templates/express/.github/CODEOWNERS +22 -0
  36. package/templates/express/.github/dependabot.yml +48 -0
  37. package/templates/express/.github/pull_request_template.md +58 -0
  38. package/templates/express/.github/workflows/ci.yml +278 -0
  39. package/templates/express/.github/workflows/load-test.yml +41 -0
  40. package/templates/express/.gitleaks.toml +18 -0
  41. package/templates/express/.lintstagedrc.json +3 -0
  42. package/templates/express/.nvmrc +1 -0
  43. package/templates/express/.prettierrc +8 -0
  44. package/templates/express/.sequelizerc +8 -0
  45. package/templates/express/AGENTS.md +94 -0
  46. package/templates/express/ARCHITECTURE.md +161 -0
  47. package/templates/express/Dockerfile +40 -0
  48. package/templates/express/Makefile +79 -0
  49. package/templates/express/QUICKSTART.md +131 -0
  50. package/templates/express/docker-compose.yml +114 -0
  51. package/templates/express/docs/PLANS.md +111 -0
  52. package/templates/express/docs/QUALITY_SCORE.md +66 -0
  53. package/templates/express/docs/design-docs/core-beliefs.md +89 -0
  54. package/templates/express/docs/design-docs/decisions/ADR-000-infrastructure.md +70 -0
  55. package/templates/express/docs/design-docs/decisions/ADR-001-observability.md +82 -0
  56. package/templates/express/docs/design-docs/decisions/index.md +23 -0
  57. package/templates/express/docs/design-docs/index.md +11 -0
  58. package/templates/express/docs/design-docs/observability.md +194 -0
  59. package/templates/express/docs/design-docs/team-workflow.md +64 -0
  60. package/templates/express/docs/exec-plans/active/.gitkeep +0 -0
  61. package/templates/express/docs/exec-plans/completed/.gitkeep +0 -0
  62. package/templates/express/docs/exec-plans/tech-debt-tracker.md +31 -0
  63. package/templates/express/docs/generated/.gitkeep +0 -0
  64. package/templates/express/docs/product-specs/ROADMAP.md +49 -0
  65. package/templates/express/docs/product-specs/draft/.gitkeep +0 -0
  66. package/templates/express/docs/product-specs/index.md +21 -0
  67. package/templates/express/docs/product-specs/ready/.gitkeep +0 -0
  68. package/templates/express/eslint.config.mjs +94 -0
  69. package/templates/express/gitignore +36 -0
  70. package/templates/express/infra/monitoring/alloy/config.alloy +59 -0
  71. package/templates/express/jest.config.ts +61 -0
  72. package/templates/express/package.json +45 -0
  73. package/templates/express/scripts/openapi.export.ts +77 -0
  74. package/templates/express/tests/architecture/isolation.test.ts +51 -0
  75. package/templates/express/tests/architecture/layers.test.ts +83 -0
  76. package/templates/express/tests/integration/health.test.ts +40 -0
  77. package/templates/express/tests/integration/isolation.test.template.ts +88 -0
  78. package/templates/express/tests/load/smoke.js +46 -0
  79. package/templates/express/tests/load/soak.js +45 -0
  80. package/templates/express/tests/load/stress.js +44 -0
  81. package/templates/express/tests/unit/utils/response.util.test.ts +53 -0
  82. package/templates/express/tsconfig.build.json +8 -0
  83. package/templates/express/tsconfig.json +20 -0
  84. package/templates/fastapi/.claude/CLAUDE.md +122 -0
  85. package/templates/fastapi/.claude/agents/arch-validator.md +64 -0
  86. package/templates/fastapi/.claude/agents/db-optimizer.md +59 -0
  87. package/templates/fastapi/.claude/agents/doc-gardener.md +55 -0
  88. package/templates/fastapi/.claude/agents/garbage-collector.md +74 -0
  89. package/templates/fastapi/.claude/agents/gate-checker.md +133 -0
  90. package/templates/fastapi/.claude/agents/reviewer.md +74 -0
  91. package/templates/fastapi/.claude/agents/security-auditor.md +69 -0
  92. package/templates/fastapi/.claude/hooks/post-write.sh +65 -0
  93. package/templates/fastapi/.claude/rules/api.md +119 -0
  94. package/templates/fastapi/.claude/rules/architecture.md +84 -0
  95. package/templates/fastapi/.claude/rules/database.md +153 -0
  96. package/templates/fastapi/.claude/rules/jobs.md +85 -0
  97. package/templates/fastapi/.claude/rules/security.md +102 -0
  98. package/templates/fastapi/.claude/rules/testing.md +207 -0
  99. package/templates/fastapi/.claude/settings.json +50 -0
  100. package/templates/fastapi/.claude/skills/00-infra-setup/SKILL.md +311 -0
  101. package/templates/fastapi/.claude/skills/01-write-roadmap/SKILL.md +61 -0
  102. package/templates/fastapi/.claude/skills/02-write-spec/SKILL.md +82 -0
  103. package/templates/fastapi/.claude/skills/03-write-plan/SKILL.md +83 -0
  104. package/templates/fastapi/.claude/skills/04-build-layer/SKILL.md +170 -0
  105. package/templates/fastapi/.claude/skills/05-validate-layer/SKILL.md +20 -0
  106. package/templates/fastapi/.claude/skills/06-push-layer/SKILL.md +31 -0
  107. package/templates/fastapi/.claude/skills/07-layer-check/SKILL.md +51 -0
  108. package/templates/fastapi/.claude/skills/08-garbage-collect/SKILL.md +47 -0
  109. package/templates/fastapi/.claude/skills/09-doc-garden/SKILL.md +9 -0
  110. package/templates/fastapi/.claude/skills/10-db-optimize/SKILL.md +53 -0
  111. package/templates/fastapi/.claude/skills/11-load-test/SKILL.md +81 -0
  112. package/templates/fastapi/.dockerignore +30 -0
  113. package/templates/fastapi/.github/dependabot.yml +24 -0
  114. package/templates/fastapi/AGENTS.md +87 -0
  115. package/templates/fastapi/ARCHITECTURE.md +141 -0
  116. package/templates/fastapi/Makefile +42 -0
  117. package/templates/fastapi/README.md +179 -0
  118. package/templates/fastapi/docs/PLANS.md +26 -0
  119. package/templates/fastapi/docs/QUALITY_SCORE.md +26 -0
  120. package/templates/fastapi/docs/design-docs/core-beliefs.md +46 -0
  121. package/templates/fastapi/docs/design-docs/decisions/ADR-000-infrastructure.md +79 -0
  122. package/templates/fastapi/docs/design-docs/decisions/index.md +7 -0
  123. package/templates/fastapi/docs/design-docs/index.md +9 -0
  124. package/templates/fastapi/docs/exec-plans/active/.gitkeep +0 -0
  125. package/templates/fastapi/docs/exec-plans/completed/.gitkeep +0 -0
  126. package/templates/fastapi/docs/exec-plans/tech-debt-tracker.md +19 -0
  127. package/templates/fastapi/docs/generated/.gitkeep +0 -0
  128. package/templates/fastapi/docs/product-specs/ROADMAP.md +49 -0
  129. package/templates/fastapi/docs/product-specs/draft/.gitkeep +0 -0
  130. package/templates/fastapi/docs/product-specs/index.md +18 -0
  131. package/templates/fastapi/docs/product-specs/ready/.gitkeep +0 -0
  132. package/templates/fastapi/gitignore +46 -0
  133. package/templates/fastapi/scripts/gate.sh +68 -0
  134. package/templates/nestjs/.claude/CLAUDE.md +121 -0
  135. package/templates/nestjs/.claude/agents/db-optimizer.md +51 -0
  136. package/templates/nestjs/.claude/agents/doc-gardener.md +45 -0
  137. package/templates/nestjs/.claude/agents/garbage-collector.md +50 -0
  138. package/templates/nestjs/.claude/agents/gate-checker.md +127 -0
  139. package/templates/nestjs/.claude/agents/reviewer.md +68 -0
  140. package/templates/nestjs/.claude/agents/security-auditor.md +72 -0
  141. package/templates/nestjs/.claude/hooks/post-write.sh +91 -0
  142. package/templates/nestjs/.claude/rules/api.md +87 -0
  143. package/templates/nestjs/.claude/rules/architecture.md +104 -0
  144. package/templates/nestjs/.claude/rules/database.md +111 -0
  145. package/templates/nestjs/.claude/rules/security.md +110 -0
  146. package/templates/nestjs/.claude/rules/testing.md +145 -0
  147. package/templates/nestjs/.claude/settings.json +33 -0
  148. package/templates/nestjs/.claude/skills/00-infra-setup/SKILL.md +222 -0
  149. package/templates/nestjs/.claude/skills/01-write-spec/SKILL.md +25 -0
  150. package/templates/nestjs/.claude/skills/02-write-plan/SKILL.md +18 -0
  151. package/templates/nestjs/.claude/skills/03-build-layer/SKILL.md +242 -0
  152. package/templates/nestjs/.claude/skills/04-validate-layer/SKILL.md +4 -0
  153. package/templates/nestjs/.claude/skills/05-push-layer/SKILL.md +6 -0
  154. package/templates/nestjs/.claude/skills/06-layer-check/SKILL.md +12 -0
  155. package/templates/nestjs/.claude/skills/07-garbage-collect/SKILL.md +2 -0
  156. package/templates/nestjs/.claude/skills/08-doc-garden/SKILL.md +4 -0
  157. package/templates/nestjs/.claude/skills/09-api-sync/SKILL.md +36 -0
  158. package/templates/nestjs/.claude/skills/10-load-test/SKILL.md +29 -0
  159. package/templates/nestjs/.env.example +11 -0
  160. package/templates/nestjs/AGENTS.md +88 -0
  161. package/templates/nestjs/ARCHITECTURE.md +192 -0
  162. package/templates/nestjs/docs/PLANS.md +22 -0
  163. package/templates/nestjs/docs/QUALITY_SCORE.md +27 -0
  164. package/templates/nestjs/docs/design-docs/core-beliefs.md +18 -0
  165. package/templates/nestjs/docs/design-docs/decisions/ADR-000-infrastructure.md +58 -0
  166. package/templates/nestjs/docs/design-docs/decisions/index.md +7 -0
  167. package/templates/nestjs/docs/design-docs/index.md +7 -0
  168. package/templates/nestjs/docs/exec-plans/active/.gitkeep +0 -0
  169. package/templates/nestjs/docs/exec-plans/completed/.gitkeep +0 -0
  170. package/templates/nestjs/docs/exec-plans/tech-debt-tracker.md +16 -0
  171. package/templates/nestjs/docs/generated/.gitkeep +0 -0
  172. package/templates/nestjs/docs/product-specs/draft/.gitkeep +0 -0
  173. package/templates/nestjs/docs/product-specs/index.md +7 -0
  174. package/templates/nestjs/docs/product-specs/ready/.gitkeep +0 -0
  175. package/templates/nestjs/gitignore +8 -0
  176. package/templates/nextjs/.claude/CLAUDE.md +139 -0
  177. package/templates/nextjs/.claude/agents/arch-validator.md +66 -0
  178. package/templates/nextjs/.claude/agents/contract-checker.md +78 -0
  179. package/templates/nextjs/.claude/agents/doc-gardener.md +52 -0
  180. package/templates/nextjs/.claude/agents/garbage-collector.md +61 -0
  181. package/templates/nextjs/.claude/agents/gate-checker.md +129 -0
  182. package/templates/nextjs/.claude/agents/perf-auditor.md +109 -0
  183. package/templates/nextjs/.claude/agents/reviewer.md +79 -0
  184. package/templates/nextjs/.claude/agents/security-auditor.md +50 -0
  185. package/templates/nextjs/.claude/hooks/post-write.sh +121 -0
  186. package/templates/nextjs/.claude/rules/api-contract.md +166 -0
  187. package/templates/nextjs/.claude/rules/architecture.md +99 -0
  188. package/templates/nextjs/.claude/rules/components.md +122 -0
  189. package/templates/nextjs/.claude/rules/security.md +122 -0
  190. package/templates/nextjs/.claude/rules/testing.md +187 -0
  191. package/templates/nextjs/.claude/scripts/infra-setup.sh +550 -0
  192. package/templates/nextjs/.claude/settings.json +33 -0
  193. package/templates/nextjs/.claude/skills/00-infra-setup/SKILL.md +471 -0
  194. package/templates/nextjs/.claude/skills/01-write-roadmap/SKILL.md +77 -0
  195. package/templates/nextjs/.claude/skills/02-write-spec/SKILL.md +78 -0
  196. package/templates/nextjs/.claude/skills/03-write-plan/SKILL.md +91 -0
  197. package/templates/nextjs/.claude/skills/04-build-layer/SKILL.md +141 -0
  198. package/templates/nextjs/.claude/skills/05-validate-layer/SKILL.md +13 -0
  199. package/templates/nextjs/.claude/skills/06-push-layer/SKILL.md +20 -0
  200. package/templates/nextjs/.claude/skills/07-layer-check/SKILL.md +28 -0
  201. package/templates/nextjs/.claude/skills/08-garbage-collect/SKILL.md +14 -0
  202. package/templates/nextjs/.claude/skills/09-doc-garden/SKILL.md +12 -0
  203. package/templates/nextjs/.claude/skills/10-api-sync/SKILL.md +50 -0
  204. package/templates/nextjs/.claude/skills/11-perf-budget/SKILL.md +51 -0
  205. package/templates/nextjs/.dockerignore +25 -0
  206. package/templates/nextjs/.gitattributes +10 -0
  207. package/templates/nextjs/.github/CODEOWNERS +24 -0
  208. package/templates/nextjs/.github/dependabot.yml +31 -0
  209. package/templates/nextjs/.github/pull_request_template.md +59 -0
  210. package/templates/nextjs/.github/workflows/ci.yml +143 -0
  211. package/templates/nextjs/.github/workflows/lighthouse.yml +54 -0
  212. package/templates/nextjs/.github/workflows/load-test.yml +54 -0
  213. package/templates/nextjs/.husky/pre-commit +1 -0
  214. package/templates/nextjs/.lintstagedrc.json +4 -0
  215. package/templates/nextjs/.mcp.json +8 -0
  216. package/templates/nextjs/.prettierignore +13 -0
  217. package/templates/nextjs/.prettierrc +9 -0
  218. package/templates/nextjs/AGENTS.md +88 -0
  219. package/templates/nextjs/ARCHITECTURE.md +165 -0
  220. package/templates/nextjs/Dockerfile +38 -0
  221. package/templates/nextjs/Makefile +50 -0
  222. package/templates/nextjs/QUICKSTART.md +128 -0
  223. package/templates/nextjs/docs/PLANS.md +23 -0
  224. package/templates/nextjs/docs/QUALITY_SCORE.md +27 -0
  225. package/templates/nextjs/docs/design-docs/core-beliefs.md +43 -0
  226. package/templates/nextjs/docs/design-docs/decisions/ADR-000-infrastructure.md +72 -0
  227. package/templates/nextjs/docs/design-docs/decisions/index.md +7 -0
  228. package/templates/nextjs/docs/design-docs/index.md +7 -0
  229. package/templates/nextjs/docs/design-docs/team-workflow.md +66 -0
  230. package/templates/nextjs/docs/exec-plans/active/.gitkeep +0 -0
  231. package/templates/nextjs/docs/exec-plans/completed/.gitkeep +0 -0
  232. package/templates/nextjs/docs/exec-plans/tech-debt-tracker.md +19 -0
  233. package/templates/nextjs/docs/generated/.gitkeep +0 -0
  234. package/templates/nextjs/docs/product-specs/ROADMAP.md +59 -0
  235. package/templates/nextjs/docs/product-specs/draft/.gitkeep +0 -0
  236. package/templates/nextjs/docs/product-specs/index.md +18 -0
  237. package/templates/nextjs/docs/product-specs/ready/.gitkeep +0 -0
  238. package/templates/nextjs/eslint.config.mjs +150 -0
  239. package/templates/nextjs/gitignore +35 -0
  240. package/templates/nextjs/lighthouserc.js +37 -0
@@ -0,0 +1,550 @@
1
+ #!/usr/bin/env bash
2
+ # .claude/scripts/infra-setup.sh
3
+ # Deterministic POST-SCAFFOLD setup: deps + shadcn + dirs + package.json scripts +
4
+ # gate-critical config/test/observability files + husky.
5
+ # Run via: bash .claude/scripts/infra-setup.sh
6
+ #
7
+ # PRECONDITION: the Next.js app is already scaffolded (Step 1 of the skill runs
8
+ # `create-next-app` via the park-and-restore sequence — it cannot live here because
9
+ # this script's own home (.claude/) is one of the files that gets parked aside).
10
+ # After this finishes, Claude authors the remaining harness glue (Step 5 of the skill).
11
+ #
12
+ # No version pins for libraries — everything resolves to latest LTS. Reproducibility
13
+ # is provided instead by the committed package-lock.json + `npm ci` in CI + Dependabot.
14
+
15
+ set -euo pipefail
16
+
17
+ if [[ ! -f package.json ]]; then
18
+ echo "❌ No package.json — run the scaffold step first (Step 1 of /infra-setup:"
19
+ echo " park harness files → npx create-next-app@latest . → restore). Aborting."
20
+ exit 1
21
+ fi
22
+
23
+ # write_if_absent <path> ← content on stdin. Never clobbers an existing file, so a
24
+ # re-run (or a user edit) is safe. Harness-owned files are created exactly once.
25
+ write_if_absent() {
26
+ local p="$1"
27
+ if [[ -e "$p" ]]; then
28
+ echo " • skip $p (exists)"
29
+ cat >/dev/null
30
+ return
31
+ fi
32
+ mkdir -p "$(dirname "$p")"
33
+ cat >"$p"
34
+ echo " • wrote $p"
35
+ }
36
+
37
+ echo "▶ Step 1 — install dependencies (latest LTS, no pins)"
38
+ # Server state + API contract
39
+ npm install @tanstack/react-query @tanstack/react-query-devtools openapi-fetch
40
+ npm install -D openapi-typescript
41
+ # Client state
42
+ npm install zustand
43
+ # Forms + validation
44
+ npm install react-hook-form @hookform/resolvers zod
45
+ # Testing
46
+ npm install -D vitest @vitejs/plugin-react jsdom @vitest/coverage-v8
47
+ npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
48
+ npm install -D msw @playwright/test
49
+ # Performance
50
+ npm install -D @next/bundle-analyzer
51
+ # Linting (the committed eslint.config.mjs uses Next's native flat config — no FlatCompat)
52
+ npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-import
53
+ # Formatting + pre-commit (config files are committed in the template)
54
+ npm install -D prettier prettier-plugin-tailwindcss eslint-config-prettier husky lint-staged
55
+ # Core Web Vitals budget (config/workflows committed in the template)
56
+ npm install -D @lhci/cli
57
+
58
+ echo "▶ Step 2 — shadcn/ui init + base components (non-interactive)"
59
+ npx shadcn@latest init -d
60
+ npx shadcn@latest add -y button input label form card dialog sonner badge skeleton
61
+
62
+ echo "▶ Step 3 — directory structure"
63
+ mkdir -p \
64
+ src/types src/lib src/hooks src/store src/features src/components/shared src/utils \
65
+ tests/unit/hooks tests/unit/features tests/unit/components tests/unit/utils \
66
+ tests/e2e tests/e2e/helpers tests/visual tests/architecture tests/load tests/mocks tests/utils \
67
+ docs/product-specs/draft docs/product-specs/ready \
68
+ docs/exec-plans/active docs/exec-plans/completed \
69
+ docs/design-docs/decisions docs/generated
70
+
71
+ echo "▶ Step 4 — package.json scripts (patched via node, no clobber of dev/build/start)"
72
+ node -e "
73
+ const fs = require('fs');
74
+ const p = JSON.parse(fs.readFileSync('package.json', 'utf8'));
75
+ // Defaults fill in if create-next-app's names ever change; existing scripts win over
76
+ // them; harness scripts win over everything (notably: lint = 'eslint .', not 'next lint').
77
+ const defaults = { dev: 'next dev', build: 'next build', start: 'next start' };
78
+ const harness = {
79
+ 'api:sync': 'openapi-typescript openapi.json -o src/types/api.generated.ts',
80
+ 'test': 'vitest',
81
+ 'test:ui': 'vitest --ui',
82
+ 'test:coverage': 'vitest run --coverage',
83
+ 'test:e2e': 'playwright test',
84
+ 'test:visual': 'playwright test tests/visual/',
85
+ 'typecheck': 'tsc --noEmit',
86
+ 'lint': 'eslint .',
87
+ 'lint:fix': 'eslint . --fix',
88
+ 'format': 'prettier --write .',
89
+ 'format:check': 'prettier --check .',
90
+ 'gate': 'npm run typecheck && npm run lint && npm run format:check && npm run test:coverage',
91
+ 'analyze': 'ANALYZE=true next build',
92
+ 'prepare': 'husky',
93
+ };
94
+ p.scripts = Object.assign(defaults, p.scripts, harness);
95
+ fs.writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n');
96
+ console.log(' scripts:', Object.keys(p.scripts).join(', '));
97
+ "
98
+
99
+ echo "▶ Step 5 — gate-critical config files (deterministic, never improvised)"
100
+
101
+ # next.config.ts: OVERWRITE the trivial generated one with a known-good base. `output:
102
+ # 'standalone'` is REQUIRED by the Dockerfile (it copies .next/standalone) — making it
103
+ # deterministic here means the container build can't silently break. Security headers
104
+ # included. reactCompiler / typedRoutes are version-sensitive, so Claude adds them later
105
+ # (Step 6) after checking the installed Next version against the freshness rule.
106
+ cat >next.config.ts <<'EOF'
107
+ import type { NextConfig } from 'next'
108
+
109
+ const nextConfig: NextConfig = {
110
+ output: 'standalone', // minimal server bundle for the Docker runner stage
111
+ async headers() {
112
+ return [
113
+ {
114
+ source: '/(.*)',
115
+ headers: [
116
+ { key: 'X-Frame-Options', value: 'DENY' },
117
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
118
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
119
+ { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
120
+ ],
121
+ },
122
+ ]
123
+ },
124
+ }
125
+
126
+ export default nextConfig
127
+ EOF
128
+ echo " • wrote next.config.ts (output: standalone + security headers)"
129
+
130
+ write_if_absent vitest.config.ts <<'EOF'
131
+ import { defineConfig, configDefaults } from 'vitest/config'
132
+ import react from '@vitejs/plugin-react'
133
+ import path from 'path'
134
+
135
+ export default defineConfig({
136
+ plugins: [react()],
137
+ test: {
138
+ environment: 'jsdom',
139
+ globals: true,
140
+ setupFiles: ['./tests/setup.ts'],
141
+ // Vitest runs unit + architecture tests ONLY. Playwright owns tests/e2e &
142
+ // tests/visual; k6 owns tests/load — exclude them or Vitest tries to run them
143
+ // and crashes on the Playwright/k6 globals.
144
+ include: ['tests/**/*.{test,spec}.{ts,tsx}', 'src/**/*.{test,spec}.{ts,tsx}'],
145
+ exclude: [...configDefaults.exclude, 'tests/e2e/**', 'tests/visual/**', 'tests/load/**'],
146
+ // env.ts validates NEXT_PUBLIC_* at import — provide them so api-client imports
147
+ // don't throw "Invalid environment variables" in jsdom.
148
+ env: { NEXT_PUBLIC_API_URL: 'http://localhost:8000' },
149
+ coverage: {
150
+ provider: 'v8',
151
+ // Scope coverage to the layers the rules actually govern (utils/hooks/features/
152
+ // components). lib/store/app/instrumentation/types are infra — not gated — so
153
+ // they don't drag the gate below threshold for being untested.
154
+ include: [
155
+ 'src/utils/**/*.{ts,tsx}',
156
+ 'src/hooks/**/*.{ts,tsx}',
157
+ 'src/features/**/*.{ts,tsx}',
158
+ 'src/components/**/*.{ts,tsx}',
159
+ ],
160
+ exclude: ['src/components/ui/**', 'src/components/providers.tsx', 'src/**/*.d.ts'],
161
+ // Per-layer thresholds — these MATCH .claude/rules/testing.md and the PR
162
+ // template. The flat block is the global floor; the glob blocks raise
163
+ // specific layers. A breach fails `vitest run --coverage` (and therefore CI).
164
+ thresholds: {
165
+ lines: 70,
166
+ functions: 70,
167
+ branches: 70,
168
+ statements: 70,
169
+ 'src/utils/**': { lines: 100, functions: 100, branches: 100, statements: 100 },
170
+ 'src/hooks/**': { lines: 80, functions: 80, branches: 80, statements: 80 },
171
+ 'src/features/**': { lines: 70, functions: 70, branches: 70, statements: 70 },
172
+ 'src/components/**': { lines: 70, functions: 70, branches: 70, statements: 70 },
173
+ },
174
+ },
175
+ },
176
+ resolve: { alias: { '@': path.resolve(__dirname, './src') } },
177
+ })
178
+ EOF
179
+
180
+ write_if_absent playwright.config.ts <<'EOF'
181
+ import { defineConfig, devices } from '@playwright/test'
182
+
183
+ export default defineConfig({
184
+ testDir: './tests/e2e',
185
+ fullyParallel: true,
186
+ forbidOnly: !!process.env.CI,
187
+ retries: process.env.CI ? 2 : 0,
188
+ reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list',
189
+ use: {
190
+ baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000',
191
+ trace: 'on-first-retry',
192
+ },
193
+ projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
194
+ webServer: {
195
+ command: 'npm run dev',
196
+ url: 'http://localhost:3000',
197
+ reuseExistingServer: !process.env.CI,
198
+ },
199
+ })
200
+ EOF
201
+
202
+ write_if_absent tests/setup.ts <<'EOF'
203
+ import '@testing-library/jest-dom/vitest'
204
+ import { beforeAll, afterAll, afterEach } from 'vitest'
205
+ import { server } from './mocks/server'
206
+
207
+ beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
208
+ afterEach(() => server.resetHandlers())
209
+ afterAll(() => server.close())
210
+ EOF
211
+
212
+ echo "▶ Step 6 — gate-critical test artifacts (the gate references these — ship them real)"
213
+
214
+ write_if_absent tests/architecture/layers.test.ts <<'EOF'
215
+ import { describe, it, expect } from 'vitest'
216
+ import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs'
217
+ import { join } from 'node:path'
218
+
219
+ // Layer import rules. A layer must NOT import from any of its "forbidden" layers.
220
+ // Aliases are matched as `@/<layer>` and bare `app/` imports. This is the structural
221
+ // backstop to the ESLint no-restricted-imports rules (defence in depth: ESLint runs
222
+ // per-file with config that can be disabled; this test reads the raw import graph).
223
+ const RULES: Record<string, string[]> = {
224
+ 'src/types': ['@/lib', '@/hooks', '@/features', '@/store', '@/components'],
225
+ 'src/lib': ['@/hooks', '@/features', '@/components'],
226
+ 'src/store': ['@/hooks', '@/features', '@/components'],
227
+ 'src/hooks': ['@/features', '@/components'],
228
+ 'src/features': ['app/'],
229
+ }
230
+
231
+ function walk(dir: string): string[] {
232
+ if (!existsSync(dir)) return []
233
+ return readdirSync(dir).flatMap((name) => {
234
+ const full = join(dir, name)
235
+ if (statSync(full).isDirectory()) return walk(full)
236
+ return /\.(ts|tsx)$/.test(full) && !full.endsWith('.d.ts') ? [full] : []
237
+ })
238
+ }
239
+
240
+ const IMPORT_RE = /(?:import|from)\s+['"]([^'"]+)['"]/g
241
+
242
+ function importsOf(file: string): string[] {
243
+ const src = readFileSync(file, 'utf8')
244
+ const out: string[] = []
245
+ for (const m of src.matchAll(IMPORT_RE)) out.push(m[1])
246
+ return out
247
+ }
248
+
249
+ describe('layer import rules', () => {
250
+ for (const [layer, forbidden] of Object.entries(RULES)) {
251
+ it(`${layer} does not import from ${forbidden.join(', ')}`, () => {
252
+ const violations: string[] = []
253
+ for (const file of walk(layer)) {
254
+ for (const imp of importsOf(file)) {
255
+ const hit = forbidden.some((f) => {
256
+ const base = f.endsWith('/') ? f.slice(0, -1) : f
257
+ return imp === base || imp.startsWith(base + '/')
258
+ })
259
+ if (hit) violations.push(`${file} → ${imp}`)
260
+ }
261
+ }
262
+ expect(violations, violations.join('\n')).toHaveLength(0)
263
+ })
264
+ }
265
+ })
266
+
267
+ describe('app pages contain no business logic', () => {
268
+ it('page.tsx files import features, not hooks directly', () => {
269
+ // Supports both app-router layouts (src/app with --src-dir, or top-level app/).
270
+ const pages = [...walk('src/app'), ...walk('app')].filter((f) => f.endsWith('page.tsx'))
271
+ const violations = pages.filter((f) =>
272
+ importsOf(f).some((imp) => imp.startsWith('@/hooks')),
273
+ )
274
+ expect(violations, violations.join('\n')).toHaveLength(0)
275
+ })
276
+ })
277
+ EOF
278
+
279
+ write_if_absent tests/e2e/helpers/auth.ts <<'EOF'
280
+ import { type Page, expect } from '@playwright/test'
281
+
282
+ // Logs a user in through the real login form and waits for the post-login redirect.
283
+ // Adjust selectors / destination to match your app's auth flow. Used by the
284
+ // REQUIRED cross-user isolation test (tests/e2e/isolation.spec.ts).
285
+ export async function loginAs(page: Page, email: string, password = 'password123') {
286
+ await page.goto('/login')
287
+ await page.getByLabel('Email').fill(email)
288
+ await page.getByLabel('Password').fill(password)
289
+ await page.getByRole('button', { name: /sign in/i }).click()
290
+ await expect(page).not.toHaveURL(/\/login$/)
291
+ }
292
+ EOF
293
+
294
+ write_if_absent tests/e2e/isolation.spec.ts <<'EOF'
295
+ import { test, expect } from '@playwright/test'
296
+ import { loginAs } from './helpers/auth'
297
+
298
+ // The single most important security test: confirm User B cannot reach User A's
299
+ // resource, and that the app reveals 404 (not 403 — a 403 leaks that it exists).
300
+ // Add one of these per user-owned resource type. The backend enforces isolation via
301
+ // `userId in WHERE`; this proves the frontend surfaces it correctly.
302
+ //
303
+ // SKIPPED BY DEFAULT: a fresh scaffold has no /login flow or backend, so this would
304
+ // fail CI on day one. Change `test.skip` → `test` (and wire helpers/auth.ts) the moment
305
+ // you ship your first user-owned resource. The PR template makes that a DoD item.
306
+ test.skip('User B cannot access User A resource (404, not 403, not the data)', async ({ page }) => {
307
+ await loginAs(page, 'usera@example.com')
308
+ await page.getByRole('button', { name: 'New Application' }).click()
309
+ await page.getByLabel('Company').fill('Acme Corp')
310
+ await page.getByRole('button', { name: 'Create' }).click()
311
+ await expect(page).toHaveURL(/\/applications\/[\w-]+$/)
312
+ const url = page.url()
313
+
314
+ await loginAs(page, 'userb@example.com')
315
+ const res = await page.goto(url)
316
+
317
+ expect(res?.status()).toBe(404)
318
+ await expect(page.getByText('Acme Corp')).toHaveCount(0)
319
+ await expect(page.getByText(/not found/i)).toBeVisible()
320
+ })
321
+ EOF
322
+
323
+ # k6 load scripts — pure, app-independent; driven by .github/workflows/load-test.yml.
324
+ # All three share the same request shape and differ only in load profile.
325
+ write_if_absent tests/load/smoke.js <<'EOF'
326
+ import http from 'k6/http'
327
+ import { check, sleep } from 'k6'
328
+
329
+ const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
330
+ const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''
331
+
332
+ // Smoke: minimal load — proves the deploy is up and within budget at all.
333
+ export const options = {
334
+ vus: 1,
335
+ duration: '30s',
336
+ thresholds: {
337
+ http_req_failed: ['rate<0.01'],
338
+ http_req_duration: ['p(95)<800'],
339
+ },
340
+ }
341
+
342
+ const params = AUTH_TOKEN ? { headers: { Authorization: `Bearer ${AUTH_TOKEN}` } } : {}
343
+
344
+ export default function () {
345
+ const res = http.get(`${BASE_URL}/`, params)
346
+ check(res, { 'status is 200': (r) => r.status === 200 })
347
+ sleep(1)
348
+ }
349
+ EOF
350
+
351
+ write_if_absent tests/load/stress.js <<'EOF'
352
+ import http from 'k6/http'
353
+ import { check, sleep } from 'k6'
354
+
355
+ const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
356
+ const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''
357
+
358
+ // Stress: ramp to find the breaking point. Looser failure budget than smoke.
359
+ export const options = {
360
+ stages: [
361
+ { duration: '1m', target: 50 },
362
+ { duration: '3m', target: 50 },
363
+ { duration: '1m', target: 100 },
364
+ { duration: '3m', target: 100 },
365
+ { duration: '1m', target: 0 },
366
+ ],
367
+ thresholds: {
368
+ http_req_failed: ['rate<0.05'],
369
+ http_req_duration: ['p(95)<1500'],
370
+ },
371
+ }
372
+
373
+ const params = AUTH_TOKEN ? { headers: { Authorization: `Bearer ${AUTH_TOKEN}` } } : {}
374
+
375
+ export default function () {
376
+ const res = http.get(`${BASE_URL}/`, params)
377
+ check(res, { 'status is 200': (r) => r.status === 200 })
378
+ sleep(1)
379
+ }
380
+ EOF
381
+
382
+ write_if_absent tests/load/soak.js <<'EOF'
383
+ import http from 'k6/http'
384
+ import { check, sleep } from 'k6'
385
+
386
+ const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
387
+ const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''
388
+
389
+ // Soak: sustained moderate load for an hour — surfaces memory leaks / slow drift.
390
+ export const options = {
391
+ stages: [
392
+ { duration: '2m', target: 30 },
393
+ { duration: '56m', target: 30 },
394
+ { duration: '2m', target: 0 },
395
+ ],
396
+ thresholds: {
397
+ http_req_failed: ['rate<0.02'],
398
+ http_req_duration: ['p(95)<1000'],
399
+ },
400
+ }
401
+
402
+ const params = AUTH_TOKEN ? { headers: { Authorization: `Bearer ${AUTH_TOKEN}` } } : {}
403
+
404
+ export default function () {
405
+ const res = http.get(`${BASE_URL}/`, params)
406
+ check(res, { 'status is 200': (r) => r.status === 200 })
407
+ sleep(1)
408
+ }
409
+ EOF
410
+
411
+ echo "▶ Step 7 — observability + contract glue (logger, api-error, error boundary, instrumentation)"
412
+
413
+ write_if_absent src/lib/api-error.ts <<'EOF'
414
+ // src/lib/api-error.ts
415
+ // Normalises backend errors into a typed ApiError so hooks NEVER discard the backend
416
+ // code/message for a hard-coded string. Reads the canonical error envelope
417
+ // { error: { code, message, details? } } and falls back for a bare FastAPI { detail }
418
+ // or an opaque error. See .claude/rules/api-contract.md (Canonical Wire Contract).
419
+ export class ApiError extends Error {
420
+ constructor(
421
+ public readonly code: string,
422
+ message: string,
423
+ public readonly details?: unknown,
424
+ ) {
425
+ super(message)
426
+ this.name = 'ApiError'
427
+ }
428
+ }
429
+
430
+ export function toApiError(error: unknown, fallback = 'Request failed'): ApiError {
431
+ const e = error as
432
+ | { error?: { code?: string; message?: string; details?: unknown }; detail?: unknown }
433
+ | undefined
434
+ if (e?.error?.message) return new ApiError(e.error.code ?? 'unknown', e.error.message, e.error.details)
435
+ if (typeof e?.detail === 'string') return new ApiError('unknown', e.detail)
436
+ return new ApiError('unknown', fallback)
437
+ }
438
+ EOF
439
+
440
+ write_if_absent src/lib/logger.ts <<'EOF'
441
+ // src/lib/logger.ts
442
+ // Zero-dependency structured logger for the frontend. Emits single-line JSON so logs
443
+ // are greppable in the browser console AND parseable when captured by a runtime/edge
444
+ // collector. This is the ONLY sanctioned logging path — raw console.log is flagged by
445
+ // .claude/hooks/post-write.sh. Swap `emit` for a transport (Sentry, OTel logs) later.
446
+
447
+ type Level = 'debug' | 'info' | 'warn' | 'error'
448
+ type Fields = Record<string, unknown>
449
+
450
+ const ORDER: Record<Level, number> = { debug: 10, info: 20, warn: 30, error: 40 }
451
+
452
+ // NODE_ENV is a build-time constant Next inlines — the one process.env read allowed
453
+ // outside env.ts (the validated NEXT_PUBLIC boundary).
454
+ function threshold(): number {
455
+ return process.env.NODE_ENV === 'production' ? ORDER.info : ORDER.debug
456
+ }
457
+
458
+ function emit(level: Level, msg: string, fields?: Fields) {
459
+ if (ORDER[level] < threshold()) return
460
+ const line = JSON.stringify({ level, msg, time: new Date().toISOString(), ...fields })
461
+ if (level === 'error') console.error(line)
462
+ else if (level === 'warn') console.warn(line)
463
+ else console.info(line)
464
+ }
465
+
466
+ export const logger = {
467
+ debug: (msg: string, fields?: Fields) => emit('debug', msg, fields),
468
+ info: (msg: string, fields?: Fields) => emit('info', msg, fields),
469
+ warn: (msg: string, fields?: Fields) => emit('warn', msg, fields),
470
+ error: (msg: string, fields?: Fields) => emit('error', msg, fields),
471
+ }
472
+ EOF
473
+
474
+ write_if_absent src/instrumentation.ts <<'EOF'
475
+ // src/instrumentation.ts
476
+ // Next.js instrumentation hook — runs once when the server process boots. This is the
477
+ // seam for wiring OpenTelemetry / a trace exporter later; for now it emits a structured
478
+ // boot log so deploys are observable.
479
+ // https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
480
+ export async function register() {
481
+ const { logger } = await import('@/lib/logger')
482
+ logger.info('app.boot', { runtime: process.env.NEXT_RUNTIME ?? 'nodejs' })
483
+ }
484
+ EOF
485
+
486
+ write_if_absent src/app/global-error.tsx <<'EOF'
487
+ 'use client' // Client: error boundaries must be client components
488
+
489
+ // src/app/global-error.tsx
490
+ // Catches render errors that escape the route tree and reports them through the
491
+ // structured logger. Replace the logger.error call with your error-tracking SDK
492
+ // (Sentry, etc.) when one is wired. global-error replaces the root layout, so it
493
+ // must render its own <html>/<body>.
494
+ import { useEffect } from 'react'
495
+ import { logger } from '@/lib/logger'
496
+
497
+ export default function GlobalError({
498
+ error,
499
+ reset,
500
+ }: {
501
+ error: Error & { digest?: string }
502
+ reset: () => void
503
+ }) {
504
+ useEffect(() => {
505
+ logger.error('unhandled.render_error', {
506
+ message: error.message,
507
+ digest: error.digest,
508
+ stack: error.stack,
509
+ })
510
+ }, [error])
511
+
512
+ return (
513
+ <html lang="en">
514
+ <body>
515
+ <div role="alert" style={{ padding: '2rem', fontFamily: 'system-ui' }}>
516
+ <h1>Something went wrong</h1>
517
+ <p>An unexpected error occurred. The team has been notified.</p>
518
+ <button onClick={() => reset()}>Try again</button>
519
+ </div>
520
+ </body>
521
+ </html>
522
+ )
523
+ }
524
+ EOF
525
+
526
+ write_if_absent .env.example <<'EOF'
527
+ # Public base URL of the backend API (validated in src/lib/env.ts).
528
+ NEXT_PUBLIC_API_URL=http://localhost:8000
529
+ EOF
530
+
531
+ echo "▶ Step 8 — activate husky (pre-commit hook is committed at .husky/pre-commit)"
532
+ npm run prepare
533
+
534
+ echo "▶ Step 8b — format the scaffold so the first gate run is clean"
535
+ # create-next-app emits double-quote/semicolon code; the harness Prettier config differs.
536
+ # Format now so `npm run format:check` (in the gate/CI) passes without a manual pass.
537
+ npm run format >/dev/null 2>&1 || echo " ⚠ prettier format skipped (non-fatal)"
538
+
539
+ echo "▶ Step 9 — commit the lockfile for reproducible installs"
540
+ # `npm install` above generated package-lock.json. Committing it (with `npm ci` in CI)
541
+ # is what makes scaffolds reproducible despite the no-pins policy. Dependabot then
542
+ # proposes reviewed bumps. (Actual commit happens in the skill's Commit step.)
543
+ if [[ ! -f package-lock.json ]]; then
544
+ echo " ⚠ no package-lock.json found — expected after npm install. Check npm version."
545
+ fi
546
+
547
+ echo "✅ Deterministic infra complete."
548
+ echo " Configs, gate tests, load tests, and observability glue are written."
549
+ echo " Next: Claude authors the remaining glue (Step 5 of the skill) — env.ts,"
550
+ echo " api-client.ts, utils, store, providers, mocks, and ADR-000."
@@ -0,0 +1,33 @@
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(bash *)",
10
+ "Bash(sh *)",
11
+ "Bash(cat *)",
12
+ "Bash(ls *)",
13
+ "Bash(grep *)",
14
+ "Bash(find *)",
15
+ "Bash(wc *)",
16
+ "Bash(k6 *)"
17
+ ],
18
+ "deny": []
19
+ },
20
+ "hooks": {
21
+ "PostToolUse": [
22
+ {
23
+ "matcher": "Write|Edit|MultiEdit",
24
+ "hooks": [
25
+ {
26
+ "type": "command",
27
+ "command": "bash .claude/hooks/post-write.sh"
28
+ }
29
+ ]
30
+ }
31
+ ]
32
+ }
33
+ }