@polymorphism-tech/morph-spec 9.3.0 → 9.5.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 (107) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/MORPH.md +23 -33
  7. package/framework/agents.json +10 -9
  8. package/framework/docs/CLI.md +3 -3
  9. package/framework/evals/review-checklist.md +1 -1
  10. package/framework/hooks/README.md +3 -2
  11. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  12. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -15
  13. package/framework/hooks/claude-code/core/comment-guard.js +1 -0
  14. package/framework/hooks/claude-code/core/conventional-commit.js +57 -0
  15. package/framework/hooks/claude-code/core/denylist.json +63 -1
  16. package/framework/hooks/claude-code/core/enforce-phase-writes.js +0 -8
  17. package/framework/hooks/claude-code/core/gate-guard.js +0 -8
  18. package/framework/hooks/claude-code/core/loop-detect.js +0 -3
  19. package/framework/hooks/claude-code/core/pre-bash-denylist.js +2 -1
  20. package/framework/hooks/claude-code/core/protect-spec-files.js +0 -7
  21. package/framework/hooks/claude-code/core/validate-completion.js +0 -4
  22. package/framework/hooks/claude-code/core/worktree-context.js +0 -2
  23. package/framework/hooks/claude-code/statusline.py +3 -44
  24. package/framework/hooks/shared/build-check.js +12 -19
  25. package/framework/hooks/shared/command-tokens.js +17 -0
  26. package/framework/hooks/shared/comment-rules.js +11 -1
  27. package/framework/hooks/shared/hook-response.js +0 -4
  28. package/framework/hooks/shared/payload-utils.js +1 -35
  29. package/framework/hooks/shared/phase-utils.js +0 -12
  30. package/framework/hooks/shared/slice-progress.js +0 -2
  31. package/framework/hooks/shared/state-reader.js +0 -19
  32. package/framework/hooks/shared/telemetry-logger.js +1 -1
  33. package/framework/rules/csharp-standards.md +5 -3
  34. package/framework/rules/dotnet-templates.md +2 -2
  35. package/framework/rules/frontend-standards.md +1 -2
  36. package/framework/rules/nextjs-standards.md +10 -8
  37. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  38. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  39. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  40. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  41. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  42. package/framework/skills/core/morph-media/SKILL.md +1 -1
  43. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  44. package/framework/skills/core/morph-review/SKILL.md +1 -1
  45. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  46. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  47. package/framework/standards/STANDARDS.json +87 -37
  48. package/framework/standards/STANDARDS.meta.json +41 -15
  49. package/framework/standards/architecture/code-comments.md +1 -1
  50. package/framework/standards/architecture/engineering-lens.md +100 -0
  51. package/framework/standards/architecture/vertical-slice/vertical-slice.md +7 -9
  52. package/framework/standards/backend/dotnet/analyzers.md +59 -0
  53. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -12
  54. package/framework/standards/frontend/admin-app/data-table.md +1 -3
  55. package/framework/standards/frontend/admin-app/page-structure.md +8 -4
  56. package/framework/standards/frontend/nextjs/app-router.md +3 -4
  57. package/framework/standards/frontend/nextjs/components.md +2 -15
  58. package/framework/standards/frontend/nextjs/data-fetching.md +2 -3
  59. package/framework/standards/frontend/nextjs/lint.md +141 -0
  60. package/framework/standards/frontend/nextjs/motion-patterns.md +2 -7
  61. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -2
  62. package/framework/standards/frontend/nextjs/nextjs-patterns.md +5 -7
  63. package/framework/standards/frontend/nextjs/project-structure.md +3 -2
  64. package/framework/standards/frontend/nextjs/state-management.md +6 -7
  65. package/framework/templates/README.md +4 -5
  66. package/framework/templates/REGISTRY.json +21 -516
  67. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +54 -56
  68. package/package.json +3 -2
  69. package/src/commands/project/worktree.js +1 -1
  70. package/src/commands/state/archive.js +1 -1
  71. package/src/commands/state/finish.js +1 -1
  72. package/src/core/paths/output-schema.js +0 -9
  73. package/src/core/state/feature-identity.js +0 -2
  74. package/src/core/state/state-manager.js +1 -169
  75. package/src/core/templates/template-registry.js +1 -43
  76. package/src/core/templates/template-renderer.js +0 -23
  77. package/src/dashboard/app/lib/format.js +0 -7
  78. package/src/dashboard/app/lib/phases.js +0 -7
  79. package/src/dashboard/scanner/activity-scanner.js +0 -4
  80. package/src/lib/ai/ai-project-scan.js +1 -6
  81. package/src/lib/graph/graphify-runtime.js +0 -1
  82. package/src/lib/mutation/snapshot.js +1 -5
  83. package/src/lib/risk-detector.js +0 -5
  84. package/src/lib/stack/stack-profile.js +0 -4
  85. package/src/lib/stack/stack-scan.js +18 -9
  86. package/src/lib/tasks/eval-node.js +1 -7
  87. package/src/lib/validators/validation-runner.js +0 -43
  88. package/src/lib/validators/verify-runner.js +18 -2
  89. package/src/utils/agents-installer.js +12 -19
  90. package/src/utils/claude-settings-manager.js +0 -28
  91. package/src/utils/file-copier.js +0 -53
  92. package/src/utils/hooks-installer.js +1 -1
  93. package/framework/hooks/shared/activity-logger.js +0 -131
  94. package/framework/standards/frontend/design-system/naming.md +0 -68
  95. package/framework/templates/meta-prompts/fusion/fusion-agent.md +0 -76
  96. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +0 -100
  97. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +0 -90
  98. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +0 -84
  99. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +0 -44
  100. package/framework/templates/meta-prompts/validators/checkpoint-validator.md +0 -107
  101. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +0 -94
  102. package/src/commands/validation/index.js +0 -2
  103. package/src/lib/validators/code-budget/index.js +0 -2
  104. package/src/lib/validators/contracts/index.js +0 -1
  105. package/src/lib/validators/nextjs/index.js +0 -1
  106. package/src/lib/validators/packages/index.js +0 -1
  107. package/src/lib/validators/reuse/index.js +0 -2
