@polymorphism-tech/morph-spec 8.35.1 → 8.37.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 (51) hide show
  1. package/CLAUDE.md +10 -8
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +4 -3
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/MORPH.md +24 -23
  8. package/framework/docs/CLI.md +212 -15
  9. package/framework/evals/proof-rubrics/eval-de-modelo.md +16 -0
  10. package/framework/evals/rubrics/condensed.md +3 -1
  11. package/framework/evals/rubrics/test-coverage.md +8 -0
  12. package/framework/hooks/shared/phase-utils.js +1 -0
  13. package/framework/phases.json +1 -1
  14. package/framework/rules/morph-workflow.md +1 -1
  15. package/framework/schemas/config.schema.json +7 -0
  16. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  17. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  18. package/framework/skills/core/morph-apply/SKILL.md +21 -10
  19. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  20. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  21. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  22. package/framework/skills/core/morph-implement/SKILL.md +20 -3
  23. package/framework/skills/core/morph-media/SKILL.md +1 -1
  24. package/framework/skills/core/morph-plan/SKILL.md +41 -3
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +11 -2
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  30. package/framework/standards/STANDARDS.json +2 -2
  31. package/framework/standards/ai-agents/prompt-sources.md +4 -3
  32. package/framework/standards/ai-agents/testing-ai.md +14 -13
  33. package/framework/templates/dotnet/ai-kit/README.md +22 -11
  34. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +23 -10
  35. package/package.json +1 -1
  36. package/src/commands/project/doctor.js +51 -2
  37. package/src/commands/tasks/dag.js +13 -1
  38. package/src/commands/validation/verify.js +59 -17
  39. package/src/core/paths/output-schema.js +7 -0
  40. package/src/lib/detectors/index.js +225 -217
  41. package/src/lib/risk-detector.js +38 -18
  42. package/src/lib/specs/done-criteria-check.js +200 -0
  43. package/src/lib/specs/usecase-check.js +129 -5
  44. package/src/lib/tasks/dotnet-test-introspection.js +632 -0
  45. package/src/lib/tasks/eval-node.js +3 -3
  46. package/src/lib/tasks/test-nodes.js +190 -20
  47. package/src/lib/tasks/test-runner.js +210 -17
  48. package/src/lib/tasks/test-scope.js +597 -0
  49. package/src/lib/validators/traceability-check.js +9 -2
  50. package/src/lib/validators/verify-runner.js +190 -27
  51. package/src/lib/worktree/docker-audit.js +5 -1
@@ -9,7 +9,7 @@
9
9
  - **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **`uiux`** é o único gate cujo `AskUserQuestion` o hook `state-sync.js` não reconhece por texto (só casa "Gate 1/2/3") — aprove-o sempre explicitamente por este comando ou por `advance`, nunca contando com o hook. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
10
10
  - **`morph-spec advance <feature>`** — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta).
11
11
  - **`morph-spec score <feature> <task> [score] [--dims a,c,q,t]`** — grava `taskScores` no feature.json com timestamp server-side; **nunca edite taskScores à mão**. **Duas recusas duras (exit 1), nunca um registro silencioso:** `<score>` em branco ou ausente é **recusado, jamais gravado como `0`** (o `Number('')` que valia `0` gravava um zero fabricado — e era esse zero que o Gate 3 lia depois, como `evalScore: 0 / evalScoreSource: taskScores-proxy`); e `<task>` fora de `^T\d+$` é **recusado** em vez de aninhado (`T3.2` virava `taskScores.T3["2"]` — inválido contra `feature.schema.json` e invisível ao proxy). O `[score]` aparece opcional só para que o valor ausente chegue ao comando e receba uma mensagem que nomeia o argumento e a ordem posicional — ele continua obrigatório. Em PowerShell, cite o valor de `--dims` entre aspas (`--dims "9.5,9,9,9"`) — sem aspas, a lista pode chegar como tokens separados; o parser aceita ambas as formas, mas citar evita qualquer ambiguidade.
