@polymorphism-tech/morph-spec 8.44.0 → 8.45.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 (193) hide show
  1. package/CLAUDE.md +10 -9
  2. package/README.md +10 -10
  3. package/bin/morph-spec.js +10 -13
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -4
  7. package/docs/QUICKSTART.md +6 -6
  8. package/framework/CLAUDE.md +6 -8
  9. package/framework/MORPH.md +39 -45
  10. package/framework/agents.json +8 -9
  11. package/framework/commands/morph-apply.md +1 -3
  12. package/framework/commands/morph-hotfix.md +1 -1
  13. package/framework/commands/morph-proposal.md +2 -2
  14. package/framework/docs/CLI.md +10 -10
  15. package/framework/evals/review-checklist.md +90 -0
  16. package/framework/evals/trace-schema.md +2 -12
  17. package/framework/hooks/README.md +2 -3
  18. package/framework/hooks/claude-code/core/heavy-gate.js +1 -1
  19. package/framework/hooks/claude-code/core/protect-spec-files.js +2 -2
  20. package/framework/hooks/claude-code/core/telemetry-log.js +2 -2
  21. package/framework/hooks/claude-code/core/trace-autogen.js +1 -22
  22. package/framework/hooks/claude-code/core/validate-completion.js +3 -56
  23. package/framework/hooks/claude-code/statusline.sh +11 -11
  24. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  25. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  26. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  27. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  28. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  29. package/framework/hooks/shared/build-check.js +1 -1
  30. package/framework/hooks/shared/comment-scanner.js +1 -1
  31. package/framework/hooks/shared/phase-utils.js +0 -5
  32. package/framework/hooks/shared/stdin-reader.js +26 -26
  33. package/framework/phases.json +4 -6
  34. package/framework/rubrics/backend.json +1 -1
  35. package/framework/rubrics/default.json +1 -1
  36. package/framework/rubrics/frontend.json +1 -1
  37. package/framework/rules/morph-workflow.md +6 -6
  38. package/framework/schemas/feature.schema.json +2 -28
  39. package/framework/schemas/tasks.schema.json +6 -2
  40. package/framework/skills/README.md +1 -2
  41. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  42. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  43. package/framework/skills/core/morph-apply/SKILL.md +145 -246
  44. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  45. package/framework/skills/core/morph-hotfix/SKILL.md +5 -3
  46. package/framework/skills/core/morph-implement/SKILL.md +101 -201
  47. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  48. package/framework/skills/core/morph-media/SKILL.md +1 -1
  49. package/framework/skills/core/morph-plan/SKILL.md +171 -287
  50. package/framework/skills/core/morph-proposal/SKILL.md +4 -5
  51. package/framework/skills/core/morph-review/SKILL.md +109 -430
  52. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  53. package/framework/skills/core/morph-standards/SKILL.md +2 -2
  54. package/framework/skills/core/morph-uiux/SKILL.md +2 -3
  55. package/framework/standards/STANDARDS.json +10 -10
  56. package/framework/standards/ai-agents/agent-archetypes.md +1 -1
  57. package/framework/standards/ai-agents/agent-session.md +1 -1
  58. package/framework/standards/ai-agents/agent-spec.md +1 -1
  59. package/framework/standards/ai-agents/batching.md +1 -1
  60. package/framework/standards/ai-agents/context-providers.md +1 -1
  61. package/framework/standards/ai-agents/conversational-agent-with-phases.md +1 -1
  62. package/framework/standards/ai-agents/cost-and-budget.md +1 -1
  63. package/framework/standards/ai-agents/durable-workflows-hitl.md +1 -1
  64. package/framework/standards/ai-agents/guardrails.md +1 -1
  65. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  66. package/framework/standards/ai-agents/mcp-server.md +1 -1
  67. package/framework/standards/ai-agents/mcp-tools.md +1 -1
  68. package/framework/standards/ai-agents/media-pipeline.md +1 -1
  69. package/framework/standards/ai-agents/media-video.md +1 -1
  70. package/framework/standards/ai-agents/middleware-patterns.md +1 -1
  71. package/framework/standards/ai-agents/modalities-image-gen.md +1 -1
  72. package/framework/standards/ai-agents/modalities-speech-to-text.md +1 -1
  73. package/framework/standards/ai-agents/modalities-text-to-speech.md +1 -1
  74. package/framework/standards/ai-agents/modalities-vision.md +1 -1
  75. package/framework/standards/ai-agents/multi-agent-patterns.md +1 -1
  76. package/framework/standards/ai-agents/observability-patterns.md +3 -3
  77. package/framework/standards/ai-agents/production.md +2 -2
  78. package/framework/standards/ai-agents/prompt-sources.md +1 -1
  79. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  80. package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -1
  81. package/framework/standards/ai-agents/service-tier-flex.md +1 -1
  82. package/framework/standards/ai-agents/setup.md +1 -1
  83. package/framework/standards/ai-agents/structured-output.md +1 -1
  84. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  85. package/framework/standards/ai-agents/testing-ai.md +1 -1
  86. package/framework/standards/ai-agents/tool-adapters.md +1 -1
  87. package/framework/standards/ai-agents/vector-data-extensions.md +1 -1
  88. package/framework/standards/ai-agents/workflows.md +1 -1
  89. package/framework/templates/CLAUDE.md.core.template +1 -1
  90. package/framework/templates/IDE-SNIPPETS.md +262 -262
  91. package/framework/templates/README.md +1 -1
  92. package/framework/templates/REGISTRY.json +0 -81
  93. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  94. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  95. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  96. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  97. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  98. package/framework/templates/docs/spec.md +19 -205
  99. package/framework/templates/docs/ui-components.md +52 -52
  100. package/framework/templates/docs/ui-design-system.md +76 -76
  101. package/framework/templates/docs/ui-flows.md +167 -167
  102. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  103. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  104. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  105. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  106. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  107. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  108. package/framework/templates/infrastructure/github/README.md +56 -56
  109. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  110. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  111. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  112. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  113. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  114. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  115. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  116. package/framework/templates/state.template.json +17 -17
  117. package/framework/templates/ui/design-system.css +226 -226
  118. package/package.json +2 -2
  119. package/src/commands/e2e/e2e.js +1 -1
  120. package/src/commands/project/heavy.js +1 -1
  121. package/src/commands/project/status.js +3 -3
  122. package/src/commands/state/approve.js +7 -49
  123. package/src/commands/state/gate-check.js +16 -106
  124. package/src/commands/state/gate-decision.js +1 -1
  125. package/src/commands/state/report-check.js +42 -17
  126. package/src/commands/validation/mutate.js +1 -1
  127. package/src/commands/validation/scout-check.js +28 -0
  128. package/src/commands/validation/validate-feature.js +5 -32
  129. package/src/commands/validation/verify.js +3 -4
  130. package/src/core/paths/output-schema.js +0 -2
  131. package/src/core/state/state-manager.js +1 -2
  132. package/src/core/templates/template-renderer.js +477 -477
  133. package/src/dashboard/app/lib/format.js +5 -0
  134. package/src/dashboard/app/main.jsx +13 -13
  135. package/src/dashboard/app/pages/FeatureDetail.jsx +16 -22
  136. package/src/dashboard/app/pages/Features.jsx +7 -7
  137. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  138. package/src/dashboard/app/pages/Rubrics.jsx +7 -11
  139. package/src/dashboard/scanner/docs-scanner.js +1 -2
  140. package/src/dashboard/scanner/features-scanner.js +3 -4
  141. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  142. package/src/dashboard/vite.config.js +24 -24
  143. package/src/lib/agents/output-collector.js +0 -1
  144. package/src/lib/detectors/design-system-detector.js +188 -188
  145. package/src/lib/evaluator-report.js +52 -334
  146. package/src/lib/generators/design-system-generator.js +508 -508
  147. package/src/lib/generators/handoff-generator.js +4 -5
  148. package/src/lib/installers/memory-installer.js +2 -2
  149. package/src/lib/mutation/manifest.js +8 -2
  150. package/src/lib/mutation/report.js +5 -3
  151. package/src/lib/mutation/runner.js +9 -7
  152. package/src/lib/risk-detector.js +60 -122
  153. package/src/lib/scout/scout-check.js +293 -0
  154. package/src/lib/tasks/task-classifier.js +30 -30
  155. package/src/lib/tasks/task-parser.js +209 -209
  156. package/src/lib/tasks/tasks-json-parser.js +7 -5
  157. package/src/lib/tasks/test-nodes.js +1 -1
  158. package/src/lib/tasks/test-runner.js +1 -1
  159. package/src/lib/validators/css/css-validator.js +340 -340
  160. package/src/lib/validators/reuse/reuse-validator.js +6 -8
  161. package/src/lib/validators/verify-runner.js +1 -1
  162. package/src/scripts/setup-infra.js +1 -1
  163. package/src/utils/agent-id-mapper.js +11 -11
  164. package/src/utils/agents-installer.js +5 -9
  165. package/src/utils/claude-settings-manager.js +300 -300
  166. package/src/utils/hooks-installer.js +3 -10
  167. package/src/utils/skills-installer.js +1 -1
  168. package/framework/evals/proof-rubrics/README.md +0 -76
  169. package/framework/evals/proof-rubrics/eval-de-modelo.md +0 -80
  170. package/framework/evals/proof-rubrics/fiacao.md +0 -54
  171. package/framework/evals/proof-rubrics/guard-cego.md +0 -52
  172. package/framework/evals/proof-rubrics/media-claims.md +0 -109
  173. package/framework/evals/proof-rubrics/mutation-proof.md +0 -107
  174. package/framework/evals/proof-rubrics/observabilidade-genai.md +0 -119
  175. package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +0 -48
  176. package/framework/evals/rubrics/README.md +0 -45
  177. package/framework/evals/rubrics/architecture.md +0 -103
  178. package/framework/evals/rubrics/code-quality.md +0 -98
  179. package/framework/evals/rubrics/condensed.md +0 -57
  180. package/framework/evals/rubrics/contracts.md +0 -67
  181. package/framework/evals/rubrics/test-coverage.md +0 -99
  182. package/framework/evals/scenarios/dotnet-crud.yaml +0 -42
  183. package/framework/evals/scenarios/nextjs-auth.yaml +0 -38
  184. package/framework/evals/suites/standard.yaml +0 -5
  185. package/framework/hooks/claude-code/core/post-edit-typecheck.js +0 -127
  186. package/framework/skills/core/morph-eval/SKILL.md +0 -64
  187. package/framework/templates/.idea/morph-templates.xml +0 -92
  188. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
  189. package/framework/templates/docs/clarifications.md +0 -253
  190. package/framework/templates/docs/onboarding.md +0 -122
  191. package/framework/templates/docs/schema-analysis.md +0 -119
  192. package/framework/templates/docs/user-stories.md +0 -34
  193. package/src/commands/state/score.js +0 -147
