@polymorphism-tech/morph-spec 8.37.1 → 8.40.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 (317) hide show
  1. package/CLAUDE.md +37 -13
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +75 -49
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +6 -5
  8. package/framework/MORPH.md +25 -22
  9. package/framework/agents.json +96 -36
  10. package/framework/docs/CLI.md +54 -17
  11. package/framework/evals/rubrics/code-quality.md +5 -0
  12. package/framework/evals/rubrics/condensed.md +6 -2
  13. package/framework/evals/rubrics/contracts.md +20 -0
  14. package/framework/evals/rubrics/test-coverage.md +17 -6
  15. package/framework/hooks/README.md +9 -3
  16. package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
  17. package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
  18. package/framework/hooks/claude-code/core/validate-completion.js +16 -90
  19. package/framework/hooks/claude-code/core/worktree-context.js +10 -0
  20. package/framework/hooks/shared/command-tokens.js +96 -0
  21. package/framework/hooks/shared/git-exclude.js +44 -0
  22. package/framework/hooks/shared/git-paths.js +52 -0
  23. package/framework/hooks/shared/graph-freshness.js +380 -0
  24. package/framework/hooks/shared/graph-refresh.js +416 -0
  25. package/framework/hooks/shared/morph-cli.js +17 -5
  26. package/framework/hooks/shared/redact-secrets.js +55 -0
  27. package/framework/hooks/shared/telemetry-logger.js +25 -2
  28. package/framework/hooks/shared/transcript-usage.js +80 -0
  29. package/framework/mcp/registry.json +33 -2
  30. package/framework/rubrics/backend.json +2 -2
  31. package/framework/rubrics/default.json +2 -2
  32. package/framework/rubrics/frontend.json +2 -2
  33. package/framework/rules/csharp-standards.md +1 -0
  34. package/framework/rules/infrastructure-standards.md +0 -1
  35. package/framework/rules/morph-workflow.md +10 -4
  36. package/framework/rules/nextjs-standards.md +1 -0
  37. package/framework/schemas/config.schema.json +41 -1
  38. package/framework/schemas/feature.schema.json +5 -1
  39. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  40. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  41. package/framework/skills/core/morph-apply/SKILL.md +40 -11
  42. package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
  43. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  44. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  45. package/framework/skills/core/morph-implement/SKILL.md +44 -10
  46. package/framework/skills/core/morph-media/SKILL.md +1 -1
  47. package/framework/skills/core/morph-plan/SKILL.md +73 -15
  48. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  49. package/framework/skills/core/morph-review/SKILL.md +51 -13
  50. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  51. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  52. package/framework/skills/core/morph-uiux/SKILL.md +39 -13
  53. package/framework/standards/STANDARDS.json +189 -14
  54. package/framework/standards/ai-agents/context-providers.md +16 -26
  55. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  56. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  57. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  58. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  59. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  60. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  61. package/framework/standards/ai-agents/testing-ai.md +2 -1
  62. package/framework/standards/architecture/code-comments.md +100 -0
  63. package/framework/standards/backend/dotnet/testing.md +22 -0
  64. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  65. package/framework/standards/frontend/admin-app/data-table.md +562 -0
  66. package/framework/standards/frontend/admin-app/feedback.md +236 -0
  67. package/framework/standards/frontend/admin-app/forms.md +232 -0
  68. package/framework/standards/frontend/admin-app/page-structure.md +203 -0
  69. package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
  70. package/framework/standards/frontend/nextjs/components.md +37 -58
  71. package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
  72. package/framework/standards/frontend/nextjs/forms.md +31 -30
  73. package/framework/standards/frontend/nextjs/state-management.md +2 -1
  74. package/framework/standards/frontend/nextjs/testing.md +18 -0
  75. package/framework/standards/integration/mcp/mcp-tools.md +48 -117
  76. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  77. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  78. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  79. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  80. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  81. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  82. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  83. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  84. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  85. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  86. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  87. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  88. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  89. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  90. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  91. package/framework/templates/code/dotnet/test.cs +2 -87
  92. package/framework/templates/code/typescript/contracts.ts +140 -167
  93. package/framework/templates/context/design-system.md +3 -3
  94. package/framework/templates/docs/proposal.md +11 -0
  95. package/framework/templates/docs/spec.md +15 -0
  96. package/framework/templates/docs/ui-mockups.md +115 -98
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  118. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  119. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  120. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  121. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  122. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  123. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  124. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  125. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  126. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  127. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  128. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  129. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  130. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  131. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  132. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  133. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  134. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  135. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  136. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  137. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  138. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  139. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  140. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  141. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  142. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  143. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  144. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  145. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  146. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  147. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  148. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  149. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  150. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  151. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  152. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  153. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  154. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  155. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  156. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  157. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  158. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  159. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  160. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  161. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  162. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  163. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  164. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  165. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  166. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  167. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  168. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  169. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  170. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  171. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  172. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  173. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  174. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  175. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  176. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  177. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  178. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  179. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  180. package/framework/templates/feature/decisions.md +124 -122
  181. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  182. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  183. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  184. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  185. package/framework/templates/integrations/asaas-client.cs +332 -387
  186. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  187. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  188. package/framework/templates/project/validate.js +97 -120
  189. package/package.json +2 -2
  190. package/src/commands/dashboard/dashboard.js +80 -53
  191. package/src/commands/graph/affected.js +4 -3
  192. package/src/commands/graph/build.js +109 -18
  193. package/src/commands/graph/explain.js +8 -5
  194. package/src/commands/graph/status.js +14 -8
  195. package/src/commands/mcp/mcp-setup.js +37 -9
  196. package/src/commands/observability/cost.js +148 -18
  197. package/src/commands/project/doctor.js +85 -15
  198. package/src/commands/project/init.js +6 -3
  199. package/src/commands/project/install-plugin-cmd.js +1 -1
  200. package/src/commands/project/update.js +30 -0
  201. package/src/commands/project/worktree.js +73 -90
  202. package/src/commands/state/advance.js +18 -0
  203. package/src/commands/state/approve.js +15 -0
  204. package/src/commands/state/archive.js +62 -9
  205. package/src/commands/state/finish.js +62 -14
  206. package/src/commands/state/gate-check.js +8 -1
  207. package/src/commands/state/gate-decision.js +8 -4
  208. package/src/commands/state/report-check.js +90 -9
  209. package/src/commands/state/usecase.js +66 -0
  210. package/src/commands/tasks/dag.js +17 -2
  211. package/src/commands/validation/validate-feature.js +6 -1
  212. package/src/commands/validation/verify.js +20 -1
  213. package/src/dashboard/app/App.jsx +80 -16
  214. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  215. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  216. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  217. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  218. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  219. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  220. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  221. package/src/dashboard/app/components/Markdown.jsx +47 -0
  222. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  223. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  224. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  225. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  226. package/src/dashboard/app/hooks/usePref.js +38 -0
  227. package/src/dashboard/app/lib/format.js +69 -0
  228. package/src/dashboard/app/lib/phases.js +56 -0
  229. package/src/dashboard/app/lib/project.js +50 -0
  230. package/src/dashboard/app/pages/Activity.jsx +24 -0
  231. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  232. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  233. package/src/dashboard/app/pages/Features.jsx +400 -0
  234. package/src/dashboard/app/pages/Guide.jsx +146 -0
  235. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  236. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  237. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  238. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  239. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  240. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  241. package/src/dashboard/app/pages/Standards.jsx +407 -332
  242. package/src/dashboard/app/styles/index.css +187 -47
  243. package/src/dashboard/index.html +14 -12
  244. package/src/dashboard/package.json +32 -24
  245. package/src/dashboard/scanner/activity-scanner.js +351 -0
  246. package/src/dashboard/scanner/agents-scanner.js +63 -1
  247. package/src/dashboard/scanner/docs-scanner.js +59 -0
  248. package/src/dashboard/scanner/feature-status.js +10 -0
  249. package/src/dashboard/scanner/features-scanner.js +360 -0
  250. package/src/dashboard/scanner/hooks-scanner.js +8 -1
  251. package/src/dashboard/scanner/index.js +14 -7
  252. package/src/dashboard/scanner/phases-scanner.js +14 -15
  253. package/src/dashboard/scanner/portfolio.js +153 -0
  254. package/src/dashboard/scanner/project-api.js +195 -0
  255. package/src/dashboard/scanner/rules-scanner.js +42 -37
  256. package/src/dashboard/scanner/skills-scanner.js +105 -43
  257. package/src/dashboard/scanner/standards-scanner.js +4 -1
  258. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  259. package/src/lib/ai/comment-stripper.js +101 -4
  260. package/src/lib/detectors/claude-config-detector.js +2 -7
  261. package/src/lib/detectors/index.js +5 -1
  262. package/src/lib/evaluator-report.js +47 -0
  263. package/src/lib/generators/handoff-generator.js +4 -18
  264. package/src/lib/generators/settings-generator.js +208 -209
  265. package/src/lib/git/default-branch.js +52 -0
  266. package/src/lib/git/tree-fingerprint.js +58 -0
  267. package/src/lib/git/tree-state.js +167 -0
  268. package/src/lib/graph/god-nodes.js +12 -3
  269. package/src/lib/graph/graph-health.js +48 -2
  270. package/src/lib/graph/graph-loader.js +99 -25
  271. package/src/lib/graph/graphifyignore.js +145 -28
  272. package/src/lib/graph/sanitize.js +117 -11
  273. package/src/lib/installers/mcp-installer.js +724 -719
  274. package/src/lib/installers/memory-installer.js +1 -2
  275. package/src/lib/projects/registry.js +154 -0
  276. package/src/lib/risk-detector.js +90 -0
  277. package/src/lib/specs/acervo-format.js +70 -0
  278. package/src/lib/specs/acervo-lifecycle.js +264 -0
  279. package/src/lib/specs/adr-index.js +169 -0
  280. package/src/lib/specs/revocations.js +156 -0
  281. package/src/lib/specs/screen-checklist.js +357 -0
  282. package/src/lib/specs/usecase-check.js +20 -16
  283. package/src/lib/specs/usecase-promoter.js +103 -38
  284. package/src/lib/specs/usecase-registry.js +7 -7
  285. package/src/lib/tasks/tasks-json-parser.js +2 -3
  286. package/src/lib/telemetry/metrics.js +386 -0
  287. package/src/lib/telemetry/telemetry-reader.js +161 -51
  288. package/src/lib/telemetry/verify-event.js +79 -0
  289. package/src/lib/validators/code-budget/code-budget-validator.js +200 -0
  290. package/src/lib/validators/code-budget/comment-scanner.js +75 -0
  291. package/src/lib/validators/code-budget/index.js +9 -0
  292. package/src/lib/validators/reuse/reuse-validator.js +4 -2
  293. package/src/lib/validators/reuse/symbol-scanner.js +27 -22
  294. package/src/lib/validators/validation-runner.js +21 -5
  295. package/src/lib/validators/verify-runner.js +11 -0
  296. package/src/lib/worktree/finish-backup.js +739 -0
  297. package/src/lib/worktree/ignored-triage.js +15 -8
  298. package/src/lib/worktree/worktree-audit.js +5 -0
  299. package/src/scripts/install-plugin.js +1 -1
  300. package/src/scripts/setup-infra.js +31 -22
  301. package/src/utils/agents-installer.js +338 -24
  302. package/src/utils/banner.js +1 -1
  303. package/src/utils/hooks-installer.js +75 -15
  304. package/src/commands/index.js +0 -13
  305. package/src/commands/project/checkpoint.js +0 -209
  306. package/src/core/state/phase-state-machine.js +0 -197
  307. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  308. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  309. package/src/dashboard/scanner/squads-scanner.js +0 -13
  310. package/src/dashboard/scanner/state-scanner.js +0 -85
  311. package/src/lib/generators/index.js +0 -10
  312. package/src/lib/hooks/hook-executor.js +0 -167
  313. package/src/lib/validators/checkpoint-persistence.js +0 -39
  314. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  315. package/src/lib/validators/shared/index.js +0 -12
  316. package/src/lib/validators/shared/issue-counter.js +0 -14
  317. package/src/lib/validators/shared/result-formatter.js +0 -124
