@polymorphism-tech/morph-spec 8.46.0 → 9.0.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 (304) hide show
  1. package/CLAUDE.md +53 -44
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +38 -55
  4. package/bin/validate.js +5 -134
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/create-umbraco-lp.md +2 -0
  12. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  13. package/framework/commands/morph-apply.md +5 -28
  14. package/framework/commands/morph-archive.md +2 -2
  15. package/framework/commands/morph-hotfix.md +8 -8
  16. package/framework/commands/morph-infra.md +17 -203
  17. package/framework/commands/morph-media.md +3 -0
  18. package/framework/commands/morph-preflight.md +7 -31
  19. package/framework/commands/morph-proposal.md +2 -50
  20. package/framework/commands/morph-status.md +3 -3
  21. package/framework/commands/morph-troubleshoot.md +2 -14
  22. package/framework/docs/CLI.md +585 -286
  23. package/framework/evals/review-checklist.md +82 -41
  24. package/framework/hooks/README.md +10 -11
  25. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  26. package/framework/hooks/claude-code/core/denylist.json +33 -3
  27. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  28. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  30. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  31. package/framework/hooks/claude-code/core/state-sync.js +17 -19
  32. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  33. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  34. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  35. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  36. package/framework/hooks/claude-code/statusline.py +55 -41
  37. package/framework/hooks/package.json +3 -0
  38. package/framework/hooks/shared/activity-logger.js +1 -1
  39. package/framework/hooks/shared/build-check.js +10 -6
  40. package/framework/hooks/shared/command-tokens.js +169 -0
  41. package/framework/hooks/shared/comment-rules.js +1 -1
  42. package/framework/hooks/shared/comment-scanner.js +10 -23
  43. package/framework/hooks/shared/comment-stripper.js +1 -1
  44. package/framework/hooks/shared/e2e-check.js +67 -31
  45. package/framework/hooks/shared/errata.js +72 -0
  46. package/framework/hooks/shared/feature-locator.js +1 -1
  47. package/framework/hooks/shared/frontend-locations.js +6 -0
  48. package/framework/hooks/shared/gate-question.js +18 -0
  49. package/framework/hooks/shared/phase-utils.js +15 -32
  50. package/framework/hooks/shared/slice-progress.js +118 -0
  51. package/framework/hooks/shared/spawn-command.js +23 -0
  52. package/framework/hooks/shared/state-reader.js +61 -87
  53. package/framework/hooks/shared/state-writer.js +3 -3
  54. package/framework/mcp/registry.json +3 -4
  55. package/framework/phases.json +23 -39
  56. package/framework/rules/ai-agents-standards.md +18 -16
  57. package/framework/rules/asaas-standards.md +3 -1
  58. package/framework/rules/csharp-standards.md +10 -8
  59. package/framework/rules/data-standards.md +5 -5
  60. package/framework/rules/dotnet-templates.md +1 -1
  61. package/framework/rules/frontend-standards.md +4 -2
  62. package/framework/rules/gsap-standards.md +10 -5
  63. package/framework/rules/hangfire-standards.md +3 -1
  64. package/framework/rules/infrastructure-standards.md +4 -2
  65. package/framework/rules/neon-auth-standards.md +3 -1
  66. package/framework/rules/neon-standards.md +6 -4
  67. package/framework/rules/nextjs-standards.md +13 -11
  68. package/framework/rules/nextjs-templates.md +1 -1
  69. package/framework/rules/resend-standards.md +3 -1
  70. package/framework/schemas/config.schema.json +14 -9
  71. package/framework/schemas/feature.schema.json +15 -52
  72. package/framework/skills/README.md +4 -5
  73. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  74. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  75. package/framework/skills/core/morph-apply/SKILL.md +150 -223
  76. package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
  77. package/framework/skills/core/morph-machine/SKILL.md +4 -4
  78. package/framework/skills/core/morph-media/SKILL.md +3 -3
  79. package/framework/skills/core/morph-plan/SKILL.md +138 -300
  80. package/framework/skills/core/morph-review/SKILL.md +112 -137
  81. package/framework/skills/core/morph-standards/SKILL.md +29 -41
  82. package/framework/skills/core/morph-uiux/SKILL.md +78 -153
  83. package/framework/standards/STANDARDS.json +90 -1219
  84. package/framework/standards/STANDARDS.meta.json +1305 -0
  85. package/framework/standards/ai-agents/evals-with-cache.md +2 -3
  86. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  87. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  88. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  89. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  90. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  91. package/framework/standards/ai-agents/setup.md +2 -2
  92. package/framework/standards/ai-agents/structured-output.md +1 -1
  93. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  94. package/framework/standards/architecture/code-comments.md +8 -8
  95. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  96. package/framework/standards/backend/api/validation.md +135 -47
  97. package/framework/standards/backend/database/postgresql/database.md +11 -7
  98. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  99. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  100. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  101. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  102. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  103. package/framework/standards/backend/dotnet/testing.md +1 -1
  104. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  105. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  106. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  107. package/framework/standards/data/nosql/cache/redis.md +120 -44
  108. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  109. package/framework/standards/frontend/design-system/animations.md +1 -1
  110. package/framework/standards/frontend/design-system/naming.md +1 -1
  111. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  112. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  113. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  114. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  115. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  116. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  117. package/framework/templates/README.md +2 -3
  118. package/framework/templates/REGISTRY.json +6 -170
  119. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  120. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  121. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  122. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  123. package/framework/templates/docs/plan.md +72 -0
  124. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  127. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  128. package/framework/templates/infrastructure/github/README.md +2 -1
  129. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  130. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  131. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  132. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  133. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  134. package/package.json +1 -1
  135. package/src/commands/e2e/e2e.js +2 -2
  136. package/src/commands/observability/cost.js +18 -60
  137. package/src/commands/observability/telemetry.js +3 -5
  138. package/src/commands/project/doctor-procs.js +1 -1
  139. package/src/commands/project/doctor.js +9 -9
  140. package/src/commands/project/fleet.js +4 -4
  141. package/src/commands/project/heavy.js +3 -3
  142. package/src/commands/project/init.js +6 -1
  143. package/src/commands/project/setup-infra-cmd.js +2 -1
  144. package/src/commands/project/stack-scan.js +58 -0
  145. package/src/commands/project/standards.js +79 -25
  146. package/src/commands/project/status.js +29 -69
  147. package/src/commands/project/update.js +33 -19
  148. package/src/commands/project/worktree.js +1 -1
  149. package/src/commands/state/advance.js +62 -46
  150. package/src/commands/state/approve.js +95 -138
  151. package/src/commands/state/archive.js +20 -84
  152. package/src/commands/state/auto-approval.js +68 -0
  153. package/src/commands/state/create.js +28 -1
  154. package/src/commands/state/delete.js +1 -1
  155. package/src/commands/state/finish.js +11 -23
  156. package/src/commands/state/gate-check.js +86 -95
  157. package/src/commands/state/gate-decision.js +6 -4
  158. package/src/commands/state/handoff.js +1 -1
  159. package/src/commands/state/list.js +9 -9
  160. package/src/commands/state/report-check.js +157 -311
  161. package/src/commands/state/retype.js +9 -3
  162. package/src/commands/validation/mutate.js +77 -14
  163. package/src/commands/validation/plan-check.js +81 -0
  164. package/src/commands/validation/validate-feature.js +60 -135
  165. package/src/commands/validation/verify.js +41 -82
  166. package/src/core/paths/output-schema.js +20 -67
  167. package/src/core/state/gate-order.js +5 -6
  168. package/src/core/state/lifecycle-audit.js +2 -2
  169. package/src/core/state/state-manager.js +17 -98
  170. package/src/core/templates/template-registry.js +2 -2
  171. package/src/core/templates/template-seed.js +22 -0
  172. package/src/core/work-types.js +10 -10
  173. package/src/dashboard/app/App.jsx +0 -2
  174. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  175. package/src/dashboard/app/components/CommandPalette.jsx +7 -8
  176. package/src/dashboard/app/components/EntityLink.jsx +1 -2
  177. package/src/dashboard/app/components/Markdown.jsx +1 -1
  178. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  179. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  180. package/src/dashboard/app/lib/phases.js +2 -4
  181. package/src/dashboard/app/pages/Activity.jsx +1 -1
  182. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  183. package/src/dashboard/app/pages/Features.jsx +9 -9
  184. package/src/dashboard/app/pages/Guide.jsx +2 -2
  185. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  186. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  187. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  188. package/src/dashboard/app/pages/Standards.jsx +1 -1
  189. package/src/dashboard/scanner/activity-scanner.js +6 -16
  190. package/src/dashboard/scanner/agents-scanner.js +1 -1
  191. package/src/dashboard/scanner/docs-scanner.js +5 -23
  192. package/src/dashboard/scanner/features-scanner.js +95 -60
  193. package/src/dashboard/scanner/index.js +0 -2
  194. package/src/dashboard/scanner/project-api.js +1 -1
  195. package/src/dashboard/scanner/rules-scanner.js +15 -10
  196. package/src/dashboard/scanner/skills-scanner.js +2 -3
  197. package/src/dashboard/scanner/standards-scanner.js +2 -1
  198. package/src/lib/ci/ci-verify.js +17 -1
  199. package/src/lib/ci/github-ci.js +1 -1
  200. package/src/lib/detectors/index.js +3 -8
  201. package/src/lib/generators/handoff-generator.js +34 -65
  202. package/src/lib/generators/settings-generator.js +11 -89
  203. package/src/lib/git/evidence-staleness.js +1 -1
  204. package/src/lib/git/tree-fingerprint.js +19 -7
  205. package/src/lib/git/tree-state.js +2 -2
  206. package/src/lib/heavy/cleanup.js +1 -5
  207. package/src/lib/installers/memory-installer.js +6 -8
  208. package/src/lib/mutation/commands.js +3 -2
  209. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  210. package/src/lib/mutation/plan.js +14 -6
  211. package/src/lib/mutation/runner.js +1 -1
  212. package/src/lib/mutation/subset.js +46 -9
  213. package/src/lib/plan/artifact-vocabulary.js +60 -0
  214. package/src/lib/plan/checks.js +234 -0
  215. package/src/lib/plan/plan-parser.js +209 -0
  216. package/src/lib/plan/plan-template.js +18 -0
  217. package/src/lib/review-reports.js +222 -0
  218. package/src/lib/risk-detector.js +134 -135
  219. package/src/lib/specs/adr-index.js +50 -69
  220. package/src/lib/specs/design-seed.js +18 -0
  221. package/src/lib/specs/frontmatter.js +44 -0
  222. package/src/lib/specs/gfm-table.js +17 -0
  223. package/src/lib/specs/old-format.js +68 -0
  224. package/src/lib/specs/revocations.js +37 -95
  225. package/src/lib/specs/screen-checklist.js +18 -17
  226. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  227. package/src/lib/stack/stack-profile.js +1 -1
  228. package/src/lib/stack/stack-scan.js +267 -0
  229. package/src/lib/standards/registry.js +43 -0
  230. package/src/lib/standards/sections.js +58 -0
  231. package/src/lib/tasks/eval-node.js +4 -12
  232. package/src/lib/tasks/test-nodes.js +1 -74
  233. package/src/lib/tasks/test-runner.js +75 -117
  234. package/src/lib/telemetry/metrics.js +8 -69
  235. package/src/lib/telemetry/telemetry-reader.js +5 -118
  236. package/src/lib/telemetry/verify-event.js +5 -7
  237. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  238. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  239. package/src/lib/validators/reuse/reuse-validator.js +24 -48
  240. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  241. package/src/lib/validators/traceability-check.js +85 -129
  242. package/src/lib/validators/validation-runner.js +8 -88
  243. package/src/lib/validators/verify-runner.js +134 -301
  244. package/src/lib/worktree/docker-audit.js +1 -2
  245. package/src/lib/worktree/fleet-scanner.js +25 -38
  246. package/src/scripts/setup-infra.js +42 -22
  247. package/src/utils/agents-installer.js +92 -13
  248. package/src/utils/claude-settings-manager.js +0 -35
  249. package/src/utils/file-copier.js +34 -6
  250. package/src/utils/hooks-installer.js +16 -9
  251. package/src/utils/skills-installer.js +1 -2
  252. package/framework/commands/commit.md +0 -166
  253. package/framework/evals/trace-schema.md +0 -67
  254. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  255. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  256. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  257. package/framework/hooks/shared/tasks-validator.js +0 -111
  258. package/framework/rubrics/backend.json +0 -41
  259. package/framework/rubrics/default.json +0 -36
  260. package/framework/rubrics/frontend.json +0 -36
  261. package/framework/rules/morph-task-tracking.md +0 -54
  262. package/framework/rules/morph-workflow.md +0 -244
  263. package/framework/schemas/tasks.schema.json +0 -79
  264. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  265. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  266. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  267. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  268. package/framework/templates/docs/proposal.md +0 -192
  269. package/framework/templates/docs/spec.md +0 -57
  270. package/framework/templates/docs/ui-components.md +0 -52
  271. package/framework/templates/docs/ui-design-system.md +0 -76
  272. package/framework/templates/docs/ui-flows.md +0 -167
  273. package/framework/templates/examples/spec-examples.md +0 -91
  274. package/framework/templates/feature/decisions.md +0 -124
  275. package/framework/templates/feature/recap.md +0 -145
  276. package/framework/templates/project/validate.js +0 -97
  277. package/framework/templates/tasks.template.json +0 -17
  278. package/src/commands/state/usecase.js +0 -143
  279. package/src/commands/tasks/dag.js +0 -181
  280. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  281. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  282. package/src/lib/agents/output-collector.js +0 -76
  283. package/src/lib/evaluator-report.js +0 -91
  284. package/src/lib/specs/acervo-format.js +0 -70
  285. package/src/lib/specs/acervo-lifecycle.js +0 -264
  286. package/src/lib/specs/done-criteria-check.js +0 -200
  287. package/src/lib/specs/usecase-check.js +0 -301
  288. package/src/lib/specs/usecase-promoter.js +0 -301
  289. package/src/lib/specs/usecase-registry.js +0 -237
  290. package/src/lib/tasks/dispatch-planner.js +0 -319
  291. package/src/lib/tasks/mermaid-renderer.js +0 -132
  292. package/src/lib/tasks/task-classifier.js +0 -30
  293. package/src/lib/tasks/task-parser.js +0 -209
  294. package/src/lib/tasks/tasks-json-parser.js +0 -296
  295. package/src/lib/tasks/test-scope.js +0 -597
  296. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  297. package/src/lib/validators/architecture/index.js +0 -7
  298. package/src/lib/validators/css/css-validator.js +0 -340
  299. package/src/lib/validators/css/index.js +0 -7
  300. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  301. package/src/lib/validators/design-system/index.js +0 -7
  302. package/src/lib/validators/score-aggregator.js +0 -244
  303. package/src/lib/validators/ui/index.js +0 -7
  304. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -9,30 +9,28 @@
