@polymorphism-tech/morph-spec 8.10.0 → 8.12.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/README.md +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -0
- package/framework/MORPH.md +4 -0
- package/framework/commands/morph-proposal.md +4 -3
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +1 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +66 -31
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +7 -8
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/mcp/mcp-setup.js +6 -1
- package/src/core/templates/template-registry.js +0 -59
- package/src/dashboard/scanner/hooks-scanner.js +64 -6
- package/src/dashboard/scanner/standards-scanner.js +15 -3
- package/src/lib/stack/stack-profile.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +37 -1
- package/src/lib/validators/validation-runner.js +32 -12
- package/src/lib/validators/content/content-validator.js +0 -89
- package/src/lib/validators/content/index.js +0 -7
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
|
|
4
4
|
|
|
5
5
|
**Package:** `@polymorphism-tech/morph-spec`
|
|
6
|
-
**Version:** 8.
|
|
6
|
+
**Version:** 8.12.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -368,4 +368,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
368
368
|
|
|
369
369
|
---
|
|
370
370
|
|
|
371
|
-
*morph-spec v8.
|
|
371
|
+
*morph-spec v8.12.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -24,6 +24,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
24
24
|
|
|
25
25
|
**SEMPRE:**
|
|
26
26
|
- Auto-advance entre tarefas durante a fase implement — não pause sem ambiguidade bloqueante.
|
|
27
|
+
- Abrir toda feature `feature`/`bug` com a escolha de **modo de descoberta** (entrevista ou automático), depois de varrer o ambiente e montar a árvore de decisões abertas. **No modo entrevista, uma pergunta por chamada de `AskUserQuestion`** — o "1-4 perguntas por chamada" do bloco NUNCA é o teto da tool, não licença para agrupar a entrevista. O Gate 1 pausa sempre nesse modo.
|
|
27
28
|
- Atualizar o progresso em `tasks.json` ao concluir cada task gravando apenas os campos mutáveis `status`, `outputs` e `notes` — via `Write` OU `Edit` (o hook `protect-spec-files` faz diff semântico e permite qualquer um dos dois desde que o contrato do plano permaneça congelado).
|
|
28
29
|
- Apresentar gates com o template fixo definido em MORPH.md / morph-plan.
|
|
29
30
|
- Registrar decisões não triviais em `decisions.md`.
|
package/framework/MORPH.md
CHANGED
|
@@ -28,6 +28,8 @@ Entre gates, você opera com autonomia total. O dev valida. Você executa.
|
|
|
28
28
|
|
|
29
29
|
**Regra de ouro:** Dúvida que não bloqueia o progresso → resolver e registrar. Dúvida que bloqueia → uma pergunta, máximo.
|
|
30
30
|
|
|
31
|
+
**Escopo da regra de ouro:** ela vale **entre gates** — durante plan, implement e review. Ela **não** vale na abertura de uma feature: a fase proposal pode rodar em **modo entrevista**, em que perguntar até fechar o entendimento é o comportamento desejado, escolhido pelo usuário na pergunta de abertura (`morph-brainstorming` Parte 2). Entrevistar antes de especificar é barato; descobrir o mal-entendido depois do Gate 2 não é.
|
|
32
|
+
|
|
31
33
|
**Código antes do juiz:** o `morph-spec verify` é a barreira determinística e barata — build, testes e validadores estáticos rodam primeiro, sempre igual, sem custo de tokens. O juiz LLM (morph-eval) só é acionado quando o verify passa. Código é o herói não-celebrado do loop: ele pega o que pega de graça; o juiz gasta tokens só no que o código não consegue ver.
|
|
32
34
|
|
|
33
35
|
---
|
|
@@ -91,6 +93,8 @@ O fluxo padrão de uma feature tem quatro fases. Cada fase tem uma pasta dedicad
|
|
|
91
93
|
| implement | `3-implement/` | Nenhum | Todas as tasks do tasks.json com status `done` |
|
|
92
94
|
| review | `4-review/` | Gate 3 (trust-aware) | `review-report.md` aprovado e commitado |
|
|
93
95
|
|
|
96
|
+
A fase `proposal` tem dois **modos de descoberta**, escolhidos pelo usuário na abertura (só `workType: feature` e `bug` — `chore`/`hotfix` nascem em `implement`): **entrevista** (uma pergunta por vez até a árvore de decisões abertas zerar; Gate 1 pausa sempre, porque a confirmação do entendimento compartilhado é o gate) e **automático** (até 4 perguntas em lote; Gate 1 trust-aware). Nos dois modos a varredura do ambiente roda **antes** de qualquer pergunta — fato que o `morph-stack-scan` ou o acervo de use cases responde não vira pergunta ao usuário. Detalhes em `morph-brainstorming`.
|
|
97
|
+
|
|
94
98
|
A fase `1-design/` (design system, mockups, flows) é opcional — aplica-se apenas a features com componentes de UI novos e deve ser explicitamente requisitada pelo usuário.
|
|
95
99
|
|
|
96
100
|
Não avance de fase sem o gate correspondente ter sido confirmado.
|
|
@@ -21,10 +21,11 @@ Conduz a feature do request inicial até o Gate 2 (plano aprovado), sem interven
|
|
|
21
21
|
- Se não existe: criar a estrutura e seguir o passo 2.
|
|
22
22
|
|
|
23
23
|
2. **Invocar `morph-brainstorming`** (skill em `framework/skills/core/morph-brainstorming/`):
|
|
24
|
-
-
|
|
25
|
-
-
|
|
24
|
+
- Varre o ambiente **antes** de qualquer pergunta (stack, contexto do projeto, acervo de use cases).
|
|
25
|
+
- Monta a árvore de decisões abertas — só o que a varredura não respondeu.
|
|
26
|
+
- Abre com a escolha de **modo de descoberta** (entrevista ou automático), recomendando um.
|
|
26
27
|
- Produz `proposal.md` em `.morph/features/{feature-name}/0-proposal/`.
|
|
27
|
-
- Apresenta o **Gate 1
|
|
28
|
+
- Apresenta o **Gate 1** (que pausa sempre no modo entrevista).
|
|
28
29
|
|
|
29
30
|
3. **Aguardar aprovação do Gate 1.**
|
|
30
31
|
- Resposta válida: "aprovado", "pode avançar", "lgtm".
|
|
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# create-umbraco-lp
|
|
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# migrate-lp-to-umbraco
|
|
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: morph-brainstorming
|
|
3
|
-
description: Entende o negócio, os usuários e a arquitetura antes de qualquer planejamento técnico. Para projetos existentes, detecta desvios de stack e propõe plano de compliance.
|
|
3
|
+
description: Entende o negócio, os usuários e a arquitetura antes de qualquer planejamento técnico. Varre o ambiente primeiro, monta a árvore de decisões abertas e abre com a escolha de modo de descoberta (entrevista uma-pergunta-por-vez ou automático). Para projetos existentes, detecta desvios de stack e propõe plano de compliance.
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber um novo request de feature, antes de morph-plan
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-brainstorming
|
|
@@ -31,44 +31,72 @@ O harness conhece o stack por padrão. Não pergunte sobre tecnologia a menos qu
|
|
|
31
31
|
|
|
32
32
|
## Modo bug-lean (`workType: bug`)
|
|
33
33
|
|
|
34
|
-
Quando a feature tem `workType: bug` (roteada pela `morph-proposal` — ver MORPH.md §2.1 "Roteamento por tipo de trabalho"), o brainstorming é enxuto
|
|
34
|
+
Quando a feature tem `workType: bug` (roteada pela `morph-proposal` — ver MORPH.md §2.1 "Roteamento por tipo de trabalho"), o brainstorming é enxuto. A varredura (Parte 0) continua rodando normalmente — o diagnóstico precisa do contexto técnico —, mas a árvore da Parte 1 é de **repro, diagnóstico e comportamento do sistema**, não de negócio; em modo automático (Parte 3) isso significa no máximo 1 pergunta de repro, e só se o pedido original não deixar claro. O `proposal.md` (Parte 4) foca em **Repro / Diagnóstico / Root cause provável / Escopo do fix**, sem as seções de negócio completas.
|
|
35
35
|
|
|
36
36
|
Para `workType: feature` (ou features sem `workType` — compat, ver MORPH.md §2.1), o fluxo completo abaixo permanece integral.
|
|
37
37
|
|
|
38
38
|
## O que fazer
|
|
39
39
|
|
|
40
|
-
### Parte
|
|
40
|
+
### Parte 0 — Varredura (antes de qualquer pergunta)
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
- Qual problema de negócio esta feature resolve?
|
|
44
|
-
- Quem são os usuários afetados e quais são os fluxos críticos deles?
|
|
45
|
-
- Qual é o critério de sucesso — como saberemos que a feature está pronta?
|
|
46
|
-
- Há integrações externas, dependências de outras features ou restrições de prazo?
|
|
42
|
+
Fato não se pergunta. Antes de levar a primeira pergunta ao usuário, levante o que o ambiente já responde:
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
### Parte 2 — Detecção de contexto (greenfield vs brownfield)
|
|
51
|
-
|
|
52
|
-
2. Invoque `Skill(morph-stack-scan)` para detectar contexto. A skill:
|
|
44
|
+
1. Invoque `Skill(morph-stack-scan)`. A skill:
|
|
53
45
|
- Curto-circuita em projetos greenfield (sem source files) — retorna relatório vazio.
|
|
54
46
|
- Em brownfield, varre stack atual (.csproj, package.json, migrations), padrões (Guid.NewGuid vs Guid.CreateVersion7, sealed, result pattern), features existentes.
|
|
55
47
|
- Produz `.morph/features/{feature}/0-proposal/stack-scan.md`.
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
2. Leia o contexto que já existe no projeto: `.morph/context/` (se existir), `.morph/config/config.json` (stack, integrations, trust), as features ativas em `.morph/features/` e o **acervo de use cases** em `.morph/specs/usecases/`. O comportamento já implementado é o contexto mais barato que existe — e é ele que evita perguntar o que o sistema já responde.
|
|
50
|
+
|
|
51
|
+
3. Reconciliação de design-system (se aplicável): comparar `.morph/context/design-system.md` (se existir) com o que foi detectado no stack-scan e com bundles/mocks recentes do usuário. Se houver **divergência** (tokens diferentes, framework version mismatch, fonte aprovada distinta), marque `signals.designSystemDivergence = true` — a escolha da fonte vira uma **decisão da árvore** (Parte 1), não uma pergunta solta. Resolvida, atualize `.morph/context/design-system.md` com a fonte escolhida + entrada de changelog (data, motivo).
|
|
52
|
+
|
|
53
|
+
4. Desvio de stack detectado: gere `migration-plan.md` em `.morph/features/{feature}/0-proposal/`. "A migração entra nesta feature ou vira feature separada?" também é decisão da árvore.
|
|
54
|
+
|
|
55
|
+
### Parte 1 — Árvore de decisões abertas
|
|
56
|
+
|
|
57
|
+
Enumere **só o que a varredura não respondeu**. Uma decisão entra na árvore quando (a) muda o escopo, o comportamento ou a arquitetura da feature, e (b) o ambiente não a resolve.
|
|
58
|
+
|
|
59
|
+
Ordene por dependência: decisão que **poda** outras vem primeiro. "O fluxo é síncrono ou assíncrono?" antecede "qual é a política de retry" — a primeira resposta pode eliminar a segunda inteira.
|
|
60
|
+
|
|
61
|
+
Publique a **árvore de decisões** ao usuário antes de perguntar qualquer coisa. Ela é o sinal de progresso da entrevista e a base da recomendação de modo na Parte 2.
|
|
62
|
+
|
|
63
|
+
### Parte 2 — Pergunta de abertura (modo de descoberta)
|
|
64
|
+
|
|
65
|
+
Aplica-se a `workType: feature` e `bug`. **`chore` e `hotfix` não passam por aqui** — nascem com os Gates 1 e 2 carimbados `policy:{tipo}` e começam direto em `implement`.
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
- **Greenfield:** seguir para a Parte 3.
|
|
66
|
-
- **Stack padrão:** seguir para a Parte 3.
|
|
67
|
-
- **Desvio de stack:** gerar `migration-plan.md` em `.morph/features/{feature}/0-proposal/`. Perguntar via `AskUserQuestion`: "A migração de stack deve ser incluída nesta feature ou tratada numa feature separada?" Registrar decisão e seguir.
|
|
67
|
+
Faça **uma** `AskUserQuestion` apresentando a árvore e recomendando um modo. A recomendação sai do tamanho e do acoplamento da árvore: muitas decisões abertas, ou decisões que dependem umas das outras → recomende **entrevista**; árvore rasa ou já respondida pela varredura → recomende **automático**. A opção recomendada vai em primeiro lugar, com o sufixo `(Recomendado)` no label.
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
```
|
|
70
|
+
question: "Antes de começar {feature}: entrevista ou automático? Encontrei {n} decisões abertas: {resumo da árvore}"
|
|
71
|
+
header: "Descoberta"
|
|
72
|
+
options:
|
|
73
|
+
- { label: "{modo recomendado} (Recomendado)", description: "{por que a árvore recomenda este modo}" }
|
|
74
|
+
- { label: "{o outro modo}", description: "{o que muda na prática}" }
|
|
75
|
+
```
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
### Parte 3 — Entendimento
|
|
78
|
+
|
|
79
|
+
**Modo entrevista.** Percorra a árvore, **uma pergunta por chamada** de `AskUserQuestion` — perguntas em lote desorientam e impedem que uma resposta pode ramos. Regras:
|
|
80
|
+
|
|
81
|
+
- Toda pergunta traz **resposta recomendada**, como primeira opção, marcada `(Recomendado)`.
|
|
82
|
+
- Cada resposta risca, poda ou abre ramos; mostre a árvore atualizada quando ela mudar de forma.
|
|
83
|
+
- Fato descoberto no ambiente nunca vira pergunta. As **decisões** são do usuário; os **fatos** são seus.
|
|
84
|
+
- Termina quando a árvore zera. Então apresente o **entendimento compartilhado** (problema, escopo com exclusões, usuários e fluxos, riscos) — a confirmação dele é o Gate 1, que **pausa sempre** neste modo.
|
|
85
|
+
- O usuário pode encerrar a entrevista a qualquer momento; o que ficou aberto vira exclusão explícita de escopo no `proposal.md`.
|
|
86
|
+
|
|
87
|
+
**Modo automático.** Até 4 perguntas via `AskUserQuestion` cobrindo o que a árvore deixou aberto:
|
|
88
|
+
- Qual problema de negócio esta feature resolve?
|
|
89
|
+
- Quem são os usuários afetados e quais são os fluxos críticos deles?
|
|
90
|
+
- Qual é o critério de sucesso — como saberemos que a feature está pronta?
|
|
91
|
+
- Há integrações externas, dependências de outras features ou restrições de prazo?
|
|
92
|
+
|
|
93
|
+
Árvore vazia → nenhuma pergunta. O Gate 1 segue trust-aware (regras do MORPH.md §5).
|
|
94
|
+
|
|
95
|
+
**Em `workType: bug`**, a árvore é de **repro, diagnóstico e comportamento do sistema** — não de negócio. Em modo automático vale o bug-lean: no máximo 1 pergunta de repro, e só se o pedido original não deixar claro.
|
|
96
|
+
|
|
97
|
+
### Parte 4 — Produção do proposal.md
|
|
98
|
+
|
|
99
|
+
1. Produza `proposal.md` em `.morph/features/{feature}/0-proposal/` com:
|
|
72
100
|
- **Problema:** o que está sendo resolvido e para quem (máx. 2 parágrafos)
|
|
73
101
|
- **Escopo:** o que está incluído e o que está explicitamente excluído
|
|
74
102
|
- **Usuários e fluxos críticos:** quem usa e qual é o caminho principal
|
|
@@ -82,21 +110,28 @@ Para `workType: feature` (ou features sem `workType` — compat, ver MORPH.md §
|
|
|
82
110
|
|
|
83
111
|
Um bom proposal.md tem no máximo 2 páginas. Se está crescendo além disso, o escopo precisa ser cortado.
|
|
84
112
|
|
|
85
|
-
|
|
113
|
+
2. Apresente o Gate 1 segundo o modo de descoberta e o trust mode.
|
|
86
114
|
|
|
87
115
|
Ler `.morph/config/config.json → trust` (default `"auto"`). Calcular os sinais de risco em linguagem natural — sem scripts:
|
|
88
116
|
|
|
89
|
-
- `
|
|
117
|
+
- `discoveryMode` = `"interview"` ou `"auto"`, conforme a resposta da Parte 2.
|
|
118
|
+
- `designSystemDivergence` = detectado na Parte 0 (fontes de design conflitantes).
|
|
90
119
|
- `standardsViolations` = lista de violações detectadas.
|
|
91
120
|
|
|
92
|
-
**Decisão de pausa** (regras da tabela do MORPH.md §5): `trust: manual` → pausa sempre; `designSystemDivergence` → pausa; `standardsViolations` não-vazio → pausa; caso contrário → auto-passa (`pause: false`). Monte `reason` com o sinal que disparou.
|
|
121
|
+
**Decisão de pausa** (regras da tabela do MORPH.md §5): **modo entrevista → pausa sempre** ("não aja até eu confirmar" é literalmente o Gate 1: a confirmação do entendimento compartilhado é o gate); `trust: manual` → pausa sempre; `designSystemDivergence` → pausa; `standardsViolations` não-vazio → pausa; caso contrário → auto-passa (`pause: false`). Monte `reason` com o sinal que disparou.
|
|
93
122
|
|
|
94
123
|
**Persista a decisão ANTES de apresentar ou auto-aprovar — via CLI, nunca editando feature.json à mão** (o Edit manual colide com o hook state-sync, que reescreve o arquivo entre o seu Read e o Edit — erro "file modified since read"):
|
|
95
124
|
|
|
96
125
|
```
|
|
97
|
-
|
|
126
|
+
# modo entrevista — o gate pausa sempre
|
|
127
|
+
morph-spec gate-decision {feature} proposal --pause true --reason "modo entrevista — entendimento compartilhado confirmado pelo usuário" --signals '{"discoveryMode":"interview","designSystemDivergence":false,"standardsViolations":[]}'
|
|
128
|
+
|
|
129
|
+
# modo automático, sem sinais de risco
|
|
130
|
+
morph-spec gate-decision {feature} proposal --pause false --reason "nenhum sinal de risco" --signals '{"discoveryMode":"auto","designSystemDivergence":false,"standardsViolations":[]}'
|
|
98
131
|
```
|
|
99
132
|
|
|
133
|
+
`signals` é free-form no `feature.schema.json` — `discoveryMode` não exige mudança de schema, e deixa o modo escolhido auditável no `feature.json` commitado.
|
|
134
|
+
|
|
100
135
|
O comando grava `gateDecisions.proposal = { pause, reason, signals, trust, decidedAt }` no `feature.json` autoritativo com timestamp server-side (shape canônico em `.morph/framework/schemas/feature.schema.json`). O hook `gate-guard.js` confere depois se a ação (pausar vs auto-aprovar) bateu com esta decisão e avisa em divergência. Sem o registro o guard não verifica — não pule.
|
|
101
136
|
|
|
102
137
|
**Se `pause === true`** (manual mode OU risco detectado em auto), use `AskUserQuestion`:
|
|
@@ -134,5 +169,5 @@ Para `workType: feature` (ou features sem `workType` — compat, ver MORPH.md §
|
|
|
134
169
|
## Referências
|
|
135
170
|
- `.morph/framework/MORPH.md` — seções 1 (Papéis), 2 (Fases), 5 (Gates), Trust Mode
|
|
136
171
|
- `.morph/framework/standards/STANDARDS.json` — para referência de standards relevantes à stack
|
|
137
|
-
- `framework/skills/core/morph-stack-scan/SKILL.md` — invocada na Parte
|
|
172
|
+
- `framework/skills/core/morph-stack-scan/SKILL.md` — invocada na Parte 0 (varredura), antes de qualquer pergunta
|
|
138
173
|
- `.morph/framework/MORPH.md` §5 — regras de pausa do Gate 1 (trust mode)
|
|
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
|
|
6
6
|
user-invokable: false
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-eval
|
|
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber "/morph-hotfix {feature}", quando a morph-proposal classifica o pedido como workType hotfix (Passo 0), ou o usuário diz "produção caiu / hotfix / urgente"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|
|
@@ -4,7 +4,7 @@ description: Guia a execução de cada tarefa de implementação com standard e
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar cada task de implementação listada em tasks.json
|
|
6
6
|
user-invokable: false
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-implement
|
|
@@ -4,7 +4,7 @@ description: Guia a produção dos artefatos de planejamento de uma feature (use
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-proposal
|
|
@@ -47,7 +47,7 @@ morph-spec create {feature-name} --request "<pedido do usuário>"
|
|
|
47
47
|
|
|
48
48
|
- **`hotfix`** → invoque `Skill(morph-hotfix)` passando `{feature-name}` e **ENCERRE** — não siga o restante deste fluxo. A skill cuida de scout, brief, fix cirúrgico, test loop e Gate 3 (sempre humano).
|
|
49
49
|
- **`chore`** → fast-track: os Gates 1 e 2 já nasceram carimbados `policy:chore` no `create`. Escreva um `2-plan/tasks.json` mínimo (1-3 tasks) e invoque `Skill(morph-apply)` diretamente — **exceção documentada** à regra do passo 7 ("não despachar `morph-apply` automaticamente"): não pausar numa mudança óbvia é exatamente o valor do fast-track.
|
|
50
|
-
- **`bug`** → siga para `Skill(morph-brainstorming)` em modo **bug-lean**: produz um `proposal.md` enxuto (repro / diagnóstico / root cause / escopo
|
|
50
|
+
- **`bug`** → siga para `Skill(morph-brainstorming)` em modo **bug-lean**: produz um `proposal.md` enxuto (repro / diagnóstico / root cause / escopo do fix — não a proposta completa de uma feature nova; **sem critérios de aceite**, que moram na pós-condição do use case) e depois um plano enxuto via `Skill(morph-plan)`. Gates 1 e 2 continuam trust-aware (passos 3 e 6) — só o conteúdo dos artefatos é mais curto.
|
|
51
51
|
- **`feature`** (default — inclui features criadas antes do v8 sem `workType`) → siga o fluxo completo abaixo (passos 1-7): brainstorming → Gate 1 → plan → Gate 2.
|
|
52
52
|
|
|
53
53
|
### 1. Determinar feature name e estado
|
|
@@ -59,20 +59,19 @@ morph-spec create {feature-name} --request "<pedido do usuário>"
|
|
|
59
59
|
|
|
60
60
|
### 2. Invocar `Skill(morph-brainstorming)`
|
|
61
61
|
|
|
62
|
-
Passa o nome da feature. A skill:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- Detecta divergência entre fontes de design-system e marca `signals.designSystemDivergence`.
|
|
66
|
-
- Produz `proposal.md` em `.morph/features/{feature}/0-proposal/`.
|
|
62
|
+
Passa o nome da feature. A skill é a **dona da fase de entendimento** — não re-descreva o comportamento dela aqui. Em resumo: varre o ambiente antes de perguntar qualquer coisa (`Skill(morph-stack-scan)` + contexto do projeto + acervo de use cases), monta a **árvore de decisões abertas**, abre com a escolha de **modo de descoberta** (entrevista ou automático — só `feature` e `bug`), detecta divergência entre fontes de design-system (`signals.designSystemDivergence`) e produz `proposal.md` em `.morph/features/{feature}/0-proposal/`. Detalhes em `framework/skills/core/morph-brainstorming/SKILL.md`.
|
|
63
|
+
|
|
64
|
+
No **modo entrevista** o Gate 1 pausa sempre — a confirmação do entendimento compartilhado é o gate. O modo escolhido chega ao passo 3 em `gateDecisions.proposal.signals.discoveryMode`.
|
|
67
65
|
|
|
68
66
|
### 3. Gate 1 (trust-aware)
|
|
69
67
|
|
|
70
68
|
Ler `.morph/config/config.json → trust` (default `"auto"`). Calcular sinais de risco:
|
|
71
69
|
|
|
70
|
+
- `discoveryMode` — `"interview"` ou `"auto"`, decidido na pergunta de abertura da morph-brainstorming.
|
|
72
71
|
- `designSystemDivergence` — detectado pela morph-brainstorming.
|
|
73
72
|
- `standardsViolations` — violações de mandate listadas na proposal (não deveria, mas verifica).
|
|
74
73
|
|
|
75
|
-
**Decisão de pausa** (regras da tabela do MORPH.md §5, aplicadas em linguagem natural — sem scripts): `trust: manual` → `pause: true` sempre; `designSystemDivergence` → pausa; `standardsViolations` não-vazio → pausa; caso contrário → `pause: false` (auto-passa). Monte `reason` com o sinal que disparou.
|
|
74
|
+
**Decisão de pausa** (regras da tabela do MORPH.md §5, aplicadas em linguagem natural — sem scripts): `discoveryMode: "interview"` → `pause: true` sempre; `trust: manual` → `pause: true` sempre; `designSystemDivergence` → pausa; `standardsViolations` não-vazio → pausa; caso contrário → `pause: false` (auto-passa). Monte `reason` com o sinal que disparou.
|
|
76
75
|
|
|
77
76
|
**Se `pause === true`**, apresentar via `AskUserQuestion`:
|
|
78
77
|
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-review
|
|
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-stack-scan
|
|
@@ -4,7 +4,7 @@ description: Localiza e carrega um standard específico do STANDARDS.json sob de
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: quando precisar consultar um standard específico durante a implementação
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.12.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
package/package.json
CHANGED
|
@@ -34,8 +34,13 @@ import { detectProject } from '../../lib/detectors/index.js';
|
|
|
34
34
|
/**
|
|
35
35
|
* Detect the current project's stack.
|
|
36
36
|
* Prefers .morph/config/config.json (set during morph-spec init) over filesystem re-detection.
|
|
37
|
+
*
|
|
38
|
+
* Exported for testing: the stack decides which MCPs are relevant, and it is
|
|
39
|
+
* the one branch here worth pinning — `.morph/config/config.json` is the only
|
|
40
|
+
* place a compound stack (`dotnet-nextjs`) survives, since filesystem
|
|
41
|
+
* re-detection collapses it to whichever half it notices first.
|
|
37
42
|
*/
|
|
38
|
-
async function detectStack(targetPath) {
|
|
43
|
+
export async function detectStack(targetPath) {
|
|
39
44
|
// 1. Prefer the authoritative config (handles compound stacks like nextjs+dotnet)
|
|
40
45
|
try {
|
|
41
46
|
const configPath = join(targetPath, '.morph', 'config', 'config.json');
|
|
@@ -151,65 +151,6 @@ export function getRequiredTemplates(stackId = null, projectPath = null) {
|
|
|
151
151
|
return templates;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/**
|
|
155
|
-
* Get infrastructure templates for a specific stack.
|
|
156
|
-
*
|
|
157
|
-
* @param {string} stackId - Stack identifier
|
|
158
|
-
* @param {string} [projectPath] - Optional project path
|
|
159
|
-
* @returns {Array} Infrastructure template array
|
|
160
|
-
*/
|
|
161
|
-
export function getInfrastructureTemplates(stackId, projectPath = null) {
|
|
162
|
-
const registry = loadTemplateRegistry(projectPath);
|
|
163
|
-
return registry.infrastructureTemplates?.[stackId] || [];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Get contract templates for a specific stack.
|
|
168
|
-
*
|
|
169
|
-
* @param {string} stackId - Stack identifier
|
|
170
|
-
* @param {string} [projectPath] - Optional project path
|
|
171
|
-
* @returns {Array} Contract template array
|
|
172
|
-
*/
|
|
173
|
-
export function getContractTemplates(stackId, projectPath = null) {
|
|
174
|
-
const registry = loadTemplateRegistry(projectPath);
|
|
175
|
-
return registry.contractTemplates?.[stackId] || [];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Get integration templates for a specific stack.
|
|
180
|
-
*
|
|
181
|
-
* @param {string} stackId - Stack identifier
|
|
182
|
-
* @param {string} [projectPath] - Optional project path
|
|
183
|
-
* @returns {Array} Integration template array
|
|
184
|
-
*/
|
|
185
|
-
export function getIntegrationTemplates(stackId, projectPath = null) {
|
|
186
|
-
const registry = loadTemplateRegistry(projectPath);
|
|
187
|
-
return registry.integrationTemplates?.[stackId] || [];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Get SaaS templates for a specific stack.
|
|
192
|
-
*
|
|
193
|
-
* @param {string} stackId - Stack identifier
|
|
194
|
-
* @param {string} [projectPath] - Optional project path
|
|
195
|
-
* @returns {Array} SaaS template array
|
|
196
|
-
*/
|
|
197
|
-
export function getSaasTemplates(stackId, projectPath = null) {
|
|
198
|
-
const registry = loadTemplateRegistry(projectPath);
|
|
199
|
-
return registry.saasTemplates?.[stackId] || [];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Get example templates.
|
|
204
|
-
*
|
|
205
|
-
* @param {string} [projectPath] - Optional project path
|
|
206
|
-
* @returns {Array} Example template array
|
|
207
|
-
*/
|
|
208
|
-
export function getExampleTemplates(projectPath = null) {
|
|
209
|
-
const registry = loadTemplateRegistry(projectPath);
|
|
210
|
-
return registry.exampleTemplates || [];
|
|
211
|
-
}
|
|
212
|
-
|
|
213
154
|
/**
|
|
214
155
|
* Search templates by keyword (searches id, name, description).
|
|
215
156
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
2
|
-
import { join, basename
|
|
2
|
+
import { join, basename } from 'path';
|
|
3
|
+
import { getHookDefinitions } from '../../utils/hooks-installer.js';
|
|
3
4
|
|
|
4
5
|
function findJsFiles(dir) {
|
|
5
6
|
const results = [];
|
|
@@ -25,17 +26,74 @@ function extractImports(source) {
|
|
|
25
26
|
return imports;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Claude Code event name → the kebab-case key the Hook Inspector groups by.
|
|
31
|
+
* `pre-compact` and `teammate-idle` have no MORPH hooks — those nodes render
|
|
32
|
+
* empty on purpose.
|
|
33
|
+
*/
|
|
34
|
+
const EVENT_KEYS = {
|
|
35
|
+
SessionStart: 'session-start',
|
|
36
|
+
UserPromptSubmit: 'user-prompt',
|
|
37
|
+
PreToolUse: 'pre-tool-use',
|
|
38
|
+
PostToolUse: 'post-tool-use',
|
|
39
|
+
Stop: 'stop',
|
|
40
|
+
SubagentStop: 'stop',
|
|
41
|
+
PreCompact: 'pre-compact',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build filename → [{ event, matcher }] from the installer's MORPH_HOOKS.
|
|
46
|
+
*
|
|
47
|
+
* The event is NOT derivable from the path. Before the v4→v5 migration the
|
|
48
|
+
* hooks sat in per-event folders, so `basename(dirname(file))` produced the
|
|
49
|
+
* event key; the migration flattened everything into `core/`, and from then on
|
|
50
|
+
* every hook reported `event: 'core'`. Nothing crashed — the Inspector groups
|
|
51
|
+
* by event, found no key it knew, and rendered every node empty. Twelve hooks,
|
|
52
|
+
* zero shown.
|
|
53
|
+
*
|
|
54
|
+
* `getHookDefinitions()` is the single source of truth (the same array the
|
|
55
|
+
* installer writes into settings.local.json), so the registration and the
|
|
56
|
+
* drawing can no longer disagree.
|
|
57
|
+
*/
|
|
58
|
+
export function hookRegistrations(definitions = getHookDefinitions()) {
|
|
59
|
+
const byFile = new Map();
|
|
60
|
+
for (const def of definitions) {
|
|
61
|
+
const key = EVENT_KEYS[def.event];
|
|
62
|
+
if (!key) continue;
|
|
63
|
+
for (const h of def.hooks || []) {
|
|
64
|
+
const file = basename(String(h.command || '').trim());
|
|
65
|
+
if (!file.endsWith('.js')) continue;
|
|
66
|
+
if (!byFile.has(file)) byFile.set(file, []);
|
|
67
|
+
byFile.get(file).push({ event: key, rawEvent: def.event, matcher: def.matcher || null });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return byFile;
|
|
71
|
+
}
|
|
72
|
+
|
|
28
73
|
export function scanHooks(frameworkDir) {
|
|
29
74
|
const hooksDir = join(frameworkDir, 'hooks', 'claude-code');
|
|
30
75
|
if (!existsSync(hooksDir)) return [];
|
|
31
|
-
|
|
76
|
+
|
|
77
|
+
const registrations = hookRegistrations();
|
|
78
|
+
|
|
79
|
+
return findJsFiles(hooksDir).flatMap(filePath => {
|
|
32
80
|
const source = readFileSync(filePath, 'utf8');
|
|
33
|
-
|
|
34
|
-
|
|
81
|
+
const filename = basename(filePath);
|
|
82
|
+
const base = {
|
|
35
83
|
name: basename(filePath, '.js'),
|
|
36
|
-
filename
|
|
37
|
-
source,
|
|
84
|
+
filename,
|
|
85
|
+
source,
|
|
86
|
+
imports: extractImports(source),
|
|
38
87
|
description: extractJsDocDescription(source),
|
|
39
88
|
};
|
|
89
|
+
|
|
90
|
+
// One entry per (file × event): telemetry-log is registered on both
|
|
91
|
+
// PostToolUse and UserPromptSubmit, and must show under both.
|
|
92
|
+
const regs = registrations.get(filename);
|
|
93
|
+
if (!regs || regs.length === 0) {
|
|
94
|
+
// On disk but never registered — real dead weight, and worth seeing.
|
|
95
|
+
return [{ ...base, event: 'unregistered', matcher: null }];
|
|
96
|
+
}
|
|
97
|
+
return regs.map(r => ({ ...base, event: r.event, rawEvent: r.rawEvent, matcher: r.matcher }));
|
|
40
98
|
});
|
|
41
99
|
}
|
|
@@ -7,9 +7,21 @@ export function scanStandards(frameworkDir, agents) {
|
|
|
7
7
|
const sJson = JSON.parse(readFileSync(standardsJsonPath, 'utf8'));
|
|
8
8
|
const refMap = {};
|
|
9
9
|
for (const agent of agents) {
|
|
10
|
-
for (const std of agent.standards) {
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
for (const std of agent.standards || []) {
|
|
11
|
+
// agents.json stores standards as plain id STRINGS. Reading `std.id` off
|
|
12
|
+
// a string yields undefined, so every reference piled into refMap
|
|
13
|
+
// [undefined] and the lookup below missed all 60 — the Standards graph
|
|
14
|
+
// drew standards with no agent edge at all. The object form is the v4
|
|
15
|
+
// shape (`{ id, scope, priority }`); still accepted so a mixed or
|
|
16
|
+
// restored agents.json does not silently lose edges again.
|
|
17
|
+
const id = typeof std === 'string' ? std : std?.id;
|
|
18
|
+
if (!id) continue;
|
|
19
|
+
if (!refMap[id]) refMap[id] = [];
|
|
20
|
+
refMap[id].push({
|
|
21
|
+
agentId: agent.id,
|
|
22
|
+
scope: typeof std === 'string' ? null : (std.scope ?? null),
|
|
23
|
+
priority: typeof std === 'string' ? null : (std.priority ?? null),
|
|
24
|
+
});
|
|
13
25
|
}
|
|
14
26
|
}
|
|
15
27
|
const list = sJson.standards.map(std => {
|
|
@@ -9,7 +9,6 @@ import { join } from 'path';
|
|
|
9
9
|
* @property {boolean} isDotnet
|
|
10
10
|
* @property {boolean} isNextjs
|
|
11
11
|
* @property {string[]} defaultValidators
|
|
12
|
-
* @property {string[]} checkpointValidators
|
|
13
12
|
* @property {string|null} buildCommand
|
|
14
13
|
*/
|
|
15
14
|
|
|
@@ -21,19 +20,16 @@ const PROFILES = {
|
|
|
21
20
|
dotnet: {
|
|
22
21
|
isDotnet: true, isNextjs: false,
|
|
23
22
|
defaultValidators: ['architecture', 'packages'],
|
|
24
|
-
checkpointValidators: ['architecture', 'packages', 'design-system', 'security'],
|
|
25
23
|
buildCommand: 'dotnet build --no-restore',
|
|
26
24
|
},
|
|
27
25
|
nextjs: {
|
|
28
26
|
isDotnet: false, isNextjs: true,
|
|
29
27
|
defaultValidators: ['nextjs-component'],
|
|
30
|
-
checkpointValidators: ['nextjs-component', 'css'],
|
|
31
28
|
buildCommand: 'npx tsc --noEmit',
|
|
32
29
|
},
|
|
33
30
|
empty: {
|
|
34
31
|
isDotnet: false, isNextjs: false,
|
|
35
32
|
defaultValidators: [],
|
|
36
|
-
checkpointValidators: [],
|
|
37
33
|
buildCommand: null,
|
|
38
34
|
},
|
|
39
35
|
};
|
|
@@ -57,7 +53,6 @@ function mergeProfiles(profiles) {
|
|
|
57
53
|
isDotnet: profiles.some((p) => p.isDotnet),
|
|
58
54
|
isNextjs: profiles.some((p) => p.isNextjs),
|
|
59
55
|
defaultValidators: [...new Set(profiles.flatMap((p) => p.defaultValidators))],
|
|
60
|
-
checkpointValidators: [...new Set(profiles.flatMap((p) => p.checkpointValidators))],
|
|
61
56
|
buildCommand: profiles.map((p) => p.buildCommand).find(Boolean) ?? null,
|
|
62
57
|
};
|
|
63
58
|
}
|
|
@@ -28,8 +28,34 @@ const EVENT_HANDLER_PATTERNS = [
|
|
|
28
28
|
/\bonFocus\b/,
|
|
29
29
|
/\bonBlur\b/,
|
|
30
30
|
/\bonKeyDown\b/,
|
|
31
|
+
// Qualquer outro handler de evento em JSX (onMouseEnter, onKeyUp, onScroll, onDrop…).
|
|
32
|
+
// Sem isto, a lista fechada acima transforma interatividade legítima em falso-positivo.
|
|
33
|
+
/\bon[A-Z]\w*\s*=\s*\{/,
|
|
31
34
|
];
|
|
32
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Chamada de QUALQUER hook, não só os sete nativos: `useRouter()`, `useQuery()`, `useForm<T>()`.
|
|
38
|
+
* O `[<(]` é o que faz `useState<Record<string, string>>(` contar — um hook com argumento de
|
|
39
|
+
* tipo não casa com `useState\s*\(` e passava despercebido.
|
|
40
|
+
*/
|
|
41
|
+
const HOOK_CALL_PATTERN = /\buse[A-Z]\w*\s*[<(]/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* APIs de React que só existem no cliente. `forwardRef` e `createContext` lançam em Server
|
|
45
|
+
* Component, então um módulo que os usa PRECISA da diretiva, mesmo sem hook nem handler.
|
|
46
|
+
*/
|
|
47
|
+
const CLIENT_ONLY_APIS = [/\bforwardRef\b/, /\bcreateContext\b/];
|
|
48
|
+
|
|
49
|
+
/** Globais de browser: quem os toca não roda no servidor. */
|
|
50
|
+
const BROWSER_GLOBALS = /\b(window|document|localStorage|sessionStorage|navigator)\s*[.[]/;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pacotes cujos componentes são client-only. O caso que mais gerava ruído é o wrapper de
|
|
54
|
+
* shadcn/Radix: ele só reexporta primitivas, então não tem hook nem handler no próprio
|
|
55
|
+
* arquivo — e era acusado para sempre, apesar de estar correto.
|
|
56
|
+
*/
|
|
57
|
+
const CLIENT_ONLY_IMPORTS = /from\s+['"](@radix-ui\/|sonner|@react-pdf\/|file-saver|framer-motion|motion\/|recharts|next\/navigation|react-hook-form|@tanstack\/react-query|embla-carousel|cmdk|vaul|react-day-picker|input-otp|@dnd-kit\/)/;
|
|
58
|
+
|
|
33
59
|
/** Next.js special files exempt from naming checks */
|
|
34
60
|
const NEXTJS_SPECIAL_FILES = ['page.tsx', 'layout.tsx', 'loading.tsx', 'error.tsx', 'not-found.tsx', 'route.ts', 'middleware.ts'];
|
|
35
61
|
|
|
@@ -75,7 +101,17 @@ export function validateNextComponent(content, filePath) {
|
|
|
75
101
|
if (hasUseClient) {
|
|
76
102
|
const hasEventHandlers = EVENT_HANDLER_PATTERNS.some(pattern => pattern.test(content));
|
|
77
103
|
const hasClientHooks = CLIENT_HOOKS.some(hook => new RegExp(`\\b${hook}\\b`).test(content));
|
|
78
|
-
|
|
104
|
+
// Só acusa quando NENHUM sinal de necessidade de cliente aparece. Um falso-positivo aqui
|
|
105
|
+
// pede a remoção de uma diretiva que o build precisa, então a checagem erra para o lado
|
|
106
|
+
// de ficar calada.
|
|
107
|
+
const needsClient =
|
|
108
|
+
hasEventHandlers ||
|
|
109
|
+
hasClientHooks ||
|
|
110
|
+
HOOK_CALL_PATTERN.test(content) ||
|
|
111
|
+
CLIENT_ONLY_APIS.some(pattern => pattern.test(content)) ||
|
|
112
|
+
BROWSER_GLOBALS.test(content) ||
|
|
113
|
+
CLIENT_ONLY_IMPORTS.test(content);
|
|
114
|
+
if (!needsClient) {
|
|
79
115
|
issues.push({
|
|
80
116
|
type: 'warning',
|
|
81
117
|
message: "'use client' directive present but no interactivity detected (no hooks, no event handlers)",
|
|
@@ -16,7 +16,11 @@ import { aggregateScores, loadRubric } from './score-aggregator.js';
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Heavy validators skipped in 'light' mode (checkpoint-only).
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* What light mode actually leaves behind is whatever the stack contributes that
|
|
21
|
+
* is NOT in this set — in practice `contrast` and `nextjs-component`. There are
|
|
22
|
+
* no `spec` or `content` validators; the switch in runSingleValidator has no
|
|
23
|
+
* case for either, and the old comment here promised both.
|
|
20
24
|
*/
|
|
21
25
|
const HEAVY_VALIDATORS = new Set([
|
|
22
26
|
'architecture', 'packages', 'contract-compliance',
|
|
@@ -49,27 +53,38 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
49
53
|
validators: {} // No validator-specific constraints
|
|
50
54
|
};
|
|
51
55
|
|
|
52
|
-
// Determine which validators to run
|
|
53
|
-
|
|
56
|
+
// Determine which validators to run.
|
|
57
|
+
// COPY: the mode filter below used to splice in place, and when the caller
|
|
58
|
+
// passed `options.validators` that array WAS this array — so a light run
|
|
59
|
+
// emptied the caller's own list, and running the same list twice validated
|
|
60
|
+
// less the second time.
|
|
61
|
+
const detected = options.validators || detectValidators(projectPath);
|
|
62
|
+
let validatorIds = [...detected];
|
|
54
63
|
|
|
55
64
|
// Filter by mode: 'light' skips heavy validators, 'full' (default) runs all
|
|
56
65
|
const mode = options.mode || 'full';
|
|
66
|
+
const droppedByMode = [];
|
|
57
67
|
if (mode === 'light') {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
}
|
|
68
|
+
validatorIds = validatorIds.filter(id => {
|
|
69
|
+
if (HEAVY_VALIDATORS.has(id)) { droppedByMode.push(id); return false; }
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
// Merge constraint-enabled validators
|
|
67
75
|
validatorIds.push(...constraints.enabledValidators);
|
|
68
76
|
|
|
69
77
|
if (validatorIds.length === 0) {
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
// Say which of the two it is. On a pure `dotnet` project every default
|
|
79
|
+
// validator is heavy, so a light run legitimately runs none — but the old
|
|
80
|
+
// message blamed the stack ("no validators configured"), which reads as
|
|
81
|
+
// "your stack has nothing to check" when the full run has two.
|
|
82
|
+
result.summary = droppedByMode.length > 0
|
|
83
|
+
? `No validators ran — light mode skipped ${droppedByMode.length} heavy validator(s): ${droppedByMode.join(', ')}`
|
|
84
|
+
: 'No validators ran — no validators configured for this stack';
|
|
85
|
+
console.log(chalk.gray(`\n ℹ️ ${result.summary.replace('No validators ran — ', '')}`));
|
|
72
86
|
result._skipReason = result.summary;
|
|
87
|
+
result._droppedByMode = droppedByMode;
|
|
73
88
|
return result;
|
|
74
89
|
}
|
|
75
90
|
|
|
@@ -87,7 +102,12 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
87
102
|
// Positive check on isDotnet (contract-compliance is a VSA/.NET concern) —
|
|
88
103
|
// NOT `!isNextjs`, which used to wrongly suppress it for a compound
|
|
89
104
|
// "dotnet-nextjs" stack that has a .NET backend but is ALSO isNextjs.
|
|
90
|
-
|
|
105
|
+
// `mode: 'light'` has to hold here too. This push sits AFTER the mode
|
|
106
|
+
// filter, so contract-compliance — which is in HEAVY_VALIDATORS — used to
|
|
107
|
+
// slip into every light run that had a contracts file, making light mode
|
|
108
|
+
// heavier than declared on exactly the compound stacks it matters for.
|
|
109
|
+
const contractsIsHeavyAndSkipped = mode === 'light' && HEAVY_VALIDATORS.has('contract-compliance');
|
|
110
|
+
if (existsSync(contractsPath) && _profileForContracts.isDotnet && !contractsIsHeavyAndSkipped) {
|
|
91
111
|
validatorIds.push('contract-compliance');
|
|
92
112
|
}
|
|
93
113
|
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Content Validator - Validates structure and content of output files
|
|
5
|
-
*
|
|
6
|
-
* Ensures spec.md, tasks.json, and other outputs have required sections
|
|
7
|
-
* and proper structure before allowing phase transitions.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Validate spec.md structure
|
|
12
|
-
* @param {string} specPath - Path to spec.md file
|
|
13
|
-
* @returns {Object} Validation result
|
|
14
|
-
*/
|
|
15
|
-
export function validateSpecContent(specPath) {
|
|
16
|
-
if (!existsSync(specPath)) {
|
|
17
|
-
return {
|
|
18
|
-
valid: false,
|
|
19
|
-
missing: ['File does not exist'],
|
|
20
|
-
errors: ['Spec file not found at: ' + specPath]
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const content = readFileSync(specPath, 'utf8');
|
|
25
|
-
|
|
26
|
-
// Required sections (flexible regex handles numbered variants like '## 1. Overview')
|
|
27
|
-
const requiredSections = [
|
|
28
|
-
{ label: '## Overview', pattern: /^##\s+(?:\d+\.?\s+)?Overview/i },
|
|
29
|
-
{ label: '## Functional Requirements', pattern: /^##\s+(?:\d+\.?\s+)?Functional Requirements/i },
|
|
30
|
-
{ label: '## Technical', pattern: /^##\s+(?:\d+\.?\s+)?Technical/i },
|
|
31
|
-
{ label: '## Data Model', pattern: /^##\s+(?:\d+\.?\s+)?Data Model/i },
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
const lines = content.split('\n');
|
|
35
|
-
const missing = requiredSections.filter(s =>
|
|
36
|
-
!lines.some(line => s.pattern.test(line))
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
// Additional quality checks
|
|
40
|
-
const errors = [];
|
|
41
|
-
const warnings = [];
|
|
42
|
-
|
|
43
|
-
// Missing sections are warnings, not errors — not every spec needs all sections
|
|
44
|
-
// (e.g., frontend-only features may not have Data Model, CLI tools may not have Technical Architecture)
|
|
45
|
-
if (missing.length > 0) {
|
|
46
|
-
missing.forEach(s => {
|
|
47
|
-
warnings.push(`Missing recommended section: ${s.label}`);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Check minimum content length
|
|
52
|
-
if (content.length < 500) {
|
|
53
|
-
warnings.push('Spec seems very short (< 500 characters). Consider adding more detail.');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check for placeholder text that should be replaced
|
|
57
|
-
const placeholders = ['TODO', 'TBD', 'PLACEHOLDER', '{{'];
|
|
58
|
-
placeholders.forEach(placeholder => {
|
|
59
|
-
if (content.includes(placeholder)) {
|
|
60
|
-
warnings.push(`Spec contains "${placeholder}" - ensure all placeholders are replaced`);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// Check for anti-patterns mentioned in the plan
|
|
65
|
-
if (content.toLowerCase().includes('manual azure portal')) {
|
|
66
|
-
errors.push(
|
|
67
|
-
'Spec mentions manual Azure portal creation. ' +
|
|
68
|
-
'All infrastructure must be defined in Bicep (Infrastructure as Code).'
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (content.toLowerCase().includes('create resource manually')) {
|
|
73
|
-
errors.push(
|
|
74
|
-
'Spec mentions manual resource creation. ' +
|
|
75
|
-
'Use IaC (Bicep) for all infrastructure resources.'
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
valid: errors.length === 0,
|
|
81
|
-
missing,
|
|
82
|
-
errors,
|
|
83
|
-
warnings,
|
|
84
|
-
sections: {
|
|
85
|
-
required: requiredSections.length,
|
|
86
|
-
found: requiredSections.length - missing.length
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
}
|