@@ -1,373 +1,91 @@
1
- /**
2
- * Leitura do `4-review/evaluator-report.md` — o relatório BRUTO do avaliador
3
- * independente despachado pela `morph-review` no Gate 3.
4
- *
5
- * Existe como módulo próprio porque DOIS comandos dependem do mesmo arquivo por
6
- * razões diferentes, e antes o liam de maneiras diferentes:
7
- *
8
- * - `gate-check` lê o token `**Composite-Score:**` para saber o score REAL da
9
- * feature (em vez do proxy de médias de auto-avaliação);
10
- * - `approve` exige o arquivo como precondição do Gate 3.
11
- *
12
- * A divergência era concreta: `approve.js` montava o path à mão com `join(...)`
13
- * enquanto `gate-check.js` resolvia por `getAbsoluteOutputPath`. Dois caminhos
14
- * para o mesmo arquivo é a receita de drift silencioso (a lição A5), e a
15
- * checagem por `existsSync` puro do `approve` aceitava um relatório SEM o token
16
- * que o `gate-check` então descartava — o Gate 3 passava carimbado `auto` sobre
17
- * uma média de auto-avaliação, exatamente o que MORPH.md §5 proíbe.
18
- *
19
- * ── O TOKEN VIGENTE É O ÚLTIMO ──────────────────────────────────────────────
20
- *
21
- * O `evaluator-report.md` é APPEND-ONLY por contrato: uma reavaliação acrescenta
22
- * ao fim, preservando a análise anterior. Este módulo lia o COMEÇO (`String.match`
23
- * com regex sem flag `g`), então num relatório reavaliado o gate era decidido
24
- * pelo score SUPERADO. Medido em campo três vezes na mesma epic — #90 (8.7 →
25
- * 9.4), #92 (8.2 → 9.0) e #93 (8.9 → 8.3 → 9.1) — e nas três o avaliador teve de
26
- * desarmar o token antigo à mão para o gate ler a nota vigente.
27
- *
28
- * O caso perigoso é o INVERSO e é silencioso: numa reavaliação que BAIXA a nota
29
- * (o #93 caiu de 8.9 para 8.3 quando uma correção abriu um bloqueante), o gate
30
- * leria a nota antiga, mais alta, e AUTO-PASSARIA o Gate 3 sobre um produto com
31
- * bloqueante aberto — a mesma classe de defeito que a v8.26.0 existiu para
32
- * fechar, voltando por outra porta: lendo um veredito real, porém vencido.
33
- *
34
- * DECISÃO EXPLÍCITA — "o último vence", e não "múltiplos tokens viram
35
- * `report-ambiguous` e recusam":
36
- *
37
- * 1. É a semântica do próprio artefato. Append-only significa que o arquivo é
38
- * um LOG, e o valor corrente de um log é a sua última entrada. Ler o
39
- * primeiro não é uma política conservadora — é ler a entrada errada.
40
- * 2. Recusar transformaria um veredito REAL e CORRENTE numa pausa que só sai
41
- * editando à mão um arquivo declarado append-only. Isto é exatamente o
42
- * contorno manual que este conserto existe para eliminar, promovido a
43
- * requisito.
44
- * 3. Recusar não é mais seguro em nenhum dos dois casos observados: com a nota
45
- * SUBINDO, o humano é obrigado a mexer no relatório para liberar um gate
46
- * que já foi aprovado; com a nota CAINDO, "o último vence" já pausa sozinho
47
- * pela regra de score, que é a razão certa a reportar.
48
- *
49
- * Ambiguidade não fica silenciosa, ainda assim: `evaluatorReportState` devolve
50
- * `scoreTokens` (quantos tokens vivos o arquivo tem) e `superseded` (os valores
51
- * anteriores, na ordem do arquivo). Quem consome pode dizer "9.1, terceira
52
- * rodada, superando 8.9 e 8.3" em vez de fingir que só havia um veredito.
53
- */
54
-
55
1
  import { existsSync, readFileSync } from 'fs';
