@polymorphism-tech/morph-spec 8.41.0 → 8.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CLAUDE.md +5 -3
  2. package/README.md +3 -3
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +1 -1
  7. package/framework/agents.json +6 -6
  8. package/framework/hooks/README.md +1 -0
  9. package/framework/hooks/claude-code/core/chain.js +2 -0
  10. package/framework/hooks/claude-code/core/comment-guard.js +107 -0
  11. package/framework/hooks/shared/comment-rules.js +171 -0
  12. package/{src/lib/validators/code-budget → framework/hooks/shared}/comment-scanner.js +2 -2
  13. package/{src/lib/ai → framework/hooks/shared}/comment-stripper.js +2 -1
  14. package/framework/hooks/shared/source-files.js +34 -0
  15. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  16. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  17. package/framework/skills/core/morph-apply/SKILL.md +4 -2
  18. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  19. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  20. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  21. package/framework/skills/core/morph-implement/SKILL.md +4 -2
  22. package/framework/skills/core/morph-media/SKILL.md +1 -1
  23. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  24. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  25. package/framework/skills/core/morph-review/SKILL.md +1 -1
  26. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  27. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  28. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  29. package/framework/standards/STANDARDS.json +1 -1
  30. package/framework/standards/architecture/code-comments.md +9 -2
  31. package/package.json +1 -1
  32. package/src/lib/ai/code-audit.js +1 -1
  33. package/src/lib/ai/registry-audit.js +1 -1
  34. package/src/lib/tasks/dotnet-test-introspection.js +1 -1
  35. package/src/lib/validators/code-budget/code-budget-validator.js +4 -6
  36. package/src/lib/validators/code-budget/index.js +1 -1
  37. package/src/lib/validators/reuse/symbol-scanner.js +1 -28
  38. package/src/utils/hooks-installer.js +3 -2
package/CLAUDE.md CHANGED
@@ -106,6 +106,7 @@ State lives in `.morph/state.json`. Phase derived from filesystem (not stored).
106
106
  - **Output type normalization** — `normalizeOutputType()` auto-converts kebab-case to camelCase
107
107
  - **Template helpers** — Handlebars v2.0: pascalCase, camelCase, snakeCase, kebabCase, pluralize, plus comparison/logic helpers
108
108
  - **CLI structure** — Commands are thin wrappers; business logic in `src/core/` or `src/lib/`
109
+ - **Comentários** — o padrão é nenhum: nome, tipo e teste explicam o código. Só o porquê que o código não mostra (invariante, ordem que importa, quirk externo com link), em até 3 linhas. Sem história de processo (issue, PR, feature, "medido na 8.x") no código: isso vai no commit e na memória. JSDoc só em export que outro módulo consome e cujo contrato o nome não diz. **Não imite a densidade dos arquivos existentes**: boa parte do código legado deste repo é mais comentada do que esta regra. O hook `comment-guard` roda aqui também (`.claude/settings.json`)
109
110
 
110
111
  ## Framework Development Guardrails
111
112
 
@@ -147,7 +148,7 @@ When modifying framework components, verify the full scope of related files.
147
148
  | `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 |
148
149
  | `src/lib/specs/screen-checklist.js` (checklist de tela, #129) | o sinal mora aqui (`collectScreenChecklist`) e tem QUATRO leitores: `gate-check.js` na `uiux` (`problems` → pausa) e no `review` (`uncovered`: item decidido sem linha na seção `## Checklist de tela` que `evaluator-report.js` lê — última ocorrência; tela toda "não se aplica" não deve nada), e `approve.js`/`advance.js` com assinatura `auto` (`uiuxAutoApprovalBlock`). Política só em `risk-detector.js`: `uiuxChecklistPause` é o predicado ÚNICO do gate-check pausar e do auto recusar — uma segunda regra no advance fez approve e advance discordarem da mesma feature — e `gateDecisions.uiux.pause === false` destrava os dois só para o mockup que a decisão viu: `persistGateDecision` (`gate-decision.js`, o escritor único de `gateDecisions`) grava `mockupsHash` e `uiuxAutoApprovalBlock` o compara com o arquivo atual — os dois leem `mockupsHash` daqui, e um segundo cálculo do hash (outra normalização de BOM/CRLF) faz toda decisão parecer de outro mockup. `--force` não destrava em nenhum dos dois. Mockup legado (`## S\d+:` e nenhuma `## Tela:`, o template anterior) é isento nas duas fases com `exempt` = `LEGACY_MOCKUP_EXEMPTION` à vista no `getRiskSignals`; qualquer outro heading é "nenhuma tela declarada", senão trocar o heading pularia o checklist (fixture: `test/fixtures/mockups/legacy-ui-mockups.md`, o template de main). Os rótulos dos itens casam com DOIS textos que o parser não vê, os dois com canário em `test/lib/specs/screen-checklist.test.js`: o template do prompt da `morph-uiux` §3 e `framework/templates/docs/ui-mockups.md`; mais o formato de linha que o prompt do avaliador (`morph-review` §4) e a rubrica `contracts.md` ("Contrato de tela") prescrevem. A `Superfície` é validada contra as chaves de `surfaces` da `ui-designer` (`knownSurfaces`) — é a chave que o `morph-plan` §5a usa para somar standards |
149
150
  | `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` |
150
- | `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 |
151
+ | `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 `framework/hooks/shared/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 |
151
152
  | `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) |
152
153
  | `--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 |
153
154
  | `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` |
@@ -157,7 +158,8 @@ When modifying framework components, verify the full scope of related files.
157
158
  | `framework/hooks/shared/graph-refresh.js` (o refresh em background) | os dois disparos (`graph-refresh-kick`, depois de Bash/PowerShell com `git`/`gh`/`morph-spec`, e o SessionStart de `worktree-context.js`), `graph build\|refresh` (o MESMO lock por repositório) e `worktree.js` (`seedGraphFromRoot`: a worktree nasce com o grafo da raiz e o registro dela — em dia no HEAD só se a raiz estava). **Só a raiz**: o worker destacado nasce com `cwd` no git common dir e nunca recebe o caminho de uma worktree — no win32 um diretório em uso não se apaga, e o `finish` de uma worktree com refresh correndo falhava no meio. A worktree anda por `morph-spec graph refresh`, em primeiro plano. `runGraphify` roda o graphify com `cwd` no git common dir, nunca na árvore (ele lê o alvo, o ignore e o git pelo argumento — medido no 0.9.50). O disparo lê o HEAD pelo disco (`readPrimaryHead`, `git-paths.js`) e não solta processo quando o grafo não pede refresh: ele roda a cada comando git do agente. O step 18b de `setup-infra.js` não instala o hook git do graphify: ele não roda em worktree, não tem `post-merge` e carimba como em dia um grafo sem os arquivos do merge |
158
159
  | `framework/hooks/shared/git-exclude.js` | ÚNICO escritor dos blocos do morph-spec no `info/exclude` do COMMON dir — o `.git/worktrees/<id>/info/exclude` o git não lê para worktree vinculada. Leitores que passam o seu marcador: `worktree.js`, `handoff-generator.js` e `graph-refresh.js`. Resolve o common dir pelo disco, sem subprocesso: um `git` gerado por um processo destacado abria janela de console no win32 |
159
160
  | `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) |