@@ -0,0 +1,100 @@
1
+ # Lente de engenharia — código para quem herda amanhã
2
+
3
+ > **Scope:** universal
4
+ > **Layer:** 0 (resumo injetado pelas rules `csharp-standards` (`**/*.cs`, `**/*.csproj`) e `nextjs-standards` (`**/*.ts`, `**/*.tsx`); texto completo sob demanda via `morph-spec standards --find`)
5
+ > **Keywords:** clean code, código limpo, fragilidade, código morto, excesso, legibilidade, extensibilidade, flag booleana, efeito colateral, conventional commits
6
+ > **Load When:** escrevendo, planejando ou revisando código em qualquer stack
7
+
8
+ **Verified against:** C#, TypeScript e JavaScript (convenção de linguagem, sem pacote externo); Conventional Commits 1.0.0. Last-verified: 2026-10-18.
9
+
10
+ ---
11
+
12
+ ## Quem herda amanhã
13
+
14
+ Escreva para quem abre o repo amanhã e precisa entregar uma feature na semana seguinte. A
15
+ convenção que o projeto já usa vence o seu gosto. O mais simples que resolve vence o mais geral.
16
+ Bug se corrige na causa raiz, nunca no sintoma.
17
+
18
+ ## Fragilidade
19
+
20
+ Código que quebra fácil:
21
+
22
+ - `catch` vazio, ou que só loga e segue como se nada tivesse acontecido.
23
+ - Null ou ausência vindo de fronteira (HTTP, banco, arquivo, LLM) sem tratamento.
24
+ - Dependência de ordem implícita: método que só funciona se outro rodou antes.
25
+ - Parser estrito diante de dado real (campo opcional tratado como obrigatório, enum fechado para
26
+ valor externo).
27
+ - Guard cego: a checagem e o dado saem do mesmo serializador, então ela nunca falha.
28
+ - Teste que passa sem provar a regra (ver `backend-dotnet-testing` / `frontend-nextjs-testing`).
29
+
30
+ ## Código morto
31
+
32
+ Símbolo, endpoint, flag, config ou tabela sem consumidor real sai. Feature começada e largada
33
+ sai. Dois caminhos que fazem a mesma coisa viram um. Quando "X só é usado por Y", pergunte para
34
+ quê Y existe: muitas vezes os dois saem. Código comentado sai, porque o git guarda o antigo.
35
+
36
+ ## Excesso
37
+
38
+ - Abstração (interface, factory, base class, strategy) sem um segundo uso hoje.
39
+ - Camada que só repassa a chamada.
40
+ - Config ou parâmetro para um caso que ninguém pediu (o "e se um dia eu quiser voar com o carro":
41
+ construa as asas quando for voar).
42
+ - Stdlib, plataforma ou dependência já instalada reinventada à mão.
43
+
44
+ No plano, todo CRIAR de abstração diz no `why` o segundo uso ou a próxima adição que ela barateia.
45
+
46
+ ## Legibilidade
47
+
48
+ - **Nomes:** descritivos, pronunciáveis, buscáveis, na linguagem do domínio. Distinção com
49
+ significado (`sourceAccount`/`targetAccount`, nunca `account1`/`account2`). Sem prefixo de tipo
50
+ nem caractere especial. Classe é substantivo, método é verbo, e um padrão aparece no nome
51
+ (`InvoiceFactory`). Um idioma por identificador.
52
+ - **Funções:** pequenas, com um objetivo só. Poucos parâmetros (passou de 3, agrupe num tipo).
53
+ Sem efeito colateral fora do próprio objeto que o nome não anuncie.
54
+ - **Sem flag booleana que bifurca o método** — são dois métodos:
55
+
56
+ ```ts
57
+ // ruim
58
+ sendInvoice(invoice, true);
59
+ // bom
60
+ sendInvoiceByEmail(invoice);
61
+ sendInvoiceByWhatsApp(invoice);
62
+ ```
63
+
64
+ - **Condicional positiva:** `if (order.isEmpty)` em vez de `if (!order.hasItems)`; o `!` some na leitura.
65
+ - **Literal repetido vira constante** (detalhe em `architecture-code-comments`).
66
+ - **Estrutura:** por feature/slice (`architecture-vertical-slice-vertical-slice`); variável declarada
67
+ perto do uso; funções de cima para baixo, a chamadora antes da chamada; funções afins juntas; sem
68
+ alinhamento horizontal; formatação pelo formatter do projeto.
69
+ - **Comentários:** `architecture-code-comments` é a regra única.
70
+
71
+ ## Extensibilidade
72
+
73
+ A próxima adição provável (uma skill nova, um campo novo de config refletido na tela, um provider
74
+ novo) deve tocar poucos arquivos e nenhum `switch` espalhado. Se exige editar N lugares que precisam
75
+ concordar, o atrito é o registro que falta, não a falta de uma camada genérica.
76
+
77
+ ## IA e código
78
+
79
+ Regra que nunca pode ser quebrada fica em código, nunca vira prompt (`ai-agents-guardrails`). O
80
+ inverso também vale: regex, heurística ou árvore de `if` que classifica linguagem natural é candidata
81
+ a structured output, e a troca só entra medida por eval (`ai-agents-evals-with-cache`).
82
+
83
+ ## Git
84
+
85
+ A branch é `morph/{f}`, criada pelo CLI. Commits seguem Conventional Commits:
86
+ `<tipo>(<escopo>): <assunto>`, com o tipo em `feat`, `fix`, `refactor`, `test`, `chore`, `docs`,
87
+ `perf`, `ci`, `build`, `style` ou `revert`. O hook `conventional-commit` recusa outra forma.
88
+
89
+ ## O que bloqueia no Gate 3
90
+
91
+ Só o que o **diff introduziu**. O que já existia no arquivo vira nota.
92
+
93
+ 1. `catch` que engole a exceção.
94
+ 2. Flag booleana que bifurca o método.
95
+ 3. Efeito colateral escondido: o método muda estado alheio e o nome não diz.
96
+ 4. Símbolo, flag ou config sem consumidor, ou código comentado.
97
+ 5. Abstração ou camada sem segundo uso e sem `why` no plano.
98
+ 6. Literal mágico repetido em comparação.
99
+
100
+ O resto (nome, tamanho, ordem, "eu faria diferente") é nota.
@@ -380,20 +380,18 @@ app.MapApiEndpoints();
380
380
  | Errors class | `{Entity}Errors` | `BookErrors` (static, shared por feature) |
