@polymorphism-tech/morph-spec 8.22.0 → 8.23.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 (36) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +14 -0
  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 +1 -1
  7. package/framework/agents.json +1 -0
  8. package/framework/docs/CLI.md +3 -2
  9. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  10. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  11. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  12. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  13. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  14. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  15. package/framework/skills/core/morph-implement/SKILL.md +14 -2
  16. package/framework/skills/core/morph-plan/SKILL.md +6 -4
  17. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  18. package/framework/skills/core/morph-review/SKILL.md +29 -3
  19. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  20. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  21. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  22. package/framework/standards/STANDARDS.json +28 -0
  23. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +5 -2
  24. package/framework/standards/backend/dotnet/testing.md +127 -0
  25. package/package.json +1 -1
  26. package/src/commands/state/approve.js +11 -0
  27. package/src/commands/state/archive.js +12 -3
  28. package/src/commands/state/finish.js +9 -4
  29. package/src/commands/state/handoff.js +3 -2
  30. package/src/commands/state/usecase.js +77 -0
  31. package/src/commands/validation/verify.js +6 -0
  32. package/src/lib/generators/handoff-generator.js +2 -2
  33. package/src/lib/specs/usecase-promoter.js +135 -19
  34. package/src/lib/specs/usecase-registry.js +237 -0
  35. package/src/lib/validators/traceability-check.js +198 -0
  36. package/src/lib/validators/verify-runner.js +14 -0
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.22.0
6
+ **Version:** 8.23.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.22.0 by [Polymorphism Tech](https://polymorphism.tech)*
371
+ *morph-spec v8.23.0 by [Polymorphism Tech](https://polymorphism.tech)*
package/bin/morph-spec.js CHANGED
@@ -42,6 +42,7 @@ import { retypeCommand } from '../src/commands/state/retype.js';
42
42
  import { handoffCommand } from '../src/commands/state/handoff.js';
43
43
  import { listCommand } from '../src/commands/state/list.js';
44
44
  import { deleteCommand } from '../src/commands/state/delete.js';
45
+ import { usecaseReserveCommand } from '../src/commands/state/usecase.js';
45
46
 
46
47
  // === Tasks ===
47
48
  import { dagCommand } from '../src/commands/tasks/dag.js';
@@ -258,6 +259,19 @@ program
258
259
  .option('--json', 'Output as JSON')
259
260
  .action((feature, options) => handoffCommand(feature, options));
260
261
 
262
+ // ─── Use cases ────────────────────────────────────────────────────────────
263
+ const usecaseCommand = program
264
+ .command('usecase')
265
+ .description('Project-wide UC-{nn} numbering (acervo + every worktree of this repo)');
266
+
267
+ usecaseCommand
268
+ .command('reserve')
269
+ .description('Atomically reserve the next N UC-{nn} numbers, shared across every worktree of this repo — replaces the old max+1 disk scan, which collides across worktrees')
270
+ .option('--count <n>', 'How many numbers to reserve', '1')
271
+ .option('--feature <name>', 'Feature to record as the reservation owner (defaults to the sole active feature, if unambiguous) — also what the promotion collision guard compares against')
272
+ .option('--json', 'Output as JSON')
273
+ .action((options) => usecaseReserveCommand(options));
274
+
261
275
  // ─── Tasks ────────────────────────────────────────────────────────────────
262
276
  program
263
277
  .command('dag <feature>')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.22.0",
3
+ "version": "8.23.0",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -185,4 +185,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
185
185
 
186
186
  ---
187
187
 
188
- *morph-spec v8.22.0 by Polymorphism Tech*
188
+ *morph-spec v8.23.0 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.22.0 by Polymorphism Tech*
252
+ *morph-spec v8.23.0 by Polymorphism Tech*
@@ -122,7 +122,7 @@ Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu gi
122
122
  - **Ambiente separado (portas):** o setup aloca um **bloco de portas determinístico** por feature em `.morph/worktree.env` (`PORT`, `MORPH_PORT_RANGE`; `ASPNETCORE_URLS` se dotnet). **Suba dev servers SEMPRE nas portas do bloco** — as portas padrão (3000/5000) pertencem à raiz. Arquivo local, git-invisível; SessionStart injeta as portas. Overrides em `config.json → fleet.basePort/blockSize`.
123
123
  - **Banco de dados:** compartilhado por padrão. Isolamento real (Neon MCP): branch de DB `morph/{f}` + connection string no `.morph/worktree.env`; sem Neon, prefixe schema/DB local pelo nome da feature. Migration destrutiva compartilhando banco → integre antes de abrir worktrees irmãos.
124
124
  - **Limitação conhecida:** `node_modules` compartilhado (junction) — mudança de dependência afeta os irmãos. Mudou dependência: conclua e integre (`finish`) antes de seguir em paralelo.
125
- - **Handoff entre sessões:** `.morph/features/{f}/handoff.md` — documento de retomada comprimido, regenerável (`morph-spec handoff <f>`), **aponta** para os artefatos em vez de copiá-los. Regenerado em `worktree setup`/`create --worktree`/`advance`. **Leia-o antes de re-derivar contexto à mão.** Nunca edite manualmente.
125
+ - **Handoff entre sessões:** `.morph/features/{f}/handoff.md` — documento de retomada comprimido, regenerável (`morph-spec handoff <f>`), **aponta** para os artefatos em vez de copiá-los. Regenerado em `worktree setup`/`create --worktree`/`advance`/`approve`. **Leia-o antes de re-derivar contexto à mão.** Nunca edite manualmente.
126
126
  - **Visão da frota:** `morph-spec fleet` agrega raiz + worktrees (fase, gates, tasks, portas, handoff, último commit, sujeira). `morph-spec worktree list` é a visão enxuta por worktree.
127
127
  - **Ownership de arquivos:** sem lock em runtime — colisão se resolve **em plan-time** (`morph-spec dag` detecta overlap de `outputs`, recomenda `isolation: "worktree"`) e **por construção** (uma feature = um worktree = um escritor). Arquivos compartilhados convergem no merge do `finish`; conflito ali é escopo sobreposto, não bug.
128
128
  - **Fechamento:** aprove o Gate 3, depois **`morph-spec finish <f> --pr|--merge`** — encerramento único e simétrico. **Arquiva** commitando `features/{f} → archive/{f}` **dentro de `morph/{f}`** e integra: `--merge` faz `--no-ff` **local** (sem push; recusa se sujo; aborta em conflito); `--pr` faz `push` + `gh pr create` (exige `origin`+`gh`, checados **antes** de arquivar). Worktree desmontado (PROVE-or-DECLINE), portas liberadas; no `--pr` a branch é **preservada**. `morph-spec archive` fica para arquivar-sem-integrar (features legadas, faxina).
@@ -20,6 +20,7 @@
20
20
  "backend-dotnet-result-pattern",
21
21
  "backend-dotnet-vsa-handler-patterns",
22
22
  "backend-dotnet-fluent-validation-vsa",
23
+ "backend-dotnet-testing",
23
24
  "backend-integrations-neon-auth-neon-auth"
24
25
  ],
25
26
  "mcp": ["context7"],
@@ -21,10 +21,11 @@
21
21
 
22
22
  ## Worktree / Fleet
23
23
 
24
- - **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
24
+ - **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra. Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
25
+ - **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
25
26
  - **`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.
26
27
  - **`morph-spec worktree setup|link|list`** — gerencia o ciclo do worktree por feature (`setup` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo). O fechamento saiu daqui — use `morph-spec finish` (acima).
27
- - **`morph-spec handoff <feature>`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. Gerado automaticamente em `setup`/`create --worktree`/`advance`.
28
+ - **`morph-spec handoff <feature>`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
28
29
  - **`morph-spec fleet`** — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, **remoto** (`local`/`pushed`/`PR #N`), **claim** (que sessão está na feature), portas, handoff e atividade. Use entre sessões para decidir onde atuar.
29
30
  - **`morph-spec claim [feature] [--release] [--list] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`.
30
31
 
@@ -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.22.0"
7
+ cliVersion: "8.23.0"
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-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.22.0"
7
+ cliVersion: "8.23.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -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.22.0"
7
+ cliVersion: "8.23.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
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.22.0"
7
+ cliVersion: "8.23.0"
8
8
  ---
9
9
 
10
10
  # morph-brainstorming
@@ -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.22.0"
7
+ cliVersion: "8.23.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.22.0"
7
+ cliVersion: "8.23.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.22.0"
7
+ cliVersion: "8.23.0"
8
8
  ---
9
9
 
10
10
  # morph-implement
@@ -129,9 +129,21 @@ Ao iniciar cada task de `tasks.json` — antes de escrever qualquer código. Nor
129
129
  - **`outputs` contém apenas paths realmente produzidos.** Deleções, remoções
130
130
  e anotações (ex.: `"pasta/ (removido: a, b)"`) se registram em `notes`,
131
131
  nunca em `outputs`. E `notes` é **uma string única** — não um array.
132
+ - **`outputs` DEVE listar TODO arquivo criado ou modificado pela task — sem exceção.**
133
+ Isso inclui artefato auxiliar que não estava no plano original (ex.: uma
134
+ factory de design-time, um seed, um arquivo de config gerado a reboque de
135
+ outro output) — se a task o produziu, ele entra em `outputs` junto com o
136
+ resto. Arquivo shipped sem dono em `outputs` de task alguma é falha de
137
+ protocolo: o Gate 3 (avaliador independente da `morph-review`) aponta como
138
+ achado de rastreabilidade, mesmo que o arquivo em si esteja correto — quem
139
+ lê `tasks.json`/`recap.md` depois precisa achar todo arquivo em produção
140
+ citado em algum lugar, e é essa a lacuna que este parágrafo fecha.
132
141
  - **Se os `outputs` reais divergirem dos planejados**, registre o porquê em
133
142
  `notes` na mesma edição — o hook exige a note quando há divergência (e a
134
- mensagem de violação aponta o campo/valor exato do problema).
143
+ mensagem de violação aponta o campo/valor exato do problema). Isto é o
144
+ caso mais comum em que a regra acima se aplica: todo arquivo extra na lista
145
+ de "realmente produzidos" acima do planejado é, por definição, uma
146
+ divergência real vs. planejado — a note explica o porquê.
135
147
  - `tasks.json` é protegido após o Gate 2: o hook `protect-spec-files` faz diff
