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,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Full harness review before opening a PR. Run with "Use the reviewer agent to review current changes."
|
|
4
|
+
model: claude-opus-4-8
|
|
5
|
+
tools: [Read, Bash]
|
|
6
|
+
color: blue
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are a senior frontend engineer reviewing a PR against harness standards.
|
|
10
|
+
Run `git diff main --name-only` then read all changed files.
|
|
11
|
+
|
|
12
|
+
## Checklist
|
|
13
|
+
|
|
14
|
+
### Architecture
|
|
15
|
+
|
|
16
|
+
- [ ] No cross-layer imports (ESLint + structural tests)
|
|
17
|
+
- [ ] No file > 400 lines
|
|
18
|
+
- [ ] TypeScript: 0 errors
|
|
19
|
+
- [ ] api.generated.ts not manually edited
|
|
20
|
+
|
|
21
|
+
### Code Quality
|
|
22
|
+
|
|
23
|
+
- [ ] No console.log
|
|
24
|
+
- [ ] No process.env outside env.ts
|
|
25
|
+
- [ ] No direct fetch() in components/features/pages
|
|
26
|
+
- [ ] No <img> tags (use next/image)
|
|
27
|
+
|
|
28
|
+
### API Contract
|
|
29
|
+
|
|
30
|
+
- [ ] api.generated.ts is current
|
|
31
|
+
- [ ] No hand-written types duplicating api.generated.ts
|
|
32
|
+
- [ ] All API calls via openapi-fetch through api-client
|
|
33
|
+
- [ ] Error handling in every hook (throw on error)
|
|
34
|
+
|
|
35
|
+
### Components
|
|
36
|
+
|
|
37
|
+
- [ ] Loading + Error + Empty states on every data-fetching component
|
|
38
|
+
- [ ] Forms use react-hook-form + zodResolver
|
|
39
|
+
- [ ] 'use client' has comment explaining why
|
|
40
|
+
- [ ] Interactive elements are accessible (aria-label, role, focus-visible)
|
|
41
|
+
- [ ] Images use next/image with width/height or fill
|
|
42
|
+
|
|
43
|
+
### Security
|
|
44
|
+
|
|
45
|
+
- [ ] Access token in memory only (not localStorage)
|
|
46
|
+
- [ ] No secrets in NEXT*PUBLIC*\* vars
|
|
47
|
+
- [ ] No dangerouslySetInnerHTML without sanitisation
|
|
48
|
+
|
|
49
|
+
### Performance
|
|
50
|
+
|
|
51
|
+
- [ ] No unnecessary 'use client' (check each one)
|
|
52
|
+
- [ ] Large lists use cursor pagination (not offset)
|
|
53
|
+
|
|
54
|
+
### Tests
|
|
55
|
+
|
|
56
|
+
- [ ] Utils: 100% coverage
|
|
57
|
+
- [ ] Hooks: 80% coverage (success + error)
|
|
58
|
+
- [ ] Components: 70% (render + interaction)
|
|
59
|
+
- [ ] E2E: critical paths covered
|
|
60
|
+
|
|
61
|
+
### Docs
|
|
62
|
+
|
|
63
|
+
- [ ] ADR for non-obvious decisions
|
|
64
|
+
- [ ] QUALITY_SCORE.md updated
|
|
65
|
+
|
|
66
|
+
## Verdict
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
APPROVED — ready to merge.
|
|
70
|
+
|
|
71
|
+
OR
|
|
72
|
+
|
|
73
|
+
CHANGES REQUIRED:
|
|
74
|
+
BLOCKING:
|
|
75
|
+
1. [file:line] [problem] → [fix]
|
|
76
|
+
|
|
77
|
+
NON-BLOCKING:
|
|
78
|
+
2. [description]
|
|
79
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-auditor
|
|
3
|
+
description: Security review for Next.js frontend. Run before any auth, token, or data-exposure PR.
|
|
4
|
+
model: claude-opus-4-8
|
|
5
|
+
tools: [Read, Bash]
|
|
6
|
+
color: red
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are a frontend security engineer. Review all changes for auth and XSS risks.
|
|
10
|
+
|
|
11
|
+
## Checklist
|
|
12
|
+
|
|
13
|
+
### Auth Token Handling
|
|
14
|
+
|
|
15
|
+
- [ ] Access token in React context only — NOT localStorage/sessionStorage/cookie
|
|
16
|
+
- [ ] Refresh token flow goes through Next.js API route (proxies to backend)
|
|
17
|
+
- [ ] No token logged in console
|
|
18
|
+
- [ ] Token cleared on logout from context
|
|
19
|
+
|
|
20
|
+
### Environment Variables
|
|
21
|
+
|
|
22
|
+
- [ ] No secrets in `NEXT_PUBLIC_*` vars (check .env.example)
|
|
23
|
+
- [ ] process.env only in src/lib/env.ts
|
|
24
|
+
- [ ] Zod schema validates all `NEXT_PUBLIC_*` vars
|
|
25
|
+
|
|
26
|
+
### XSS Prevention
|
|
27
|
+
|
|
28
|
+
- [ ] No dangerouslySetInnerHTML without DOMPurify sanitisation
|
|
29
|
+
- [ ] User-generated content rendered via JSX (auto-escaped)
|
|
30
|
+
|
|
31
|
+
### CSP Headers
|
|
32
|
+
|
|
33
|
+
- [ ] Content-Security-Policy header in next.config.ts
|
|
34
|
+
- [ ] No 'unsafe-eval' or 'unsafe-inline' in script-src
|
|
35
|
+
|
|
36
|
+
### API Client
|
|
37
|
+
|
|
38
|
+
- [ ] Auth header injected via middleware — not in individual hooks
|
|
39
|
+
- [ ] Error responses never expose raw backend error messages to UI
|
|
40
|
+
|
|
41
|
+
### Next.js API Routes
|
|
42
|
+
|
|
43
|
+
- [ ] app/api/ routes only used for cookie operations and auth proxy
|
|
44
|
+
- [ ] No business logic in api/ routes
|
|
45
|
+
|
|
46
|
+
## Verdict
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
APPROVED or CHANGES REQUIRED: [specific items with fixes]
|
|
50
|
+
```
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# .claude/hooks/post-write.sh
|
|
3
|
+
# Fires after every Write/Edit/MultiEdit on TypeScript/TSX files.
|
|
4
|
+
#
|
|
5
|
+
# Two severities:
|
|
6
|
+
# BLOCKERS (exit 2) — the agent MUST fix before continuing. These are correctness
|
|
7
|
+
# /security violations that should never land: editing the read-only generated
|
|
8
|
+
# contract, or putting an auth token in web storage.
|
|
9
|
+
# WARNINGS (exit 0) — printed for the agent to address before commit, but not fatal.
|
|
10
|
+
#
|
|
11
|
+
# Exit-code contract (Claude Code PostToolUse): a non-zero exit (2) with text on stderr
|
|
12
|
+
# is fed back to the agent as a required fix. Exit 0 with text on stdout is advisory.
|
|
13
|
+
#
|
|
14
|
+
# Parses the PostToolUse hook JSON from stdin with `node` (always present in a Node
|
|
15
|
+
# project) — no python3 dependency. Normalises the absolute path Claude Code sends
|
|
16
|
+
# (incl. Windows backslashes) to a repo-relative, forward-slash path so the layer
|
|
17
|
+
# checks below actually match.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
RAW=$(cat)
|
|
22
|
+
|
|
23
|
+
# Extract the written file path. Claude Code puts it at tool_input.file_path;
|
|
24
|
+
# fall back to other shapes defensively.
|
|
25
|
+
FILE=$(printf '%s' "$RAW" | node -e "
|
|
26
|
+
let d='';
|
|
27
|
+
process.stdin.on('data', c => d += c).on('end', () => {
|
|
28
|
+
try {
|
|
29
|
+
const j = JSON.parse(d);
|
|
30
|
+
const p = (j.tool_input && j.tool_input.file_path)
|
|
31
|
+
|| (j.tool_response && j.tool_response.filePath)
|
|
32
|
+
|| j.file_path || j.path || '';
|
|
33
|
+
process.stdout.write(p || '');
|
|
34
|
+
} catch { process.stdout.write(''); }
|
|
35
|
+
});
|
|
36
|
+
" 2>/dev/null || echo "")
|
|
37
|
+
|
|
38
|
+
[[ -z "$FILE" ]] && exit 0
|
|
39
|
+
[[ ! -f "$FILE" ]] && exit 0
|
|
40
|
+
[[ ! "$FILE" =~ \.(ts|tsx)$ ]] && exit 0
|
|
41
|
+
|
|
42
|
+
# Normalise backslashes → forward slashes for portable matching.
|
|
43
|
+
NORM="${FILE//\\//}"
|
|
44
|
+
|
|
45
|
+
WARNINGS=()
|
|
46
|
+
BLOCKERS=()
|
|
47
|
+
|
|
48
|
+
# ── BLOCKER 1: api.generated.ts is READ ONLY ───────────────────────────────
|
|
49
|
+
# Editing the generated contract by hand breaks the single source of truth.
|
|
50
|
+
if [[ "$NORM" =~ api\.generated\.ts$ ]]; then
|
|
51
|
+
BLOCKERS+=("🚫 [HOOK] $NORM is READ ONLY — it is regenerated by /api-sync. Revert this edit and run /api-sync against openapi.json instead.")
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# ── BLOCKER 2: auth tokens must never touch web storage ────────────────────
|
|
55
|
+
# Broadened from the old "token must be literally inside setItem(...)" grep so
|
|
56
|
+
# `localStorage.setItem('auth', accessToken)` (key does not contain "token") is
|
|
57
|
+
# caught too. Matches any setItem whose call references an auth-ish identifier.
|
|
58
|
+
if grep -qniE "(local|session)Storage\.setItem\([^)]*(token|jwt|auth|bearer|access|refresh|secret|credential)" "$FILE" 2>/dev/null; then
|
|
59
|
+
LINE=$(grep -niE "(local|session)Storage\.setItem" "$FILE" | head -1 | cut -d: -f1)
|
|
60
|
+
BLOCKERS+=("🚫 [HOOK] Token/credential written to web storage at $NORM:$LINE — access tokens must stay in memory (React context). Revert.")
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# ── WARNING 1: console.log in src/ or app/ (no structured console logging) ──
|
|
64
|
+
# Use the structured logger (src/lib/logger.ts) instead; raw console.log is noise.
|
|
65
|
+
if [[ "$NORM" =~ (^|/)src/ ]] || [[ "$NORM" =~ (^|/)app/ ]]; then
|
|
66
|
+
if grep -qn "console\.log\b" "$FILE" 2>/dev/null; then
|
|
67
|
+
LINE=$(grep -n "console\.log\b" "$FILE" | head -1 | cut -d: -f1)
|
|
68
|
+
WARNINGS+=("⚠ [HOOK] console.log at $NORM:$LINE — use the structured logger from src/lib/logger.ts (or remove before committing)")
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# ── WARNING 2: process.env outside src/lib/ (env.ts validates NEXT_PUBLIC_*) ──
|
|
73
|
+
# NODE_ENV / NEXT_RUNTIME are build-time constants Next inlines — not flagged.
|
|
74
|
+
# src/lib/ is the infra boundary (env.ts, logger.ts) and may read them.
|
|
75
|
+
if [[ "$NORM" =~ (^|/)src/ ]] && [[ ! "$NORM" =~ (^|/)src/lib/ ]]; then
|
|
76
|
+
if grep -nE "process\.env" "$FILE" 2>/dev/null | grep -vqE "process\.env\.(NODE_ENV|NEXT_RUNTIME)\b"; then
|
|
77
|
+
LINE=$(grep -nE "process\.env" "$FILE" | grep -vE "process\.env\.(NODE_ENV|NEXT_RUNTIME)\b" | head -1 | cut -d: -f1)
|
|
78
|
+
WARNINGS+=("⚠ [HOOK] process.env at $NORM:$LINE — use env from src/lib/env.ts")
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# ── WARNING 3: direct fetch() in components, features, or pages ─────────────
|
|
83
|
+
# Anchored so it does NOT false-positive on TanStack's refetch()/prefetchQuery()
|
|
84
|
+
# (fetch preceded by a letter or a dot is someone else's method, not a bare fetch).
|
|
85
|
+
if [[ "$NORM" =~ (^|/)(src/features|src/components|app)/ ]]; then
|
|
86
|
+
if grep -qnE "(^|[^A-Za-z.])fetch\(" "$FILE" 2>/dev/null; then
|
|
87
|
+
LINE=$(grep -nE "(^|[^A-Za-z.])fetch\(" "$FILE" | head -1 | cut -d: -f1)
|
|
88
|
+
WARNINGS+=("⚠ [HOOK] Direct fetch() at $NORM:$LINE — API calls must go through src/hooks/")
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# ── WARNING 4: raw <img> tag (should use next/image) ───────────────────────
|
|
93
|
+
if [[ "$NORM" =~ \.tsx$ ]]; then
|
|
94
|
+
if grep -qn "<img " "$FILE" 2>/dev/null; then
|
|
95
|
+
LINE=$(grep -n "<img " "$FILE" | head -1 | cut -d: -f1)
|
|
96
|
+
WARNINGS+=("⚠ [HOOK] <img> at $NORM:$LINE — use next/image <Image /> component")
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# ── WARNING 5: file size > 400 lines ───────────────────────────────────────
|
|
101
|
+
LINE_COUNT=$(wc -l < "$FILE" 2>/dev/null || echo 0)
|
|
102
|
+
if [[ "$LINE_COUNT" -gt 400 ]]; then
|
|
103
|
+
WARNINGS+=("⚠ [HOOK] $NORM has $LINE_COUNT lines — exceeds 400-line limit. Split into focused modules.")
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Print advisory warnings to stdout (guard empty array for bash 3.2 / set -u).
|
|
107
|
+
if [[ ${#WARNINGS[@]} -gt 0 ]]; then
|
|
108
|
+
for w in "${WARNINGS[@]}"; do
|
|
109
|
+
echo "$w"
|
|
110
|
+
done
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Blockers go to stderr and fail the hook so the agent must address them.
|
|
114
|
+
if [[ ${#BLOCKERS[@]} -gt 0 ]]; then
|
|
115
|
+
for b in "${BLOCKERS[@]}"; do
|
|
116
|
+
echo "$b" >&2
|
|
117
|
+
done
|
|
118
|
+
exit 2
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
exit 0
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/hooks/**/*.ts"
|
|
4
|
+
- "src/hooks/**/*.tsx"
|
|
5
|
+
- "src/lib/api-client.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# API Contract Rules — Auto-injected on hook and api-client edits
|
|
9
|
+
|
|
10
|
+
## The Golden Rule
|
|
11
|
+
`api.generated.ts` is the contract. Every API call must flow through it.
|
|
12
|
+
|
|
13
|
+
## Canonical Wire Contract (cross-stack — FastAPI · Express · NestJS)
|
|
14
|
+
|
|
15
|
+
This frontend talks to any of the harness backends. They MUST agree on ONE wire shape
|
|
16
|
+
so the client never special-cases a backend. The agreed conventions:
|
|
17
|
+
|
|
18
|
+
- **Field naming: `snake_case`** on the wire (`next_cursor_id`, `has_more`, `created_at`).
|
|
19
|
+
FastAPI/Pydantic emits this by default. Express/NestJS + Sequelize must emit it too
|
|
20
|
+
(Sequelize `underscored: true`, or a response serializer) — do NOT ship camelCase.
|
|
21
|
+
- **List envelope (cursor pagination):**
|
|
22
|
+
`{ "items": T[], "has_more": boolean, "next_cursor_id": string | null }`
|
|
23
|
+
Request params: `cursor_id` (opaque), `limit` (int).
|
|
24
|
+
- **Error envelope (every non-2xx):**
|
|
25
|
+
`{ "error": { "code": "SNAKE_CASE_CODE", "message": "human readable", "details"?: object } }`
|
|
26
|
+
(Not FastAPI's bare `{ "detail": ... }` — backends register a handler that emits this.)
|
|
27
|
+
|
|
28
|
+
**How this is enforced mechanically (not just prose):** the field names you write in
|
|
29
|
+
hooks are checked against `api.generated.ts` by `tsc`. If a backend deviates (e.g. emits
|
|
30
|
+
`nextCursorId`), `/api-sync` regenerates camelCase types and `npm run typecheck` FAILS on
|
|
31
|
+
`lastPage.next_cursor_id` — the drift cannot pass the gate silently. Agreement across
|
|
32
|
+
templates + typecheck is the guard; keep the convention above in lock-step with the
|
|
33
|
+
Express/NestJS/FastAPI templates.
|
|
34
|
+
|
|
35
|
+
## Hook Pattern — Every Hook Must Follow This
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// src/hooks/use-applications.ts
|
|
39
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
40
|
+
import { apiClient } from '@/lib/api-client'
|
|
41
|
+
import { toApiError } from '@/lib/api-error'
|
|
42
|
+
import type { components } from '@/types/api.generated'
|
|
43
|
+
|
|
44
|
+
type Application = components['schemas']['ApplicationResponse']
|
|
45
|
+
type CreateApplicationInput = components['schemas']['CreateApplicationInput']
|
|
46
|
+
|
|
47
|
+
// ─── Query Keys ─────────────────────────────────────────
|
|
48
|
+
// Centralise all query keys — never use raw strings in components
|
|
49
|
+
export const applicationKeys = {
|
|
50
|
+
all: () => ['applications'] as const,
|
|
51
|
+
lists: () => [...applicationKeys.all(), 'list'] as const,
|
|
52
|
+
list: (filters: Record<string, unknown>) =>
|
|
53
|
+
[...applicationKeys.lists(), filters] as const,
|
|
54
|
+
detail: (id: string) => [...applicationKeys.all(), 'detail', id] as const,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── List ────────────────────────────────────────────────
|
|
58
|
+
export function useApplications(filters?: { stage?: string; limit?: number }) {
|
|
59
|
+
return useQuery({
|
|
60
|
+
queryKey: applicationKeys.list(filters ?? {}),
|
|
61
|
+
queryFn: async () => {
|
|
62
|
+
const { data, error } = await apiClient.GET('/api/v1/applications', {
|
|
63
|
+
params: { query: filters },
|
|
64
|
+
})
|
|
65
|
+
if (error) throw toApiError(error, 'Failed to fetch applications')
|
|
66
|
+
return data
|
|
67
|
+
},
|
|
68
|
+
staleTime: 60_000, // 1 min — server data doesn't change every second
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Single ──────────────────────────────────────────────
|
|
73
|
+
export function useApplication(id: string) {
|
|
74
|
+
return useQuery({
|
|
75
|
+
queryKey: applicationKeys.detail(id),
|
|
76
|
+
queryFn: async () => {
|
|
77
|
+
const { data, error } = await apiClient.GET('/api/v1/applications/{id}', {
|
|
78
|
+
params: { path: { id } },
|
|
79
|
+
})
|
|
80
|
+
if (error) throw toApiError(error, `Application ${id} not found`)
|
|
81
|
+
return data
|
|
82
|
+
},
|
|
83
|
+
enabled: !!id, // don't fetch if no id
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Mutation ────────────────────────────────────────────
|
|
88
|
+
export function useCreateApplication() {
|
|
89
|
+
const queryClient = useQueryClient()
|
|
90
|
+
return useMutation({
|
|
91
|
+
mutationFn: async (body: CreateApplicationInput) => {
|
|
92
|
+
const { data, error } = await apiClient.POST('/api/v1/applications', { body })
|
|
93
|
+
if (error) throw toApiError(error, 'Failed to create application')
|
|
94
|
+
return data
|
|
95
|
+
},
|
|
96
|
+
onSuccess: () => {
|
|
97
|
+
// Invalidate all application lists — triggers refetch
|
|
98
|
+
queryClient.invalidateQueries({ queryKey: applicationKeys.lists() })
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Error Handling in Hooks
|
|
105
|
+
|
|
106
|
+
Preserve the backend error — do NOT replace it with a hard-coded English string (that
|
|
107
|
+
throws away the `code`/`message` the canonical error envelope carries). Use a shared
|
|
108
|
+
helper that reads the envelope defensively:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// src/lib/api-error.ts (authored in Phase 0 glue)
|
|
112
|
+
export class ApiError extends Error {
|
|
113
|
+
constructor(public code: string, message: string, public details?: unknown) {
|
|
114
|
+
super(message)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Reads the canonical { error: { code, message } } envelope; falls back gracefully
|
|
118
|
+
// for a bare FastAPI { detail } or an opaque error so a message ALWAYS surfaces.
|
|
119
|
+
export function toApiError(error: unknown, fallback = 'Request failed'): ApiError {
|
|
120
|
+
const e = error as { error?: { code?: string; message?: string; details?: unknown }; detail?: unknown }
|
|
121
|
+
if (e?.error?.message) return new ApiError(e.error.code ?? 'unknown', e.error.message, e.error.details)
|
|
122
|
+
if (typeof e?.detail === 'string') return new ApiError('unknown', e.detail)
|
|
123
|
+
return new ApiError('unknown', fallback)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// ✅ Throw the real error — code + message reach the UI
|
|
129
|
+
if (error) throw toApiError(error, 'Failed to fetch applications')
|
|
130
|
+
|
|
131
|
+
// ✅ Handle in component via isError + error (error.message is the backend message)
|
|
132
|
+
const { isError, error } = useApplications()
|
|
133
|
+
if (isError) return <ErrorState message={error.message} />
|
|
134
|
+
|
|
135
|
+
// ❌ Never swallow errors
|
|
136
|
+
const { data } = await apiClient.GET(...)
|
|
137
|
+
// missing: if (error) throw ...
|
|
138
|
+
|
|
139
|
+
// ❌ Never discard the backend error for a hard-coded string
|
|
140
|
+
if (error) throw new Error('Failed to fetch applications') // loses code + real message
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## staleTime Guidance
|
|
144
|
+
```typescript
|
|
145
|
+
// User-specific, changes often: staleTime: 30_000 (30s)
|
|
146
|
+
// Reference data, rarely changes: staleTime: 300_000 (5min)
|
|
147
|
+
// Static config: staleTime: Infinity
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Pagination with Cursor
|
|
151
|
+
```typescript
|
|
152
|
+
export function useApplications(cursor?: string) {
|
|
153
|
+
return useInfiniteQuery({
|
|
154
|
+
queryKey: applicationKeys.list({ cursor }),
|
|
155
|
+
queryFn: async ({ pageParam }) => {
|
|
156
|
+
const { data, error } = await apiClient.GET('/api/v1/applications', {
|
|
157
|
+
params: { query: { cursor_id: pageParam, limit: 20 } },
|
|
158
|
+
})
|
|
159
|
+
if (error) throw error
|
|
160
|
+
return data
|
|
161
|
+
},
|
|
162
|
+
getNextPageParam: (lastPage) => lastPage.next_cursor_id ?? undefined,
|
|
163
|
+
initialPageParam: undefined,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.ts"
|
|
4
|
+
- "src/**/*.tsx"
|
|
5
|
+
- "app/**/*.tsx"
|
|
6
|
+
- "app/**/*.ts"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Architecture Rules — Auto-injected on every src/ and app/ file edit
|
|
10
|
+
|
|
11
|
+
## Import Order Check — Before Writing Any Import
|
|
12
|
+
|
|
13
|
+
| Your file is in | You may import from |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `src/types/` | Nothing (third-party ok: zod) |
|
|
16
|
+
| `src/lib/` | `src/types/` only |
|
|
17
|
+
| `src/hooks/` | `src/types/`, `src/lib/` |
|
|
18
|
+
| `src/store/` | `src/types/` only |
|
|
19
|
+
| `src/utils/` | Nothing (stdlib + third-party only) |
|
|
20
|
+
| `src/components/` | `src/types/`, `src/utils/` |
|
|
21
|
+
| `src/features/` | `src/types/`, `src/lib/`, `src/hooks/`, `src/store/`, `src/components/`, `src/utils/` |
|
|
22
|
+
| `app/` | `src/features/`, `src/components/` |
|
|
23
|
+
|
|
24
|
+
## api.generated.ts — THE CONTRACT
|
|
25
|
+
```typescript
|
|
26
|
+
// ✅ Import types FROM the generated file
|
|
27
|
+
import type { components, paths } from '@/types/api.generated'
|
|
28
|
+
type Application = components['schemas']['ApplicationResponse']
|
|
29
|
+
|
|
30
|
+
// ❌ NEVER manually write a type that mirrors the generated one
|
|
31
|
+
type Application = { // if this matches api.generated.ts → DELETE IT
|
|
32
|
+
id: string
|
|
33
|
+
stage: string
|
|
34
|
+
...
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ❌ NEVER edit api.generated.ts
|
|
38
|
+
// Run /api-sync to regenerate it
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API Calls — Hooks Only
|
|
42
|
+
```typescript
|
|
43
|
+
// ✅ Feature uses a hook
|
|
44
|
+
import { useApplications } from '@/hooks/use-applications'
|
|
45
|
+
|
|
46
|
+
function ApplicationList() {
|
|
47
|
+
const { data, isPending, error } = useApplications()
|
|
48
|
+
if (isPending) return <Skeleton />
|
|
49
|
+
if (error) return <ErrorState />
|
|
50
|
+
return data?.items.map(app => <ApplicationCard key={app.id} app={app} />)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ❌ Direct fetch in component — FORBIDDEN
|
|
54
|
+
function ApplicationList() {
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
fetch('/api/v1/applications') // WRONG
|
|
57
|
+
}, [])
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Server vs Client Components
|
|
62
|
+
```typescript
|
|
63
|
+
// ✅ Server Component — default, no directive
|
|
64
|
+
export default async function ApplicationsPage() {
|
|
65
|
+
return (
|
|
66
|
+
<div>
|
|
67
|
+
<ApplicationListClient /> {/* client boundary inside */}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ✅ Client Component — with reason comment
|
|
73
|
+
'use client' // Client: uses TanStack Query (useQuery)
|
|
74
|
+
export function ApplicationListClient() {
|
|
75
|
+
const { data } = useApplications()
|
|
76
|
+
...
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ❌ 'use client' without comment
|
|
80
|
+
'use client'
|
|
81
|
+
export function SomeComponent() { ... } // Why? What browser API?
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## No Business Logic in App Pages
|
|
85
|
+
```typescript
|
|
86
|
+
// ✅ Page = layout + render feature
|
|
87
|
+
export default function ApplicationsPage() {
|
|
88
|
+
return <ApplicationsFeature />
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ❌ Business logic in page — belongs in features/
|
|
92
|
+
export default function ApplicationsPage() {
|
|
93
|
+
const [filtered, setFiltered] = useState([])
|
|
94
|
+
const handleStageChange = async (id, stage) => { // WRONG LAYER
|
|
95
|
+
await fetch(`/api/v1/applications/${id}/stage`)
|
|
96
|
+
...
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/features/**/*.tsx"
|
|
4
|
+
- "src/components/**/*.tsx"
|
|
5
|
+
- "app/**/*.tsx"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Component Rules — Auto-injected on component file edits
|
|
9
|
+
|
|
10
|
+
## Every Component Checklist
|
|
11
|
+
|
|
12
|
+
### Loading, Error, and Empty States (ALL THREE required on data-fetching components)
|
|
13
|
+
```typescript
|
|
14
|
+
// ✅ All 3 states handled
|
|
15
|
+
export function ApplicationList() {
|
|
16
|
+
const { data, isPending, isError, error } = useApplications()
|
|
17
|
+
|
|
18
|
+
if (isPending) return <ApplicationListSkeleton /> // skeleton > spinner
|
|
19
|
+
if (isError) return <ErrorState message={error.message} retry={refetch} />
|
|
20
|
+
if (!data?.items.length) return <EmptyState />
|
|
21
|
+
|
|
22
|
+
return data.items.map(app => <ApplicationCard key={app.id} app={app} />)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ❌ Missing states — silent failure
|
|
26
|
+
export function ApplicationList() {
|
|
27
|
+
const { data } = useApplications()
|
|
28
|
+
return data?.items.map(app => <ApplicationCard key={app.id} app={app} />)
|
|
29
|
+
// undefined if loading? Error? No items? All render nothing silently.
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Forms — React Hook Form + Zod + shadcn/ui
|
|
34
|
+
```typescript
|
|
35
|
+
import { useForm } from 'react-hook-form'
|
|
36
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
37
|
+
import { z } from 'zod'
|
|
38
|
+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
39
|
+
|
|
40
|
+
const createApplicationSchema = z.object({
|
|
41
|
+
company: z.string().min(1, 'Company is required'),
|
|
42
|
+
role: z.string().min(1, 'Role is required'),
|
|
43
|
+
jobUrl: z.string().url().optional(),
|
|
44
|
+
})
|
|
45
|
+
type CreateApplicationInput = z.infer<typeof createApplicationSchema>
|
|
46
|
+
|
|
47
|
+
export function CreateApplicationForm({ onSuccess }: { onSuccess: () => void }) {
|
|
48
|
+
const form = useForm<CreateApplicationInput>({
|
|
49
|
+
resolver: zodResolver(createApplicationSchema),
|
|
50
|
+
defaultValues: { company: '', role: '' },
|
|
51
|
+
})
|
|
52
|
+
const { mutate, isPending } = useCreateApplication()
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Form {...form}>
|
|
56
|
+
<form onSubmit={form.handleSubmit(data => mutate(data, { onSuccess }))}>
|
|
57
|
+
<FormField control={form.control} name="company" render={({ field }) => (
|
|
58
|
+
<FormItem>
|
|
59
|
+
<FormLabel>Company</FormLabel>
|
|
60
|
+
<FormControl><Input {...field} /></FormControl>
|
|
61
|
+
<FormMessage />
|
|
62
|
+
</FormItem>
|
|
63
|
+
)} />
|
|
64
|
+
<Button type="submit" disabled={isPending}>
|
|
65
|
+
{isPending ? 'Creating...' : 'Create'}
|
|
66
|
+
</Button>
|
|
67
|
+
</form>
|
|
68
|
+
</Form>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Images — Always next/image
|
|
74
|
+
```typescript
|
|
75
|
+
// ✅
|
|
76
|
+
import Image from 'next/image'
|
|
77
|
+
<Image src="/logo.png" alt="Logo" width={120} height={40} />
|
|
78
|
+
|
|
79
|
+
// ❌ Flagged by post-write hook + ESLint (next/core-web-vitals no-img-element)
|
|
80
|
+
<img src="/logo.png" alt="Logo" />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Accessibility (every interactive element)
|
|
84
|
+
```typescript
|
|
85
|
+
// ✅
|
|
86
|
+
<button
|
|
87
|
+
aria-label="Delete application"
|
|
88
|
+
onClick={handleDelete}
|
|
89
|
+
className="focus-visible:ring-2 focus-visible:ring-ring"
|
|
90
|
+
>
|
|
91
|
+
<TrashIcon aria-hidden />
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
// ❌
|
|
95
|
+
<div onClick={handleDelete}> {/* not focusable, no role */}
|
|
96
|
+
<TrashIcon />
|
|
97
|
+
</div>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Tailwind + shadcn/ui — Use cn() for conditional classes
|
|
101
|
+
```typescript
|
|
102
|
+
import { cn } from '@/utils/cn.util'
|
|
103
|
+
|
|
104
|
+
<div className={cn(
|
|
105
|
+
'rounded-lg border p-4',
|
|
106
|
+
isPending && 'opacity-50 pointer-events-none',
|
|
107
|
+
isError && 'border-destructive',
|
|
108
|
+
)}>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### No Inline Event Handlers with Business Logic
|
|
112
|
+
```typescript
|
|
113
|
+
// ✅ Handler defined, logic delegated to mutation
|
|
114
|
+
const { mutate: deleteApp } = useDeleteApplication()
|
|
115
|
+
<Button onClick={() => deleteApp(app.id)}>Delete</Button>
|
|
116
|
+
|
|
117
|
+
// ❌ Inline logic
|
|
118
|
+
<Button onClick={async () => {
|
|
119
|
+
await fetch(`/api/v1/applications/${app.id}`, { method: 'DELETE' })
|
|
120
|
+
router.refresh()
|
|
121
|
+
}}>Delete</Button>
|
|
122
|
+
```
|