@polymorphism-tech/morph-spec 9.7.0 → 9.8.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 (54) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +1 -1
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/agents.json +1 -1
  8. package/framework/docs/CLI.md +18 -9
  9. package/framework/evals/review-checklist.md +3 -3
  10. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  11. package/framework/hooks/claude-code/statusline.sh +11 -11
  12. package/framework/hooks/shared/heavy-slots.js +41 -4
  13. package/framework/schemas/config.schema.json +1 -1
  14. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  15. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  16. package/framework/skills/core/morph-apply/SKILL.md +2 -2
  17. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  18. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  19. package/framework/skills/core/morph-media/SKILL.md +1 -1
  20. package/framework/skills/core/morph-plan/SKILL.md +2 -2
  21. package/framework/skills/core/morph-review/SKILL.md +1 -1
  22. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  23. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  24. package/framework/templates/.idea/morph-templates.xml +92 -0
  25. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  26. package/framework/templates/IDE-SNIPPETS.md +262 -262
  27. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  28. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  29. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  30. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  31. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  32. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  33. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  34. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  35. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  36. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  37. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  38. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  39. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  40. package/framework/templates/state.template.json +17 -17
  41. package/framework/templates/ui/design-system.css +226 -226
  42. package/package.json +1 -1
  43. package/src/commands/project/worktree.js +24 -2
  44. package/src/commands/state/finish.js +43 -1
  45. package/src/commands/state/report-check.js +36 -1
  46. package/src/commands/validation/verify.js +3 -0
  47. package/src/dashboard/app/main.jsx +13 -13
  48. package/src/dashboard/vite.config.js +24 -24
  49. package/src/lib/heavy/gate.js +22 -1
  50. package/src/lib/review-reports.js +5 -3
  51. package/src/lib/tasks/test-nodes.js +24 -6
  52. package/src/lib/tasks/test-runner.js +40 -1
  53. package/src/lib/validators/verify-runner.js +50 -12
  54. package/src/utils/agents-installer.js +1 -1
