@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
@@ -0,0 +1,234 @@
1
+ import { createHash } from 'crypto';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
4
+ import { frozenBody } from '../../../framework/hooks/shared/errata.js';
5
+ import { readAgentsJson } from '../specs/screen-checklist.js';
6
+ import { findArtifactTerms } from './artifact-vocabulary.js';
7
+ import { PROOF_KINDS, REUSE_VERDICTS, ADR_SCOPES, SECTIONS, CONFLICTS_HEADING } from './plan-parser.js';
8
+
9
+ export const CHECKS_VERSION = 1;
10
+ export const MAX_SLICE_FILES = 8;
11
+ export const MAX_SLICES = 12;
12
+ export const MAX_PARALLEL = 4;
13
+
14
+ /** sha256 of the plan without its errata: appending a correction never invalidates checks.json. */
15
+ export function planHash(text) {
16
+ return createHash('sha256').update(frozenBody(text), 'utf8').digest('hex');
17
+ }
18
+
19
+ /**
20
+ * Persona ids the project can dispatch: the installed agents.json, else the package's.
21
+ *
22
+ * @param {string} cwd
23
+ * @returns {string[]|null} null when no agents.json is readable — personas then go unchecked
24
+ */
25
+ export function knownPersonas(cwd) {
26
+ const agents = readAgentsJson(cwd)?.agents;
27
+ return agents && typeof agents === 'object' ? Object.keys(agents) : null;
28
+ }
29
+
30
+ /** The ids on a dependency cycle, or null. */
31
+ export function dependencyCycle(slices) {
32
+ const deps = new Map(slices.map((s) => [s.id, s.depends ?? []]));
33
+ const state = new Map();
34
+ const stack = [];
35
+ const visit = (id) => {
36
+ if (state.get(id) === 'done' || !deps.has(id)) return null;
37
+ if (state.get(id) === 'open') return stack.slice(stack.indexOf(id)).concat(id);
38
+ state.set(id, 'open');
39
+ stack.push(id);
40
+ for (const dep of deps.get(id)) {
41
+ const cycle = visit(dep);
42
+ if (cycle) return cycle;
43
+ }
44
+ stack.pop();
45
+ state.set(id, 'done');
46
+ return null;
47
+ };
48
+ for (const id of deps.keys()) {
49
+ const cycle = visit(id);
50
+ if (cycle) return cycle;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ const fileKey = (f) => f.replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase();
56
+
57
+ /**
58
+ * Waves of slices that may run at the same time: a slice joins a wave only
59
+ * after every slice it depends on ran in an earlier one, and never with a
60
+ * slice that touches one of its files. At most MAX_PARALLEL per wave.
61
+ *
62
+ * @param {{ id: string, depends?: string[], files?: string[] }[]} slices
63
+ * @returns {string[][]} empty when the dependencies have a cycle
64
+ */
65
+ export function parallelGroups(slices) {
66
+ if (dependencyCycle(slices)) return [];
67
+ const ids = new Set(slices.map((s) => s.id));
68
+ const depth = new Map();
69
+ const depthOf = (slice) => {
70
+ if (depth.has(slice.id)) return depth.get(slice.id);
71
+ const parents = (slice.depends ?? []).filter((d) => ids.has(d)).map((d) => slices.find((s) => s.id === d));
72
+ const value = parents.length === 0 ? 0 : 1 + Math.max(...parents.map(depthOf));
73
+ depth.set(slice.id, value);
74
+ return value;
75
+ };
76
+ const layers = [];
77
+ for (const slice of slices) (layers[depthOf(slice)] ??= []).push(slice);
78
+
79
+ const groups = [];
80
+ for (const layer of layers.filter(Boolean)) {
81
+ const open = [];
82
+ for (const slice of layer) {
83
+ const files = new Set((slice.files ?? []).map(fileKey));
84
+ const group = open.find((g) => g.ids.length < MAX_PARALLEL && ![...files].some((f) => g.files.has(f)));
85
+ if (group) {
86
+ group.ids.push(slice.id);
87
+ files.forEach((f) => group.files.add(f));
88
+ } else {
89
+ open.push({ ids: [slice.id], files });
90
+ }
91
+ }
92
+ groups.push(...open.map((g) => g.ids));
93
+ }
94
+ return groups;
95
+ }
96
+
97
+ /**
98
+ * Judge a parsed plan.
99
+ *
100
+ * @param {ReturnType<import('./plan-parser.js').parsePlan>} plan
101
+ * @param {{ personas?: string[]|null, surfaces?: string[]|null }} [known]
102
+ * @returns {{ errors: string[], warnings: string[] }}
103
+ */
104
+ export function validatePlan(plan, { personas = null, surfaces = null } = {}) {
105
+ const errors = [];
106
+ const warnings = [];
107
+ const at = (line) => (line ? ` (linha ${line})` : '');
108
+
109
+ for (const key of ['problem', 'scenarios', 'decisions', 'slices']) {
110
+ if (!plan.present[key]) errors.push(`seção obrigatória ausente: ## ${SECTIONS[key]}`);
111
+ }
112
+ if (plan.present.decisions && !plan.conflicts) {
113
+ errors.push(`### ${CONFLICTS_HEADING} ausente em ## ${SECTIONS.decisions} — escreva "nenhum" quando não houver`);
114
+ } else if (plan.conflicts && !plan.conflicts.text) {
115
+ errors.push(`### ${CONFLICTS_HEADING} está vazio${at(plan.conflicts.line)} — escreva "nenhum" quando não houver`);
116
+ }
117
+ if (plan.sectionsAfterErrata.length > 0) {
118
+ errors.push(`## ${SECTIONS.errata} precisa ser a última seção — depois dela: ${plan.sectionsAfterErrata.join(', ')}`);
119
+ }
120
+
121
+ const duplicated = (items) => items.map((i) => i.id).filter((id, i, all) => all.indexOf(id) !== i);
122
+ for (const [label, items] of [['cenário', plan.scenarios], ['fatia', plan.slices], ['ADR', plan.adrs]]) {
123
+ for (const id of new Set(duplicated(items))) errors.push(`${label} duplicado: ${id}`);
124
+ }
125
+
126
+ if (plan.present.scenarios && plan.scenarios.length === 0) {
127
+ errors.push(`## ${SECTIONS.scenarios} sem nenhum cenário no formato "- **C1** — Dado …, quando …, então …"`);
128
+ }
129
+ for (const c of plan.scenarios) {
130
+ if (c.proof === null) {
131
+ errors.push(`${c.id} sem "prova:" (${PROOF_KINDS.join(' | ')})${at(c.line)}`);
132
+ } else if (!PROOF_KINDS.includes(c.proof.toLowerCase())) {
133
+ errors.push(`${c.id}: prova "${c.proof}" fora de ${PROOF_KINDS.join(' | ')}${at(c.line)}`);
134
+ }
135
+ const terms = findArtifactTerms(c.text);
136
+ if (terms.length > 0 && !c.artifactWhy) {
137
+ errors.push(`${c.id} descreve artefato ("${terms.join(', ')}") sem "> Por que artefato:" — reescreva como efeito observável ou justifique${at(c.line)}`);
138
+ }
139
+ }
140
+
141
+ const scenarioIds = new Set(plan.scenarios.map((c) => c.id));
142
+ const sliceIds = new Set(plan.slices.map((s) => s.id));
143
+ const covered = new Set();
144
+ for (const s of plan.slices) {
145
+ if (!s.persona) {
146
+ errors.push(`${s.id} sem "- persona:"${at(s.line)}`);
147
+ } else if (personas && !personas.includes(s.persona)) {
148
+ errors.push(`${s.id}: persona "${s.persona}" não existe em agents.json (${personas.join(', ')})${at(s.line)}`);
149
+ }
150
+ if (s.files.length > MAX_SLICE_FILES) {
151
+ errors.push(`${s.id} toca ${s.files.length} arquivos — o teto é ${MAX_SLICE_FILES}; divida a fatia${at(s.line)}`);
152
+ }
153
+ for (const c of s.covers) {
154
+ if (scenarioIds.has(c)) covered.add(c);
155
+ else errors.push(`${s.id} cobre ${c}, que não existe em ## ${SECTIONS.scenarios}${at(s.line)}`);
156
+ }
157
+ for (const d of s.depends) {
158
+ if (!sliceIds.has(d)) errors.push(`${s.id} depende de ${d}, que não existe${at(s.line)}`);
159
+ }
160
+ if (s.covers.length === 0) warnings.push(`${s.id} não cobre nenhum cenário${at(s.line)}`);
161
+ if (s.unknownKeys.length > 0) warnings.push(`${s.id}: chave desconhecida ${s.unknownKeys.join(', ')}${at(s.line)}`);
162
+ if (s.surface && surfaces && !surfaces.includes(s.surface)) {
163
+ warnings.push(`${s.id}: superfície "${s.surface}" não é uma de ${surfaces.join(', ')}${at(s.line)}`);
164
+ }
165
+ }
166
+ for (const c of plan.scenarios) {
167
+ if (!covered.has(c.id)) errors.push(`${c.id} não é coberto por nenhuma fatia ("- cobre: ${c.id}")`);
168
+ }
169
+ const cycle = dependencyCycle(plan.slices);
170
+ if (cycle) errors.push(`ciclo em "depende": ${cycle.join(' → ')}`);
171
+ if (plan.slices.length > MAX_SLICES) warnings.push(`${plan.slices.length} fatias — acima de ${MAX_SLICES}, considere dividir a feature`);
172
+
173
+ for (const r of plan.reuse) {
174
+ if (!REUSE_VERDICTS.includes(r.verdict)) {
175
+ errors.push(`## ${SECTIONS.reuse}: veredito "${r.verdict}" de "${r.concept}" fora de ${REUSE_VERDICTS.join(' | ')}${at(r.line)}`);
176
+ } else if (r.verdict === 'CRIAR' && !r.why) {
177
+ warnings.push(`## ${SECTIONS.reuse}: CRIAR "${r.concept}" sem why${at(r.line)}`);
178
+ }
179
+ }
180
+ for (const a of plan.adrs) {
181
+ if (!ADR_SCOPES.includes(a.scope)) warnings.push(`${a.id} sem **Alcance:** ${ADR_SCOPES.join(' | ')}${at(a.line)}`);
182
+ }
183
+
184
+ return { errors, warnings };
185
+ }
186
+
187
+ /** The checks.json document for a parsed plan. */
188
+ export function buildChecks(feature, text, plan) {
189
+ return {
190
+ version: CHECKS_VERSION,
191
+ feature,
192
+ planHash: planHash(text),
193
+ checks: plan.scenarios.map((c) => ({
194
+ id: c.id, text: c.text, proof: c.proof ? c.proof.toLowerCase() : null, artifactWhy: c.artifactWhy || null,
195
+ })),
196
+ slices: plan.slices.map((s) => ({
197
+ id: s.id, title: s.title, persona: s.persona, surface: s.surface,
198
+ standards: s.standards, files: s.files, depends: s.depends, covers: s.covers,
199
+ })),
200
+ reuse: plan.reuse.map(({ concept, verdict, target, why }) => ({ concept, verdict, target, why })),
201
+ adrs: plan.adrs.map((a) => ({ id: a.id, title: a.title, scope: ADR_SCOPES.includes(a.scope) ? a.scope : null })),
202
+ };
203
+ }
204
+
205
+ /**
206
+ * The feature's extracted checks and whether they still describe the plan on
207
+ * disk. plan-check writes them only with zero errors, so `fresh` also means
208
+ * the plan passed every plan-check rule (no slice over MAX_SLICE_FILES, every
209
+ * scenario covered).
210
+ *
211
+ * @returns {{ status: 'fresh'|'no-plan'|'no-checks'|'unreadable'|'stale', checks: object|null }}
212
+ */
213
+ export function readPlanChecks(cwd, featureName) {
214
+ const planPath = getAbsoluteOutputPath(cwd, featureName, 'plan');
215
+ const checksPath = getAbsoluteOutputPath(cwd, featureName, 'checks');
216
+ if (!existsSync(planPath)) return { status: 'no-plan', checks: null };
217
+ if (!existsSync(checksPath)) return { status: 'no-checks', checks: null };
218
+ let checks;
219
+ try {
220
+ checks = JSON.parse(readFileSync(checksPath, 'utf8'));
221
+ } catch {
222
+ return { status: 'unreadable', checks: null };
223
+ }
224
+ if (checks?.version !== CHECKS_VERSION || !Array.isArray(checks.slices) || !Array.isArray(checks.checks)) {
225
+ return { status: 'unreadable', checks: null };
226
+ }
227
+ let plan;
228
+ try {
229
+ plan = readFileSync(planPath, 'utf8');
230
+ } catch {
231
+ return { status: 'no-plan', checks };
232
+ }
233
+ return { status: checks.planHash === planHash(plan) ? 'fresh' : 'stale', checks };
234
+ }
@@ -0,0 +1,209 @@
1
+ import { tableCells, isTableLine, SEPARATOR_ROW_RE } from '../specs/gfm-table.js';
2
+ import { ARTIFACT_MARKER_RE } from './artifact-vocabulary.js';
3
+ import { markdownLines } from '../../../framework/hooks/shared/errata.js';
4
+
5
+ export const PROOF_KINDS = ['teste', 'e2e', 'eval', 'manual'];
6
+ export const REUSE_VERDICTS = ['REUSAR', 'ESTENDER', 'CRIAR'];
7
+ export const ADR_SCOPES = ['feature', 'projeto'];
8
+
9
+ export const SECTIONS = {
10
+ problem: 'Problema',
11
+ outOfScope: 'Fora de escopo',
12
+ assumptions: 'Suposições não confirmadas',
13
+ scenarios: 'Cenários',
14
+ decisions: 'Decisões',
15
+ architecture: 'Arquitetura',
16
+ reuse: 'Reuso',
17
+ slices: 'Fatias',
18
+ contract: 'Contrato de implementação',
19
+ errata: 'Errata',
20
+ };
21
+
22
+ export const CONFLICTS_HEADING = 'Conflitos com o acervo';
23
+
24
+ const SLICE_KEYS = {
25
+ persona: 'persona',
26
+ 'superfície': 'surface',
27
+ superficie: 'surface',
28
+ standards: 'standards',
29
+ arquivos: 'files',
30
+ depende: 'depends',
31
+ cobre: 'covers',
32
+ };
33
+
34
+ const DASH = String.raw`\s+[—–-]\s+`;
35
+ const SCENARIO_RE = new RegExp(String.raw`^[-*]\s+\*\*(C\d+)\*\*${DASH}(.+)$`);
36
+ const ADR_RE = new RegExp(String.raw`^###\s+(ADR-\d+)${DASH}(.+?)\s*$`);
37
+ const SLICE_RE = new RegExp(String.raw`^###\s+(F\d+)${DASH}(.+?)\s*$`);
38
+ const PROOF_RE = /^\s*prova\s*:\s*(.*?)\s*$/i;
39
+ const WHY_RE = new RegExp(String.raw`^\s*>\s*` + ARTIFACT_MARKER_RE.source + String.raw`\s*(.*?)\s*$`, 'i');
40
+ const SCOPE_RE = /^\s*\*\*Alcance:\*\*\s*(.*?)\s*$/i;
41
+ const SLICE_LINE_RE = /^\s*[-*]\s+([\p{L}]+)\s*:\s*(.*?)\s*$/u;
42
+
43
+ const fold = (s) => s.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
44
+
45
+ const listOf = (value) => value
46
+ .split(',')
47
+ .map((v) => v.replace(/`/g, '').trim())
48
+ .filter((v) => v && !/^[—–-]$/.test(v) && fold(v) !== 'nenhum' && fold(v) !== 'nenhuma');
49
+
50
+ function splitSections(lines) {
51
+ const sections = [];
52
+ let current = null;
53
+ for (const line of lines) {
54
+ const h2 = !line.fenced && /^##\s+(.+?)\s*$/.exec(line.raw);
55
+ if (h2 && !line.raw.startsWith('###')) {
56
+ current = { name: h2[1], line: line.n, lines: [] };
57
+ sections.push(current);
58
+ continue;
59
+ }
60
+ if (current) current.lines.push(line);
61
+ }
62
+ return sections;
63
+ }
64
+
65
+ function parseScenarios(lines) {
66
+ const scenarios = [];
67
+ let current = null;
68
+ for (const line of lines) {
69
+ if (line.fenced) { current = null; continue; }
70
+ const m = SCENARIO_RE.exec(line.raw);
71
+ if (m) {
72
+ current = { id: m[1], text: m[2].trim(), proof: null, artifactWhy: null, line: line.n };
73
+ scenarios.push(current);
74
+ continue;
75
+ }
76
+ if (!current) continue;
77
+ if (/^\s*$/.test(line.raw) || /^#/.test(line.raw) || /^[-*]\s/.test(line.raw)) { current = null; continue; }
78
+ const why = WHY_RE.exec(line.raw);
79
+ if (why) { current.artifactWhy = why[1] || ''; continue; }
80
+ const proof = PROOF_RE.exec(line.raw);
81
+ if (proof && current.proof === null) { current.proof = proof[1]; continue; }
82
+ if (/^\s*>/.test(line.raw)) continue;
83
+ current.text = `${current.text} ${line.raw.trim()}`;
84
+ }
85
+ return scenarios;
86
+ }
87
+
88
+ function parseDecisions(lines) {
89
+ const adrs = [];
90
+ let conflicts = null;
91
+ let current = null;
92
+ for (const line of lines) {
93
+ if (line.fenced) {
94
+ if (current?.kind === 'conflicts') conflicts.body.push(line.raw);
95
+ else if (current?.kind === 'adr') current.entry.body.push(line.raw);
96
+ continue;
97
+ }
98
+ const adr = ADR_RE.exec(line.raw);
99
+ if (adr) {
100
+ current = { kind: 'adr', entry: { id: adr[1], title: adr[2], scope: null, body: [], line: line.n } };
101
+ adrs.push(current.entry);
102
+ continue;
103
+ }
104
+ const h3 = /^###\s+(.+?)\s*$/.exec(line.raw);
105
+ if (h3) {
106
+ if (fold(h3[1]) === fold(CONFLICTS_HEADING)) {
107
+ conflicts = { line: line.n, body: [] };
108
+ current = { kind: 'conflicts' };
109
+ } else {
110
+ current = null;
111
+ }
112
+ continue;
113
+ }
114
+ if (current?.kind === 'adr') {
115
+ current.entry.body.push(line.raw);
116
+ const scope = current.entry.scope === null && SCOPE_RE.exec(line.raw);
117
+ if (scope) current.entry.scope = scope[1].replace(/`/g, '').trim().toLowerCase();
118
+ } else if (current?.kind === 'conflicts') {
119
+ conflicts.body.push(line.raw);
120
+ }
121
+ }
122
+ for (const a of adrs) a.body = a.body.join('\n').trim();
123
+ return {
124
+ adrs,
125
+ conflicts: conflicts && { line: conflicts.line, text: conflicts.body.join('\n').trim() },
126
+ };
127
+ }
128
+
129
+ function parseReuse(lines) {
130
+ const rows = [];
131
+ let header = null;
132
+ for (const line of lines) {
133
+ if (line.fenced || !isTableLine(line.raw) || SEPARATOR_ROW_RE.test(line.raw)) continue;
134
+ const cells = tableCells(line.raw);
135
+ if (!header) { header = cells.map(fold); continue; }
136
+ const at = (name) => cells[header.indexOf(name)] ?? '';
137
+ rows.push({
138
+ concept: at('conceito'),
139
+ verdict: at('veredito').replace(/`|\*/g, '').trim().toUpperCase(),
140
+ target: at('alvo'),
141
+ why: at('why'),
142
+ line: line.n,
143
+ });
144
+ }
145
+ return { header, rows };
146
+ }
147
+
148
+ function parseSlices(lines) {
149
+ const slices = [];
150
+ let current = null;
151
+ for (const line of lines) {
152
+ if (line.fenced) continue;
153
+ const m = SLICE_RE.exec(line.raw);
154
+ if (m) {
155
+ current = {
156
+ id: m[1], title: m[2], persona: null, surface: null,
157
+ standards: [], files: [], depends: [], covers: [], unknownKeys: [], line: line.n,
158
+ };
159
+ slices.push(current);
160
+ continue;
161
+ }
162
+ if (/^###\s/.test(line.raw)) { current = null; continue; }
163
+ if (!current) continue;
164
+ const kv = SLICE_LINE_RE.exec(line.raw);
165
+ if (!kv) continue;
166
+ const key = SLICE_KEYS[fold(kv[1])] ?? SLICE_KEYS[kv[1].toLowerCase()];
167
+ if (!key) { current.unknownKeys.push(kv[1]); continue; }
168
+ if (key === 'persona' || key === 'surface') {
169
+ const value = kv[2].replace(/`/g, '').trim();
170
+ current[key] = value && !/^[—–-]$/.test(value) ? value : null;
171
+ } else {
172
+ current[key] = listOf(kv[2]);
173
+ }
174
+ }
175
+ return slices;
176
+ }
177
+
178
+ /**
179
+ * Parse `2-plan/plan.md` into its structure. Pure: no validation, no I/O —
180
+ * `checks.js` judges what this returns.
181
+ *
182
+ * @param {string} text
183
+ */
184
+ export function parsePlan(text) {
185
+ const lines = markdownLines(text);
186
+ const sections = splitSections(lines);
187
+ const byName = new Map();
188
+ for (const s of sections) {
189
+ const key = Object.keys(SECTIONS).find((k) => fold(SECTIONS[k]) === fold(s.name));
190
+ if (key && !byName.has(key)) byName.set(key, s);
191
+ }
192
+ const body = (key) => byName.get(key)?.lines ?? [];
193
+ const decisions = parseDecisions(body('decisions'));
194
+ const reuse = parseReuse(body('reuse'));
195
+ const errata = byName.get('errata');
196
+
197
+ return {
198
+ sections: sections.map((s) => ({ name: s.name, line: s.line, key: [...byName].find(([, v]) => v === s)?.[0] ?? null })),
199
+ present: Object.fromEntries(Object.keys(SECTIONS).map((k) => [k, byName.has(k)])),
200
+ scenarios: parseScenarios(body('scenarios')),
201
+ adrs: decisions.adrs,
202
+ conflicts: decisions.conflicts,
203
+ reuse: reuse.rows,
204
+ reuseHeader: reuse.header,
205
+ slices: parseSlices(body('slices')),
206
+ errata: errata ? errata.lines.map((l) => l.raw).join('\n').trim() : null,
207
+ sectionsAfterErrata: errata ? sections.filter((s) => s.line > errata.line).map((s) => s.name) : [],
208
+ };
209
+ }
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from 'fs';
2
+ import { templateSourcePath, writeIfAbsent } from '../../core/templates/template-seed.js';
3
+ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
4
+
5
+ const FEATURE_PLACEHOLDER = /\{\{FEATURE_NAME\}\}/g;
6
+
7
+ export function planTemplatePath(cwd = process.cwd()) {
8
+ return templateSourcePath('plan', cwd);
9
+ }
10
+
11
+ export function renderPlanSkeleton(featureName, cwd = process.cwd()) {
12
+ return readFileSync(planTemplatePath(cwd), 'utf8').replace(FEATURE_PLACEHOLDER, featureName);
13
+ }
14
+
15
+ /** @returns {{ path: string, written: boolean }} `written: false` when the file already existed. */
16
+ export function writePlanSkeleton(featureName, cwd = process.cwd()) {
17
+ return writeIfAbsent(getAbsoluteOutputPath(cwd, featureName, 'plan'), renderPlanSkeleton(featureName, cwd));
18
+ }
@@ -0,0 +1,222 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { OUTPUT_SCHEMA, getAbsoluteOutputPath } from '../core/paths/output-schema.js';
4
+ import { PARTIAL_MARKER } from '../../framework/hooks/shared/slice-progress.js';
5
+
6
+ export const VERDICT_TOKEN = '**Verdict:**';
7
+ export const REVIEWED_SHA_TOKEN = '**Reviewed-SHA:**';
8
+ export const VERDICTS = ['SHIP', 'FIX'];
9
+ export const BLOCKER_TYPES = ['check', 'teste', 'regressao', 'seguranca', 'escopo', 'arquitetura', 'performance'];
10
+ export const CHECK_STATUSES = ['PROVADO', 'NÃO PROVADO', 'FALHA'];
11
+
12
+ export const REVIEW_ROLES = {
13
+ verifier: { output: 'verifierReport', persona: 'morph-verifier' },
14
+ reviewer: { output: 'reviewerReport', persona: 'morph-reviewer' },
15
+ security: { output: 'securityReport', persona: 'morph-security' },
16
+ };
17
+ const ROLE_ORDER = Object.keys(REVIEW_ROLES);
18
+
19
+ export const reviewFileName = (role) => OUTPUT_SCHEMA[REVIEW_ROLES[role].output].filename;
20
+
21
+ // Append-only: uma reavaliação acrescenta ao fim, e vale o ÚLTIMO token/seção.
22
+ const VERDICT_RE = /\*\*Verdict:\*\*\s*([A-Za-z]+)/g;
23
+ const REVIEWED_SHA_RE = /\*\*Reviewed-SHA:\*\*\s*`?([0-9a-f]{7,40})/gi;
24
+ const sectionRe = (title) => new RegExp(`^##\\s+${title}\\s*$`, 'gim');
25
+ const BLOCKERS_SECTION_RE = sectionRe('Bloqueadores');
26
+ const CHECKS_SECTION_RE = sectionRe('Checks');
27
+ const MEDIA_SECTION_RE = /^##\s+M[íi]dia\s+gerada\s*$/m;
28
+ const SCREEN_CHECKLIST_SECTION_RE = sectionRe('Checklist\\s+de\\s+tela');
29
+
30
+ /** Hotfix: só o verificador, escrito pelo orquestrador sem dispatch. */
31
+ export function requiredReviewRoles({ workType, securityRequired = false } = {}) {
32
+ if (workType === 'hotfix') return ['verifier'];
33
+ return securityRequired ? ['verifier', 'reviewer', 'security'] : ['verifier', 'reviewer'];
34
+ }
35
+
36
+ function sectionBodies(text, re) {
37
+ return [...text.matchAll(re)].map((m) => {
38
+ const rest = text.slice(m.index + m[0].length);
39
+ const next = rest.search(/^#{1,2}\s/m);
40
+ return next === -1 ? rest : rest.slice(0, next);
41
+ });
42
+ }
43
+
44
+ function lastSectionBody(text, re) {
45
+ const bodies = sectionBodies(text, re);
46
+ return bodies.length ? bodies[bodies.length - 1] : null;
47
+ }
48
+
49
+ const normalize = (s) => String(s ?? '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/\s+/g, ' ').trim();
50
+ const cleanId = (s) => String(s ?? '').replace(/[*`_]/g, '').trim().toUpperCase();
51
+
52
+ function tableRows(body) {
53
+ const rows = [];
54
+ for (const line of body.split(/\r?\n/)) {
55
+ if (!/^\s*\|/.test(line)) continue;
56
+ const cells = line.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
57
+ if (cells.every((c) => /^:?-{2,}:?$/.test(c) || c === '')) continue;
58
+ rows.push(cells);
59
+ }
60
+ return rows;
61
+ }
62
+
63
+ /** `| id | tipo | onde | evidência |`; `untyped` = tipo fora de {@link BLOCKER_TYPES}. */
64
+ function parseBlockers(body) {
65
+ const blockers = [];
66
+ const untyped = [];
67
+ for (const cells of tableRows(body)) {
68
+ if (/^#$|^id$/i.test(cells[0]) && /^tipo$/i.test(cells[1] ?? '')) continue;
69
+ const row = { id: cleanId(cells[0]), type: normalize(cells[1]), where: cells[2] ?? '', evidence: cells[3] ?? '' };
70
+ (BLOCKER_TYPES.includes(row.type) ? blockers : untyped).push(row);
71
+ }
72
+ return { blockers, untyped };
73
+ }
74
+
75
+ const STATUS_BY_KEY = Object.fromEntries(CHECK_STATUSES.map((s) => [normalize(s), s]));
76
+
77
+ /**
78
+ * `| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |` de TODAS as seções: uma confirmação
79
+ * traz só os Cn que mudou, então vale a última linha de cada Cn. Status por igualdade exata.
80
+ */
81
+ function parseChecks(bodies) {
82
+ const byId = new Map();
83
+ for (const cells of bodies.flatMap(tableRows)) {
84
+ const id = cleanId(cells[0]);
85
+ if (!/^C\d+$/.test(id)) continue;
86
+ const raw = String(cells[1] ?? '').replace(/[*`]/g, '').trim();
87
+ byId.delete(id);
88
+ byId.set(id, { id, status: STATUS_BY_KEY[normalize(raw)] ?? raw, evidence: cells[2] ?? '' });
89
+ }
90
+ return [...byId.values()];
91
+ }
92
+
93
+ /** Um relatório de review lido do texto. `checks` é `null` sem a seção `## Checks`. */
94
+ export function parseReviewReport(text) {
95
+ if (typeof text !== 'string') {
96
+ return { partial: false, verdict: undefined, blockers: [], untyped: [], superseded: [], checks: null, reviewedSha: null };
97
+ }
98
+ const firstLine = text.split(/\r?\n/).find((l) => l.trim() !== '') ?? '';
99
+ const verdicts = [...text.matchAll(VERDICT_RE)].map((m) => m[1].toUpperCase());
100
+ const blockersBody = lastSectionBody(text, BLOCKERS_SECTION_RE);
101
+ const checksBodies = sectionBodies(text, CHECKS_SECTION_RE);
102
+ const shas = [...text.matchAll(REVIEWED_SHA_RE)].map((m) => m[1]);
103
+ return {
104
+ partial: PARTIAL_MARKER.test(firstLine),
105
+ verdict: verdicts.length ? verdicts[verdicts.length - 1] : undefined,
106
+ ...(blockersBody === null ? { blockers: [], untyped: [] } : parseBlockers(blockersBody)),
107
+ superseded: verdicts.slice(0, -1),
108
+ checks: checksBodies.length === 0 ? null : parseChecks(checksBodies),
109
+ reviewedSha: shas.length ? shas[shas.length - 1] : null,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * `missing`: sem arquivo. `partial`: `PARCIAL` na 1ª linha, com ou sem veredito.
115
+ * `unverdicted`: sem `**Verdict:** SHIP|FIX`. `ok`: legível — quem decide é o risk-detector.
116
+ */
117
+ function stateOf(path, text) {
118
+ if (text === null) return { status: 'missing', path };
119
+ const parsed = parseReviewReport(text);
120
+ if (parsed.partial) return { status: 'partial', path, ...parsed };
121
+ if (!VERDICTS.includes(parsed.verdict)) return { status: 'unverdicted', path, ...parsed };
122
+ return { status: 'ok', path, ...parsed };
123
+ }
124
+
125
+ function readText(path) {
126
+ try {
127
+ return existsSync(path) ? readFileSync(path, 'utf8') : null;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ /** Os três relatórios a partir do diretório da feature — sem git; o dashboard lê árvores arquivadas. */
134
+ export function reviewReportsFromDir(featureDir) {
135
+ const out = {};
136
+ for (const role of ROLE_ORDER) {
137
+ const path = join(featureDir, '4-review', reviewFileName(role));
138
+ out[role] = stateOf(path, readText(path));
139
+ }
140
+ return out;
141
+ }
142
+
143
+ export function reviewReportPath(cwd, featureName, role) {
144
+ return getAbsoluteOutputPath(cwd, featureName, REVIEW_ROLES[role].output);
145
+ }
146
+
147
+ export function reviewReports(cwd, featureName) {
148
+ return reviewReportsFromDir(join(cwd, '.morph', 'features', featureName));
149
+ }
150
+
151
+ /**
152
+ * Todo Cn precisa de uma linha, e só `PROVADO` cobre.
153
+ *
154
+ * @param {string[]} checkIds
155
+ * @param {{id: string, status: string}[]|null} rows - `parseReviewReport(...).checks`
156
+ * @returns {{ missing: string[], notProven: {id: string, status: string}[] }}
157
+ */
158
+ export function checksCoverage(checkIds, rows) {
159
+ const byId = new Map((rows ?? []).map((r) => [r.id, r]));
160
+ const missing = [];
161
+ const notProven = [];
162
+ for (const id of checkIds.map(cleanId)) {
163
+ const row = byId.get(id);
164
+ if (!row) missing.push(id);
165
+ else if (row.status !== 'PROVADO') notProven.push({ id, status: row.status || 'vazio' });
166
+ }
167
+ return { missing, notProven };
168
+ }
169
+
170
+ /**
171
+ * O sinal que `reviewPause` (risk-detector) lê; `checkIds: null` = cobertura não conferível.
172
+ *
173
+ * @param {ReturnType<typeof reviewReportsFromDir>} reports
174
+ * @param {{ required: string[], checkIds: string[]|null }} opts
175
+ */
176
+ export function reviewSignal(reports, { required, checkIds }) {
177
+ const out = { required, reports: {} };
178
+ for (const role of ROLE_ORDER) {
179
+ const r = reports[role];
180
+ if (!required.includes(role) && r.status === 'missing') continue;
181
+ out.reports[role] = r.status === 'missing'
182
+ ? { status: 'missing' }
183
+ : {
184
+ status: r.status,
185
+ verdict: r.verdict ?? null,
186
+ blockers: r.blockers.length,
187
+ blockerIds: r.blockers.map((b) => b.id),
188
+ untyped: r.untyped.length,
189
+ superseded: r.superseded,
190
+ };
191
+ }
192
+ out.checks = checkIds === null
193
+ ? { status: 'no-checks', missing: [], notProven: [] }
194
+ : { status: 'ok', total: checkIds.length, ...checksCoverage(checkIds, reports.verifier.checks) };
195
+ return out;
196
+ }
197
+
198
+ /**
199
+ * Veredito combinado para leitura (dashboard, list): FIX se qualquer relatório
200
+ * presente diz FIX ou tem bloqueador; SHIP só com todo exigido em SHIP.
201
+ */
202
+ export function combinedVerdict(reports, required) {
203
+ const present = ROLE_ORDER.filter((r) => reports[r].status !== 'missing');
204
+ if (present.some((r) => reports[r].verdict === 'FIX' || reports[r].blockers.length > 0)) return 'FIX';
205
+ return required.every((r) => reports[r].status === 'ok' && reports[r].verdict === 'SHIP') ? 'SHIP' : null;
206
+ }
207
+
208
+ /** Todos os bloqueadores tipados dos relatórios presentes, com o papel que os abriu. */
209
+ export function allBlockers(reports) {
210
+ return ROLE_ORDER.flatMap((role) => (reports[role].blockers ?? []).map((b) => ({ ...b, role })));
211
+ }
212
+
213
+ export function verifierMediaSection(cwd, featureName) {
214
+ const text = readText(reviewReportPath(cwd, featureName, 'verifier'));
215
+ return text !== null && MEDIA_SECTION_RE.test(text);
216
+ }
217
+
218
+ export function verifierScreenChecklist(cwd, featureName) {
219
+ const text = readText(reviewReportPath(cwd, featureName, 'verifier'));
220
+ const body = text === null ? null : lastSectionBody(text, SCREEN_CHECKLIST_SECTION_RE);
221
+ return body === null ? { section: false, body: '' } : { section: true, body };
222
+ }