12
- - **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp). **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `scope` ("task group … does not touch runtime") são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). **A dispensa também depende de QUEM carimbou.** O stamp `verification` é singular e qualquer `verify` o sobrescreve, de qualquer escopo — então um stamp de escopo `task:{id}` no momento do ship significa que o último `verify` **não foi o da feature**. A dispensa (`project`/`scope`) só conta vinda de `scope: 'feature'`; de um stamp de task ela pausa, nomeando o escopo que carimbou e mandando rodar `morph-spec verify {feature}` sem task. Sem isso, um `morph-spec verify {f} {task}` de uma task cujo grupo não toca runtime (a última task de uma feature é tipicamente `group: docs`) compra o Gate 3 da feature inteira numa máquina onde a app nunca subiu. Escopo ausente cai do mesmo lado seguro que razão nenhuma. A regra governa a **dispensa**, não a prova: um e2e que rodou e passou vale de qualquer escopo. `e2eStatus`/`e2eReason`/`e2eSkipClass`/`verificationScope` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`. **Prova de mutação:** no ship, o `gate-check` lê do MESMO `evaluator-report.md` se existe a seção `## Mutações` (`evaluatorProofState`, em `src/lib/evaluator-report.js` — leitor único) e grava `signals.mutationProof = {section, min, productionCode}`. Relatório **sem** a seção numa feature com código de produção (ao menos uma task com `group` diferente de `docs`) **pausa o Gate 3**, qualquer que seja o score: um teste verde prova que o código roda, não que o teste morde. Feature 100% `docs` é isenta, e `tasks.json` ausente também (sem plano não há como afirmar que existe código de produção). `min` é o menor `**Proof-Scores:**` do relatório e impõe teto em `test-coverage` (rubricas em `framework/evals/proof-rubrics/`); `null` é **sem teto**, jamais teto 0. `mutationProof` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos sinais de e2e. **O token vigente é o ÚLTIMO:** o `evaluator-report.md` é append-only, então numa reavaliação o `**Composite-Score:**` que decide o gate é o do FIM do arquivo — ler o primeiro auto-passaria o Gate 3 com a nota superada quando uma correção BAIXA a nota. **Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, score, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma task de `tasks.json` cujo array `standards` cita `standards/ai-agents/modalities-image-gen.md`, `standards/ai-agents/media-video.md` ou `standards/ai-agents/media-pipeline.md` — e a grafia do disco (`framework/standards/…`) conta igual, porque um plano escrito com o prefixo do disco desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (rubrica `framework/evals/proof-rubrics/media-claims.md`). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `score` é a chave `media=` dos `**Proof-Scores:**` e impõe teto em `architecture`, **não** em `test-coverage`: ela fica FORA do `min` do `mutationProof`, senão um `media` baixo derrubaria o teto de teste por um motivo que não é de teste. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco.
12
+ - **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp). **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `scope` ("task group … does not touch runtime") são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). **A dispensa também depende de QUEM carimbou.** O stamp `verification` é singular e qualquer `verify` o sobrescreve, de qualquer escopo — então um stamp de escopo `task:{id}` no momento do ship significa que o último `verify` **não foi o da feature**. A dispensa (`project`/`scope`) só conta vinda de `scope: 'feature'`; de um stamp de task ela pausa, nomeando o escopo que carimbou e mandando rodar `morph-spec verify {feature}` sem task. Sem isso, um `morph-spec verify {f} {task}` de uma task cujo grupo não toca runtime (a última task de uma feature é tipicamente `group: docs`) compra o Gate 3 da feature inteira numa máquina onde a app nunca subiu. Escopo ausente cai do mesmo lado seguro que razão nenhuma. **E o escopo de task pausa o ship por si só**, antes de qualquer leitura do e2e: se o último stamp `verification` for de escopo `task:{id}`, o `review` pausa com `último morph-spec verify foi de escopo "task:{id}" (recorte parcial de task)`, mesmo com todos os nós verdes — build, testes e validadores daquele verify rodaram só para a task, e desde o recorte automático (ver `verify` abaixo) todo `verify {f} {task}` roda menos que a suíte. O caminho de volta é rodar `morph-spec verify {feature}` antes do ship. Stamp sem `scope` (feature legada) não pausa por esta regra, e fora do `review` ela não vale. `e2eStatus`/`e2eReason`/`e2eSkipClass`/`verificationScope` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`. **Prova de mutação:** no ship, o `gate-check` lê do MESMO `evaluator-report.md` se existe a seção `## Mutações` (`evaluatorProofState`, em `src/lib/evaluator-report.js` — leitor único) e grava `signals.mutationProof = {section, min, productionCode}`. Relatório **sem** a seção numa feature com código de produção (ao menos uma task com `group` diferente de `docs`) **pausa o Gate 3**, qualquer que seja o score: um teste verde prova que o código roda, não que o teste morde. Feature 100% `docs` é isenta, e `tasks.json` ausente também (sem plano não há como afirmar que existe código de produção). `min` é o menor `**Proof-Scores:**` do relatório e impõe teto em `test-coverage` (rubricas em `framework/evals/proof-rubrics/`); `null` é **sem teto**, jamais teto 0. `mutationProof` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos sinais de e2e. **O token vigente é o ÚLTIMO:** o `evaluator-report.md` é append-only, então numa reavaliação o `**Composite-Score:**` que decide o gate é o do FIM do arquivo — ler o primeiro auto-passaria o Gate 3 com a nota superada quando uma correção BAIXA a nota. **Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, score, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma task de `tasks.json` cujo array `standards` cita `standards/ai-agents/modalities-image-gen.md`, `standards/ai-agents/media-video.md` ou `standards/ai-agents/media-pipeline.md` — e a grafia do disco (`framework/standards/…`) conta igual, porque um plano escrito com o prefixo do disco desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (rubrica `framework/evals/proof-rubrics/media-claims.md`). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `score` é a chave `media=` dos `**Proof-Scores:**` e impõe teto em `architecture`, **não** em `test-coverage`: ela fica FORA do `min` do `mutationProof`, senão um `media` baixo derrubaria o teto de teste por um motivo que não é de teste. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco.
13
13
  - **`morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — o **escape manual**: registra uma decisão já tomada pela sessão (ex.: `discoveryMode: "interview"`, sinal que `shouldPauseGate` nem representa). Mesmo write path do `gate-check` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
14
14
  - **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`score`/`gate-decision`/`gate-check` recusam (exit 1) quando um worktree vivo é dono da feature e a sessão está em outra árvore — sem isso o comando carimbaria o gate/score/decisão na cópia congelada da raiz, criando o drift em vez de só herdá-lo. A recusa nomeia o worktree e o comando exato para rodar lá; `--force` **não** dispensa (é sobre gate order, não sobre árvore). Rodando de dentro do worktree dono, ou numa feature sem worktree algum, nada muda.
15
15
 
@@ -37,8 +37,8 @@
37
37
 
38
38
  ## Validação
39
39
 
40
- - **`morph-spec dag <feature>`** — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. `--mermaid` nunca é bloqueado.
41
- - **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--filter <ids>] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-e2e] [--fresh-e2e]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **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. **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. Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). 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. **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo. Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). **`--filter <ids>` escolhe quais suítes rodam** (lista separada por vírgula, casando `id` exato ou substring): o loop por task usa `verify {f} {task} --filter {stack-da-task}` porque o relógio de uma task não paga a suíte inteira, e o Gate 3 usa `verify {f} --json` **sem filtro** porque a prova que o avaliador lê tem de cobrir todos os nós (o CI idem). 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. **`--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). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar. 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. Também **não** persiste o stamp `verification`: é manutenção de config, não prova de gate. **`--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. Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo.
40
+ - **`morph-spec dag <feature>`** — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. `doneCriteria` com vocabulário de artefato (`grep`, "contém a string", "nenhum arquivo", glob `**/`…) sem `Por que artefato:` também é aviso, sob `⚠ Critérios de artefato` — o exit não muda, e o `--json` traz a chave irmã `artifactCriteria`. `--mermaid` nunca é bloqueado.
41
+ - **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--filter <ids>] [--full] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **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. **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. Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). 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. **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo. Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). **No escopo de task o recorte é automático:** `verify {f} {task}` deixa o `group` da task escolher os nós e os arquivos de teste de `outputs` estreitarem o comando dentro de cada nó — o relógio de uma task não paga a suíte inteira (regras, veredito e limites em *O recorte por task*, na seção **Suítes de teste**). **`--filter <ids>` força a escolha de nós** (lista separada por vírgula, casando `id` exato ou substring; desliga a escolha pelo `group`, e o estreitamento dentro dos nós continua) e **`--full` desliga o recorte** (o `group` não escolhe nós e nenhum comando é estreitado; os nós com `e2e: true` e os de `runner: "evals"` continuam só no escopo de feature; no escopo de feature `--full` é no-op, porque ali nada é recortado). O Gate 3 continua sendo `verify {f} --json` **sem filtro**, porque a prova que o avaliador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` de escopo de **feature** não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um de escopo de feature 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. No escopo de task as três gravam normalmente — o stamp já diz `task:{id}`, e o Gate 3 pausa nele. 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. **`--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). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar. 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. **Com `{task}` também é erro de uso (exit 2)**, recusado antes do build: no escopo de task o verify recorta nós e testes, e baseline de recorte não é baseline — o gravador ainda recusa, uma segunda vez, qualquer medição que carregue `selection`. Também **não** persiste o stamp `verification`: é manutenção de config, não prova de gate. **`--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. **`--no-persist` não grava nada no `feature.json`** — nem o histórico `verifications.*` nem o stamp `verification` que o gate lê: a execução vira sonda, nunca prova para gate algum, qualquer que seja a regra de carimbo do escopo. E o `--filter` só estreita o nó de testes (build, validadores e e2e rodam igual). Por isso a execução de evidência de eval do `morph-implement` §5 é `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist`: só o eval, sem stack, sem escrever no `feature.json`. Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo.
42
42
  - **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). Na dúvida, rebuild — o layer cache do Docker faz disso barato. O motivo impresso diz **o que** decidiu o reuso, e as duas procedências nunca se confundem: `source unchanged since build` é o carimbo, `source is bind-mounted` é o disco. E a segunda só sai quando **todo** serviço com seção `build:` monta a raiz do projeto — um sidecar Playwright montando `./e2e` ao lado de uma app compilada em imagem não fala pela stack inteira; nesse caso quem decide é o carimbo. `--fresh` derruba com volumes (`down -v`), apaga o carimbo e re-roda o seed. `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do avaliador, em vez de deixá-lo adivinhar `localhost:3000`. `down [--volumes]` é best-effort e nunca bloqueia. Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
43
43
  - **`morph-spec doctor [--full] [--mcp] [--reset]`** — checagem de saúde padrão (sem `--worktrees`): instalação (versões, `.claude/`, hooks, agents, grafo de codebase, templates, `state.json`) mais duas auditorias.
44
44
  **Ciclo de vida das features:** ordem de gates, feature sem identidade, pasta órfã, entrada de índice morta, `branch-drift` (a cópia da raiz atrasada em relação ao `morph/{f}` commitado) e `abandoned-scaffold`. Este último é o esqueleto que o `create --worktree` deixa para trás: `feature.json` sozinho, `in_progress`, **0 artefatos, 0 gates aprovados** e `morph/{f}` **sem commit próprio** (a branch ainda apontando para o commit de merge de outra PR). As quatro condições valem juntas e só depois de **24h** — recém-nascida tem exatamente a mesma forma, e alarme em todo `create` é alarme que ninguém lê. Fora de repo git, ou com a default branch indeterminável, o check cala (não dá para provar). Reparo: `morph-spec delete <f> --archive`, ou retome a feature.
@@ -80,7 +80,7 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
80
80
  | Campo | Obrigatório | Semântica |
81
81
  | --- | --- | --- |
82
82
  | `id` | **sim** | `^[a-z0-9][a-z0-9-]*$`, único no array. É o que `--filter` casa e a chave de `runs[]`. `id` duplicado é erro de config, não empate silencioso — **os dois** lados falham |
83
- | `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify |
83
+ | `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify — **com uma exceção:** o recorte do escopo de task (*O recorte por task*, abaixo) estreita o comando e o registra: `runs[].command` é o que rodou, `runs[].selection.declaredCommand` o declarado |
84
84
  | `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 |