160
- | `src/lib/validators/code-budget/**` | `validation-runner.js` (case + push universal fora do `light`), `framework/rubrics/*.json` (o id `code-budget` em `code_quality` nas TRÊS, senão o aviso não pesa), `framework/docs/CLI.md` (o que conta como teste e os limites 1,5×/40 linhas) e `test/integration/code-budget-verify.test.js`. Lê `addedLinesAgainst` (`default-branch.js`), `isTestFile`/`isProjectSource` (`symbol-scanner.js`, a mesma régua de teste do `reuse`) e `stripComments` com `js` (`comment-stripper.js`). Sempre `warning`. Não conseguir medir é skip `unavailable` com razão, e o `validation-runner` não põe validador pulado em `results`: pulado não conta como verde |
161
+ | `src/lib/validators/code-budget/**` | `validation-runner.js` (case + push universal fora do `light`), `framework/rubrics/*.json` (o id `code-budget` em `code_quality` nas TRÊS, senão o aviso não pesa), `framework/docs/CLI.md` (o que conta como teste e os limites 1,5×/40 linhas) e `test/integration/code-budget-verify.test.js`. Lê `addedLinesAgainst` (`default-branch.js`), `isTestFile` (`symbol-scanner.js`, a mesma régua de teste do `reuse`), `isProjectSource`/`isGenerated` (`hooks/shared/source-files.js`) e `commentLines`/`citesProcess` (`hooks/shared/comment-scanner.js`, sobre `stripComments` com `js`). Sempre `warning`. Não conseguir medir é skip `unavailable` com razão, e o `validation-runner` não põe validador pulado em `results`: pulado não conta como verde |
162
+ | `framework/hooks/shared/comment-rules.js` / `comment-scanner.js` / `source-files.js` (o hook `comment-guard`) | Os dois leitores do MESMO comentário: o hook `comment-guard` (PostToolUse `Write\|Edit`, bloqueia na hora da edição) e o `code-budget` (avisa no `verify`). Moram em `hooks/shared` porque hook não importa `src/`; o `src/` importa daqui, nunca uma cópia. O hook julga só o comentário ADICIONADO (`tool_response.originalFile`, medido: `null` no Write que cria) e só FORMA — processo, banner/`#region`, bloco > 3 linhas, doc-comment fora de pacote publicado —, nunca "repete o código". É o ÚLTIMO elo da chain `Write\|Edit` (um block encerra a chain; canário em `test/hooks/hooks-installer.test.js`). Marcador que template precisa manter (`READS STANDARD(S):`, `VERIFY:`) entra em `ALLOWED`; o canário de template em `test/hooks/comment-rules.test.js` reprova template que dispararia o hook no scaffold |
161
163
  | o ciclo de reuso (`reuse-map.md`) | `output-schema.js` (+ `npm run generate`), `framework/schemas/tasks.schema.json` + `tasks-json-parser.js` (campo `reuse`), `morph-plan` §1c/§5b, template de dispatch de `morph-implement`/`morph-apply`, personas de `agents.json`, `evals/rubrics/code-quality.md` + `condensed.md`, `morph-review` (insumo do avaliador), `test/framework/graph-fallback.test.js` |
162
164
  | `framework/mcp/registry.json` | mcp-installer.js, claude-config-detector.js, agents-installer.js (personas' `mcp` field), `scripts/validate-framework.js` Check 8, `test/framework/mcp-registry.test.js` |
163
165
  | Any `framework/` file that calls `morph-spec graph` | The fallback to grep must be declared in the SAME file — `test/framework/graph-fallback.test.js` enforces it (ADR-002: a skill that requires the graph with no fallback is a bug) |
@@ -168,7 +170,7 @@ When modifying framework components, verify the full scope of related files.
168
170
  | `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`) |