136
148
  semântico e libera Write ou Edit desde que nenhum campo de contrato do plano
137
149
  (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`)
@@ -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.22.0"
7
+ cliVersion: "8.23.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -56,7 +56,7 @@ Ao iniciar a fase plan — após o `proposal.md` ser aprovado e antes de qualque
56
56
 
57
57
  **A regra que sustenta a camada inteira: pós-condição precisa ser observável em teste.** "O sistema fica consistente" não passa. "A resposta contém todos os veterinários não arquivados" passa. Antes de escrever cada pós-condição, pergunte: *que asserção eu escreveria para verificar isto?* Se não há resposta, a pós-condição está mal formulada — reescreva ou descarte. Sem esse filtro a camada vira prosa e não chega ao `doneCriteria`.
58
58
 
59
- **Identificador sequencial no projeto, não na feature.** Antes de numerar, leia o acervo `.morph/specs/usecases/` (se existir) mais os use cases das features ativas em `.morph/features/*/2-plan/usecases/`; tome o maior `UC-{nn}` e some um. Numerar por feature garante colisão no momento da promoção.
59
+ **Identificador sequencial, reservado — nunca leia o disco à mão.** Rode `morph-spec usecase reserve --count {n} --feature {feature}` (`n` = quantos UCs esta feature vai criar; pode reservar de novo se descobrir mais no meio do plano). Um scan manual do maior `UC-{nn}` só enxerga a ÁRVORE CORRENTE — cada worktree tem a própria cópia de `.morph/features/`, então dois worktrees planejando ao mesmo tempo calculam o mesmo maior número e mintam o MESMO `UC-{nn}` sem saber um do outro; na promoção do `finish`, o segundo a fechar sobrescreve por ID e **apaga o use case do primeiro** — já causou perda de dados em produção. `usecase reserve` aloca atomicamente a partir de um contador compartilhado em `<git-common-dir>/morph-spec/uc-registry.json` (visível a todo worktree do repo, mesmo mecanismo do `claim`/fleet), semeado do maior número já visto no acervo + nas features da árvore corrente + no próprio contador — nunca regride. Fora de um repositório git, cai para o scan local antigo e avisa que o paralelismo entre worktrees não está protegido.
60
60
 
61
61
  **Seções obrigatórias:** `Pré-condições` (o que já é verdade antes), `Cenário principal` (o caminho feliz, numerado, **curto** — não é o lugar para exaustão de casos) e `Pós-condições` (**P1**, **P2**… — o que passa a ser verdade, cada uma testável; é a seção que viaja **literal** ao `doneCriteria`, ver abaixo).
62
62
 
@@ -67,12 +67,14 @@ Ao iniciar a fase plan — após o `proposal.md` ser aprovado e antes de qualque
67
67
  | Tipo | Use case |
68
68
  |---|---|
69
69
  | `feature` | Obrigatório quando há comportamento observável. Feature puramente técnica (refactor, infra, migração) dispensa — **registre a dispensa em `decisions.md`**. |
70
- | `bug` | Não cria novo: **copie** o `UC-{nn}-*.md` do acervo para `2-plan/usecases/` e **corrija ou adicione a pós-condição** lá — o bug é a pós-condição que faltava. |
70
+ | `bug` | Não cria novo, não reserva número novo: **copie** o `UC-{nn}-*.md` do acervo para `2-plan/usecases/` **mantendo o nome do arquivo publicado** (mesmo slug) e **corrija ou adicione a pós-condição** lá — o bug é a pós-condição que faltava, não a identidade do UC. É esse mesmo nome, inalterado, que faz o `finish` reconhecer a correção como o MESMO UC (abaixo) em vez de uma colisão. |
71
71
  | `chore` | Nenhum (já pula a fase plan por política). |
72
72
  | `hotfix` | Não bloqueia. Referencia se existir; o pós-mortem pode virar pós-condição numa feature posterior. |
73
73
 
74
74
  **Nunca edite `.morph/specs/usecases/` à mão.** A única via de escrita no acervo é a promoção do `finish`, que lê **só** de `2-plan/usecases/`: é ela que carimba `status: implemented`, sobrescreve por ID (mesmo com slug novo) e leva o arquivo no commit de arquivamento. Editar o acervo direto pula os três e cria uma segunda verdade fora do mecanismo. Mudar um UC existente = copiar para o `2-plan/usecases/` da feature, corrigir lá, e deixar o `finish` sobrescrever.
75
75
 
76
+ **Backstop de colisão na promoção.** Se, apesar da reserva, duas features chegarem à promoção com o MESMO `UC-{nn}` e slugs diferentes — registro de reserva aponta para uma feature diferente da que já está no acervo —, o `finish`/`archive` **recusa promover esse UC específico** (nunca recusa fechar a feature: fail-open, só aquele use case fica de fora) e imprime onde o arquivo ficou e como recuperar. Se acontecer: renumere com `usecase reserve` e promova manualmente, ou — se for mesmo o caso `bug` — confira se o slug bate com o publicado.
77
+
76
78
  O use case fica **editável depois do Gate 2**, por design — é o que permite mudar o comportamento e re-rodar `implement`. A trilha de auditoria fica no git.
77
79
 
78
80
  3. Produza `spec.md` em `.morph/features/{feature}/2-plan/`:
@@ -223,7 +225,7 @@ Ao registrar a aprovação do Gate 2, atualize o frontmatter de cada use case de
223
225
  - `morph-spec standards --find "<query>"` — para identificar standards relevantes ao mandate (o índice inteiro só como navegação exploratória; ver `morph-standards`)
224
226
  - `.morph/framework/agents.json` — personas por domínio; lido UMA VEZ no passo 5a para carimbar `agent`/`standards` por task
225
227
  - `.morph/framework/schemas/tasks.schema.json` — schema obrigatório de tasks.json
226
- - `.morph/specs/usecases/` — acervo permanente de use cases do projeto (leia antes de numerar; em `bug`, é onde mora o UC a corrigir)
228
+ - `.morph/specs/usecases/` — acervo permanente de use cases do projeto (`usecase reserve` já lê antes de numerar; em `bug`, é onde mora o UC a corrigir)
227
229
  - `.morph/features/{feature}/0-proposal/proposal.md` — escopo da feature
228
230
  - `.morph/features/{feature}/0-proposal/stack-scan.md` — contexto técnico detectado
229
231
  - `.morph/framework/schemas/tasks.schema.json` — schema e enums de tasks.json
@@ -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.22.0"
7
+ cliVersion: "8.23.0"
8
8
  ---
9
9
 
10
10
  # morph-proposal
@@ -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.22.0"
7
+ cliVersion: "8.23.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -42,7 +42,23 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
42
42
  confirme que usam duas constantes de prefixo nomeadas distintas, nenhuma reusada para o
43
43
  outro propósito. Falha aqui é bloqueante — quebra silenciosa em runtime, não em build.
44
44
 
45
- 4. **Despache o avaliador independente** — o score do Gate 3 nunca é auto-atribuído por quem implementou:
45
+ 4. **Antes de despachar, verifique se já existe um relatório íntegro** — inclusive em sessão
46
+ retomada. **Esta verificação NÃO é pulada porque a sessão foi retomada** — é exatamente esse o
47
+ cenário que já mordeu em produção: 4 sessões retomadas re-despacharam o avaliador do Gate 3 sem
48
+ nunca ler o `evaluator-report.md` que já existia, íntegro, desde o 1º dispatch (`APPROVED`, score
49
+ 9.3) — e a 4ª sessão chegou a afirmar, falsamente, que "o avaliador nunca rodou". `SessionStart:
50
+ resume`/`init` não reseta este passo: ele roda sempre, a cada retomada de controle, antes de
51
+ cogitar um dispatch.
52
+ - Rode `morph-spec report-check {feature} --json` (sem `task`, modo feature inteira) e leia o
53
+ campo `evaluator` da resposta — ou, equivalente, leia `4-review/evaluator-report.md` direto e
54
+ confira: scores por dimensão presentes + a linha `**Composite-Score:**` presente = **íntegro**.
55
+ - **Se íntegro** (`evaluator.exists && evaluator.complete`): **NÃO despache de novo.** Pule direto
56
+ para "**Leia primeiro**" logo abaixo — leia o arquivo e siga para o passo 5 (síntese do
57
+ review-report a partir dele). Re-despachar aqui joga fora um veredito de Gate 3 já válido.
58
+ - **Se ausente, oco, ou sem scores por dimensão:** este é exatamente o caso de falha de protocolo
59
+ que este passo trata — prossiga com o dispatch abaixo.
60
+
61
+ **Despache o avaliador independente** — o score do Gate 3 nunca é auto-atribuído por quem implementou:
46
62
  - Rode `morph-spec verify {feature} --json` (escopo feature) e capture o JSON — é a saída real de build+testes+validadores+e2e que você anexa ao prompt do avaliador independente (`morph-evaluator`).
47
63
  - **Confira o nó `e2e` do verify antes de despachar.** Ele carrega o estado do stack:
48
64
  - `pass` → a aplicação subiu e está saudável; o avaliador pode navegá-la. **Passe a URL no prompt**
@@ -53,6 +69,9 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
53
69
  legítimo; "docker indisponível no ambiente" numa feature de UI **não** é — registre você mesmo
54
70
  `morph-spec gate-decision {feature} review --pause true --reason "e2e não verificado: {razão}"`.
55
71
  O disco não distingue dispensa legítima de verificação que faltou; esse julgamento é seu.
72
+ - **Confira também `traceability.warnings`** (advisory, nunca faz `verify` falhar): cada entrada é
73
+ um arquivo citado no `recap.md` sem task dona em `outputs` — repasse ao avaliador como sinal de
74
+ rastreabilidade para a dimensão de qualidade, não como bloqueio automático do gate.
56
75
  - Gere o **diff da implementação** (`git diff` do range da feature, ou a lista de arquivos do recap.md com seus conteúdos atuais).
57
76
  - Despache o subagent registrado `morph-evaluator` (gerado de `agents.json` — a persona do avaliador é o system prompt dele, com Read/Glob/Grep + Bash somente para verificação + Playwright para navegar, sem Write/Edit), com contexto fresco:
58
77
  ```
@@ -61,7 +80,14 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
61
80
  + nó e2e do verify (stack no ar? em qual URL?)
62
81
  instrução: |
63
82
  Você NÃO participou da implementação — avalie o que está no diff,
64
- contra o mandate. Você tem Bash: use `morph-spec verify`, testes/build
83
+ contra o mandate. NÃO leia `4-review/evaluator-report.md` neste
84
+ caminho antes de escrever o seu: se esse arquivo já existir quando
85
+ você começar, é resíduo de uma tentativa anterior — NÃO é insumo,
86
+ é o SEU caminho de escrita, não de leitura. Sua nota nasce do
87
+ mandate + spec + diff + testes, nunca do veredito de outro
88
+ avaliador; ler o relatório de um antecessor antes de pontuar é
89
+ ancoragem — exatamente o que "contexto fresco" existe para evitar.
90
+ Você tem Bash: use `morph-spec verify`, testes/build
65
91
  do stack e `git log/diff/show` para conferir por conta própria o que
66
92
  foi reportado, em vez de confiar cegamente. Aplique as rubricas em
67
93
  .morph/framework/evals/rubrics/ (architecture 30%, contracts 25%,
@@ -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.22.0"
7
+ cliVersion: "8.23.0"
8
8
  ---
9
9
 
10
10
  # morph-stack-scan
@@ -4,7 +4,7 @@ description: Localiza um standard específico via busca server-side (morph-spec
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.22.0"
7
+ cliVersion: "8.23.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX — design system, mockups, compone
4
4
  version: core
5
5
  when-to-invoke: ao tomar o ramo uiux (morph-spec advance --design), depois do Gate 1 e antes do morph-plan
6
6
  user-invokable: true
7
- cliVersion: "8.22.0"
7
+ cliVersion: "8.23.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux
@@ -1779,6 +1779,34 @@
1779
1779
  "data-ingestion": "#microsoftextensionsdataingestion-preview--pipeline-pattern-para-adoção-futura"
1780
1780
  },
1781
1781
  "digest": "Microsoft.Extensions.VectorData.Abstractions (GA) is the provider-agnostic vector-store abstraction; Microsoft.Extensions.DataIngestion (preview) is the companion ETL pipeline. Concrete connectors (Postgres, Qdrant, SQL Server...) ship under Microsoft.SemanticKernel.Connectors.* package names despite having nothing to do with Semantic Kernel — an explicit exception to ai-agents-setup's SK ban. Default RAG storage stays ai-agents-rag-custom-pgvector; adopt VectorData only for genuine multi-backend portability, since the Postgres connector is still prerelease."
1782
+ },
1783
+ {
1784
+ "id": "backend-dotnet-testing",
1785
+ "name": "testing",
1786
+ "path": "backend/dotnet/testing.md",
1787
+ "category": "backend",
1788
+ "subcategory": "dotnet",
1789
+ "tags": [
1790
+ "backend",
1791
+ "dotnet"
1792
+ ],
1793
+ "stacks": [
1794
+ "dotnet"
1795
+ ],
1796
+ "aliases": [
1797
+ "xunit",
1798
+ "TimeProvider",
1799
+ "FakeTimeProvider",
1800
+ "fake clock",
1801
+ "integration test",
1802
+ "ProblemDetails",
1803
+ "error body assertion"
1804
+ ],
1805
+ "anchors": {
1806
+ "clock": "#rule-1--inject-timeprovider-fake-it-with-faketimeprovider-in-tests",
1807
+ "errorbody": "#rule-2--error-path-tests-assert-the-response-body-not-just-the-status-code"
1808
+ },
1809
+ "digest": "Inject TimeProvider (never DateTime.UtcNow / DateOnly.FromDateTime(DateTime.UtcNow) directly in a handler or validator) and fake it with FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing) in integration tests — deterministic, no midnight-UTC flake, boundary dates (today vs. yesterday) become facts about the fake clock instead of when the suite happens to run. xUnit + WebApplicationFactory error-path tests (400/404/409) must assert the ProblemDetails response BODY (title/detail/violated field or error code), never just the HTTP status code — a status-only assertion lets the wrong validation rule or the wrong error branch pass silently as long as the status matches."
1782
1810
  }
1783
1811
  ],
1784
1812
  "sharedAliases": {
@@ -23,7 +23,10 @@ One validator per request DTO. Lives in the same feature folder as the handler:
23
23
  // Features/BookFeature/CreateBook/CreateBookValidator.cs
24
24
  public class CreateBookValidator : AbstractValidator<CreateBookRequest>
25
25
  {
26
- public CreateBookValidator()
26
+ // TimeProvider injected, never DateTime.UtcNow read directly — see
27
+ // backend-dotnet-testing (Rule 1): a validator that reads the real clock
28
+ // cannot be tested deterministically with FakeTimeProvider.
29
+ public CreateBookValidator(TimeProvider timeProvider)
27
30
  {
28
31
  RuleFor(c => c.Title)
29
32
  .NotEmpty().WithMessage("Title is required")
@@ -41,7 +44,7 @@ public class CreateBookValidator : AbstractValidator<CreateBookRequest>
41
44
 
42
45
  RuleFor(c => c.PublishedYear)
43
46
  .GreaterThan(1000).WithMessage("Published year must be a valid year")
44
- .LessThanOrEqualTo(DateTime.UtcNow.Year)
47
+ .LessThanOrEqualTo(timeProvider.GetUtcNow().Year)
45
48
  .WithMessage("Published year cannot be in the future");
46
49
  }
47
50
  }
@@ -0,0 +1,127 @@
1
+ # .NET Integration Testing — VSA
2
+
3
+ > **Scope:** dotnet-vsa
4
+ > **Layer:** 0 (always load)
5
+ > **Keywords:** testing, xunit, integration test, TimeProvider, FakeTimeProvider, WebApplicationFactory, ProblemDetails, error body assertion
6
+ > **Load When:** writing or reviewing tests for a dotnet VSA project
7
+
8
+ **Verified against:** .NET 10 + xUnit 2 + `Microsoft.Extensions.TimeProvider.Testing`. Last-verified: 2026-08-12.
9
+
10
+ ---
11
+
12
+ ## Overview
13
+
14
+ Integration tests exercise the real ASP.NET Core pipeline (`WebApplicationFactory`) against
15
+ an in-memory/test database — the pattern used across `backend-dotnet-vsa-handler-patterns`'
16
+ handler examples. Two failure modes recur in generated test suites and each has a dedicated
17
+ rule below: tests that read the real system clock instead of an injected, fakeable one; and
18
+ error-path tests that assert only the HTTP status code, leaving the actual failure contract
19
+ (the response body) unverified.
20
+
21
+ ---
22
+
23
+ ## Rule 1 — Inject `TimeProvider`, fake it with `FakeTimeProvider` in tests
24
+
25
+ Any logic that compares against "now" (a past-date validation, an expiry check, a due-date
26
+ default) MUST read the clock through an injected `TimeProvider` — never `DateTime.UtcNow` or
27
+ `DateOnly.FromDateTime(DateTime.UtcNow)` directly in a handler or validator. A validator that
28
+ reads the real clock makes "today" a moving target: a test asserting "today is valid, yesterday
29
+ is rejected" flakes for real around UTC midnight, and is untestable for the "exactly at the
30
+ boundary" case at all.
31
+
32
+ ```csharp
33
+ // Features/VisitFeature/CloseVisit/CloseVisitValidator.cs
34
+ public sealed class CloseVisitValidator : AbstractValidator<CloseVisitRequest>
35
+ {
36
+ public CloseVisitValidator(TimeProvider timeProvider)
37
+ {
38
+ RuleFor(c => c.FollowUpDate)
39
+ .Must(date => date!.Value >= Today(timeProvider))
40
+ .WithErrorCode(VisitErrors.FollowUpDateInPast.Code)
41
+ .WithMessage(VisitErrors.FollowUpDateInPast.Description)
42
+ .When(c => c.FollowUpDate.HasValue);
43
+ }
44
+
45
+ private static DateOnly Today(TimeProvider timeProvider) =>
46
+ DateOnly.FromDateTime(timeProvider.GetUtcNow().UtcDateTime);
47
+ }
48
+ ```
49
+
50
+ Register the real clock once in `Program.cs`:
51
+
52
+ ```csharp
53
+ builder.Services.AddSingleton(TimeProvider.System);
54
+ ```
55
+
56
+ In tests, swap it for `Microsoft.Extensions.TimeProvider.Testing`'s `FakeTimeProvider` —
57
+ deterministic, no dependency on when the suite happens to run:
58
+
59
+ ```csharp
60
+ // tests/.../Infrastructure/VetClinicApiFactory.cs
61
+ public sealed class VetClinicApiFactory : WebApplicationFactory<Program>
62
+ {
63
+ public FakeTimeProvider TimeProvider { get; } = new(new DateTimeOffset(2026, 6, 15, 0, 0, 0, TimeSpan.Zero));
64
+
65
+ protected override void ConfigureWebHost(IWebHostBuilder builder) =>
66
+ builder.ConfigureServices(services =>
67
+ {
68
+ services.RemoveAll<TimeProvider>();
69
+ services.AddSingleton<TimeProvider>(TimeProvider);
70
+ });
71
+ }
72
+ ```
73
+
74
+ ```csharp
75
+ // The boundary case ("today is accepted, yesterday is not") becomes a fact about
76
+ // the fake clock, not about when CI happens to run.
77
+ DateOnly today = DateOnly.FromDateTime(factory.TimeProvider.GetUtcNow().UtcDateTime);
78
+ DateOnly yesterday = today.AddDays(-1);
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Rule 2 — Error-path tests assert the response BODY, not just the status code
84
+
85
+ A test that only checks `response.StatusCode` proves the endpoint returns *a* 400/404/409 — it
86
+ proves nothing about *why*, and lets the wrong validation rule or the wrong error code pass
87
+ silently as long as the status happens to match. Every error-path test MUST also assert the
88
+ `ProblemDetails` body: at minimum `title` (or `type`) and the specific field/reason that
89
+ triggered the failure.
90
+
91
+ ```csharp
92
+ // Wrong — proves only that SOMETHING was rejected, not what:
93
+ HttpResponseMessage response = await client.PostAsJsonAsync($"visits/{id}/close", body);
94
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
95
+ ```
96
+
97
+ ```csharp
98
+ // Right — the body is the actual contract a client integrates against:
99
+ HttpResponseMessage response = await client.PostAsJsonAsync($"visits/{id}/close", body);
100
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
101
+
102
+ ProblemDetails? problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
103
+ Assert.NotNull(problem);
104
+ Assert.Contains("FollowUpDate", problem!.Extensions["errors"]!.ToString());
105
+ Assert.Equal(VisitErrors.FollowUpDateInPast.Code, GetErrorCode(problem));
106
+ ```
107
+
108
+ The same discipline applies to 404 (`NotFound` — assert the entity/id named in the problem
109
+ detail) and 409 (`Conflict` — assert the conflict reason, e.g. `"Visit.AlreadyClosed"`, not just
110
+ that a conflict happened). A status-only assertion is a regression magnet: swapping
111
+ `VisitErrors.NotFound` for `VisitErrors.AlreadyClosed` in the wrong branch still returns 404/409
112
+ respectively and the test suite stays green.
113
+
114
+ ---
115
+
116
+ ## Anti-Patterns
117
+
118
+ | Do NOT | Do Instead |
119
+ |--------|------------|
120
+ | Read `DateTime.UtcNow` / `DateOnly.FromDateTime(DateTime.UtcNow)` directly in a handler or validator | Inject `TimeProvider`, read `timeProvider.GetUtcNow()` |
121
+ | Leave a test's "past date" derived from the real system clock (`DateTime.UtcNow.AddDays(-1)`) | Swap `TimeProvider` for `FakeTimeProvider` in the test factory and derive dates from it |
122
+ | Assert only `response.StatusCode` on an error path | Also deserialize and assert the `ProblemDetails` body (title/detail/violated field or error code) |
123
+ | Treat a passing status-only test as proof the error message/code is correct | Assert the specific error code/field — a wrong branch can still return the right status |
124
+
125
+ ---
126
+
127
+ *MORPH-SPEC by Polymorphism Tech*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.22.0",
3
+ "version": "8.23.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -325,6 +325,17 @@ export async function approveCommand(featureName, gateName, options = {}) {
325
325
  ...(selfAssessed ? { selfAssessed: true } : {}),
326
326
  });
327
327
 
328
+ // Fleet handoff (mirrors advance.js): approving a gate is exactly when a
329
+ // feature hands over between sessions too — `approve` is the real path
330
+ // Gate 2 took in the field (`approve <f> plan --mode auto`), not just
331
+ // `advance`, so a handoff.md left stale after approve was the common
332
+ // case, not the edge case. Same helper, same fail-open contract: a
333
+ // regeneration failure must never fail the approval itself.
334
+ try {
335
+ const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
336
+ writeHandoff(featureName, process.cwd());
337
+ } catch { /* fail-open */ }
338
+
328
339
  if (selfAssessed) {
329
340
  console.log(chalk.yellow('\n⚠️ Gate 3 aprovado SEM avaliador independente (--self-assessed).'));
330
341
  console.log(chalk.gray(' Registrado em feature.json → approvalGates.review.selfAssessed.'));
@@ -63,7 +63,8 @@ function readReviewApproved(fjPath) {
63
63
  * @param {string} feature
64
64
  * @param {{ cwd?: string, force?: boolean, git?: boolean }} [opts]
65
65
  * @returns {{ ok: boolean, error?: string, archived?: boolean, alreadyArchived?: boolean,
66
- * committed?: boolean, staged?: boolean, archivePath?: string, promotedUseCases?: string[] }}
66
+ * committed?: boolean, staged?: boolean, archivePath?: string, promotedUseCases?: string[],
67
+ * refusedUseCases?: Array<{id: string, incoming: string, existing: string, existingFeature: string, message: string}> }}
67
68
  */