85
85
  | `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 |
86
86
  | `expectedCount` | não | Baseline. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
@@ -88,6 +88,7 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
88
88
  | `build` | não | Projetos a compilar antes (só `runner: "dotnet"`). Declarado em nó não-dotnet é erro de config, não campo silenciosamente ignorado |
89
89
  | `e2e` | não (`false`) | Exige a stack de pé. Roda em escopo de feature; é pulado **com motivo** no loop por task e dentro do `--linux` |
90
90
  | `linuxImage` | não | Imagem do container deste nó sob `--linux` |
91
+ | `groups` | não (default pelo `runner`) | A que `group` de task o nó responde no recorte por task: subconjunto não vazio, sem repetição, de `backend` \| `frontend` \| `tests` \| `infra` \| `docs` (o mesmo enum do `group` de `tasks.json`). Ausente: `dotnet`/`node-test` → `backend`; `vitest`/`jest`/`playwright` → `frontend`; `none`/`evals` não têm default — nó sem grupo nunca sai do escopo pelo `group`. Valor inválido é erro de config (`invalid-test-node`), como nos demais campos |
91
92
 
92
93
 
93
94
  ### O runner `evals`: regressão de prompt e de modelo com cache commitada
@@ -148,10 +149,13 @@ lista plural de nós existe para matar.
148
149
  `eval node runs at feature scope only`, o mesmo tratamento de `e2e: true`. O relógio de uma task não
149
150
  paga uma suíte de eval, e o resultado que o Gate 3 lê é o de escopo de feature.
150
151
 
151
- **Contagem.** O `scenarios` do sumário vence quando legível; sem sumário legível, o extrator `dotnet`
152
- decide (um eval **é** `dotnet test`). Nos dois caminhos, **`null` é DESCONHECIDO, jamais zero** — um
153
- sumário ilegível não pode fabricar uma queda contra `expectedCount`. E um sumário dizendo
154
- `scenarios: 0` com saída 0 é `fail` (`eval-empty-run`): nada falhou porque nada rodou.
152
+ **Contagem.** A contagem do nó é o `scenarios` do sumário — e só ele: sem sumário legível ela é
153
+ **`null`, DESCONHECIDA, jamais zero**, e um sumário ilegível não pode fabricar uma queda contra
154
+ `expectedCount` nem virar baseline. E um sumário dizendo
155
+ `scenarios: 0` com saída 0 é `fail` (`eval-empty-run`): nada falhou porque nada rodou. A unidade do
156
+ nó é o **cenário**: a contagem que o extrator lê da saída conta `[Fact]`, e nunca é comparada com o
157
+ `expectedCount` (antes era, e um eval 24/24 saía `count-drop: esperado 24, executado 2`). O arquivo do
158
+ sumário é apagado antes de cada rodada: um sumário herdado de outra rodada nunca é lido como desta.
155
159
 
156
160
  **Cache fria avisa, não reprova.** Um run que foi ao provider marca `cache: "cold"` e emite warning de
157
161
  que a árvore mudou e precisa ser commitada. Gastar dinheiro sem pedir é ruim; mentir sobre o resultado
@@ -194,9 +198,16 @@ projeto ao comportamento anterior, que é exatamente o defeito.
194
198
  **`tests: []` ≠ `tests` ausente.** O array vazio é declaração explícita de "não há suítes" (um nó
195
199
  `skip`); a chave **ausente** cai no fallback de quatro passos, que continua íntegro e é o caminho de
196
200
  todo projeto criado antes desta chave: `project.testCommand` → `.sln`/`.csproj` na raiz → varredura de
197
- `*Tests.csproj` (que **não enxerga worktrees**) → `package.json` com `scripts.test`. Sem `config.json`
198
- nenhum, o `verify` resolve exatamente **um** nó `default` — o repositório-fonte do morph-spec roda
199
- assim, e nada aqui depende do arquivo existir.
201
+ `*Tests.csproj` (que **não enxerga worktrees**) → `package.json` com `scripts.test`, que dá o nó `default`. **Ao lado dele**, quando nada foi declarado
202
+ (nem `project.tests[]` nem `project.testCommand`), entra um nó por `package.json` que cite `vitest` ou
203
+ `jest` em `dependencies`/`devDependencies`, procurado em `.`, `src/frontend`, `src/web`, `frontend`,
204
+ `apps/web`, `packages/app` e `project.frontendPath` (quando fica dentro da raiz): id `vitest`/`jest` na
205
+ raiz e `vitest-src-web`/`jest-frontend` fora dela, comando `npx vitest run` / `npx jest --ci`. É palpite,
206
+ não declaração: um `package.json` ilegível é pulado em silêncio, e o da raiz não entra quando o próprio
207
+ `default` já é o `npm test`. Um `project.testCommand` declarado vence por inteiro — nada é somado a ele.
208
+ Sem `config.json` nenhum vale a mesma regra (o repositório-fonte do morph-spec resolve só o `default`,
209
+ `npm test`), e nada aqui depende do arquivo existir. Com algo declarado, o `doctor --full` avisa (WARN,
210
+ nunca `✗`) sobre um `package.json` de frontend que nenhum nó — ou o `testCommand` — cobre.
200
211
 
201
212
  **Forma do resultado.** `nodes.tests` continua um objeto com `status` (o rollup) e ganha `runs[]` — uma
202
213
  entrada por nó, com `id`, `command`, `cwd`, `status`, `durationMs`, `count`, `expectedCount` e
@@ -206,7 +217,9 @@ se qualquer run reprovar, e um nó que estoura o relógio não interrompe os seg
206
217
  **qual** falhou manda o leitor para a metade errada do repositório.
207
218
 
208
219
  **Contagem e a regra assimétrica.** O extrator lê o sumário do runner (somando sumários encadeados no
209
- `dotnet`, e exigindo a linha `Tests` — não `Test Files` — no vitest). Contra `expectedCount`: **queda
220
+ `dotnet` — a linha do VSTest ou o bloco do Microsoft.Testing.Platform, este em pt-BR e en-US; um bloco
221
+ MTP por invocação, mesmo numa solução de vários projetos —, e exigindo a linha `Tests` — não
222
+ `Test Files` — no vitest). Contra `expectedCount`: **queda
210
223
  reprova** (mesmo com exit 0 — é regressão silenciosa), **alta apenas avisa** e pede regravação
211
224
  (reprovar a alta ensinaria o agente a apagar testes), e contagem **desconhecida** é `null`, jamais `0`
212
225
  — um zero fabricaria queda a partir de "não deu para ler o output". O aviso de alta sai **mesmo com o
@@ -219,7 +232,7 @@ antes da execução, com `dotnet build "<alvo>" --nologo -v q`. Compilação que
219
232
  build nesta invocação reprova com `stale-binary`, também sem executar: o predicado é "houve build
220
233
  aqui", nunca "a flag está ausente". Sem isso, um projeto de teste que deixou de compilar era executado
221
234
  a partir do binário do último build bom e reportava verde. `--no-build` continua legítimo — com
222
- `build[]` declarado ele roda normalmente, e o comando do usuário nunca é reescrito. O prebuild é
235
+ `build[]` declarado ele roda normalmente, e o comando do usuário nunca é reescrito para isso. O prebuild é
223
236
  **condicional** (só com `build[]` declarado ou com `--no-build` no comando): um `dotnet test X.csproj`
224
237
  sem `--no-build` já compila sozinho, e pré-compilar ali só dobraria o relógio do caminho quente.
225
238
 
@@ -229,7 +242,8 @@ atômica `tmp.{pid}` + rename, preservando todo o resto do arquivo). Recusa **qu
229
242
  vermelha, execução **parcial** (sob `--filter`, mesmo com o verify verde: baseline de meia frota é
230
243
  afirmação sobre a outra metade que ninguém fez), contagem desconhecida, e **sem `config.json` ou sem
231
244
  `project.tests[]`** (erro de uso, exit 2; o arquivo nunca é criado implicitamente — gravar os nós que o
232
- fallback sintetiza exigiria inventar a declaração que o comando veio manter). Verde com gravação
245
+ fallback sintetiza exigiria inventar a declaração que o comando veio manter). E, antes das quatro, `{task}` na
246
+ linha de comando é erro de uso (exit 2): baseline é da frota inteira, e o escopo de task recorta nós e testes. Verde com gravação
233
247
  recusada sai **exit 1**, com a razão em **stderr** (sobrevive ao `--json`). A gravação é governada
234
248
  pelos **nós de teste**, não pelos quatro nós do verify: um `validators` vermelho não muda quantos
235
249
  testes rodaram. E o comando **não** persiste o stamp `verification` — é manutenção de config, não prova
@@ -243,9 +257,10 @@ passa com aviso de alta, e o comando grava.
243
257
 
244
258
  | Momento | Comando | Por quê |
245
259
  | --- | --- | --- |
246
- | Loop por task | `verify {f} {task} --filter {stack-da-task}` | O relógio de uma task não paga a suíte inteira; a task tocou **um** stack |
260
+ | Loop por task | `verify {f} {task}` (recorte automático: o `group` escolhe os nós, os `outputs` estreitam os testes — ver *O recorte por task*; `--filter {stack-da-task}` só para forçar os nós, `--full` para desligar) | O relógio de uma task não paga a suíte inteira; a task tocou **um** stack |
247
261
  | Gate 3 | `verify {f} --json` (sem filtro) | A prova que o avaliador lê tem de cobrir **todos** os nós |
248
262
  | CI | a suíte inteira, todos os nós | idem — e é onde as duas plataformas rodam |
263
+ | Evidência de eval (task com `evidencias/` em `outputs`) | `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist` | Só o nó `evals` (pulado no escopo de task), sem stack e sem carimbo — não é a verificação da task, é a prova que o `morph-implement` §5 grava |
249
264
 
250
265
  > **Não existe constante de 600 s neste CLI** (`grep -rn "600000" src/ framework/` → zero). O teto que
251
266
  > corta uma suíte longa é o do **Bash do harness**, externo ao morph-spec, e não há o que "subir". Os
@@ -257,6 +272,188 @@ passa com aviso de alta, e o comando grava.
257
272
  > Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum
258
273
  > teste falhou`) em vez de devolver log truncado.
