@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.
- package/CLAUDE.md +10 -8
- package/README.md +2 -2
- package/bin/morph-spec.js +4 -3
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +24 -23
- package/framework/docs/CLI.md +212 -15
- package/framework/evals/proof-rubrics/eval-de-modelo.md +16 -0
- package/framework/evals/rubrics/condensed.md +3 -1
- package/framework/evals/rubrics/test-coverage.md +8 -0
- package/framework/hooks/shared/phase-utils.js +1 -0
- package/framework/phases.json +1 -1
- package/framework/rules/morph-workflow.md +1 -1
- package/framework/schemas/config.schema.json +7 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +21 -10
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +20 -3
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +41 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +11 -2
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +2 -2
- package/framework/standards/ai-agents/prompt-sources.md +4 -3
- package/framework/standards/ai-agents/testing-ai.md +14 -13
- package/framework/templates/dotnet/ai-kit/README.md +22 -11
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +23 -10
- package/package.json +1 -1
- package/src/commands/project/doctor.js +51 -2
- package/src/commands/tasks/dag.js +13 -1
- package/src/commands/validation/verify.js +59 -17
- package/src/core/paths/output-schema.js +7 -0
- package/src/lib/detectors/index.js +225 -217
- package/src/lib/risk-detector.js +38 -18
- package/src/lib/specs/done-criteria-check.js +200 -0
- package/src/lib/specs/usecase-check.js +129 -5
- package/src/lib/tasks/dotnet-test-introspection.js +632 -0
- package/src/lib/tasks/eval-node.js +3 -3
- package/src/lib/tasks/test-nodes.js +190 -20
- package/src/lib/tasks/test-runner.js +210 -17
- package/src/lib/tasks/test-scope.js +597 -0
- package/src/lib/validators/traceability-check.js +9 -2
- package/src/lib/validators/verify-runner.js +190 -27
- package/src/lib/worktree/docker-audit.js +5 -1
package/framework/docs/CLI.md
CHANGED
|
@@ -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.
|
|
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>`
|
|
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.**
|
|
152
|
-
|
|
153
|
-
|
|
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`.
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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).
|
|
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}
|
|
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
|
|
package/framework/phases.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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:
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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.**
|
|
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`.
|