@polymorphism-tech/morph-spec 8.47.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 (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  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/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -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
@@ -252,35 +289,11 @@ export function shouldPauseGate(config, signals) {
252
289
  };
253
290
  }
254
291
 
255
- // No gate de SHIP: um `verification` PRESENTE cujo escopo não é o da feature
256
- // é um recorte parcial — de UMA task, nunca da app inteira. O stamp é
257
- // SINGULAR, sobrescrito por qualquer verify de qualquer escopo, e vale
258
- // INDEPENDENTE do que os nós individuais dizem: mesmo com e2e 'pass' dentro
259
- // daquele recorte, build/tests/validators também rodaram só para a task, não
260
- // para a feature. Antes desta regra, um `verify {f} {task}` totalmente verde
261
- // comprava o Gate 3 da feature que ele nem tentou verificar por inteiro — já
262
- // acontecia manualmente (`--filter`), e o recorte automático de PR B faz
263
- // acontecer em TODA task. Por isso corre ANTES do bloco de e2e abaixo: ele
264
- // pausa por si só, sem depender de e2eStatus/e2eReason.
265
- //
266
- // `typeof === 'string'` exclui `null`/ausente (features legadas sem stamp) —
267
- // narrow, nunca widen. O caminho de volta é sempre o mesmo, barato e nomeado:
268
- // `morph-spec verify {feature}` (suíte inteira) antes do ship.
269
- if (signals.gate === 'review'
270
- && typeof signals.verificationScope === 'string'
271
- && signals.verificationScope !== FEATURE_SCOPE) {
272
- return {
273
- pause: true,
274
- reason: `último morph-spec verify foi de escopo "${signals.verificationScope}" (recorte parcial de task)`
275
- + ' — rode `morph-spec verify {feature}` (suíte inteira) antes do ship',
276
- };
277
- }
278
-
279
292
  // No gate de SHIP apenas: o nó e2e do último verify PULOU. Um skip não
280
293
  // reprova o rollup, então `verificationStatus` sai 'pass' e o gate ficava
281
294
  // cego — auto-passando um Gate 3 sobre uma app que ninguém subiu. A razão
282
- // literal decide: dispensa legítima (o projeto não containeriza; o grupo da
283
- // 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
284
297
  // prova (docker indisponível, `--skip-e2e`, razão nenhuma) pausa. É a prosa
285
298
  // do morph-review virando regra — "docker indisponível não é dispensa" — e
286
299
  // é o que dispensa o `gate-decision --pause false` manual que ela mandava
@@ -291,26 +304,12 @@ export function shouldPauseGate(config, signals) {
291
304
  if (E2E_SKIP_CLASSES_THAT_PAUSE.includes(klass)) {
292
305
  return { pause: true, reason: e2ePauseReason(klass, signals.e2eReason) };
293
306
  }
294
- // Segunda pergunta, que a razão sozinha não responde: QUEM carimbou. Um
295
- // escopo de TASK (`task:{id}`) já pausou acima, antes deste bloco — o que
296
- // sobra a checar aqui é só a ausência de escopo (`null`/indefinido:
297
- // feature legada, ou stamp anterior à introdução do campo). Cai do mesmo
298
- // lado seguro que `unknown` pelo mesmo motivo de sempre: recusar é barato,
299
- // aprovar sem prova de QUEM respondeu pela app não é. O caminho de volta é
300
- // o mesmo (rodar `verify {feature}`), e é o que morph-review já manda
301
- // fazer antes do Gate 3.
302
- if (signals.verificationScope == null) {
303
- return {
304
- pause: true,
305
- reason: e2eScopeRefusal(klass, signals.verificationScope, signals.e2eReason),
306
- };
307
- }
308
307
  }
309
308
 
310
309
  // Furo 2: no gate de SHIP (review) apenas, um stamp de verification cujo
311
310
  // sha diverge do HEAD atual significa que código mudou depois do último