259
274
 
275
+ ### O recorte por task: `verify {f} {task}`
276
+
277
+ Sem task, o `verify` roda tudo e **nunca recorta** — é a prova do Gate 3. Com `{task}`, ele lê de
278
+ `tasks.json` o `group` e os `outputs` da task e toma duas decisões independentes:
279
+
280
+ 1. **Quais nós.** O `group` da task escolhe os nós pelo campo `groups` de cada um (ou pelo default do
281
+ `runner`, na tabela de campos acima). Um nó que serve outro grupo sai do escopo e **aparece** em
282
+ `runs[]` como `skip` com o motivo (`out of task scope: …`) — a menos que seja dono de um arquivo de
283
+ teste da task (rede para task mal agrupada) ou não tenha grupo algum. Grupo que nenhum nó serve (por
284
+ default, `tests` e `infra`) ou task sem `group` → todos os nós. Task `docs` roda só os nós donos de
285
+ algum arquivo dos `outputs` — dentro do `cwd` do nó, ou compilado pelo `.csproj` de um link do
286
+ comando; um nó de `cwd` `.` é dono de tudo, então no layout de nó único a task `docs` roda o nó inteiro.
287
+ Arquivo dos `outputs` que ainda não existe em disco não conta: uma task `docs` cujos `outputs` ainda
288
+ não existem tira **todos** os nós do escopo, e o rollup `tests` sai `skip` (`every test node was
289
+ skipped`).
290
+ 2. **Quais testes dentro do nó.** Os arquivos de teste que estão nos `outputs` estreitam o comando
291
+ declarado — **só** quando ele tem uma forma reconhecida. Qualquer outra coisa roda o nó **inteiro**,
292
+ com o motivo em `runs[].selection.reason`.
293
+
294
+ | Runner | Forma reconhecida do comando | O que o recorte acrescenta | Prova de que algum teste rodou |
295
+ | --- | --- | --- | --- |
296
+ | `vitest` | um comando só, com `vitest` e `run` (sem `;`, `&`, `\|` nem `npm`/`pnpm`/`yarn`/`bun run`) | os caminhos dos arquivos, relativos ao `cwd` | contagem > 0 |
297
+ | `jest` | um comando só, com `jest` | `--runTestsByPath "<caminho>" …` | contagem > 0 |
298
+ | `dotnet`, VSTest | links `dotnet test …` encadeados só por `&&` | `--filter "FullyQualifiedName~<Tipo>\|…"`; link sem projeto, ou com `.sln`, é re-apontado para o `.csproj` dono (`dotnet test "<rel.csproj>" …`) | contagem > 0 **do nó** — numa cadeia, a soma dos links (limite (h)); o VSTest sai 0 com filtro que não casa nada |
299
+ | `dotnet`, MTP + xunit.v3 | idem, com `global.json` → `test.runner: Microsoft.Testing.Platform` | `--filter-class "*<Tipo>*"` por tipo; link sem projeto ganha `--project "<rel.csproj>"` | contagem > 0 do sumário MTP (pt-BR e en-US); sem sumário legível, exit 0 (o MTP sai 8 com zero testes) |
300
+ | `dotnet`, MTP + MSTest/NUnit | idem | `--filter "FullyQualifiedName~<Tipo>\|…"` | idem |
301
+
302
+ `node-test`, `playwright` e `none` não têm recorte dentro do nó: rodam inteiros (nós `e2e: true` e
303
+ `evals` seguem pulados no escopo de task, como antes). **Piso de projeto no MTP:** `--minimum-expected-tests
304
+ 1` na linha de comando **não** sobrescreve o piso do projeto — um recorte verde sairia 9 —, mas a
305
+ propriedade global do MSBuild sim (medido). Por isso, quando o piso vem de **um** lugar só, o recorte
306
+ acrescenta `"-p:TestingPlatformCommandLineArguments=<o mesmo valor, com o piso em 1>"`. Um lugar só é: um
307
+ único `<TestingPlatformCommandLineArguments>` literal no próprio `.csproj`, sem atributo, dentro de um
308
+ `<PropertyGroup>` sem atributo direto sob `<Project>` (nunca num `<Target>`, num `<Choose>`/`<When>` ou num
309
+ grupo com `Condition=`), sem `TreatAsLocalProperty` e sem `<Import>` no `.csproj`; sem `$(…)`, `;`, `,`,
310
+ aspas, crase, `\`, `!`, `@`, `%` ou entidade no valor; a propriedade ausente do `.csproj.user` e de todo
311
+ `Directory.Build.props/.targets/.rsp` e `Directory.Packages.props` até a raiz do **disco** (o MSBuild não
312
+ para no repositório); nenhum piso no `*testconfig.json` ao lado; e o comando sem declarar a propriedade ele
313
+ mesmo. O `-p:` troca o valor **inteiro**, então os outros argumentos vão junto, literais; verde sai 0, zero
314
+ testes segue saindo 8, e nada é recompilado (medido). É o caso do `ai-kit` do framework e dos projetos
315
+ copiados dele. Piso em qualquer outra forma, sob VSTest ou num link que roda mais de um projeto roda o nó
316
+ **inteiro**. **O que fica fora:** um pacote NuGet cujo `build/*.targets` acrescente argumentos à
317
+ propriedade não é lido, e esses argumentos caem durante o recorte; um `--explicit` do xunit.v3 chegando por
318
+ aí muda **quais** testes rodam. `<VSTestTestCaseFilter>` declarado também roda o nó inteiro (um `--filter`
319
+ acrescentado o substituiria em silêncio).
320
+
321
+ **O seletor .NET é o TIPO, sem namespace.** Cada `class`/`record`/`struct`/`interface` declarado nos
322
+ arquivos de teste da task, em qualquer profundidade, vira um seletor com o nome **nu**. Medido no MTP +
323
+ xunit.v3: `--filter-class "*<Tipo>*"` roda o tipo em qualquer namespace, os aninhados `Outer+Inner`,
324
+ arquivo com BOM e namespace global, e todo tipo cujo nome contém o seletor; `FullyQualifiedName~<Tipo>`
325
+ é substring, mesma semântica. O recorte pode **acrescentar** testes, nunca tirar os do arquivo. (Sem o
326
+ curinga à esquerda, `<Tipo>*` não roda nada — medido, exit 8.)
327
+
328
+ **Falha para o lado seguro (.NET).** O `.cs` é lido por uma varredura que não é parser, e o recorte não
329
+ depende de ela acertar: toda palavra-chave de tipo contada no **texto cru** do arquivo tem de ser uma
330
+ declaração que a varredura nomeou, senão o nó roda inteiro. Também rodam o nó inteiro, cada caso com o
331
+ seu motivo:
332
+
333
+ - arquivo que declara uma classe `abstract` (os testes rodam sob o nome das derivadas, talvez em
334
+ arquivos que a task não tocou);
335
+ - arquivo do projeto de teste **sem** atributo de teste — helper, fake, fixture: quem o usa está em
336
+ outro arquivo. Atributo de teste é o **nome inteiro** (`Fact`, `Theory`, `Test`, `TestMethod`,
337
+ `DataTestMethod`, `TestCase`, `TestCaseSource`, com ou sem `Attribute`, qualificado ou não) numa
338
+ seção de atributo: o `, TestContext ctx` ou `, Factory f` do construtor de um helper não conta;
339
+ - **qualquer** arquivo que não é C# e que o nó pode rodar (dentro do `cwd`, ou cujo projeto mais
340
+ próximo é o de um link) quando o projeto mais próximo dele (`.csproj`/`.fsproj`/`.vbproj`) é de teste —
341
+ `.razor` do bUnit, `.feature` do Reqnroll/SpecFlow, snapshot `.verified.txt` do Verify, dado de teste —
342
+ ou, fora de projeto de teste reconhecido, `.razor`/`.cshtml`/`.fs`/`.fsi`/`.fsx`/`.vb` que lê como
343
+ teste: a varredura só lê C#, então ele não tem seletor, e os testes que ele alimenta podem estar em
344
+ arquivos que a task não tocou. Arquivo de projeto que não é de teste (componente ou `appsettings.json`
345
+ da app) não custa nada;
346
+ - arquivo tocado que parece teste mas o reconhecedor não classifica como tal (nunca é deixado de fora);
347
+ - literal ou comentário sem fim, chave desbalanceada, buraco de interpolação cortado por aspas,
348
+ diretiva de pré-processador entre a palavra-chave e o nome;
349
+ - link cujo projeto não resolve para um `.csproj` existente dentro da raiz (`--project=X.csproj`,
350
+ `--project:X.csproj`, `.csproj` fora da raiz);
351
+ - projeto que faz o MTP **ignorar um exit code** — `--ignore-exit-code` ou
352
+ `TESTINGPLATFORM_EXITCODE_IGNORE` citado em qualquer ponto do `.csproj` ou de um
353
+ `Directory.Build.props/.targets` até a raiz (em `<TestingPlatformCommandLineArguments>` ou numa
354
+ propriedade que ela referencia, como `$(MtpExtraArgs)`), ou num `*testconfig.json` ao lado — e
355
+ `TESTINGPLATFORM_EXITCODE_IGNORE` não vazia no ambiente do verify: sem sumário legível, o exit 0 é a
356
+ prova de que o recorte rodou teste, e com o 8 ignorado zero testes saem 0 (roda inteiro mesmo que o
357
+ sumário fosse ser legível — isso é decidido antes da rodada);
358
+ - comando que já carrega `--filter`/`--treenode-filter`, argumentos depois de `--`,
359
+ `--ignore-exit-code` ou `--minimum-expected-tests` (inclusive dentro de `-p:`); `global.json`
360
+ ilegível; MTP com framework sem filtro de classe conhecido; alvo MTP de solução ou pasta, alvo VSTest
361
+ de pasta; `--solution`, `--directory`, `--test-modules` e `--project` que não aponta um `.csproj` —
362
+ em token separado **ou** com o valor depois de `=`/`:` num token só (`--solution=X.sln`,
363
+ `--test-modules:…`, `--project=<pasta>`); nome de tipo fora de `[A-Za-z0-9_.]`.
364
+
365
+ O dono de um arquivo tocado é o `.csproj` do link, nunca o `cwd` do nó: num `dotnet test --project
366
+ ../B.Tests/B.Tests.csproj`, os arquivos de `B.Tests` são desse link. Link cujo projeto não compila
367
+ nenhum arquivo da task sai da cadeia. **O custo, dito:** texto com cara de declaração num comentário ou
368
+ string (`// the class under test`), restrição genérica seguida de nome (`where T : class where U : …`),
369
+ `: class // nota`, `record with`, `$"{d["k"]}"`, atributo de teste por alias
370
+ (`using F = Xunit.FactAttribute;`) — tudo isso roda o nó inteiro. Custa relógio, não um teste pulado.
371
+
372
+ **O veredito da rodada recortada** é próprio:
373
+
374
+ - **Nunca compara com `expectedCount`**: a baseline é da suíte inteira — um recorte que roda 12 de
375
+ 3.082 não é queda, e um superconjunto não é alta. Nó rodado **inteiro** no escopo de task continua
376
+ com a regra de contagem de sempre.
377
+ - Verde exige prova de que rodou teste: contagem > 0 **do nó** (numa cadeia VSTest, a soma dos links —
378
+ limite (h)). No MTP a contagem vem do sumário (lido em pt-BR e en-US); sem sumário legível, a prova é
379
+ o exit 0 (por isso o que faz o MTP ignorar exit code roda inteiro — acima; o que o varredor não vê é
380
+ o limite (f)). Contagem 0 → `narrowed-zero-tests: o recorte executou 0 testes` — no MTP inclusive
381
+ com exit 0, que é o que um exit 8 ignorado produz (medido). Como o MTP imprime um bloco de sumário por
382
+ invocação, numa cadeia `&&` **cada** bloco é julgado: um bloco com `total: 0` é vermelho
383
+ (`narrowed-zero-tests: um link do recorte executou 0 testes …`) mesmo que a soma dos links dê mais que
384
+ zero. Verde sem contagem legível (e sem o exit 0
385
+ do MTP) → `narrowed-count-unknown` — desconhecido nunca vira verde.
386
+ - Vermelho com a assinatura de zero testes do runner (dotnet: exit 8; VSTest: `No test matches the
387
+ given testcase filter` / `Nenhum teste corresponde ao filtro de testcase`; vitest: `No test files
388
+ found`; jest: `No tests found`) e sem contagem > 0 → `narrowed-zero-tests (<runner>, exit <n>)`.
389
+ Qualquer outro vermelho, exit 9 inclusive, mantém a razão crua do executor.
390
+ - As contagens de vitest, jest e dotnet são lidas depois de tirar os códigos ANSI da saída.
391
+
392
+ **O que fica registrado.** Cada entrada de `runs[]` ganha `selection` — `null` no escopo de feature; no
393
+ de task, `mode` (`narrowed` / `whole` / `out-of-scope` / `full`), `reason`, `declaredCommand`, `files`,
394
+ `selectors` e `zeroGuard`. `runs[].command` é o que rodou. `nodes.tests.scope` (`scope`, `derivation`,
395
+ `group`, `testFiles`) sai só no `--json`, e nada da `selection` vai para o `feature.json`: o stamp grava
396
+ `scope: 'task:{id}'`, e o `gate-check` do `review` pausa sobre ele (seção **Gates e estado**). A saída
397
+ humana imprime uma linha `↳ {id} {mode} {detalhe}` por nó.
398
+
399
+ **Os `outputs` têm de ser os reais.** O recorte lê os `outputs` do `tasks.json` na hora do verify, e
400
+ caminho que não existe em disco é ignorado. Por isso o `morph-apply` grava os `outputs` reais **antes**
401
+ do verify do loop: com os planejados, um arquivo de teste tocado e ausente deles fica fora do recorte.
402
+ Cada caminho é lido na **grafia do disco**: `web/Src/Foo.test.ts` nos `outputs` e `web/src/foo.test.ts`
403
+ no disco → o comando recebe `src/foo.test.ts` (o contêiner do `--linux` diferencia maiúsculas, e o jest
404
+ pula em silêncio o caminho que não acha). Só a **caixa** vem do disco: caminho que passa por um link
405
+ (symlink ou junction) fica como escrito, e o arquivo segue no nó cujo `cwd` o contém.
406
+ E uma **pasta** nos `outputs` não estreita nada — quais
407
+ arquivos dela a task tocou não se sabe: todo nó cujo `cwd`, ou a pasta do projeto de um link, se
408
+ sobrepõe a ela (uma contém a outra) roda **inteiro**, e nunca sai do escopo por ela.
409
+
410
+ **`--filter` e `--full`.** `--filter <ids>` força a escolha de nós: desliga a escolha pelo `group`, e o
411
+ estreitamento dentro dos nós continua. `--full` desliga as duas decisões — cada nó roda o comando
412
+ declarado, com `mode: 'full'` (o `--filter`, se vier junto, continua escolhendo os nós). No escopo de
413
+ feature, `--full` é no-op.
414
+
415
+ **Limites conhecidos do recorte** — o que ele **não** garante hoje:
416
+
417
+ - (a) classe base de teste **não** abstrata cujos testes são herdados por derivadas em arquivos que a
418
+ task não tocou: roda a classe tocada; as derivadas, só se o nome delas contiver o da base
419
+ (`*ContractTests*` casa `SqlContractTests`, não `SqlTests`);
420
+ - (b) `partial class` que é `abstract` só na parte de **outro** arquivo: não é detectada, e o arquivo
421
+ tocado estreita;
422
+ - (c) link VSTest para `.sln` num nó cujo `cwd` é mais estreito que a solução: é re-apontado só para os
423
+ projetos dos arquivos **dentro** do `cwd` — arquivo de teste da task em outro projeto da solução não
424
+ roda nesse nó. O mesmo vale para a **escolha** dos nós: a rede da task mal agrupada (e a pasta tocada)
425
+ só alcança o nó cujo `cwd`, ou o `.csproj` de um link, contém o arquivo — o `.sln` não é lido, e um nó
426
+ que só chega ao arquivo pela solução sai do escopo pelo `group`;
427
+ - (d) o atributo de teste é reconhecido pelo nome, sem compilar. A seção tem de **fechar** como seção
428
+ de atributo (`]` seguido de declaração ou do fim do arquivo), então coleção em várias linhas com
429
+ `Env.Test`, linha `[Env.Test, Env.Dev],` e `Test("a")` não enganam mais. O que ainda engana, sondado:
430
+ um **padrão de lista** que abre a linha (ou vem depois de `;` `{` `}` `]`), cujo elemento se chama
431
+ exatamente `Test`, `Fact`, … (qualificado ou não) e cujo `]` é seguido de um combinador —
432
+ `[Env.Test] or [Env.Dev] => true` num helper faz dele arquivo de teste, que estreita para o nome do
433
+ helper. Se um teste real
434
+ tiver nome que contém o do helper (`*FakeClock*` casa `FakeClockTests`), esse teste roda e a rodada
435
+ pode sair **verde sem rodar os testes que usam o helper**, que estão em arquivos que a task não tocou.
436
+ Sem teste com nome assim, o filtro não casa nada: no MTP sai 8 (vermelho); no VSTest é vermelho num nó
437
+ de link único, mas numa cadeia de links cai no limite (h). Atributo customizado (`[SkippableFact]`) ou
438
+ por alias não é visto: o arquivo conta como helper e o nó roda inteiro (custo, não risco);
439
+ - (e) arquivo de teste JS/TS com nome fora de `*.test.*` / `*.spec.*` / `__tests__/` e sem `describe(`
440
+ / `it(` / `test(` no início de uma linha não é reconhecido: o nó estreita para os outros arquivos da
441
+ task;
442
+ - (f) no MTP com o sumário **ilegível** (o `dotnet` num idioma que não é pt-BR nem en-US) a prova volta
443
+ a ser só o exit code, e o recorte só vê o que o varredor lê (o comando, o `.csproj`, os
444
+ `Directory.Build.props/.targets` até a raiz, o `*testconfig.json` ao lado e o ambiente do verify):
445
+ `--ignore-exit-code` ou `TESTINGPLATFORM_EXITCODE_IGNORE` que chegue por um `<Import>` (não é
446
+ seguido), por um `Directory.Build.rsp`, por um `.runsettings` ou pela imagem do contêiner do `--linux`
447
+ não é visto — o nó estreita, e zero testes podem sair 0: verde. Com o sumário legível o mesmo caso sai
448
+ `total: 0` e é vermelho (medido) — numa cadeia `&&` também, porque cada link imprime o próprio bloco;
449
+ - (g) dois `.csproj` na mesma pasta: o dono é o primeiro em ordem alfabética;
450
+ - (h) numa **cadeia** VSTest (vários links `&&`), a contagem é a **soma** dos links: um link cujo filtro
451
+ não casou nada (o VSTest sai 0) fica verde ao lado de um link que contou. "Contagem > 0" é prova por
452
+ nó, não por link. No MTP isso não acontece: o link vazio sai 8 e derruba a cadeia — e, se esse 8 for
453
+ ignorado, o bloco de sumário dele diz `total: 0`, que derruba o recorte (com o sumário legível).
454
+
455
+ A saída de todos é a mesma: `--full`.
456
+
260
457
  ## Instalação e atualização
261
458
 
262
459
  - **`morph-spec update [--templates] [--standards] [--dry-run] [--force]`** — reinstala os payloads do framework no projeto (standards, hooks, schemas, rubrics, evals, `.claude/{commands,rules,skills,agents}`, `.claude/CLAUDE.md`, `.gitignore`/`.gitattributes`, statusline global).
@@ -47,6 +47,22 @@ duas é o defeito inteiro que esta classe de rubrica persegue.
47
47
  errada quando **a asserção começou a reprovar**. As duas situações produzem o mesmo comando e
48
48
  vereditos opostos: diga qual das duas era, e como sabe.
49
49
 
50
+ ## Insumo: 3-implement/evidencias/
51
+
52
+ Task cujo `doneCriteria` julga saída de LLM grava `.morph/features/{feature}/3-implement/evidencias/{taskId}.md`
53
+ (`morph-plan` §5): critério, nó `evals` e cenário, modelo, versão do prompt, entrada, saída e
54
+ veredito. Não é o `3-implement/reports/{taskId}.md` — aquele é o que o sub-agente diz que fez. É
55
+ insumo, não veredito: leia antes de pontuar e confronte o veredito com a saída registrada.
56
+
57
+ - **Evidência atada a um nó `evals`** (`runner: "evals"`): as bandas acima valem como estão, e a
58
+ degradação desta rubrica usa **o cenário que a evidência aponta** — é ele que tem de cair, pelo
59
+ nome.
60
+ - **Evidência sem nó** ("nenhum nó — execução manual"): prova que o prompt rodou **uma vez**, não que
61
+ algo pegaria a regressão — não há cenário a degradar. Banda **4-6** no máximo, por melhor que a
62
+ saída registrada pareça.
63
+ - **Evidência devida e ausente** (o path está em `outputs` e o arquivo não existe): superfície de
64
+ prompt/modelo mexida sem eval nenhum — banda 0-3.
65
+
50
66
  ## Como registrar no relatório
51
67
 
52
68
  Uma linha por cláusula degradada, dizendo o que foi degradado e qual cenário caiu **pelo nome**:
@@ -31,12 +31,14 @@ Se o diff toca endpoint minimal API ou fetch cross-camada, independente do resto
31
31
  | **Arquitetura** | Ignora a arquitetura do spec.md; sem feature folder; deps circulares; ou violação mecânica acima | Pastas corretas, responsabilidades se misturam; deps cross-feature sem justificativa; domain acoplado a infra | Desvios menores justificados; deps cross-feature via abstração; impurezas de infra pontuais | Feature folder rigoroso; zero deps cross-feature diretas; domain isolado; DI onde exige |
32
32
  | **Contratos** | Sem tipos ou `any` generalizado; zero validação; sem tratamento de erro; naming arbitrário | `any`/genérico visível; validação parcial; mix exceptions/Result; naming inconsistente | Tipos bem definidos, `any` raro e justificado; validação na maioria; erro consistente | Zero `any`; validação em todos endpoints; Result pattern/discriminated union; naming consistente |
33
33
  | **Qualidade** | Nomes arbitrários; god objects; duplicação massiva; recria o que o `reuse-map` mandou REUSAR; fora do padrão do stack | Nomes genéricos (`data`,`temp`); funções >30 linhas; duplicação perceptível; conceito da MESMA feature redeclarado | Nomes claros, raras exceções; bem dimensionado; duplicação mínima; no máximo um conceito recriado | Nomes comunicam intenção; funções ≤20-25 linhas, 1 responsabilidade; zero duplicação/dead code; nada criado que já existia no projeto |
34
- | **Testes** | Sem testes ou sem asserts reais; zero edge case; nomes genéricos | Happy path OK, edge cases parciais; erros só óbvios; testes interdependentes | Happy path completo; maioria dos edge cases; erros nos fluxos principais | Toda pós-condição do `usecase` tem teste; A1/A2 cobertos; edge+erro cobertos; isolados |
34
+ | **Testes** | Sem testes ou sem asserts reais; zero edge case; nomes genéricos | Happy path OK, edge cases parciais; erros só óbvios; testes interdependentes; teste de texto de arquivo sem substituto em runtime | Happy path completo; maioria dos edge cases; erros nos fluxos principais | Toda pós-condição do `usecase` tem teste; A1/A2 cobertos; edge+erro cobertos; isolados |
35
35
 
36
36
  > **Qualidade olha o CODEBASE, não só o diff.** Símbolo novo cujo nome já existe no projeto não aparece no diff — é preciso perguntar de fora dele. O nó `validators` do `morph-spec verify` já traz os achados do validador `reuse`; o `2-plan/reuse-map.md` diz o que o plano decidiu por conceito.
37
37
 
38
38
  > Task com `usecase: UC-nn`: pós-condição sem teste **impede a faixa 9-10** em Testes, mesmo com o resto da suíte completo (herdado de `test-coverage.md`).
39
39
 
40
+ > Teste que só lê texto de arquivo não cobre pós-condição de comportamento: sem substituto em runtime, ela conta como sem teste (herdado de `test-coverage.md`, pergunta 7).
41
+
40
42
  ## Score composto
41
43
 
42
44
  ```
@@ -58,6 +58,14 @@ Avalia se os testes cobrem os comportamentos esperados, são isolados e têm nom
58
58
  min; o fecho de um fail-open abriu um P1; a correção desse P1 saiu com meio predicado. Nenhuma
59
59
  apareceu no score, porque score não pergunta isso. **Sem resposta a esta pergunta numa correção,
60
60
  o teto de Cobertura é 8.**
61
+ 7. **Quantos testes desta task asseram sobre TEXTO DE ARQUIVO** (prompt, migration, config,
62
+ markdown, código-fonte lido como string)? **Cada um tem substituto em runtime** — um teste
63
+ que exercita o sistema ou, em critério sobre saída de LLM, a evidência em
64
+ `3-implement/evidencias/{taskId}.md` apontando um eval? Teste de texto prova a VERSÃO do
65
+ arquivo, não o comportamento: quando é o único teste de uma pós-condição, **a pós-condição
66
+ conta como sem teste** (pergunta 0 — teto 8). Exceções: pós-condição com
67
+ `> [!note] Por que artefato:` e o único teste de estrutura por fonte de prompt
68
+ (`ai-agents-testing-ai`). Medido: 12 suítes, 106 testes sobre versões superadas do prompt.
61
69
 
62
70
  ## Tetos de prova (Gate 3)
63
71
 
@@ -35,6 +35,7 @@ export const OUTPUT_PHASE_MAP = {
35
35
  reuseMap: 'plan',
36
36
  usecases: 'plan',
37
37
  recap: 'implement',
38
+ evidencias: 'implement',
38
39
  reviewReport: 'review',
39
40
  evaluatorReport: 'review',
40
41
  };
@@ -63,7 +63,7 @@
63
63
  "Maintain recap.md and execution trace"
64
64
  ],
65
65
  "requiredOutputs": ["recap"],
66
- "optionalOutputs": [],
66
+ "optionalOutputs": ["evidencias"],
67
67
  "pausePoints": [],
68
68
  "skill": "morph-implement"
69
69
  },
@@ -73,7 +73,7 @@ All outputs go in `.morph/features/{feature}/`:
73
73
  | Proposal | `0-proposal/proposal.md`, `stack-scan.md` |
74
74
  | Design (optional) | `1-design/design-system.md`, `mockups.md`, `components.md`, `flows.md` |
75
75
  | Plan | `2-plan/spec.md`, `mandate.md`, `tasks.json`, `decisions.md` |
76
- | Implement | `3-implement/recap.md` + source code |
76
+ | Implement | `3-implement/recap.md` + source code; `3-implement/evidencias/{taskId}.md` (condicional — task cujo `doneCriteria` julga saída de LLM) |
77
77
  | Review | `4-review/review-report.md` |
78
78
 
79
79
  ---
@@ -63,6 +63,13 @@
63
63
  "minLength": 1,
64
64
  "description": "Overrides the container image used by `verify --linux` for this node."
65
65
  },
66
+ "groups": {
67
+ "type": "array",
68
+ "minItems": 1,
69
+ "uniqueItems": true,
70
+ "items": { "type": "string", "enum": ["backend", "frontend", "tests", "infra", "docs"] },
71
+ "description": "Which task group(s) (tasks.json `group`) this node verifies. Absent means the runner's own default applies (RUNNER_DEFAULT_GROUPS in src/lib/tasks/test-nodes.js) — `none`/`evals` have no default and are unscoped unless declared here."
72
+ },
66
73
  "evals": {
67
74
  "type": "object",
68
75
  "additionalProperties": false,
@@ -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.35.1"
7
+ cliVersion: "8.37.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.35.1"
7
+ cliVersion: "8.37.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.35.1"
7
+ cliVersion: "8.37.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -95,11 +95,18 @@ while (true):
95
95
  // só que sem reler o documento inteiro da skill de novo).
96
96
  aplicar feedback priorizado da tentativa anterior ao código já implementado
97
97
 
98
+ // OUTPUTS ANTES DO VERIFY — o recorte por task lê `outputs` de tasks.json.
99
+ // Grave neles (campo mutável, tool Edit) os arquivos REAIS desta tentativa: do
100
+ // relatório `3-implement/reports/{task.id}.md` quando despachada, do que você
101
+ // escreveu quando inline. Divergiu do plano → `notes` na mesma edição.
102
+ atualizar task.outputs em tasks.json
103
+
98
104
  // CÓDIGO ANTES DO JUIZ — barreira determinística e barata (build+testes+validadores).
99
- // Loop por task: --filter com os ids das suítes do stack que a task tocou
100
- // (o relógio de uma task não paga a suíte inteira). Sem project.tests[]
101
- // declarado há um nó só (`default`) e o --filter não tem o que escolher.
102
- run: morph-spec verify {feature} {task.id} [--filter {ids-do-stack-da-task}] --json
105
+ // Loop por task: o recorte é AUTOMÁTICO — o `group` da task escolhe os nós e os
106
+ // arquivos de teste de `outputs` estreitam o comando dentro de cada nó (o relógio
107
+ // de uma task não paga a suíte inteira). `--filter {ids}` só para FORÇAR a
108
+ // escolha de nós; `--full` desliga o recorte.
109
+ run: morph-spec verify {feature} {task.id} --json
103
110
  if verify.status == 'fail':
104
111
  // vermelho: feedback determinístico direto ao loop, SEM gastar o juiz LLM.
105
112
  feedback = verify.feedback // usado como input do próximo attempt
@@ -118,8 +125,11 @@ while (true):
118
125
  break
119
126
 
120
127
  if best.score >= 9:
121
- marcar task.status = 'done' em tasks.json (tool Edit — atualize também outputs
122
- reais e, se divergirem do planejado, registre o porquê em notes)
128
+ // task com `3-implement/evidencias/{id}.md` em `outputs`: confira o arquivo antes do `done`
129
+ // (morph-implement §5). Em batch paralelo sem isolamento o sub-agente não roda eval — o
130
+ // ORQUESTRADOR roda, após o batch, `morph-spec verify {feature} --filter {id-do-nó-evals} --skip-e2e --no-persist`
131
+ marcar task.status = 'done' em tasks.json (tool Edit — confirme os outputs
132
+ reais, já gravados antes do verify, e, se divergirem do planejado, o porquê em notes)
123
133
  else:
124
134
  // 3 tentativas sem atingir 9 — AQUI sim escala ao humano (escape definido).
125
135
  // Esta é a única pausa do loop de implement, e só após esgotar o loop de correção.
@@ -128,7 +138,7 @@ while (true):
128
138
  options: [aceitar (marca done com ressalva no recap), continuar tentando (+3 tentativas), pular task (marca blocked)]
129
139
  ```
130
140
 
131
- > **Loop por task filtra; Gate 3 e CI não.** `--filter {ids}` roda só as suítes do stack que a task tocou — os outros nós **aparecem** em `nodes.tests.runs[]` como `skip` nomeando o filtro, em vez de sumir, e um `--filter` que não casa nada é erro de uso (exit 2), nunca um verde. A verificação pós-batch e a do Gate 3 (`morph-spec verify {feature} --json`) rodam **sem filtro**: a prova que o avaliador lê tem de cobrir todos os nós, e o CI idem. Forma de `project.tests[]` e a tabela da divisão: `.morph/framework/CLI.md`, seção "Suítes de teste".
141
+ > **Loop por task filtra; Gate 3 e CI não.** No loop o filtro é **automático**: `verify {feature} {task.id}` deixa o `group` da task escolher os nós e os arquivos de teste de `outputs` estreitarem o comando — cada nó **aparece** em `nodes.tests.runs[]` com `selection` (`narrowed`/`whole`/`out-of-scope`/`full` e o motivo), nenhum some. `--filter {ids}` só força a escolha de nós (os não casados aparecem como `skip` nomeando o filtro, e um `--filter` que não casa nada é erro de uso, exit 2, nunca um verde); `--full` desliga o recorte. A verificação pós-batch e a do Gate 3 (`morph-spec verify {feature} --json`) rodam **sem filtro**, e `verify {feature}` **nunca recorta**: a prova que o avaliador lê tem de cobrir todos os nós, e o CI idem. Forma de `project.tests[]`, regras e limites do recorte, e a tabela da divisão: `.morph/framework/CLI.md`, seção "Suítes de teste".
132
142
  >
133
143
  > **Verify vermelho consome uma tentativa** do loop (≤3) e devolve `feedback[]` acionável (erros de build, falhas de teste — linhas prefixadas por `[tests/{id}]`, que nomeiam a suíte —, issues de validador). O `morph-eval` NUNCA é invocado sobre código que não passou no verify — o juiz julga arquitetura/contratos/qualidade, o que o código determinístico não pega sozinho. Código roda antes, grátis; o juiz gasta tokens só no que sobra.
134
144
 
@@ -214,7 +224,7 @@ Aguardar todos os agentes terminarem, depois **auditar os relatórios contra o t
214
224
  - Para cada relatório retornado:
215
225
  - Aplicar `Skill(morph-eval)`.
216
226
  - Gravar o score: `morph-spec score {feature} {taskId} {score} --dims {a},{c},{q},{t}`.
217
- - Se score >= 9: marcar `done` em tasks.json (tool Edit — só campos mutáveis).
227
+ - Se score >= 9 (e `3-implement/evidencias/{taskId}.md` gravada, se a task a deve — checagem do §3a): marcar `done` em tasks.json (tool Edit — só campos mutáveis).
218
228
  - Se score < 9: marcar `pending` novamente com nota — volta ao modo sequencial, onde o loop de correção (até 3 tentativas) busca o score 9.
219
229
 
220
230
  Depois do batch paralelo, executar o restante em modo sequencial (passo 3a).
@@ -223,7 +233,7 @@ Depois do batch paralelo, executar o restante em modo sequencial (passo 3a).
223
233
 
224
234
  Quando o `morph-spec dag {feature}` reporta `isolation.recommended: "worktree"` (dois fluxos paralelos planejam escrever no mesmo path — colisão de `outputs`), despache cada fluxo com o parâmetro `isolation: "worktree"` do tool Agent. Cada agente trabalha numa working tree própria; build/test rodam lá dentro sem colidir.
225
235
 
226
- 1. **Dispatch:** mesmo `subagent_type: "morph-{id}"` e standards de §3b, mais `isolation: "worktree"`. O worktree vem do hook `WorktreeCreate` do morph: nasce em `worktrees/<n>` na branch `morph-task/<n>`, com junctions (`node_modules`, `.claude`, `.morph/framework`) e `.morph/worktree.env` já prontos — se faltar infra mesmo assim, o agente roda `morph-spec worktree link --all` (fail-open); se ainda impossível, reporta "verificação: não executada" (o verify pós-merge cobre). Dev server/compose dentro do worktree já sai nas portas do bloco (o hook `worktree-env-inject` prefixa o comando; fora do Claude, `eval "$(morph-spec env)"`). O agente commita na branch do worktree como último ato e reporta a branch.
236
+ 1. **Dispatch:** mesmo `subagent_type: "morph-{id}"` e standards de §3b, mais `isolation: "worktree"`. O worktree vem do hook `WorktreeCreate` do morph: nasce em `worktrees/<n>` na branch `morph-task/<n>`, com junctions (`node_modules`, `.claude`, `.morph/framework`) e `.morph/worktree.env` já prontos — se faltar infra mesmo assim, o agente roda `morph-spec worktree link --all` (fail-open); se ainda impossível, reporta "verificação: não executada" (o verify pós-merge cobre). Dev server/compose dentro do worktree já sai nas portas do bloco (o hook `worktree-env-inject` prefixa o comando; fora do Claude, `eval "$(morph-spec env)"`). O agente commita na branch do worktree como último ato e reporta a branch. Um `morph-spec verify {feature} {task}` rodado ali pelo agente é **consultivo**: lê os `outputs` **planejados** (o sub-agent não edita `tasks.json` — cláusula 2), não é persistido (worktree não registrado: `unregistered-worktree`) e não prova nada — a prova é o verify de escopo feature do passo 3.
227
237
  2. **Integração (orquestrador, após todos os relatórios):** faça o merge de cada branch reportada, uma por vez, na ordem de chegada — `git merge --no-ff <branch>`. Conflito → `git merge --abort`, marque a task de volta a `pending` com nota "merge conflict — reexecutar sequencial", siga para a próxima branch. Os paths em conflito são exatamente os de `isolation.collisions` do dag.
228
238
  3. **Pós-merge:** rode `morph-spec verify {feature} --json` UMA vez (o merge é o novo ponto de integração — verificação compartilhada obrigatória), depois `morph-eval` + `morph-spec score` por relatório, como em §3b.
229
239
  4. **Cleanup:** o hook `WorktreeRemove` remove o worktree **limpo** ao fim do subagent (branch sobrevive) — mas não conte com ele: rode `morph-spec worktree remove --target <path>` por worktree que sobrou (NUNCA `git worktree remove` cru — as junctions do passo 1 seriam atravessadas e o `node_modules`/`.claude` da raiz apagados) + `git branch -d morph-task/<n>` das branches integradas + `git worktree prune` (fail-open). Worktree sujo (subagent não commitou) fica, vira `unintegrated-work` no `doctor --worktrees`, e a task volta a `pending`. Branches não integradas ficam e são registradas no recap com o motivo.
@@ -265,6 +275,7 @@ re-apresente o gate**: siga direto para o encerramento (§6) conforme a decisão
265
275
 
266
276
  - Todas as tasks em `tasks.json` com status `done` ou `skipped`.
267
277
  - `.morph/features/{feature}/3-implement/recap.md` produzido por morph-implement.
278
+ - `.morph/features/{feature}/3-implement/evidencias/{taskId}.md` — só nas tasks que o listam em `outputs` (critério sobre saída de LLM, `morph-plan` §5): evidência de runtime gravada por morph-implement e lida pelo avaliador do Gate 3 — não é o `reports/{taskId}.md` do sub-agente.
268
279
  - `.morph/features/{feature}/4-review/review-report.md` produzido por morph-review.
269
280
  - Gate 3 registrado aprovado em `.morph/features/{feature}/feature.json` (autoritativo) — via `morph-spec approve` em auto, ou pelo hook state-sync no caminho manual.
270
281
  - Feature com Gate 3 aprovado e **fechada** por `morph-spec finish --pr|--merge` (arquiva carimbando `status: "completed"` + `archivedAt` e desmonta num passo; `--merge` integra na hora, `--pr` só abre o PR — `aguardando-merge` até o merge) — ou, se legada, por `morph-spec archive`.