@polymorphism-tech/morph-spec 9.4.0 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +1 -0
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/MORPH.md +23 -33
  8. package/framework/agents.json +6 -7
  9. package/framework/docs/CLI.md +12 -8
  10. package/framework/hooks/README.md +2 -2
  11. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  12. package/framework/hooks/claude-code/core/comment-guard.js +1 -0
  13. package/framework/hooks/claude-code/core/denylist.json +63 -1
  14. package/framework/hooks/claude-code/core/enforce-phase-writes.js +0 -8
  15. package/framework/hooks/claude-code/core/gate-guard.js +0 -8
  16. package/framework/hooks/claude-code/core/heavy-gate.js +5 -3
  17. package/framework/hooks/claude-code/core/loop-detect.js +0 -3
  18. package/framework/hooks/claude-code/core/pre-bash-denylist.js +2 -1
  19. package/framework/hooks/claude-code/core/protect-spec-files.js +0 -7
  20. package/framework/hooks/claude-code/core/validate-completion.js +0 -4
  21. package/framework/hooks/claude-code/core/worktree-context.js +0 -2
  22. package/framework/hooks/claude-code/statusline.py +3 -44
  23. package/framework/hooks/shared/build-check.js +12 -19
  24. package/framework/hooks/shared/comment-rules.js +11 -1
  25. package/framework/hooks/shared/feature-locator.js +7 -5
  26. package/framework/hooks/shared/heavy-command.js +12 -0
  27. package/framework/hooks/shared/heavy-config.js +28 -1
  28. package/framework/hooks/shared/heavy-slots.js +41 -5
  29. package/framework/hooks/shared/hook-response.js +0 -4
  30. package/framework/hooks/shared/payload-utils.js +1 -35
  31. package/framework/hooks/shared/phase-utils.js +0 -12
  32. package/framework/hooks/shared/slice-progress.js +0 -2
  33. package/framework/hooks/shared/state-reader.js +0 -19
  34. package/framework/hooks/shared/telemetry-logger.js +1 -1
  35. package/framework/rules/frontend-standards.md +1 -2
  36. package/framework/rules/nextjs-standards.md +9 -8
  37. package/framework/schemas/config.schema.json +12 -1
  38. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  39. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  40. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  41. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  42. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  43. package/framework/skills/core/morph-media/SKILL.md +1 -1
  44. package/framework/skills/core/morph-plan/SKILL.md +2 -2
  45. package/framework/skills/core/morph-review/SKILL.md +1 -1
  46. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  47. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  48. package/framework/standards/STANDARDS.json +32 -29
  49. package/framework/standards/STANDARDS.meta.json +13 -15
  50. package/framework/standards/architecture/code-comments.md +1 -1
  51. package/framework/standards/frontend/admin-app/data-table.md +1 -3
  52. package/framework/standards/frontend/admin-app/page-structure.md +8 -4
  53. package/framework/standards/frontend/nextjs/app-router.md +3 -4
  54. package/framework/standards/frontend/nextjs/components.md +2 -15
  55. package/framework/standards/frontend/nextjs/data-fetching.md +2 -3
  56. package/framework/standards/frontend/nextjs/lint.md +141 -0
  57. package/framework/standards/frontend/nextjs/motion-patterns.md +2 -7
  58. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -2
  59. package/framework/standards/frontend/nextjs/nextjs-patterns.md +5 -7
  60. package/framework/standards/frontend/nextjs/project-structure.md +3 -2
  61. package/framework/standards/frontend/nextjs/state-management.md +6 -7
  62. package/framework/standards/infrastructure/docker/local-compose-isolation.md +1 -0
  63. package/framework/templates/README.md +4 -4
  64. package/framework/templates/REGISTRY.json +21 -309
  65. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +54 -56
  66. package/package.json +3 -2
  67. package/src/commands/e2e/e2e.js +3 -0
  68. package/src/commands/project/env.js +3 -3
  69. package/src/commands/project/heavy.js +10 -3
  70. package/src/commands/project/worktree.js +2 -1
  71. package/src/commands/state/finish.js +17 -3
  72. package/src/commands/validation/mutate.js +2 -2
  73. package/src/commands/validation/verify.js +3 -0
  74. package/src/core/paths/output-schema.js +0 -9
  75. package/src/core/state/feature-identity.js +0 -2
  76. package/src/core/state/state-manager.js +1 -169
  77. package/src/core/templates/template-renderer.js +0 -23
  78. package/src/dashboard/app/lib/format.js +0 -7
  79. package/src/dashboard/app/lib/phases.js +0 -7
  80. package/src/dashboard/scanner/activity-scanner.js +0 -4
  81. package/src/lib/ai/ai-project-scan.js +1 -6
  82. package/src/lib/e2e/compose-env.js +26 -3
  83. package/src/lib/graph/graphify-runtime.js +0 -1
  84. package/src/lib/heavy/gate.js +23 -4
  85. package/src/lib/heavy/live-stacks.js +64 -0
  86. package/src/lib/mutation/snapshot.js +1 -5
  87. package/src/lib/risk-detector.js +9 -6
  88. package/src/lib/stack/stack-profile.js +0 -4
  89. package/src/lib/stack/stack-scan.js +15 -4
  90. package/src/lib/tasks/eval-node.js +1 -7
  91. package/src/lib/validators/validation-runner.js +0 -43
  92. package/src/lib/validators/verify-runner.js +18 -2
  93. package/src/lib/worktree/docker-audit.js +7 -3
  94. package/src/lib/worktree/env-allocator.js +27 -5
  95. package/src/lib/worktree/ignored-triage.js +8 -0
  96. package/src/utils/agents-installer.js +12 -19
  97. package/src/utils/claude-settings-manager.js +0 -28
  98. package/src/utils/file-copier.js +0 -53
  99. package/framework/hooks/shared/activity-logger.js +0 -131
  100. package/framework/standards/frontend/design-system/naming.md +0 -68
  101. package/src/commands/validation/index.js +0 -2
  102. package/src/lib/validators/code-budget/index.js +0 -2
  103. package/src/lib/validators/contracts/index.js +0 -1
  104. package/src/lib/validators/nextjs/index.js +0 -1
  105. package/src/lib/validators/packages/index.js +0 -1
  106. package/src/lib/validators/reuse/index.js +0 -2
