@polymorphism-tech/morph-spec 9.1.0 → 9.2.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 (76) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +7 -1
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +1 -0
  7. package/framework/MORPH.md +2 -2
  8. package/framework/agents.json +8 -8
  9. package/framework/docs/CLI.md +16 -14
  10. package/framework/evals/review-checklist.md +12 -2
  11. package/framework/hooks/README.md +8 -44
  12. package/framework/hooks/claude-code/core/state-sync.js +1 -1
  13. package/framework/hooks/claude-code/statusline.sh +11 -11
  14. package/framework/rules/nextjs-standards.md +1 -1
  15. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  16. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  17. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  18. package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
  19. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  20. package/framework/skills/core/morph-media/SKILL.md +1 -1
  21. package/framework/skills/core/morph-plan/SKILL.md +8 -6
  22. package/framework/skills/core/morph-review/SKILL.md +9 -6
  23. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  24. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  25. package/framework/standards/STANDARDS.json +2 -2
  26. package/framework/standards/backend/dotnet/testing.md +10 -4
  27. package/framework/standards/frontend/nextjs/testing.md +10 -6
  28. package/framework/templates/.idea/morph-templates.xml +92 -0
  29. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  30. package/framework/templates/IDE-SNIPPETS.md +262 -262
  31. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  32. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  33. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  34. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  35. package/framework/templates/docs/plan.md +3 -1
  36. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  37. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  38. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  39. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  40. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  41. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  42. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  43. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  44. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  45. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  46. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  47. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  48. package/framework/templates/state.template.json +17 -17
  49. package/framework/templates/ui/design-system.css +226 -226
  50. package/package.json +1 -1
  51. package/src/commands/state/approve.js +2 -2
  52. package/src/commands/validation/mutate.js +9 -6
  53. package/src/commands/validation/plan-check.js +4 -2
  54. package/src/commands/validation/tests-audit.js +7 -0
  55. package/src/commands/validation/validate-feature.js +4 -2
  56. package/src/commands/validation/verify.js +15 -15
  57. package/src/dashboard/app/main.jsx +13 -13
  58. package/src/dashboard/vite.config.js +24 -24
  59. package/src/lib/ci/workflow-scaffold.js +12 -0
  60. package/src/lib/e2e/applicability.js +37 -0
  61. package/src/lib/mutation/adapters.js +6 -17
  62. package/src/lib/mutation/runner.js +23 -75
  63. package/src/lib/mutation/trx.js +0 -1
  64. package/src/lib/plan/checks.js +29 -2
  65. package/src/lib/plan/plan-parser.js +6 -2
  66. package/src/lib/review-reports.js +6 -3
  67. package/src/lib/risk-detector.js +2 -0
  68. package/src/lib/tests-audit/audit.js +96 -0
  69. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  70. package/src/lib/validators/validation-runner.js +6 -38
  71. package/src/lib/validators/verify-runner.js +36 -21
  72. package/src/utils/agents-installer.js +1 -1
  73. package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
  74. package/framework/hooks/git/pre-commit/agents.sh +0 -25
  75. package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
  76. package/framework/hooks/git/pre-push/run-tests.sh +0 -44
@@ -4,7 +4,7 @@ description: Conduz a AUTORIA de mídia gerada por IA — escolher o modelo pelo
4
4
  version: core
5
5
  when_to_use: Use quando a tarefa for PRODUZIR imagem ou vídeo por IA (catálogo, marketing, asset de LP) e alguém precisar decidir modelo, prompt e aprovação
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.2.0"
8
8
  stacks:
9
9
  - dotnet
10
10
  - nextjs
@@ -4,7 +4,7 @@ description: Leva uma feature do pedido ao plano aprovado — roteia o tipo de t
4
4
  version: core
5
5
  when_to_use: Use quando uma feature, bug ou chore começar ("/morph-plan {feature}", "vamos planejar X"), ou ao retomar uma feature cujo gate plan ainda não foi aprovado
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.2.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -66,19 +66,21 @@ Exit ≠ 0 → descarte o mapa e re-despache **uma** vez com as `failures` colad
66
66
 
67
67
  O `create` já escreveu o esqueleto em `2-plan/plan.md` (leia e preencha; `chore` e `hotfix` nascem com o gate carimbado e criam o arquivo numa única escrita); gramática completa: `morph-spec plan-check --help`. `plan-check` é o único leitor; o que ele não reconhece, ninguém lê.
68
68
 