56
2
  import { getAbsoluteOutputPath } from '../core/paths/output-schema.js';
57
3
 
58
- /**
59
- * O token machine-readable que o avaliador grava por contrato (`morph-review`
60
- * §4). Espelho de `REVIEWED_SHA_TOKEN` do approve.js — mesma ideia: um relatório
61
- * de LLM só é auditável por máquina se carregar um campo que a máquina lê.
62
- */
63
- export const COMPOSITE_SCORE_TOKEN = '**Composite-Score:**';
64
-
65
- /**
66
- * O token das rubricas de PROVA (`.morph/framework/evals/proof-rubrics/`):
67
- * `**Proof-Scores:** mutation=9 verde=8 fiacao=9 guard=7 eval=8`. Mesma convenção
68
- * literal do Composite-Score de propósito — mesmo arquivo, mesmo módulo leitor,
69
- * mesmo estilo de token. Ele pontua a EVIDÊNCIA que o avaliador produziu, nunca
70
- * o produto: não entra no composto, impõe teto em `test-coverage`.
71
- */
72
- export const PROOF_SCORES_TOKEN = '**Proof-Scores:**';
73
-
74
- /** `g` é obrigatório: o arquivo é append-only e o token vigente é o ÚLTIMO. */
75
- const COMPOSITE_SCORE_RE = /\*\*Composite-Score:\*\*\s*([0-9]+(?:\.[0-9]+)?)/g;
76
-
77
- /** Idem — uma reavaliação reescreve os Proof-Scores acrescentando ao fim. */
78
- const PROOF_SCORES_RE = /\*\*Proof-Scores:\*\*[ \t]*([^\r\n]*)/g;
4
+ export const VERDICT_TOKEN = '**Verdict:**';
5
+ export const VERDICTS = ['SHIP', 'FIX'];
6
+ export const BLOCKER_TYPES = ['uc', 'teste', 'regressao', 'seguranca'];
79
7
 
