@polymorphism-tech/morph-spec 8.36.0 → 8.37.1
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 +5 -5
- package/README.md +2 -2
- package/bin/morph-spec.js +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/docs/CLI.md +47 -24
- 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 +1 -1
- 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 +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +2 -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/ai-agents/rag-custom-pgvector.md +7 -3
- package/framework/templates/dotnet/ai-kit/README.md +12 -7
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +12 -6
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +16 -11
- package/package.json +1 -1
- package/src/commands/validation/verify.js +21 -8
- package/src/lib/tasks/dotnet-test-introspection.js +134 -19
- package/src/lib/tasks/eval-node.js +3 -3
- package/src/lib/tasks/test-runner.js +88 -15
- package/src/lib/tasks/test-scope.js +47 -17
- package/src/lib/validators/verify-runner.js +46 -15
package/CLAUDE.md
CHANGED
|
@@ -127,12 +127,12 @@ When modifying framework components, verify the full scope of related files.
|
|
|
127
127
|
| `src/lib/git/pending-prs.js` | os TRÊS consumidores da mesma síntese "aguardando merge": `list.js` (linha sintetizada), `fleet-scanner.js` (entrada `pr-pending`) e `doctor.js` (`auditPullRequests`). Uma feature visível numa superfície e invisível na outra é o bug que este módulo existe para impedir |
|
|
128
128
|
| `src/lib/git/gh.js` | `pr-index.js`, `pr-files.js` e `finish.js` — as DUAS formas de invocar o `gh` viraram uma; uma segunda cópia de `resolveGh` volta a tornar o caminho de sucesso do índice intestável |
|
|
129
129
|
| `src/lib/worktree/feature-truth.js` | The four leaders reading the same "which tree owns this feature" predicate: `fleet-scanner.js`, `list.js`, `handoff.js`, and the mutator guard (`approve.js`/`advance.js`/`score.js`/`gate-decision.js`/`gate-check.js`) |
|
|
130
|
-
| `src/lib/tasks/test-runner.js` | `verify-runner.js` (o nó `tests` repassa o `--timeout` e lê `timedOut`), `src/commands/project/worktree.js` (as constantes `WORKTREES_DIR`/`LEGACY_WORKTREES_DIR` que a descoberta ignora — duplicar as strings aqui é como o bug volta), `test/lib/test-runner.test.js` (o canário do ignore de worktrees e o do timeout escalado). Um worktree é o checkout de OUTRA feature: varrê-lo dobra o relógio e reporta sobre código fora da árvore avaliada. `stripAnsi` roda antes de TODO extrator de contagem (ponto único em `parseTestCount`) e das strings de `isZeroTestRun` — uma cor SGR no meio do sumário derrotava os regexes ancorados (#110). `ZERO_RUN_SIGNATURES` mistura o medido e o documentado — medidos (capturas em `test/fixtures/test-output/`): o exit 8 do MTP e a string pt-BR do VSTest; só documentados, sem captura: a string en-US do VSTest e as de vitest/jest. O exit 9 fica FORA de propósito: é piso de projeto, não zero testes. Leitor: `judgeNarrowedRun` (`verify-runner.js`). `extractCsprojTargets`, `isInside` e `canonicalPath` também são lidos por `test-scope.js` (`canonicalPath` dá a todo path de `outputs` a grafia do DISCO — no win32 um `Foo.test.ts` mal grafado passa no `isFile`, e sob `--linux`, case-sensitive, o runner não o acha enquanto um irmão mantém a contagem acima de zero); `dotnet-test-introspection.js` lê só `isInside`. Um segundo canonicalizador é como os dois lados passam a discordar sobre o mesmo path |
|
|
131
|
-
| `src/lib/tasks/test-scope.js` (o recorte por task, #87) | Este DECIDE, `verify-runner.js` executa: `applyTaskScope` roda UMA vez em `runVerify`, antes do build, e `preDecideRun` (pula `out-of-scope`) e `judgeRun` → `judgeNarrowedRun` (nunca `compareTestCount`; zero ou desconhecido é vermelho) são a cópia única para host e `--linux` — o container recebe o recorte pelo `node.command`. Importa `effectiveGroups` (`test-nodes.js`), `extractCsprojTargets`/`isInside`/`canonicalPath` (`test-runner.js`), `normalizePath` (`traceability-check.js` — um normalizador só para `outputs`), `samePathCase` (`docker-audit.js` — só a CAIXA vem do disco: um path que atravessa symlink/junction fica como escrito, senão o arquivo muda de nó) e a introspecção .NET abaixo. `runs[].selection` sai só no `--json`: `nodeStatuses()` não o persiste. Forma de comando nova se lê e se MEDE (`test/fixtures/test-output/README.md`) antes de estreitar; o resto roda INTEIRO com motivo. Canários: `test/lib/tasks/test-scope.test.js`, o bloco `runVerify — task-scope recorte` de `test/lib/verify-runner.test.js`, `test/commands/verify-filter.test.js` e a prosa em `test/framework/verify-stack-docs-prose.test.js` |
|
|
132
|
-
| `src/lib/tasks/dotnet-test-introspection.js` | Só LÊ (dividido de `test-scope.js`): o varredor C# que apaga literais (`blankCSharpLiterals`), o tripwire contra o texto CRU + o balanço de chaves (`csharpClasses`), `classNamesOf`, `dotnetTestMode` (lê o `global.json` por `jsoncToJson` de `src/lib/ai/comment-stripper.js`), o piso `--minimum-expected-tests` / `<VSTestTestCaseFilter>` do projeto, `ignoresExitCode` (o MESMO scan de csproj/Directory.Build.*/testconfig: `--ignore-exit-code` ou `TESTINGPLATFORM_EXITCODE_IGNORE` desarma a
|
|
130
|
+
| `src/lib/tasks/test-runner.js` | `verify-runner.js` (o nó `tests` repassa o `--timeout` e lê `timedOut`), `src/commands/project/worktree.js` (as constantes `WORKTREES_DIR`/`LEGACY_WORKTREES_DIR` que a descoberta ignora — duplicar as strings aqui é como o bug volta), `test/lib/test-runner.test.js` (o canário do ignore de worktrees e o do timeout escalado). Um worktree é o checkout de OUTRA feature: varrê-lo dobra o relógio e reporta sobre código fora da árvore avaliada. `stripAnsi` roda antes de TODO extrator de contagem (ponto único em `parseTestCount`) e das strings de `isZeroTestRun` — uma cor SGR no meio do sumário derrotava os regexes ancorados (#110). O extrator `dotnet` lê os DOIS sumários (#115): a linha VSTest e o bloco MTP (cabeçalho `Test run summary:`/`Resumo da execução de teste:` na coluna 0, `total:` indentado), em pt-BR e en-US — UM bloco por invocação de `dotnet test` (uma solução de dois projetos dá um bloco agregado, medido), então somar blocos é somar links `&&`; um terceiro locale é `null`. `mtpSummaryTotals` (um total por bloco) é o que `judgeNarrowedRun` lê para achar, numa cadeia MTP recortada, o link que rodou 0 (exit 8 ignorado) escondido pela soma. `ZERO_RUN_SIGNATURES` mistura o medido e o documentado — medidos (capturas em `test/fixtures/test-output/`): o exit 8 do MTP e a string pt-BR do VSTest; só documentados, sem captura: a string en-US do VSTest e as de vitest/jest. O exit 9 fica FORA de propósito: é piso de projeto, não zero testes. Leitor: `judgeNarrowedRun` (`verify-runner.js`). `extractCsprojTargets`, `isInside` e `canonicalPath` também são lidos por `test-scope.js` (`canonicalPath` dá a todo path de `outputs` a grafia do DISCO — no win32 um `Foo.test.ts` mal grafado passa no `isFile`, e sob `--linux`, case-sensitive, o runner não o acha enquanto um irmão mantém a contagem acima de zero); `dotnet-test-introspection.js` lê só `isInside`. Um segundo canonicalizador é como os dois lados passam a discordar sobre o mesmo path |
|
|
131
|
+
| `src/lib/tasks/test-scope.js` (o recorte por task, #87) | Este DECIDE, `verify-runner.js` executa: `applyTaskScope` roda UMA vez em `runVerify`, antes do build, e `preDecideRun` (pula `out-of-scope`) e `judgeRun` → `judgeNarrowedRun` (nunca `compareTestCount`; zero ou desconhecido é vermelho) são a cópia única para host e `--linux` — o container recebe o recorte pelo `node.command`. Importa `effectiveGroups` (`test-nodes.js`), `extractCsprojTargets`/`isInside`/`canonicalPath` (`test-runner.js`), `normalizePath` (`traceability-check.js` — um normalizador só para `outputs`), `samePathCase` (`docker-audit.js` — só a CAIXA vem do disco: um path que atravessa symlink/junction fica como escrito, senão o arquivo muda de nó) e a introspecção .NET abaixo. `runs[].selection` sai só no `--json`: `nodeStatuses()` não o persiste. Piso `--minimum-expected-tests` de projeto MTP (#114): quando `floorOverride` o lê de UM lugar, o link recortado ganha `"-p:TestingPlatformCommandLineArguments=<valor com o piso em 1>"` (o `-p:` troca o valor inteiro — medido, sem recompilar); qualquer outra forma roda inteiro, e também o comando que já declara a propriedade (o `-p:` do recorte vem por último e ganharia) e o link que roda mais de um projeto (o `-p:` é global à invocação e trocaria os argumentos do outro). Forma de comando nova se lê e se MEDE (`test/fixtures/test-output/README.md`) antes de estreitar; o resto roda INTEIRO com motivo. Canários: `test/lib/tasks/test-scope.test.js`, o bloco `runVerify — task-scope recorte` de `test/lib/verify-runner.test.js`, `test/commands/verify-filter.test.js` e a prosa em `test/framework/verify-stack-docs-prose.test.js` |
|
|
132
|
+
| `src/lib/tasks/dotnet-test-introspection.js` | Só LÊ (dividido de `test-scope.js`): o varredor C# que apaga literais (`blankCSharpLiterals`), o tripwire contra o texto CRU + o balanço de chaves (`csharpClasses`), `classNamesOf`, `dotnetTestMode` (lê o `global.json` por `jsoncToJson` de `src/lib/ai/comment-stripper.js`), o piso `--minimum-expected-tests` / `<VSTestTestCaseFilter>` do projeto, `floorOverride` (#114: o valor do `-p:` que rebaixa o piso a 1 e repassa o resto — só com UM `<TestingPlatformCommandLineArguments>` literal no csproj, sem atributo, num `<PropertyGroup>` sem atributo direto sob `<Project>` (`enclosingElements` anda nas tags: `Condition` no GRUPO, `<Target>` e `<Choose>` injetavam um filtro que o MSBuild não usaria), sem `TreatAsLocalProperty`, sem `<Import>`, sem a propriedade em nenhum outro arquivo que liga o projeto, sem piso no testconfig e sem `` $ @ % ; , & " ' ` \ ! `` no valor; `null` roda inteiro; `bindingMsbuildFiles` é a lista única de arquivos MSBuild que `msbuildDeclares` também lê — o `.csproj.user` e `Directory.Build.props/.targets/.rsp` + `Directory.Packages.props` até a raiz do DISCO, porque o MSBuild não para no repositório), `ignoresExitCode` (o MESMO scan de csproj/Directory.Build.*/testconfig: `--ignore-exit-code` ou `TESTINGPLATFORM_EXITCODE_IGNORE` desarma a prova do recorte MTP que resta quando o sumário é ilegível, o exit code — desde a #115 a contagem do sumário, pt-BR/en-US, prova primeiro, e o scan fica porque decide ANTES da rodada), o projeto de teste dono de um `.cs` (`dotnetTestProjectOf` — só `.cs`/`*.csproj`, via `nearestCsprojOf`), `nearestProjectOf` + `isDotnetTestProject` (qualquer arquivo; cobrem `.csproj`/`.fsproj`/`.vbproj` — `test-scope.js` roda o nó INTEIRO por QUALQUER arquivo não-`.cs` que o nó pode rodar cujo projeto mais próximo é de teste: `.feature`, `.verified.txt`, `.razor`, dado de teste), `isNonCSharpSource`/`isDotnetSource`, `readsLikeTestSource` (o marcador FROUXO no texto cru atrás de `unrecognisedTestFile` e do segundo braço do F2 — falso positivo só custa relógio) e `CS_TEST_ATTRIBUTE` (nome inteiro numa seção de atributo que FECHA — `, TestContext` num construtor e uma coleção `[\n Env.Test,\n]` compravam um verde; o resíduo sondado mora no JSDoc de `csharpClasses`). **Falha para o lado SEGURO:** varredura desbalanceada, literal sem fim, buraco de interpolação cortado ou palavra-chave de tipo que não virou declaração → o nó roda INTEIRO; um erro que ESTREITA roda menos testes e sai verde. `test-scope.js` reexporta `dotnetTestMode`/`classNamesOf`, e `test/lib/tasks/dotnet-test-introspection.test.js` pina que são as MESMAS funções. A garantia e o que ela NÃO cobre moram no JSDoc de `csharpClasses`: mexeu no varredor, reescreva-o junto e prove por mutação que o tripwire ainda morde |
|
|
133
133
|
| `src/lib/tasks/linux-runner.js` | `verify-runner.js` (`runNodesInLinux` faz as MESMAS pré-decisões do laço local e julga o resultado pelo MESMO `judgeRun` — uma segunda cópia dessa regra é como container e host passam a discordar da mesma suíte), `framework/hooks/shared/e2e-check.js` (`defaultExec`/`isDockerAvailable` são reusados, nunca recriados), `src/lib/worktree/dirs.js` (nomes do contêiner de worktree nas exclusões do `tar`), `framework/schemas/config.schema.json` (`linuxImage`) e `test/lib/tasks/linux-runner.test.js` (o canário do bind mount: montar a árvore em `/work` é v9fs case-insensitive e não reproduz F35) |
|
|
134
134
|
| `--record-baseline` (`recordTestBaselines` em `src/lib/tasks/test-nodes.js`) | `verify-runner.js` (`baselineTargetProblem` recusa ANTES do build; `recordBaselineFrom` só grava com todos os `runs[]` verdes), `src/commands/validation/verify.js` (não persiste stamp: é manutenção de config, não prova de gate), `framework/schemas/config.schema.json` (`expectedCount`) e `atomicWriteJson` de `state-manager.js` — o número é MEDIDO por comando, nunca digitado (D8). Com `{task}` é erro de uso ANTES do build (`baselineTargetProblem`, #87), e `recordTestBaselines` recusa de novo qualquer medição com `selection` — a segunda barreira mora no gravador |
|
|
135
|
-
| `src/lib/tasks/eval-node.js` | os leitores do MESMO nó `runner: "evals"`: `test-nodes.js` (o enum de `TEST_RUNNERS` e a validação fail-closed do bloco `evals`), `verify-runner.js` (`preDecideRun` faz o pré-voo — os três estados de cache reprovam ANTES de `dotnet test`; `judgeEvalRun` lê o sumário depois), `verify.js` (`evalsStatusOf` deriva `nodes.evals` do stamp) e `framework/schemas/config.schema.json`. O pin tem UM leitor (`readAiPin`), chamado com duas raízes (`framework/` e `.morph/framework/`) — um segundo parser de `ai-pin.json` é como as duas verdades começam. **`null` é DESCONHECIDO, nunca 0**: sumário ilegível não pode fabricar queda contra `expectedCount`, e miss de cache é `fail` nomeado, JAMAIS `skip`. `--linux` com nó de eval é erro de uso: o canal `MORPH_EVAL_*` carrega caminhos absolutos do host e o container roda uma cópia em `/work` |
|
|
135
|
+
| `src/lib/tasks/eval-node.js` | os leitores do MESMO nó `runner: "evals"`: `test-nodes.js` (o enum de `TEST_RUNNERS` e a validação fail-closed do bloco `evals`), `verify-runner.js` (`preDecideRun` faz o pré-voo — os três estados de cache reprovam ANTES de `dotnet test`; `judgeEvalRun` lê o sumário depois), `verify.js` (`evalsStatusOf` deriva `nodes.evals` do stamp) e `framework/schemas/config.schema.json`. O pin tem UM leitor (`readAiPin`), chamado com duas raízes (`framework/` e `.morph/framework/`) — um segundo parser de `ai-pin.json` é como as duas verdades começam. **`null` é DESCONHECIDO, nunca 0**: sumário ilegível não pode fabricar queda contra `expectedCount`, e miss de cache é `fail` nomeado, JAMAIS `skip`. A unidade do nó é o CENÁRIO: só `judgeEvalRun` compara com `expectedCount` — `judgeRun` passa baseline `null` para `runner: "evals"` (#110 parte 2: os `[Fact]` do extrator contra a baseline de cenários reprovavam um 24/24), e sem sumário legível a contagem é `null`, nunca a do extrator — nem compara nem vira baseline. O sumário (`morph-eval-{id}-{pid}.json` no temp) é APAGADO antes e depois da rodada: um arquivo herdado sob o mesmo pid era lido como desta. `--linux` com nó de eval é erro de uso: o canal `MORPH_EVAL_*` carrega caminhos absolutos do host e o container roda uma cópia em `/work` |
|
|
136
136
|
| `nodeStatuses()` em `src/commands/validation/verify.js` | os TRÊS blocos `nodes` de `framework/schemas/feature.schema.json`, todos `additionalProperties: false` — `verification.nodes`, `verifications.feature.nodes` e `taskVerifications.*.nodes`. A função é escritora ÚNICA dos três: um campo novo aqui sem os três no schema faz o histórico violar o próprio contrato. Canário: "os DOIS escritores do mapa de nós concordam" em `test/commands/verify.test.js` |
|
|
137
137
|
| `src/lib/graph/**` | `test/lib/graph/*.test.js`, the committed fixture `test/fixtures/graph/namespace-hierarchy.graph.json` (ADR-003 regression — never hand-edit it), `doctor.js` (`codebase graph` check), `architecture-scanner.js` (centrality section), `setup-infra.js` step 18b |
|
|
138
138
|
| `src/lib/validators/reuse/**` | `validation-runner.js` (case + push universal), `framework/rubrics/*.json` (o id `reuse` mapeado numa categoria, senão o achado não pesa no score), `test/fixtures/graph/prospectpro-reuse.graph.json` (recorte do grafo real — regerar com `build-reuse-fixture.mjs`, nunca editar à mão) |
|
|
@@ -143,7 +143,7 @@ When modifying framework components, verify the full scope of related files.
|
|
|
143
143
|
| `src/lib/worktree/env-allocator.js` (`portLayout`) | os DOIS escritores do bloco — `buildEnvFileContent` (`.morph/worktree.env`) e `compose-env.js` (`buildComposeEnv`, usado por `e2e up`, pelo nó `e2e` do `verify`, por `stack-teardown.js` e por `morph-spec env`) — e os leitores `worktree-context.js`, `handoff-generator.js`, `worktree-env-inject.js`, o template `docker-compose.template.yml` (variáveis que ele publica) e o standard `local-compose-isolation.md` (tabela de offsets). Canário: "worktree.env e compose env concordam byte a byte" em `test/lib/e2e/compose-env.test.js`. Offset novo aqui sem entrar no template é porta que ninguém publica |
|
|
144
144
|
| `src/lib/tasks/test-nodes.js` | os DOIS leitores de `validateTestNodes` — `verify-runner.js` (fail-closed: nó inválido avermelha o gate) e `doctor.js` (`doctor --full`, camada 3 do contrato do spec §2.2: WARN, e **pula sem imprimir nada** sem `config.json` ou sem `project.tests`, senão quebra `test/commands/doctor.test.js:46`). Uma segunda noção de "nó válido" faz o mesmo config passar num lugar e falhar no outro. `morphConfigPath` deriva de `CONFIG_FILE_NAME` (`core/state/state-manager.js`), a mesma constante de `readMorphConfig` — o JSDoc promete "one spelling" e agora é literal. O campo `groups` (#87): `TASK_GROUPS` é o MESMO enum do `group` de `tasks.schema.json` e de `config.schema.json` (canário em `test/lib/tasks/test-nodes.test.js`), `RUNNER_DEFAULT_GROUPS` dá o default por runner e `effectiveGroups` é o que `test-scope.js` lê — `null` é nó sem grupo, nunca palpite. `discoverFrontendPackages` (a união do fallback de `resolveTestNodes` e o WARN do `doctor --full`) varre as pastas de `PACKAGE_LOCATIONS`, de `src/lib/detectors/index.js` — uma segunda lista de pastas é como dois detectores começam a discordar |
|
|
145
145
|
| `src/lib/worktree/docker-audit.js` | `worktree-audit.js` (`auditWorktrees` concatena os achados e é quem PASSA as features conhecidas — um segundo scan aqui discordaria sobre o que é órfão), `doctor.js` (a classificação `info`/`warn` = não-falha em `doctorWorktreesCommand`; e `samePathCase`, a regra de caixa de path que o WARN de frontend do `doctor --full` e o `spelled` de `test-scope.js` reusam — uma segunda cópia dela foi removida), `env-allocator.js` (`allocatedFeatures` é read-only de propósito: `allocateEnvBlock` ESCREVE, e uma auditoria que aloca inventa o estado que reporta), `test/lib/worktree/docker-audit.test.js`. Nunca no `doctor --full` (D10): `test/commands/doctor.test.js:46` exige zero `✗` neste repo |
|
|
146
|
-
| `src/lib/risk-detector.js` | os DOIS lados da mesma decisão de gate: `gate-check.js` (`collectSignals` COLETA do disco; nenhum sinal que descreva um FATO pode entrar em `OVERRIDABLE_SIGNAL_KEYS` — `gate`/`workType`/`e2e*` são o precedente), `src/commands/validation/verify.js` (o stamp `verification` é a ÚNICA coisa que o gate lê: campo novo aqui só vira sinal se o stamp o gravar), `framework/schemas/feature.schema.json` (`verification` é `additionalProperties: false`), `framework/MORPH.md` §5 (tabela de sinais — ela se declara fonte única) e `framework/docs/CLI.md`. Política escrita no coletor cria duas fontes para o mesmo fato — foi assim que `gate-check` e `approve` divergiram uma vez sobre o score do Gate 3. **`verificationScope` é sinal, não auditoria**: a dispensa de e2e (`project`/`scope`) só conta vinda de um stamp de escopo `feature` — um `verify {f} {task}` de task `group: docs` carimba a razão de dispensa e compraria o Gate 3 da feature inteira. **E o escopo de task pausa o ship por si só** (#87): no `review`, `verificationScope` string ≠ `'feature'` pausa ANTES do bloco de e2e, mesmo com todos os nós verdes — o recorte automático faz TODO `verify {f} {task}` rodar menos que a suíte; `null` (stamp legado) não pausa por esta regra. O par mora em `verify.js`: `--filter` de escopo de feature não persiste o stamp (`reason: 'filtered'`) |
|
|
146
|
+
| `src/lib/risk-detector.js` | os DOIS lados da mesma decisão de gate: `gate-check.js` (`collectSignals` COLETA do disco; nenhum sinal que descreva um FATO pode entrar em `OVERRIDABLE_SIGNAL_KEYS` — `gate`/`workType`/`e2e*` são o precedente), `src/commands/validation/verify.js` (o stamp `verification` é a ÚNICA coisa que o gate lê: campo novo aqui só vira sinal se o stamp o gravar), `framework/schemas/feature.schema.json` (`verification` é `additionalProperties: false`), `framework/MORPH.md` §5 (tabela de sinais — ela se declara fonte única) e `framework/docs/CLI.md`. Política escrita no coletor cria duas fontes para o mesmo fato — foi assim que `gate-check` e `approve` divergiram uma vez sobre o score do Gate 3. **`verificationScope` é sinal, não auditoria**: a dispensa de e2e (`project`/`scope`) só conta vinda de um stamp de escopo `feature` — um `verify {f} {task}` de task `group: docs` carimba a razão de dispensa e compraria o Gate 3 da feature inteira. **E o escopo de task pausa o ship por si só** (#87): no `review`, `verificationScope` string ≠ `'feature'` pausa ANTES do bloco de e2e, mesmo com todos os nós verdes — o recorte automático faz TODO `verify {f} {task}` rodar menos que a suíte; `null` (stamp legado) não pausa por esta regra. O par mora em `verify.js`: `--filter` de escopo de feature não persiste o stamp (`reason: 'filtered'`), nem `--skip-tests`/`--skip-build` (`'skip-tests'`/`'skip-build'`, #113 — nó pulado não reprova o rollup e nenhuma regra do gate lê `tests`/`build` pulados; a lista é `PARTIAL_RUN_FLAGS`) |
|
|
147
147
|
| `src/lib/installers/effects.js` | os NOVE pontos de emissão que atravessam o caminho de `update`: `update.js` (clean, payloads, spawn de `generate-refs.js`, `gitIndex`), `skills-installer.js`, `agents-installer.js`, `setup-infra.js` (`installRulesForStack`, `updateClaudeMdAgentsBlock`), `hooks-installer.js` (`installClaudeHooks`, `installGlobalStatusline`), `file-copier.js` (`updateGitignore`, `ensureGitattributes`), `claude-md-injector.js`, `version-checker.js` (`saveProjectMorphVersion`) e `templates-reconcile.js`. **O caminho real também emite** — o resumo de uma execução real sai de `list()`, e é por isso que um ponto de emissão esquecido some das DUAS saídas ao mesmo tempo. Canários: `test/lib/installers/effects.test.js` (igualdade dry × real do MESMO roteiro) e `test/commands/update-dry-run.test.js` — que tem DOIS canários e precisa dos dois: **igualdade** prova que os dois lados percorrem o mesmo caminho, **diferencial** (mudar `--templates` muda a previsão) prova que o caminho lê a flag. Só igualdade aprova uma previsão e uma execução que ignoram a MESMA flag — foi medido nesta feature, por mutação. Um laço de limpeza de órfão novo tem de nascer calado em `fx.dryRun` (D6): o clean o subsome. E a lista de `frameworkDirsToClean` é pinada **à mão** no teste (`DIRETORIOS_APAGADOS_POR_INTEIRO`, dez caminhos, `deepEqual` ordenado): derivar a expectativa chamando a própria função deixou a suíte inteira cega — medido, dava para remover 2 das 10 entradas sem matar nenhum dos 207 arquivos de teste |
|
|
148
148
|
| `src/lib/ai/**` (`doctor --ai`) | `src/commands/project/doctor.js` (`doctorAiCommand` + a guarda de uso indevido), `bin/morph-spec.js` (`--ai`), `framework/docs/CLI.md`. **`--ai` NUNCA entra no `doctor --full` nem no default** — o renderer do `--full` manda tudo que não é `ok`/`warn` para `✗` e `test/commands/doctor.test.js` exige zero `✗` neste repositório. **Nada alcançável por `--ai` pode escrever**: `test/commands/doctor-ai.test.js` varre o grafo de imports E o corpo de `doctorAiCommand` (com os imports dinâmicos fixados nos dois permitidos), reprovando `writeFileSync`/`spawn`/`fs-extra` em CÓDIGO — comentários apagados antes, porque os módulos EXPLICAM por escrito que não fazem isso e medir a menção seria medir uma cópia do objeto. `null` é DESCONHECIDO e vira `skip` **com razão obrigatória**; `[]` é "perguntei e não achei" e vira `ok` |
|
|
149
149
|
| `src/lib/ai/registry-audit.js` | o leitor de PRODUÇÃO do mesmo documento: `framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs` (`CommentHandling = Skip`, `AllowTrailingCommas`, `AllowedAliasProperties` com `pricing`/`pricingWaiver`) e o exemplar `model-registry.v2.json` que o framework manda COPIAR. A auditoria tem de enxergar a **mesma forma** que o leitor de produção enxerga: com `JSON.parse` estrito, duas das cinco checagens saíam `skip` sobre o exemplar canônico — e `skip` não muda exit code, então o silêncio era total. A tolerância a JSONC vive em `src/lib/ai/comment-stripper.js`, ÚNICA (o `code-audit.js` usa o mesmo varredor) — e a paridade é nos DOIS sentidos: `jsoncToJson` **lança** no comentário de bloco não fechado, porque o `JsonCommentHandling.Skip` do .NET lança, e ser mais frouxo que o kit é aprovar documento que a app recusa carregar. Ao mascarar strings, **as aspas delimitadoras ficam**: apagá-las junto faz um elemento-string virar espaço puro e o detector de vírgula sobrando comer um SEPARADOR real (`{"tags":["a","b"]}` saía inválido). Canário obrigatório: array terminado em **string**, com e sem vírgula sobrando — uma suíte só com arrays de número passa dos dois lados dessa inversão. A **REGRA DURA** do exemplar — *todo alias declara `pricing` OU `pricingWaiver`, nunca os dois, nunca nenhum* — é do alias, não de um mapa no topo: ler só o mapa dava 6 falsos positivos sobre um documento correto. O canário **copia o exemplar do disco**, nunca uma paráfrase dele |
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
|
|
4
4
|
|
|
5
5
|
**Package:** `@polymorphism-tech/morph-spec`
|
|
6
|
-
**Version:** 8.
|
|
6
|
+
**Version:** 8.37.1
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -379,4 +379,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
379
379
|
|
|
380
380
|
---
|
|
381
381
|
|
|
382
|
-
*morph-spec v8.
|
|
382
|
+
*morph-spec v8.37.1 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -383,8 +383,8 @@ program
|
|
|
383
383
|
.option('--full', 'No escopo de task, desliga o recorte automático e roda todos os nós inteiros (o group da task não escolhe nós e os outputs não estreitam comandos). No escopo de feature não muda nada: ali nada é recortado')
|
|
384
384
|
.option('--record-baseline', 'Measure every test node and write expectedCount back into .morph/config/config.json (only from an all-green run at FEATURE scope; with a task id it is a usage error; never creates config.json)')
|
|
385
385
|
.option('--refresh-cache', 'Rewrite the response cache of every `runner: "evals"` node against the real provider. NEVER implicit: a cache rewrite is a paid call nobody asked for. Requires the declared envFile; no eval node declared, --skip-tests, or a --filter that excludes them all are usage errors (exit 2), refused BEFORE the cache is touched')
|
|
386
|
-
.option('--skip-tests', 'Skip the test node')
|
|
387
|
-
.option('--skip-build', 'Skip the build node')
|
|
386
|
+
.option('--skip-tests', 'Skip the test node. At feature scope the run is partial, so the verification stamp is NOT written (reason "skip-tests")')
|
|
387
|
+
.option('--skip-build', 'Skip the build node. At feature scope the run is partial, so the verification stamp is NOT written (reason "skip-build")')
|
|
388
388
|
.option('--skip-e2e', 'Skip the e2e node (Docker bring-up + Playwright)')
|
|
389
389
|
.option('--fresh-e2e', 'Recreate the e2e stack from scratch: down -v (named volumes included), drop the freshness stamp, rebuild, re-seed auth')
|
|
390
390
|
.option('--no-persist', 'Do not persist the result to feature.json')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/docs/CLI.md
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
## Validação
|
|
39
39
|
|
|
40
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-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'`): rodada parcial nunca fica no lugar da prova do Gate 3. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar. **`--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.
|
|
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.
|
|
@@ -149,10 +149,13 @@ lista plural de nós existe para matar.
|
|
|
149
149
|
`eval node runs at feature scope only`, o mesmo tratamento de `e2e: true`. O relógio de uma task não
|
|
150
150
|
paga uma suíte de eval, e o resultado que o Gate 3 lê é o de escopo de feature.
|
|
151
151
|
|
|
152
|
-
**Contagem.**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
`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.
|
|
156
159
|
|
|
157
160
|
**Cache fria avisa, não reprova.** Um run que foi ao provider marca `cache: "cold"` e emite warning de
|
|
158
161
|
que a árvore mudou e precisa ser commitada. Gastar dinheiro sem pedir é ruim; mentir sobre o resultado
|
|
@@ -214,7 +217,9 @@ se qualquer run reprovar, e um nó que estoura o relógio não interrompe os seg
|
|
|
214
217
|
**qual** falhou manda o leitor para a metade errada do repositório.
|
|
215
218
|
|
|
216
219
|
**Contagem e a regra assimétrica.** O extrator lê o sumário do runner (somando sumários encadeados no
|
|
217
|
-
`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
|
|
218
223
|
reprova** (mesmo com exit 0 — é regressão silenciosa), **alta apenas avisa** e pede regravação
|
|
219
224
|
(reprovar a alta ensinaria o agente a apagar testes), e contagem **desconhecida** é `null`, jamais `0`
|
|
220
225
|
— um zero fabricaria queda a partir de "não deu para ler o output". O aviso de alta sai **mesmo com o
|
|
@@ -254,7 +259,7 @@ passa com aviso de alta, e o comando grava.
|
|
|
254
259
|
| --- | --- | --- |
|
|
255
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 |
|
|
256
261
|
| Gate 3 | `verify {f} --json` (sem filtro) | A prova que o avaliador lê tem de cobrir **todos** os nós |
|
|
257
|
-
| CI | a suíte inteira, todos os nós | idem
|
|
262
|
+
| CI | a suíte inteira, todos os nós | idem |
|
|
258
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 |
|
|
259
264
|
|
|
260
265
|
> **Não existe constante de 600 s neste CLI** (`grep -rn "600000" src/ framework/` → zero). O teto que
|
|
@@ -291,16 +296,26 @@ Sem task, o `verify` roda tudo e **nunca recorta** — é a prova do Gate 3. Com
|
|
|
291
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 |
|
|
292
297
|
| `jest` | um comando só, com `jest` | `--runTestsByPath "<caminho>" …` | contagem > 0 |
|
|
293
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 |
|
|
294
|
-
| `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>"` | exit 0 (o MTP sai 8 com zero testes) |
|
|
295
|
-
| `dotnet`, MTP + MSTest/NUnit | idem | `--filter "FullyQualifiedName~<Tipo>\|…"` |
|
|
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 |
|
|
296
301
|
|
|
297
302
|
`node-test`, `playwright` e `none` não têm recorte dentro do nó: rodam inteiros (nós `e2e: true` e
|
|
298
|
-
`evals` seguem pulados no escopo de task, como antes). **Piso de projeto no MTP:**
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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`
|
|
304
319
|
acrescentado o substituiria em silêncio).
|
|
305
320
|
|
|
306
321
|
**O seletor .NET é o TIPO, sem namespace.** Cada `class`/`record`/`struct`/`interface` declarado nos
|
|
@@ -337,8 +352,9 @@ seu motivo:
|
|
|
337
352
|
`TESTINGPLATFORM_EXITCODE_IGNORE` citado em qualquer ponto do `.csproj` ou de um
|
|
338
353
|
`Directory.Build.props/.targets` até a raiz (em `<TestingPlatformCommandLineArguments>` ou numa
|
|
339
354
|
propriedade que ela referencia, como `$(MtpExtraArgs)`), ou num `*testconfig.json` ao lado — e
|
|
340
|
-
`TESTINGPLATFORM_EXITCODE_IGNORE` não vazia no ambiente do verify:
|
|
341
|
-
recorte rodou teste, e com o 8 ignorado zero testes saem 0
|
|
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);
|
|
342
358
|
- comando que já carrega `--filter`/`--treenode-filter`, argumentos depois de `--`,
|
|
343
359
|
`--ignore-exit-code` ou `--minimum-expected-tests` (inclusive dentro de `-p:`); `global.json`
|
|
344
360
|
ilegível; MTP com framework sem filtro de classe conhecido; alvo MTP de solução ou pasta, alvo VSTest
|
|
@@ -359,10 +375,14 @@ string (`// the class under test`), restrição genérica seguida de nome (`wher
|
|
|
359
375
|
3.082 não é queda, e um superconjunto não é alta. Nó rodado **inteiro** no escopo de task continua
|
|
360
376
|
com a regra de contagem de sempre.
|
|
361
377
|
- Verde exige prova de que rodou teste: contagem > 0 **do nó** (numa cadeia VSTest, a soma dos links —
|
|
362
|
-
limite (h))
|
|
363
|
-
que o
|
|
364
|
-
`narrowed-zero-tests: o recorte executou 0 testes
|
|
365
|
-
|
|
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.
|
|
366
386
|
- Vermelho com a assinatura de zero testes do runner (dotnet: exit 8; VSTest: `No test matches the
|
|
367
387
|
given testcase filter` / `Nenhum teste corresponde ao filtro de testcase`; vitest: `No test files
|
|
368
388
|
found`; jest: `No tests found`) e sem contagem > 0 → `narrowed-zero-tests (<runner>, exit <n>)`.
|
|
@@ -419,15 +439,18 @@ feature, `--full` é no-op.
|
|
|
419
439
|
- (e) arquivo de teste JS/TS com nome fora de `*.test.*` / `*.spec.*` / `__tests__/` e sem `describe(`
|
|
420
440
|
/ `it(` / `test(` no início de uma linha não é reconhecido: o nó estreita para os outros arquivos da
|
|
421
441
|
task;
|
|
422
|
-
- (f) no MTP
|
|
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
|
|
423
444
|
`Directory.Build.props/.targets` até a raiz, o `*testconfig.json` ao lado e o ambiente do verify):
|
|
424
445
|
`--ignore-exit-code` ou `TESTINGPLATFORM_EXITCODE_IGNORE` que chegue por um `<Import>` (não é
|
|
425
446
|
seguido), por um `Directory.Build.rsp`, por um `.runsettings` ou pela imagem do contêiner do `--linux`
|
|
426
|
-
não é visto — o nó estreita, e zero testes podem sair 0: verde
|
|
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;
|
|
427
449
|
- (g) dois `.csproj` na mesma pasta: o dono é o primeiro em ordem alfabética;
|
|
428
450
|
- (h) numa **cadeia** VSTest (vários links `&&`), a contagem é a **soma** dos links: um link cujo filtro
|
|
429
451
|
não casou nada (o VSTest sai 0) fica verde ao lado de um link que contou. "Contagem > 0" é prova por
|
|
430
|
-
nó, não por link. No MTP isso não acontece: o link vazio sai 8 e derruba a cadeia
|
|
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).
|
|
431
454
|
|
|
432
455
|
A saída de todos é a mesma: `--full`.
|
|
433
456
|
|
|
@@ -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.1"
|
|
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.1"
|
|
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.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber um novo request de feature, antes de morph-plan
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-brainstorming
|
|
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
|
|
6
6
|
user-invokable: false
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-eval
|
|
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber "/morph-hotfix {feature}", quando a morph-proposal classifica o pedido como workType hotfix (Passo 0), ou o usuário diz "produção caiu / hotfix / urgente"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|
|
@@ -77,7 +77,7 @@ Rode a cadeia determinística focada na(s) task(s) do fix, não uma suíte ad-ho
|
|
|
77
77
|
morph-spec verify {feature} {task} --json
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
Use `--skip-tests`/`--skip-build` quando fizer sentido para velocidade (ex.: iterar rápido no build antes de rodar a suíte completa), mas o `T2` de verify/regressão do passo 4 precisa fechar verde antes do Gate 3. Capture a saída real dos testes no `recap.md` — não descreva "testes passaram" sem colar a evidência.
|
|
80
|
+
Use `--skip-tests`/`--skip-build` quando fizer sentido para velocidade (ex.: iterar rápido no build antes de rodar a suíte completa), mas o `T2` de verify/regressão do passo 4 precisa fechar verde antes do Gate 3. No escopo de **feature**, `--skip-tests`/`--skip-build` não gravam o stamp `verification` (`reason: 'skip-tests'`/`'skip-build'`): o Gate 3 só lê uma execução completa. Capture a saída real dos testes no `recap.md` — não descreva "testes passaram" sem colar a evidência.
|
|
81
81
|
|
|
82
82
|
### 7. Gate 3 — SEMPRE humano
|
|
83
83
|
|
|
@@ -4,7 +4,7 @@ description: Guia a execução de cada tarefa de implementação com standard e
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar cada task de implementação listada em tasks.json
|
|
6
6
|
user-invokable: false
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-implement
|
|
@@ -4,7 +4,7 @@ description: Conduz a AUTORIA de mídia gerada por IA — escolher o modelo pelo
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: quando a tarefa é PRODUZIR imagem ou vídeo por IA (catálogo, marketing, asset de LP) e alguém precisa decidir modelo, prompt e aprovação
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
stacks:
|
|
9
9
|
- dotnet
|
|
10
10
|
- nextjs
|
|
@@ -4,7 +4,7 @@ description: Guia a produção dos artefatos de planejamento de uma feature (use
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-proposal
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-review
|
|
@@ -59,7 +59,7 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
|
|
|
59
59
|
que este passo trata — prossiga com o dispatch abaixo.
|
|
60
60
|
|
|
61
61
|
**Despache o avaliador independente** — o score do Gate 3 nunca é auto-atribuído por quem implementou:
|
|
62
|
-
- Rode `morph-spec verify {feature} --json` (escopo feature) e capture o JSON — é a saída real de build+testes+validadores+e2e que você anexa ao prompt do avaliador independente (`morph-evaluator`). **Sem `--filter`, sempre:** o `--filter` é do loop por task, onde o relógio de uma task não paga a suíte inteira; a prova que o avaliador lê tem de cobrir **todos** os nós de `nodes.tests.runs[]` (uma suíte filtrada aparece como `skip` nomeando o filtro — Gate 3 aberto sobre isso é verde sobre código não testado). Se a suíte inteira não couber numa chamada, rode-a em background; não a encolha com `--filter`. `verify {feature}` (sem task) **nunca recorta** — o recorte automático é só do escopo de task —, e um `verify {feature} --filter …` nem persiste o stamp (`persisted: false`, `reason: 'filtered'`). Rode-o **depois** da última task: o `gate-check {feature} review` pausa quando o último stamp `verification` é de escopo `task:{id}`, porque aquela rodada cobriu só a task.
|
|
62
|
+
- Rode `morph-spec verify {feature} --json` (escopo feature) e capture o JSON — é a saída real de build+testes+validadores+e2e que você anexa ao prompt do avaliador independente (`morph-evaluator`). **Sem `--filter`, sempre:** o `--filter` é do loop por task, onde o relógio de uma task não paga a suíte inteira; a prova que o avaliador lê tem de cobrir **todos** os nós de `nodes.tests.runs[]` (uma suíte filtrada aparece como `skip` nomeando o filtro — Gate 3 aberto sobre isso é verde sobre código não testado). Se a suíte inteira não couber numa chamada, rode-a em background; não a encolha com `--filter`. `verify {feature}` (sem task) **nunca recorta** — o recorte automático é só do escopo de task —, e um `verify {feature} --filter …` nem persiste o stamp (`persisted: false`, `reason: 'filtered'`) — o mesmo vale para `--skip-tests`/`--skip-build` no escopo de feature. Rode-o **depois** da última task: o `gate-check {feature} review` pausa quando o último stamp `verification` é de escopo `task:{id}`, porque aquela rodada cobriu só a task.
|
|
63
63
|
- **Confira o nó `e2e` do verify antes de despachar.** Ele carrega o estado do stack:
|
|
64
64
|
- `pass` → a aplicação subiu e está saudável; o avaliador pode navegá-la. **Passe a URL no prompt**
|
|
65
65
|
— cada feature roda no seu bloco de portas, não em `localhost:3000`; pegue de
|
|
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-stack-scan
|
|
@@ -4,7 +4,7 @@ description: Localiza um standard específico via busca server-side (morph-spec
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: quando precisar consultar um standard específico durante a implementação
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX — design system, mockups, compone
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao tomar o ramo uiux (morph-spec advance --design), depois do Gate 1 e antes do morph-plan
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.37.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-uiux
|
|
@@ -188,7 +188,10 @@ The agent decides autonomously when to call `VectorSearchTool.SearchAsync`. It r
|
|
|
188
188
|
|
|
189
189
|
## Ingestão delta por lookup-key
|
|
190
190
|
|
|
191
|
-
**O defeito que esta seção existe para matar
|
|
191
|
+
**O defeito que esta seção existe para matar é o apaga-e-regrava.** `KbStore.IngestAsync` do GHLBrain faz `DELETE … WHERE location_id AND source` e re-insere/re-embeda **tudo** do `source` a cada ingestão, mudou ou não (`docs/specs/elevacao-9/brutos/scan-ghlbrain.md:412`): 288 fichas re-embedadas, uma chamada de embedding por item, a cada reindex em massa do catálogo — e, com id posicional, inserir um trecho no topo de um documento torna "novos" todos os de baixo.
|
|
192
|
+
|
|
193
|
+
> [!warning] Errata (2026-09-15, medida no código e no Neon de produção — Morph.GHLBrain#96)
|
|
194
|
+
> A versão anterior deste parágrafo afirmava também uma **janela** entre o apagar e o gravar em que a busca não acha nada. **No GHLBrain ela não existe:** `ConvVectorSql.ExecuteAsync` roda o `DELETE` e os `INSERT`s numa transação só, e o embedding acontece **antes** de a transação abrir — em READ COMMITTED o leitor vê o conteúdo antigo até o commit. O custo medido também é pequeno (324 trechos, ~US$ 0,0005 por re-embed completo, 1 trecho por `source`): lá, o ganho real do delta é **latência** (o reindex em massa) e **prevenção** (documento de até ~70 trechos). A janela é real em quem apaga fora da transação que grava — é o caso que a regra 4 abaixo cobre.
|
|
192
195
|
|
|
193
196
|
### O contrato — uma função pura, sem I/O
|
|
194
197
|
|
|
@@ -220,7 +223,8 @@ public sealed record DeltaPlan<T>(
|
|
|
220
223
|
1. **`Unchanged` não é re-embedado.** É a economia inteira. Num republish típico a lista grande é `Unchanged` e as outras três são curtas — e é exatamente isso que a ingestão anterior escondia.
|
|
221
224
|
2. **`Removed` são as chaves presentes no acervo e ausentes do `incoming`.** Sem esse balde, conteúdo apagado na origem continua sendo devolvido pela busca.
|
|
222
225
|
3. **O id continua determinístico** — `DeterministicUuid.V5(namespace, "{loc}:{source}:{idx}")` já é a prática — para que `Updated` seja **upsert**, não delete+insert. Delete+insert quebra referências e troca o id de um chunk que só mudou de texto.
|
|
223
|
-
|
|
226
|
+
**Ressalva medida (Morph.GHLBrain#96, ADR-2 da `delta-ingestion`):** quando o item é um *trecho* de documento, a posição como lookup-key derrota a regra 1 — inserir um trecho no topo desloca os índices e marca todo trecho abaixo como `Updated`, re-embedando tudo. Ali a chave certa é o **hash do conteúdo** (`lookupKey` e `contentHash` recebem o mesmo delegado): `Updated` fica vazio por construção, editar um trecho vira `Added` + `Removed`, e trechos idênticos são deduplicados antes do `Plan` para a chave duplicada nunca disparar. A chave posicional continua certa quando o item tem identidade própria (uma ficha por produto, um documento inteiro).
|
|
227
|
+
4. **Sem janela vazia: nunca apague antes de gravar fora da transação que grava.** Apagar e gravar em transações separadas (ou sem transação) é o que faz a busca devolver vazio durante a reingestão. Aplique `Added`/`Updated` primeiro e só então `Removed`, dentro da mesma transação — com a transação a janela já não existe, e a ordem mantém o código correto se um dia ela for partida.
|
|
224
228
|
|
|
225
229
|
### Chave duplicada no `incoming` é erro de entrada
|
|
226
230
|
|
|
@@ -308,4 +312,4 @@ ALTER TABLE kb_embeddings ENABLE ROW LEVEL SECURITY;
|
|
|
308
312
|
|
|
309
313
|
---
|
|
310
314
|
|
|
311
|
-
*MORPH-SPEC by Polymorphism Tech — ai-agents/rag-custom-pgvector.md v2.
|
|
315
|
+
*MORPH-SPEC by Polymorphism Tech — ai-agents/rag-custom-pgvector.md v2.1 (2026-09-15 — errata da janela no GHLBrain; ressalva da chave por hash)*
|
|
@@ -41,11 +41,11 @@ dotnet test --solution Morph.AiKit.sln -c Release --no-build
|
|
|
41
41
|
> (`Resumo da execução de teste: Aprovado!` / `Test run summary: Passed!`) e a
|
|
42
42
|
> contagem vem em linha própria, indentada, com **`total:` minúsculo** — nada
|
|
43
43
|
> disso é o sumário do VSTest. Medido nos dois locales em 2026‑09‑08.
|
|
44
|
-
> Consequência: **um parser escrito para o sumário do VSTest não conta os testes
|
|
44
|
+
> Consequência: **um parser escrito só para o sumário do VSTest não conta os testes
|
|
45
45
|
> deste projeto** — um parser que procure uma linha *começando* em
|
|
46
46
|
> `Aprovado!`/`Passed!` **e** contendo `Total de testes:`/`Total:` falha nos dois
|
|
47
|
-
> eixos, e nos dois locales. O
|
|
48
|
-
>
|
|
47
|
+
> eixos, e nos dois locales. O `morph-spec verify` lê o bloco MTP nos dois locales
|
|
48
|
+
> desde a issue #115; antes, a contagem deste projeto voltava desconhecida.
|
|
49
49
|
|
|
50
50
|
> **Descoberta parcial não pode passar verde.** O `.csproj` de teste declara
|
|
51
51
|
> `TestingPlatformCommandLineArguments` com o piso `--minimum-expected-tests N`
|
|
@@ -58,10 +58,15 @@ dotnet test --solution Morph.AiKit.sln -c Release --no-build
|
|
|
58
58
|
> do `morph-spec verify`); por isso nenhum chamador repassa a flag. Ao adicionar
|
|
59
59
|
> testes, suba o piso — `test/framework/ai-kit-ci-packaging.test.js` reprova um
|
|
60
60
|
> piso que ficou para trás da contagem de `[Fact]`/`[Theory]`.
|
|
61
|
-
> **Consequência para o recorte por task:**
|
|
62
|
-
>
|
|
63
|
-
>
|
|
64
|
-
>
|
|
61
|
+
> **Consequência para o recorte por task:** `--minimum-expected-tests 1` na
|
|
62
|
+
> linha de comando não sobrescreve o valor do projeto (medido: o recorte verde
|
|
63
|
+
> sai 9 do mesmo jeito), mas a propriedade global do MSBuild sim. Por isso
|
|
64
|
+
> `morph-spec verify {f} {task}` estreita este projeto repassando
|
|
65
|
+
> `"-p:TestingPlatformCommandLineArguments=…"` com o valor do `.csproj` e o piso
|
|
66
|
+
> em 1 — verde sai 0, zero testes segue saindo 8, nada recompila (issue #114).
|
|
67
|
+
> Mantenha o piso **nesse** elemento, literal (sem `Condition`, sem `$(…)`, sem
|
|
68
|
+
> outra declaração da propriedade em `Directory.Build.*`): em qualquer outra
|
|
69
|
+
> forma o recorte volta a rodar o nó inteiro.
|
|
65
70
|
|
|
66
71
|
> **`dotnet test` precisa rodar COM O CWD DENTRO deste diretório.** O opt-in do
|
|
67
72
|
> Microsoft.Testing.Platform (que `xunit.v3` 4.x usa) vive em `global.json`, e o
|
|
@@ -5,12 +5,18 @@ namespace Morph.AiKit.Rag;
|
|
|
5
5
|
/// </summary>
|
|
6
6
|
/// <remarks>
|
|
7
7
|
/// <para>
|
|
8
|
-
/// <b>O defeito que este tipo existe para matar</b> e
|
|
9
|
-
/// conhecimento do GHLBrain <b>apaga
|
|
10
|
-
/// pago em embedding de
|
|
11
|
-
///
|
|
12
|
-
/// ingestao inteira escondia: quase sempre <see cref="Unchanged"/> e a lista
|
|
13
|
-
/// outras tres sao curtas.
|
|
8
|
+
/// <b>O defeito que este tipo existe para matar</b> e o apaga-e-regrava: a base de
|
|
9
|
+
/// conhecimento do GHLBrain <b>apaga e re-embeda o documento inteiro</b> a cada ingestao,
|
|
10
|
+
/// mudou ou nao. O custo e pago em embedding de trecho que nao mudou e em latencia (uma
|
|
11
|
+
/// chamada de embedding por item no reindex em massa). Um plano em quatro baldes torna
|
|
12
|
+
/// visivel o que a ingestao inteira escondia: quase sempre <see cref="Unchanged"/> e a lista
|
|
13
|
+
/// grande e as outras tres sao curtas.
|
|
14
|
+
/// </para>
|
|
15
|
+
/// <para>
|
|
16
|
+
/// Errata (2026-09-15, Morph.GHLBrain#96): a versao anterior deste comentario citava tambem
|
|
17
|
+
/// uma janela entre o apagar e o gravar em que a busca nao acha nada. No GHLBrain o apagar e
|
|
18
|
+
/// o gravar ja rodam numa transacao so, com o embedding antes dela, entao essa janela nao
|
|
19
|
+
/// existe la. Ela existe em quem apaga fora da transacao que grava.
|
|
14
20
|
/// </para>
|
|
15
21
|
/// <para>
|
|
16
22
|
/// <b>E um plano, nao uma execucao.</b> Nada aqui toca banco, vetor ou arquivo: quem aplica
|