9
9
  * - designSystemDivergence: boolean — conflicting design sources detected
10
10
  * - standardsViolations: string[] — violations of the plan's forbidden patterns
11
11
  * - verificationStatus: string — último `morph-spec verify` ('fail' pausa, qualquer gate)
12
- * - verificationScope + gate: string — presente e != 'feature' pausa SÓ no ship: o
13
- * último `verify` foi um recorte de task, não da feature inteira (`null`/ausente
14
- * não conta — narrow, nunca widen)
15
12
  * - e2eStatus + e2eReason + gate — nó e2e do último verify. Um `skip`
16
- * cuja razão NÃO seja dispensa legítima pausa SÓ no ship (gate 'review').
17
- * Sem escopo (stamp sem o campo) cai do mesmo lado seguro que `unknown`
13
+ * cuja razão NÃO seja dispensa legítima pausa SÓ no ship (gate 'review')
18
14
  * - verificationStale + gate: bool/str — stamp de um commit cujo código já mudou
19
15
  * (fora de .morph/) pausa SÓ no ship (gate 'review')
20
- * - evaluatorVerdict + gate: object — { status, verdict, blockers, untyped } do
21
- * evaluator-report.md. No ship, só `SHIP` sem bloqueador libera
16
+ * - reviewReports + gate: object — `reviewSignal` (review-reports.js): os
17
+ * relatórios exigidos e a cobertura dos Cn. No ship, só SHIP sem bloqueador
18
+ * em todo exigido, com todo Cn PROVADO, libera
22
19
  * - mediaClaims + gate: object — { section, featureGeneratesMedia }