80
- /**
81
- * A seção de prova de mutação. Exige `##`: um `# Mutações` é outra estrutura
82
- * (H1 é título de documento, não seção), e o template que a `morph-review`
83
- * entrega ao avaliador gera exatamente `## Mutações`. Aceita a grafia sem
84
- * acento porque um relatório escrito em terminal ASCII continua sendo o mesmo
85
- * fato — o que não se aceita é o nível errado de cabeçalho.
86
- */
87
- const MUTATION_SECTION_RE = /^##\s+Muta[çc][õo]es\s*$/m;
88
-
89
- /**
90
- * A seção de prova de MÍDIA. Mesma disciplina do `## Mutações`: exige `##`, e
91
- * aceita a grafia sem acento porque um relatório escrito em terminal ASCII
92
- * continua sendo o mesmo fato. `Mídia gerada` / `Midia gerada`.
93
- */
8
+ // O relatório é append-only: uma reavaliação acrescenta ao fim, e o token vigente é o ÚLTIMO.
9
+ const VERDICT_RE = /\*\*Verdict:\*\*\s*([A-Za-z]+)/g;
10
+ const BLOCKERS_SECTION_RE = /^##\s+Bloqueadores\s*$/gim;
94
11
  const MEDIA_SECTION_RE = /^##\s+M[íi]dia\s+gerada\s*$/m;
95
-
96
- /**
97
- * A seção em que o avaliador confere o checklist de tela do mockup contra a
98
- * tela entregue (`morph-review` §4). Mesma disciplina: exige `##`.
99
- */
100
12
  const SCREEN_CHECKLIST_SECTION_RE = /^##\s+Checklist\s+de\s+tela\s*$/gim;