381
381
  | DTO record (GetAll) | `{Entity}Dto` | `BookDto` |
382
382
  | API tag constant | `ApiTags.{Entities}` | `ApiTags.Books` |
383
- | Entity class | `{Entity}` (`sealed`) | `Book` |
383
+ | Entity class | `{Entity}` | `Book` |
384
384
 
385
385
  ---
386
386
 
387
387
  ## Padrões Obrigatórios
388
388
 
389
389
  1. **Handler records no mesmo arquivo**: `Request` e `Response` são declarados no mesmo arquivo `.cs` que o `Handler`.
390
- 2. **`sealed` em tudo**: handlers, endpoints, validators, request/response records usam `sealed`.
391
- 3. **`Guid.CreateVersion7()`** para gerar IDs (não `Guid.NewGuid()`).
392
- 4. **`result.Match()`** nos endpoints — o sucesso recebe o valor no lambda (nunca ler `result.Value`) e a falha vira `error.ToHttpResult()` (status pelo `ErrorType`, nunca `Results.BadRequest` para todo erro).
393
- 5. **IApiEndpoint `internal sealed`** — endpoints não são públicos.
394
- 6. **`{Entity}Errors` compartilhado** por todas as operações da feature (1 arquivo por feature, não por slice).
395
- 7. **`ApplicationDbContext` direto no slice**: CRUD por chave via `DbSet<T>`; queries específicas (joins, projeção, paginação) via LINQ no handler. Nunca uma Application Service layer entre handler e dados; nunca repository genérico (`IRepository<T>`/`IUnitOfWork`).
396
- 8. **`SaveChangesAsync()`** explícito em operações de escrita (Create, Update, Delete).
390
+ 2. **`result.Match()`** nos endpoints — o sucesso recebe o valor no lambda (nunca ler `result.Value`) e a falha vira `error.ToHttpResult()` (status pelo `ErrorType`, nunca `Results.BadRequest` para todo erro).
391
+ 3. **IApiEndpoint `internal`** — endpoints não são públicos.
392
+ 4. **`{Entity}Errors` compartilhado** por todas as operações da feature (1 arquivo por feature, não por slice).
393
+ 5. **`ApplicationDbContext` direto no slice**: CRUD por chave via `DbSet<T>`; queries específicas (joins, projeção, paginação) via LINQ no handler. Nunca uma Application Service layer entre handler e dados; nunca repository genérico (`IRepository<T>`/`IUnitOfWork`).
394
+ 6. **`SaveChangesAsync()`** explícito em operações de escrita (Create, Update, Delete).
397
395
  9. **Sem validator em GetAll** — GetAllBooksHandler não tem validator (sem parâmetros para validar).