169
171
  | `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 |
170
172
  | `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` |
171
- | `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 o `code-budget` também, o único com a opção `js` — template e regex literal de TS/JS; sem ela o varredor é o de antes) — 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 |
173
+ | `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 `framework/hooks/shared/comment-stripper.js`, ÚNICA (o `code-audit.js` usa o mesmo varredor, e o `code-budget` também, o único com a opção `js` — template e regex literal de TS/JS; sem ela o varredor é o de antes) — 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 |
172
174
  | `src/lib/worktree/dirs.js` | ÚNICA fonte do nome do container (`worktrees/`, sem ponto — Jest esconde diretório com ponto e reporta 0 testes sem erro). `worktree.js` re-exporta; `graphifyignore.js`, `file-copier.js`, `architecture-scanner.js`, `test-runner.js`, `worktree-audit.js`, os scanners do dashboard (`features-scanner.js`, `project-api.js`), `tasks/linux-runner.js` (exclusões do `tar` do `--linux`) importam. `test/lib/worktree/dirs.test.js` cobra cada leitor |
173
175
  | `src/dashboard/**` (páginas e scanners) | `test/dashboard/pages-smoke.test.js` renderiza as 7 rotas e os painéis de detalhe pela config Vite do dashboard com os dados REAIS do scanner — build verde não prova nada (8.38.0 saiu com `ReferenceError` na rota inicial). Campo que a página lê tem de sair do scanner: `activePhases` é DERIVADO das linhas de dispatch das skills de fase (`personaPhases`, com `isImplementer`), nunca uma lista à mão. Edge do React Flow só existe no cliente (chave duplicada não aparece no SSR — o dedupe de `standardRefs` mora no scanner). `src/dashboard/.gitignore` é o que mantém `node_modules` fora do tarball (`test/dashboard/packaging.test.js`). `scanner/feature-status.js` (`isFinished`) é a regra ÚNICA de "feature encerrada" do servidor (portfólio, `awaiting`, `taskIndex`) e do cliente (lista e quadro) — arquivada conta como encerrada seja qual for o `status` legado. O plugin engole o `full-reload` do Vite para `.html` sob `.morph/` (`isMorphDataReload`): `handleHotUpdate` devolvendo `[]` NÃO segura `.html` no Vite 6, e um agente gravando um mockup recarregava o dashboard. `resolveFeatureTree` (git, ~0,3 s por chamada) é lembrado por feature no `project-api.js` e só é esquecido por evento estrutural (`add`/`unlink`/`addDir`/`unlinkDir`) ou TTL de 60 s — um scan síncrono trava o event loop do dev server inteiro |
174
176
  | `src/lib/projects/registry.js` (projetos da máquina, `~/.morph-spec/projects.json`) | os TRÊS escritores automáticos — `init`, `update` (nunca sob `--dry-run`) e `dashboard` — mais o add/remove do portfólio (`src/dashboard/scanner/portfolio.js`). Registrar é fail-open: registro ilegível ou não gravável nunca derruba o comando. Nunca registra pasta sob o temp do sistema (as suítes rodam `init`/`update` ali às centenas, inclusive por CLI em spawn que herda o home real) nem checkout de worktree (`WORKTREE_DIR_NAMES`). Teste que registra usa `MORPH_PROJECTS_REGISTRY` apontando para um temp, ou o seam `tempRoot` — nenhuma suíte escreve o `~/.morph-spec` de quem roda. Leitura poda projeto cuja pasta sumiu; descoberta é UM nível abaixo das pastas-mãe e só sugere, nunca adiciona |
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.41.0
6
+ **Version:** 8.42.0
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -245,7 +245,7 @@ Guardrail hooks are installed into `.claude/settings.local.json` across 7 event
245
245
  | `SessionStart` | `worktree-context.js` | Injects feature identity (worktree) or active-feature summary (root) |
246
246
  | `PreToolUse` (Write\|Edit) | `protect-spec-files.js`, `enforce-phase-writes.js` | Blocks edits to the frozen plan contract post-Gate-2; enforces phase folders |
247
247
  | `PreToolUse` (Bash) | `pre-bash-denylist.js` | Detects destructive shell patterns |
248
- | `PostToolUse` | `post-edit-typecheck.js`, `tasks-json-guard.js`, `trace-autogen.js` | Stack-aware typecheck; guards `tasks.json` plan-contract edits; auto-generates execution traces |
248
+ | `PostToolUse` | `post-edit-typecheck.js`, `tasks-json-guard.js`, `trace-autogen.js`, `comment-guard.js` | Stack-aware typecheck; guards `tasks.json` plan-contract edits; auto-generates execution traces; blocks added comments the comment standard forbids |
249
249
  | `PostToolUse` | `state-sync.js`, `gate-guard.js`, `loop-detect.js` | Syncs feature state; enforces gate ordering; detects repeated-action loops |
250
250
  | `Stop` | `validate-completion.js` | Warns about incomplete tasks, missing outputs, or pending approval gates |
251
251
 
@@ -379,4 +379,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
379
379
 
380
380
  ---
381
381
 
382
- *morph-spec v8.41.0 by [Polymorphism Tech](https://polymorphism.tech)*
382
+ *morph-spec v8.42.0 by [Polymorphism Tech](https://polymorphism.tech)*
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.41.0",
3
+ "version": "8.42.0",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -187,4 +187,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
187
187
 
188
188
  ---
189
189
 
190
- *morph-spec v8.41.0 by Polymorphism Tech*
190
+ *morph-spec v8.42.0 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.41.0 by Polymorphism Tech*
252
+ *morph-spec v8.42.0 by Polymorphism Tech*
@@ -20,7 +20,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
20
20
  - Editar `.morph/specs/usecases/` à mão — só o `finish` escreve lá. Mudar um UC existente (`bug`): copie para `2-plan/usecases/` da feature e corrija lá.
21
21
  - Listar perguntas em texto simples — sempre `AskUserQuestion` (1-4 perguntas por chamada).
22
22
  - Usar `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), ou SQL cru sem justificativa.
23
- - Comentar o que nome, tipo ou teste já dizem: o padrão é nenhum comentário (`architecture-code-comments`).
23
+ - Comentar o que nome, tipo ou teste já dizem: o padrão é nenhum comentário, mesmo ao lado de código comentado; o elo com a pós-condição vai no nome do teste (`architecture-code-comments`, hook `comment-guard`).
24
24
  - Editar `taskScores`, `gateDecisions` ou gates de `feature.json` à mão — sempre via `morph-spec score`/`gate-check`/`approve` (timestamps server-side; Edit manual colide com o hook `state-sync`).
25
25
 
26
26
  **SEMPRE:**
@@ -48,7 +48,7 @@
48
48
  "model": "sonnet",
49
49
  "maxTurns": 120,
50
50
  "effort": "medium",
51
- "persona": "Você é um engenheiro .NET sênior. Pensa em VSA com handlers diretos, result pattern, sealed classes, Guid.CreateVersion7(). Acesso a dados: ApplicationDbContext injetado direto no slice (DbSet<T> já é repository, DbContext já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em decisions.md; nunca uma camada de Service/Manager; nunca repository genérico (IRepository<T>/IUnitOfWork). Não usa exceptions como fluxo. Consulta o standard antes de escrever cada slice. Nomeia tudo em PascalCase para tipos, camelCase para parâmetros. REUSO ANTES DE CRIAR: antes de declarar record/class/interface novo, rode `morph-spec graph explain <Nome>` e `graph affected <Conceito> --uses-only` (sem grafo: `grep -rn`); se já existe algo que cobre o caso, reuse ou estenda, e se criar mesmo assim diga por que em uma linha no relatório da task. COMENTÁRIO E TESTE: o padrão é nenhum comentário (nome, tipo e teste explicam); só o porquê que o código não mostra, nunca `///`/JSDoc que repete a assinatura nem história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
51
+ "persona": "Você é um engenheiro .NET sênior. Pensa em VSA com handlers diretos, result pattern, sealed classes, Guid.CreateVersion7(). Acesso a dados: ApplicationDbContext injetado direto no slice (DbSet<T> já é repository, DbContext já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em decisions.md; nunca uma camada de Service/Manager; nunca repository genérico (IRepository<T>/IUnitOfWork). Não usa exceptions como fluxo. Consulta o standard antes de escrever cada slice. Nomeia tudo em PascalCase para tipos, camelCase para parâmetros. REUSO ANTES DE CRIAR: antes de declarar record/class/interface novo, rode `morph-spec graph explain <Nome>` e `graph affected <Conceito> --uses-only` (sem grafo: `grep -rn`); se já existe algo que cobre o caso, reuse ou estenda, e se criar mesmo assim diga por que em uma linha no relatório da task. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
52
52
  },
53
53
  "ef-modeler": {
54
54
  "title": "EF Core / Database Modeler",
@@ -88,7 +88,7 @@
88
88
  "model": "sonnet",
89
89
  "maxTurns": 120,
90
90
  "effort": "medium",
91
- "persona": "Você é um especialista em EF Core sobre Neon (Postgres). Pensa em migrations explícitas, configurações fluentes, navigation properties bem nomeadas, índices intencionais. Sempre Guid.CreateVersion7() para PKs. Sealed entities. Nunca leak de DbContext fora do handler. REUSO ANTES DE CRIAR: antes de declarar entidade, configuração ou value object novo, rode `morph-spec graph explain <Nome>` (sem grafo: `grep -rn`) — entidade duplicada vira migration divergente; se criar mesmo assim, justifique em uma linha no relatório da task. COMENTÁRIO E TESTE: o padrão é nenhum comentário (nome, tipo e teste explicam); só o porquê que o código não mostra, nunca `///`/JSDoc que repete a assinatura nem história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto. NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável (prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — nunca chame tool destrutiva sem confirmação humana explícita."
91
+ "persona": "Você é um especialista em EF Core sobre Neon (Postgres). Pensa em migrations explícitas, configurações fluentes, navigation properties bem nomeadas, índices intencionais. Sempre Guid.CreateVersion7() para PKs. Sealed entities. Nunca leak de DbContext fora do handler. REUSO ANTES DE CRIAR: antes de declarar entidade, configuração ou value object novo, rode `morph-spec graph explain <Nome>` (sem grafo: `grep -rn`) — entidade duplicada vira migration divergente; se criar mesmo assim, justifique em uma linha no relatório da task. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto. NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável (prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — nunca chame tool destrutiva sem confirmação humana explícita."
92
92
  },
93
93
  "maf-expert": {
94
94
  "title": "Microsoft Agent Framework Expert",
@@ -149,7 +149,7 @@
149
149
  "model": "sonnet",
150
150
  "maxTurns": 120,
151
151
  "effort": "medium",
152
- "persona": "Você é especialista no Microsoft Agent Framework (MAF 1.0 GA). Aplica a regra do sweet-spot: single-agent + structured output + tools + RAG custom resolvem 95%. Refere alias do Model Registry, nunca model hardcoded. Provider-agnostic, direct models sempre (OpenAI/Google/Anthropic/Ollama) — nunca hosted agent service. Não usa Semantic Kernel nem Workflows como default — só quando fan-in/out genuíno aparece. Telemetria via UseOpenTelemetry(sourceName, configure) no builder, nos DOIS níveis (agente e chat client), com EnableSensitiveData desligado fora de dev — AgentOpenTelemetryConsts e agent.WithOpenTelemetry() NÃO existem no pin (medido por reflexão); os nomes de ActivitySource, span e métrica estão no standard ai-agents-observability-patterns, não os deduza. Para OpenAI o caminho default é a Responses API (GetResponsesClient() + AsAIAgent); no Model Registry, alias que OMITE `api` significa `responses` — Chat Completions é opt-in declarado, nunca suposição. HISTÓRICO DE CONVERSA: conversa própria como default (tabela/jsonb do projeto exposta por um ChatHistoryProvider seu, com store: false), nunca o AgentSession — o AgentSession com provider embutido só é a fonte certa quando o histórico é SÓ a lista de mensagens, sem campo de negócio e sem leitura fora do agente; retry, remontagem e auditoria exigem histórico visível e manipulável. INSTRUÇÕES E TOOLS POR FASE: compositor determinístico em C# testável (um record Instructions + Tools escolhido por switch antes de montar ChatOptions), nem context provider nem tool — AIContext.Tools ACRESCENTA tools e nunca remove, então só o código consegue tirar uma tool da mesa. DESCOBERTA DE STANDARD: a lista acima é o que se cola em todo dispatch, não o catálogo — antes de concluir que um assunto não tem standard, rode `morph-spec standards --find \"<tema>\" --category ai-agents`. REUSO ANTES DE CRIAR: antes de escrever tool, contrato ou serviço de agente novo, rode `morph-spec graph explain <Nome>` (sem grafo: `grep -rn`); se já existe algo que cobre o caso, reuse ou estenda, e se criar mesmo assim justifique em uma linha no relatório da task. COMENTÁRIO E TESTE: o padrão é nenhum comentário (nome, tipo e teste explicam); só o porquê que o código não mostra, nunca `///`/JSDoc que repete a assinatura nem história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
152
+ "persona": "Você é especialista no Microsoft Agent Framework (MAF 1.0 GA). Aplica a regra do sweet-spot: single-agent + structured output + tools + RAG custom resolvem 95%. Refere alias do Model Registry, nunca model hardcoded. Provider-agnostic, direct models sempre (OpenAI/Google/Anthropic/Ollama) — nunca hosted agent service. Não usa Semantic Kernel nem Workflows como default — só quando fan-in/out genuíno aparece. Telemetria via UseOpenTelemetry(sourceName, configure) no builder, nos DOIS níveis (agente e chat client), com EnableSensitiveData desligado fora de dev — AgentOpenTelemetryConsts e agent.WithOpenTelemetry() NÃO existem no pin (medido por reflexão); os nomes de ActivitySource, span e métrica estão no standard ai-agents-observability-patterns, não os deduza. Para OpenAI o caminho default é a Responses API (GetResponsesClient() + AsAIAgent); no Model Registry, alias que OMITE `api` significa `responses` — Chat Completions é opt-in declarado, nunca suposição. HISTÓRICO DE CONVERSA: conversa própria como default (tabela/jsonb do projeto exposta por um ChatHistoryProvider seu, com store: false), nunca o AgentSession — o AgentSession com provider embutido só é a fonte certa quando o histórico é SÓ a lista de mensagens, sem campo de negócio e sem leitura fora do agente; retry, remontagem e auditoria exigem histórico visível e manipulável. INSTRUÇÕES E TOOLS POR FASE: compositor determinístico em C# testável (um record Instructions + Tools escolhido por switch antes de montar ChatOptions), nem context provider nem tool — AIContext.Tools ACRESCENTA tools e nunca remove, então só o código consegue tirar uma tool da mesa. DESCOBERTA DE STANDARD: a lista acima é o que se cola em todo dispatch, não o catálogo — antes de concluir que um assunto não tem standard, rode `morph-spec standards --find \"<tema>\" --category ai-agents`. REUSO ANTES DE CRIAR: antes de escrever tool, contrato ou serviço de agente novo, rode `morph-spec graph explain <Nome>` (sem grafo: `grep -rn`); se já existe algo que cobre o caso, reuse ou estenda, e se criar mesmo assim justifique em uma linha no relatório da task. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
153
153
  },
154
154
  "nextjs-expert": {
155
155
  "title": "Next.js / React Expert",
@@ -201,7 +201,7 @@
201
201
  "model": "sonnet",
202
202
  "maxTurns": 120,
203
203
  "effort": "medium",
204
- "persona": "Você escreve Next.js App Router idiomático. Server Components por padrão, Client Components apenas quando há interatividade real. Tailwind + Shadcn + Framer Motion. Tipos via Zod. Nada de getServerSideProps. RSC streaming quando faz sentido. REUSO ANTES DE CRIAR: antes de declarar tipo, hook ou componente novo, rode `morph-spec graph explain <Nome>` (sem grafo: `grep -rn`) — o tipo local redeclarado em cada arquivo é o defeito mais comum aqui; reuse ou estenda o que existe, e se criar mesmo assim justifique em uma linha no relatório da task. COMENTÁRIO E TESTE: o padrão é nenhum comentário (nome, tipo e teste explicam); só o porquê que o código não mostra, nunca `///`/JSDoc que repete a assinatura nem história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
204
+ "persona": "Você escreve Next.js App Router idiomático. Server Components por padrão, Client Components apenas quando há interatividade real. Tailwind + Shadcn + Framer Motion. Tipos via Zod. Nada de getServerSideProps. RSC streaming quando faz sentido. REUSO ANTES DE CRIAR: antes de declarar tipo, hook ou componente novo, rode `morph-spec graph explain <Nome>` (sem grafo: `grep -rn`) — o tipo local redeclarado em cada arquivo é o defeito mais comum aqui; reuse ou estenda o que existe, e se criar mesmo assim justifique em uma linha no relatório da task. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
205
205
  },
206
206
  "ui-designer": {
207
207
  "title": "UI/UX Designer",
@@ -283,7 +283,7 @@
283
283
  "model": "sonnet",
284
284
  "maxTurns": 120,
285
285
  "effort": "medium",
286
- "persona": "Você desenha sistemas, não telas isoladas. Pensa em tokens (cor, espaço, tipografia), componentes reutilizáveis, estados (default/hover/focus/disabled/loading/error), acessibilidade WCAG AA. Mockups em Tailwind direto, sem Figma intermediário. TELA DE DADOS: lista pagina, ordena e filtra no servidor e tem os três estados (vazio, carregando, erro); toda tela sai com o checklist de tela da morph-uiux preenchido, superfície incluída. REUSO ANTES DE CRIAR: componente reutilizável se BUSCA antes de se escrever — rode `morph-spec graph explain <Nome>` e confira `.morph/context/design-system.md` (sem grafo: `grep -rn`); criar um novo exige justificativa em uma linha no relatório da task, a mesma regra que já vale para token de design. COMENTÁRIO E TESTE: o padrão é nenhum comentário (nome, tipo e teste explicam); só o porquê que o código não mostra, nunca `///`/JSDoc que repete a assinatura nem história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
286
+ "persona": "Você desenha sistemas, não telas isoladas. Pensa em tokens (cor, espaço, tipografia), componentes reutilizáveis, estados (default/hover/focus/disabled/loading/error), acessibilidade WCAG AA. Mockups em Tailwind direto, sem Figma intermediário. TELA DE DADOS: lista pagina, ordena e filtra no servidor e tem os três estados (vazio, carregando, erro); toda tela sai com o checklist de tela da morph-uiux preenchido, superfície incluída. REUSO ANTES DE CRIAR: componente reutilizável se BUSCA antes de se escrever — rode `morph-spec graph explain <Nome>` e confira `.morph/context/design-system.md` (sem grafo: `grep -rn`); criar um novo exige justificativa em uma linha no relatório da task, a mesma regra que já vale para token de design. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
287
287
  },
288
288
  "infra-engineer": {
289
289
  "title": "Infrastructure / Jobs Engineer",
@@ -327,7 +327,7 @@
327
327
  "model": "sonnet",
328
328
  "maxTurns": 120,
329
329
  "effort": "medium",
330
- "persona": "Você cuida de infra: jobs assíncronos com Hangfire (apenas quando há razão real), deployment, observabilidade. Pensa em retry, idempotência, dead-letter, métricas. Não inventa infra — usa o que o stack já oferece. Operações no GitHub (issues, PRs, releases, Actions) sempre via `gh` CLI no Bash — o morph-spec não usa MCP do GitHub. Coolify sempre via SSH + curl na API local — não há MCP nem CLI. COMENTÁRIO E TESTE: o padrão é nenhum comentário (nome, tipo e teste explicam); só o porquê que o código não mostra, nunca `///`/JSDoc que repete a assinatura nem história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto. NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável (prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — nunca chame tool destrutiva sem confirmação humana explícita."
330
+ "persona": "Você cuida de infra: jobs assíncronos com Hangfire (apenas quando há razão real), deployment, observabilidade. Pensa em retry, idempotência, dead-letter, métricas. Não inventa infra — usa o que o stack já oferece. Operações no GitHub (issues, PRs, releases, Actions) sempre via `gh` CLI no Bash — o morph-spec não usa MCP do GitHub. Coolify sempre via SSH + curl na API local — não há MCP nem CLI. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto. NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável (prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — nunca chame tool destrutiva sem confirmação humana explícita."
331
331
  },
332
332
  "evaluator": {
333
333
  "title": "Code & Architecture Evaluator",
@@ -57,6 +57,7 @@ framework/hooks/
57
57
  | **PostToolUse** (Write\|Edit) | post-edit-typecheck.js | Advisory | Debounced `tsc --noEmit` after TS/TSX edits; silent on success. Also touches `.morph/memory/build-dirty.json` on every source-file edit — the write side of validate-completion.js's Stop-hook build-dirty gate |
58
58
  | **PostToolUse** (Write\|Edit) | tasks-json-guard.js | Advisory | Validates the written tasks.json schema (status/effort enums); warns so the LLM fixes it immediately |
59
59
  | **PostToolUse** (Write\|Edit) | trace-autogen.js | State sync | Auto-generates `.morph/traces/YYYY-MM-DD-{feature}-implement.json` from tasks.json + recap.md + taskScores on every tasks.json write — the skills never write traces by hand |
60
+ | **PostToolUse** (Write\|Edit) | comment-guard.js | Blocking | Blocks when the edit ADDED a comment the `architecture-code-comments` standard never allows: process history (UC/ADR/task/gate), banner/`#region`, a block over 3 lines, or a doc-comment outside a published package. Old comments are never reported; last link of the chain |
60
61
  | **PostToolUse** (Write\|Edit\|AskUserQuestion) | state-sync.js | State sync | Autonomous state updates (feature register, output tracking, gate approval) |
61
62
  | **PostToolUse** (Write\|Edit\|AskUserQuestion) | gate-guard.js | Advisory | Warns (never blocks) when a gate action disagrees with the persisted gateDecisions; logs one `coherent`/`divergent` line per check in `.morph/logs/activity.jsonl` (issue #63) |
62
63
  | **PostToolUse** (all tools) | telemetry-log.js | Telemetry | Appends every tool call (skill, sub-agent dispatch + prompt, knowledge-base read, bash/edit) to `.morph/logs/events.jsonl`; silent, fail-open — the NLH execution-path capture read back by `morph-spec telemetry` |
@@ -101,6 +101,8 @@ async function main() {
101
101
 
102
102
  // PreToolUse: o primeiro bloqueio vence e encerra a cadeia.
103
103
  if (result.decision === 'block') {
104
+ // A later link blocking must not swallow what earlier links reported (a tsc error on the same edit).
105
+ if (contexts.length) result = { ...result, reason: [...contexts, result.reason].join('\n\n') };
104
106
  console.log(JSON.stringify(result));
105
107
  process.exit(0);
106
108
  }
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUse Hook: comments the edit added in a shape the
5
+ * `architecture-code-comments` standard never allows.
6
+ *
7
+ * Event: PostToolUse | Matcher: Write|Edit
8
+ *
9
+ * `decision: block` makes the agent fix it in the same turn; the edit itself
10
+ * already landed. Only comments absent from `tool_response.originalFile`
11
+ * count, so editing a legacy file never reports its old comments.
12
+ *
13
+ * Fail-open: any error passes silently.
14
+ */
15
+
16
+ import { readFileSync } from 'fs';
17
+ import { isAbsolute, relative, resolve } from 'path';
18
+
19
+ import { blockResult, runStandalone } from '../../shared/hook-response.js';
20
+ import { commentFindings, docCommentsForbidden, MAX_BLOCK_LINES } from '../../shared/comment-rules.js';
21
+ import { isGenerated, isProjectSource } from '../../shared/source-files.js';
22
+
23
+ const MAX_LISTED = 10;
24
+
25
+ const RULE_LABEL = {
26
+ process: 'história de processo (UC/ADR/task/gate)',
27
+ banner: 'banner ou #region',
28
+ 'doc-comment': '///·JSDoc em código que não é pacote publicado',
29
+ 'long-block': `bloco com mais de ${MAX_BLOCK_LINES} linhas`,
30
+ };
31
+
32
+ function spliceEdit(original, oldString, newString, replaceAll) {
33
+ if (replaceAll) return original.split(oldString).join(newString);
34
+ const at = original.indexOf(oldString);
35
+ if (at < 0) return null;
36
+ return original.slice(0, at) + newString + original.slice(at + oldString.length);
37
+ }
38
+
39
+ function readOrNull(file) {
40
+ try {
41
+ return readFileSync(file, 'utf8');
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function contentsOf(payload, absolute) {
48
+ const input = payload.tool_input ?? {};
49
+ const response = payload.tool_response ?? {};
50
+ const before = response.originalFile ?? '';
51
+ if (payload.tool_name === 'Write') {
52
+ if (response.originalFile === undefined) return null;
53
+ return { before, after: readOrNull(absolute) ?? input.content ?? '' };
54
+ }
55
+ if (typeof response.originalFile !== 'string') return null;
56
+ const after = readOrNull(absolute)
57
+ ?? spliceEdit(response.originalFile, input.old_string ?? '', input.new_string ?? '', input.replace_all === true);
58
+ return after === null ? null : { before, after };
59
+ }
60
+
61
+ // NOT_SOURCE matches `build/`, `out/` anywhere: judged on an absolute path,
62
+ // a project checked out under such a folder would be skipped whole.
63
+ function projectRelative(absolute, projectDir) {
64
+ const rel = relative(projectDir, absolute);
65
+ return rel && !rel.startsWith('..') && !isAbsolute(rel) ? rel : absolute;
66
+ }
67
+
68
+ function describe(file, findings) {
69
+ const listed = findings.slice(0, MAX_LISTED)
70
+ .map((f) => `- ${file}:${f.line} — ${RULE_LABEL[f.rule]} — ${f.text}`);
71
+ if (findings.length > MAX_LISTED) listed.push(`- (+${findings.length - MAX_LISTED})`);
72
+ return [
73
+ 'Comentários que o padrão do projeto não aceita (architecture-code-comments: o padrão é nenhum comentário):',
74
+ ...listed,
75
+ '',
76
+ 'Remova-os agora. O que o nome, o tipo ou o teste podem dizer vai para eles; o elo com a pós-condição vai no NOME do teste.',
77
+ `Só fica o porquê que o código não mostra (invariante, ordem que importa, quirk externo com link), em até ${MAX_BLOCK_LINES} linhas.`,
78
+ ].join('\n');
79
+ }
80
+
81
+ /**
82
+ * @param {object} payload - PostToolUse stdin payload
83
+ * @returns {Promise<object|null>}
84
+ */
85
+ export async function run(payload) {
86
+ try {
87
+ if (!['Write', 'Edit'].includes(payload?.tool_name)) return null;
88
+ const file = payload.tool_input?.file_path ?? '';
89
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || payload.cwd || process.cwd();
90
+ const absolute = isAbsolute(file) ? file : resolve(projectDir, file);
91
+ const judged = projectRelative(absolute, projectDir);
92
+ if (!isProjectSource(judged) || isGenerated(judged)) return null;
93
+
94
+ const contents = contentsOf(payload, absolute);
95
+ if (!contents) return null;
96
+
97
+ let docForbidden = false;
98
+ try { docForbidden = docCommentsForbidden(absolute); } catch { /* unreadable project file: stay quiet */ }
99
+
100
+ const findings = commentFindings({ file, ...contents, docCommentsForbidden: docForbidden });
101
+ return findings.length ? blockResult(describe(file, findings)) : null;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ await runStandalone(run);
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The comment shapes the `architecture-code-comments` standard forbids in
3
+ * every case, judged only on the comments an edit ADDED. Whether a short
4
+ * comment restates the code is not decidable here; that stays with the
5
+ * Gate 3 evaluator and the `code-budget` density.
6
+ *
7
+ * @module comment-rules
8
+ */
9
+
10
+ import { existsSync, readdirSync, readFileSync } from 'fs';
11
+ import { dirname, join } from 'path';
12
+
13
+ import { citesProcess, commentLines } from './comment-scanner.js';
14
+
15
+ export const MAX_BLOCK_LINES = 3;
16
+
17
+ const ALLOWED = [
18
+ /READS STANDARDS?:/,
19
+ /\bVERIFY:/,
20
+ /eslint-(?:disable|enable)/,
21
+ /@ts-(?:ignore|expect-error|nocheck|check)/,
22
+ /<auto-generated/i,
23
+ /prettier-ignore/,
24
+ /ReSharper\s+(?:disable|restore)/,
25
+ /@(?:vitest|jest)-environment/,
26
+ /^\/\/\/\s*<reference\b/,
27
+ /^#!/,
28
+ /istanbul ignore|c8 ignore|v8 ignore/,
29
+ ];
30
+
31
+ const DECORATION = /[-=*#~_+]{3,}/;
32
+ const REGION = /^\s*#\s*(?:end)?region\b/;
33
+
34
+ function bodyOf(text) {
35
+ return text.replace(/^(?:\/\/+|\/\*+|\*+)/, '').replace(/\*+\/$/, '').trim();
36
+ }
37
+
38
+ function isBanner(text) {
39
+ const body = bodyOf(text);
40
+ if (REGION.test(body)) return true;
41
+ if (!DECORATION.test(body)) return false;
42
+ const words = body.replace(/[-=*#~_+|]/g, '').trim();
43
+ return words === '' || /^[-=*#~_+]{3,}.*[-=*#~_+]{3,}$/.test(body);
44
+ }
45
+
46
+ function isDocComment(text, isCs) {
47
+ return isCs ? /^\/\/\/(?!\/)/.test(text) : /^\/\*\*(?!\*)/.test(text);
48
+ }
49
+
50
+ function nearest(startDir, test) {
51
+ let dir = startDir;
52
+ for (;;) {
53
+ const hit = test(dir);
54
+ if (hit) return hit;
55
+ const parent = dirname(dir);
56
+ if (parent === dir) return null;
57
+ dir = parent;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Doc-comments are allowed only on a package consumed outside the repo. When
63
+ * that cannot be read from the nearest project file, the rule stays quiet.
64
+ *
65
+ * @param {string} file - absolute path of the edited file
66
+ * @returns {boolean}
67
+ */
68
+ export function docCommentsForbidden(file) {
69
+ const dir = dirname(file);
70
+ if (/\.cs$/i.test(file)) {
71
+ const proj = nearest(dir, (d) => {
72
+ try {
73
+ const name = readdirSync(d).find((n) => /\.csproj$/i.test(n));
74
+ return name ? join(d, name) : null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ });
79
+ if (!proj) return false;
80
+ const xml = readFileSync(proj, 'utf8');
81
+ return !/<(?:IsPackable|GeneratePackageOnBuild)>\s*true\s*</i.test(xml) && !/<PackageId>/i.test(xml);
82
+ }
83
+ const pkg = nearest(dir, (d) => (existsSync(join(d, 'package.json')) ? join(d, 'package.json') : null));
84
+ if (!pkg) return false;
85
+ return JSON.parse(readFileSync(pkg, 'utf8')).private === true;
86
+ }
87
+
88
+ /** Lines of `after` whose comment is not one `before` already had (moved comments do not count). */
89
+ function addedCommentLines(before, after, file) {
90
+ const remaining = new Map();
91
+ for (const text of commentLines(before, file).values()) {
92
+ remaining.set(text, (remaining.get(text) ?? 0) + 1);
93
+ }
94
+ const added = new Map();
95
+ for (const [line, text] of commentLines(after, file)) {
96
+ const left = remaining.get(text) ?? 0;
97
+ if (left > 0) remaining.set(text, left - 1);
98
+ else added.set(line, text);
99
+ }
100
+ return added;
101
+ }
102
+
103
+ function addedRegionLines(before, after) {
104
+ const old = new Map();
105
+ for (const row of before.split('\n')) {
106
+ if (REGION.test(row)) old.set(row.trim(), (old.get(row.trim()) ?? 0) + 1);
107
+ }
108
+ const out = [];
109
+ after.split('\n').forEach((row, i) => {
110
+ if (!REGION.test(row)) return;
111
+ const left = old.get(row.trim()) ?? 0;
112
+ if (left > 0) old.set(row.trim(), left - 1);
113
+ else out.push({ line: i + 1, text: row.trim() });
114
+ });
115
+ return out;
116
+ }
117
+
118
+ /**
119
+ * @param {{ file: string, before: string, after: string, docCommentsForbidden?: boolean }} input
120
+ * @returns {Array<{ line: number, rule: 'process'|'banner'|'doc-comment'|'long-block', text: string }>}
121
+ */
122
+ export function commentFindings({ file, before, after, docCommentsForbidden: docForbidden = false }) {
123
+ const isCs = /\.cs$/i.test(file);
124
+ const all = commentLines(after, file);
125
+ const added = addedCommentLines(before, after, file);
126
+ const findings = [];
127
+ const flagged = new Set();
128
+ const flag = (line, rule, text) => {
129
+ if (flagged.has(line)) return;
130
+ flagged.add(line);
131
+ findings.push({ line, rule, text });
132
+ };
133
+
134
+ for (const [line, text] of added) {
135
+ if (ALLOWED.some((re) => re.test(text))) continue;
136
+ if (citesProcess(text)) flag(line, 'process', text);
137
+ else if (isBanner(text)) flag(line, 'banner', text);
138
+ }
139
+ for (const { line, text } of addedRegionLines(before, after)) flag(line, 'banner', text);
140
+
141
+ if (docForbidden) {
142
+ for (const [line, text] of added) {
143
+ const opensBlock = !isCs || !isDocComment(all.get(line - 1) ?? '', isCs);
144
+ if (isDocComment(text, isCs) && opensBlock) flag(line, 'doc-comment', text);
145
+ }
146
+ }
147
+
148
+ const rows = after.split('\n');
149
+ const lines = [...all.keys()]
150
+ .filter((line) => (rows[line - 1] ?? '').trim() === all.get(line))
151
+ .sort((a, b) => a - b);
152
+ let run = [];
153
+ const closeRun = () => {
154
+ if (run.length && isDocComment(all.get(run[0]), isCs)) {
155
+ run = [];
156
+ return;
157
+ }
158
+ const addedInRun = run.filter((l) => added.has(l) && !ALLOWED.some((re) => re.test(all.get(l))));
159
+ if (addedInRun.length > MAX_BLOCK_LINES) {
160
+ flag(run[0], 'long-block', `${run.length} linhas: ${all.get(run[0])}`);
161
+ }
162
+ run = [];
163
+ };
164
+ for (const line of lines) {
165
+ if (run.length && line !== run[run.length - 1] + 1) closeRun();
166
+ run.push(line);
167
+ }
168
+ closeRun();
169
+
170
+ return findings.sort((a, b) => a.line - b.line);
171
+ }
@@ -5,10 +5,10 @@
5
5
  * comentário de string para o `doctor --ai`, com a sintaxe de TS/JS ligada
6
6
  * fora do C#. Um regex por linha confundiria `https://` com comentário.
7
7
  *
8
- * @module code-budget-comment-scanner
8
+ * @module comment-scanner
9
9
  */
10
10
 
11
- import { stripComments } from '../../ai/comment-stripper.js';
11
+ import { stripComments } from './comment-stripper.js';
12
12
 
13
13
  /** Vocabulário de processo do morph-spec, fora o id de task (abaixo). */
14
14
  const PROCESS_CITATION = [
@@ -11,7 +11,8 @@
11
11
  * canônico `model-registry.v2.json` usa e que o leitor C# do kit aceita de
12
12
  * propósito (`JsonCommentHandling.Skip`, `AllowTrailingCommas`);
13
13
  * - `dotnet-test-introspection.js` — lê o `global.json` por `jsoncToJson`;
14
- * - `validators/code-budget/comment-scanner.js` — acha as linhas de
14
+ * - `comment-scanner.js` (ao lado; lido pelo `code-budget` e pelo hook
15
+ * `comment-guard`) — acha as linhas de
15
16
  * comentário de `.cs`/`.ts`/`.js`, e é o único que liga a opção `js`.
16
17
  *
17
18
  * **Por que não um regex.** O exemplar tem `"http://localhost:11434/v1"` e uma
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Which files count as the project's hand-written source. One reading for the
3
+ * `reuse` and `code-budget` validators and the `comment-guard` hook, so the
4
+ * three never disagree about the same file.
5
+ *
6
+ * @module source-files
7
+ */
8
+
9
+ const SCANNABLE = /\.(cs|ts|tsx|js|jsx|mjs|cjs)$/i;
10
+
11
+ // `.morph/` is real: an archived feature's `contracts.cs` entered ProspectPRO's
12
+ // graph as a second `RoulettePriority`.
13
+ const NOT_SOURCE = /(^|\/)(?:\.morph|node_modules|dist|build|out|\.next|bin|obj|coverage)\//;
14
+
15
+ const DECLARATION_FILE = /\.d\.ts$/i;
16
+
17
+ const GENERATED = /(?:\.Designer|\.g|\.g\.i)\.cs$|ModelSnapshot\.cs$/i;
18
+
19
+ /**
20
+ * @param {string} sourceFile
21
+ * @returns {boolean} project code in a scanned extension, tests included
22
+ */
23
+ export function isProjectSource(sourceFile) {
24
+ const f = String(sourceFile || '').replace(/\\/g, '/');
25
+ return SCANNABLE.test(f) && !DECLARATION_FILE.test(f) && !NOT_SOURCE.test(f);
26
+ }
27
+
28
+ /**
29
+ * @param {string} sourceFile
30
+ * @returns {boolean} generated C# nobody writes by hand
31
+ */
32
+ export function isGenerated(sourceFile) {
33
+ return GENERATED.test(String(sourceFile || ''));
34
+ }
@@ -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.41.0"
7
+ cliVersion: "8.42.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.41.0"
7
+ cliVersion: "8.42.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.41.0"
7
+ cliVersion: "8.42.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -193,7 +193,9 @@ dispatch Agent (subagent_type: "morph-{id}") with prompt:
193
193
  Comentário: o padrão é nenhum. Nome, tipo e teste explicam o código; sem
194
194
  `///`/JSDoc em membro de aplicação, cabeçalho, banner de seção ou história
195
195
  da task. Só o porquê que o código não mostra, em uma linha
196
- (`architecture-code-comments`).
196
+ (`architecture-code-comments`). O elo com a pós-condição vai no nome do
197
+ teste; a densidade do arquivo vizinho não é modelo. O hook `comment-guard`
198
+ bloqueia a edição que adiciona história, banner ou bloco longo.
197
199
  Não modifique spec.md, mandate.md.
198
200
 
199
201
  Contrato do relatório final:
@@ -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.41.0"
7
+ cliVersion: "8.42.0"
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.41.0"
7
+ cliVersion: "8.42.0"
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.41.0"
7
+ cliVersion: "8.42.0"
8
8
  ---
9
9
 
10
10
  # morph-hotfix
@@ -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.41.0"
7
+ cliVersion: "8.42.0"
8
8
  ---
9
9
 
10
10
  # morph-implement
@@ -94,7 +94,9 @@ Ao iniciar cada task de `tasks.json` — antes de escrever qualquer código. Nor
94
94
  Comentário: o padrão é nenhum. Nome, tipo e teste explicam o código; sem
95
95
  `///`/JSDoc em membro de aplicação, cabeçalho, banner de seção ou história
96
96
  da task. Só o porquê que o código não mostra, em uma linha
97
- (`architecture-code-comments`).
97
+ (`architecture-code-comments`). O elo com a pós-condição vai no nome do
98
+ teste; a densidade do arquivo vizinho não é modelo. O hook `comment-guard`
99
+ bloqueia a edição que adiciona história, banner ou bloco longo.
98
100
 
99
101
  Contrato do relatório final:
100
102
  1. GRAVE `.morph/features/{feature}/3-implement/reports/[ID].md` ANTES de
@@ -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.41.0"
7
+ cliVersion: "8.42.0"
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.41.0"
7
+ cliVersion: "8.42.0"
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.41.0"
7
+ cliVersion: "8.42.0"
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.41.0"
7
+ cliVersion: "8.42.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -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.41.0"
7
+ cliVersion: "8.42.0"
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.41.0"
7
+ cliVersion: "8.42.0"
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.41.0"
7
+ cliVersion: "8.42.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux
@@ -2848,7 +2848,7 @@
2848
2848
  "not-worth": "#o-que-não-merece",
2849
2849
  "size": "#tamanho"
2850
2850
  },
2851
- "digest": "Default is NO comment: code explains itself through names, types and tests. Before commenting, rename, extract a function or named constant, encode the state in the type, or write a test; comment only what none of these can express: an invariant the type does not express, an order that matters, an external quirk (third-party API, library bug with its link), or the contract of a package published for consumers outside the repo. Never process history (task, UC, ADR, FR, Gate, evaluator, spec.md, mockup: commit message and decisions.md), never restate the code, no XML doc/JSDoc on application members that repeats the signature, no file headers, section banners, #region, commented-out code or loose TODOs, no claims about behavior (make them test assertions). One line is normal, ~3 is the ceiling; more than one comment line per ten code lines means the names are not doing the work. The verify validators node (code-budget) warns on added comments that cite process and on added comment density.",
2851
+ "digest": "Default is NO comment: code explains itself through names, types and tests. Before commenting, rename, extract a function or named constant, encode the state in the type, or write a test; comment only what none of these can express: an invariant the type does not express, an order that matters, an external quirk (third-party API, library bug with its link), or the contract of a package published for consumers outside the repo. Never process history (task, UC, ADR, FR, Gate, evaluator, spec.md, mockup: commit message and decisions.md; in a test the link to the postcondition goes in the test NAME), never restate the code, no XML doc/JSDoc on application members that repeats the signature, no file headers, section banners, #region, commented-out code or loose TODOs, no claims about behavior (make them test assertions). One line is normal, ~3 is the ceiling; more than one comment line per ten code lines means the names are not doing the work. A heavily commented neighbour file is not a model. The comment-guard hook blocks an edit that ADDS process history, a banner/#region, a block over 3 lines or a doc-comment outside a published package; the verify validators node (code-budget) warns on added comments that cite process and on added comment density.",
2852
2852
  "verifiedAgainst": "C#, TypeScript e JavaScript (convenção de linguagem, sem pacote externo).",
2853
2853
  "provedBy": []
2854
2854
  }
@@ -43,7 +43,8 @@ Só o **porquê que o código não mostra**:
43
43
 
44
44
  - **História de processo.** Task, UC, ADR, FR, Gate, avaliador, `spec.md`, mockup, "confirmado ao
45
45
  implementar a T11", "o Gate 3 pediu". Isso vai na mensagem de commit e no `decisions.md`, onde tem
46
- data e autor e não envelhece dentro do arquivo.
46
+ data e autor e não envelhece dentro do arquivo. Em teste, o elo com a pós-condição vai no **nome**
47
+ do teste (`P5_estado_do_lead_espelha_o_crm`, `DisplayName`), nunca em `// UC-672 P5`.
47
48
  - **Repetição do código.** `// incrementa o contador` sobre `count++`, `// valida o request` sobre
48
49
  `validator.Validate(request)`. Nome bom substitui comentário.
49
50
  - **Doc-comment que repete a assinatura.** `/// <summary>Obtém o pedido.</summary>` sobre
@@ -63,7 +64,8 @@ Só o **porquê que o código não mostra**:
63
64
  Uma linha é o normal; teto de ~3. Acima disso o comentário justifica o volume (algoritmo não
64
65
  óbvio, armadilha de terceiro com a reprodução). Bloco longo narrando decisões é `decisions.md` com
65
66
  outro nome. Em volume, código de aplicação fica perto de zero: mais de uma linha de comentário a
66
- cada dez de código é sinal de que os nomes não estão fazendo o trabalho.
67
+ cada dez de código é sinal de que os nomes não estão fazendo o trabalho. Arquivo vizinho muito
68
+ comentado não é modelo: código novo segue este padrão, não a densidade do legado.
67
69
 
68
70
  ## Exemplo
69
71
 
@@ -90,6 +92,11 @@ await upsert({ key: doc.sourceId, hash: sha256(doc.body) });
90
92
 
91
93
  ## Na revisão
92
94
 
95
+ Na hora da edição, o hook `comment-guard` bloqueia o comentário **adicionado** que é história de
96
+ processo, banner/`#region`, bloco de mais de 3 linhas ou `///`/JSDoc em projeto que não é pacote
97
+ publicado. Comentário que o arquivo já tinha nunca é acusado. Repetir o código em uma linha não é
98
+ detectável por forma: fica com a revisão abaixo.
99
+
93
100
  Comentário que o código dispensava é achado da rubrica `code-quality` no Gate 3. O nó `validators`
94
101
  do `morph-spec verify` mede, nas linhas que a feature adicionou (validador `code-budget`), quantas
95
102
  linhas de comentário citam processo e a densidade de comentário sobre o código adicionado, e avisa
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.41.0",
3
+ "version": "8.42.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -25,7 +25,7 @@
25
25
  import { readFileSync } from 'node:fs';
26
26
  import { relative } from 'node:path';
27
27
  import { collectProjectFiles } from './ai-project-scan.js';
28
- import { stripComments } from './comment-stripper.js';
28
+ import { stripComments } from '../../../framework/hooks/shared/comment-stripper.js';
29
29
 
30
30
  /** Símbolos renomeados antes do 1.0 GA, e o nome vivo de cada um. */
31
31
  export const RENAMED_SYMBOLS = {
@@ -22,7 +22,7 @@ import { readFileSync } from 'node:fs';
22
22
  import { existsSync } from 'node:fs';
23
23
  import { isAbsolute, join, basename } from 'node:path';
24
24
  import { collectProjectFiles } from './ai-project-scan.js';
25
- import { jsoncToJson } from './comment-stripper.js';
25
+ import { jsoncToJson } from '../../../framework/hooks/shared/comment-stripper.js';
26
26
 
27
27
  /** Limite de tamanho para um JSON candidato (bytes de texto) — evita ler dumps. */
28
28
  const MAX_JSON_CHARS = 512 * 1024;
@@ -23,7 +23,7 @@ import { existsSync, readFileSync, readdirSync } from 'fs';
23
23
  import { dirname, join, resolve } from 'path';
24
24
 
25
25
  import { isInside } from './test-runner.js';
26
- import { jsoncToJson } from '../ai/comment-stripper.js';
26
+ import { jsoncToJson } from '../../../framework/hooks/shared/comment-stripper.js';
27
27
 
28
28
  const MTP_RUNNER = 'Microsoft.Testing.Platform';
29
29
  const FLOOR_PROPERTY = /<TestingPlatformCommandLineArguments\b[^>]*>[^<]*--minimum-expected-tests/i;
@@ -21,8 +21,9 @@ import { join } from 'path';
21
21
 
22
22
  import { addedLinesAgainst, detectDefaultBranch } from '../../git/default-branch.js';
23
23
  import { resolveBaseRef } from '../../git/branch-classifier.js';
24
- import { isProjectSource, isTestFile } from '../reuse/symbol-scanner.js';
25
- import { citesProcess, commentLines } from './comment-scanner.js';
24
+ import { isTestFile } from '../reuse/symbol-scanner.js';
25
+ import { isGenerated, isProjectSource } from '../../../../framework/hooks/shared/source-files.js';
26
+ import { citesProcess, commentLines } from '../../../../framework/hooks/shared/comment-scanner.js';
26
27
 
27
28
  /** Razão teste ÷ produção acima da qual se avisa. */
28
29
  export const MAX_TEST_RATIO = 1.5;
@@ -39,9 +40,6 @@ export const MIN_CODE_LINES = 40;
39
40
  /** Quantos locais citar no aviso de comentário antes de resumir com "(+N)". */
40
41
  const MAX_CITED = 4;
41
42
 
42
- /** Código gerado não é escrito por ninguém, então não entra em nenhum dos lados. */
43
- const GENERATED = /(?:\.Designer|\.g|\.g\.i)\.cs$|ModelSnapshot\.cs$/i;
44
-
45
43
  const decimal = (n) => n.toFixed(1).replace('.', ',');
46
44
 
47
45
  /**
@@ -56,7 +54,7 @@ export function measureCodeBudget(files) {
56
54
  const measured = { commentLines: 0, processLines: [], testLines: 0, productionLines: 0, commentsByFile: {} };
57
55
 
58
56
  for (const { path, content, added } of files) {
59
- if (!isProjectSource(path) || GENERATED.test(path)) continue;
57
+ if (!isProjectSource(path) || isGenerated(path)) continue;
60
58
  const addedSet = added ? new Set(added) : null;
61
59
  const isAdded = (n) => addedSet === null || addedSet.has(n);
62
60
 
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export * from './code-budget-validator.js';
9
- export * from './comment-scanner.js';
9
+ export * from '../../../../framework/hooks/shared/comment-scanner.js';
@@ -18,24 +18,7 @@
18
18
  */
19
19
 
20
20
  import { isTestPath, languageOf } from '../../graph/sanitize.js';
21
-
22
- /** Extensões que este scanner sabe ler. Fora daqui, o arquivo é ignorado. */
23
- const SCANNABLE = /\.(cs|ts|tsx|js|jsx|mjs|cjs)$/i;
24
-
25
- /**
26
- * Diretórios que contêm arquivos com extensão de código mas que não são o
27
- * código-fonte do projeto. `.morph/` é o caso que apareceu de verdade: o
28
- * `archive/{feature}/1-design/contracts.cs` de uma feature arquivada entra no
29
- * grafo do ProspectPRO como um tipo `RoulettePriority` a mais, e acusaria a
30
- * entidade real de duplicar o próprio artefato de planejamento.
31
- */
32
- const NOT_SOURCE = /(^|\/)(?:\.morph|node_modules|dist|build|out|\.next|bin|obj|coverage)\//;
33
-
34
- /**
35
- * Declaration file do TypeScript. `auth-debug.d.ts` + `auth-debug.ts`
36
- * declarando o mesmo tipo é o mecanismo da linguagem, não duplicação.
37
- */
38
- const DECLARATION_FILE = /\.d\.ts$/i;
21
+ import { isProjectSource } from '../../../../framework/hooks/shared/source-files.js';
39
22
 
40
23
  /**
41
24
  * Nomes que a convenção usa como tipo LOCAL de arquivo, não como conceito do
@@ -63,16 +46,6 @@ const WEB_DECL =
63
46
  /** Linha que é claramente comentário — não vale a pena parsear. */
64
47
  const COMMENT_LINE = /^\s*(?:\/\/|\/\*|\*|<!--)/;
65
48
 
66
- /**
67
- * @param {string} sourceFile
68
- * @returns {boolean} código do projeto numa extensão lida, fora de `.d.ts` e de
69
- * pasta que não é fonte — teste incluído
70
- */
71
- export function isProjectSource(sourceFile) {
72
- const f = String(sourceFile || '').replace(/\\/g, '/');
73
- return SCANNABLE.test(f) && !DECLARATION_FILE.test(f) && !NOT_SOURCE.test(f);
74
- }
75
-
76
49
  /**
77
50
  * @param {string} sourceFile
78
51
  * @returns {boolean} arquivo de teste, pelo mesmo predicado do grafo
@@ -216,14 +216,15 @@ const MORPH_HOOKS = [
216
216
  // order as before. post-edit-typecheck declared no timeout of its own (it
217
217
  // can legitimately spend up to its internal TSC_TIMEOUT_MS=30000 running
218
218
  // `npx tsc --noEmit`), so the chain declares none rather than under-
219
- // provisioning it with just the sum of the other two links' 3000ms each. ===
219
+ // provisioning it with just the sum of the other two links' 3000ms each.
220
+ // comment-guard is LAST: it blocks, and a block ends the chain. ===
220
221
  {
221
222
  event: 'PostToolUse',
222
223
  matcher: 'Write|Edit',
223
224
  hooks: [{
224
225
  type: 'command',
225
226
  command: 'node framework/hooks/claude-code/core/chain.js',
226
- args: ['post-edit-typecheck', 'tasks-json-guard', 'trace-autogen']
227
+ args: ['post-edit-typecheck', 'tasks-json-guard', 'trace-autogen', 'comment-guard']
227
228
  }]
228
229
  },
229
230