101
13
 
102
- /**
103
- * As chaves reconhecidas de `**Proof-Scores:**`, uma por rubrica de prova.
104
- * Chave fora desta lista é IGNORADA (não vira score, não vira erro): o relatório
105
- * é texto de LLM e uma chave inventada não deve nem contaminar nenhum mínimo nem
106
- * derrubar a leitura das outras.
107
- *
108
- * `media` PRECISA estar aqui. Sem isso a rubrica `media-claims.md` seria
109
- * decorativa: o avaliador escreveria `media=7` e o número seria descartado em
110
- * silêncio, que é o caso "campo publicado sem nenhum leitor".
111
- */
112
- export const PROOF_SCORE_KEYS = ['mutation', 'verde', 'fiacao', 'guard', 'eval', 'media', 'obs'];
113
-
114
- /**
115
- * As chaves que NÃO entram no `min` de `test-coverage` porque impõem teto em
116
- * OUTRA dimensão: `media` (teto em `architecture`) e `obs` (teto em
117
- * `code-quality`).
118
- *
119
- * **`obs` chegou exatamente pelo caminho que o parágrafo abaixo antecipou**, e
120
- * vale registrar porque o aviso não bastou. A feature irmã (#94) criou a rubrica
121
- * `observabilidade-genai.md`, escreveu `obs=` onze vezes no seu próprio
122
- * `evaluator-report.md` — e a chave não estava em `PROOF_SCORE_KEYS`, nem lá nem
123
- * na base dela. O parser a descartava em silêncio (ver o `continue` de
124
- * `parseProofScores`), então a rubrica nasceu DECORATIVA e atravessou **onze
125
- * rodadas de Gate 3** sem que ninguém percebesse: o número era escrito, lido por
126
- * um humano no relatório, e nunca chegava ao gate. Só o rebase, que põe as duas
127
- * features na mesma árvore, tornou o buraco visível.
128
- *
129
- * **Isto é uma exclusão, e a forma importa mais que a lista.** A primeira versão
130
- * era o inverso — uma lista fechada das quatro chaves que entram. Ela responde
131
- * certo hoje e erra sozinha amanhã: a feature irmã acrescenta uma quinta chave
132
- * (`eval`) com rubrica própria, e depois do rebase esse `eval=N` cairia do teto
133
- * **em silêncio** — o "campo publicado sem nenhum leitor" que este mesmo arquivo
134
- * existe para impedir, produzido pela correção.
135
- *
136
- * Por exclusão, uma chave nova nasce DENTRO do mínimo por padrão, e tirá-la de lá
137
- * passa a ser um ato deliberado e visível: uma linha aqui, com a dimensão que ela
138
- * limita escrita ao lado. Errar para o lado de "conta no mínimo" custa um teto a
139
- * mais; errar para o outro lado devolve o gate cego.
140
- *
141
- * O motivo de `media` estar fora continua o mesmo: as quatro do mínimo perguntam
142
- * "a evidência é real?"; `media` pergunta "a chamada mora no lugar certo? que
143
- * estados existem? o que é publicado?". Um `media=4` derrubando o teto de TESTE
144
- * seriam dois fatos diferentes lidos como um só.
145
- */
146
- export const NON_MUTATION_PROOF_KEYS = ['media', 'obs'];
147
-
148
- /**
149
- * As chaves que entram no `min` que impõe teto em `test-coverage`: DERIVADA de
150
- * {@link PROOF_SCORE_KEYS} menos {@link NON_MUTATION_PROOF_KEYS} — nunca digitada.
151
- * Ver o racional da exclusão acima.
152
- */
153
- export const MUTATION_PROOF_KEYS =
154
- PROOF_SCORE_KEYS.filter((k) => !NON_MUTATION_PROOF_KEYS.includes(k));
155
-
156
- /** Path canônico do evaluator-report da feature. Fonte única para os dois comandos. */
157
14
  export function evaluatorReportPath(cwd, featureName) {
158
15
  return getAbsoluteOutputPath(cwd, featureName, 'evaluatorReport');
159
16
  }
160
17
 
161
- /** Conteúdo do relatório, ou null quando ausente/ilegível. Nunca lança. */
162
18
  function readReport(cwd, featureName) {
163
19
  try {
164
20
  const p = evaluatorReportPath(cwd, featureName);
165
- if (!existsSync(p)) return null;
166
- return readFileSync(p, 'utf8');
21
+ return existsSync(p) ? readFileSync(p, 'utf8') : null;
167
22
  } catch {
168
23
  return null;
169
24
  }
170
25
  }
171
26
 