23
20
  * - screenChecklist + gate: object — { mockups, legacy, screens, problems, exempt?,
24
21
  * section?, uncovered? } derivado do 1-design/mockups.md. No uiux, checklist
25
22
  * de tela incompleto pausa; no ship, item decidido sem a conferência do
26
- * avaliador pausa. Mockup legado é isento nos dois, com a razão listada
27
- * - oversizedTasks + gate: array — tasks acima de MAX_TASK_OUTPUTS outputs;
28
- * pausa SÓ no plano (gate 'plan'): a task tem de caber num dispatch
29
- * - taskCount: number — total tasks (pause if > 20)
23
+ * verificador pausa. Mockup legado é isento nos dois, com a razão listada
24
+ * - planChecks + gate: string — `readPlanChecks(...).status`: os checks
25
+ * extraídos contra o plano; no gate do plano, tudo que não é `fresh` pausa
26
+ * - sliceCount: number — fatias extraídas (pausa acima de MAX_SLICES)
30
27
  */
31
28
 
32
- const SCOPE_TASK_THRESHOLD = 20;
29
+ import { MAX_SLICES } from './plan/checks.js';
30
+ import { BLOCKER_TYPES, reviewFileName } from './review-reports.js';
33
31
 
34
- // Caminho alterado que pede o avaliador em opus: o resto da review roda em sonnet.
35
- const OPUS_PATH_RE = /(^|[/._-])(auth|login|senha|password|token|secret|crypto|permiss|rbac|acl|lgpd|pii|privacy|payment|pagamento|billing|cobranca|invoice|asaas|stripe|migrations?)([/._-]|$)/i;
32
+ // Caminho alterado que pede a revisão de segurança (persona `security`, opus).
33
+ const SECURITY_PATH_RE = /(^|[/._-])(auth|login|senha|password|token|secret|crypto|permiss|rbac|acl|lgpd|pii|privacy|payment|pagamento|billing|cobranca|invoice|asaas|stripe|migrations?)([/._-]|$)/i;
36
34
 
37
35
  // Work types whose ship gate (review) always pauses for a human, regardless
38
36
  // of trust mode — a type policy, not a per-feature risk signal. Mirrors
@@ -53,8 +51,6 @@ const E2E_SKIP_PATTERNS = [
53
51
  [/^requested$/i, 'requested'],
54
52
  // "project does not containerize (no compose file)" — não há app para subir.
55
53
  [/does not containerize/i, 'project'],
56
- // 'task group "docs" does not touch runtime' — escopo da task, não da app.
57
- [/does not touch runtime/i, 'scope'],
58
54
  ];
59
55
 
60
56
  // Classes de skip que NÃO são dispensa: o e2e não rodou e nada no repositório
