@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,44 @@
1
+ /**
2
+ * git-exclude.js — o escritor ÚNICO dos blocos que o morph-spec mantém no
3
+ * `info/exclude` do git.
4
+ *
5
+ * O arquivo lido é o do COMMON dir (`git rev-parse --git-common-dir`), nunca o
6
+ * `.git/worktrees/<id>/info/exclude`: o git não lê o deste para uma worktree
7
+ * vinculada, e o do common dir vale para toda árvore do repositório — inclusive
8
+ * a raiz primária. Cada chamador passa o seu marcador, e o bloco só ganha as
9
+ * linhas que ainda faltam.
10
+ *
11
+ * Idempotente e fail-open: um exclude que não pôde ser escrito custa um
12
+ * arquivo aparecendo como `??`, nunca um erro.
13
+ */
14
+
15
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'fs';
16
+ import { join } from 'path';
17
+ import { resolveGitCommonDir } from './git-paths.js';
18
+
19
+ /**
20
+ * @param {string} cwd - qualquer diretório dentro do repositório
21
+ * @param {string[]} patterns - linhas prontas (já ancoradas, ex. `/graphify-out`)
22
+ * @param {string} marker - comentário que identifica o bloco do chamador
23
+ */
24
+ export function ensureCommonExclude(cwd, patterns, marker) {
25
+ if (!patterns.length) return;
26
+ try {
27
+ // Pelo disco, sem subprocesso: um `git` gerado aqui abria uma janela de
28
+ // console no win32 quando o chamador era um processo destacado.
29
+ const common = resolveGitCommonDir(cwd);
30
+ if (!common) return;
31
+ const infoDir = join(common, 'info');
32
+ const excludePath = join(infoDir, 'exclude');
33
+ const current = existsSync(excludePath) ? readFileSync(excludePath, 'utf8') : '';
34
+ const present = new Set(current.split(/\r?\n/).map((l) => l.trim()));
35
+ const missing = patterns.filter((p) => !present.has(p));
36
+ if (!missing.length) return;
37
+ mkdirSync(infoDir, { recursive: true });
38
+ appendFileSync(excludePath, [
39
+ current && !current.endsWith('\n') ? '\n' : '',
40
+ present.has(marker) ? '' : `${marker}\n`,
41
+ `${missing.join('\n')}\n`,
42
+ ].join(''));
43
+ } catch { /* fail-open */ }
44
+ }
@@ -83,6 +83,23 @@ export function isSecondaryWorktree(startDir) {
83
83
  }
84
84
  }
85
85
 