398
396
 
399
397
  ---
@@ -402,7 +400,7 @@ app.MapApiEndpoints();
402
400
 
403
401
  | Anti-pattern | Por quê não |
404
402
  |--------------|-------------|
405
- | `AggregateRoot` | VSA não usa DDD — entidade simples com `sealed class` |
403
+ | `AggregateRoot` | VSA não usa DDD — entidade simples |
406
404
  | `DomainEvent` | VSA não tem Domain Events — reações são handlers separados |
407
405
  | `MediatR` / `IRequest` / `IRequestHandler` | Substituído por `IHandler<TRequest, TResponse>` próprio |
408
406
  | DTOs compartilhados entre features | Cada slice define seus próprios records |
@@ -0,0 +1,59 @@
1
+ # Analyzers .NET — o build cobra, a prosa não repete
2
+
3
+ > **Scope:** dotnet
4
+ > **Layer:** 0 (resumo injetado pela rule `csharp-standards` (`**/*.cs`, `**/*.csproj`); o `stack-scan` acusa o projeto sem estes arquivos)
5
+ > **Keywords:** analyzer, roslyn, BannedApiAnalyzers, BannedSymbols, globalconfig, Guid.NewGuid, CreateVersion7, CancellationToken, CA2016, RS0030
6
+ > **Load When:** projeto .NET sem `BannedSymbols.txt`, ou o build acusou RS0030/CA2016
7
+
8
+ **Verified against:** .NET SDK 10.0.402, Microsoft.CodeAnalysis.BannedApiAnalyzers 5.6.0 (sonda: `Guid.NewGuid()` → erro RS0030, token não repassado → erro CA2016, código conforme → 0 erros). Last-verified: 2026-10-18.
9
+
10
+ ---
11
+
12
+ Regra de código que tem motivo e nunca pode ser quebrada vira **erro de build**, não frase que o
13
+ agente relê a cada sessão. Estes três arquivos na raiz da solution bastam; nenhum `.csproj` muda.
14
+
15
+ ## Arquivos
16
+
17
+ `Directory.Build.props` (se já existe, some o `ItemGroup` ao dele):
18
+
19
+ ```xml
20
+ <Project>
21
+ <ItemGroup>
22
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" PrivateAssets="all" />
23
+ <AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" />
24
+ <GlobalAnalyzerConfigFiles Include="$(MSBuildThisFileDirectory)morph.globalconfig" />
25
+ </ItemGroup>
26
+ </Project>
27
+ ```
28
+
29
+ Com Central Package Management (`Directory.Packages.props`), a versão vai lá e o
30
+ `PackageReference` fica sem `Version`.
31
+
32
+ `BannedSymbols.txt`:
33
+
34
+ ```text
35
+ M:System.Guid.NewGuid;Use Guid.CreateVersion7(): ordenado, nao fragmenta o indice da PK
36
+ ```
37
+
38
+ `morph.globalconfig`:
39
+
40
+ ```ini
41
+ is_global = true
42
+ dotnet_diagnostic.RS0030.severity = error
43
+ dotnet_diagnostic.CA2016.severity = error
44
+ ```
45
+
46
+ ## O que cada regra compra
47
+
48
+ | Diagnóstico | Bloqueia | Por quê |
49
+ |---|---|---|
50
+ | RS0030 | `Guid.NewGuid()` | UUID v4 é aleatório: PK nele fragmenta o índice B-tree do Postgres a cada insert; `Guid.CreateVersion7()` é ordenado no tempo |
51
+ | CA2016 | método que recebe `CancellationToken` e não o repassa | requisição cancelada continua segurando conexão do banco e chamada ao LLM |
52
+
53
+ API nova para banir entra como linha no `BannedSymbols.txt` (formato `M:`/`T:`/`P:` com a
54
+ mensagem depois do `;`), com o porquê na mensagem. Nunca uma regra nova em prosa.
55
+
56
+ ## O que não é regra
57
+
58
+ `sealed`, primary constructor, `var`, expression-bodied member e ordem de `using` são gosto: nem
59
+ prosa nem analyzer. Siga o arquivo vizinho, e o formatter do projeto decide a forma.
@@ -11,7 +11,7 @@
11
11
 
