@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Memory Installer — populates Claude Code persistent memory during morph-init.
2
+ * Memory Installer — populates Claude Code persistent memory during `morph-spec init`.
3
3
  *
4
4
  * Writes two memory files (project + reference) plus an entry in MEMORY.md
5
5
  * so future Claude Code sessions enter pre-contextualized about the
@@ -67,7 +67,6 @@ metadata:
67
67
  - When user mentions a feature, check \`.morph/features/\` first.
68
68
  - When user types \`/morph-proposal\`, invoke \`Skill(morph-proposal)\` directly.
69
69
  - In \`trust: "auto"\`, gates auto-pass; only AskUserQuestion on risk signals.
70
- - Prefer morph-* skills over superpowers:* equivalents in this project.
71
70
  `;
72
71
  }
73
72
 
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Where the morph-spec projects on this machine are — the list the dashboard's
3
+ * portfolio reads. Nothing else records it: each project only knows its own
4
+ * `.morph/`.
5
+ *
6
+ * Three writers register a project as a side note of what they already do —
7
+ * `init`, `update` (never under `--dry-run`) and `dashboard` — and the
8
+ * dashboard adds and removes on request. Registering is fail-open: a registry
9
+ * that cannot be read or written never fails the command that tried.
10
+ *
11
+ * The file lives in the user's home (`~/.morph-spec/projects.json`, or
12
+ * `MORPH_PROJECTS_REGISTRY`), read through `os.homedir()` at call time so a
13
+ * suite that isolates the home also isolates this file. Paths under the
14
+ * system temp dir are never registered: the test suites run `init`/`update`
15
+ * there by the hundred, including through spawned CLIs that keep the real home.
16
+ */
17
+
18
+ import { existsSync, readFileSync, readdirSync } from 'fs';
19
+ import { homedir, tmpdir } from 'os';
20
+ import { join, resolve, basename, dirname } from 'path';
21
+ import { atomicWriteJson } from '../../core/state/state-manager.js';
22
+ import { canonicalPath, isInside } from '../tasks/test-runner.js';
23
+ import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
24
+
25
+ export const REGISTRY_VERSION = 1;
26
+
27
+ export function registryPath() {
28
+ return process.env.MORPH_PROJECTS_REGISTRY || join(homedir(), '.morph-spec', 'projects.json');
29
+ }
30
+
31
+ /** One spelling per directory: canonical, forward slashes (the registry is read back on any OS). */
32
+ export function projectKey(dir) {
33
+ return canonicalPath(resolve(dir)).replace(/\\/g, '/');
34
+ }
35
+
36
+ const sameDir = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
37
+
38
+ /** A folder is a morph-spec project when morph-spec installed its config there. */
39
+ export function isMorphProject(dir) {
40
+ return existsSync(join(dir, '.morph', 'config', 'config.json'));
41
+ }
42
+
43
+ /** A worktree checkout is part of its repo's project, never a project of its own. */
44
+ function insideWorktreeContainer(dir) {
45
+ const parent = basename(dirname(dir));
46
+ return WORKTREE_DIR_NAMES.includes(parent);
47
+ }
48
+
49
+ function projectName(dir) {
50
+ try {
51
+ const config = JSON.parse(readFileSync(join(dir, '.morph', 'config', 'config.json'), 'utf8'));
52
+ const name = config?.project?.name;
53
+ // Some configs carry the project path as its name; the folder reads better.
54
+ return typeof name === 'string' && name.trim() && !/[\\/]/.test(name) ? name.trim() : basename(dir);
55
+ } catch {
56
+ return basename(dir);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * The registered projects that still exist. A folder that was deleted or lost
62
+ * its `.morph/` drops out on read — the file is rewritten on the next write.
63
+ *
64
+ * @returns {{ version: number, projects: Array<{ dir: string, name: string, addedAt: string, lastSeenAt: string, source: string }> }}
65
+ */
66
+ export function readRegistry() {
67
+ let data = null;
68
+ try {
69
+ const path = registryPath();
70
+ if (existsSync(path)) data = JSON.parse(readFileSync(path, 'utf8'));
71
+ } catch { /* unreadable registry reads as empty */ }
72
+ const projects = Array.isArray(data?.projects) ? data.projects : [];
73
+ return {
74
+ version: REGISTRY_VERSION,
75
+ projects: projects.filter((p) => typeof p?.dir === 'string' && isMorphProject(p.dir)),
76
+ };
77
+ }
78
+
79
+ function write(projects) {
80
+ atomicWriteJson(registryPath(), { version: REGISTRY_VERSION, projects });
81
+ }
82
+
83
+ /**
84
+ * Note that `dir` is a morph-spec project. Idempotent; refreshes `lastSeenAt`.
85
+ * Never throws.
86
+ *
87
+ * @param {string} dir
88
+ * @param {'init'|'update'|'dashboard'|'manual'} source
89
+ * @param {{ tempRoot?: string }} [opts] - test seam: the folder whose
90
+ * projects are never registered (the system temp dir)
91
+ * @returns {boolean} true when the project is in the registry afterwards
92
+ */
93
+ export function registerProject(dir, source, { tempRoot = tmpdir() } = {}) {
94
+ try {
95
+ const key = projectKey(dir);
96
+ if (isInside(canonicalPath(key), canonicalPath(tempRoot))) return false;
97
+ if (!isMorphProject(key) || insideWorktreeContainer(key)) return false;
98
+ const now = new Date().toISOString();
99
+ const { projects } = readRegistry();
100
+ const found = projects.find((p) => sameDir(p.dir, key));
101
+ if (found) {
102
+ found.lastSeenAt = now;
103
+ found.name = projectName(key);
104
+ } else {
105
+ projects.push({ dir: key, name: projectName(key), addedAt: now, lastSeenAt: now, source });
106
+ }
107
+ write(projects);
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /** Take a project off the list. Its files are not touched. Never throws. */
115
+ export function unregisterProject(dir) {
116
+ try {
117
+ const key = projectKey(dir);
118
+ const { projects } = readRegistry();
119
+ const kept = projects.filter((p) => !sameDir(p.dir, key));
120
+ if (kept.length !== projects.length) write(kept);
121
+ return kept.length !== projects.length;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * morph-spec projects directly under each root that are not registered yet —
129
+ * the portfolio offers them, the person decides. One level deep: a repos
130
+ * folder is the expected root, and a deeper walk would enter node_modules and
131
+ * worktrees for nothing.
132
+ *
133
+ * @param {string[]} roots
134
+ * @returns {Array<{ dir: string, name: string }>}
135
+ */
136
+ export function discoverProjects(roots) {
137
+ const registered = readRegistry().projects.map((p) => p.dir);
138
+ const out = [];
139
+ const seen = new Set();
140
+ for (const root of roots) {
141
+ let entries = [];
142
+ try { entries = readdirSync(root, { withFileTypes: true }); } catch { continue; }
143
+ for (const e of entries) {
144
+ if (!e.isDirectory() || e.name.startsWith('.') || e.name === 'node_modules') continue;
145
+ const dir = join(root, e.name);
146
+ if (!isMorphProject(dir)) continue;
147
+ const key = projectKey(dir);
148
+ if (seen.has(key.toLowerCase()) || registered.some((r) => sameDir(r, key))) continue;
149
+ seen.add(key.toLowerCase());
150
+ out.push({ dir: key, name: projectName(key) });
151
+ }
152
+ }
153
+ return out.sort((a, b) => a.name.localeCompare(b.name));
154
+ }
@@ -23,6 +23,10 @@
23
23
  * evaluator-report.md. No ship, relatório SEM a seção `## Mutações` numa
24
24
  * feature com código de produção pausa: score é opinião, mutação é prova
25
25
  * - mediaClaims + gate: object — { section, score, featureGeneratesMedia }
26
+ * - screenChecklist + gate: object — { mockups, legacy, screens, problems, exempt?,
27
+ * section?, uncovered? } derivado do 1-design/mockups.md. No uiux, checklist
28
+ * de tela incompleto pausa; no ship, item decidido sem a conferência do
29
+ * avaliador pausa. Mockup legado é isento nos dois, com a razão listada
26
30
  * - evalScore: number — morph-eval score (pause if < 9)
27
31
  * - taskCount: number — total tasks (pause if > 20)
28
32
  */
@@ -133,6 +137,35 @@ function e2eScopeRefusal(klass, scope, reason) {
133
137
  + ' rode `morph-spec verify {feature}` (sem task) antes do ship';
134
138
  }
135
139
 
140
+ /** A razão gravada no sinal quando o checklist de tela isenta um mockup legado. */
141
+ export const LEGACY_MOCKUP_EXEMPTION = 'checklist dispensado: mockup no formato antigo';
142
+
143
+ /** Os dois primeiros itens de uma lista, e quantos ficaram de fora. */
144
+ function firstOf(items) {
145
+ const shown = items.slice(0, 2).join('; ');
146
+ return items.length > 2 ? `${shown} (+${items.length - 2})` : shown;
147
+ }
148
+
149
+ /**
150
+ * Por que o checklist de tela pausa o Gate uiux, ou null. Exportada porque o
151
+ * `approve`/`advance` com assinatura `auto` recusam pela MESMA regra: uma
152
+ * aprovação automática que passasse onde o gate-check pausa seria o atalho.
153
+ *
154
+ * Mockup ausente (`mockups: false`) não conta — nem toda feature de UI tem
155
+ * tela —, e o legado chega sem problemas: a auditoria o isenta.
156
+ *
157
+ * @param {object|undefined} screenChecklist - o sinal de `collectScreenChecklist`
158
+ * @returns {string|null}
159
+ */
160
+ export function uiuxChecklistPause(screenChecklist) {
161
+ if (screenChecklist?.mockups !== true) return null;
162
+ const problems = screenChecklist.problems ?? [];
163
+ if (problems.length === 0) return null;
164
+ return `checklist de tela incompleto em 1-design/mockups.md: ${firstOf(problems)}`
165
+ + ' — volume, paginação, ordenação, filtro, os três estados, mobile e abas por tela'
166
+ + ' ("não se aplica" vale, se escrito)';
167
+ }
168
+
136
169
  /**
137
170
  * @param {object} config - .morph/config/config.json contents (uses `trust`)
138
171
  * @param {object} signals
@@ -362,6 +395,36 @@ export function shouldPauseGate(config, signals) {
362
395
  };
363
396
  }
364
397
 
398
+ // No fecho do DESIGN: o mockup declara telas sem o checklist de tela
399
+ // completo. É ele que faz paginação, ordenação, filtro e os três estados
400
+ // virarem `doneCriteria` — sem ele, uma lista de 2 000 linhas é aprovada
401
+ // listando tudo, e o implementador entrega exatamente o critério.
402
+ if (signals.gate === 'uiux') {
403
+ const reason = uiuxChecklistPause(signals.screenChecklist);
404
+ if (reason) return { pause: true, reason };
405
+ }
406
+
407
+ // No SHIP, depois da cláusula de mídia e pela mesma disciplina: o design
408
+ // decidiu itens de tela e o avaliador não registrou a conferência deles
409
+ // contra a tela entregue. Cobra-se o item DEVIDO (`uncovered`): tela toda
410
+ // "não se aplica", mockup legado ou ausente não devem nada.
411
+ if (signals.gate === 'review' && (signals.screenChecklist?.uncovered?.length ?? 0) > 0) {
412
+ const { section, uncovered } = signals.screenChecklist;
413
+ if (section !== true) {
414
+ return {
415
+ pause: true,
416
+ reason: `relatório do avaliador sem a seção \`## Checklist de tela\` — o design decidiu ${firstOf(uncovered)}`
417
+ + ' e a avaliação não registrou a conferência contra a tela entregue'
418
+ + ' (rubrica evals/rubrics/contracts.md, "Contrato de tela")',
419
+ };
420
+ }
421
+ return {
422
+ pause: true,
423
+ reason: `a seção \`## Checklist de tela\` não confere ${firstOf(uncovered)}`
424
+ + ' — uma linha por item decidido de cada tela, mesmo quando não verificável',
425
+ };
426
+ }
427
+
365
428
  if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
366
429
  return {
367
430
  pause: true,
@@ -455,6 +518,33 @@ export function getRiskSignals(signals) {
455
518
  }
456
519
  }
457
520
 
521
+ // Listado mesmo quando NÃO pausa: a isenção (sem mockup, legado, nada
522
+ // devido) e o checklist conferido precisam ser distinguíveis de uma omissão.
523
+ if ((signals.gate === 'uiux' || signals.gate === 'review') && signals.screenChecklist) {
524
+ const { mockups, legacy, exempt, screens = [], problems = [], section, uncovered = [] } = signals.screenChecklist;
525
+ if (!mockups) {
526
+ out.push('sem 1-design/mockups.md — checklist de tela não se aplica');
527
+ } else if (legacy === true) {
528
+ out.push(exempt ?? LEGACY_MOCKUP_EXEMPTION);
529
+ } else if (signals.gate === 'uiux') {
530
+ out.push(problems.length > 0
531
+ ? `checklist de tela incompleto (${problems.length} problema(s))`
532
+ : `checklist de tela completo (${screens.length} tela(s))`);
533
+ } else if (uncovered.length > 0) {
534
+ out.push(section !== true
535
+ ? 'relatório sem a seção `## Checklist de tela`'
536
+ : `checklist de tela sem conferência em ${uncovered.length} tela(s)`);
537
+ } else if (screens.length === 0) {
538
+ // O design passou pela uiux sem tela nenhuma: nada a cobrar no ship, mas
539
+ // não pode ler igual a uma tela toda "não se aplica".
540
+ out.push(`checklist de tela: ${firstOf(problems)}`);
541
+ } else if (section === true) {
542
+ out.push(`checklist de tela conferido (${screens.length} tela(s))`);
543
+ } else {
544
+ out.push(`checklist de tela sem item a conferir (${screens.length} tela(s))`);
545
+ }
546
+ }
547
+
458
548
  return out;
459
549
  }
460
550
 
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The shapes every reader and writer of the acervo agrees on — one copy each.
3
+ * A second copy of any of these is how two readers start disagreeing about the
4
+ * same file: a use case one of them sees and the other does not, a table row
5
+ * one of them splits differently.
6
+ */
7
+
8
+ import { existsSync, readdirSync } from 'fs';
9
+ import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
10
+
11
+ /** A use-case id: `UC-07`. */
12
+ export const UC_ID_RE = /^UC-\d+$/;
13
+
14
+ /** A use case no longer holds as the system's behaviour (or holds only in its `scope`). */
15
+ export const SUPERSEDED = 'superseded';
16
+
17
+ const UC_FILENAME_RE = new RegExp(OUTPUT_SCHEMA.usecases.filenamePattern);
18
+
19
+ /** `UC-07-listar-veterinarios.md` → `UC-07`. Null when the name isn't a use case. */
20
+ export function useCaseId(filename) {
21
+ const m = /^(UC-\d+)-/.exec(filename);
22
+ return m ? m[1] : null;
23
+ }
24
+
25
+ /** The name the promotion carries: `UC-{nn}-{slug}.md`, slug in lowercase kebab. */
26
+ export const isCanonicalUseCaseName = (name) => UC_FILENAME_RE.test(name);
27
+
28
+ /**
29
+ * Every use case in `dir`, grouped by id. By default only the canonical names
30
+ * — the ones the promotion ever carries. `canonicalOnly: false` also lists any
31
+ * `UC-{nn}-….md`, for the plan-time check that has always accepted them and
32
+ * now warns instead. An id with more than one file is a broken acervo, and
33
+ * readers must be able to see it rather than silently pick one.
34
+ *
35
+ * @param {string} dir
36
+ * @param {{ canonicalOnly?: boolean }} [options]
37
+ * @returns {Map<string, string[]>} id → filenames, sorted; empty when the folder is missing or unreadable
38
+ */
39
+ export function useCaseFilesById(dir, { canonicalOnly = true } = {}) {
40
+ const byId = new Map();
41
+ try {
42
+ if (!existsSync(dir)) return byId;
43
+ for (const name of readdirSync(dir).sort()) {
44
+ const id = useCaseId(name);
45
+ if (!id || !name.endsWith('.md')) continue;
46
+ if (canonicalOnly && !isCanonicalUseCaseName(name)) continue;
47
+ if (!byId.has(id)) byId.set(id, []);
48
+ byId.get(id).push(name);
49
+ }
50
+ } catch { /* unreadable — nothing to see */ }
51
+ return byId;
52
+ }
53
+
54
+ /** A markdown table separator row: `|---|:--:|`, with or without the outer pipes. */
55
+ export const SEPARATOR_ROW_RE = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
56
+
57
+ /** A line that belongs to a table: it carries an unescaped pipe (GFM does not require the leading one). */
58
+ export const isTableLine = (line) => /(?<!\\)\|/.test(line);
59
+
60
+ /**
61
+ * Split a table row on unescaped pipes, outer pipes optional; `\|` inside a
62
+ * cell is a literal pipe.
63
+ *
64
+ * @param {string} row
65
+ * @returns {string[]} trimmed cells
66
+ */
67
+ export function tableCells(row) {
68
+ const inner = row.trim().replace(/^\|/, '').replace(/(?<!\\)\|$/, '');
69
+ return inner.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, '|').trim());
70
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Lifecycle of the project's acervo: a use case stops being the current
3
+ * behaviour when a later feature supersedes it.
4
+ *
5
+ * status: superseded
6
+ * superseded_by: UC-589
7
+ * scope: "vitrine pública, fora de `.morph-admin-shell`" ← only when partial
8
+ *
9
+ * Three roles live here:
10
+ * - the ONE rule for that frontmatter (`supersessionProblems`) — the check
11
+ * reads it, and the writer below only ever produces what it accepts;
12
+ * - the close-time writer (`applySpecRevocations`): what the feature's
13
+ * `spec.md` declared it revokes lands on `.morph/specs/usecases/` and on
14
+ * the ADR index, fail-open like the promotion it follows;
15
+ * - the plan-time check (`checkLifecycle`), shared by `dag` and `usecase
16
+ * check`, run while `spec.md` can still be fixed.
17
+ */
18
+
19
+ import { readFileSync, writeFileSync } from 'fs';
20
+ import { join } from 'path';
21
+ import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
22
+ import {
23
+ SPECS_USECASES_PATHSPEC,
24
+ hasFrontmatter,
25
+ readFrontmatterField,
26
+ removeFrontmatterField,
27
+ setFrontmatterField,
28
+ } from './usecase-promoter.js';
29
+ import { SUPERSEDED, UC_ID_RE, useCaseFilesById } from './acervo-format.js';
30
+ import { parseRevocations } from './revocations.js';
31
+ import { ADR_INDEX_PATHSPEC, readAdrIndex, updateAdrIndex } from './adr-index.js';
32
+
33
+ export { SUPERSEDED };
34
+
35
+ const UC = OUTPUT_SCHEMA.usecases;
36
+ const ACERVO_LABEL = SPECS_USECASES_PATHSPEC.replace(/\\/g, '/');
37
+ const ADR_INDEX_LABEL = `${ADR_INDEX_PATHSPEC.replace(/\\/g, '/')}/`;
38
+
39
+ /** @returns {{ status: string|null, supersededBy: string|null, scope: string|null }} */
40
+ export function lifecycleOf(content) {
41
+ return {
42
+ status: readFrontmatterField(content, 'status'),
43
+ supersededBy: readFrontmatterField(content, 'superseded_by'),
44
+ scope: readFrontmatterField(content, 'scope'),
45
+ };
46
+ }
47
+
48
+ /**
49
+ * The rule for a use case's supersession frontmatter.
50
+ *
51
+ * @param {string} content
52
+ * @param {string} id - the use case's own id
53
+ * @returns {string[]} one line per problem; `[]` = well formed
54
+ */
55
+ export function supersessionProblems(content, id) {
56
+ const { status, supersededBy, scope } = lifecycleOf(content);
57
+ const problems = [];
58
+ if (status === SUPERSEDED) {
59
+ if (!supersededBy) problems.push('status: superseded sem superseded_by');
60
+ else if (!UC_ID_RE.test(supersededBy)) problems.push(`superseded_by "${supersededBy}" não é um UC-{nn}`);
61
+ else if (supersededBy === id) problems.push('superseded_by aponta para o próprio use case');
62
+ } else if (supersededBy !== null || scope !== null) {
63
+ problems.push(`${supersededBy !== null ? 'superseded_by' : 'scope'} sem status: superseded`);
64
+ }
65
+ if (scope !== null && !String(scope).trim()) problems.push('scope vazio');
66
+ return problems;
67
+ }
68
+
69
+ function readOrNull(path) {
70
+ try { return readFileSync(path, 'utf8'); } catch { return null; }
71
+ }
72
+
73
+ const specPathOf = (featureDir) => join(featureDir, OUTPUT_SCHEMA.spec.phaseDir, OUTPUT_SCHEMA.spec.filename);
74
+
75
+ /** A finished feature cannot be re-closed: the fix is the next feature's revocation row. */
76
+ const FIX_NEXT_TIME = 'corrija a linha na seção de revogações do spec.md — a feature já arquivada não é reprocessada, ' +
77
+ 'então a revogação corrigida entra no spec.md da próxima feature';
78
+
79
+ /**
80
+ * Mark the acervo use cases the entries revoke. Runs after the promotion, so
81
+ * the acervo already holds every substitute this feature brought — except the
82
+ * ones the promotion refused, which only LOOK present (the file under that id
83
+ * belongs to another feature).
84
+ *
85
+ * @returns {{ superseded: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
86
+ * problems: Array<{id: string, reason: string, message: string}> }}
87
+ */
88
+ function supersedeUseCases(cwd, entries, refusedIds) {
89
+ const superseded = [];
90
+ const problems = [];
91
+ if (!entries.length) return { superseded, problems };
92
+
93
+ const dir = join(cwd, SPECS_USECASES_PATHSPEC);
94
+ const files = useCaseFilesById(dir);
95
+ const problem = (id, reason, message) => problems.push({ id, reason, message });
96
+
97
+ for (const e of entries) {
98
+ const targets = files.get(e.id) ?? [];
99
+ if (targets.length === 0) {
100
+ problem(e.id, 'not-in-acervo', `${e.id} não está no acervo (${ACERVO_LABEL}/) — nada foi marcado; ${FIX_NEXT_TIME}`);
101
+ continue;
102
+ }
103
+ if (targets.length > 1) {
104
+ problem(e.id, 'duplicate-in-acervo',
105
+ `${e.id} tem ${targets.length} arquivos no acervo (${targets.join(', ')}) — nenhum foi marcado; o acervo precisa de um arquivo por UC`);
106
+ continue;
107
+ }
108
+ if (refusedIds.has(e.substitute)) {
109
+ problem(e.id, 'substitute-refused',
110
+ `o substituto ${e.substitute} de ${e.id} não foi promovido (colisão de numeração) — ${e.id} não foi marcado`);
111
+ continue;
112
+ }
113
+ if (!files.has(e.substitute)) {
114
+ problem(e.id, 'substitute-not-found',
115
+ `o substituto ${e.substitute} de ${e.id} não está no acervo — ${e.id} não foi marcado; ${FIX_NEXT_TIME}`);
116
+ continue;
117
+ }
118
+
119
+ const file = targets[0];
120
+ try {
121
+ const path = join(dir, file);
122
+ const before = readFileSync(path, 'utf8');
123
+ if (!hasFrontmatter(before)) {
124
+ problem(e.id, 'no-frontmatter', `${ACERVO_LABEL}/${file} não tem frontmatter — o status não foi aplicado`);
125
+ continue;
126
+ }
127
+ let next = setFrontmatterField(before, 'status', SUPERSEDED);
128
+ next = setFrontmatterField(next, 'superseded_by', e.substitute);
129
+ // JSON-quoted: free text with `: ` or a leading backtick breaks a bare YAML scalar.
130
+ next = e.scope
131
+ ? setFrontmatterField(next, 'scope', JSON.stringify(e.scope))
132
+ : removeFrontmatterField(next, 'scope');
133
+ if (next !== before) writeFileSync(path, next, 'utf8');
134
+ superseded.push({ id: e.id, file, supersededBy: e.substitute, ...(e.scope ? { scope: e.scope } : {}) });
135
+ } catch {
136
+ problem(e.id, 'unwritable', `${ACERVO_LABEL}/${file} não pôde ser lido/gravado — o status não foi aplicado`);
137
+ }
138
+ }
139
+ return { superseded, problems };
140
+ }
141
+
142
+ /**
143
+ * Apply, at close time, what the feature's `spec.md` declared it revokes.
144
+ * Fail-open: nothing here may refuse the close; what did not apply comes back
145
+ * in `problems`, next to the row's own parse problems.
146
+ *
147
+ * @param {string} cwd - project root that owns `.morph/specs/`
148
+ * @param {string} featureDir - the feature folder (already moved to the archive)
149
+ * @param {{ refused?: Array<{ id: string }> }} [promotion] - the promotion's result, run just before
150
+ * @returns {{ superseded: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
151
+ * adrIndex: ReturnType<typeof updateAdrIndex>|null,
152
+ * problems: Array<{id: string|null, reason: string, message: string}> }}
153
+ */
154
+ export function applySpecRevocations(cwd, featureDir, promotion = {}) {
155
+ const out = { superseded: [], adrIndex: null, problems: [] };
156
+
157
+ const spec = readOrNull(specPathOf(featureDir));
158
+ const parsed = spec === null ? { entries: [], problems: [] } : parseRevocations(spec);
159
+ out.problems.push(...parsed.problems);
160
+
161
+ try {
162
+ const refusedIds = new Set((promotion.refused ?? []).map((r) => r.id));
163
+ const uc = supersedeUseCases(cwd, parsed.entries.filter((e) => e.kind === 'uc'), refusedIds);
164
+ out.superseded = uc.superseded;
165
+ out.problems.push(...uc.problems);
166
+ } catch { /* fail-open */ }
167
+
168
+ try {
169
+ out.adrIndex = updateAdrIndex(cwd, featureDir, parsed.entries.filter((e) => e.kind === 'adr'));
170
+ out.problems.push(...out.adrIndex.problems);
171
+ } catch { /* fail-open */ }
172
+
173
+ return out;
174
+ }
175
+
176
+ /**
177
+ * Plan-time check of a feature against the acervo's lifecycle.
178
+ *
179
+ * Warnings only, never errors: a superseded use case is a question for the
180
+ * plan, and the revocation rows are re-reported at close time anyway.
181
+ *
182
+ * @param {{ cwd: string, featureDir: string, tasksData?: object|null }} input
183
+ * @returns {{ warnings: string[] }}
184
+ */
185
+ export function checkLifecycle({ cwd, featureDir, tasksData = null }) {
186
+ const warnings = [];
187
+ const acervoDir = join(cwd, SPECS_USECASES_PATHSPEC);
188
+ const ownDir = join(featureDir, UC.phaseDir, UC.dirname);
189
+ const acervo = useCaseFilesById(acervoDir);
190
+ const own = useCaseFilesById(ownDir);
191
+
192
+ const ownLife = new Map();
193
+ for (const [id, files] of own) {
194
+ const file = files[files.length - 1];
195
+ const content = readOrNull(join(ownDir, file));
196
+ if (content === null) continue;
197
+ ownLife.set(id, lifecycleOf(content));
198
+ for (const p of supersessionProblems(content, id)) warnings.push(`2-plan/usecases/${file}: ${p}`);
199
+ }
200
+
201
+ // The acervo can hold hundreds of use cases: read only the ones this plan touches.
202
+ const acervoLife = new Map();
203
+ const acervoOf = (id) => {
204
+ if (acervoLife.has(id)) return acervoLife.get(id);
205
+ const files = acervo.get(id) ?? [];
206
+ if (files.length > 1) warnings.push(`${ACERVO_LABEL}/: ${id} tem ${files.length} arquivos (${files.join(', ')}) — o acervo precisa de um arquivo por UC`);
207
+ const file = files[files.length - 1];
208
+ const content = file ? readOrNull(join(acervoDir, file)) : null;
209
+ const life = content === null ? null : lifecycleOf(content);
210
+ if (content !== null) {
211
+ for (const p of supersessionProblems(content, id)) warnings.push(`${ACERVO_LABEL}/${file}: ${p}`);
212
+ }
213
+ acervoLife.set(id, life);
214
+ return life;
215
+ };
216
+
217
+ const spec = readOrNull(specPathOf(featureDir));
218
+ const rev = spec === null ? { entries: [], problems: [] } : parseRevocations(spec);
219
+ for (const p of rev.problems) warnings.push(`spec.md, revogações: ${p.message}`);
220
+
221
+ const revokedHere = new Map();
222
+ let indexedAdrs = null;
223
+ for (const e of rev.entries) {
224
+ if (e.kind === 'adr') {
225
+ indexedAdrs ??= new Set(readAdrIndex(cwd).map((r) => r.ref));
226
+ if (!indexedAdrs.has(e.id)) {
227
+ warnings.push(`spec.md, revogações: ${e.id} não está no índice de ADRs ativos (${ADR_INDEX_LABEL}) — o finish não terá o que retirar`);
228
+ }
229
+ continue;
230
+ }
231
+ revokedHere.set(e.id, e);
232
+ if (!acervo.has(e.id)) {
233
+ warnings.push(`spec.md, revogações: ${e.id} não está no acervo (${ACERVO_LABEL}/) — o finish não terá o que marcar`);
234
+ } else {
235
+ acervoOf(e.id);
236
+ }
237
+ if (!acervo.has(e.substitute) && !own.has(e.substitute)) {
238
+ warnings.push(`spec.md, revogações: o substituto ${e.substitute} de ${e.id} não existe no acervo nem em 2-plan/usecases/ — o finish não aplicará a linha`);
239
+ }
240
+ }
241
+
242
+ const tasks = Array.isArray(tasksData?.tasks) ? tasksData.tasks : [];
243
+ for (const task of tasks) {
244
+ const id = task?.usecase;
245
+ if (!id) continue;
246
+ const inAcervo = acervoOf(id);
247
+ const inOwn = ownLife.get(id);
248
+ const here = revokedHere.get(id);
249
+
250
+ let where = null;
251
+ if (inAcervo?.status === SUPERSEDED) where = { src: 'está superseded no acervo', ...inAcervo };
252
+ else if (inOwn?.status === SUPERSEDED) where = { src: 'está superseded na cópia da feature', ...inOwn };
253
+ else if (here) where = { src: 'é revogado por esta feature (spec.md)', supersededBy: here.substitute, scope: here.scope };
254
+ if (!where) continue;
255
+
256
+ const by = where.supersededBy;
257
+ const detail = [by ? `substituto: ${by}` : 'sem substituto declarado', where.scope ? `ainda vale em: ${where.scope}` : null]
258
+ .filter(Boolean).join('; ');
259
+ const advice = where.scope ? 'confira que a task realiza a parte que ainda vale' : by ? `cite ${by}` : 'confira o acervo';
260
+ warnings.push(`${task.id}.usecase → ${id} ${where.src} (${detail}) — ${advice}`);
261
+ }
262
+
263
+ return { warnings };
264
+ }