package/CLAUDE.md CHANGED
@@ -13,7 +13,7 @@ This repo is the **framework source code** — not a project that uses morph-spe
13
13
  ```bash
14
14
  # Once per clone: the dashboard render smoke test (test/dashboard/pages-smoke.test.js)
15
15
  # needs the dashboard's own deps and FAILS without them — CI installs them too
16
- npm install --prefix src/dashboard
16
+ cd src/dashboard && npm install
17
17
 
18
18
  # Dev loop: only the test files the current change can reach (vs the merge-base with main)
19
19
  npm run test:changed # add -- --list to see the selection and why
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:** 9.7.0
6
+ **Version:** 9.8.0
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -380,4 +380,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
380
380
 
381
381
  ---
382
382
 
383
- *morph-spec v9.7.0 by [Polymorphism Tech](https://polymorphism.tech)*
383
+ *morph-spec v9.8.0 by [Polymorphism Tech](https://polymorphism.tech)*
package/bin/morph-spec.js CHANGED
@@ -344,7 +344,7 @@ program
344
344
  .option('--filter <ids>', 'Run only the test nodes whose id matches (comma-separated; exact or substring). Unmatched nodes are reported as skip; matching nothing is a usage error (exit 2)')
345
345
  .option('--linux', 'Run the test nodes inside a Linux container, on a COPY of the tree (the host tree is mounted read-only and copied in — a bind mount is case-insensitive and cannot reproduce case defects). No Docker = usage error, never a skip')
346
346
  .option('--linux-image <img>', 'Image used by --linux (default: mcr.microsoft.com/dotnet/sdk:10.0)')
347
- .option('--record-baseline', 'Measure every test node and write expectedCount back into .morph/config/config.json (only from an all-green run; never creates config.json)')
347
+ .option('--record-baseline', 'Measure every test node and write expectedCount of the node-test/evals nodes back into .morph/config/config.json, removing it from the other runners (only from an all-green run; never creates config.json)')
348
348
  .option('--refresh-cache', 'Rewrite the response cache of every `runner: "evals"` node against the real provider. NEVER implicit: a cache rewrite is a paid call nobody asked for. Requires the declared envFile; no eval node declared, --skip-tests, or a --filter that excludes them all are usage errors (exit 2), refused BEFORE the cache is touched')
349
349
  .option('--skip-tests', 'Skip the test node. The run is partial, so the verification stamp is NOT written (reason "skip-tests")')
350
350
  .option('--skip-build', 'Skip the build node. The run is partial, so the verification stamp is NOT written (reason "skip-build")')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "9.7.0",
3
+ "version": "9.8.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 v9.7.0 by Polymorphism Tech*
188
+ *morph-spec v9.8.0 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v9.7.0 by Polymorphism Tech*
252
+ *morph-spec v9.8.0 by Polymorphism Tech*
@@ -398,7 +398,7 @@
398
398
  ],
399
399
  "model": "sonnet",
400
400
  "maxTurns": 60,
401
- "persona": "Você é o revisor independente do Gate 3, read-only. Leia como o arquiteto que herda este repo amanhã: simples, legível, fácil de estender. Não participou da implementação e não prova cenários (é o verificador). Não roda teste nem `verify` (o hook `test-budget` nega). Você lê SÓ o commit revisado: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, `git log {base}..{sha}` — nunca a árvore de trabalho (Read/Glob/Grep sobre `src/` leem o disco, onde o verificador pode estar com um mutante aplicado). `base` e `sha` vêm no dispatch. Julga o diff em cinco dimensões, na seção \"Revisor\" de `.morph/framework/evals/review-checklist.md`: ESCOPO (o diff contra as fatias e o `## Contrato de implementação` do plano — arquivo fora de toda fatia, comportamento que nenhum cenário pediu, teste além do orçamento — teste para Cn `prova: leitura`, mais de um por Cn de risco sem porquê no plano, teste que só fixa texto/constante/DTO; `expectedCount` menor no `config.json` sem teste apagado no mesmo diff é `regressao`); ARQUITETURA (padrão ou abstração nova contra o stack do projeto e os standards — camada que o projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano — e o que o DIFF introduziu da lista fechada `architecture-engineering-lens#review-blockers`; o que já existia no arquivo é nota); REGRESSÃO (para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`; sem grafo — `no-graph`, `not-found`, `ambiguous` — caia para `git grep -n <símbolo> {sha}` e siga, a ausência de grafo nunca bloqueia; ausência de consumidor nunca sai do grafo); O QUE A CORREÇÃO ABRIU (revisando uma correção de rodada anterior, releia o comportamento vizinho); PERFORMANCE só com evidência (lista sem paginação no servidor sobre volume real, consulta N+1 visível no diff) e RASTREABILIDADE de chamada a LLM (dá para reconstruir o que foi ao provedor e o que voltou?). Tipos de bloqueador: `escopo`, `arquitetura`, `regressao`, `performance`. Seu default é SHIP: dúvida vira nota. Arquivo do framework ou da feature (o checklist, o plan.md) você lê com Read, nunca `sed`/`cat`/`head`; código do commit, com `git show {sha}:<caminho>`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`)."
401
+ "persona": "Você é o revisor independente do Gate 3, read-only. Leia como o arquiteto que herda este repo amanhã: simples, legível, fácil de estender. Não participou da implementação e não prova cenários (é o verificador). Não roda teste nem `verify` (o hook `test-budget` nega). Você lê SÓ o commit revisado: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, `git log {base}..{sha}` — nunca a árvore de trabalho (Read/Glob/Grep sobre `src/` leem o disco, onde o verificador pode estar com um mutante aplicado). `base` e `sha` vêm no dispatch. Julga o diff em cinco dimensões, na seção \"Revisor\" de `.morph/framework/evals/review-checklist.md`: ESCOPO (o diff contra as fatias e o `## Contrato de implementação` do plano — arquivo fora de toda fatia, comportamento que nenhum cenário pediu, teste além do orçamento — teste para Cn `prova: leitura`, mais de um por Cn de risco sem porquê no plano, teste que só fixa texto/constante/DTO; teste apagado sem porquê no relatório da fatia, edição de `.sln`/`.csproj`/glob do runner que tira testes da execução, ou `expectedCount` menor no `config.json` sem teste apagado no mesmo diff, é `regressao`); ARQUITETURA (padrão ou abstração nova contra o stack do projeto e os standards — camada que o projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano — e o que o DIFF introduziu da lista fechada `architecture-engineering-lens#review-blockers`; o que já existia no arquivo é nota); REGRESSÃO (para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`; sem grafo — `no-graph`, `not-found`, `ambiguous` — caia para `git grep -n <símbolo> {sha}` e siga, a ausência de grafo nunca bloqueia; ausência de consumidor nunca sai do grafo); O QUE A CORREÇÃO ABRIU (revisando uma correção de rodada anterior, releia o comportamento vizinho); PERFORMANCE só com evidência (lista sem paginação no servidor sobre volume real, consulta N+1 visível no diff) e RASTREABILIDADE de chamada a LLM (dá para reconstruir o que foi ao provedor e o que voltou?). Tipos de bloqueador: `escopo`, `arquitetura`, `regressao`, `performance`. Seu default é SHIP: dúvida vira nota. Arquivo do framework ou da feature (o checklist, o plan.md) você lê com Read, nunca `sed`/`cat`/`head`; código do commit, com `git show {sha}:<caminho>`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`)."
402
402
  },