12
12
  ## Overview
13
13
 
14
- Each feature in VSA is a self-contained slice with 5 files in a feature folder. Handlers implement `IHandler<TRequest, TResponse>` (not MediatR). Dependencies use primary constructors. The data path is `Handler -> ApplicationDbContext` — no service layer, no generic repository in between.
14
+ Each feature in VSA is a self-contained slice with 5 files in a feature folder. Handlers implement `IHandler<TRequest, TResponse>` (not MediatR). The data path is `Handler -> ApplicationDbContext` — no service layer, no generic repository in between.
15
15
 
16
16
  ---
17
17
 
@@ -108,11 +108,8 @@ public sealed class CreateBookHandler(
108
108
 
109
109
  ### Key Patterns
110
110
 
111
- - **`sealed class`** — handlers are never inherited
112
- - **`sealed record`** — DTOs are immutable value types
113
- - **Primary constructors** — `(ApplicationDbContext db)` replaces field + constructor boilerplate
114
- - **`Guid.CreateVersion7()`** — time-sortable, index-friendly UUID (not `Guid.NewGuid()`)
115
- - **`CancellationToken`** — passed through every async call
111
+ - **`record`** — DTOs are immutable value types
112
+ - **`Guid.CreateVersion7()` and a forwarded `CancellationToken`** — enforced by the build (`backend-dotnet-analyzers`), not by review
116
113
  - **`Result<T>` return** — never throw for expected failures
117
114
 
118
115
  ---
@@ -149,7 +146,7 @@ internal sealed class CreateBookEndpoint : IApiEndpoint
149
146
 
150
147
  ### Endpoint Conventions
151
148
 
152
- - **`internal sealed`** — endpoints are not public API
149
+ - **`internal`** — endpoints are not public API
153
150
  - **`IApiEndpoint`** — single interface for auto-discovery
154
151
  - **Handler injected via DI** — resolved as `IHandler<TReq, TRes>` parameter
155
152
  - **`Match()` for responses** — the success lambda receives the value (never read `.Value` directly); the failure lambda is `error.ToHttpResult()` from `Extensions/ErrorHttpMapping.cs` (`backend/dotnet/result-pattern.md`), which maps `ErrorType` to the status — never `Results.BadRequest` for every error
@@ -299,13 +296,9 @@ builder.Services.AddExceptionHandler<CustomExceptionHandler>()
299
296
  | Use MediatR `IRequest`/`Send()` | Use `IHandler<TReq, TRes>` resolved directly from DI |
300
297
  | Create a service/manager layer | Handler uses `ApplicationDbContext` directly |
301
298
  | Add a generic repository (`IRepository<T>`/`IUnitOfWork`) | `DbSet<T>` is the repository; `DbContext` is the unit of work |
302
- | Use `Guid.NewGuid()` | Use `Guid.CreateVersion7()` for time-sortable IDs |
303
- | Make handler classes non-sealed | Always `sealed class` |
304
- | Use mutable DTOs (class with setters) | Always `sealed record` with positional parameters |
305
- | Omit `CancellationToken` from async methods | Pass `CancellationToken` through every async call |
299
+ | Use mutable DTOs (class with setters) | Always `record` with positional parameters |
306
300
  | Put business logic in endpoints | Endpoints only map HTTP and call `Match()` |
307
301
  | Share handlers across features | Each feature has its own handler, even if similar |
308
- | Use constructor + field for DI | Use primary constructors: `class Handler(IDep dep)` |
309
302
  | Group by technical concern (Controllers/, Services/) | Group by feature: `Features/BookFeature/CreateBook/` |
310
303
  | Route data access through any indirection layer | Inject `DbContext` and write LINQ in the slice; dedicated store only with `plan.md` (`## Decisões`) justification |
311
304
  | Use raw SQL (`FromSqlRaw` / `ExecuteQuery(string)`) by default | Use LINQ; raw SQL only with a `plan.md` (`## Decisões`) justification |
@@ -82,9 +82,7 @@ The page parses the query, renders the toolbar, and wraps the results in a `Susp
82
82
  // app/(admin)/products/page.tsx
83
83
  import { Suspense } from 'react';
84
84
  import { DataTableSkeleton } from '@/components/data-table-skeleton';
85
- import { ProductResults } from '@/features/products/components/product-results';
86
- import { ProductToolbar } from '@/features/products/components/product-toolbar';
87
- import { productListQuerySchema } from '@/features/products/types/product-list.schemas';
85
+ import { ProductResults, ProductToolbar, productListQuerySchema } from '@/features/products';
88
86
 
89
87
  export default async function ProductsPage({
90
88
  searchParams,
@@ -35,10 +35,14 @@ The page reads `tab` from `searchParams` and renders only that tab's content on
35
35
 
36
36
  ```tsx
37
37
  // app/(admin)/products/[id]/page.tsx
38
- import { ProductTabs, PRODUCT_TABS, type ProductTab } from '@/features/products/components/product-tabs';
39
- import { ProductDetailsTab } from '@/features/products/components/product-details-tab';
40
- import { ProductVariantsTab } from '@/features/products/components/product-variants-tab';
41
- import { ProductHistoryTab } from '@/features/products/components/product-history-tab';
38
+ import {
39
+ PRODUCT_TABS,
40
+ ProductDetailsTab,
41
+ ProductHistoryTab,
42
+ ProductTabs,
43
+ ProductVariantsTab,
44
+ type ProductTab,
45
+ } from '@/features/products';
42
46
 
43
47
  export default async function ProductPage({
44
48
  params,
@@ -16,9 +16,9 @@ Next.js App Router with TypeScript strict mode. Server Components by default, Cl
16
16
  - ALWAYS default to Server Components — add `'use client'` only when needed
17
17
  - ALWAYS keep `app/` directory for routing only — no business logic in page files
18
18
  - NEVER put data fetching in Client Components when a Server Component can do it
19
- - NEVER use `useEffect` to fetch data — use Server Components or TanStack Query
20
19
  - ALWAYS co-locate loading/error UI: `loading.tsx`, `error.tsx` next to `page.tsx`
21
20
  - ALWAYS use TypeScript — no `.js` or `.jsx` files in the project
21
+ - Fetch inside `useEffect` and `'use client'` in the root `layout.tsx` are lint errors (`frontend-nextjs-lint`)
22
22
 
23
23
  ## Server vs Client Components
24
24
 
@@ -68,7 +68,7 @@ src/app/
68
68
  | `page.tsx` | Route segment UI | Server Component (default) |
69
69
  | `layout.tsx` | Shared UI wrapper | Server Component |
70
70
  | `loading.tsx` | Suspense fallback | Server Component |
71
- | `error.tsx` | Error boundary | **Must be Client Component** |
71
+ | `error.tsx` | Error boundary | Client Component |
72
72
  | `not-found.tsx` | 404 page | Server Component |
73
73
  | `route.ts` | API route (avoid — use .NET API) | — |
74
74
 
@@ -80,7 +80,7 @@ the same-origin proxy prefix Client Components use. See `frontend/nextjs/data-fe
80
80
 
81
81
  ```tsx
82
82
  // app/(dashboard)/users/page.tsx
83
- import { UserList } from '@/features/users/components/user-list';
83
+ import { UserList } from '@/features/users';
84
84
  import { BACKEND_API_PREFIX } from '@/lib/api/prefixes'; // server-direct prefix ONLY
85
85
 
86
86
  async function getUsers(): Promise<User[]> {
@@ -123,7 +123,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
123
123
  | Wrong | Right | Why |
124
124
  |-------|-------|-----|
125
125
  | `'use client'` on every component | Only add when needed | Kills SSR benefits, bloats JS bundle |
126
- | `useEffect(() => { fetch(...) }, [])` | Server Component fetch or TanStack Query | Two renders, no caching, no SSR |
127
126
  | Business logic in `page.tsx` | Move to `features/` | pages are routes, not controllers |
128
127
  | `fetch` without error handling | Always check `res.ok` | Silent failures in production |
129
128
 
@@ -14,10 +14,10 @@ Three-tier component hierarchy. `components/ui/` is shadcn primitives (never edi
14
14
  ## Core Rules
15
15
 
16
16
  - NEVER edit files in `components/ui/` — they are regenerated by shadcn CLI
17
- - NEVER import from `features/` inside `components/` — components know nothing about domain
18
17
  - ALWAYS compose shadcn primitives in `components/` instead of editing them
19
18
  - ALWAYS add `'use client'` only if the component uses hooks, events, or browser APIs
20
19
  - NEVER pass raw API data directly to a component — transform to props first
20
+ - `components/` importing `features/` and the legacy `@/components/ui/form` are lint errors (`frontend-nextjs-lint`)
21
21
 
22
22
  ## Three-Tier Hierarchy
23
23
 
@@ -89,26 +89,13 @@ export function UsersPageHeader({ canInvite }: { canInvite: boolean }) {
89
89
 
90
90
  A feature **list** is a Tier 3 component too: the feature owns the columns and the URL ⇄ table mapping, the shared table owns the rendering — `admin-app/data-table.md` → "The Feature Table".
91
91
 
92
- ## Component Props Conventions
93
-
94
- ```tsx
95
- interface UserCardProps {
96
- user: User;
97
- onEdit?: (id: string) => void;
98
- className?: string; // Always allow className for Tailwind override
99
- }
100
-
101
- export function UserCard({ user, onEdit, className }: UserCardProps) {}
102
- ```
103
-
104
92
  ## Common Mistakes
105
93
 
106
94
  | Wrong | Right | Why |
107
95
  |-------|-------|-----|
108
96
  | Edit `components/ui/button.tsx` | Wrap in `components/action-button.tsx` | shadcn CLI overwrites it |
109
- | `import { useUsers } from '@/features/users'` in `components/` | Move to a feature component | Breaks tier isolation |
110
97
  | `'use client'` on all components | Only on interactive ones | Unnecessary client JS |
111
- | Pass raw fetch response as prop | Type and validate with Zod first | Runtime type safety |
98
+ | Pass raw fetch response as prop | Map it to typed props in the feature | The component stops depending on the API shape |
112
99
 
113
100
  ---
114
101
 
@@ -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