@@ -63,23 +59,13 @@ const E2E_SKIP_PATTERNS = [
63
59
  // esta regra existe para eliminar.
64
60
  const E2E_SKIP_CLASSES_THAT_PAUSE = ['environment', 'requested', 'unknown'];
65
61
 
66
- // O ÚNICO escopo de stamp que responde pela feature inteira. `morph-spec verify
67
- // {f} {task}` carimba `scope: 'task:{id}'`, e o nó e2e de um verify por task
68
- // pula com a razão do GRUPO daquela task ('task group "docs" does not touch
69
- // runtime') — uma afirmação sobre UMA task, nunca sobre a app. Sem este teste,
70
- // a última task de uma feature (tipicamente `group: docs`) compra o Gate 3 da
71
- // feature que ela mesma acabou de escrever, numa máquina onde a app nunca
72
- // subiu. Ausente/desconhecido cai do mesmo lado seguro que `unknown`: recusar
73
- // é barato, aprovar sem prova não é.
74
- const FEATURE_SCOPE = 'feature';
75
-
76
62
  /**
77
63
  * Classifica a razão literal de um `skip` do nó e2e — de QUEM é a ausência de
78
64
  * prova. Existe porque `verificationStatus` é o rollup do `verify` e um nó que
79
65
  * pula NÃO o reprova: sem classificar a razão, "o e2e rodou e passou" e "o e2e
80
66
  * nunca rodou" chegam ao gate como o mesmo `pass`.
81
67
  *
82
- * A assimetria é o ponto: `project`/`scope` descrevem o REPOSITÓRIO dizendo que
68
+ * A assimetria é o ponto: `project` descreve o REPOSITÓRIO dizendo que
83
69
  * não há runtime a exercer (dispensa legítima, registrada com motivo);
84
70
  * `environment`/`requested` descrevem a MÁQUINA ou o operador, e nenhum dos
85
71
  * dois é evidência sobre o código. `unknown` cai no lado seguro pelo mesmo
@@ -88,7 +74,7 @@ const FEATURE_SCOPE = 'feature';
88
74
  * @param {string|null|undefined} reason - `verification.e2eReason` (a razão
89
75
  * literal que o `morph-spec verify` carimbou). Vazia/ausente ⇒ 'unknown' —
90
76
  * DESCONHECIDO, jamais lido como dispensa.
91
- * @returns {'environment'|'requested'|'project'|'scope'|'unknown'}
77
+ * @returns {'environment'|'requested'|'project'|'unknown'}
92
78
  */
93
79
  export function classifyE2eSkip(reason) {
94
80
  const text = typeof reason === 'string' ? reason.trim() : '';
@@ -116,20 +102,6 @@ function e2ePauseReason(klass, reason) {
116
102
  return `e2e pulado por razão não classificada: "${literal}" — sem classificação, não há dispensa`;
117
103
  }
118
104
 
119
- /**
120
- * Mensagem de recusa por ESCOPO do stamp: a razão do skip É uma dispensa
121
- * legítima (`project`/`scope`), mas quem a carimbou foi um verify que não
122
- * responde pela feature. Nomeia o escopo real para que o leitor saiba qual
123
- * comando rodar, em vez de rodar o mesmo de novo.
124
- */
125
- function e2eScopeRefusal(klass, scope, reason) {
126
- const literal = typeof reason === 'string' && reason.trim() !== '' ? reason.trim() : 'sem motivo registrado';
127
- const where = typeof scope === 'string' && scope.trim() !== '' ? scope.trim() : 'ausente';
128
- return `dispensa de e2e [${klass}] veio de um verify de escopo "${where}", não da feature:`
129
- + ` "${literal}" — um stamp de task responde por UMA task, não pela app inteira;`
130
- + ' rode `morph-spec verify {feature}` (sem task) antes do ship';
131
- }
132
-
133
105
  /** A razão gravada no sinal quando o checklist de tela isenta um mockup legado. */
134
106
  export const LEGACY_MOCKUP_EXEMPTION = 'checklist dispensado: mockup no formato antigo';
135
107
 
@@ -159,50 +131,115 @@ export function uiuxChecklistPause(screenChecklist) {
159
131
  + ' ("não se aplica" vale, se escrito)';
160
132
  }
161
133
 
162
- function blockerList(v) {
163
- return firstOf(v.blockerIds ?? []);
164
- }
165
-
166
- /** Por que o veredito do avaliador segura o ship, ou null. */
167
- export function verdictPause(v) {
168
- if (!v || v.status === 'missing') {
169
- return 'Gate 3 sem `4-review/evaluator-report.md` — despache o avaliador independente;'
170
- + ' auto-avaliação de quem implementou não decide o ship';
171
- }
172
- if (v.status !== 'ok') {
173
- return 'evaluator-report sem `**Verdict:** SHIP|FIX` — o avaliador não seguiu o contrato do relatório';
134
+ const ROLE_PERSONA = { verifier: 'morph-verifier', reviewer: 'morph-reviewer', security: 'morph-security' };
135
+
136
+ /** Por que os relatórios de review seguram o ship, ou null. */
137
+ export function reviewPause(signal) {
138
+ const required = signal?.required ?? ['verifier'];
139
+ const reports = signal?.reports ?? {};
140
+ for (const role of required) {
141
+ const r = reports[role];
142
+ const file = `4-review/${reviewFileName(role)}`;
143
+ if (!r || r.status === 'missing') {
144
+ return `Gate 3 sem \`${file}\` — despache o \`${ROLE_PERSONA[role]}\` (contexto fresco);`
145
+ + ' auto-avaliação de quem implementou não decide o ship';
146
+ }
147
+ if (r.status === 'partial') {
148
+ return `\`${file}\` está PARCIAL — retome o mesmo agente por SendMessage; parcial não é veredito`;
149
+ }
150
+ if (r.status !== 'ok') {
151
+ return `\`${file}\` sem \`**Verdict:** SHIP|FIX\` — o relatório não seguiu o contrato`;
152
+ }
174
153
  }
175
- if ((v.untyped ?? 0) > 0) {
176
- return `${v.untyped} bloqueador(es) sem tipo de evidência (uc | teste | regressao | seguranca)`
154
+ const present = Object.entries(reports).filter(([, r]) => r.status === 'ok');
155
+ const untyped = present.reduce((n, [, r]) => n + (r.untyped ?? 0), 0);
156
+ if (untyped > 0) {
157
+ return `${untyped} bloqueador(es) sem tipo de evidência (${BLOCKER_TYPES.join(' | ')})`
177
158
  + ' — sem evidência tipada não é bloqueador: reclassifique como nota ou complete a evidência';
178
159
  }
179
- if ((v.blockers ?? 0) > 0) return `avaliador abriu ${v.blockers} bloqueador(es): ${blockerList(v)}`;
180
- if (v.verdict !== 'SHIP') return 'veredito do avaliador é FIX';
160
+ for (const [role, r] of present) {
161
+ if ((r.blockers ?? 0) > 0) return `${role} abriu ${r.blockers} bloqueador(es): ${firstOf(r.blockerIds ?? [])}`;
162
+ }
163
+ for (const [role, r] of present) {
164
+ if (r.verdict !== 'SHIP') return `veredito do ${role} é ${r.verdict}`;
165
+ }
166
+ const checks = signal?.checks;
167
+ if (!checks || checks.status !== 'ok') {
168
+ return 'sem `2-plan/checks.json` legível — a cobertura dos cenários pelo verificador não é conferível';
169
+ }
170
+ const gaps = [
171
+ ...checks.missing.map((id) => `${id} sem linha`),
172
+ ...checks.notProven.map(({ id, status }) => `${id} ${status}`),
173
+ ];
174
+ if (gaps.length) {
175
+ return `\`## Checks\` do verificador não prova ${firstOf(gaps)}`
176
+ + ' — todo cenário precisa de uma linha PROVADO com evidência';
177
+ }
181
178
  return null;
182
179
  }
183
180
 
184
- function verdictSummary(v) {
185
- if (!v || v.status !== 'ok') return 'Gate 3 sem veredito do avaliador independente';
186
- return `veredito ${v.verdict}, ${v.blockers ?? 0} bloqueador(es)`;
181
+ function reviewSummary(signal) {
182
+ const reports = signal?.reports ?? {};
183
+ const roles = Object.keys(reports);
184
+ if (roles.length === 0) return 'Gate 3 sem relatórios de review';
185
+ const verdicts = roles.map((role) => `${role} ${reports[role].verdict ?? reports[role].status}`).join(', ');
186
+ const blockers = roles.reduce((n, role) => n + (reports[role].blockers ?? 0), 0);
187
+ const checks = signal.checks?.status === 'ok'
188
+ ? `; checks ${signal.checks.total - signal.checks.missing.length - signal.checks.notProven.length}/${signal.checks.total} PROVADO`
189
+ : '';
190
+ return `review: ${verdicts}; ${blockers} bloqueador(es)${checks}`;
187
191
  }
188
192
 
189
193
  /**
190
- * Modelo do avaliador do Gate 3: `opus` quando a feature toca caminho sensível
191
- * (auth, segredo, permissão, dado pessoal, dinheiro, migration), prompt/modelo
192
- * ou geração de mídia; `sonnet` no resto.
194
+ * Modelo do verificador: `opus` quando a feature mexe em prompt/modelo ou
195
+ * gera mídia; `sonnet` no resto. Caminho sensível não muda o verificador:
196
+ * despacha a persona `security` ({@link securityReviewFor}).
193
197
  *
194
- * @param {{ changedFiles?: string[], promptModelTouched?: boolean, generatesMedia?: boolean }} input
198
+ * @param {{ promptModelTouched?: boolean, generatesMedia?: boolean }} input
195
199
  * @returns {{ model: 'opus'|'sonnet', reasons: string[] }}
196
200
  */
197
- export function evaluatorModelFor({ changedFiles = [], promptModelTouched = false, generatesMedia = false } = {}) {
201
+ export function verifierModelFor({ promptModelTouched = false, generatesMedia = false } = {}) {
198
202
  const reasons = [];
199
- const risky = changedFiles.filter((f) => OPUS_PATH_RE.test(f.replaceAll('\\', '/')));
200
- if (risky.length) reasons.push(`caminho sensível: ${firstOf(risky)}`);
201
203
  if (promptModelTouched) reasons.push('prompt/modelo alterado');
202
204
  if (generatesMedia) reasons.push('gera mídia');
203
205
  return { model: reasons.length ? 'opus' : 'sonnet', reasons };
204
206
  }
205
207
 
208
+ /**
209
+ * A revisão de segurança é exigida quando o diff toca auth, segredo,
210
+ * permissão, dado pessoal, pagamento ou migration.
211
+ *
212
+ * @param {string[]} changedFiles
213
+ * @returns {{ required: boolean, reasons: string[] }}
214
+ */
215
+ export function securityReviewFor(changedFiles = []) {
216
+ const risky = changedFiles.filter((f) => SECURITY_PATH_RE.test(f.replaceAll('\\', '/')));
217
+ return { required: risky.length > 0, reasons: risky.length ? [`caminho sensível: ${firstOf(risky)}`] : [] };
218
+ }
219
+
220
+ const PLAN_CHECKS_REASONS = {
221
+ 'no-plan': 'sem 2-plan/plan.md',
222
+ 'no-checks': 'sem 2-plan/checks.json',
223
+ unreadable: '2-plan/checks.json ilegível ou de outra versão',
224
+ stale: '2-plan/checks.json não corresponde ao plan.md atual',
225
+ };
226
+
227
+ /**
228
+ * Por que o gate do plano não fecha sem pessoa: os checks extraídos são o que
229
+ * o apply despacha, o report-check confere e a review cobra — um plano sem
230
+ * eles, ou com os de outra versão do plano, aprovaria critérios que ninguém
231
+ * extraiu.
232
+ * Regra única do gate-check (pausa) e do approve/advance automáticos (recusa).
233
+ *
234
+ * @param {string|undefined} status - `readPlanChecks(...).status`
235
+ * @returns {string|null}
236
+ */
237
+ export function planChecksPause(status) {
238
+ if (status === undefined || status === 'fresh') return null;
239
+ const what = PLAN_CHECKS_REASONS[status] ?? `checks.json em estado "${status}"`;
240
+ return `${what} — rode \`morph-spec plan-check {feature}\` até zero erro antes do gate do plano`;
241
+ }
242
+
206
243
  /**
207
244
  * @param {object} config - .morph/config/config.json contents (uses `trust`)
208
245
  * @param {object} signals
@@ -244,35 +281,19 @@ export function shouldPauseGate(config, signals) {
244
281
  };
245
282
  }
246
283
 
247
- // No gate de SHIP: um `verification` PRESENTE cujo escopo não é o da feature
248
- // é um recorte parcial — de UMA task, nunca da app inteira. O stamp é
249
- // SINGULAR, sobrescrito por qualquer verify de qualquer escopo, e vale
250
- // INDEPENDENTE do que os nós individuais dizem: mesmo com e2e 'pass' dentro
251
- // daquele recorte, build/tests/validators também rodaram só para a task, não
252
- // para a feature. Antes desta regra, um `verify {f} {task}` totalmente verde
253
- // comprava o Gate 3 da feature que ele nem tentou verificar por inteiro — já
254
- // acontecia manualmente (`--filter`), e o recorte automático de PR B faz
255
- // acontecer em TODA task. Por isso corre ANTES do bloco de e2e abaixo: ele
256
- // pausa por si só, sem depender de e2eStatus/e2eReason.
257
- //
258
- // `typeof === 'string'` exclui `null`/ausente (features legadas sem stamp) —
259
- // narrow, nunca widen. O caminho de volta é sempre o mesmo, barato e nomeado:
260
- // `morph-spec verify {feature}` (suíte inteira) antes do ship.
261
- if (signals.gate === 'review'
262
- && typeof signals.verificationScope === 'string'
263
- && signals.verificationScope !== FEATURE_SCOPE) {
284
+ // No ship, ausência de stamp não é "verde": uma feature foi aprovada em campo sem verify nenhum.
285
+ if (signals.gate === 'review' && !signals.verificationStatus) {
264
286
  return {
265
287
  pause: true,
266
- reason: `último morph-spec verify foi de escopo "${signals.verificationScope}" (recorte parcial de task)`
267
- + ' — rode `morph-spec verify {feature}` (suíte inteira) antes do ship',
288
+ reason: 'nenhum `morph-spec verify` carimbado nesta feature — rode `morph-spec verify {feature}` antes do ship',
268
289
  };
269
290
  }
270
291
 
271
292
  // No gate de SHIP apenas: o nó e2e do último verify PULOU. Um skip não
272
293
  // reprova o rollup, então `verificationStatus` sai 'pass' e o gate ficava
273
294
  // cego — auto-passando um Gate 3 sobre uma app que ninguém subiu. A razão
274
- // literal decide: dispensa legítima (o projeto não containeriza; o grupo da
275
- // task não toca runtime) passa e fica REGISTRADA nos signals; ausência de
295
+ // literal decide: dispensa legítima (o projeto não containeriza) passa e
296
+ // fica REGISTRADA nos signals; ausência de
276
297
  // prova (docker indisponível, `--skip-e2e`, razão nenhuma) pausa. É a prosa
277
298
  // do morph-review virando regra — "docker indisponível não é dispensa" — e
278
299
  // é o que dispensa o `gate-decision --pause false` manual que ela mandava
@@ -283,26 +304,12 @@ export function shouldPauseGate(config, signals) {
283
304
  if (E2E_SKIP_CLASSES_THAT_PAUSE.includes(klass)) {
284
305
  return { pause: true, reason: e2ePauseReason(klass, signals.e2eReason) };
285
306
  }
286
- // Segunda pergunta, que a razão sozinha não responde: QUEM carimbou. Um
287
- // escopo de TASK (`task:{id}`) já pausou acima, antes deste bloco — o que
288
- // sobra a checar aqui é só a ausência de escopo (`null`/indefinido:
289
- // feature legada, ou stamp anterior à introdução do campo). Cai do mesmo
290
- // lado seguro que `unknown` pelo mesmo motivo de sempre: recusar é barato,
291
- // aprovar sem prova de QUEM respondeu pela app não é. O caminho de volta é
292
- // o mesmo (rodar `verify {feature}`), e é o que morph-review já manda
293
- // fazer antes do Gate 3.
294
- if (signals.verificationScope == null) {
295
- return {
296
- pause: true,
297
- reason: e2eScopeRefusal(klass, signals.verificationScope, signals.e2eReason),
298
- };
299
- }
300
307
  }
301
308
 
302
309
  // Furo 2: no gate de SHIP (review) apenas, um stamp de verification cujo
303
310
  // sha diverge do HEAD atual significa que código mudou depois do último
304
311
  // `morph-spec verify` — o resultado carimbado não cobre mais o que está
305
- // prestes a ser aprovado. Pausa só aqui, nunca em plan/uiux/proposal: essa
312
+ // prestes a ser aprovado. Pausa só aqui, nunca em plan/uiux: essa
306
313
  // é uma regra de ship, não uma regra geral de gate. Stamp AUSENTE não entra
307
314
  // aqui (verificationStale é fail-open para `false` em collectSignals
308
315
  // quando não há stamp) — features legadas sem stamp continuam liberadas.
@@ -314,7 +321,7 @@ export function shouldPauseGate(config, signals) {
314
321
  }
315
322
 
316
323
  if (signals.gate === 'review') {
317
- const reason = verdictPause(signals.evaluatorVerdict);
324
+ const reason = reviewPause(signals.reviewReports);
318
325
  if (reason) return { pause: true, reason };
319
326
  }
320
327
 
@@ -323,9 +330,8 @@ export function shouldPauseGate(config, signals) {
323
330
  // única parte do produto cujo comportamento não é lido por build nem por teste
324
331
  // unitário: os dois continuam verdes com o prompt trocado.
325
332
  //
326
- // DEPOIS da cláusula de procedência do score, de propósito: quando as duas
327
- // valem, "Gate 3 sem score do avaliador independente" continua sendo a razão
328
- // reportada — é o defeito mais grave dos dois.
333
+ // DEPOIS dos relatórios de review, de propósito: relatório ausente é o
334
+ // defeito mais grave dos dois e é a razão reportada.
329
335
  //
330
336
  // PAUSA, jamais bloqueia: `approve review` não ganha precondição nova. Uma
331
337
  // segunda precondição dura ali tornaria inaprovável, inclusive por decisão
@@ -363,23 +369,23 @@ export function shouldPauseGate(config, signals) {
363
369
  && signals.mediaClaims.section !== true) {
364
370
  return {
365
371
  pause: true,
366
- reason: 'relatório do avaliador sem a seção `## Mídia gerada` — as quatro afirmações de mídia'
372
+ reason: '`4-review/verifier.md` sem a seção `## Mídia gerada` — as quatro afirmações de mídia'
367
373
  + ' (custo, revisão humana, marcação, hot path) não foram provadas'
368
- + ' (evals/review-checklist.md, "Feature que gera mídia")',
374
+ + ' (evals/review-checklist.md, "Verificador")',
369
375
  };
370
376
  }
371
377
 
372
378
  // No fecho do DESIGN: o mockup declara telas sem o checklist de tela
373
379
  // completo. É ele que faz paginação, ordenação, filtro e os três estados
374
- // virarem `doneCriteria` — sem ele, uma lista de 2 000 linhas é aprovada
375
- // listando tudo, e o implementador entrega exatamente o critério.
380
+ // virarem cenário do plano — sem ele, uma lista de 2 000 linhas é aprovada
381
+ // listando tudo, e o implementador entrega exatamente o cenário.
376
382
  if (signals.gate === 'uiux') {
377
383
  const reason = uiuxChecklistPause(signals.screenChecklist);
378
384
  if (reason) return { pause: true, reason };
379
385
  }
380
386
 
381
387
  // No SHIP, depois da cláusula de mídia e pela mesma disciplina: o design
382
- // decidiu itens de tela e o avaliador não registrou a conferência deles
388
+ // decidiu itens de tela e o verificador não registrou a conferência deles
383
389
  // contra a tela entregue. Cobra-se o item DEVIDO (`uncovered`): tela toda
384
390
  // "não se aplica", mockup legado ou ausente não devem nada.
385
391
  if (signals.gate === 'review' && (signals.screenChecklist?.uncovered?.length ?? 0) > 0) {
@@ -387,8 +393,8 @@ export function shouldPauseGate(config, signals) {
387
393
  if (section !== true) {
388
394
  return {
389
395
  pause: true,
390
- reason: `relatório do avaliador sem a seção \`## Checklist de tela\` — o design decidiu ${firstOf(uncovered)}`
391
- + ' e a avaliação não registrou a conferência contra a tela entregue'
396
+ reason: `\`4-review/verifier.md\` sem a seção \`## Checklist de tela\` — o design decidiu ${firstOf(uncovered)}`
397
+ + ' e o verificador não registrou a conferência contra a tela entregue'
392
398
  + ' (evals/review-checklist.md, "Contrato de tela")',
393
399
  };
394
400
  }
@@ -399,18 +405,15 @@ export function shouldPauseGate(config, signals) {
399
405
  };
400
406
  }
401
407
 
402
- if (signals.gate === 'plan' && Array.isArray(signals.oversizedTasks) && signals.oversizedTasks.length > 0) {
403
- const list = signals.oversizedTasks.map((t) => `${t.id} (${t.outputs})`).join(', ');
404
- return {
405
- pause: true,
406
- reason: `task grande demais para um dispatch: ${list} outputs — fatie por pós-condição`,
407
- };
408
+ if (signals.gate === 'plan') {
409
+ const reason = planChecksPause(signals.planChecks);
410
+ if (reason) return { pause: true, reason };
408
411
  }
409
412
 
410
- if (typeof signals.taskCount === 'number' && signals.taskCount > SCOPE_TASK_THRESHOLD) {
413
+ if (typeof signals.sliceCount === 'number' && signals.sliceCount > MAX_SLICES) {
411
414
  return {
412
415
  pause: true,
413
- reason: `scope of ${signals.taskCount} tasks exceeds threshold ${SCOPE_TASK_THRESHOLD}`,
416
+ reason: `${signals.sliceCount} fatias, acima de ${MAX_SLICES} — o plano é grande demais para uma feature`,
414
417
  };
415
418
  }
416
419
 
@@ -436,15 +439,12 @@ export function getRiskSignals(signals) {
436
439
  const literal = typeof signals.e2eReason === 'string' && signals.e2eReason.trim() !== ''
437
440
  ? signals.e2eReason.trim()
438
441
  : 'sem motivo registrado';
439
- const where = typeof signals.verificationScope === 'string' && signals.verificationScope.trim() !== ''
440
- ? signals.verificationScope.trim()
441
- : 'escopo ausente';
442
- out.push(`e2e pulado [${klass}] (stamp de ${where}): ${literal}`);
442
+ out.push(`e2e pulado [${klass}]: ${literal}`);
443
443
  }
444
444
  if (signals.gate === 'review' && signals.verificationStale === true) {
445
445
  out.push('verification stamp desatualizado (stale) no ship');
446
446
  }
447
- if (signals.gate === 'review') out.push(verdictSummary(signals.evaluatorVerdict));
447
+ if (signals.gate === 'review') out.push(reviewSummary(signals.reviewReports));
448
448
  // Listado mesmo quando NÃO pausa, pela mesma razão do e2e: um projeto que não
449
449
  // declara caminho de prompt/modelo é indistinguível, no relatório, de um que
450
450
  // declara e não tocou em nada — e as duas coisas significam coisas diferentes.
@@ -456,11 +456,11 @@ export function getRiskSignals(signals) {
456
456
  out.push(`prompt/modelo tocado (${touched.length}) com eval ${signals.evalsStatus ?? 'não declarado'}`);
457
457
  }
458
458
  }
459
- if (signals.gate === 'plan' && Array.isArray(signals.oversizedTasks) && signals.oversizedTasks.length > 0) {
460
- out.push(`tasks grandes demais: ${signals.oversizedTasks.map((t) => t.id).join(', ')}`);
459
+ if (signals.gate === 'plan' && signals.planChecks && signals.planChecks !== 'fresh') {
460
+ out.push(`checks.json ${signals.planChecks}`);
461
461
  }
462
- if (typeof signals.taskCount === 'number' && signals.taskCount > SCOPE_TASK_THRESHOLD) {
463
- out.push(`${signals.taskCount} tasks (over threshold)`);
462
+ if (typeof signals.sliceCount === 'number' && signals.sliceCount > MAX_SLICES) {
463
+ out.push(`${signals.sliceCount} fatias (acima de ${MAX_SLICES})`);
464
464
  }
465
465
  // Listado mesmo quando NÃO pausa, pela mesma razão do sinal irmão: a isenção
466
466
  // de uma feature que não gera mídia tem de ser VISÍVEL, senão é
@@ -468,7 +468,7 @@ export function getRiskSignals(signals) {
468
468
  if (signals.gate === 'review' && signals.mediaClaims?.featureGeneratesMedia === true) {
469
469
  out.push(signals.mediaClaims.section === true
470
470
  ? 'prova de mídia registrada'
471
- : 'relatório sem a seção `## Mídia gerada` (prova de mídia ausente)');
471
+ : 'verifier.md sem a seção `## Mídia gerada` (prova de mídia ausente)');
472
472
  }
473
473
 
474
474
  // Listado mesmo quando NÃO pausa: a isenção (sem mockup, legado, nada
@@ -485,7 +485,7 @@ export function getRiskSignals(signals) {
485
485
  : `checklist de tela completo (${screens.length} tela(s))`);
486
486
  } else if (uncovered.length > 0) {
487
487
  out.push(section !== true
488
- ? 'relatório sem a seção `## Checklist de tela`'
488
+ ? 'verifier.md sem a seção `## Checklist de tela`'
489
489
  : `checklist de tela sem conferência em ${uncovered.length} tela(s)`);
490
490
  } else if (screens.length === 0) {
491
491
  // O design passou pela uiux sem tela nenhuma: nada a cobrar no ship, mas
@@ -502,7 +502,6 @@ export function getRiskSignals(signals) {
502
502
  }
503
503
 
504
504
  export const RISK_CONSTANTS = {
505
- SCOPE_TASK_THRESHOLD,
506
505
  FORCED_PAUSE_WORK_TYPES,
507
506
  E2E_SKIP_CLASSES_THAT_PAUSE,
508
507
  };
@@ -1,36 +1,25 @@
1
1
  /**
2
2
  * The index of the ADRs that still hold across the project:
3
- * `.morph/specs/adrs/`, one file per ADR, committed next to the use-case acervo.
4
- *
5
- * Every feature writes ADRs into its own `decisions.md`, and most of them only
6
- * matter inside that feature. The index keeps the few that set a project-wide
7
- * rule, so brainstorming and plan read one folder instead of every archived
8
- * `decisions.md` — those stay as history, not as instruction.
9
- *
10
- * One file per ADR (`{feature}--ADR-{nnn}.md`), like the use-case acervo:
11
- * parallel features that index different ADRs never touch the same file, so
12
- * their archive commits merge cleanly — a single shared page conflicted on the
13
- * second merge.
14
- *
15
- * Maintained by finish/archive alone, inside the archive commit:
16
- * - enters: an ADR of the closing feature whose block carries `**Alcance:** projeto`;
17
- * - leaves: an ADR the closing feature's `spec.md` revokes in full (file deleted);
3
+ * `.morph/specs/adrs/`, one file per ADR, maintained by finish/archive alone,
4
+ * inside the archive commit, from the closing feature's `2-plan/plan.md`:
5
+ * - enters: an ADR of `## Decisões` with `**Alcance:** projeto`;
6
+ * - leaves: an ADR its `### Conflitos com o acervo` revokes in full (file deleted);
18
7
  * - narrows: one it revokes in part keeps its file, with `scope:` where it still holds.
19
- * Nothing to add or revoke → nothing is touched (nor created).
8
+ * One file per ADR so parallel features that index different ADRs merge cleanly.
20
9
  */
21
10
 
22
11
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
23
12
  import { basename, join } from 'path';
13
+ import { readFrontmatterField, setFrontmatterField } from './frontmatter.js';
14
+ import { parseRevocations } from './revocations.js';
15
+ import { parsePlan } from '../plan/plan-parser.js';
24
16
  import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
25
- import { readFrontmatterField, setFrontmatterField } from './usecase-promoter.js';
26
17
 
27
18
  /** Project-relative path of the index folder (git pathspec-ready). */
28
19
  export const ADR_INDEX_PATHSPEC = join('.morph', 'specs', 'adrs');
29
20
 
30
- const ADR_HEADING_RE = /^(#{2,6})\s+(ADR-\d+)\b[\s:—–-]*(.*?)\s*$/;
31
- const ANY_HEADING_RE = /^(#{1,6})\s/;
32
- /** `**Alcance:** projeto` — also `**Alcance**: projeto`. */
33
- const PROJECT_SCOPE_RE = /^\s*\*\*Alcance:?\*\*:?\s*projeto\b/i;
21
+ const PLAN = OUTPUT_SCHEMA.plan;
22
+ const INDEX_LABEL = '.morph/specs/adrs/';
34
23
  const INDEX_FILE_RE = /^([a-z0-9][a-z0-9-]*)--(ADR-\d+)\.md$/;
35
24
 
36
25
  /** `admin/ADR-001` ↔ `admin--ADR-001.md`. */
@@ -40,50 +29,19 @@ const refOf = (name) => {
40
29
  return m ? `${m[1]}/${m[2]}` : null;
41
30
  };
42
31
 
43
- /**
44
- * The ADRs of one `decisions.md` marked as project-wide, each with its block.
45
- *
46
- * @param {string} content
47
- * @returns {Array<{ adr: string, title: string, body: string }>}
48
- */
49
- export function projectAdrs(content) {
50
- const out = [];
51
- let current = null; // { adr, title, level, marked, lines }
52
- const close = () => {
53
- if (current?.marked) out.push({ adr: current.adr, title: current.title, body: current.lines.join('\n').trim() });
54
- current = null;
55
- };
56
-
57
- for (const line of String(content ?? '').split(/\r?\n/)) {
58
- const adr = ADR_HEADING_RE.exec(line);
59
- if (adr) {
60
- close();
61
- current = { adr: adr[2], title: adr[3], level: adr[1].length, marked: false, lines: [] };
62
- continue;
63
- }
64
- const heading = ANY_HEADING_RE.exec(line);
65
- if (current && heading && heading[1].length <= current.level) { close(); continue; }
66
- if (!current) continue;
67
- if (PROJECT_SCOPE_RE.test(line)) current.marked = true;
68
- current.lines.push(line);
69
- }
70
- close();
71
- return out;
72
- }
73
-
74
32
  function renderAdrFile({ ref, title, body }, feature) {
75
- const decisions = `.morph/archive/${feature}/${OUTPUT_SCHEMA.decisions.phaseDir}/${OUTPUT_SCHEMA.decisions.filename}`;
33
+ const source = `.morph/archive/${feature}/${PLAN.phaseDir}/${PLAN.filename}`;
76
34
  return [
77
35
  '---',
78
36
  `adr: ${ref}`,
79
37
  `title: ${JSON.stringify(title)}`,
80
- `source: ${decisions}`,
38
+ `source: ${source}`,
81
39
  '---',
82
40
  '',
83
41
  `# ${ref}: ${title}`,
84
42
  '',
85
- '> Mantido pelo `morph-spec finish` — não edite à mão. Vale no projeto até que o `spec.md` de uma',
86
- '> feature o revogue: por inteiro, e este arquivo sai; em parte, e `scope:` diz onde ele ainda vale.',
43
+ '> Mantido pelo `morph-spec finish` — não edite à mão. Vale no projeto até que o `plan.md` de uma',
44
+ '> feature o revogue em `### Conflitos com o acervo`: por inteiro, e este arquivo sai; em parte, e `scope:` diz onde ele ainda vale.',
87
45
  '',
88
46
  body,
89
47
  '',
@@ -112,29 +70,29 @@ export function readAdrIndex(cwd) {
112
70
  }
113
71
 
114
72
  /**
115
- * Apply a closing feature to the index: its project-wide ADRs enter, and the
116
- * ADR revocations its `spec.md` declared delete a file (total) or narrow it
117
- * (parcial).
73
+ * Apply a closing feature's plan to the index: its project-wide ADRs enter, and
74
+ * the ADRs its conflicts table revokes are deleted (total) or narrowed (parcial).
75
+ * An unreadable or absent plan touches nothing.
118
76
  *
119
77
  * @param {string} cwd
120
78
  * @param {string} featureDir - the feature folder (already archived, at close time)
121
- * @param {Array<{ id: string, change: 'total'|'parcial', scope: string|null }>} revocations - ADR entries of `parseRevocations`
122
79
  * @returns {{ changed: boolean, added: string[], revoked: string[], narrowed: string[],
123
- * problems: Array<{ id: string, reason: string, message: string }> }}
80
+ * problems: Array<{ id: string|null, reason: string, message: string }> }}
124
81
  */
125
- export function updateAdrIndex(cwd, featureDir, revocations = []) {
82
+ export function updateAdrIndex(cwd, featureDir) {
126
83
  const result = { changed: false, added: [], revoked: [], narrowed: [], problems: [] };
127
84
  const feature = basename(featureDir);
128
85
  const dir = join(cwd, ADR_INDEX_PATHSPEC);
129
86
 
130
- let marked = [];
87
+ let plan = null;
131
88
  try {
132
- const decisions = join(featureDir, OUTPUT_SCHEMA.decisions.phaseDir, OUTPUT_SCHEMA.decisions.filename);
133
- if (existsSync(decisions)) marked = projectAdrs(readFileSync(decisions, 'utf8'));
134
- } catch { /* unreadable decisions.md — nothing enters */ }
89
+ const planPath = join(featureDir, PLAN.phaseDir, PLAN.filename);
90
+ if (existsSync(planPath)) plan = parsePlan(readFileSync(planPath, 'utf8'));
91
+ } catch { /* unreadable plan.md — nothing enters or leaves */ }
92
+ if (!plan) return result;
135
93
 
136
- for (const { adr, title, body } of marked) {
137
- const ref = `${feature}/${adr}`;
94
+ for (const { id, title, body } of plan.adrs.filter((a) => a.scope === 'projeto')) {
95
+ const ref = `${feature}/${id}`;
138
96
  const path = join(dir, adrFileName(ref));
139
97
  const next = renderAdrFile({ ref, title, body }, feature);
140
98
  const existed = existsSync(path);
@@ -145,13 +103,15 @@ export function updateAdrIndex(cwd, featureDir, revocations = []) {
145
103
  if (!existed) result.added.push(ref);
146
104
  }
147
105
 
106
+ const { entries: revocations, problems } = parseRevocations(plan.conflicts?.text);
107
+ result.problems.push(...problems);
148
108
  for (const rev of revocations) {
149
109
  const path = join(dir, adrFileName(rev.id));
150
110
  if (!existsSync(path)) {
151
111
  result.problems.push({
152
112
  id: rev.id,
153
113
  reason: 'adr-not-indexed',
154
- message: `${rev.id} não está no índice de ADRs ativos (${ADR_INDEX_PATHSPEC.replace(/\\/g, '/')}/) — nada a retirar; o decisions.md arquivado segue como história`,
114
+ message: `${rev.id} não está no índice de ADRs ativos (${INDEX_LABEL}) — nada a retirar; o plan.md arquivado segue como história`,
155
115
  });
156
116
  continue;
157
117
  }
@@ -167,3 +127,24 @@ export function updateAdrIndex(cwd, featureDir, revocations = []) {
167
127
 
168
128
  return result;
169
129
  }
130
+
131
+ /**
132
+ * What the finish would fail to apply from a plan's revocation table, reported
133
+ * while the plan can still change.
134
+ *
135
+ * @param {string} cwd
136
+ * @param {ReturnType<typeof parsePlan>} plan
137
+ * @returns {string[]}
138
+ */
139
+ export function revocationWarnings(cwd, plan) {
140
+ const { entries, problems } = parseRevocations(plan.conflicts?.text);
141
+ const warnings = problems.map((p) => `### Conflitos com o acervo: ${p.message}`);
142
+ if (entries.length === 0) return warnings;
143
+ const indexed = new Set(readAdrIndex(cwd).map((a) => a.ref));
144
+ for (const e of entries) {
145
+ if (!indexed.has(e.id)) {
146
+ warnings.push(`### Conflitos com o acervo: ${e.id} não está no índice de ADRs ativos (${INDEX_LABEL}) — o finish não terá o que retirar`);
147
+ }
148
+ }
149
+ return warnings;
150
+ }