69
- **Cenários** (`## Cenários`): `- **Cn** — Dado …, quando …, então …`, com `prova:` (`teste` | `e2e` | `eval` | `manual`) **na linha seguinte, sem linha em branco** — linha vazia encerra o cenário.
70
- - **Observável em teste**: que asserção eu escreveria? Sem resposta, reescreva ou descarte.
69
+ **Cenários** (`## Cenários`): `- **Cn** — Dado …, quando …, então …`, com `prova:` (`teste` | `e2e` | `eval` | `manual` | `leitura`) **na linha seguinte, sem linha em branco** — linha vazia encerra o cenário.
70
+ - **Teste é por risco, não por código novo.** `prova: teste` exige `risco:` (`regra` | `dinheiro` | `contrato` | `concorrência` | `dado` | `regressão`) na linha abaixo: ramo de regra de negócio, valor, contrato externo, corrida, integridade de dado ou o bug real. Sem risco (CRUD, DTO, mapping, wiring, constante, texto, layout) a prova é `leitura` — o verificador fecha citando `arquivo:linha` — ou `e2e` quando o fluxo é o que importa. O `plan-check` recusa `teste` sem `risco:`.
71
+ - **Observável**: que asserção (ou que linha de código) prova? Sem resposta, reescreva ou descarte.
71
72
  - **Artefato não é comportamento.** Cenário que cita `grep`, "contém a string", "o binário", "nenhum arquivo", "arquivo/pasta/diretório em" ou glob `**/` descreve o repositório. Tente o efeito em runtime; se o artefato É o entregável, justifique em `> Por que artefato:` logo abaixo (nunca na mesma linha, nunca `> [!note]`).
72
73
  - **Saída de LLM** (o que o modelo responde) prova-se por `eval` de runtime, nunca pelo texto do prompt: evidência em `3-implement/evidencias/{Cn}.md`, path listado em `arquivos:` da fatia; com nó `runner: "evals"` (standard `ai-agents-evals-with-cache`) o cenário mora no dataset. No máximo 1 teste de estrutura da fonte do prompt.
73
74
  - **"O comando de teste da raiz roda de ponta a ponta"** prova-se pelo `verify {feature}` (o nó `e2e` e o nó de teste `e2e: true` já percorrem a cadeia): nomeie essa rodada como a prova, nunca uma execução avulsa. Precisando mesmo rodar fora dele, numa linha só (o Bash não guarda o env entre chamadas): `eval "$(morph-spec env {feature})" && morph-spec heavy run -- <comando>` — o `COMPOSE_PROJECT_NAME` da feature é o que o `finish` derruba com os volumes. `prova: manual` registra passo e resultado em `3-implement/evidencias/{Cn}.md`.
74
- - **Orçamento de teste**: um teste por cenário, um de regressão por bug; acima disso, uma linha de porquê. Cenários do mesmo comportamento são linhas de tabela (`[Theory]`/`it.each`); nenhum teste que só fixa texto. O `verify` avisa quando o teste passa de 1,5× a produção.
75
+ - **Orçamento de teste**: um teste por cenário de risco — o cenário é a unidade, nunca a pós-condição nem o componente; variantes são linhas de tabela (`[Theory]`/`it.each`); acima disso, o porquê na linha do cenário. Nenhum teste que só fixa texto, prompt ou constante, nenhum de DTO/mapping, e o mesmo comportamento numa camada só (handler **ou** endpoint). Teste além do orçamento é escopo para o revisor. O `verify` avisa quando o teste passa da produção.
75
76
 