312
311
  // `morph-spec verify` — o resultado carimbado não cobre mais o que está
313
- // 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
314
313
  // é uma regra de ship, não uma regra geral de gate. Stamp AUSENTE não entra
315
314
  // aqui (verificationStale é fail-open para `false` em collectSignals
316
315
  // quando não há stamp) — features legadas sem stamp continuam liberadas.
@@ -322,7 +321,7 @@ export function shouldPauseGate(config, signals) {
322
321
  }
323
322
 
324
323
  if (signals.gate === 'review') {
325
- const reason = verdictPause(signals.evaluatorVerdict);
324
+ const reason = reviewPause(signals.reviewReports);
326
325
  if (reason) return { pause: true, reason };
327
326
  }
328
327
 
@@ -331,9 +330,8 @@ export function shouldPauseGate(config, signals) {
331
330
  // única parte do produto cujo comportamento não é lido por build nem por teste
332
331
  // unitário: os dois continuam verdes com o prompt trocado.
333
332
  //
334
- // DEPOIS da cláusula de procedência do score, de propósito: quando as duas
335
- // valem, "Gate 3 sem score do avaliador independente" continua sendo a razão
336
- // 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.
337
335
  //
338
336
  // PAUSA, jamais bloqueia: `approve review` não ganha precondição nova. Uma
339
337
  // segunda precondição dura ali tornaria inaprovável, inclusive por decisão
@@ -371,23 +369,23 @@ export function shouldPauseGate(config, signals) {
371
369
  && signals.mediaClaims.section !== true) {
372
370
  return {
373
371
  pause: true,
374
- 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'
375
373
  + ' (custo, revisão humana, marcação, hot path) não foram provadas'
376
- + ' (evals/review-checklist.md, "Feature que gera mídia")',
374
+ + ' (evals/review-checklist.md, "Verificador")',
377
375
  };
378
376
  }
379
377
 
380
378
  // No fecho do DESIGN: o mockup declara telas sem o checklist de tela
381
379
  // completo. É ele que faz paginação, ordenação, filtro e os três estados
382
- // virarem `doneCriteria` — sem ele, uma lista de 2 000 linhas é aprovada
383
- // 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.
384
382
  if (signals.gate === 'uiux') {
385
383
  const reason = uiuxChecklistPause(signals.screenChecklist);
386
384
  if (reason) return { pause: true, reason };
387
385
  }
388
386
 
389
387
  // No SHIP, depois da cláusula de mídia e pela mesma disciplina: o design
390
- // 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
391
389
  // contra a tela entregue. Cobra-se o item DEVIDO (`uncovered`): tela toda
392
390
  // "não se aplica", mockup legado ou ausente não devem nada.
393
391
  if (signals.gate === 'review' && (signals.screenChecklist?.uncovered?.length ?? 0) > 0) {
@@ -395,8 +393,8 @@ export function shouldPauseGate(config, signals) {
395
393
  if (section !== true) {
396
394
  return {
397
395
  pause: true,
398
- reason: `relatório do avaliador sem a seção \`## Checklist de tela\` — o design decidiu ${firstOf(uncovered)}`
399
- + ' 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'
400
398
  + ' (evals/review-checklist.md, "Contrato de tela")',
401
399
  };
402
400
  }
@@ -407,18 +405,15 @@ export function shouldPauseGate(config, signals) {
407
405
  };
408
406
  }
409
407
 
410
- if (signals.gate === 'plan' && Array.isArray(signals.oversizedTasks) && signals.oversizedTasks.length > 0) {
411
- const list = signals.oversizedTasks.map((t) => `${t.id} (${t.outputs})`).join(', ');
412
- return {
413
- pause: true,
414
- reason: `task grande demais para um dispatch: ${list} outputs — fatie por pós-condição`,
415
- };
408
+ if (signals.gate === 'plan') {
409
+ const reason = planChecksPause(signals.planChecks);
410
+ if (reason) return { pause: true, reason };
416
411
  }
417
412
 
418
- if (typeof signals.taskCount === 'number' && signals.taskCount > SCOPE_TASK_THRESHOLD) {
413
+ if (typeof signals.sliceCount === 'number' && signals.sliceCount > MAX_SLICES) {
419
414
  return {
420
415
  pause: true,
421
- 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`,
422
417
  };
423
418
  }
424
419
 
@@ -444,15 +439,12 @@ export function getRiskSignals(signals) {
444
439
  const literal = typeof signals.e2eReason === 'string' && signals.e2eReason.trim() !== ''
445
440
  ? signals.e2eReason.trim()
446
441
  : 'sem motivo registrado';
447
- const where = typeof signals.verificationScope === 'string' && signals.verificationScope.trim() !== ''
448
- ? signals.verificationScope.trim()
449
- : 'escopo ausente';
450
- out.push(`e2e pulado [${klass}] (stamp de ${where}): ${literal}`);
442
+ out.push(`e2e pulado [${klass}]: ${literal}`);
451
443
  }
452
444
  if (signals.gate === 'review' && signals.verificationStale === true) {
453
445
  out.push('verification stamp desatualizado (stale) no ship');
454
446
  }
455
- if (signals.gate === 'review') out.push(verdictSummary(signals.evaluatorVerdict));
447
+ if (signals.gate === 'review') out.push(reviewSummary(signals.reviewReports));
456
448
  // Listado mesmo quando NÃO pausa, pela mesma razão do e2e: um projeto que não
457
449
  // declara caminho de prompt/modelo é indistinguível, no relatório, de um que
458
450
  // declara e não tocou em nada — e as duas coisas significam coisas diferentes.
@@ -464,11 +456,11 @@ export function getRiskSignals(signals) {
464
456
  out.push(`prompt/modelo tocado (${touched.length}) com eval ${signals.evalsStatus ?? 'não declarado'}`);
465
457
  }
466
458
  }
467
- if (signals.gate === 'plan' && Array.isArray(signals.oversizedTasks) && signals.oversizedTasks.length > 0) {
468
- 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}`);
469
461
  }
470
- if (typeof signals.taskCount === 'number' && signals.taskCount > SCOPE_TASK_THRESHOLD) {
471
- 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})`);
472
464
  }
473
465
  // Listado mesmo quando NÃO pausa, pela mesma razão do sinal irmão: a isenção
474
466
  // de uma feature que não gera mídia tem de ser VISÍVEL, senão é
@@ -476,7 +468,7 @@ export function getRiskSignals(signals) {
476
468
  if (signals.gate === 'review' && signals.mediaClaims?.featureGeneratesMedia === true) {
477
469
  out.push(signals.mediaClaims.section === true
478
470
  ? 'prova de mídia registrada'
479
- : '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)');
480
472
  }
481
473
 
482
474
  // Listado mesmo quando NÃO pausa: a isenção (sem mockup, legado, nada
@@ -493,7 +485,7 @@ export function getRiskSignals(signals) {
493
485
  : `checklist de tela completo (${screens.length} tela(s))`);
494
486
  } else if (uncovered.length > 0) {
495
487
  out.push(section !== true
496
- ? 'relatório sem a seção `## Checklist de tela`'
488
+ ? 'verifier.md sem a seção `## Checklist de tela`'
497
489
  : `checklist de tela sem conferência em ${uncovered.length} tela(s)`);
498
490
  } else if (screens.length === 0) {
499
491
  // O design passou pela uiux sem tela nenhuma: nada a cobrar no ship, mas
@@ -510,7 +502,6 @@ export function getRiskSignals(signals) {
510
502
  }
511
503
 
512
504
  export const RISK_CONSTANTS = {
513
- SCOPE_TASK_THRESHOLD,
514
505
  FORCED_PAUSE_WORK_TYPES,
515
506
  E2E_SKIP_CLASSES_THAT_PAUSE,
516
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
+ }