@@ -9,16 +9,16 @@
9
9
 
10
10
  ---
11
11
 
12
- Server Components for initial page load. TanStack Query for client mutations and interactive data. Never `useEffect` for fetching.
12
+ Server Components for initial page load. TanStack Query for client mutations and interactive data.
13
13
 
14
14
  ## Core Rules
15
15
 
16
16
  - ALWAYS use Server Components for initial data that doesn't require interactivity
17
17
  - ALWAYS use TanStack Query (`useQuery`, `useMutation`) for client-side data needs
18
- - NEVER use `useEffect(() => { fetch(...) }, [])` — this is the old pattern
19
18
  - ALWAYS validate API responses with Zod before using them
20
19
  - ALWAYS use query key factories for consistent cache invalidation
21
20
  - NEVER fetch a whole collection to page, sort or filter it in the browser — a list is paged on the server, with the query in the URL (`admin-app/data-table.md`)
21
+ - Fetch inside `useEffect` and a DB driver in Next are lint errors (`frontend-nextjs-lint`)
22
22
 
23
23
  ## TanStack Query Setup
24
24
 
@@ -134,7 +134,6 @@ export function UserListClient({ initialData }: { initialData: User[] }) {
134
134
 
135
135
  | Wrong | Right | Why |
136
136
  |-------|-------|-----|
137
- | `useEffect(() => { fetch(...) }, [])` | `useQuery(...)` | Two renders, no cache, no SSR |
138
137
  | No Zod on API response | `schema.parse(await res.json())` | Type safety at runtime |
139
138
  | Hardcoded query keys: `['users']` | Key factory: `userKeys.lists()` | Consistent invalidation |
140
139
  | `queryClient.invalidateQueries(['users'])` | `{ queryKey: userKeys.lists() }` | Object syntax required in v5 |
@@ -0,0 +1,141 @@
1
+ # ESLint Next.js — o lint cobra, a prosa não repete
2
+
3
+ > **Scope:** nextjs
4
+ > **Layer:** 0 (resumo injetado pela rule `nextjs-standards` (`**/*.ts`, `**/*.tsx`); o `stack-scan` acusa o projeto sem este arquivo)
5
+ > **Keywords:** eslint, lint, eslint.config.mjs, no-restricted-imports, no-restricted-paths, no-restricted-syntax, no-default-export, no-explicit-any, framer-motion, zustand
6
+ > **Load When:** projeto Next.js sem `eslint.config.mjs` com `no-restricted-imports`, ou o lint acusou uma destas regras
7
+
8
+ **Verified against:** eslint 9.39.5, eslint-config-next 16.4.2 (typescript-eslint 8.71.2, eslint-plugin-import 2.32.0), next 16.4.2, typescript 6.0.3 (sonda: um arquivo ruim por regra → erro da regra esperada, `no-console` → aviso, arquivos conformes e o scaffold `morph-spec scaffold nextjs` → 0 problemas). Last-verified: 2026-10-18.
9
+
10
+ ---
11
+
12
+ Regra de código que tem motivo e que uma máquina enxerga vira **erro de lint**, não frase que o
13
+ agente relê a cada sessão. Um arquivo na raiz do app Next basta, e só usa o que o
14
+ `eslint-config-next` já instala (typescript-eslint e eslint-plugin-import): nenhum plugin novo.
15
+
16
+ ## Arquivos
17
+
18
+ Dependências (`npm i -D eslint@9 eslint-config-next typescript`). ESLint 9 é a faixa que
19
+ `eslint-plugin-import` e `eslint-plugin-react` declaram como peer; o typescript-eslint do
20
+ `eslint-config-next` pede TypeScript `<6.1`.
21
+
22
+ No Next 16 o `next build` não roda mais o lint. O nó de build do `morph-spec verify` roda o
23
+ eslint (`--quiet`: só erro reprova) **só nos arquivos que a feature mudou**, na raiz do app, quando
24
+ acha `eslint.config.*` e o pacote `eslint` instalado: dívida de lint antiga nunca deixa a feature
25
+ vermelha. As zonas são relativas a essa raiz.
26
+
27
+ `eslint.config.mjs`:
28
+
29
+ ```js
30
+ import { defineConfig, globalIgnores } from 'eslint/config'
31
+ import nextVitals from 'eslint-config-next/core-web-vitals'
32
+ import nextTs from 'eslint-config-next/typescript'
33
+
34
+ const BANNED = [
35
+ [['framer-motion'], 'Motion entra por motion/react: um único caminho de import.'],
36
+ [['zustand', 'redux', '@reduxjs/toolkit'], 'Estado de servidor é do TanStack Query; página e filtro vão na URL.'],
37
+ [['pg', '@neondatabase/serverless', '@prisma/client', 'drizzle-orm', 'postgres'], 'O Next nunca fala com o banco: o dado vem da API .NET.'],
38
+ ]
39
+
40
+ const DEEP_FEATURE = {
41
+ group: ['@/features/*/*'],
42
+ message: 'Fora da feature, importe a API pública (@/features/x), nunca um caminho interno.',
43
+ }
44
+
45
+ const restrictedImports = (...extraPatterns) => ['error', {
46
+ paths: [
47
+ ...BANNED.flatMap(([names, message]) => names.map((name) => ({ name, message }))),
48
+ { name: '@/components/ui/form', message: 'Form legado do shadcn: Controller + Field (frontend-nextjs-forms).' },
49
+ ],
50
+ patterns: [
51
+ ...BANNED.map(([names, message]) => ({ group: names.map((n) => `${n}/*`), message })),
52
+ ...extraPatterns,
53
+ ],
54
+ }]
55
+
56
+ const FETCH_IN_EFFECT = {
57
+ selector:
58
+ "CallExpression[callee.name='useEffect'] CallExpression:matches([callee.name='fetch'], [callee.name='apiClient'], [callee.object.name='apiClient'])",
59
+ message: 'Dado do servidor vem de Server Component ou TanStack Query, nunca de fetch em useEffect.',
60
+ }
61
+
62
+ export default defineConfig([
63
+ ...nextVitals,
64
+ ...nextTs,
65
+ globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
66
+ {
67
+ rules: {
68
+ 'no-restricted-imports': restrictedImports(DEEP_FEATURE),
69
+ 'import/no-restricted-paths': ['error', {
70
+ zones: [
71
+ { target: './src/components', from: './src/features', message: 'components/ é apresentação: não conhece feature.' },
72
+ { target: './src/features', from: './src/app', message: 'app/ importa de features/, nunca o contrário.' },
73
+ { target: './src/hooks', from: ['./src/lib/api-client.ts', './src/lib/api'], message: 'Hook compartilhado não chama API: isso é hook de feature.' },
74
+ ],
75
+ }],
76
+ 'no-restricted-syntax': ['error', FETCH_IN_EFFECT],
77
+ 'import/no-default-export': 'error',
78
+ '@typescript-eslint/no-explicit-any': 'error',
79
+ 'no-console': 'warn',
80
+ },
81
+ },
82
+ {
83
+ files: ['src/features/**'],
84
+ rules: { 'no-restricted-imports': restrictedImports() },
85
+ },
86
+ {
87
+ files: ['app/layout.tsx', 'src/app/layout.tsx'],
88
+ rules: {
89
+ 'no-restricted-syntax': ['error', FETCH_IN_EFFECT, {
90
+ selector: "Program > ExpressionStatement[directive='use client']",
91
+ message: "O layout raiz é Server Component (metadata): o 'use client' vai no provider que ele renderiza.",
92
+ }],
93
+ },
94
+ },
95
+ {
96
+ files: [
97
+ '**/app/**/{page,layout,loading,error,global-error,not-found,forbidden,unauthorized,template,default,route}.{ts,tsx}',
98
+ '**/app/**/{sitemap,robots,manifest,icon,apple-icon,opengraph-image,twitter-image}.{ts,tsx}',
99
+ '**/*.config.{js,mjs,cjs,ts}',
100
+ ],
101
+ rules: { 'import/no-default-export': 'off' },
102
+ },
103
+ ])
104
+ ```
105
+
106
+ Cada override de `no-restricted-imports` e `no-restricted-syntax` substitui a lista inteira da
107
+ regra naquele arquivo, por isso o bloco do layout repete `FETCH_IN_EFFECT` e o de `features/`
108
+ repete os pacotes banidos.
109
+
110
+ ## O que cada regra compra
111
+
112
+ | Regra | Bloqueia | Por quê |
113
+ |---|---|---|
114
+ | `no-restricted-imports` `@/features/*/*` (fora de `src/features/`) | `app/`, `components/` e `lib/` lendo arquivo interno de uma feature | o `index.ts` é a API pública: refatorar dentro da feature não quebra quem está fora |
115
+ | `no-restricted-imports` `framer-motion` | o segundo caminho de import do Motion | `motion/react` e `framer-motion` juntos duplicam o runtime no bundle e dividem o código em duas APIs |
116
+ | `no-restricted-imports` `zustand`, `redux`, `@reduxjs/toolkit` | store global | cópia do estado do servidor fica velha sem o cache do TanStack Query; página e filtro na URL sobrevivem a refresh e link |
117
+ | `no-restricted-imports` `pg`, `@neondatabase/serverless`, `@prisma/client`, `drizzle-orm`, `postgres` | driver de banco no Next | regra de negócio e acesso a dados moram no .NET (EF Core); um segundo caminho ao banco pula a autorização dele |
118
+ | `no-restricted-imports` `@/components/ui/form` | o `Form` legado do shadcn | a documentação do shadcn é escrita em `Controller` + `Field`; a tela legada migra quando é tocada |
119
+ | `import/no-restricted-paths` `src/components` ↛ `src/features` | componente compartilhado que conhece domínio | ele deixa de ser compartilhável e cria ciclo feature → components → feature |
120
+ | `import/no-restricted-paths` `src/features` ↛ `src/app` | feature importando rota | a dependência é de mão única: a rota compõe a feature |
121
+ | `import/no-restricted-paths` `src/hooks` ↛ cliente da API | hook compartilhado chamando o backend | chamada à API é de feature, com a key factory dela; hook compartilhado é só de UI |
122
+ | `no-restricted-syntax` fetch/`apiClient` dentro de `useEffect` | busca de dado no efeito | sem cache, sem dedup, corrida entre requisições e cascata de render; Server Component ou TanStack Query resolvem |
123
+ | `no-restricted-syntax` `'use client'` em `app/layout.tsx` | layout raiz no cliente | perde `metadata` e manda a árvore inteira para o bundle do cliente |
124
+ | `import/no-default-export` (exceto arquivos de rota e `*.config.*`) | `export default` em componente e hook | nome único em todo import: busca e rename acham todos os usos; o Next exige `default` só nos arquivos de convenção |
125
+ | `@typescript-eslint/no-explicit-any` | `any` | desliga o tipo no ponto onde o dado da API entra; o certo é schema Zod + `z.infer` |
126
+ | `no-console` (aviso) | `console.log` esquecido | ruído no console do usuário e dado vazando; aviso, porque o diagnóstico local é legítimo |
127
+
128
+ Restrição nova entra como linha neste arquivo, com o porquê na `message`. Nunca uma regra nova
129
+ em prosa. Exceção pontual é `// eslint-disable-next-line <regra> -- <motivo>` na linha, nunca o
130
+ desligamento da regra no projeto.
131
+
132
+ ## O que não é regra
133
+
134
+ Nome de classe CSS (BEM, prefixo de página, namespace `--app-`, sufixo de estado), catálogo de
135
+ classes `.animate-*`/`.stagger-*`, `className` opcional em toda prop, Zod em props, quantos
136
+ momentos de motion uma página tem, o deslocamento de uma transição, o skeleton com
137
+ `animate-pulse`, os defaults de `staleTime`/`retry` do `QueryClient` e quantos níveis de prop
138
+ drilling são aceitáveis são gosto: nem prosa nem lint. Siga o arquivo vizinho, e o formatter do
139
+ projeto decide a forma. Também saíram da prosa o `'use client'` no `error.tsx` (o `next build`
140
+ recusa sem ele) e o prefixo `use` do hook (o `react-hooks/rules-of-hooks` que vem no
141
+ `eslint-config-next` acusa hook chamado fora de função `useX`).
@@ -221,14 +221,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
221
221
 
222
222
  - `mode="wait"` ensures the exit animation completes before the entering page renders.
223
223
  - The `key` prop must change on navigation — use `usePathname()` from `next/navigation`.
224
- - Keep page transitions subtle (`y: 8`, fast duration) — aggressive transitions hurt perceived performance.
225
224
 
226
225
  ---
227
226
 
228
227
  ## Section 6: Loading Patterns
229
228
 
230
- Prefer CSS `animate-pulse` for skeleton loaders over Motion — CSS animations run off the main thread and have lower performance overhead.
231
-
232
229
  ```tsx
233
230
  // components/skeleton.tsx
234
231
  import { cn } from '@/lib/utils';
@@ -315,11 +312,8 @@ This CSS fallback catches animations outside Motion (Tailwind `animate-*`, third
315
312
  | `duration: 0.8` for UI interactions | `duration: durations.fast` (0.15) for micro-interactions |
316
313
  | Hardcoded easing `[0.4, 0, 0.2, 1]` | `ease: easings.standard` (from design-tokens) |
317
314
  | No `useReducedMotion()` check | Always check or use CSS media query |
318
- | Animating everything on the page | Reserve motion for 3-5 meaningful moments per page |
319
315
  | Forgetting `animation-fill-mode: backwards` in stagger | Will cause a flash before the animation starts |
320
316
  | `AnimatePresence` without `key` changes | Exit animations will never fire |
321
- | Using `motion` for skeleton loaders | Use CSS `animate-pulse` — lower performance cost |
322
- | `npm install framer-motion` / `import ... from 'framer-motion'` | `npm install motion` / `import ... from 'motion/react'` — same code underneath, but one import path per codebase |
323
317
  | Need ScrollTrigger, SVG morph, or complex timelines | Use GSAP — see `frontend/gsap/gsap-core.md` |
324
318
 
325
319
  ---
@@ -333,7 +327,8 @@ npm install motion
333
327
  > The package is `motion` and components import from `motion/react`. `framer-motion`
334
328
  > is not deprecated: it is published with the same version and `motion` depends on
335
329
  > it. New code still depends on `motion` and imports from `motion/react`, so the
336
- > codebase has a single import path; do not add `framer-motion` alongside it.
330
+ > codebase has a single import path; a `framer-motion` import is a lint error
331
+ > (`frontend-nextjs-lint`).
337
332
 
338
333
  Verify the installed version supports the features used here (requires motion >= 11; current major on npm is 14):
339
334
 
@@ -15,9 +15,9 @@ Consistent naming prevents filesystem bugs (Linux case-sensitivity) and keeps th
15
15
 
16
16
  - ALWAYS use kebab-case for file names — never PascalCase or camelCase
17
17
  - ALWAYS use PascalCase for React component exports
18
- - ALWAYS use camelCase starting with `use` for hook exports
19
18
  - ALWAYS suffix schema files with `.schemas.ts` and type files with `.types.ts`
20
19
  - NEVER mix cases in the same category — no `UserCard.tsx` alongside `user-profile.tsx`
20
+ - Named exports, the hook `use` prefix and `default` only in route files are lint errors (`frontend-nextjs-lint`), not rules to reread
21
21
 
22
22
  ## Complete Reference Table
23
23
 
@@ -63,7 +63,6 @@ export type UpdateUserInput = z.infer<typeof updateUserSchema>;
63
63
  |-------|-------|-----|
64
64
  | `UserCard.tsx` | `user-card.tsx` | Linux servers are case-sensitive |
65
65
  | `useUsers.ts` | `use-users.ts` | Inconsistent with Next.js file conventions |
66
- | `export default function UserCard` | `export function UserCard` | Named exports are more refactor-safe |
67
66
  | `type User = { id: string }` | `type User = z.infer<typeof userSchema>` | Single source of truth |
68
67
 
69
68
  ---
@@ -16,7 +16,8 @@ Stack: Next.js 15 + Neon PostgreSQL + .NET Backend
16
16
  - ALWAYS use App Router (not Pages Router)
17
17
  - Default to Server Components -- add `'use client'` only when needed
18
18
  - ALWAYS colocate loading.tsx and error.tsx with page.tsx
19
- - NEVER call the database directly from client -- use Route Handlers as BFF
19
+ - Route Handlers are a BFF that delegates to the .NET API
20
+ - A DB driver, `any`, fetch inside `useEffect` and `'use client'` in the root layout are lint errors (`frontend-nextjs-lint`)
20
21
  - ALWAYS validate inputs with Zod on both client and server
21
22
  - Use TypeScript strict mode (`"strict": true`)
22
23
 
@@ -58,7 +59,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
58
59
  // loading.tsx
59
60
  export default function Loading() { return <div className="animate-pulse">Loading...</div>; }
60
61
 
61
- // error.tsx — MUST be 'use client'
62
+ // error.tsx
62
63
  'use client';
63
64
  export default function Error({ error, reset }: { error: Error; reset: () => void }) {
64
65
  return <div><h2>Something went wrong</h2><button onClick={reset}>Try again</button></div>;
@@ -187,7 +188,7 @@ export function DocumentForm() {
187
188
 
188
189
  ## shadcn/ui
189
190
 
190
- Install: `npx shadcn@latest init` then `npx shadcn@latest add button input card dialog form`.
191
+ Install: `npx shadcn@latest init` then `npx shadcn@latest add button input card dialog field`.
191
192
  Components are copied to `components/ui/` -- NOT an npm dependency, your code to customize.
192
193
 
193
194
  ## TypeScript Strict Patterns
@@ -195,7 +196,7 @@ Components are copied to `components/ui/` -- NOT an npm dependency, your code to
195
196
  | Pattern | Approach |
196
197
  |---------|----------|
197
198
  | API responses | Zod schema + `z.infer<typeof schema>` |
198
- | Props | Explicit interface, no `any` |
199
+ | Props | Explicit interface |
199
200
  | Event handlers | `React.ChangeEvent<HTMLInputElement>` |
200
201
  | Null safety | `?.` over type assertions, `if (!data) return null` |
201
202
 
@@ -204,10 +205,7 @@ Components are copied to `components/ui/` -- NOT an npm dependency, your code to
204
205
  | Wrong | Right | Why |
205
206
  |-------|-------|-----|
206
207
  | `'use client'` on every component | Default to Server Components | Unnecessary JS bundle size |
207
- | Direct DB from client | Route Handler `/api/*` as BFF | Exposes queries, harder to secure |
208
- | `any` for API responses | Zod schema + infer | No runtime safety |
209
208
  | Missing loading.tsx | Colocate with page.tsx | Blank page during load |
210
- | `useEffect` for data fetching | React Query `useQuery` | No caching, race conditions |
211
209
  | Form validation on submit only | Zod resolver + react-hook-form | Delayed error feedback |
212
210
  | shadcn as npm package | `npx shadcn@latest add` | Copy-paste system, not a dependency |
213
211
 
@@ -14,10 +14,11 @@ Feature-based architecture with a shared core. Features are self-contained; shar
14
14
  ## Core Rules
15
15
 
16
16
  - ALWAYS use feature-based folders: `features/{feature-name}/`
17
- - ALWAYS keep `app/` for routing only — import from `features/`, not the other way
17
+ - ALWAYS keep `app/` for routing only — the screen's logic lives in `features/`
18
18
  - NEVER import from one feature into another — extract to `components/` or `lib/` if shared
19
19
  - ALWAYS use `src/` directory (configured in `tsconfig.json` with `@/` alias)
20
20
  - NEVER put business logic in `components/` — it is presentation only
21
+ - Deep imports into a feature from outside it, `components/` → `features/`, `features/` → `app/` and shared `hooks/` → the API client are lint errors (`frontend-nextjs-lint`)
21
22
 
22
23
  ## Canonical Folder Tree
23
24
 
@@ -82,7 +83,7 @@ export { useCreateUser } from './hooks/use-create-user';
82
83
  export type { User, CreateUserInput } from './types/user.types';
83
84
  ```
84
85
 
85
- Import from the index, not deep paths:
86
+ Outside the feature, import from the index:
86
87
  ```ts
87
88
  // GOOD
88
89
  import { UserList, useUsers } from '@/features/users';
@@ -16,7 +16,7 @@ Server state (API data) lives in TanStack Query. UI state lives in React. No glo
16
16
  - ALWAYS use TanStack Query for anything that comes from the .NET API
17
17
  - ALWAYS use `useState` for local UI state (modal open, form step)
18
18
  - ALWAYS keep a list's page, sort and filters, and a page's active tab, in the URL (`searchParams`) — never in `useState`: a refresh or a shared link must reopen the same view (`admin-app/data-table.md`, `admin-app/page-structure.md`)
19
- - NEVER install Zustand or Redux without first exhausting Server Components + TanStack Query
19
+ - `zustand`, `redux` and `@reduxjs/toolkit` are lint errors (`frontend-nextjs-lint`); the only way past it is the exception below
20
20
  - NEVER use React Context for server state — Context does not cache or deduplicate
21
21
  - ALWAYS derive state from server data rather than syncing it to local state
22
22
 
@@ -30,9 +30,9 @@ Is this local UI state (open/closed, selected, current step)?
30
30
  YES → useState in the component that needs it
31
31
 
32
32
  Does multiple components need this UI state?
33
- YES — is it parent-child? → prop drilling (2-3 levels is fine)
33
+ YES — is it parent-child? → props
34
34
  YES — is it truly global? → React Context (theme, auth user, locale)
35
- YES — complex with many actions? → Consider Zustand (last resort)
35
+ YES — complex with many actions? → global store (exception below)
36
36
  ```
37
37
 
38
38
  ## Server State (TanStack Query)
@@ -55,9 +55,10 @@ function UserList() {
55
55
  }
56
56
  ```
57
57
 
58
- ## When Zustand Is Justified
58
+ ## When a Global Store Is Justified
59
59
 
60
- Only add Zustand if ALL of these are true:
60
+ The lint blocks the import. Lift it only if ALL of these are true, with one
61
+ `// eslint-disable-next-line no-restricted-imports -- <reason>` in the single store file:
61
62
  1. State is needed in 5+ unrelated components
62
63
  2. State has complex update logic (not just toggle/set)
63
64
  3. Context causes measurable re-render performance issues
@@ -67,10 +68,8 @@ Only add Zustand if ALL of these are true:
67
68
 
68
69
  | Wrong | Right | Why |
69
70
  |-------|-------|-----|
70
- | `useEffect` + `useState` for API data | TanStack Query | Stale data, no cache, double-render |
71
71
  | Global store for user list | `useUsers()` | TanStack Query handles sync/cache |
72
72
  | Context for everything | useState + prop drilling first | Context causes all consumers to re-render |
73
- | Zustand by default | Start with useState | YAGNI — most state is local |
74
73
 
75
74
  ---
76
75
 
@@ -96,5 +96,6 @@ volumes:
96
96
  ```bash
97
97
  # inside the worktree, outside Claude Code
98
98
  eval "$(morph-spec env)" # PowerShell: morph-spec env --powershell | Invoke-Expression
99
+ # --with-secrets also loads the e2e.auth.envFile credentials
99
100
  docker compose up -d --wait # lands in project morph-{feature}, on the block's ports
100
101
  ```
@@ -8,8 +8,8 @@
8
8
  nextjs) rather than by stack. A template for a .NET slice serves every stack with a .NET
9
9
  backend, so a new stack reuses what exists instead of copying it.
10
10
 
11
- `REGISTRY.json` is the source of truth: it maps each template `id` to its `path`, category,
12
- technology and applicable stacks. Nothing resolves a template by hardcoded path — code goes
11
+ `REGISTRY.json` is the source of truth: it maps each template `id` to its `kind`, `path` and
12
+ `category` (the shape per kind is in its `kinds` block). Nothing resolves a template by hardcoded path — code goes
13
13
  through the registry, and this README never repeats a path the registry already owns.
14
14
 
15
15
  ---
@@ -109,8 +109,8 @@ updates** — only override when the packaged template genuinely does not fit.
109
109
  1. Create the file under the right technology directory:
110
110
  `framework/templates/code/{technology}/...` (Handlebars syntax, `.hbs` when the extension
111
111
  would otherwise confuse tooling).
112
- 2. Register it in `REGISTRY.json` with a unique `id`, its `path`, `category`, `technology`
113
- and applicable stacks.
112
+ 2. Register it in `REGISTRY.json` with a unique `id`, its `kind`, `path` and `category`, plus
113
+ the fields its kind requires (`kinds` block; `scripts/validate-framework.js` checks them).
114
114
  3. If a generator should emit it, add the `id` to the relevant list in
115
115
  `src/lib/generators/scaffold-generator.js`.
116
116