86
+ /**
87
+ * Top of the working tree that contains `startDir` — the directory holding the
88
+ * nearest `.git` (file or directory), which is what `git rev-parse
89
+ * --show-toplevel` answers, without the subprocess. `null` outside any repo.
90
+ *
91
+ * @param {string} startDir
92
+ * @returns {string|null}
93
+ */
94
+ export function workTreeTop(startDir) {
95
+ try {
96
+ const found = findDotGit(startDir);
97
+ return found ? dirname(found.path) : null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
86
103
  /**
87
104
  * Resolve the git COMMON dir for `startDir` — pure filesystem, no subprocess.
88
105
  * Always returns an absolute path (unlike `git rev-parse --git-common-dir`,
@@ -117,3 +134,38 @@ export function resolveGitCommonDir(startDir) {
117
134
  return null;
118
135
  }
119
136
  }
137
+
138
+ const SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
139
+
140
+ /**
141
+ * The commit HEAD points at in the PRIMARY checkout `root`, read from disk —
142
+ * `git rev-parse HEAD` without the subprocess: a detached SHA, or the branch's
143
+ * loose ref, then `packed-refs`. `null` for every shape it does not read (a
144
+ * linked worktree, reftable, a branch with no commit yet): the caller asks git.
145
+ *
146
+ * @param {string} root
147
+ * @returns {string|null}
148
+ */
149
+ export function readPrimaryHead(root) {
150
+ try {
151
+ const gitDir = join(root, '.git');
152
+ if (!statSync(gitDir).isDirectory()) return null;
153
+ const head = readFileSync(join(gitDir, 'HEAD'), 'utf8').trim();
154
+ if (SHA.test(head)) return head;
155
+ const ref = /^ref:\s*(refs\/\S+)$/.exec(head)?.[1];
156
+ if (!ref) return null;
157
+ try {
158
+ const loose = readFileSync(join(gitDir, ref), 'utf8').trim();
159
+ if (SHA.test(loose)) return loose;
160
+ } catch {
161
+ // not loose — packed
162
+ }
163
+ for (const line of readFileSync(join(gitDir, 'packed-refs'), 'utf8').split(/\r?\n/)) {
164
+ const [sha, name] = line.split(' ');
165
+ if (name === ref && SHA.test(sha)) return sha;
166
+ }
167
+ return null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
@@ -0,0 +1,380 @@
1
+ /**
2
+ * graph-freshness.js — a regra ÚNICA de "o grafo desta árvore está em dia".
3
+ *
4
+ * Todo mundo que pergunta lê daqui: o refresh em background
5
+ * (graph-refresh.js, decide se roda), o `graph status|explain|affected` e o
6
+ * `doctor` (via src/lib/graph/graph-loader.js `getFreshness`) e o
7
+ * `graph build|refresh` (grava o resultado). Mora em hooks/shared porque hook
8
+ * não importa `src/`. Uma segunda cópia da regra é como o status dizia
9
+ * "fresco" enquanto o refresh achava "velho", ou o contrário.
10
+ *
11
+ * Duas fontes, nesta ordem:
12
+ * 1. o carimbo `built_at_commit` do graph.json contra o HEAD;
13
+ * 2. o REGISTRO da última rodada, por árvore, em
14
+ * `<git-common-dir>/morph-spec/graph-refresh-state.json`:
15
+ * `{ [árvore]: { head, exit, ignoreHash?, at, unavailable?, seeded? } }`.
16
+ *
17
+ * O registro existe porque o `graphify update` do 0.9.50 NÃO recarimba quando
18
+ * a topologia não muda — "No code-graph topology changes detected; outputs
19
+ * left untouched" (watch.py): commit só de `.morph/`, corpo de método editado,
20
+ * commit num arquivo excluído. O carimbo fica no commit antigo e, sem o
21
+ * registro, o grafo parecia velho para sempre e cada disparo rodava o graphify
22
+ * de novo (~80 s no MORPH_GHLBrain). Rodada com exit 0 no HEAD H vale como
23
+ * grafo em dia em H: o 0.9.50 sai 1 quando recusa escrever (`_check_shrink`,
24
+ * grafo ilegível), e "untouched" sai 0.
25
+ *
26
+ * As regras do `.graphifyignore` entram por CONTEÚDO, nunca pelo relógio: o
27
+ * registro guarda o hash das regras sob as quais o grafo EM DISCO foi
28
+ * extraído (uma rodada que falhou não o muda), e um hash diferente do das
29
+ * regras que valem hoje é grafo velho — numa worktree sem arquivo próprio,
30
+ * as da raiz, que são as que o `graph refresh` usa ali. Comentário, linha
31
+ * em branco, BOM e CRLF não contam — trocar o cabeçalho não pede refresh.
32
+ * Grafo sem registro das regras (anterior a este estado) com regras valendo é
33
+ * `rules-unknown`: um refresh grava o registro e encerra.
34
+ *
35
+ * Fail-open: nada aqui lança.
36
+ *
37
+ * @module graph-freshness
38
+ */
39
+
40
+ import { createHash } from 'crypto';
41
+ import { execFileSync } from 'child_process';
42
+ import {
43
+ closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync,
44
+ renameSync, unlinkSync, writeFileSync,
45
+ } from 'fs';
46
+ import { basename, dirname, join, resolve } from 'path';
47
+ import { resolveGitCommonDir } from './git-paths.js';
48
+
49
+ export const GRAPH_DIR = 'graphify-out';
50
+ export const GRAPHIFY_IGNORE = '.graphifyignore';
51
+ const STATE_FILE = 'graph-refresh-state.json';
52
+
53
+ /** @param {string} tree */
54
+ export function graphFile(tree) {
55
+ return join(tree, GRAPH_DIR, 'graph.json');
56
+ }
57
+
58
+ /**
59
+ * `built_at_commit` do graph.json. O graphify o grava como a ÚLTIMA chave, então
60
+ * a leitura do fim do arquivo evita parsear 18 MB; a leitura inteira fica de
61
+ * reserva para qualquer outra forma.
62
+ *
63
+ * @param {string} tree
64
+ * @returns {string|null}
65
+ */
66
+ export function readBuiltAtCommit(tree) {
67
+ const file = graphFile(tree);
68
+ let fd;
69
+ try {
70
+ fd = openSync(file, 'r');
71
+ const size = fstatSync(fd).size;
72
+ const len = Math.min(size, 4096);
73
+ const buf = Buffer.alloc(len);
74
+ readSync(fd, buf, 0, len, size - len);
75
+ const m = /"built_at_commit"\s*:\s*"([0-9a-f]{7,64})"/.exec(buf.toString('utf8'));
76
+ if (m) return m[1];
77
+ } catch {
78
+ return null;
79
+ } finally {
80
+ if (fd !== undefined) try { closeSync(fd); } catch { /* já fechado */ }
81
+ }
82
+ try {
83
+ return JSON.parse(readFileSync(file, 'utf8').replace(/^\uFEFF/, ''))?.built_at_commit ?? null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {string} tree
91
+ * @returns {string|null}
92
+ */
93
+ export function readHead(tree) {
94
+ try {
95
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
96
+ cwd: tree, encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'], timeout: 10000,
97
+ }).trim() || null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * As REGRAS de um `.graphifyignore`, sem o que o graphify não lê: comentário,
105
+ * linha em branco, espaço no fim, BOM e CRLF. A ordem fica — no gitignore ela
106
+ * muda o resultado.
107
+ *
108
+ * @param {string} text
109
+ * @returns {string}
110
+ */
111
+ export function ignoreRules(text) {
112
+ return String(text ?? '')
113
+ .replace(/^\uFEFF/, '')
114
+ .split(/\r?\n/)
115
+ .map((line) => line.trimEnd())
116
+ .filter((line) => line && !line.startsWith('#'))
117
+ .join('\n');
118
+ }
119
+
120
+ /**
121
+ * @param {string|null} text
122
+ * @returns {string|null} null quando não há arquivo
123
+ */
124
+ export function rulesHash(text) {
125
+ if (text === null || text === undefined) return null;
126
+ return createHash('sha256').update(ignoreRules(text)).digest('hex').slice(0, 16);
127
+ }
128
+
129
+ /**
130
+ * A árvore principal do repositório — a única que o refresh em background
131
+ * toca. `null` fora de um repositório e quando o common dir não é o `.git` de
132
+ * uma árvore (repositório bare, `--separate-git-dir`): aí não há raiz.
133
+ *
134
+ * @param {string} dir - qualquer diretório do repositório, raiz ou worktree
135
+ * @returns {string|null}
136
+ */
137
+ export function primaryTree(dir) {
138
+ const common = dir ? resolveGitCommonDir(dir) : null;
139
+ if (!common || basename(common) !== '.git') return null;
140
+ return dirname(common);
141
+ }
142
+
143
+ /**
144
+ * 1ª linha do `.graphifyignore` que o `graph refresh` põe numa worktree só
145
+ * durante a rodada. Comentário não muda as regras nem o hash, e é por ele que a
146
+ * sobra de uma rodada cujo processo morreu no meio é reconhecida.
147
+ */
148
+ export const TEMP_IGNORE_MARKER = '# morph-spec: cópia temporária do graph refresh, apagada ao fim da rodada';
149
+
150
+ /** @param {string|null} text */
151
+ export function isTemporaryIgnore(text) {
152
+ return String(text ?? '').replace(/^/, '').split(/\r?\n/, 1)[0] === TEMP_IGNORE_MARKER;
153
+ }
154
+
155
+ function readText(path) {
156
+ try {
157
+ return readFileSync(path, 'utf8');
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * O `.graphifyignore` com que o graphify roda nesta árvore: o do checkout;
165
+ * numa worktree sem um (ou só com a sobra temporária), o da raiz, que é o que o
166
+ * `graph refresh` põe no lugar durante a rodada.
167
+ *
168
+ * @param {string} tree
169
+ * @returns {string|null} null quando nenhum arquivo vale
170
+ */
171
+ export function ignoreTextOf(tree) {
172
+ const own = readText(join(tree, GRAPHIFY_IGNORE));
173
+ if (own !== null && !isTemporaryIgnore(own)) return own;
174
+ const root = primaryTree(tree);
175
+ if (!root || treeKey(root) === treeKey(tree)) return null;
176
+ const inherited = readText(join(root, GRAPHIFY_IGNORE));
177
+ return inherited !== null && !isTemporaryIgnore(inherited) ? inherited : null;
178
+ }
179
+
180
+ /**
181
+ * @param {string} tree
182
+ * @returns {string|null} null quando nenhum `.graphifyignore` vale para a árvore
183
+ */
184
+ export function ignoreRulesHash(tree) {
185
+ return rulesHash(ignoreTextOf(tree));
186
+ }
187
+
188
+ /**
189
+ * As linhas de `git status --porcelain` de `tree` sem a da sobra do
190
+ * `.graphifyignore` temporário: ela é só uma cópia das regras da raiz, e o
191
+ * `?? .graphifyignore` que deixa fazia o `finish` e o
192
+ * `worktree remove --if-clean` recusarem. Só lê — quem confere a árvore chama
193
+ * isto antes das recusas, e `dropLeftoverIgnore` só depois delas.
194
+ *
195
+ * @param {string} tree
196
+ * @param {string[]} lines
197
+ * @returns {string[]}
198
+ */
199
+ export function withoutLeftoverIgnore(tree, lines) {
200
+ const leftover = `?? ${GRAPHIFY_IGNORE}`;
201
+ if (!lines.includes(leftover) || !isTemporaryIgnore(readText(join(tree, GRAPHIFY_IGNORE)))) return lines;
202
+ return lines.filter((line) => line !== leftover);
203
+ }
204
+
205
+ /**
206
+ * Apaga a sobra do `.graphifyignore` temporário, reconhecida pelo marcador.
207
+ *
208
+ * @param {string} tree
209
+ * @returns {boolean} apagou
210
+ */
211
+ export function dropLeftoverIgnore(tree) {
212
+ const path = join(tree, GRAPHIFY_IGNORE);
213
+ if (!isTemporaryIgnore(readText(path))) return false;
214
+ try {
215
+ unlinkSync(path);
216
+ return true;
217
+ } catch {
218
+ return false;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Chave de uma árvore no registro. No win32 o caminho não distingue caixa, e
224
+ * `R:\x` e `r:\x` são a mesma árvore.
225
+ *
226
+ * @param {string} tree
227
+ */
228
+ export function treeKey(tree) {
229
+ const path = resolve(tree);
230
+ return process.platform === 'win32' ? path.toLowerCase() : path;
231
+ }
232
+
233
+ /**
234
+ * @param {string} dir - qualquer diretório do repositório
235
+ * @returns {string|null}
236
+ */
237
+ export function refreshStatePath(dir) {
238
+ const common = dir ? resolveGitCommonDir(dir) : null;
239
+ return common ? join(common, 'morph-spec', STATE_FILE) : null;
240
+ }
241
+
242
+ function readState(path) {
243
+ try {
244
+ const state = JSON.parse(readFileSync(path, 'utf8'));
245
+ return state && typeof state === 'object' && !Array.isArray(state) ? state : {};
246
+ } catch {
247
+ return {};
248
+ }
249
+ }
250
+
251
+ /**
252
+ * @param {string} tree
253
+ * @returns {{head: string|null, exit: number|null, ignoreHash?: string|null, at: string, unavailable?: boolean, seeded?: boolean}|null}
254
+ */
255
+ export function readRefreshRecord(tree) {
256
+ const path = refreshStatePath(tree);
257
+ if (!path) return null;
258
+ const record = readState(path)[treeKey(tree)];
259
+ return record && typeof record === 'object' ? record : null;
260
+ }
261
+
262
+ function pause(ms) {
263
+ try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* sem espera */ }
264
+ }
265
+
266
+ /**
267
+ * Grava o registro de `tree` (substitui o anterior). Por tmp + rename: quem lê
268
+ * no meio vê o arquivo velho ou o novo, nunca meio. As árvores que não existem
269
+ * mais saem na mesma escrita — uma worktree removida não deixa lixo.
270
+ *
271
+ * @param {string} tree
272
+ * @param {object} record
273
+ * @returns {object|null} o registro gravado, ou null (sem git, disco)
274
+ */
275
+ export function writeRefreshRecord(tree, record) {
276
+ const path = refreshStatePath(tree);
277
+ if (!path) return null;
278
+ const key = treeKey(tree);
279
+ const entry = { ...record, at: new Date().toISOString() };
280
+ const tmp = `${path}.tmp.${process.pid}`;
281
+ try {
282
+ mkdirSync(dirname(path), { recursive: true });
283
+ const state = readState(path);
284
+ for (const other of Object.keys(state)) {
285
+ if (other !== key && !existsSync(other)) delete state[other];
286
+ }
287
+ state[key] = entry;
288
+ writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
289
+ // No win32 o rename sobre um arquivo aberto por outro leitor falha por um
290
+ // instante (EPERM/EBUSY); o registro vale umas tentativas.
291
+ for (let attempt = 0; ; attempt++) {
292
+ try {
293
+ renameSync(tmp, path);
294
+ return entry;
295
+ } catch (err) {
296
+ if (attempt >= 4 || !['EPERM', 'EBUSY', 'EACCES'].includes(err?.code)) throw err;
297
+ pause(25);
298
+ }
299
+ }
300
+ } catch {
301
+ try { unlinkSync(tmp); } catch { /* nunca criado */ }
302
+ return null;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Registra uma rodada do graphify sobre `tree`. As regras só mudam com exit 0:
308
+ * uma rodada que falhou não mexeu no grafo, e o hash descreve o grafo em
309
+ * disco, não a tentativa.
310
+ *
311
+ * @param {string} tree
312
+ * @param {{head: string|null, exit: number|null, ignoreHash: string|null, unavailable?: boolean}} run
313
+ * `head` e `ignoreHash` lidos ANTES da rodada — é o que o graphify leu
314
+ */
315
+ export function recordRefreshRun(tree, { head, exit, ignoreHash, unavailable = false }) {
316
+ const previous = readRefreshRecord(tree);
317
+ let rules = {};
318
+ if (exit === 0) rules = { ignoreHash };
319
+ else if (previous && 'ignoreHash' in previous) rules = { ignoreHash: previous.ignoreHash };
320
+ return writeRefreshRecord(tree, {
321
+ head: head ?? null,
322
+ exit: exit ?? null,
323
+ ...rules,
324
+ ...(unavailable ? { unavailable: true } : {}),
325
+ });
326
+ }
327
+
328
+ /**
329
+ * O frescor do grafo de `tree`.
330
+ *
331
+ * missing sem graph.json
332
+ * unknown sem git, ou sem carimbo nem registro que diga o commit
333
+ * stale atrás do HEAD (`reason` ausente), ou em dia no commit mas com as
334
+ * regras do ignore mudadas (`ignore-changed`) ou desconhecidas
335
+ * (`rules-unknown`)
336
+ * fresh carimbo = HEAD, ou rodada com exit 0 neste HEAD — e as regras
337
+ * que valem para a árvore são as do grafo (`ignoreTextOf`; nenhum
338
+ * arquivo valendo não afirma nada)
339
+ *
340
+ * @param {string} tree - o diretório que contém `graphify-out/`
341
+ * @param {{graph?: object, head?: string|null}} [known] o que o chamador já
342
+ * leu: o grafo parseado (o carimbo sai dele) e o HEAD
343
+ * @returns {{state: 'missing'|'unknown'|'stale'|'fresh', built: string|null, head: string|null, reason?: 'ignore-changed'|'rules-unknown', record: object|null}}
344
+ */
345
+ export function graphFreshness(tree, { graph, head } = {}) {
346
+ if (!graph && !existsSync(graphFile(tree))) {
347
+ return { state: 'missing', built: null, head: null, record: null };
348
+ }
349
+ const built = graph ? (graph.built_at_commit ?? null) : readBuiltAtCommit(tree);
350
+ const current = head !== undefined ? head : readHead(tree);
351
+ const record = readRefreshRecord(tree);
352
+ const base = { built, head: current, record };
353
+ if (!current) return { state: 'unknown', ...base };
354
+
355
+ const atHead = built === current || (record?.exit === 0 && record.head === current);
356
+ if (!atHead) return { state: built ? 'stale' : 'unknown', ...base };
357
+
358
+ const rules = ignoreRulesHash(tree);
359
+ if (rules !== null) {
360
+ if (!record || !('ignoreHash' in record)) return { state: 'stale', reason: 'rules-unknown', ...base };
361
+ if (record.ignoreHash !== rules) return { state: 'stale', reason: 'ignore-changed', ...base };
362
+ }
363
+ return { state: 'fresh', ...base };
364
+ }
365
+
366
+ /**
367
+ * Um refresh em background resolveria? Grafo velho, e sem uma rodada que já
368
+ * FALHOU neste mesmo HEAD — essa só se repete quando o HEAD andar (ou por
369
+ * `morph-spec graph refresh`, que é explícito). "graphify ausente" não conta
370
+ * como falha: instalado, ele roda no próximo disparo.
371
+ *
372
+ * @param {{state: string, head: string|null, record: object|null}} freshness
373
+ * @returns {boolean}
374
+ */
375
+ export function refreshPending(freshness) {
376
+ if (freshness?.state !== 'stale') return false;
377
+ const r = freshness.record;
378
+ const failedHere = r && r.head === freshness.head && r.exit !== 0 && !r.unavailable;
379
+ return !failedHere;
380
+ }