76
77
  ```markdown
77
78
  ## Cenários
78
79
  - **C1** — Dado um tenant com `criterion_overrides`, quando ele é publicado, então a publicação é recusada nomeando a chave.
79
80
  prova: teste
81
+ risco: contrato
80
82
  - **C2** — Nenhum arquivo em `tenants/**` contém a chave `criterion_overrides`.
81
- prova: teste
83
+ prova: leitura
82
84
  > Por que artefato: a chave saiu do contrato; nenhum leitor de runtime a consulta.
83
85
 
84
86
  ## Fatias
@@ -136,7 +138,7 @@ A pergunta **nomeia a feature**: é por ela que o hook `state-sync` grava a apro
136
138
 
137
139
  ## 8. Deltas por tipo
138
140
 
139
- - **`bug` (bug-lean):** a varredura roda inteira; a árvore é de repro, diagnóstico e comportamento do sistema, não de negócio; no modo automático, no máximo 1 pergunta de repro, e só se o pedido não deixar claro. `## Problema` traz repro, diagnóstico e causa provável; os cenários são o sintoma ausente e a regressão; `### Conflitos com o acervo` continua presente.
141
+ - **`bug` (bug-lean):** a varredura roda inteira; a árvore é de repro, diagnóstico e comportamento do sistema, não de negócio; no modo automático, no máximo 1 pergunta de repro, e só se o pedido não deixar claro. `## Problema` traz repro, diagnóstico e causa provável; um cenário é o sintoma ausente com `prova: teste` e `risco: regressão` (o `plan-check` recusa bug sem ele) — esse é o teste que fica; `### Conflitos com o acervo` continua presente.
140
142
  - **`chore`:** nasce com o gate `plan` carimbado por política. Escreva o plan.md curto numa única escrita (o gate já aprovado congela o arquivo depois de criado): `## Problema`, um ou dois cenários, `## Decisões` só com `### Conflitos com o acervo`, uma fatia. `plan-check` e `Skill(morph-apply)`.
141
143
  - **`hotfix`:** é da `morph-hotfix`.
142
144
 
@@ -4,7 +4,7 @@ description: Conduz o Gate 3 — despacha verificador ∥ revisor (∥ seguranç
4
4
  version: core
5
5
  when_to_use: Use quando a fase review começar, com todas as fatias com relatório final e o verify da feature verde
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.2.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -32,14 +32,15 @@ Commite o trabalho da feature (`git add <paths>` + `git commit`) e rode
32
32
  `morph-spec verify {feature} --ci --json` com `run_in_background`. Ele empurra a branch, espera o run
33
33
  e grava o stamp do artefato que o run publicou; sem CI, roda local e diz por quê.
34
34
  **Sem `--filter`, sempre**: o Gate 3 roda a suíte inteira. Esta é a única execução de testes do Gate 3 — nenhum agente
35
- roda a suíte de novo. `repeatOf` no JSON = nada mudou, use aquela rodada; `reason: "stamp-reused"` = sem CI e
36
- nada fora de `.morph/` andou desde o último verify verde: ele vale, não rode de novo.
35
+ roda a suíte de novo. `repeatOf` no JSON = nada mudou, use aquela rodada; `reason: "stamp-reused"` = nada
36
+ fora de `.morph/` andou desde o último verify verde (local ou CI): ele vale e o CI não roda de novo.
37
37
  - **Quando a saída mostrar `CI rodando`, despache (passo 3) sem esperar o fim**: o push e o
38
38
  workflow já estão commitados e a foto da árvore não os acusa. Saiu `verify local`: espere o fim
39
39
  antes do passo 3 — suíte local e `mutate` disputam a máquina. Exit 2 = trabalho fora de commit.
40
40
  - Nó `e2e`: `pass` local → `morph-spec e2e status {feature} --json` → `baseUrl` vai ao verificador.
41
41
  `fail` → correção (CI: `gh run view <id> --log-failed`). `skip` → quem classifica é o `gate-check`
42
- (`classifyE2eSkip`): dispensa de projeto passa; ambiente, `--skip-e2e` ou razão desconhecida pausam.
42
+ (`classifyE2eSkip`): dispensa de projeto e `not applicable` (feature sem UI/spec/container no diff e sem
43
+ `prova: e2e`) passam; ambiente, `--skip-e2e` ou razão desconhecida pausam.
43
44
  - `traceability.warnings` do verify vão ao revisor como sinal.
44
45
 
45
46
  ## 3. Dispatch — verificador ∥ revisor (∥ segurança)
@@ -49,7 +50,8 @@ Anote `base` (merge-base com a default) e `sha` (`git rev-parse HEAD`). Blast ra
49
50
  símbolo público do diff, `morph-spec graph affected <símbolo> --prod --json`; sem grafo
50
51
  (`no-graph`/`not-found`/`ambiguous`), siga sem o blast radius — o revisor cai para `git grep` — e
51
52
  registre a ausência no recado a ele. Cada agente nasce com contexto fresco e **nunca recebe relatório anterior** (ancoragem).
52
- Despache todos na mesma resposta:
53
+ `chore` tem review leve: só o revisor (∥ segurança, quando exigida) — sem verificador, sem cobertura de
54
+ `Cn`, sem mutação. Despache todos na mesma resposta:
53
55
  ```
54
56
  dispatch Agent (subagent_type: "morph-verifier", model: {reviewModels.verifier.model}) with prompt:
55
57
  feature, base, sha, JSON do verify (nós, e2e, URL) se já terminou, 1-design/mockups.md e
@@ -92,7 +94,8 @@ retomar, despache um novo.
92
94
  ## 5. Bloqueadores
93
95
  O avaliador também erra: **antes de editar, reverifique cada bloqueador no código** — evidência que
94
96
  não se reproduz vira pergunta ao mesmo agente, não correção. Confirmado: corrija (loop da
95
- `morph-apply`), commite, rode o `verify {feature} --ci` de novo e uma foto `--before-eval` nova.
97
+ `morph-apply`), commite, rode `verify {feature}` local (o stamp verde dele é a prova: um `--ci` depois
98
+ reusa, não roda de novo) e uma foto `--before-eval` nova.
96
99
  Retome por `SendMessage` TODO agente exigido, com o `sha` novo e `tree.dirty`/`tree.commits`: quem
97
100
  abriu bloqueadores **confirme só os bloqueadores** (retomando o MESMO agente que os abriu); os outros
98
101
  julgam só o delta `{sha anterior}..{sha novo}` — o que a correção abriu. `reviewModels.security.required`
@@ -4,7 +4,7 @@ description: Localiza um standard específico via busca server-side (morph-spec
4
4
  version: core
5
5
  when_to_use: Use quando precisar consultar um standard específico durante a implementação
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.2.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX de uma feature com plano aprovado
4
4
  version: core
5
5
  when_to_use: Use quando o ramo de design for tomado (morph-spec advance {feature} --design, depois do gate do plano) e antes de /morph-apply
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.2.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux
@@ -966,7 +966,7 @@
966
966
  "errorbody": "#rule-2--error-path-tests-assert-the-response-body-not-just-the-status-code",
967
967
  "test-budget": "#test-budget"
968
968
  },
969
- "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. Test budget per task: one test per postcondition plus one regression test per bug, more only with a one-line justification in the task; scenarios of the same behavior (edge cases included) are rows of one [Theory], never copied [Fact]s; no test whose only assertion is a copy, config or prose string (that is an analyzer or a publish-time validation, outside the suite count); shared fixtures before new helpers; bugs and pure logic get tests in any order — the implementer runs its own filter once, never red-green cycles nor production mutation (the Gate 3 mutation proof shows the test bites); a surviving mutant is killed by tightening an assertion or adding a row, not by a new test."
969
+ "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. Tests are paid for by risk, not new code: one test per plan scenario with risco: (rule, money, contract, concurrency, data, regression) plus the regression test of the bug fixed, more only with a one-line justification on the scenario; a scenario without risk (CRUD, DTO, mapping, wiring, constants) is prova: leitura and gets no test; one layer per behavior (handler or endpoint, not both); scenarios of the same behavior (edge cases included) are rows of one [Theory], never copied [Fact]s; no test whose only assertion is a copy, config or prose string (that is an analyzer or a publish-time validation, outside the suite count); shared fixtures before new helpers; bugs and risky scenarios get tests in any order — the implementer runs its own filter once, never red-green cycles nor production mutation (the Gate 3 mutation proof shows the test bites); a surviving mutant is killed by tightening an assertion or adding a row, not by a new test."
970
970
  },
971
971
  {
972
972
  "id": "backend-dotnet-vsa-handler-patterns",
@@ -1516,7 +1516,7 @@
1516
1516
  "e2e": "#test-file-co-location",
1517
1517
  "test-budget": "#test-budget"
1518
1518
  },
1519
- "digest": "Vitest for unit tests, React Testing Library for component tests. Mock next/navigation with vi.mock. Test Server Actions with direct function calls. Use MSW for API mocking in component tests. Playwright for E2E tests against running app. Never test implementation details — test user behavior and output. Test budget per task: one test per postcondition plus one regression test per bug, more only with a one-line justification in the task; scenarios of the same behavior are rows of one it.each, never copied it() blocks; no test whose only assertion is a copy string (lint or publish-time validation instead); shared wrappers, MSW handlers and factories before new helpers; bugs and pure logic get tests in any order — the implementer runs its own filter once, never red-green cycles nor production mutation."
1519
+ "digest": "Vitest for unit tests, React Testing Library for component tests. Mock next/navigation with vi.mock. Test Server Actions with direct function calls. Use MSW for API mocking in component tests. Playwright for E2E tests against running app. Never test implementation details — test user behavior and output. Tests are paid for by risk, not new code: one test per plan scenario with risco: plus the regression test of the bug fixed, more only with a one-line justification on the scenario; a component is not a budget unit — rendering, layout and copy are prova: leitura or e2e; scenarios of the same behavior are rows of one it.each, never copied it() blocks; no test whose only assertion is a copy string (lint or publish-time validation instead); shared wrappers, MSW handlers and factories before new helpers; bugs and risky scenarios get tests in any order — the implementer runs its own filter once, never red-green cycles nor production mutation."
1520
1520
  },
1521
1521
  {
1522
1522
  "id": "frontend-scroll-driven-frame-scrub",
@@ -115,8 +115,12 @@ respectively and the test suite stays green.
115
115
 
116
116
  ## Test budget
117
117
 
118
- The plan (`morph-plan`) gives each task a budget: **one test per postcondition, plus one
119
- regression test per bug fixed**. Going above it takes a one-line justification in the task.
118
+ Tests are paid for by **risk, not by new code**. The plan (`morph-plan`) marks each scenario
119
+ with `prova:` and, for a test, a `risco:` (business rule, money, external contract, concurrency,
120
+ data integrity, regression). The budget is **one test per risky scenario, plus the regression test of
121
+ the bug being fixed**; going above it takes a one-line justification on the scenario. A scenario
122
+ without risk — CRUD, DTO, mapping, wiring, constants, layout — is `prova: leitura`: the Gate 3
123
+ verifier proves it by reading the code, and it gets no test.
120
124
 
121
125
  - **Table before copy.** Scenarios of the same behavior — including its edge cases (null, empty,
122
126
  boundary) — are rows of one `[Theory]` (`[InlineData]`/`[MemberData]`), never N `[Fact]` methods
@@ -125,11 +129,13 @@ regression test per bug fixed**. Going above it takes a one-line justification i
125
129
  UI copy, tenant config or prose proves the file's version, not behavior. Enforce it with an
126
130
  analyzer, a lint rule or a validation at publish time, outside the suite count. The versioned
127
131
  prompt case (at most one structural test) is `ai-agents-testing-ai`.
132
+ - **One layer per behavior.** A rule tested in the handler is not tested again through the
133
+ endpoint or the SQL integration: pick the cheapest layer that bites (wiring is the verifier's).
128
134
  - **Shared fixture by default.** Reuse the project's `WebApplicationFactory` fixture, fakes and
129
135
  builders. Before writing a new helper, look for the existing one (`morph-spec graph explain
130
136
  <Name>`; fallback without a graph: `grep -rn`).
131
- - **Order is free, proof is not the implementer's:** a bug gets a regression test, and pure logic
132
- gets its tests, in whatever order the code comes out. The implementer does not run red→green
137
+ - **Order is free, proof is not the implementer's:** a bug gets a regression test, and a risky
138
+ scenario gets its test, in whatever order the code comes out. The implementer does not run red→green
133
139
  cycles nor mutate production code to watch a test fail; it runs its own filter once before the
134
140
  report, and the Gate 3 mutation proof is what shows the test bites.
135
141
  - **A surviving mutant** of the Gate 3 mutation proof is killed by tightening an existing assertion
@@ -17,7 +17,7 @@ Vitest + React Testing Library for component tests. MSW for API mocking. Co-loca
17
17
  - ALWAYS use `@testing-library/user-event` for interactions — not `fireEvent`
18
18
  - ALWAYS mock the API layer with MSW — never mock `fetch` directly
19
19
  - NEVER test implementation details — test what the user sees
20
- - ALWAYS test the happy path + one error path per component
20
+ - ONLY test a component when a plan scenario with `risco:` lives in it — a component is not a unit of the budget
21
21
 
22
22
  ## Test File Co-location
23
23
 
@@ -103,9 +103,13 @@ export function QueryClientWrapper({ children }: { children: React.ReactNode })
103
103
 
104
104
  ## Test Budget
105
105
 
106
- The plan (`morph-plan`) gives each task a budget: **one test per postcondition, plus one
107
- regression test per bug fixed**. Going above it takes a one-line justification in the task. The
108
- happy path + one error path per component above is that budget applied to a component.
106
+ Tests are paid for by **risk, not by new code**. The plan (`morph-plan`) marks each scenario
107
+ with `prova:` and, for a test, a `risco:` (business rule, money, external contract, concurrency,
108
+ data integrity, regression). The budget is **one test per risky scenario, plus the regression test of
109
+ the bug being fixed**; going above it takes a one-line justification on the scenario. A scenario
110
+ without risk — CRUD, DTO, mapping, wiring, constants, layout — is `prova: leitura`: the Gate 3
111
+ verifier proves it by reading the code, and it gets no test. Rendering, layout and copy are
112
+ `leitura` or `e2e`, never a unit test per component.
109
113
 
110
114
  - **Table before copy.** Scenarios of the same behavior — edge cases included — are rows of one
111
115
  `it.each`/`test.each`, never N `it()` blocks that differ by one value.
@@ -116,8 +120,8 @@ happy path + one error path per component above is that budget applied to a comp
116
120
  - **Shared helpers by default.** Reuse the existing wrappers (`test/helpers.tsx`), MSW handlers and
117
121
  factories. Before writing a new one, look for it (`morph-spec graph explain <Name>`; fallback
118
122
  without a graph: `grep -rn`).
119
- - **Order is free, proof is not the implementer's:** a bug gets a regression test, and pure logic
120
- (formatters, reducers, schema) gets its tests, in whatever order the code comes out. The
123
+ - **Order is free, proof is not the implementer's:** a bug gets a regression test, and a risky
124
+ scenario (a formatter of money, a reducer with rules, a schema) gets its test, in whatever order the code comes out. The
121
125
  implementer does not run red→green cycles nor mutate production code to watch a test fail; it
122
126
  runs its own filter once before the report, and the Gate 3 mutation proof shows the test bites.
123
127
 
@@ -0,0 +1,92 @@
1
+ <templateSet group="MORPH Templates">
2
+ <template name="morph-pascal" value="{{pascalCase $FEATURE_NAME$}}" description="Convert to PascalCase (UserAuth)" toReformat="false" toShortenFQNames="true">
3
+ <variable name="FEATURE_NAME" expression="" defaultValue="&quot;FEATURE_NAME&quot;" alwaysStopAt="true" />
4
+ <context>
5
+ <option name="OTHER" value="true" />
6
+ </context>
7
+ </template>
8
+
9
+ <template name="morph-camel" value="{{camelCase $FEATURE_NAME$}}" description="Convert to camelCase (userAuth)" toReformat="false" toShortenFQNames="true">
10
+ <variable name="FEATURE_NAME" expression="" defaultValue="&quot;FEATURE_NAME&quot;" alwaysStopAt="true" />
11
+ <context>
12
+ <option name="OTHER" value="true" />
13
+ </context>
14
+ </template>
15
+
16
+ <template name="morph-snake" value="{{snakeCase $FEATURE_NAME$}}" description="Convert to snake_case (user_auth)" toReformat="false" toShortenFQNames="true">
17
+ <variable name="FEATURE_NAME" expression="" defaultValue="&quot;FEATURE_NAME&quot;" alwaysStopAt="true" />
18
+ <context>
19
+ <option name="OTHER" value="true" />
20
+ </context>
21
+ </template>
22
+
23
+ <template name="morph-title" value="{{titleCase $FEATURE_NAME$}}" description="Convert to Title Case (User Auth)" toReformat="false" toShortenFQNames="true">
24
+ <variable name="FEATURE_NAME" expression="" defaultValue="&quot;FEATURE_NAME&quot;" alwaysStopAt="true" />
25
+ <context>
26
+ <option name="OTHER" value="true" />
27
+ </context>
28
+ </template>
29
+
30
+ <template name="morph-kebab" value="{{kebabCase $FEATURE_NAME$}}" description="Convert to kebab-case (user-auth)" toReformat="false" toShortenFQNames="true">
31
+ <variable name="FEATURE_NAME" expression="" defaultValue="&quot;FEATURE_NAME&quot;" alwaysStopAt="true" />
32
+ <context>
33
+ <option name="OTHER" value="true" />
34
+ </context>
35
+ </template>
36
+
37
+ <template name="morph-if" value="{{#if $CONDITION$}}&#10; $END$&#10;{{/if}}" description="Handlebars if block" toReformat="false" toShortenFQNames="true">
38
+ <variable name="CONDITION" expression="" defaultValue="&quot;condition&quot;" alwaysStopAt="true" />
39
+ <context>
40
+ <option name="OTHER" value="true" />
41
+ </context>
42
+ </template>
43
+
44
+ <template name="morph-if-else" value="{{#if $CONDITION$}}&#10; $TRUE_BRANCH$&#10;{{else}}&#10; $FALSE_BRANCH$&#10;{{/if}}" description="Handlebars if-else block" toReformat="false" toShortenFQNames="true">
45
+ <variable name="CONDITION" expression="" defaultValue="&quot;condition&quot;" alwaysStopAt="true" />
46
+ <variable name="TRUE_BRANCH" expression="" defaultValue="&quot;&quot;" alwaysStopAt="true" />
47
+ <variable name="FALSE_BRANCH" expression="" defaultValue="&quot;&quot;" alwaysStopAt="true" />
48
+ <context>
49
+ <option name="OTHER" value="true" />
50
+ </context>
51
+ </template>
52
+
53
+ <template name="morph-each" value="{{#each $ITEMS$}}&#10; {{this.$PROPERTY$}}&#10;{{/each}}" description="Handlebars each loop" toReformat="false" toShortenFQNames="true">
54
+ <variable name="ITEMS" expression="" defaultValue="&quot;items&quot;" alwaysStopAt="true" />
55
+ <variable name="PROPERTY" expression="" defaultValue="&quot;name&quot;" alwaysStopAt="true" />
56
+ <context>
57
+ <option name="OTHER" value="true" />
58
+ </context>
59
+ </template>
60
+
61
+ <template name="morph-service" value="namespace {{NAMESPACE}}.Application.Features.{{pascalCase FEATURE_NAME}}.Services;&#10;&#10;public class {{pascalCase FEATURE_NAME}}Service(&#10; I{{pascalCase FEATURE_NAME}}Repository repository,&#10; ILogger&lt;{{pascalCase FEATURE_NAME}}Service&gt; logger) : I{{pascalCase FEATURE_NAME}}Service&#10;{&#10; public async Task&lt;{{pascalCase FEATURE_NAME}}Dto?&gt; GetByIdAsync(int id, CancellationToken cancellationToken = default)&#10; {&#10; $END$&#10; }&#10;}" description="MORPH .NET Service class template" toReformat="false" toShortenFQNames="true">
62
+ <context>
63
+ <option name="C_SHARP" value="true" />
64
+ </context>
65
+ </template>
66
+
67
+ <template name="morph-repository" value="public class {{pascalCase FEATURE_NAME}}Repository(AppDbContext context) : I{{pascalCase FEATURE_NAME}}Repository&#10;{&#10; private readonly DbSet&lt;{{pascalCase FEATURE_NAME}}&gt; _dbSet = context.Set&lt;{{pascalCase FEATURE_NAME}}&gt;();&#10;&#10; public async Task&lt;{{pascalCase FEATURE_NAME}}?&gt; GetByIdAsync(int id, CancellationToken cancellationToken = default)&#10; {&#10; $END$&#10; }&#10;}" description="MORPH .NET Repository class template" toReformat="false" toShortenFQNames="true">
68
+ <context>
69
+ <option name="C_SHARP" value="true" />
70
+ </context>
71
+ </template>
72
+
73
+ <template name="morph-component" value="@page &quot;/{{kebabCase FEATURE_NAME}}&quot;&#10;@inject I{{pascalCase FEATURE_NAME}}Service {{pascalCase FEATURE_NAME}}Service&#10;@inject ILogger&lt;{{pascalCase FEATURE_NAME}}Page&gt; Logger&#10;&#10;&lt;PageTitle&gt;{{pascalCase FEATURE_NAME}}&lt;/PageTitle&gt;&#10;&#10;&lt;div class=&quot;container-fluid&quot;&gt;&#10; $END$&#10;&lt;/div&gt;&#10;&#10;@code {&#10;&#10;}" description="MORPH Blazor component template" toReformat="false" toShortenFQNames="true">
74
+ <context>
75
+ <option name="HTML" value="true" />
76
+ </context>
77
+ </template>
78
+
79
+ <template name="morph-migration" value="public partial class Add{{pascalCase FEATURE_NAME}} : Migration&#10;{&#10; protected override void Up(MigrationBuilder migrationBuilder)&#10; {&#10; migrationBuilder.CreateTable(&#10; name: &quot;{{pascalCase FEATURE_NAME}}s&quot;,&#10; columns: table =&gt; new&#10; {&#10; Id = table.Column&lt;int&gt;(type: &quot;int&quot;, nullable: false)&#10; .Annotation(&quot;SqlServer:Identity&quot;, &quot;1, 1&quot;),&#10; $END$&#10; },&#10; constraints: table =&gt;&#10; {&#10; table.PrimaryKey(&quot;PK_{{pascalCase FEATURE_NAME}}s&quot;, x =&gt; x.Id);&#10; });&#10; }&#10;&#10; protected override void Down(MigrationBuilder migrationBuilder)&#10; {&#10; migrationBuilder.DropTable(name: &quot;{{pascalCase FEATURE_NAME}}s&quot;);&#10; }&#10;}" description="MORPH EF Core migration template" toReformat="false" toShortenFQNames="true">
80
+ <context>
81
+ <option name="C_SHARP" value="true" />
82
+ </context>
83
+ </template>
84
+
85
+ <template name="morph-ts-contract" value="export interface {{pascalCase FEATURE_NAME}} {&#10; id: number;&#10; $FIELD1$&#10;}&#10;&#10;export interface Create{{pascalCase FEATURE_NAME}}Request {&#10; $FIELD2$&#10;}&#10;&#10;export interface Update{{pascalCase FEATURE_NAME}}Request {&#10; $END$&#10;}" description="MORPH TypeScript contract template" toReformat="false" toShortenFQNames="true">
86
+ <variable name="FIELD1" expression="" defaultValue="&quot;&quot;" alwaysStopAt="true" />
87
+ <variable name="FIELD2" expression="" defaultValue="&quot;&quot;" alwaysStopAt="true" />
88
+ <context>
89
+ <option name="TypeScript" value="true" />
90
+ </context>
91
+ </template>
92
+ </templateSet>
@@ -0,0 +1,186 @@
1
+ {
2
+ "MORPH Template - PascalCase": {
3
+ "prefix": "morph-pascal",
4
+ "body": ["{{pascalCase ${1:FEATURE_NAME}}}"],
5
+ "description": "Convert to PascalCase (UserAuth)"
6
+ },
7
+ "MORPH Template - camelCase": {
8
+ "prefix": "morph-camel",
9
+ "body": ["{{camelCase ${1:FEATURE_NAME}}}"],
10
+ "description": "Convert to camelCase (userAuth)"
11
+ },
12
+ "MORPH Template - snake_case": {
13
+ "prefix": "morph-snake",
14
+ "body": ["{{snakeCase ${1:FEATURE_NAME}}}"],
15
+ "description": "Convert to snake_case (user_auth)"
16
+ },
17
+ "MORPH Template - Title Case": {
18
+ "prefix": "morph-title",
19
+ "body": ["{{titleCase ${1:FEATURE_NAME}}}"],
20
+ "description": "Convert to Title Case (User Auth)"
21
+ },
22
+ "MORPH Template - kebab-case": {
23
+ "prefix": "morph-kebab",
24
+ "body": ["{{kebabCase ${1:FEATURE_NAME}}}"],
25
+ "description": "Convert to kebab-case (user-auth)"
26
+ },
27
+ "MORPH Template - UPPER_SNAKE_CASE": {
28
+ "prefix": "morph-upper-snake",
29
+ "body": ["{{upperSnakeCase ${1:FEATURE_NAME}}}"],
30
+ "description": "Convert to UPPER_SNAKE_CASE (USER_AUTH)"
31
+ },
32
+ "MORPH Template - Conditional If": {
33
+ "prefix": "morph-if",
34
+ "body": [
35
+ "{{#if ${1:condition}}}",
36
+ " $2",
37
+ "{{/if}}"
38
+ ],
39
+ "description": "Handlebars if block"
40
+ },
41
+ "MORPH Template - Conditional If-Else": {
42
+ "prefix": "morph-if-else",
43
+ "body": [
44
+ "{{#if ${1:condition}}}",
45
+ " $2",
46
+ "{{else}}",
47
+ " $3",
48
+ "{{/if}}"
49
+ ],
50
+ "description": "Handlebars if-else block"
51
+ },
52
+ "MORPH Template - Each Loop": {
53
+ "prefix": "morph-each",
54
+ "body": [
55
+ "{{#each ${1:items}}}",
56
+ " {{this.$2}}",
57
+ "{{/each}}"
58
+ ],
59
+ "description": "Handlebars each loop"
60
+ },
61
+ "MORPH Template - Unless": {
62
+ "prefix": "morph-unless",
63
+ "body": [
64
+ "{{#unless ${1:condition}}}",
65
+ " $2",
66
+ "{{/unless}}"
67
+ ],
68
+ "description": "Handlebars unless block"
69
+ },
70
+ "MORPH Template - .NET Service Class": {
71
+ "prefix": "morph-service",
72
+ "body": [
73
+ "namespace {{NAMESPACE}}.Application.Features.{{pascalCase FEATURE_NAME}}.Services;",
74
+ "",
75
+ "public class {{pascalCase FEATURE_NAME}}Service(",
76
+ " I{{pascalCase FEATURE_NAME}}Repository repository,",
77
+ " ILogger<{{pascalCase FEATURE_NAME}}Service> logger) : I{{pascalCase FEATURE_NAME}}Service",
78
+ "{",
79
+ " public async Task<{{pascalCase FEATURE_NAME}}Dto?> GetByIdAsync(int id, CancellationToken cancellationToken = default)",
80
+ " {",
81
+ " $1",
82
+ " }",
83
+ "}"
84
+ ],
85
+ "description": "MORPH .NET Service class template"
86
+ },
87
+ "MORPH Template - .NET Repository Class": {
88
+ "prefix": "morph-repository",
89
+ "body": [
90
+ "public class {{pascalCase FEATURE_NAME}}Repository(AppDbContext context) : I{{pascalCase FEATURE_NAME}}Repository",
91
+ "{",
92
+ " private readonly DbSet<{{pascalCase FEATURE_NAME}}> _dbSet = context.Set<{{pascalCase FEATURE_NAME}}>();",
93
+ "",
94
+ " public async Task<{{pascalCase FEATURE_NAME}}?> GetByIdAsync(int id, CancellationToken cancellationToken = default)",
95
+ " {",
96
+ " $1",
97
+ " }",
98
+ "}"
99
+ ],
100
+ "description": "MORPH .NET Repository class template"
101
+ },
102
+ "MORPH Template - Blazor Component": {
103
+ "prefix": "morph-component",
104
+ "body": [
105
+ "@page \"/{{kebabCase FEATURE_NAME}}\"",
106
+ "@inject I{{pascalCase FEATURE_NAME}}Service {{pascalCase FEATURE_NAME}}Service",
107
+ "@inject ILogger<{{pascalCase FEATURE_NAME}}Page> Logger",
108
+ "",
109
+ "<PageTitle>{{pascalCase FEATURE_NAME}}</PageTitle>",
110
+ "",
111
+ "<div class=\"container-fluid\">",
112
+ " $1",
113
+ "</div>",
114
+ "",
115
+ "@code {",
116
+ " $2",
117
+ "}"
118
+ ],
119
+ "description": "MORPH Blazor component template"
120
+ },
121
+ "MORPH Template - EF Core Migration": {
122
+ "prefix": "morph-migration",
123
+ "body": [
124
+ "public partial class Add{{pascalCase FEATURE_NAME}} : Migration",
125
+ "{",
126
+ " protected override void Up(MigrationBuilder migrationBuilder)",
127
+ " {",
128
+ " migrationBuilder.CreateTable(",
129
+ " name: \"{{pascalCase FEATURE_NAME}}s\",",
130
+ " columns: table => new",
131
+ " {",
132
+ " Id = table.Column<int>(type: \"int\", nullable: false)",
133
+ " .Annotation(\"SqlServer:Identity\", \"1, 1\"),",
134
+ " $1",
135
+ " },",
136
+ " constraints: table =>",
137
+ " {",
138
+ " table.PrimaryKey(\"PK_{{pascalCase FEATURE_NAME}}s\", x => x.Id);",
139
+ " });",
140
+ " }",
141
+ "",
142
+ " protected override void Down(MigrationBuilder migrationBuilder)",
143
+ " {",
144
+ " migrationBuilder.DropTable(name: \"{{pascalCase FEATURE_NAME}}s\");",
145
+ " }",
146
+ "}"
147
+ ],
148
+ "description": "MORPH EF Core migration template"
149
+ },
150
+ "MORPH Template - Hangfire Job": {
151
+ "prefix": "morph-job",
152
+ "body": [
153
+ "public class {{pascalCase FEATURE_NAME}}ProcessorJob(",
154
+ " I{{pascalCase FEATURE_NAME}}Service service,",
155
+ " ILogger<{{pascalCase FEATURE_NAME}}ProcessorJob> logger) : I{{pascalCase FEATURE_NAME}}ProcessorJob",
156
+ "{",
157
+ " [AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]",
158
+ " [Queue(\"default\")]",
159
+ " [JobDisplayName(\"{{pascalCase FEATURE_NAME}} Processing - ID: {0}\")]",
160
+ " public async Task ExecuteAsync(int id, CancellationToken cancellationToken)",
161
+ " {",
162
+ " $1",
163
+ " }",
164
+ "}"
165
+ ],
166
+ "description": "MORPH Hangfire job template"
167
+ },
168
+ "MORPH Template - TypeScript Contract": {
169
+ "prefix": "morph-ts-contract",
170
+ "body": [
171
+ "export interface {{pascalCase FEATURE_NAME}} {",
172
+ " id: number;",
173
+ " $1",
174
+ "}",
175
+ "",
176
+ "export interface Create{{pascalCase FEATURE_NAME}}Request {",
177
+ " $2",
178
+ "}",
179
+ "",
180
+ "export interface Update{{pascalCase FEATURE_NAME}}Request {",
181
+ " $3",
182
+ "}"
183
+ ],
184
+ "description": "MORPH TypeScript contract template"
185
+ }
186
+ }