68
69
  export function runArchive(feature, opts = {}) {
69
70
  const cwd = opts.cwd || process.cwd();
@@ -156,9 +157,11 @@ export function runArchive(feature, opts = {}) {
156
157
  // (MORPH.md §2.2). Fail-open: any failure degrades to "promoted nothing",
157
158
  // never to a refused close.
158
159
  let promotedUseCases = [];
160
+ let refusedUseCases = [];
159
161
  try {
160
- const { promoted } = promoteUseCases(cwd, archivePath);
162
+ const { promoted, refused } = promoteUseCases(cwd, archivePath);
161
163
  promotedUseCases = promoted;
164
+ refusedUseCases = refused;
162
165
  if (promoted.length && useGit && staged) {
163
166
  // This staging exists for the NEW files: promoted use cases are untracked,
164
167
  // and the scoped `commit -- <pathspec>` below only knows paths git already
@@ -203,7 +206,7 @@ export function runArchive(feature, opts = {}) {
203
206
  } catch { /* index is derivable — non-fatal */ }
204
207
  }
205
208
 
206
- return { ok: true, archived: true, committed, staged, archivePath, promotedUseCases };
209
+ return { ok: true, archived: true, committed, staged, archivePath, promotedUseCases, refusedUseCases };
207
210
  }
208
211
 
209
212
  /**
@@ -244,6 +247,12 @@ export async function archiveCommand(featureName, options = {}) {
244
247
  if (result.promotedUseCases?.length) {
245
248
  console.log(chalk.gray(` ${result.promotedUseCases.length} use case(s) promoted → .morph/specs/usecases/ (status: implemented).`));
246
249
  }
250
+ if (result.refusedUseCases?.length) {
251
+ console.log(chalk.yellow(`\n⚠️ ${result.refusedUseCases.length} use case(s) NOT promoted — numbering collision:`));
252
+ for (const r of result.refusedUseCases) {
253
+ console.log(chalk.yellow(` ${r.message}`));
254
+ }
255
+ }
247
256
  if (result.committed) console.log(chalk.gray(` Move committed on the current branch.\n`));
248
257
  else if (result.staged) console.log(chalk.gray(` Move staged in git; commit it when ready.\n`));
249
258
  else console.log('');