@polymorphism-tech/morph-spec 10.6.0 → 10.7.1
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/bin/morph-spec.js +11 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/docs/CLI.md +2 -2
- package/framework/hooks/shared/phase-utils.js +0 -2
- package/framework/hooks/statusline.js +3 -1
- package/framework/rules/plan-grammar.md +6 -2
- 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 +2 -2
- package/framework/skills/core/morph-plan/SKILL.md +5 -4
- package/framework/skills/core/morph-uiux/SKILL.md +2 -2
- package/framework/templates/docs/plan.md +3 -3
- package/package.json +1 -1
- package/src/commands/state/recap.js +6 -5
- package/src/commands/validation/plan-check.js +4 -7
- package/src/core/paths/output-schema.js +3 -1
- package/src/lib/plan/checks.js +6 -6
- package/src/lib/plan/plan-parser.js +3 -0
- package/src/lib/recap/recap.js +6 -5
- package/src/lib/verify/detached.js +3 -0
- package/src/lib/view/feature-html.js +307 -0
- package/src/lib/view/feature-page.client.js +116 -0
- package/src/lib/view/feature-page.css +169 -0
- package/src/lib/view/feature-view.js +104 -0
- package/src/lib/view/flow-svg.js +63 -0
- package/src/lib/view/tokens.css +18 -0
- package/src/lib/plan/plan-html.js +0 -125
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:** 10.
|
|
6
|
+
**Version:** 10.7.1
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -376,4 +376,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
376
376
|
|
|
377
377
|
---
|
|
378
378
|
|
|
379
|
-
*morph-spec v10.
|
|
379
|
+
*morph-spec v10.7.1 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -580,4 +580,15 @@ program
|
|
|
580
580
|
.option('--json', 'Output the aggregate as JSON')
|
|
581
581
|
.action((feature, options) => costCommand(feature, options));
|
|
582
582
|
|
|
583
|
+
// The page follows every command that moves the feature; 'exit' also covers their process.exit().
|
|
584
|
+
const VIEW_COMMANDS = new Set(['plan-check', 'gate-check', 'gate-decision', 'approve', 'unapprove', 'advance', 'verify', 'report-check']);
|
|
585
|
+
program.hook('preAction', async (_, action) => {
|
|
586
|
+
const [feature] = action.args;
|
|
587
|
+
if (!VIEW_COMMANDS.has(action.name()) || !/^[a-z0-9][\w-]*$/i.test(feature ?? '')) return;
|
|
588
|
+
const { refreshesFeatureView } = await import('../src/lib/verify/detached.js');
|
|
589
|
+
if (!refreshesFeatureView()) return;
|
|
590
|
+
const { refreshFeatureView } = await import('../src/lib/view/feature-view.js');
|
|
591
|
+
process.on('exit', () => refreshFeatureView(process.cwd(), feature));
|
|
592
|
+
});
|
|
593
|
+
|
|
583
594
|
await program.parseAsync();
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/docs/CLI.md
CHANGED
|
@@ -149,7 +149,7 @@ Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed:
|
|
|
149
149
|
|
|
150
150
|
### archive
|
|
151
151
|
|
|
152
|
-
**`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama. O arquivo guarda só `feature.json`, `2-plan/plan.md` e o recap (`
|
|
152
|
+
**`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama. O arquivo guarda só `feature.json`, `2-plan/plan.md` e a página da feature com o recap dentro (`feature.html` + `recap.json`, gerados antes da poda, enquanto os relatórios do Gate 3 ainda existem): o plano sobrevive até a um squash merge, e `feature.json.archivedFrom.sha` é o topo da branch antes da poda (`git show <sha>:.morph/features/<f>/3-implement/report.md`; num squash, o `(#N)` do commit leva ao PR e a `git fetch origin pull/N/head`).
|
|
153
153
|
|
|
154
154
|
### recap
|
|
155
155
|
|
|
@@ -530,7 +530,7 @@ A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhu
|
|
|
530
530
|
|
|
531
531
|
### plan-check
|
|
532
532
|
|
|
533
|
-
**`morph-spec plan-check <feature> [--json]`** — lê `2-plan/plan.md` com o parser único (o `morph-spec create` já escreve o esqueleto; a gramática completa sai em `morph-spec plan-check --help`) e, sem erro, grava `2-plan/checks.json` — o único escritor dele (o `protect-spec-files` recusa Write/Edit à mão) — e `
|
|
533
|
+
**`morph-spec plan-check <feature> [--json]`** — lê `2-plan/plan.md` com o parser único (o `morph-spec create` já escreve o esqueleto; a gramática completa sai em `morph-spec plan-check --help`) e, sem erro, grava `2-plan/checks.json` — o único escritor dele (o `protect-spec-files` recusa Write/Edit à mão) — e abre a vida de `feature.html`, a página única da feature (um arquivo só, na raiz dela): o que precisa de decisão, cenários, decisões ligadas aos cenários que citam, gráficos de prova, risco e reuso e, com `## Fluxo` e `## Estados` (opcionais, `- origem → destino: gatilho` por linha), os diagramas. Todo `plan-check`, `gate-check`, `gate-decision`, `approve`, `unapprove`, `advance`, `verify` e `report-check` reescreve o mesmo arquivo a partir do disco ao sair, sem custo de token; o `archive` a congela com o recap dentro. **Erros** (exit 1, nada gravado): seção obrigatória ausente (`Problema`, `Cenários`, `Decisões` com `### Conflitos com o acervo` não vazio — "nenhum" vale); `## Errata` fora do fim; `## Cenários` sem cenário; cenário sem `prova:` ou fora de `teste|e2e|eval|manual|leitura`; `prova: teste` sem `risco:` conhecido; `risco:` com `prova: leitura`; cenário com vocabulário de artefato sem `> Por que artefato:` logo abaixo; id `C`/`ADR` duplicado; `bug`/`hotfix` sem cenário de regressão provado por `teste` ou `e2e`; plano que fala em mídia gerada por IA sem citar `ai-agents-modalities-image-gen`, `ai-agents-media-video` ou `ai-agents-media-pipeline` nem declarar a linha `Mídia gerada: não` (sem a citação, as provas de mídia do Gate 3 ficariam desligadas em silêncio); veredito de reuso fora de `REUSAR|ESTENDER|CRIAR`; `## Estados` vazia ou com linha que não é transição. **Avisos:** `CRIAR` sem `why`, ADR sem `**Alcance:** feature|projeto`, `## Fatias` que sobrou da 9.x (não é lida). Com o gate `plan` aprovado e o corpo mudado fora da `## Errata`, recusa (`frozen`, exit 1) — o `planHash` ignora a errata; mudança de escopo é `unapprove <f> plan`, `plan-check` e o gate de novo. Plano extraído antes das regras de `risco:` e de mídia não passa a falhar por elas. Exit 2: `plan.md` ausente. Fora da árvore dona da feature, recusa nomeando-a. Com o plano válido, acrescenta ao `config.json` a stack que os arquivos mostram e instala em `.claude/rules/` a regra de stack que falta (uma linha por regra, em stderr; `rulesAdded` no `--json`).
|
|
534
534
|
|
|
535
535
|
### stack-set
|
|
536
536
|
|
|
@@ -13,7 +13,6 @@ export const OUTPUT_PHASE_MAP = {
|
|
|
13
13
|
plan: 'plan',
|
|
14
14
|
checks: 'plan',
|
|
15
15
|
planCritique: 'plan',
|
|
16
|
-
planView: 'plan',
|
|
17
16
|
uiMockups: 'uiux',
|
|
18
17
|
implementReport: 'implement',
|
|
19
18
|
evidencias: 'implement',
|
|
@@ -26,7 +25,6 @@ export const FILENAME_TO_OUTPUT = {
|
|
|
26
25
|
'plan.md': 'plan',
|
|
27
26
|
'checks.json': 'checks',
|
|
28
27
|
'critica.md': 'planCritique',
|
|
29
|
-
'plan.html': 'planView',
|
|
30
28
|
'mockups.md': 'uiMockups',
|
|
31
29
|
'report.md': 'implementReport',
|
|
32
30
|
'verifier.md': 'verifierReport',
|
|
@@ -110,7 +110,9 @@ function readUserSettings(env = process.env) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function git(args, cwd) {
|
|
113
|
-
|
|
113
|
+
// --no-optional-locks: status/diff don't write the index, so they never take .git/index.lock
|
|
114
|
+
// nor leave it stale when the 3s timeout kills git on a large repo
|
|
115
|
+
return execFileSync('git', ['--no-optional-locks', ...args], { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, windowsHide: true }).toString('utf8').trim();
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
// `-uno`: porcelain v2 lists untracked entries by default, which would change what the file count means.
|
|
@@ -41,7 +41,11 @@ Todo valor de baixo risco que você decidiu sem perguntar (limite, intervalo, re
|
|
|
41
41
|
|
|
42
42
|
## Estados (opcional)
|
|
43
43
|
|
|
44
|
-
Só quando a feature tem algo que muda de estado (pedido, aprovação, assinatura): uma transição por linha, `- origem → destino: gatilho` (`->` também vale).
|
|
44
|
+
Só quando a feature tem algo que muda de estado (pedido, aprovação, assinatura): uma transição por linha, `- origem → destino: gatilho` (`->` também vale). A página da feature desenha a máquina; linha que não é transição, ou seção vazia, é erro. Sem estado, não escreva a seção.
|
|
45
|
+
|
|
46
|
+
## Fluxo (opcional)
|
|
47
|
+
|
|
48
|
+
O caminho de um pedido ou dado pelo sistema, na mesma forma e com as mesmas recusas de `## Estados`: `- origem → destino: o que passa`. A página desenha o diagrama.
|
|
45
49
|
|
|
46
50
|
## Reuso
|
|
47
51
|
|
|
@@ -49,7 +53,7 @@ Só quando a feature tem algo que muda de estado (pedido, aprovação, assinatur
|
|
|
49
53
|
|
|
50
54
|
## Decisões
|
|
51
55
|
|
|
52
|
-
`### ADR-001 — título` por decisão não trivial (alternativa descartada, motivo, quem decidiu) com `**Alcance:** feature` ou `projeto
|
|
56
|
+
`### ADR-001 — título` por decisão não trivial (alternativa descartada, motivo, quem decidiu) com `**Alcance:** feature` ou `projeto`, citando os `Cn` que sustenta: a citação vira link na página da feature. Greenfield: decisão que fixa stack, layout, infraestrutura ou test runner é `projeto`. `### Conflitos com o acervo` sempre (`nenhum` vale). Proibição além das do standard `architecture-stack-baseline` é ADR.
|
|
53
57
|
|
|
54
58
|
## O que o plan-check recusa
|
|
55
59
|
|
|
@@ -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_use: Use quando for iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.7.1"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -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_use: Use quando o usuário pedir "use /migrate-lp-to-umbraco {projeto}" ou descrever a necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.7.1"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -4,7 +4,7 @@ description: Implementa o plano aprovado de uma feature morph-spec inteiro nesta
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use com o gate do plano aprovado ("/morph-apply {feature}", "implementar feature"), em "/morph-review {feature}" (entra no §5), em "/morph-hotfix {feature}" ou incidente de produção (references/hotfix.md), ou quando `morph-spec next` mandar implement, verify, fix, review ou finish
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.7.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -58,7 +58,7 @@ Verify verde: siga `references/review.md` — foto `--before-eval`, verificador
|
|
|
58
58
|
|
|
59
59
|
## 6. Fechar
|
|
60
60
|
|
|
61
|
-
`morph-spec context refresh` (regera o inventário em `.morph/context/README.md`; `design-system.md` você atualiza só se mudou token) e commite o que faltar (relatórios do review): o `finish` exige árvore limpa. Modo: `finish.mode` do config do projeto, padrão `pr`, sem perguntar: `morph-spec finish {feature} --pr|--merge`. `--pr` não integra: a feature fica `aguardando-merge` até o merge. Stamp com `pendingPr`: `--pr` em `run_in_background`, porque ele espera os checks da PR; `pr-checks-fail` ou `pr-checks-no-verdict` desfaz o archive e reabre o Gate 3: siga o `hint`. O `finish` arquiva o `feature.json`, o `plan.md` e o recap; o report e a review ficam no histórico do git. Encerrado, aponte o recap ao usuário (`.morph/archive/{feature}/
|
|
61
|
+
`morph-spec context refresh` (regera o inventário em `.morph/context/README.md`; `design-system.md` você atualiza só se mudou token) e commite o que faltar (relatórios do review): o `finish` exige árvore limpa. Modo: `finish.mode` do config do projeto, padrão `pr`, sem perguntar: `morph-spec finish {feature} --pr|--merge`. `--pr` não integra: a feature fica `aguardando-merge` até o merge. Stamp com `pendingPr`: `--pr` em `run_in_background`, porque ele espera os checks da PR; `pr-checks-fail` ou `pr-checks-no-verdict` desfaz o archive e reabre o Gate 3: siga o `hint`. O `finish` arquiva o `feature.json`, o `plan.md` e o recap; o report e a review ficam no histórico do git. Encerrado, aponte o recap ao usuário (`.morph/archive/{feature}/feature.html#recap`, abra no navegador; `morph-spec recap {feature} --piper` narra com o Piper local).
|
|
62
62
|
|
|
63
63
|
## Arquivo de teste
|
|
64
64
|
|
|
@@ -4,7 +4,7 @@ description: Leva um pedido ou o link de uma issue ao plano aprovado — roteia
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando uma feature, bug ou chore começar ("/morph-plan {feature}", "/morph-plan <link da issue>", "vamos planejar X"), ou ao retomar uma feature cujo gate plan ainda não foi aprovado
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.7.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -54,9 +54,10 @@ O `create` escreveu o esqueleto; a gramática completa chega pela rule `plan-gra
|
|
|
54
54
|
- **`## Cenários` é a definição de pronto.** Cada `Cn` é um comportamento observável que o verificador do Gate 3 vai **executar** contra a app — escreva-o como um fluxo de usuário ou uma regra que dá para disparar. `prova:` `teste` (com `risco:`) | `e2e` | `eval` | `manual` | `leitura`. Sem risco, `leitura`.
|
|
55
55
|
- **`degrau:`** quando o `Cn` não se desfaz com revert: `migrar` (toda migration, aditiva ou não: drop, rename, backfill, coluna nova) ou `irreversível` (apaga dado que alguém lê, quebra contrato externo). Sem a linha, `refazer`. Esses dois degraus pausam o gate do plano para o usuário, e o Gate 3 pausa em migration sem degrau, em arquivo de contrato (`openapi`, `.proto`) sem `irreversível`, e no bloqueador `irreversivel` do revisor.
|
|
56
56
|
- **`## Padrões assumidos`**: todo valor de baixo risco que você decidiu sem perguntar, numerado `1.`…`N.`, com o valor. É o que tira a pergunta trivial da entrevista: o usuário veta por número. Auth, credencial, permissão, pagamento e apagar dado nunca entram: são pergunta.
|
|
57
|
-
- **`## Estados`** (opcional): algo muda de estado (pedido, aprovação) → `- origem → destino: gatilho`, uma por linha;
|
|
57
|
+
- **`## Estados`** (opcional): algo muda de estado (pedido, aprovação) → `- origem → destino: gatilho`, uma por linha; a página da feature desenha a máquina.
|
|
58
|
+
- **`## Fluxo`** (opcional): o caminho de um pedido ou dado pelo sistema, na mesma forma; vira o desenho que o usuário olha em vez de ler `## Arquitetura`. Escreva-o sempre que houver mais de duas peças conversando.
|
|
58
59
|
- **`## Reuso`**: `REUSAR` | `ESTENDER` | `CRIAR`; `CRIAR` exige `why` (a consulta que respondeu "não existe").
|
|
59
|
-
- **`## Decisões`**: ADR por decisão não trivial, `**Alcance:** feature|projeto
|
|
60
|
+
- **`## Decisões`**: ADR por decisão não trivial, `**Alcance:** feature|projeto`, citando os `Cn` que sustenta ("Sustenta C1, C4"): a citação vira o link e a matriz decisão × cenário da página; `### Conflitos com o acervo` sempre. Serviço novo de dados ou plataforma (cache, fila, vetor, busca, grafo, banco de documento, arquivo, gateway de LLM): o ADR diz por que o Postgres e o Neon do projeto não cobrem (`morph-spec standards --show architecture-stack-baseline#postgres-primeiro`).
|
|
60
61
|
- **`## Arquitetura`**: onde entra, o que conversa com o quê, edge cases técnicos. Gera imagem ou vídeo por IA → cite aqui o standard (`ai-agents-modalities-image-gen`, `ai-agents-media-video`, `ai-agents-media-pipeline`): é a citação que arma as provas de mídia do Gate 3, e o `plan-check` recusa o plano que fala em mídia gerada sem ela (falso positivo: a linha `Mídia gerada: não`).
|
|
61
62
|
|
|
62
63
|
**Contrato de implementação**: o que quem implementa precisa e o código não diz.
|
|
@@ -77,7 +78,7 @@ Antes do gate, em `feature` e `bug`: a crítica em contexto fresco de `reference
|
|
|
77
78
|
`morph-spec gate-check {feature} plan [--json]`. Entrevista que zerou a árvore já é a aprovação do usuário: não repause, salvo a pausa de `signal: "rung"`, que vence e pergunta nomeando cada `Cn` e o seu degrau. Encerrada antes, com nós em `## Fora de escopo`: `morph-spec gate-decision {feature} plan --pause true --reason "entrevista encerrada com nós abertos" --signals '{"discoveryMode":"interview"}'`, e a pergunta do gate lista esses nós.
|
|
78
79
|
|
|
79
80
|
- `resolver === "session"` (`plan-check` com erro, crítica ausente ou de outra versão) → corrija o plano ou despache a crítica, e rode `plan-check` e `gate-check` de novo, sem perguntar.
|
|
80
|
-
- `resolver === "human"` → abra `
|
|
81
|
+
- `resolver === "human"` → abra `feature.html`, na raiz da feature, no navegador do usuário (`start`/`open`/`xdg-open`; a CLI a mantém, nunca escreva HTML de plano à mão; o usuário pode colar a resposta que ela monta), o entendimento (problema, fora de escopo, cenários, riscos) e `AskUserQuestion`, header "Gate plano": `"Gate do plano — Plano de {feature} aprovado?"` com `"aprovado, prosseguir" | "preciso ajustar" | "cancelar feature"`; a pergunta lista `## Padrões assumidos` para veto por número; só a decisão sensível que você tomou sozinho vira pergunta antes, na forma de `references/interview.md` (`## Forma da pergunta`). Headless: `morph-spec approve {feature} plan --approver <quem>`.
|
|
81
82
|
- `pause === false` → uma linha e `morph-spec advance {feature}` (ou `--design`).
|
|
82
83
|
|
|
83
84
|
Aprovado: "Plano aprovado.", com `## Padrões assumidos` numerado, e siga direto com `Skill(morph-apply)`, sem esperar resposta. Veto, a qualquer momento: `## Errata` com "Padrão N vetado: <valor novo>"; a implementação segue a errata. Rodou `stack-set` nesta sessão: pare em "Plano aprovado. Rode `/morph-apply {feature}` numa sessão nova", porque as rules de domínio só carregam no início de uma sessão.
|
|
@@ -4,14 +4,14 @@ description: Conduz a fase de UI/UX de uma feature com plano aprovado — render
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando o ramo de design for tomado (morph-spec advance {feature} --design), antes de /morph-apply
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.7.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-uiux
|
|
11
11
|
|
|
12
12
|
Entra depois do gate do plano, quando a feature cria ou altera UI. Sai com `1-design/` aprovado; próximo passo `/morph-apply {feature}`. Você desenha nesta sessão — o que decidir aqui é o que vai implementar depois.
|
|
13
13
|
|
|
14
|
-
A régua de sistema e de estética chega sozinha: a rule `ui-design` carrega quando você abre um arquivo de `1-design/` ou de UI.
|
|
14
|
+
A régua de sistema e de estética chega sozinha: a rule `ui-design` carrega quando você abre um arquivo de `1-design/` ou de UI. Antes do primeiro HTML, carregue `frontend-design:frontend-design` e, para gráfico ou indicador, `dataviz`, se a sessão os tiver. As telas aparecem sozinhas na página da feature (`feature.html`), com link e print.
|
|
15
15
|
|
|
16
16
|
## 1. Insumos
|
|
17
17
|
|
|
@@ -24,8 +24,8 @@ Resultado buscado: <o estado depois: o que muda, para quem, como se percebe.>
|
|
|
24
24
|
|
|
25
25
|
1. <padrão e o valor escolhido>
|
|
26
26
|
|
|
27
|
-
<!--
|
|
28
|
-
"- origem → destino: gatilho".
|
|
27
|
+
<!-- Opcionais, viram desenho em feature.html: "Fluxo" (h2), o caminho pelo sistema, e "Estados" (h2), se algo muda de
|
|
28
|
+
estado. Uma transição por linha: "- origem → destino: gatilho". -->
|
|
29
29
|
|
|
30
30
|
## Cenários
|
|
31
31
|
|
|
@@ -43,7 +43,7 @@ Resultado buscado: <o estado depois: o que muda, para quem, como se percebe.>
|
|
|
43
43
|
### ADR-001 — <título>
|
|
44
44
|
**Alcance:** feature
|
|
45
45
|
|
|
46
|
-
<Contexto, decisão e consequência. `projeto` quando a decisão vale além desta feature.>
|
|
46
|
+
<Contexto, decisão e consequência, citando os Cn que ela sustenta. `projeto` quando a decisão vale além desta feature.>
|
|
47
47
|
|
|
48
48
|
### Conflitos com o acervo
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -3,17 +3,18 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { buildRecap, changeSummary, recapScenes, renderRecapHtml } from '../../lib/recap/recap.js';
|
|
6
|
+
import { writeFeatureView } from '../../lib/view/feature-view.js';
|
|
7
|
+
import { FEATURE_VIEW_FILE } from '../../core/paths/output-schema.js';
|
|
6
8
|
import { narrateWithPiper, resolvePiper } from '../../lib/recap/piper.js';
|
|
7
9
|
|
|
8
|
-
export const RECAP_FILES = { html:
|
|
10
|
+
export const RECAP_FILES = { html: FEATURE_VIEW_FILE, data: 'recap.json' };
|
|
9
11
|
|
|
10
|
-
/** Writes `recap.
|
|
12
|
+
/** Writes `recap.json` and the page with the recap inside into `targetDir`, from the live feature folder. Never throws. */
|
|
11
13
|
export function writeRecap(featureDir, targetDir, feature, { cwd }) {
|
|
12
14
|
try {
|
|
13
15
|
const data = buildRecap(featureDir, feature, { changes: changeSummary(cwd) });
|
|
14
16
|
writeFileSync(join(targetDir, RECAP_FILES.data), JSON.stringify(data, null, 2));
|
|
15
|
-
|
|
16
|
-
return { ok: true, path: join(targetDir, RECAP_FILES.html) };
|
|
17
|
+
return writeFeatureView(targetDir, feature, { changes: data.changes, archived: true });
|
|
17
18
|
} catch (err) {
|
|
18
19
|
return { ok: false, reason: err.message };
|
|
19
20
|
}
|
|
@@ -56,7 +57,7 @@ export function recapCommand(feature, options = {}) {
|
|
|
56
57
|
note = piper.ok ? 'o piper falhou numa cena — ficou a voz do sistema' : piper.reason;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
if (narration === 'sistema')
|
|
60
|
+
if (narration === 'sistema') writeFeatureView(dir, feature, { changes: data.changes ?? null, archived: dir === archived });
|
|
60
61
|
|
|
61
62
|
if (options.json) {
|
|
62
63
|
console.log(JSON.stringify({ ok: true, path, narration, note }));
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, readFileSync
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
5
5
|
import { isGateApproved } from '../../../framework/hooks/shared/state-reader.js';
|
|
6
|
-
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
6
|
+
import { FEATURE_VIEW_FILE, getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
7
7
|
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
8
|
-
import { renderPlanHtml } from '../../lib/plan/plan-html.js';
|
|
9
8
|
import { buildChecks, planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
|
|
10
9
|
import { revocationWarnings } from '../../lib/specs/adr-index.js';
|
|
11
10
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
@@ -39,9 +38,7 @@ export function runPlanCheck(featureName, cwd = process.cwd()) {
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
atomicWriteJson(checksPath, buildChecks(featureName, text, plan));
|
|
42
|
-
|
|
43
|
-
writeFileSync(viewPath, renderPlanHtml(featureName, plan, { planHash: hash }));
|
|
44
|
-
return { ...result, ok: true, exit: 0, written: true, viewPath };
|
|
41
|
+
return { ...result, ok: true, exit: 0, written: true, viewPath: join(cwd, '.morph', 'features', featureName, FEATURE_VIEW_FILE) };
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
export function planCheckCommand(featureName, options = {}) {
|
|
@@ -82,7 +79,7 @@ export function planCheckCommand(featureName, options = {}) {
|
|
|
82
79
|
console.log(chalk.red(`✗ plan-check: ${result.errors.length} erro(s) em ${result.plan} — checks.json não foi gravado`));
|
|
83
80
|
} else {
|
|
84
81
|
console.log(chalk.green(`✓ plan-check: ${join('2-plan', 'checks.json')} gravado (${result.planHash.slice(0, 12)})`));
|
|
85
|
-
console.log(chalk.gray(`
|
|
82
|
+
console.log(chalk.gray(` página da feature: ${result.viewPath}`));
|
|
86
83
|
reportDotnetIgnores(gitignoreAdded);
|
|
87
84
|
}
|
|
88
85
|
process.exit(result.exit);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
|
|
3
|
+
/** The one page of a feature, at its root: it belongs to no phase, and only the CLI writes it. */
|
|
4
|
+
export const FEATURE_VIEW_FILE = 'feature.html';
|
|
5
|
+
|
|
3
6
|
export const OUTPUT_SCHEMA = {
|
|
4
7
|
plan: { filename: 'plan.md', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
5
8
|
checks: { filename: 'checks.json', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
6
9
|
planCritique: { filename: 'critica.md', phaseDir: '2-plan', phase: 'plan', protected: false },
|
|
7
|
-
planView: { filename: 'plan.html', phaseDir: '2-plan', phase: 'plan', protected: false },
|
|
8
10
|
|
|
9
11
|
uiMockups: { filename: 'mockups.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
10
12
|
|
package/src/lib/plan/checks.js
CHANGED
|
@@ -40,11 +40,11 @@ function defaultsWarnings(plan, at) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Optional: absent is fine; present, every line is a transition the plan view can draw.
|
|
43
|
-
function
|
|
44
|
-
if (!plan.present
|
|
45
|
-
const { transitions, invalid } = plan
|
|
46
|
-
const errors = invalid.map((l) => `## ${SECTIONS
|
|
47
|
-
if (transitions.length === 0 && invalid.length === 0) errors.push(`## ${SECTIONS
|
|
43
|
+
function transitionErrors(plan, key, at) {
|
|
44
|
+
if (!plan.present[key]) return [];
|
|
45
|
+
const { transitions, invalid } = plan[key === 'flow' ? 'flow' : 'states'];
|
|
46
|
+
const errors = invalid.map((l) => `## ${SECTIONS[key]}: "${l.raw}" não é transição — use "- origem → destino: gatilho"${at(l.line)}`);
|
|
47
|
+
if (transitions.length === 0 && invalid.length === 0) errors.push(`## ${SECTIONS[key]} vazia — liste as transições ou apague a seção`);
|
|
48
48
|
return errors;
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -120,7 +120,7 @@ export function validatePlan(plan, { workType = null, riskRules = true, text = n
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
errors.push(...defaultsErrors(plan, { workType, riskRules }, at));
|
|
123
|
-
errors.push(...
|
|
123
|
+
errors.push(...transitionErrors(plan, 'states', at), ...transitionErrors(plan, 'flow', at));
|
|
124
124
|
warnings.push(...defaultsWarnings(plan, at));
|
|
125
125
|
|
|
126
126
|
const isSlices = (s) => /^fatias$/i.test(s.name.trim());
|
|
@@ -14,6 +14,7 @@ export const SECTIONS = {
|
|
|
14
14
|
assumptions: 'Suposições não confirmadas',
|
|
15
15
|
defaults: 'Padrões assumidos',
|
|
16
16
|
states: 'Estados',
|
|
17
|
+
flow: 'Fluxo',
|
|
17
18
|
scenarios: 'Cenários',
|
|
18
19
|
decisions: 'Decisões',
|
|
19
20
|
architecture: 'Arquitetura',
|
|
@@ -191,6 +192,8 @@ export function parsePlan(text) {
|
|
|
191
192
|
reuseHeader: reuse.header,
|
|
192
193
|
defaults: parseDefaults(body('defaults')),
|
|
193
194
|
states: parseStates(body('states')),
|
|
195
|
+
flow: parseStates(body('flow')),
|
|
196
|
+
text: Object.fromEntries(Object.keys(SECTIONS).map((k) => [k, body(k).map((l) => l.raw).join('\n').trim()])),
|
|
194
197
|
errata: errata ? errata.lines.map((l) => l.raw).join('\n').trim() : null,
|
|
195
198
|
sectionsAfterErrata: errata ? sections.filter((s) => s.line > errata.line).map((s) => s.name) : [],
|
|
196
199
|
};
|
package/src/lib/recap/recap.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync } from 'child_process';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { parsePlan } from '../plan/plan-parser.js';
|
|
5
|
-
import { stateMachineSvg } from '../
|
|
5
|
+
import { stateMachineSvg } from '../view/flow-svg.js';
|
|
6
6
|
import { reviewReportsFromDir } from '../review-reports.js';
|
|
7
7
|
import { loadGraph, getFreshness } from '../graph/graph-loader.js';
|
|
8
8
|
import { parseDiff, buildDiffTree, declarationDrift } from '../graph/diff-tree.js';
|
|
@@ -19,7 +19,7 @@ const git = (cwd, ...args) => execFileSync('git', args, { cwd, stdio: 'pipe', ma
|
|
|
19
19
|
* What changed on the branch: the call tree when the graph is of HEAD and its lines hold, else the
|
|
20
20
|
* plain file and line counts. Never throws; outside git it is null.
|
|
21
21
|
*/
|
|
22
|
-
export function changeSummary(cwd, { base = null } = {}) {
|
|
22
|
+
export function changeSummary(cwd, { base = null, statOnly = false } = {}) {
|
|
23
23
|
let from = base;
|
|
24
24
|
try {
|
|
25
25
|
from ??= git(cwd, 'merge-base', detectDefaultBranch(cwd) || 'main', 'HEAD').trim();
|
|
@@ -33,6 +33,7 @@ export function changeSummary(cwd, { base = null } = {}) {
|
|
|
33
33
|
} catch {
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
|
+
if (statOnly) return { stat, tree: null };
|
|
36
37
|
const loaded = loadGraph(cwd);
|
|
37
38
|
if (!loaded.ok || getFreshness(cwd, loaded.graph).state !== 'fresh') return { stat, tree: null };
|
|
38
39
|
try {
|
|
@@ -88,7 +89,7 @@ export function recapScenes(r) {
|
|
|
88
89
|
id: 'abertura',
|
|
89
90
|
title: r.feature,
|
|
90
91
|
narration: `${r.feature}. ${r.description ?? ''} ${r.days !== null ? `Do início ao fim, ${r.days === 0 ? 'menos de um dia' : `${r.days} dia${r.days > 1 ? 's' : ''}`}.` : ''}`.replace(/\s+/g, ' ').trim(),
|
|
91
|
-
body: `<p class="lead">${esc(r.description ?? '')}</p>${r.days !== null ? `<p class="big">${r.days === 0 ? '< 1' : r.days} <span>dia${r.days
|
|
92
|
+
body: `<p class="lead">${esc(r.description ?? '')}</p>${r.days !== null ? `<p class="big">${r.days === 0 ? '< 1' : r.days} <span>dia${r.days <= 1 ? '' : 's'}</span></p>` : ''}`,
|
|
92
93
|
});
|
|
93
94
|
if (r.scenarios.length) {
|
|
94
95
|
const ok = r.scenarios.filter((c) => c.status === 'PROVADO').length;
|
|
@@ -135,6 +136,7 @@ export function recapScenes(r) {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
const SCENE_MS = 9000;
|
|
139
|
+
const TOKENS = readFileSync(new URL('../view/tokens.css', import.meta.url), 'utf8');
|
|
138
140
|
|
|
139
141
|
/** `audio`: optional `{ [sceneId]: dataUri }`; without it the page narrates with the system voice. */
|
|
140
142
|
export function renderRecapHtml(r, { audio = null } = {}) {
|
|
@@ -147,8 +149,7 @@ export function renderRecapHtml(r, { audio = null } = {}) {
|
|
|
147
149
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
148
150
|
<title>Recap — ${esc(r.feature)}</title>
|
|
149
151
|
<style>
|
|
150
|
-
:root { --bg
|
|
151
|
-
@media (prefers-color-scheme: light) { :root { --bg:#f6f7f9; --card:#fff; --fg:#14171c; --dim:#4b5260; --faint:#7a8291; --hair:#dfe3ea; --ok:#2f7d4f; --warn:#a35f00; --bad:#b4232f; --accent:#2d5bd7; } }
|
|
152
|
+
${TOKENS}:root { --bg:var(--paper); --card:var(--panel); --fg:var(--ink); --hair:var(--rule); --accent:var(--route); }
|
|
152
153
|
* { box-sizing:border-box; }
|
|
153
154
|
body { margin:0; min-height:100vh; background:var(--bg); color:var(--fg); font:16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; display:flex; flex-direction:column; }
|
|
154
155
|
.stage { flex:1; position:relative; max-width:960px; width:100%; margin:0 auto; padding:40px 16px 16px; }
|
|
@@ -51,6 +51,9 @@ const removeRun = (paths) => {
|
|
|
51
51
|
for (const file of Object.values(paths)) rmSync(file, { force: true });
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
// launch and watchdog run without a console on win32: every git the view spawns there opens a window.
|
|
55
|
+
export const refreshesFeatureView = (env = process.env) => !['launch', 'watchdog'].includes(env[DETACH_STAGE_ENV]);
|
|
56
|
+
|
|
54
57
|
export function matchesRun(record, { key, args }) {
|
|
55
58
|
return record?.key === key && JSON.stringify(record.args) === JSON.stringify(args);
|
|
56
59
|
}
|