403
403
  "security": {
404
404
  "title": "Gate 3 Security Reviewer",
@@ -290,13 +290,15 @@ As specs são procuradas na raiz, em `project.frontendPath` e, sem ele, nas past
290
290
 
291
291
  **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza.
292
292
 
293
- **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado.
293
+ **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `timeoutMs` do nó > `project.testTimeoutMs` no `config.json` > **30 min por projeto de teste encadeado** (o build também tem teto de 30 min) — uma suíte longa nunca nasce condenada pelo relógio. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado.
294
+
295
+ **Falha nomeada:** num nó de testes vermelho, o `feedback[]` traz primeiro `[tests/{id}] falhou: <teste>` (até 10, lidas da saída **inteira**: `not ok` do TAP do `node --test` — só as folhas, a suíte-mãe sai —, `FAIL`/`×` do vitest, `failed`/`Failed`/`com falha` do dotnet), depois `[tests/{id}] log completo: .morph/logs/verify-tests-{id}.log` e só então a cauda de 20 linhas; o `reason` do nó resume as três primeiras. Runner que nenhuma forma reconhece fica só com o caminho do log e a cauda. O log é gravado a cada run vermelho (e no `--linux`); erro de escrita não muda o veredito.
294
296
 
295
297
  **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build`, reconstrói e reinicia os `e2e.hostServices` e força o seed de auth a rodar de novo.
296
298
 
297
299
  **`--filter <ids>` escolhe os nós** (lista separada por vírgula, casando `id` exato ou substring). O Gate 3 é `verify {f} --json` **sem filtro**, porque a prova que o verificador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um com `--skip-tests` (`reason: 'skip-tests'`) ou `--skip-build` (`reason: 'skip-build'`): um nó pulado não reprova o rollup, e rodada parcial nunca fica no lugar da prova do Gate 3. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar.
298
300
 
299
- **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. A execução que gravou a baseline persiste o stamp; a que não gravou nada, não.
301
+ **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` dos nós `node-test`/`evals` de volta no `config.json` (e apaga o dos demais) (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. A execução que gravou a baseline persiste o stamp; a que não gravou nada, não.
300
302
 
301
303
  **`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado.
302
304
 
@@ -409,6 +411,8 @@ Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os con
409
411
 
410
412
  **Espera:** slot cheio, ou RAM livre (`os.freemem()`) abaixo do piso do tipo do comando, espera até `heavy.waitTimeoutSec` (600) avisando em **stderr** a cada 30 s e nomeando quem segura (o `--json` mantém o stdout limpo).
411
413
 
414
+ **Uma suíte de `verify` por vez:** o nó de testes de um `verify` espera também enquanto o `verify` de outra feature está no nó de testes, segurando o slot que já tinha; `heavy run` (o filtro de sub-agente), build, e2e e `mutate` não esperam por isso, só pelos slots. Estouro da espera sai `reason: "suite"` (exit 3).
415
+
412
416
  **Piso de RAM por tipo** (`heavy.minFreeMemGbByKind`, padrão `unit` 1,5 · `build` 4 · `e2e` 4 GB): `unit` = vitest/jest, `npm|pnpm|yarn test`, `npm run test[:x]` e o nó `tests` do verify; `e2e` = playwright, `docker compose up`, script com `e2e` no nome, o nó `e2e` e o `e2e up`; `build` = o resto (`dotnet build|test|publish` — o `dotnet test` compila —, `next build`, `docker build`, `mutate` e comando desconhecido). O `heavy run` e o hook do `gateBash` classificam pelo comando; o `verify` toma o slot com o piso do primeiro nó e, antes de um nó mais pesado (tests → e2e), espera de novo só pela RAM segurando o slot — estourar ali também é exit 3, sem stamp. `heavy.minFreeMemGb` (4) virou o fallback: **definido explicitamente**, vale para todo tipo que `minFreeMemGbByKind` não lista (`0` continua desligando a checagem em tudo); sem ele, cada tipo usa o seu padrão.
413
417
 
414
418
  **Ocupado por RAM** (`verify`, `e2e up`, `heavy run`): a mensagem nomeia o piso e a chave (`heavy.minFreeMemGbByKind.<tipo>`) e lista as stacks e2e de pé deste repo (`docker compose ls`, prefixo `morph-`, compose sob a raiz principal — de todas as worktrees). Feature já arquivada (`.morph/archive/<f>` sem `.morph/features/<f>`) vem com `morph-spec e2e down <f>`; stack sem feature, com `docker compose -p <nome> down`. Docker fora ou lento (10 s) = lista vazia, nunca erro.
@@ -550,9 +554,9 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
550
554
  "tests": [
551
555
  { "id": "dotnet", "runner": "dotnet", "cwd": ".",
552
556
  "command": "dotnet test tests/App.Tests/App.Tests.csproj --nologo -v q --no-build",
553
- "build": ["tests/App.Tests/App.Tests.csproj"], "expectedCount": 3057, "timeoutMs": 600000 },
557
+ "build": ["tests/App.Tests/App.Tests.csproj"], "timeoutMs": 600000 },
554
558
  { "id": "vitest", "runner": "vitest", "cwd": "catalog-admin",
555
- "command": "npx vitest run --reporter=default", "expectedCount": 3082 },
559
+ "command": "npx vitest run --reporter=default" },
556
560
  { "id": "api-integration", "runner": "dotnet", "cwd": ".",
557
561
  "command": "dotnet test tests/App.Integration.Tests/App.Integration.Tests.csproj -v q", "e2e": true }
558
562
  ]
@@ -566,8 +570,8 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
566
570
  | `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify |
567
571
  | `cwd` | não (`"."`) | Relativo à raiz. Path que resolve **fora** da raiz é recusado (endereça outra árvore); `cwd` inexistente é `fail`, não `skip` — um skip esconderia erro de plano atrás de gate verde |
568
572
  | `runner` | não (inferido) | `dotnet` \| `vitest` \| `jest` \| `node-test` \| `playwright` \| `none` \| `evals`. Escolhe o extrator de contagem e se há prebuild. `none` = contagem DESCONHECIDA, nunca zero. `evals` é o único que **nunca é inferido** e o único que exige um bloco próprio — ver a subseção abaixo |
569
- | `expectedCount` | não | Baseline. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
570
- | `timeoutMs` | não | Teto deste nó. Existe porque o default derivado (120 s × projetos encadeados) é desenhado para `dotnet test` e é hostil a uma suíte Node de doze minutos |
573
+ | `expectedCount` | não | Baseline, só nos runners de contagem estável (`node-test`, `evals`); nos outros é ignorada e o `--record-baseline` a apaga. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
574
+ | `timeoutMs` | não | Teto deste nó. Sem ele vale `project.testTimeoutMs` e, por fim, o default de 30 min × projetos encadeados |
571
575
  | `build` | não | Projetos a compilar antes (só `runner: "dotnet"`). O `mutate` não o exige: sem ele, descobre o alvo pelo comando. Declarado em nó não-dotnet é erro de config, não campo silenciosamente ignorado |
572
576
  | `e2e` | não (`false`) | Exige a stack de pé. No host roda **depois** do nó `e2e`, contra a stack que ele subiu e com o mesmo env do compose (portas, `PLAYWRIGHT_BASE_URL`, `COMPOSE_PROJECT_NAME`); stack que não subiu (`e2e` em `skip`/`fail`) pula o nó com o motivo e um aviso `[tests/{id}] warning:`. Pulado **com motivo** dentro do `--linux`. Playwright não entra aqui: é o próprio nó `e2e` (detectado ou `e2e.command`), e declará-lo também como nó de teste rodaria a suíte duas vezes |
573
577
  | `linuxImage` | não | Imagem do container deste nó sob `--linux` |
@@ -704,6 +708,11 @@ reprova** (mesmo com exit 0 — é regressão silenciosa), **alta apenas avisa**
704
708
  — um zero fabricaria queda a partir de "não deu para ler o output". O aviso de alta sai **mesmo com o
705
709
  nó verde**, que é o único lugar onde ele pode existir.
706
710
 
711
+ **Baseline só onde a contagem fica parada.** `dotnet`, `vitest`, `jest` e `playwright` crescem a cada
712
+ fatia: a baseline deles só avisava a alta ou barrava a limpeza deliberada, e teste apagado já aparece no
713
+ diff que o Gate 3 lê. Nesses runners não há baseline; vale o **piso zero** — nó verde que executou `0`
714
+ testes reprova com `suíte verde sem nenhum teste executado` (suíte que parou de rodar).
715
+
707
716
  **Prebuild e `--no-build` (por que um `dotnet test` verde podia mentir).** Num nó `runner: "dotnet"`,
708
717
  os alvos de `build[]` — ou, na falta, os `.csproj` extraídos do próprio `command` — são compilados
709
718
  antes da execução, com `dotnet build "<alvo>" --nologo -v q`. Compilação que falha reprova o nó com
@@ -716,10 +725,10 @@ a partir do binário do último build bom e reportava verde. `--no-build` contin
716
725
  sem `--no-build` já compila sozinho, e pré-compilar ali só dobraria o relógio do caminho quente.
717
726
 
718
727
  **O ciclo da baseline, e o número que só sai à mão.** `verify --record-baseline` é a única via de
719
- escrita: mede cada nó numa execução real e grava `expectedCount` de volta no `config.json` (escrita
728
+ escrita: mede cada nó numa execução real e grava `expectedCount` dos nós `node-test`/`evals` de volta no `config.json` (apaga o dos demais, listados como `removed`; escrita
720
729
  atômica `tmp.{pid}` + rename, preservando todo o resto do arquivo). Recusa **quatro** vezes — execução
721
730
  vermelha (fora o `count-drop` com exit 0, abaixo), execução **parcial** (sob `--filter`, mesmo com o verify verde: baseline de meia frota é
722
- afirmação sobre a outra metade que ninguém fez), contagem desconhecida, e **sem `config.json` ou sem
731
+ afirmação sobre a outra metade que ninguém fez), contagem desconhecida num nó `node-test`/`evals`, e **sem `config.json` ou sem
723
732
  `project.tests[]`** (erro de uso, exit 2; o arquivo nunca é criado implicitamente — gravar os nós que o
724
733
  fallback sintetiza exigiria inventar a declaração que o comando veio manter). Verde com gravação
725
734
  recusada sai **exit 1**, com a razão em **stderr** (sobrevive ao `--json`). A gravação é governada
@@ -739,7 +748,7 @@ nomeia o nó que impediu. **Apagar teste é legítimo:** a única cor vermelha q
739
748
 
740
749
  > **Não existe constante de 600 s neste CLI** (`grep -rn "600000" src/ framework/` → zero). O teto que
741
750
  > corta uma suíte longa é o do **Bash do harness**, externo ao morph-spec, e não há o que "subir". Os
742
- > relógios que o CLI de fato tem: build **120 000 ms**, tests **120 000 ms por projeto de teste
751
+ > relógios que o CLI de fato tem: build **1 800 000 ms** (30 min), tests **1 800 000 ms por projeto de teste
743
752
  > encadeado**, health do e2e **180 000 ms**, specs Playwright **300 000 ms**, mais `timeoutMs` por nó,
744
753
  > que substitui o default do `tests` quando ele não serve. `--timeout <ms>` cobre build **e** tests.
745
754
  > **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:** rode
@@ -116,9 +116,9 @@ Read-only, sobre o commit do dispatch: `git diff {base}..{sha}`, `git show {sha}
116
116
  1. **Escopo**: o diff contra as fatias (`arquivos:`) e o `## Contrato de implementação` do plano.
117
117
  Teste além do orçamento também é escopo: teste para Cn `prova: leitura`, mais de um teste por Cn de
118
118
  risco sem porquê no plano, teste que só fixa texto/constante/DTO, o mesmo comportamento em duas
119
- camadas — bloqueador `escopo` (apague, não acrescente). `expectedCount` menor no
120
- `.morph/config/config.json` do diff sem teste apagado no mesmo diff é bloqueador `regressao`: é
121
- suíte que parou de rodar, não limpeza.
119
+ camadas — bloqueador `escopo` (apague, não acrescente). Teste apagado sem porquê no relatório da
120
+ fatia, edição de `.sln`/`.csproj`/glob do runner que tira testes da execução, ou `expectedCount` menor no `.morph/config/config.json` do diff sem teste apagado no mesmo
121
+ diff, é bloqueador `regressao`: é suíte que parou de rodar, não limpeza.
122
122
  2. **Arquitetura**: padrão ou abstração nova contra o stack do projeto e os standards (camada que o
123
123
  projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano).
124
124
  3. **Regressão**: para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`.
@@ -1,11 +1,11 @@
1
- #!/usr/bin/env bash
2
- # morph-spec statusline — installed globally to ~/.claude/statusline.sh
3
- # Claude Code invokes this with JSON via stdin after each response.
4
- # Requires: Python 3 available as `python3` or `python` on PATH.
5
-
6
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
- if command -v python3 &>/dev/null; then
8
- python3 "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
9
- elif command -v python &>/dev/null; then
10
- python "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
11
- fi
1
+ #!/usr/bin/env bash
2
+ # morph-spec statusline — installed globally to ~/.claude/statusline.sh
3
+ # Claude Code invokes this with JSON via stdin after each response.
4
+ # Requires: Python 3 available as `python3` or `python` on PATH.
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ if command -v python3 &>/dev/null; then
8
+ python3 "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
9
+ elif command -v python &>/dev/null; then
10
+ python "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
11
+ fi
@@ -51,8 +51,14 @@ function liveHoldersPruningStale(dir, ctx) {
51
51
 
52
52
  const kindSuffix = (kind) => (kind ? ` (${kind})` : '');
53
53
 
54
+ // Two full verify suites never run together; a sub-agent filter or a build only takes a slot.
55
+ export const isVerifySuite = ({ label, node }) => label === 'verify' && node === 'tests';
56
+
57
+ const runningSuite = (holders, ownPath = null) => holders.find((h) => h.path !== ownPath && isVerifySuite(h));
58
+
54
59
  function blockedText(blocked, slots, now) {
55
60
  const holders = blocked.holders.map((h) => describeHolder(h, now)).join('; ');
61
+ if (blocked.reason === 'suite') return `aguardando o verify de outra feature terminar a suíte de testes (${describeHolder(blocked.holders[0], now)})`;
56
62
  if (blocked.reason === 'exclusive') return `aguardando limpeza do servidor de build (${describeHolder(blocked.exclusive, now)})`;
57
63
  if (blocked.reason === 'memory') {
58
64
  const cause = blocked.holders.length
@@ -65,13 +71,16 @@ function blockedText(blocked, slots, now) {
65
71
 
66
72
  function tryClaim(dir, opts, ctx) {
67
73
  const {
68
- slots, label, feature, node, cwd, minFreeMemGb, freeMem, kind, lease: leaseSpec,
74
+ slots, label, feature, node, cwd, minFreeMemGb, freeMem, kind, suite, lease: leaseSpec,
69
75
  } = opts;
70
76
  const guarded = withClaimMutex(dir, () => {
71
77
  const holders = liveHoldersPruningStale(dir, ctx);
72
78
  const exclusive = readExclusive(dir, ctx);
73
79
  if (exclusive) return { blocked: { reason: 'exclusive', holders, exclusive } };
80
+ // A full queue is the deeper reason: with no free slot, waiting out the other suite is not enough.
74
81
  if (holders.length >= slots) return { blocked: { reason: 'slots', holders } };
82
+ const running = suite ? runningSuite(holders) : null;
83
+ if (running) return { blocked: { reason: 'suite', holders: [running] } };
75
84
  const freeGb = freeMem() / GB;
76
85
  if (minFreeMemGb > 0 && freeGb < minFreeMemGb) return { blocked: { reason: 'memory', holders, freeGb, minFreeMemGb, kind } };
77
86
 
@@ -116,10 +125,18 @@ export function acquireHeavy(opts) {
116
125
  return { status: 'acquired', lease: null, waitedMs: 0, reentrant: true };
117
126
  }
118
127
 
128
+ return pollClaim(() => tryClaim(dir, claimOpts, ctx), {
129
+ ...opts, now, sleep, log, minFreeMemGb: claimOpts.minFreeMemGb,
130
+ });
131
+ }
132
+
133
+ function pollClaim(claim, {
134
+ slots, waitTimeoutMs, minFreeMemGb = 0, kind = null, now, sleep, log,
135
+ }) {
119
136
  const start = now();
120
137
  let lastReport = -Infinity;
121
138
  for (;;) {
122
- const attempt = tryClaim(dir, claimOpts, ctx);
139
+ const attempt = claim();
123
140
  if (attempt.lease) return { status: 'acquired', lease: attempt.lease, waitedMs: now() - start };
124
141
 
125
142
  const waited = now() - start;
@@ -130,8 +147,8 @@ export function acquireHeavy(opts) {
130
147
  reason: blocked.reason,
131
148
  holders: blocked.holders,
132
149
  freeGb: blocked.freeGb ?? null,
133
- minFreeMemGb: claimOpts.minFreeMemGb,
134
- kind: opts.kind ?? null,
150
+ minFreeMemGb,
151
+ kind,
135
152
  waitedMs: waited,
136
153
  };
137
154
  }
@@ -143,6 +160,25 @@ export function acquireHeavy(opts) {
143
160
  }
144
161
  }
145
162
 
163
+ // Keeps the slot held while it waits: releasing it would let a build or filter take the place.
164
+ export function enterVerifySuite(opts) {
165
+ const {
166
+ dir, lease, maxHoldMs,
167
+ now = Date.now, sleep = sleepSync, isAlive = pidAlive, log = (m) => process.stderr.write(`${m}\n`),
168
+ } = opts;
169
+ const ctx = { now, isAlive, maxHoldMs };
170
+ const claim = () => {
171
+ const guarded = withClaimMutex(dir, () => {
172
+ const running = runningSuite(scanHolders(dir, ctx).live, lease.path);
173
+ if (running) return { blocked: { reason: 'suite', holders: [running] } };
174
+ updateLease(lease, { node: 'tests' });
175
+ return { lease };
176
+ }, ctx);
177
+ return guarded ? guarded.value : { blocked: { reason: 'contended', holders: [] } };
178
+ };
179
+ return pollClaim(claim, { ...opts, now, sleep, log });
180
+ }
181
+
146
182
  // For a holder whose next node needs more RAM than it was admitted with: it keeps its slot and
147
183
  // waits only on memory, since re-claiming would hit the re-entrant path and skip the check.
148
184
  export function awaitFreeMemory(opts) {
@@ -206,6 +242,7 @@ export function describeBusy(result, config) {
206
242
  const key = result.kind ? `heavy.minFreeMemGbByKind.${result.kind}` : 'heavy.minFreeMemGb';
207
243
  return `sem RAM livre há ${waited}s (${result.freeGb?.toFixed(1)} GB < ${floor} GB mínimos${kindSuffix(result.kind)}, ${key})`;
208
244
  }
245
+ if (result.reason === 'suite') return `o verify de outra feature segue na suíte de testes após ${waited}s (heavy.waitTimeoutSec)`;
209
246
  if (result.reason === 'exclusive') return `limpeza do servidor de build ainda em curso após ${waited}s`;
210
247
  return `${result.holders.length}/${config.slots} slots pesados ocupados por ${waited}s (heavy.waitTimeoutSec)`;
211
248
  }
@@ -57,7 +57,7 @@
57
57
  "expectedCount": {
58
58
  "type": "integer",
59
59
  "minimum": 0,
60
- "description": "Recorded baseline. Written ONLY by `morph-spec verify --record-baseline`, from a green run — never typed by hand."
60
+ "description": "Recorded baseline, kept only for `node-test` and `evals` (ignored, and removed by --record-baseline, on the other runners). Written ONLY by `morph-spec verify --record-baseline`, from a green run — never typed by hand."
61
61
  },
62
62
  "timeoutMs": {
63
63
  "type": "integer",
@@ -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: "9.7.0"
7
+ cliVersion: "9.8.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_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: "9.7.0"
7
+ cliVersion: "9.8.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -4,7 +4,7 @@ description: Executa as fatias do plano de uma feature morph-spec — dispatch p
4
4
  version: core
5
5
  when_to_use: Use com o gate do plano aprovado, em "use /morph-apply {feature}" ou "implementar feature"
6
6
  user-invocable: true
7
- cliVersion: "9.7.0"
7
+ cliVersion: "9.8.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -129,7 +129,7 @@ config **antes** do verify: o stamp grava o HEAD, e código ou `config.json` com
129
129
 
130
130
  Depois `morph-spec verify {feature} --json`, **sem filtro** — nunca recorte o
131
131
  verify da feature. `--filter` é do loop do próprio sub-agente; o Gate 3 e o CI rodam a suíte
132
- inteira. Nó de teste sem `expectedCount`, ou fatia que apagou teste de propósito (limpeza, `morph-spec tests-audit`): acrescente `--record-baseline` a ESTA rodada — ela mede e
132
+ inteira. Nó `node-test`/`evals` sem `expectedCount`, ou fatia que apagou teste de propósito (limpeza, `morph-spec tests-audit`): acrescente `--record-baseline` a ESTA rodada — ela mede e
133
133
  prova de uma vez; nunca um segundo verify só para gravar a baseline. Vermelho: corrija com `verify.feedback` (direto no código, ou re-despachando a fatia dona)
134
134
  e rode de novo; **3 tentativas** sem verde → `AskUserQuestion` ("Verify de {feature} vermelho após 3
135
135
  tentativas — aceitar com ressalva, continuar tentando, ou parar?"). Quem conta é o `feature.json`:
@@ -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_use: Use quando chegar "/morph-hotfix {feature}", quando a morph-plan rotear o pedido como workType hotfix, ou quando o usuário disser "produção caiu / hotfix / urgente" a respeito de um incidente
6
6
  user-invocable: true
7
- cliVersion: "9.7.0"
7
+ cliVersion: "9.8.0"
8
8
  ---
9
9
 
10
10
  # morph-hotfix
@@ -4,7 +4,7 @@ description: Cuida da RAM e da CPU da máquina enquanto o morph-spec trabalha
4
4
  version: core
5
5
  when_to_use: Use quando a máquina estiver lenta ou sem RAM, um comando pesado esperou ou foi recusado pela fila, sobrou worktree de sub-agent, ou vários sub-agents pesados vão rodar em paralelo
6
6
  user-invocable: false
7
- cliVersion: "9.7.0"
7
+ cliVersion: "9.8.0"
8
8
  ---
9
9
 
10
10
  # morph-machine
@@ -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.7.0"
7
+ cliVersion: "9.8.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.7.0"
7
+ cliVersion: "9.8.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -106,7 +106,7 @@ O `create` já escreveu o esqueleto em `2-plan/plan.md` (leia e preencha; `chore
106
106
 
107
107
  **Contrato de implementação**: o que quem implementa precisa e o código não diz.
108
108
  - **Verificação de runtime:** o stack sobe por `morph-spec e2e up {feature}` (compose do projeto; sem compose, isso é uma fatia); os fluxos que o verificador navega, nos verbos dos cenários; base URL `process.env.PLAYWRIGHT_BASE_URL ?? \`http://localhost:${process.env.PORT ?? 3000}\``, compose com `"${PORT:-3000}:3000"` e sem `container_name` (standard `infrastructure/docker/local-compose-isolation`). App cujo build é caro dentro do Docker sobe como processo do host por `e2e.hostServices` (compose só com as dependências; campos em `morph-spec e2e --help`). App com login: `e2e.auth` em `config.json` (`mode: seed|storageState|bypass|none`, `seedCommand`, `storageStatePath`, `envFile`), credenciais num `.env.e2e` gitignored — **`mode` é obrigatório sempre que houver `seedCommand`**, sem ele o seed fica inerte. Biblioteca pura: registre a dispensa e o porquê.
109
- - **Baseline é um COMANDO, nunca um número.** O plano congela e é lido por dias: um número cravado está errado na primeira fatia, e quem confia nele "conserta" o que não quebrou. Registre como medir (`npm test`, `dotnet test`); o `expectedCount` sai de `morph-spec verify {feature} --record-baseline`, nunca digitado. Sem `project.stack`/`project.tests` no `config.json`, a `morph-apply` os declara a partir destes comandos antes do primeiro verify.
109
+ - **Baseline é um COMANDO, nunca um número.** O plano congela e é lido por dias: um número cravado está errado na primeira fatia, e quem confia nele "conserta" o que não quebrou. Registre como medir (`npm test`, `dotnet test`); o `expectedCount` (só `node-test`/`evals`; nos outros runners vale o piso zero) sai de `morph-spec verify {feature} --record-baseline`, nunca digitado. Sem `project.stack`/`project.tests` no `config.json`, a `morph-apply` os declara a partir destes comandos antes do primeiro verify.
110
110
  - **Pré-requisito é um PREDICADO, nunca um SHA literal:** "`git merge-base --is-ancestor origin/main HEAD` sai 0" — o SHA muda no primeiro squash.
111
111
 
112
112
  Plano bom cabe em duas páginas; crescendo além, corte escopo. Feche com `morph-spec plan-check {feature}` até zero erro. Ele também recusa veredito de reuso fora do enum, id `C`/`F`/`ADR` duplicado, `## Errata` fora do fim, `## Cenários` vazio e fatia sem `persona:`; avisa chave de fatia desconhecida, superfície fora da `ui-designer`, ADR sem **Alcance**, fatia sem `cobre:` e mais de 12 fatias.
@@ -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.7.0"
7
+ cliVersion: "9.8.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -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.7.0"
7
+ cliVersion: "9.8.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.7.0"
7
+ cliVersion: "9.8.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux
@@ -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>