172
- /**
173
- * Todos os valores de `**Composite-Score:**` do arquivo, NA ORDEM DO ARQUIVO.
174
- * O último é o vigente; os anteriores são rodadas superadas.
175
- *
176
- * @param {string} text
177
- * @returns {number[]}
178
- */
179
- function compositeScoresIn(text) {
180
- return [...text.matchAll(COMPOSITE_SCORE_RE)].map((m) => Number(m[1]));
27
+ function lastSectionBody(text, re) {
28
+ const matches = [...text.matchAll(re)];
29
+ if (matches.length === 0) return null;
30
+ const last = matches[matches.length - 1];
31
+ const rest = text.slice(last.index + last[0].length);
32
+ const next = rest.search(/^#{1,2}\s/m);
33
+ return next === -1 ? rest : rest.slice(0, next);
181
34
  }
182
35
 
183
- /**
184
- * Score composto REAL da feature, do token gravado pelo avaliador independente.
185
- *
186
- * Num relatório reavaliado (append-only), é o token do FIM — o veredito
187
- * vigente. Ver o bloco "O TOKEN VIGENTE É O ÚLTIMO" no topo do arquivo.
188
- *
189
- * @returns {number|undefined} undefined quando o relatório não existe, não é
190
- * legível, ou existe sem o token (relatório legado ou oco). `undefined` é
191
- * deliberado e não 0: quem chama distingue "sem veredito" de "veredito ruim".
192
- */
193
- export function evaluatorCompositeScore(cwd, featureName) {
194
- const text = readReport(cwd, featureName);
195
- if (text === null) return undefined;
196
- const scores = compositeScoresIn(text);
197
- return scores.length === 0 ? undefined : scores[scores.length - 1];
198
- }
36
+ const normalizeType = (s) => s.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').trim();
199
37
 
200
38
  /**
201
- * Estado do relatório para a precondição do Gate 3.
202
- *
203
- * Três valores em vez de booleano PORQUE AS TRÊS SITUAÇÕES PEDEM MENSAGENS
204
- * DIFERENTES: arquivo ausente é "despache o avaliador"; arquivo sem token é
205
- * "o avaliador rodou mas o relatório está oco — ele não seguiu o contrato";
206
- * e só o terceiro caso é aprovável.
207
- *
208
- * `scoreTokens`/`superseded` só aparecem no caso `ok` e existem para que uma
209
- * reavaliação nunca passe silenciosa: o score é o último, mas quem consome sabe
210
- * quantas rodadas o arquivo carrega e quais notas foram superadas.
211
- *
212
- * @returns {{ status: 'ok'|'missing'|'untokenized', score?: number,
213
- * scoreTokens?: number, superseded?: number[], path: string }}
39
+ * Linhas de tabela da última `## Bloqueadores`: `| id | tipo | onde | evidência |`.
40
+ * Cabeçalho e separador não contam; `untyped` são as linhas cujo tipo não é de {@link BLOCKER_TYPES}.
214
41
  */
215
- export function evaluatorReportState(cwd, featureName) {
216
- const path = evaluatorReportPath(cwd, featureName);
217
- const text = readReport(cwd, featureName);
218
- if (text === null) return { status: 'missing', path };
219
- const scores = compositeScoresIn(text);
220
- if (scores.length === 0) return { status: 'untokenized', path };
221
- return {
222
- status: 'ok',
223
- score: scores[scores.length - 1],
224
- scoreTokens: scores.length,
225
- superseded: scores.slice(0, -1),
226
- path,
227
- };
228
- }
229
-
230
- /**
231
- * Parseia o corpo de UMA linha `**Proof-Scores:**` — pares `chave=N` separados
232
- * por espaço.
233
- *
234
- * @param {string} body
235
- * @returns {Record<string, number>|undefined} undefined quando nenhum par
236
- * VÁLIDO foi lido (linha vazia, só chaves desconhecidas, ou só valores
237
- * não-numéricos). `undefined` e não `{}` pela mesma disciplina do
238
- * `evaluatorCompositeScore`: "sem veredito" tem de ser distinguível de
239
- * "veredito vazio".
240
- */
241
- function parseProofScores(body) {
242
- const out = {};
243
- for (const m of body.matchAll(/([A-Za-zÀ-ÿ][\w-]*)\s*=\s*([^\s]+)/g)) {
244
- const key = m[1].toLowerCase();
245
- if (!PROOF_SCORE_KEYS.includes(key)) continue;
246
- if (!/^[0-9]+(?:\.[0-9]+)?$/.test(m[2])) continue;
247
- out[key] = Number(m[2]);
42
+ function parseBlockers(body) {
43
+ const blockers = [];
44
+ const untyped = [];
45
+ for (const line of body.split(/\r?\n/)) {
46
+ if (!/^\s*\|/.test(line)) continue;
47
+ const cells = line.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
48
+ if (cells.every((c) => /^:?-{2,}:?$/.test(c) || c === '')) continue;
49
+ if (/^#$|^id$/i.test(cells[0]) && /^tipo$/i.test(cells[1] ?? '')) continue;
50
+ const row = { id: cells[0], type: normalizeType(cells[1] ?? ''), where: cells[2] ?? '', evidence: cells[3] ?? '' };
51
+ (BLOCKER_TYPES.includes(row.type) ? blockers : untyped).push(row);
248
52
  }
249
- return Object.keys(out).length === 0 ? undefined : out;
53
+ return { blockers, untyped };
250
54
  }
251
55
 
252
- /**
253
- * Estado de PROVA do relatório: a seção `## Mutações` está lá, e quais
254
- * Proof-Scores o avaliador registrou.
255
- *
256
- * Deriva do disco e não julga: quem decide se a ausência pausa o Gate 3 é
257
- * `risk-detector.js` (a divisão "gate-check coleta, risk-detector decide"), e
258
- * quem decide o teto é a rubrica `test-coverage.md`. O `min` NÃO é gravado
259
- * aqui — é derivado por quem consome, para que exista um só lugar onde
260
- * "mínimo de quantas chaves" é definido.
261
- *
262
- * Arquivo ausente ou ilegível: `{ mutationSection: false, mediaSection: false,
263
- * proofScores: undefined }`. Nunca lança, e nunca devolve `0` no lugar de
264
- * `undefined`.
265
- *
266
- * `mediaSection` é lido do MESMO arquivo pelo MESMO módulo, de propósito: uma
267
- * segunda leitura do relatório noutro lugar é como duas superfícies passam a
268
- * discordar sobre o que ele diz.
269
- *
270
- * @param {string} cwd
271
- * @param {string} featureName
272
- * @returns {{ mutationSection: boolean, mediaSection: boolean, proofScores: Record<string, number>|undefined, path: string }}
273
- */
274
- export function evaluatorProofState(cwd, featureName) {
275
- const path = evaluatorReportPath(cwd, featureName);
276
- const text = readReport(cwd, featureName);
277
- if (text === null) {
278
- return { mutationSection: false, mediaSection: false, proofScores: undefined, path };
279
- }
280
-
281
- // Append-only outra vez: numa reavaliação a linha nova vai para o fim, e é
282
- // ela que vale. A primeira linha descreveria a prova de uma rodada superada.
283
- const lines = [...text.matchAll(PROOF_SCORES_RE)];
284
- const proofScores = lines.length === 0
285
- ? undefined
286
- : parseProofScores(lines[lines.length - 1][1]);
287
-
56
+ export function evaluatorVerdictFromText(text) {
57
+ if (typeof text !== 'string') return { verdict: undefined, blockers: [], untyped: [], superseded: [] };
58
+ const verdicts = [...text.matchAll(VERDICT_RE)].map((m) => m[1].toUpperCase());
59
+ const body = lastSectionBody(text, BLOCKERS_SECTION_RE);
60
+ const { blockers, untyped } = body === null ? { blockers: [], untyped: [] } : parseBlockers(body);
288
61
  return {
289
- mutationSection: MUTATION_SECTION_RE.test(text),
290
- mediaSection: MEDIA_SECTION_RE.test(text),
291
- proofScores,
292
- path,
62
+ verdict: verdicts.length ? verdicts[verdicts.length - 1] : undefined,
63
+ blockers,
64
+ untyped,
65
+ superseded: verdicts.slice(0, -1),
293
66
  };
294
67
  }
295
68
 
296
69
  /**
297
- * A seção `## Checklist de tela` do relatório, com o corpo da ÚLTIMA ocorrência
298
- * — append-only outra vez: numa reavaliação a conferência nova vai para o fim.
299
- * O corpo vai cru para `checklistCoverage` (`src/lib/specs/screen-checklist.js`),
300
- * dono único do parser de linha do checklist.
301
- *
302
- * @returns {{ section: boolean, body: string }} arquivo ausente ou sem a seção:
303
- * `{ section: false, body: '' }`. Nunca lança.
70
+ * `missing`: sem relatório. `unverdicted`: relatório sem `**Verdict:** SHIP|FIX` válido.
71
+ * `ok`: veredito legível — quem decide se ele libera o ship é o risk-detector.
304
72
  */
305
- export function evaluatorScreenChecklist(cwd, featureName) {
73
+ export function evaluatorReportState(cwd, featureName) {
74
+ const path = evaluatorReportPath(cwd, featureName);
306
75
  const text = readReport(cwd, featureName);
307
- const matches = text === null ? [] : [...text.matchAll(SCREEN_CHECKLIST_SECTION_RE)];
308
- if (matches.length === 0) return { section: false, body: '' };
309
- const last = matches[matches.length - 1];
310
- const rest = text.slice(last.index + last[0].length);
311
- const next = rest.search(/^#{1,2}\s/m);
312
- return { section: true, body: next === -1 ? rest : rest.slice(0, next) };
313
- }
314
-
315
- /**
316
- * O menor Proof-Score das provas de EVIDÊNCIA — o que impõe o teto em
317
- * `test-coverage`.
318
- *
319
- * Calculado sobre {@link MUTATION_PROOF_KEYS} — que é
320
- * {@link PROOF_SCORE_KEYS} MENOS {@link NON_MUTATION_PROOF_KEYS}, e não uma lista
321
- * digitada. `media` responde a outra pergunta e limita outra dimensão
322
- * (`architecture`); ler as duas famílias como um mínimo só faria um `media=4`
323
- * derrubar o teto de teste sem que nada de teste tivesse acontecido. Uma chave
324
- * NOVA de prova entra aqui sozinha: excluí-la exige uma linha deliberada em
325
- * `NON_MUTATION_PROOF_KEYS`, e nunca acontece por esquecimento.
326
- *
327
- * @param {Record<string, number>|undefined} proofScores
328
- * @returns {number|null} null quando não há nenhum score do mínimo: SEM TETO A
329
- * APLICAR, jamais "teto 0". A mesma razão pela qual `count: null` do verify
330
- * não é 0.
331
- */
332
- export function minProofScore(proofScores) {
333
- if (!proofScores) return null;
334
- const values = MUTATION_PROOF_KEYS
335
- .map((key) => proofScores[key])
336
- .filter((n) => typeof n === 'number' && Number.isFinite(n));
337
- return values.length === 0 ? null : Math.min(...values);
76
+ if (text === null) return { status: 'missing', path };
77
+ const parsed = evaluatorVerdictFromText(text);
78
+ if (!VERDICTS.includes(parsed.verdict)) return { status: 'unverdicted', path };
79
+ return { status: 'ok', path, ...parsed };
338
80
  }
339
81
 
340
- /**
341
- * O Proof-Score de MÍDIA, lido à parte. Impõe teto em `architecture` (tabela em
342
- * `framework/evals/rubrics/architecture.md`, "Tetos de prova de mídia").
343
- *
344
- * @param {Record<string, number>|undefined} proofScores
345
- * @returns {number|null} null quando a chave não foi registrada — o caso normal
346
- * de uma feature que não gera mídia, e nunca "teto 0".
347
- */
348
- export function mediaProofScore(proofScores) {
349
- const value = proofScores?.media;
350
- return typeof value === 'number' && Number.isFinite(value) ? value : null;
82
+ export function evaluatorMediaSection(cwd, featureName) {
83
+ const text = readReport(cwd, featureName);
84
+ return text !== null && MEDIA_SECTION_RE.test(text);
351
85
  }
352
86
 
353
- /**
354
- * Os dois vereditos do relatório lidos de um TEXTO já em mãos — o score
355
- * composto vigente e os Proof-Scores vigentes, pela mesma regra append-only
356
- * (o último token vale) dos leitores por `cwd` acima.
357
- *
358
- * Existe para quem tem o relatório fora do caminho canônico — o dashboard lê
359
- * features arquivadas em `.morph/archive/{f}/4-review/` —, sem que ele
360
- * precise de uma segunda cópia dos regexes.
361
- *
362
- * @param {string} text
363
- * @returns {{ score: number|undefined, proofScores: Record<string, number>|undefined }}
364
- */
365
- export function evaluatorVerdictFromText(text) {
366
- if (typeof text !== 'string') return { score: undefined, proofScores: undefined };
367
- const scores = compositeScoresIn(text);
368
- const lines = [...text.matchAll(PROOF_SCORES_RE)];
369
- return {
370
- score: scores.length === 0 ? undefined : scores[scores.length - 1],
371
- proofScores: lines.length === 0 ? undefined : parseProofScores(lines[lines.length - 1][1]),
372
- };
87
+ export function evaluatorScreenChecklist(cwd, featureName) {
88
+ const text = readReport(cwd, featureName);
89
+ const body = text === null ? null : lastSectionBody(text, SCREEN_CHECKLIST_SECTION_RE);
90
+ return body === null ? { section: false, body: '' } : { section: true, body };
373
91
  }