@@ -0,0 +1,169 @@
1
+ /**
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);
18
+ * - 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).
20
+ */
21
+
22
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
23
+ import { basename, join } from 'path';
24
+ import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
25
+ import { readFrontmatterField, setFrontmatterField } from './usecase-promoter.js';
26
+
27
+ /** Project-relative path of the index folder (git pathspec-ready). */
28
+ export const ADR_INDEX_PATHSPEC = join('.morph', 'specs', 'adrs');
29
+
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;
34
+ const INDEX_FILE_RE = /^([a-z0-9][a-z0-9-]*)--(ADR-\d+)\.md$/;
35
+
36
+ /** `admin/ADR-001` ↔ `admin--ADR-001.md`. */
37
+ export const adrFileName = (ref) => `${ref.replace('/', '--')}.md`;
38
+ const refOf = (name) => {
39
+ const m = INDEX_FILE_RE.exec(name);
40
+ return m ? `${m[1]}/${m[2]}` : null;
41
+ };
42
+
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
+ function renderAdrFile({ ref, title, body }, feature) {
75
+ const decisions = `.morph/archive/${feature}/${OUTPUT_SCHEMA.decisions.phaseDir}/${OUTPUT_SCHEMA.decisions.filename}`;
76
+ return [
77
+ '---',
78
+ `adr: ${ref}`,
79
+ `title: ${JSON.stringify(title)}`,
80
+ `source: ${decisions}`,
81
+ '---',
82
+ '',
83
+ `# ${ref}: ${title}`,
84
+ '',
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.',
87
+ '',
88
+ body,
89
+ '',
90
+ ].join('\n');
91
+ }
92
+
93
+ /**
94
+ * The ADRs the index holds; `[]` when there is none or it cannot be read.
95
+ *
96
+ * @returns {Array<{ ref: string, file: string, title: string|null, scope: string|null }>}
97
+ */
98
+ export function readAdrIndex(cwd) {
99
+ const dir = join(cwd, ADR_INDEX_PATHSPEC);
100
+ try {
101
+ if (!existsSync(dir)) return [];
102
+ return readdirSync(dir).sort().flatMap((file) => {
103
+ const ref = refOf(file);
104
+ if (!ref) return [];
105
+ let content = '';
106
+ try { content = readFileSync(join(dir, file), 'utf8'); } catch { /* listed, unreadable */ }
107
+ return [{ ref, file, title: readFrontmatterField(content, 'title'), scope: readFrontmatterField(content, 'scope') }];
108
+ });
109
+ } catch {
110
+ return [];
111
+ }
112
+ }
113
+
114
+ /**
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).
118
+ *
119
+ * @param {string} cwd
120
+ * @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
+ * @returns {{ changed: boolean, added: string[], revoked: string[], narrowed: string[],
123
+ * problems: Array<{ id: string, reason: string, message: string }> }}
124
+ */
125
+ export function updateAdrIndex(cwd, featureDir, revocations = []) {
126
+ const result = { changed: false, added: [], revoked: [], narrowed: [], problems: [] };
127
+ const feature = basename(featureDir);
128
+ const dir = join(cwd, ADR_INDEX_PATHSPEC);
129
+
130
+ let marked = [];
131
+ 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 */ }
135
+
136
+ for (const { adr, title, body } of marked) {
137
+ const ref = `${feature}/${adr}`;
138
+ const path = join(dir, adrFileName(ref));
139
+ const next = renderAdrFile({ ref, title, body }, feature);
140
+ const existed = existsSync(path);
141
+ if (existed && readFileSync(path, 'utf8') === next) continue;
142
+ mkdirSync(dir, { recursive: true });
143
+ writeFileSync(path, next, 'utf8');
144
+ result.changed = true;
145
+ if (!existed) result.added.push(ref);
146
+ }
147
+
148
+ for (const rev of revocations) {
149
+ const path = join(dir, adrFileName(rev.id));
150
+ if (!existsSync(path)) {
151
+ result.problems.push({
152
+ id: rev.id,
153
+ 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`,
155
+ });
156
+ continue;
157
+ }
158
+ if (rev.change === 'total') {
159
+ rmSync(path, { force: true });
160
+ result.revoked.push(rev.id);
161
+ } else {
162
+ writeFileSync(path, setFrontmatterField(readFileSync(path, 'utf8'), 'scope', JSON.stringify(rev.scope)), 'utf8');
163
+ result.narrowed.push(rev.id);
164
+ }
165
+ result.changed = true;
166
+ }
167
+
168
+ return result;
169
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The plan's declaration of what a feature supersedes in the project's acervo:
3
+ * the "UCs e ADRs que esta feature altera ou revoga" section of `spec.md`.
4
+ *
5
+ * ## UCs e ADRs que esta feature altera ou revoga
6
+ *
7
+ * | Id | Mudança | Substituto | Escopo em que o antigo ainda vale |
8
+ * |---|---|---|---|
9
+ * | UC-{nn} | parcial | UC-{nn} | {onde o antigo ainda vale} |
10
+ * | {feature}/ADR-{nnn} | total | — | — |
11
+ *
12
+ * Columns are positional, so the header wording is free. Parsing is pure: the
13
+ * finish applies the entries (`acervo-lifecycle.js`), and `dag`/`usecase
14
+ * check` report the problems while `spec.md` can still be edited — it freezes
15
+ * at Gate 2, so a bad row found at close time can no longer be fixed there.
16
+ *
17
+ * Nothing ambiguous is applied: a row with any problem, and every row of an id
18
+ * declared twice, stays out of `entries`. A table inside a code fence is an
19
+ * example, never a declaration.
20
+ */
21
+
22
+ import { SEPARATOR_ROW_RE, UC_ID_RE, isTableLine, tableCells } from './acervo-format.js';
23
+
24
+ export const REVOCATIONS_TITLE = 'UCs e ADRs que esta feature altera ou revoga';
25
+
26
+ /** ADR numbers restart in every feature, so only `{feature}/ADR-{nnn}` is unambiguous. */
27
+ export const ADR_REF_RE = /^[a-z0-9][a-z0-9-]*\/ADR-\d+$/;
28
+
29
+ const HEADING_RE = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
30
+ const FENCE_RE = /^\s*(```|~~~)/;
31
+ const ID_TOKEN_RE = /\b(?:UC|ADR)-\d+\b/;
32
+ /** A list item: the shape a revocation takes when the author skips the table. */
33
+ const LIST_ITEM_RE = /^\s*(?:[-*+]|\d+[.)])\s/;
34
+
35
+ const CHANGES = new Map([['total', 'total'], ['parcial', 'parcial'], ['partial', 'parcial']]);
36
+
37
+ /** `—`, `–`, `-` and an empty cell all mean "none". */
38
+ const isNone = (cell) => /^(?:—|–|-)?$/.test(cell);
39
+
40
+ /** Id-like cells tolerate the typography an author reaches for: `code` and **bold**. */
41
+ const bare = (cell) => cell.replace(/^[`*]+|[`*]+$/g, '').trim();
42
+
43
+ /**
44
+ * The problems of one data row; `[]` = the row is applicable.
45
+ * @returns {{ entry: object|null, problems: Array<{ id: string|null, reason: string, message: string }> }}
46
+ */
47
+ function readRow(line) {
48
+ const problems = [];
49
+ const problem = (id, reason, message) => problems.push({ id, reason, message });
50
+
51
+ const row = tableCells(line);
52
+ if (row.length < 4) {
53
+ problem(null, 'malformed-row', `linha com ${row.length} coluna(s), esperado 4 (Id | Mudança | Substituto | Escopo): ${line.trim()}`);
54
+ return { entry: null, problems };
55
+ }
56
+ const id = bare(row[0]);
57
+ const changeCell = bare(row[1]).toLowerCase();
58
+ const substitute = isNone(bare(row[2])) ? null : bare(row[2]);
59
+ const scope = isNone(row[3]) ? null : row[3];
60
+
61
+ const kind = UC_ID_RE.test(id) ? 'uc' : ADR_REF_RE.test(id) ? 'adr' : null;
62
+ if (!kind) {
63
+ problem(id || null, 'malformed-id', `"${id}" não é um UC-{nn} nem um {feature}/ADR-{nnn}`);
64
+ return { entry: null, problems };
65
+ }
66
+
67
+ const change = CHANGES.get(changeCell);
68
+ if (!change) problem(id, 'malformed-change', `${id}: mudança "${bare(row[1])}" — use total ou parcial`);
69
+
70
+ if (kind === 'uc' && !substitute) {
71
+ problem(id, 'missing-substitute', `${id}: UC revogado precisa do substituto (UC-{nn}) — é o superseded_by do acervo`);
72
+ } else if (substitute && !(kind === 'uc' ? UC_ID_RE : ADR_REF_RE).test(substitute)) {
73
+ problem(id, 'malformed-substitute',
74
+ `${id}: substituto "${substitute}" — esperado ${kind === 'uc' ? 'UC-{nn}' : '{feature}/ADR-{nnn}'}`);
75
+ } else if (substitute === id) {
76
+ problem(id, 'self-substitute', `${id}: o substituto é o próprio ${id}`);
77
+ }
78
+
79
+ if (change === 'parcial' && !scope) {
80
+ problem(id, 'missing-scope', `${id}: revogação parcial precisa dizer onde o antigo ainda vale (coluna Escopo)`);
81
+ } else if (change === 'total' && scope) {
82
+ problem(id, 'scope-on-total', `${id}: revogação total com escopo "${scope}" — se o antigo ainda vale em algum lugar, a mudança é parcial`);
83
+ }
84
+
85
+ return { entry: problems.length ? { id } : { kind, id, change, substitute, scope }, problems };
86
+ }
87
+
88
+ /**
89
+ * @param {string} content - the whole `spec.md`
90
+ * @returns {{
91
+ * declared: boolean,
92
+ * entries: Array<{ kind: 'uc'|'adr', id: string, change: 'total'|'parcial', substitute: string|null, scope: string|null }>,
93
+ * problems: Array<{ id: string|null, reason: string, message: string }>
94
+ * }}
95
+ * `declared: false` — no section at all (a plan written before it existed).
96
+ */
97
+ export function parseRevocations(content) {
98
+ const lines = String(content ?? '').split(/\r?\n/);
99
+ const title = REVOCATIONS_TITLE.toLowerCase();
100
+
101
+ let start = -1;
102
+ let level = 0;
103
+ for (let i = 0; i < lines.length; i++) {
104
+ const h = HEADING_RE.exec(lines[i]);
105
+ if (h && h[2].toLowerCase().startsWith(title)) { start = i + 1; level = h[1].length; break; }
106
+ }
107
+ if (start === -1) return { declared: false, entries: [], problems: [] };
108
+
109
+ let end = lines.length;
110
+ for (let i = start; i < lines.length; i++) {
111
+ const h = HEADING_RE.exec(lines[i]);
112
+ if (h && h[1].length <= level) { end = i; break; }
113
+ }
114
+
115
+ const problems = [];
116
+ const rows = []; // { id, entry } in order, applicable or not
117
+ let fenced = false;
118
+ let separatorSeen = false;
119
+ let tableLines = 0;
120
+
121
+ for (const line of lines.slice(start, end)) {
122
+ if (FENCE_RE.test(line)) { fenced = !fenced; continue; }
123
+ if (fenced) continue;
124
+
125
+ if (!isTableLine(line)) {
126
+ const stray = LIST_ITEM_RE.test(line) ? ID_TOKEN_RE.exec(line) : null;
127
+ if (stray) {
128
+ problems.push({ id: stray[0], reason: 'outside-table',
129
+ message: `${stray[0]} aparece fora da tabela — só as linhas da tabela são aplicadas no finish` });
130
+ }
131
+ continue;
132
+ }
133
+ tableLines++;
134
+ if (SEPARATOR_ROW_RE.test(line)) { separatorSeen = true; continue; }
135
+ if (!separatorSeen) continue; // the header row: its wording is free
136
+
137
+ const { entry, problems: rowProblems } = readRow(line);
138
+ problems.push(...rowProblems);
139
+ if (entry) rows.push(entry);
140
+ }
141
+
142
+ if (tableLines > 0 && !separatorSeen) {
143
+ problems.push({ id: null, reason: 'unreadable-table',
144
+ message: 'tabela ilegível — falta a linha separadora (|---|---|---|---) depois do cabeçalho; nenhuma linha foi lida' });
145
+ }
146
+
147
+ // An id declared twice is ambiguous as a whole: none of its rows applies.
148
+ const count = new Map();
149
+ for (const r of rows) count.set(r.id, (count.get(r.id) ?? 0) + 1);
150
+ for (const [id, n] of count) {
151
+ if (n > 1) problems.push({ id, reason: 'duplicate-id', message: `${id} aparece em ${n} linhas — nenhuma foi aplicada; deixe uma só` });
152
+ }
153
+ const entries = rows.filter((r) => r.kind && count.get(r.id) === 1);
154
+
155
+ return { declared: true, entries, problems };
156
+ }
@@ -0,0 +1,357 @@
1
+ /**
2
+ * O checklist de tela do `1-design/mockups.md` — as decisões que uma tela de
3
+ * dados precisa (volume, paginação, ordenação, filtro, os três estados,
4
+ * mobile, abas) escritas por tela, na fase uiux.
5
+ *
6
+ * Existe porque o que o design não pergunta não vira `doneCriteria`, e o que
7
+ * não é critério ninguém entrega: telas de admin saíam listando tudo, sem
8
+ * paginar nem ordenar, com o design aprovado. Os leitores do MESMO parser:
9
+ *
10
+ * - `gate-check uiux` pausa com os problemas nomeados;
11
+ * - `approve`/`advance` com assinatura `auto` recusam a mesma pausa sem uma
12
+ * decisão gravada (`uiuxAutoApprovalBlock`) — a regra é a do risk-detector;
13
+ * - `gate-check review` exige que o avaliador registre a conferência de cada
14
+ * item decidido contra a tela entregue (`checklistCoverage`).
15
+ *
16
+ * Mockup no formato ANTERIOR ao checklist (telas `## S001: …`, sem nenhuma
17
+ * `## Tela:`) é legado e isento, com a isenção gravada no sinal.
18
+ *
19
+ * O formato é o que a `morph-uiux` ensina: cada tela é um `## Tela: <nome>`, e
20
+ * dentro dela um `### Checklist de tela` com uma linha por item — tabela
21
+ * (`| Paginação | servidor, 25/página |`) ou lista (`- **Paginação:** …`).
22
+ * "Não se aplica" é resposta válida; vazio, "?" ou "a definir" não são.
23
+ */
24
+
25
+ import { createHash } from 'crypto';
26
+ import { existsSync, readFileSync } from 'fs';
27
+ import { join } from 'path';
28
+ import { fileURLToPath } from 'url';
29
+ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
30
+ import { evaluatorScreenChecklist } from '../evaluator-report.js';
31
+ import { LEGACY_MOCKUP_EXEMPTION, uiuxChecklistPause } from '../risk-detector.js';
32
+
33
+ const PACKAGE_AGENTS = fileURLToPath(new URL('../../../framework/agents.json', import.meta.url));
34
+
35
+ /**
36
+ * Os itens, na ordem do template. `match` roda sobre o rótulo NORMALIZADO
37
+ * (minúsculas, sem acento): "Estado de erro", "Erro" e "**Erro:**" são o mesmo
38
+ * item. Um rótulo que casa com vários ("Estados (vazio/carregando/erro)")
39
+ * responde a todos eles.
40
+ */
41
+ export const SCREEN_CHECKLIST_ITEMS = [
42
+ { id: 'superficie', label: 'Superfície', match: /\bsuperficie\b/ },
43
+ { id: 'volume', label: 'Volume esperado', match: /\bvolume\b/ },
44
+ { id: 'paginacao', label: 'Paginação', match: /\bpaginacao\b/ },
45
+ { id: 'ordenacao', label: 'Ordenação', match: /\bordenacao\b/ },
46
+ { id: 'filtro', label: 'Filtro', match: /\bfiltros?\b/ },
47
+ { id: 'vazio', label: 'Estado vazio', match: /\bvazio\b/ },
48
+ { id: 'carregando', label: 'Estado carregando', match: /\bcarregando\b|\bcarregamento\b/ },
49
+ { id: 'erro', label: 'Estado de erro', match: /\berro\b/ },
50
+ { id: 'mobile', label: 'Mobile', match: /\bmobile\b/ },
51
+ { id: 'abas', label: 'Abas', match: /\babas\b/ },
52
+ ];
53
+
54
+ /**
55
+ * Itens que o avaliador não confere contra a tela: a superfície decide QUAIS
56
+ * standards a tela lê, não um comportamento que a tela entregue exiba.
57
+ */
58
+ const NOT_VERIFIED_AT_REVIEW = new Set(['superficie']);
59
+
60
+ /**
61
+ * Respostas que ocupam a célula sem decidir nada: as exatas, e as que COMEÇAM
62
+ * adiando a decisão ("a definir depois", "TBD.", "? talvez").
63
+ */
64
+ const PLACEHOLDERS = new Set(['', '-', '--', '—', '–', '...', 'x', 'todo', 'definir', 'pendente']);
65
+ const PLACEHOLDER_PREFIX_RE = /^(a definir|tbd|\?|…)/;
66
+
67
+ /** O formato de mockup anterior ao checklist: telas `## S001: <nome>`. */
68
+ const LEGACY_SCREEN_HEADING_RE = /^##\s+S\d+\s*:/i;
69
+
70
+ const NOT_APPLICABLE_RE = /^(nao se aplica|n\/a)\b/;
71
+
72
+ const SCREEN_HEADING_RE = /^##\s+tela\s*[:—–-]\s*(.+?)\s*#*\s*$/i;
73
+ const CHECKLIST_HEADING_RE = /^(#{3,6})\s+checklist\b/i;
74
+
75
+ /** Minúsculas, sem acento, sem marcação inline, espaços colapsados. */
76
+ export function normalizeLabel(text) {
77
+ return String(text ?? '')
78
+ .normalize('NFD')
79
+ .replace(/[\u0300-\u036f]/g, '')
80
+ .replace(/[*_`"“”]/g, '')
81
+ .toLowerCase()
82
+ .replace(/\s+/g, ' ')
83
+ .trim();
84
+ }
85
+
86
+ /** Nome de tela comparável entre o mockup e o relatório do avaliador. */
87
+ function screenKey(name) {
88
+ return normalizeLabel(name).replace(/^tela\s*[:—–-]\s*/, '').replace(/[.:;,]+$/, '');
89
+ }
90
+
91
+ /** Os itens que um rótulo responde. */
92
+ function itemsOf(label) {
93
+ const norm = normalizeLabel(label);
94
+ return SCREEN_CHECKLIST_ITEMS.filter((item) => item.match.test(norm)).map((item) => item.id);
95
+ }
96
+
97
+ /**
98
+ * Uma linha de checklist como `[rótulo, resposta]`, ou null: linha de tabela
99
+ * (separador `|---|` fora) ou item de lista `- Rótulo: resposta`.
100
+ */
101
+ function checklistRow(line) {
102
+ const trimmed = line.trim();
103
+ if (trimmed.startsWith('|')) {
104
+ const cells = tableCells(trimmed);
105
+ if (cells.length < 2 || cells.every((c) => /^:?-{2,}:?$/.test(c) || c === '')) return null;
106
+ return [cells[0], cells.slice(1).join(' ').trim()];
107
+ }
108
+ const bullet = /^[-*+]\s+(.+)$/.exec(trimmed.replace(/\*\*/g, ''));
109
+ if (!bullet) return null;
110
+ const colon = bullet[1].indexOf(':');
111
+ if (colon === -1) return null;
112
+ return [bullet[1].slice(0, colon), bullet[1].slice(colon + 1).trim()];
113
+ }
114
+
115
+ /** As células de uma linha de tabela markdown, sem as bordas. */
116
+ export function tableCells(line) {
117
+ const inner = line.trim().replace(/^\|/, '').replace(/\|$/, '');
118
+ return inner.split(/(?<!\\)\|/).map((cell) => cell.trim());
119
+ }
120
+
121
+ /** Linhas fora de blocos cercados: um exemplo dentro de ``` não é uma tela. */
122
+ function proseLines(markdown) {
123
+ const out = [];
124
+ let fenced = false;
125
+ for (const line of String(markdown ?? '').replace(/\r\n/g, '\n').split('\n')) {
126
+ if (/^\s*(```|~~~)/.test(line)) { fenced = !fenced; continue; }
127
+ if (!fenced) out.push(line);
128
+ }
129
+ return out;
130
+ }
131
+
132
+ /**
133
+ * As telas do mockup e o que o checklist de cada uma responde.
134
+ *
135
+ * @param {string} markdown - conteúdo do `1-design/mockups.md`
136
+ * @returns {Array<{ name: string, hasChecklist: boolean, answers: Map<string, string> }>}
137
+ */
138
+ export function parseScreens(markdown) {
139
+ const screens = [];
140
+ let screen = null;
141
+ let checklistLevel = 0;
142
+
143
+ for (const line of proseLines(markdown)) {
144
+ const heading = /^(#{1,6})\s/.exec(line);
145
+ if (heading) {
146
+ const level = heading[1].length;
147
+ const named = SCREEN_HEADING_RE.exec(line);
148
+ if (level <= 2) {
149
+ screen = named && named[1].trim() ? { name: named[1].trim(), hasChecklist: false, answers: new Map() } : null;
150
+ if (screen) screens.push(screen);
151
+ checklistLevel = 0;
152
+ continue;
153
+ }
154
+ const checklist = CHECKLIST_HEADING_RE.exec(line);
155
+ if (screen && checklist) {
156
+ screen.hasChecklist = true;
157
+ checklistLevel = checklist[1].length;
158
+ } else if (checklistLevel && level <= checklistLevel) {
159
+ checklistLevel = 0;
160
+ }
161
+ continue;
162
+ }
163
+ if (!screen || !checklistLevel) continue;
164
+ const row = checklistRow(line);
165
+ if (!row) continue;
166
+ for (const id of itemsOf(row[0])) {
167
+ if (!screen.answers.has(id) || !screen.answers.get(id)) screen.answers.set(id, row[1]);
168
+ }
169
+ }
170
+ return screens;
171
+ }
172
+
173
+ function isPlaceholder(answer) {
174
+ const norm = normalizeLabel(answer).replace(/[.!;:,]+$/, '').trim();
175
+ return PLACEHOLDERS.has(norm) || PLACEHOLDER_PREFIX_RE.test(norm);
176
+ }
177
+ const isNotApplicable = (answer) => NOT_APPLICABLE_RE.test(normalizeLabel(answer));
178
+
179
+ const labelOf = (id) => SCREEN_CHECKLIST_ITEMS.find((item) => item.id === id)?.label ?? id;
180
+
181
+ /**
182
+ * As superfícies que a persona `ui-designer` declara no `agents.json` — o
183
+ * vocabulário válido do item `Superfície`, que o `morph-plan` §5a usa para
184
+ * somar os standards da tela em `tasks[].standards`. Lê a cópia instalada do
185
+ * projeto; sem ela (este repositório), a do pacote. Persona sem `surfaces`
186
+ * (instalação anterior ao campo) devolve null: o valor não é validado.
187
+ *
188
+ * @param {string} cwd
189
+ * @returns {string[]|null}
190
+ */
191
+ export function knownSurfaces(cwd) {
192
+ for (const path of [join(cwd, '.morph', 'framework', 'agents.json'), PACKAGE_AGENTS]) {
193
+ if (!existsSync(path)) continue;
194
+ try {
195
+ const surfaces = JSON.parse(readFileSync(path, 'utf8'))?.agents?.['ui-designer']?.surfaces;
196
+ return surfaces && typeof surfaces === 'object' && !Array.isArray(surfaces) ? Object.keys(surfaces) : null;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+
204
+ /** A resposta de `Superfície` nomeia uma das superfícies conhecidas? */
205
+ function namesSurface(answer, surfaces) {
206
+ const norm = normalizeLabel(answer);
207
+ return surfaces.some((key) => new RegExp(`(^|[^a-z0-9-])${key}([^a-z0-9-]|$)`).test(norm));
208
+ }
209
+
210
+ /**
211
+ * Audita o checklist do mockup: toda tela tem o bloco, e todo item tem uma
212
+ * resposta escrita — e, quando `surfaces` é dado, a `Superfície` é uma delas.
213
+ *
214
+ * Sem nenhuma `## Tela:`, o mockup é `legacy` (isento) só quando declara telas
215
+ * no formato antigo; qualquer outro formato é o problema "nenhuma tela
216
+ * declarada" — senão trocar o heading seria o atalho para pular o checklist.
217
+ *
218
+ * @param {string} markdown
219
+ * @returns {{ legacy: boolean, screens: Array<{ name: string, hasChecklist: boolean, missing: string[], unanswered: string[] }>,
220
+ * problems: string[] }} `problems` vazio ⇔ checklist completo (ou isento)
221
+ */
222
+ export function auditScreenChecklist(markdown, { surfaces = null } = {}) {
223
+ const parsed = parseScreens(markdown);
224
+ const problems = [];
225
+ if (parsed.length === 0) {
226
+ if (proseLines(markdown).some((line) => LEGACY_SCREEN_HEADING_RE.test(line))) {
227
+ return { legacy: true, screens: [], problems };
228
+ }
229
+ problems.push('nenhuma tela declarada no formato `## Tela: <nome>`');
230
+ }
231
+ const screens = parsed.map((screen) => {
232
+ const missing = [];
233
+ const unanswered = [];
234
+ for (const { id } of SCREEN_CHECKLIST_ITEMS) {
235
+ if (!screen.answers.has(id)) missing.push(id);
236
+ else if (isPlaceholder(screen.answers.get(id))) unanswered.push(id);
237
+ }
238
+ if (!screen.hasChecklist) {
239
+ problems.push(`tela "${screen.name}" sem \`### Checklist de tela\``);
240
+ } else {
241
+ if (missing.length) problems.push(`tela "${screen.name}": falta ${missing.map(labelOf).join(', ')}`);
242
+ if (unanswered.length) problems.push(`tela "${screen.name}": sem resposta em ${unanswered.map(labelOf).join(', ')}`);
243
+ const surface = screen.answers.get('superficie');
244
+ if (surfaces?.length && surface !== undefined && !isPlaceholder(surface) && !isNotApplicable(surface)
245
+ && !namesSurface(surface, surfaces)) {
246
+ problems.push(`tela "${screen.name}": Superfície "${surface}" não é uma das superfícies da ui-designer (${surfaces.join(', ')})`);
247
+ }
248
+ }
249
+ return { name: screen.name, hasChecklist: screen.hasChecklist, missing, unanswered };
250
+ });
251
+ return { legacy: false, screens, problems };
252
+ }
253
+
254
+ /**
255
+ * O sinal `screenChecklist` de um gate, lido do disco. COLETA, nunca decide:
256
+ * quem pausa é o risk-detector.
257
+ *
258
+ * `mockups: false` é "a feature não tem mockup" — nada a cobrar. No review, o
259
+ * que se cobra é a conferência (`uncovered`: itens decididos sem linha na
260
+ * seção do avaliador), não o design, que já passou pelo Gate uiux.
261
+ *
262
+ * @param {string} cwd
263
+ * @param {string} featureName
264
+ * @param {string} gateName
265
+ * @returns {{ mockups: boolean, legacy: boolean, screens: string[], problems: string[],
266
+ * exempt?: string, section?: boolean, uncovered?: string[] }}
267
+ */
268
+ export function collectScreenChecklist(cwd, featureName, gateName) {
269
+ const mockups = readMockups(cwd, featureName);
270
+ if (mockups === null) return { mockups: false, legacy: false, screens: [], problems: [] };
271
+ const audit = auditScreenChecklist(mockups, { surfaces: knownSurfaces(cwd) });
272
+ const signal = { mockups: true, legacy: audit.legacy, screens: audit.screens.map((s) => s.name), problems: audit.problems };
273
+ if (audit.legacy) signal.exempt = LEGACY_MOCKUP_EXEMPTION;
274
+ if (gateName === 'review') {
275
+ const report = evaluatorScreenChecklist(cwd, featureName);
276
+ signal.section = report.section;
277
+ signal.uncovered = checklistCoverage(mockups, report.section ? report.body : '');
278
+ }
279
+ return signal;
280
+ }
281
+
282
+ /**
283
+ * Por que o Gate uiux NÃO pode ser aprovado com assinatura `auto` — a pausa
284
+ * que o `gate-check uiux` daria pelo checklist —, ou null. Lido por `approve`
285
+ * e `advance`, para que uma aprovação automática nunca passe onde o gate-check
286
+ * pausaria. Uma decisão gravada com `pause: false` (o `gate-decision` manual,
287
+ * ou um gate-check que não pausou) é a resposta a essa pausa e destrava — só
288
+ * para o mockup que ela viu: a decisão guarda `mockupsHash`, e um mockup
289
+ * escrito ou mudado depois dela volta a ser conferido.
290
+ *
291
+ * @param {string} cwd
292
+ * @param {string} featureName
293
+ * @param {object} feature - o feature.json (lê `gateDecisions.uiux`)
294
+ * @returns {string|null}
295
+ */
296
+ export function uiuxAutoApprovalBlock(cwd, featureName, feature) {
297
+ const decision = feature?.gateDecisions?.uiux;
298
+ if (decision?.pause === false && decision.mockupsHash === mockupsHash(cwd, featureName)) return null;
299
+ return uiuxChecklistPause(collectScreenChecklist(cwd, featureName, 'uiux'));
300
+ }
301
+
302
+ /**
303
+ * O que o avaliador deixou de conferir: por tela do mockup, os itens com
304
+ * decisão (nem "não se aplica", nem a superfície) sem linha na seção
305
+ * `## Checklist de tela` do relatório. Uma linha que o avaliador marca como
306
+ * não verificável conta como conferida — o que pausa é a AUSÊNCIA da linha.
307
+ *
308
+ * @param {string} designMarkdown - o `1-design/mockups.md`
309
+ * @param {string} reportSection - o corpo da seção do `evaluator-report.md`
310
+ * @returns {string[]} `"<tela>: <item>, <item>"` por tela com lacuna
311
+ */
312
+ export function checklistCoverage(designMarkdown, reportSection) {
313
+ const covered = new Map();
314
+ for (const line of proseLines(reportSection)) {
315
+ if (!line.trim().startsWith('|')) continue;
316
+ const cells = tableCells(line);
317
+ if (cells.length < 2) continue;
318
+ const key = screenKey(cells[0]);
319
+ if (!covered.has(key)) covered.set(key, new Set());
320
+ for (const id of itemsOf(cells[1])) covered.get(key).add(id);
321
+ }
322
+
323
+ const gaps = [];
324
+ for (const screen of parseScreens(designMarkdown)) {
325
+ const done = covered.get(screenKey(screen.name)) ?? new Set();
326
+ const owed = [...screen.answers]
327
+ .filter(([id, answer]) => !NOT_VERIFIED_AT_REVIEW.has(id) && !isPlaceholder(answer) && !isNotApplicable(answer))
328
+ .map(([id]) => id)
329
+ .filter((id) => !done.has(id));
330
+ if (owed.length) gaps.push(`${screen.name}: ${owed.map(labelOf).join(', ')}`);
331
+ }
332
+ return gaps;
333
+ }
334
+
335
+ /**
336
+ * O `1-design/mockups.md` por conteúdo: BOM e CRLF não contam, para que o
337
+ * checkout de outra máquina seja o mesmo mockup.
338
+ *
339
+ * @param {string} cwd
340
+ * @param {string} featureName
341
+ * @returns {string|null} null sem mockup
342
+ */
343
+ export function mockupsHash(cwd, featureName) {
344
+ const text = readMockups(cwd, featureName);
345
+ if (text === null) return null;
346
+ return createHash('sha256').update(text.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n')).digest('hex').slice(0, 16);
347
+ }
348
+
349
+ /** O `1-design/mockups.md` da feature, ou null quando ausente/ilegível. Nunca lança. */
350
+ export function readMockups(cwd, featureName) {
351
+ try {
352
+ const path = getAbsoluteOutputPath(cwd, featureName, 'uiMockups');
353
+ return existsSync(path) ? readFileSync(path, 'utf8') : null;
354
+ } catch {
355
+ return null;
356
+ }
357
+ }