@polymorphism-tech/morph-spec 8.33.0 → 8.35.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 (253) hide show
  1. package/CLAUDE.md +22 -2
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +43 -5
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +5 -4
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +2 -2
  8. package/framework/MORPH.md +20 -22
  9. package/framework/agents.json +198 -44
  10. package/framework/ai-pin.json +22 -0
  11. package/framework/commands/morph-media.md +45 -0
  12. package/framework/docs/CLI.md +238 -10
  13. package/framework/evals/proof-rubrics/README.md +76 -0
  14. package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
  15. package/framework/evals/proof-rubrics/fiacao.md +54 -0
  16. package/framework/evals/proof-rubrics/guard-cego.md +52 -0
  17. package/framework/evals/proof-rubrics/media-claims.md +109 -0
  18. package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
  19. package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
  20. package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
  21. package/framework/evals/rubrics/architecture.md +30 -0
  22. package/framework/evals/rubrics/code-quality.md +26 -0
  23. package/framework/evals/rubrics/test-coverage.md +20 -0
  24. package/framework/hooks/README.md +11 -1
  25. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
  26. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
  27. package/framework/hooks/claude-code/core/chain.js +27 -7
  28. package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
  29. package/framework/hooks/claude-code/core/worktree-context.js +83 -14
  30. package/framework/hooks/claude-code/core/worktree-create.js +206 -0
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
  32. package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
  33. package/framework/hooks/shared/command-tokens.js +23 -0
  34. package/framework/hooks/shared/e2e-check.js +0 -0
  35. package/framework/hooks/shared/git-paths.js +25 -0
  36. package/framework/hooks/shared/hook-response.js +17 -0
  37. package/framework/hooks/shared/morph-cli.js +134 -0
  38. package/framework/rules/ai-agents-standards.md +14 -2
  39. package/framework/rules/infrastructure-standards.md +3 -1
  40. package/framework/schemas/ai-pin.schema.json +97 -0
  41. package/framework/schemas/config.schema.json +107 -0
  42. package/framework/schemas/feature.schema.json +23 -4
  43. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  44. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  45. package/framework/skills/core/morph-apply/SKILL.md +28 -7
  46. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  47. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  48. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  49. package/framework/skills/core/morph-implement/SKILL.md +20 -2
  50. package/framework/skills/core/morph-media/SKILL.md +215 -0
  51. package/framework/skills/core/morph-plan/SKILL.md +38 -4
  52. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  53. package/framework/skills/core/morph-review/SKILL.md +166 -9
  54. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  55. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  56. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  57. package/framework/standards/PROVENANCE.json +81 -3
  58. package/framework/standards/STANDARDS.json +1504 -830
  59. package/framework/standards/ai-agents/agent-archetypes.md +160 -0
  60. package/framework/standards/ai-agents/agent-session.md +256 -160
  61. package/framework/standards/ai-agents/agent-spec.md +196 -0
  62. package/framework/standards/ai-agents/batching.md +217 -193
  63. package/framework/standards/ai-agents/context-providers.md +261 -188
  64. package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
  65. package/framework/standards/ai-agents/cost-and-budget.md +256 -0
  66. package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
  67. package/framework/standards/ai-agents/evals-with-cache.md +289 -0
  68. package/framework/standards/ai-agents/guardrails.md +222 -0
  69. package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
  70. package/framework/standards/ai-agents/mcp-server.md +351 -250
  71. package/framework/standards/ai-agents/mcp-tools.md +250 -197
  72. package/framework/standards/ai-agents/media-pipeline.md +352 -0
  73. package/framework/standards/ai-agents/media-video.md +267 -0
  74. package/framework/standards/ai-agents/middleware-patterns.md +241 -207
  75. package/framework/standards/ai-agents/modalities-image-gen.md +422 -173
  76. package/framework/standards/ai-agents/modalities-speech-to-text.md +177 -171
  77. package/framework/standards/ai-agents/modalities-text-to-speech.md +189 -181
  78. package/framework/standards/ai-agents/modalities-vision.md +205 -201
  79. package/framework/standards/ai-agents/multi-agent-patterns.md +211 -155
  80. package/framework/standards/ai-agents/observability-patterns.md +227 -17
  81. package/framework/standards/ai-agents/production.md +260 -231
  82. package/framework/standards/ai-agents/prompt-sources.md +389 -0
  83. package/framework/standards/ai-agents/providers/model-registry.md +293 -158
  84. package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
  85. package/framework/standards/ai-agents/service-tier-flex.md +143 -135
  86. package/framework/standards/ai-agents/setup.md +166 -50
  87. package/framework/standards/ai-agents/structured-output.md +280 -249
  88. package/framework/standards/ai-agents/sweet-spot.md +129 -109
  89. package/framework/standards/ai-agents/testing-ai.md +236 -0
  90. package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
  91. package/framework/standards/ai-agents/workflows.md +27 -7
  92. package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
  93. package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
  94. package/framework/standards/infrastructure/docker/local-compose-isolation.md +90 -0
  95. package/framework/templates/REGISTRY.json +14 -1
  96. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  97. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  98. package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
  99. package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
  100. package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
  101. package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
  102. package/framework/templates/dotnet/ai-kit/README.md +128 -0
  103. package/framework/templates/dotnet/ai-kit/global.json +9 -0
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
  118. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
  119. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
  120. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
  121. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
  122. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
  123. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
  124. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
  125. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
  126. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
  127. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
  128. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
  129. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
  130. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
  131. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
  132. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
  133. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
  134. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
  135. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
  136. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
  137. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
  138. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
  139. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
  140. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
  141. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
  142. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
  143. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
  144. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
  145. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
  146. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
  147. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
  148. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
  149. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
  150. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
  151. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
  152. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
  153. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
  154. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
  155. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
  156. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
  157. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
  158. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
  159. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
  160. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
  161. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
  162. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
  163. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
  164. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
  165. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
  166. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
  167. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
  168. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
  169. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
  170. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
  171. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
  172. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
  173. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
  174. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
  175. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
  176. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
  177. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
  178. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
  179. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
  180. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
  181. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
  182. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
  183. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
  184. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
  185. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
  186. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
  187. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
  188. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
  189. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
  190. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
  191. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
  192. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
  193. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
  194. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
  195. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
  196. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
  197. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
  198. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
  199. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
  200. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
  201. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
  202. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
  203. package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
  204. package/package.json +3 -3
  205. package/src/commands/e2e/e2e.js +91 -40
  206. package/src/commands/project/doctor.js +143 -6
  207. package/src/commands/project/env.js +95 -0
  208. package/src/commands/project/update.js +469 -153
  209. package/src/commands/project/worktree.js +274 -53
  210. package/src/commands/state/advance.js +24 -2
  211. package/src/commands/state/approve.js +33 -1
  212. package/src/commands/state/create.js +72 -11
  213. package/src/commands/state/delete.js +8 -0
  214. package/src/commands/state/finish.js +360 -53
  215. package/src/commands/state/gate-check.js +190 -6
  216. package/src/commands/state/score.js +36 -1
  217. package/src/commands/validation/verify.js +134 -25
  218. package/src/core/state/state-manager.js +9 -1
  219. package/src/dashboard/app/pages/HookInspector.jsx +430 -418
  220. package/src/dashboard/scanner/hooks-scanner.js +5 -1
  221. package/src/lib/ai/ai-checks.js +182 -0
  222. package/src/lib/ai/ai-project-scan.js +347 -0
  223. package/src/lib/ai/code-audit.js +181 -0
  224. package/src/lib/ai/comment-stripper.js +184 -0
  225. package/src/lib/ai/pin-drift.js +138 -0
  226. package/src/lib/ai/registry-audit.js +285 -0
  227. package/src/lib/e2e/compose-env.js +55 -14
  228. package/src/lib/e2e/stack-teardown.js +115 -0
  229. package/src/lib/evaluator-report.js +269 -15
  230. package/src/lib/generators/architecture-scanner.js +2 -1
  231. package/src/lib/git/default-branch.js +92 -4
  232. package/src/lib/graph/graphifyignore.js +2 -1
  233. package/src/lib/installers/effects.js +193 -0
  234. package/src/lib/installers/templates-reconcile.js +3 -3
  235. package/src/lib/risk-detector.js +284 -0
  236. package/src/lib/standards/verified-against.js +276 -0
  237. package/src/lib/tasks/eval-node.js +485 -0
  238. package/src/lib/tasks/linux-runner.js +297 -0
  239. package/src/lib/tasks/test-nodes.js +486 -0
  240. package/src/lib/tasks/test-runner.js +431 -37
  241. package/src/lib/validators/verify-runner.js +590 -28
  242. package/src/lib/worktree/dirs.js +32 -0
  243. package/src/lib/worktree/docker-audit.js +287 -0
  244. package/src/lib/worktree/env-allocator.js +69 -11
  245. package/src/lib/worktree/worktree-audit.js +70 -3
  246. package/src/scripts/setup-infra.js +24 -11
  247. package/src/utils/agents-installer.js +18 -8
  248. package/src/utils/claude-md-injector.js +10 -5
  249. package/src/utils/file-copier.js +28 -16
  250. package/src/utils/hooks-installer.js +75 -29
  251. package/src/utils/skills-installer.js +194 -187
  252. package/src/utils/version-checker.js +68 -3
  253. package/framework/standards/ai-agents/prompts-in-db.md +0 -214
@@ -15,6 +15,41 @@
15
15
  * checagem por `existsSync` puro do `approve` aceitava um relatório SEM o token
16
16
  * que o `gate-check` então descartava — o Gate 3 passava carimbado `auto` sobre
17
17
  * uma média de auto-avaliação, exatamente o que MORPH.md §5 proíbe.
18
+ *
19
+ * ── O TOKEN VIGENTE É O ÚLTIMO ──────────────────────────────────────────────
20
+ *
21
+ * O `evaluator-report.md` é APPEND-ONLY por contrato: uma reavaliação acrescenta
22
+ * ao fim, preservando a análise anterior. Este módulo lia o COMEÇO (`String.match`
23
+ * com regex sem flag `g`), então num relatório reavaliado o gate era decidido
24
+ * pelo score SUPERADO. Medido em campo três vezes na mesma epic — #90 (8.7 →
25
+ * 9.4), #92 (8.2 → 9.0) e #93 (8.9 → 8.3 → 9.1) — e nas três o avaliador teve de
26
+ * desarmar o token antigo à mão para o gate ler a nota vigente.
27
+ *
28
+ * O caso perigoso é o INVERSO e é silencioso: numa reavaliação que BAIXA a nota
29
+ * (o #93 caiu de 8.9 para 8.3 quando uma correção abriu um bloqueante), o gate
30
+ * leria a nota antiga, mais alta, e AUTO-PASSARIA o Gate 3 sobre um produto com
31
+ * bloqueante aberto — a mesma classe de defeito que a v8.26.0 existiu para
32
+ * fechar, voltando por outra porta: lendo um veredito real, porém vencido.
33
+ *
34
+ * DECISÃO EXPLÍCITA — "o último vence", e não "múltiplos tokens viram
35
+ * `report-ambiguous` e recusam":
36
+ *
37
+ * 1. É a semântica do próprio artefato. Append-only significa que o arquivo é
38
+ * um LOG, e o valor corrente de um log é a sua última entrada. Ler o
39
+ * primeiro não é uma política conservadora — é ler a entrada errada.
40
+ * 2. Recusar transformaria um veredito REAL e CORRENTE numa pausa que só sai
41
+ * editando à mão um arquivo declarado append-only. Isto é exatamente o
42
+ * contorno manual que este conserto existe para eliminar, promovido a
43
+ * requisito.
44
+ * 3. Recusar não é mais seguro em nenhum dos dois casos observados: com a nota
45
+ * SUBINDO, o humano é obrigado a mexer no relatório para liberar um gate
46
+ * que já foi aprovado; com a nota CAINDO, "o último vence" já pausa sozinho
47
+ * pela regra de score, que é a razão certa a reportar.
48
+ *
49
+ * Ambiguidade não fica silenciosa, ainda assim: `evaluatorReportState` devolve
50
+ * `scoreTokens` (quantos tokens vivos o arquivo tem) e `superseded` (os valores
51
+ * anteriores, na ordem do arquivo). Quem consome pode dizer "9.1, terceira
52
+ * rodada, superando 8.9 e 8.3" em vez de fingir que só havia um veredito.
18
53
  */
19
54
 
20
55
  import { existsSync, readFileSync } from 'fs';
@@ -27,29 +62,133 @@ import { getAbsoluteOutputPath } from '../core/paths/output-schema.js';
27
62
  */
28
63
  export const COMPOSITE_SCORE_TOKEN = '**Composite-Score:**';
29
64
 
30
- const COMPOSITE_SCORE_RE = /\*\*Composite-Score:\*\*\s*([0-9]+(?:\.[0-9]+)?)/;
65
+ /**
66
+ * O token das rubricas de PROVA (`.morph/framework/evals/proof-rubrics/`):
67
+ * `**Proof-Scores:** mutation=9 verde=8 fiacao=9 guard=7 eval=8`. Mesma convenção
68
+ * literal do Composite-Score de propósito — mesmo arquivo, mesmo módulo leitor,
69
+ * mesmo estilo de token. Ele pontua a EVIDÊNCIA que o avaliador produziu, nunca
70
+ * o produto: não entra no composto, impõe teto em `test-coverage`.
71
+ */
72
+ export const PROOF_SCORES_TOKEN = '**Proof-Scores:**';
73
+
74
+ /** `g` é obrigatório: o arquivo é append-only e o token vigente é o ÚLTIMO. */
75
+ const COMPOSITE_SCORE_RE = /\*\*Composite-Score:\*\*\s*([0-9]+(?:\.[0-9]+)?)/g;
76
+
77
+ /** Idem — uma reavaliação reescreve os Proof-Scores acrescentando ao fim. */
78
+ const PROOF_SCORES_RE = /\*\*Proof-Scores:\*\*[ \t]*([^\r\n]*)/g;
79
+
80
+ /**
81
+ * A seção de prova de mutação. Exige `##`: um `# Mutações` é outra estrutura
82
+ * (H1 é título de documento, não seção), e o template que a `morph-review`
83
+ * entrega ao avaliador gera exatamente `## Mutações`. Aceita a grafia sem
84
+ * acento porque um relatório escrito em terminal ASCII continua sendo o mesmo
85
+ * fato — o que não se aceita é o nível errado de cabeçalho.
86
+ */
87
+ const MUTATION_SECTION_RE = /^##\s+Muta[çc][õo]es\s*$/m;
88
+
89
+ /**
90
+ * A seção de prova de MÍDIA. Mesma disciplina do `## Mutações`: exige `##`, e
91
+ * aceita a grafia sem acento porque um relatório escrito em terminal ASCII
92
+ * continua sendo o mesmo fato. `Mídia gerada` / `Midia gerada`.
93
+ */
94
+ const MEDIA_SECTION_RE = /^##\s+M[íi]dia\s+gerada\s*$/m;
95
+
96
+ /**
97
+ * As chaves reconhecidas de `**Proof-Scores:**`, uma por rubrica de prova.
98
+ * Chave fora desta lista é IGNORADA (não vira score, não vira erro): o relatório
99
+ * é texto de LLM e uma chave inventada não deve nem contaminar nenhum mínimo nem
100
+ * derrubar a leitura das outras.
101
+ *
102
+ * `media` PRECISA estar aqui. Sem isso a rubrica `media-claims.md` seria
103
+ * decorativa: o avaliador escreveria `media=7` e o número seria descartado em
104
+ * silêncio, que é o caso "campo publicado sem nenhum leitor".
105
+ */
106
+ export const PROOF_SCORE_KEYS = ['mutation', 'verde', 'fiacao', 'guard', 'eval', 'media', 'obs'];
107
+
108
+ /**
109
+ * As chaves que NÃO entram no `min` de `test-coverage` porque impõem teto em
110
+ * OUTRA dimensão: `media` (teto em `architecture`) e `obs` (teto em
111
+ * `code-quality`).
112
+ *
113
+ * **`obs` chegou exatamente pelo caminho que o parágrafo abaixo antecipou**, e
114
+ * vale registrar porque o aviso não bastou. A feature irmã (#94) criou a rubrica
115
+ * `observabilidade-genai.md`, escreveu `obs=` onze vezes no seu próprio
116
+ * `evaluator-report.md` — e a chave não estava em `PROOF_SCORE_KEYS`, nem lá nem
117
+ * na base dela. O parser a descartava em silêncio (ver o `continue` de
118
+ * `parseProofScores`), então a rubrica nasceu DECORATIVA e atravessou **onze
119
+ * rodadas de Gate 3** sem que ninguém percebesse: o número era escrito, lido por
120
+ * um humano no relatório, e nunca chegava ao gate. Só o rebase, que põe as duas
121
+ * features na mesma árvore, tornou o buraco visível.
122
+ *
123
+ * **Isto é uma exclusão, e a forma importa mais que a lista.** A primeira versão
124
+ * era o inverso — uma lista fechada das quatro chaves que entram. Ela responde
125
+ * certo hoje e erra sozinha amanhã: a feature irmã acrescenta uma quinta chave
126
+ * (`eval`) com rubrica própria, e depois do rebase esse `eval=N` cairia do teto
127
+ * **em silêncio** — o "campo publicado sem nenhum leitor" que este mesmo arquivo
128
+ * existe para impedir, produzido pela correção.
129
+ *
130
+ * Por exclusão, uma chave nova nasce DENTRO do mínimo por padrão, e tirá-la de lá
131
+ * passa a ser um ato deliberado e visível: uma linha aqui, com a dimensão que ela
132
+ * limita escrita ao lado. Errar para o lado de "conta no mínimo" custa um teto a
133
+ * mais; errar para o outro lado devolve o gate cego.
134
+ *
135
+ * O motivo de `media` estar fora continua o mesmo: as quatro do mínimo perguntam
136
+ * "a evidência é real?"; `media` pergunta "a chamada mora no lugar certo? que
137
+ * estados existem? o que é publicado?". Um `media=4` derrubando o teto de TESTE
138
+ * seriam dois fatos diferentes lidos como um só.
139
+ */
140
+ export const NON_MUTATION_PROOF_KEYS = ['media', 'obs'];
141
+
142
+ /**
143
+ * As chaves que entram no `min` que impõe teto em `test-coverage`: DERIVADA de
144
+ * {@link PROOF_SCORE_KEYS} menos {@link NON_MUTATION_PROOF_KEYS} — nunca digitada.
145
+ * Ver o racional da exclusão acima.
146
+ */
147
+ export const MUTATION_PROOF_KEYS =
148
+ PROOF_SCORE_KEYS.filter((k) => !NON_MUTATION_PROOF_KEYS.includes(k));
31
149
 
32
150
  /** Path canônico do evaluator-report da feature. Fonte única para os dois comandos. */
33
151
  export function evaluatorReportPath(cwd, featureName) {
34
152
  return getAbsoluteOutputPath(cwd, featureName, 'evaluatorReport');
35
153
  }
36
154
 
155
+ /** Conteúdo do relatório, ou null quando ausente/ilegível. Nunca lança. */
156
+ function readReport(cwd, featureName) {
157
+ try {
158
+ const p = evaluatorReportPath(cwd, featureName);
159
+ if (!existsSync(p)) return null;
160
+ return readFileSync(p, 'utf8');
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Todos os valores de `**Composite-Score:**` do arquivo, NA ORDEM DO ARQUIVO.
168
+ * O último é o vigente; os anteriores são rodadas superadas.
169
+ *
170
+ * @param {string} text
171
+ * @returns {number[]}
172
+ */
173
+ function compositeScoresIn(text) {
174
+ return [...text.matchAll(COMPOSITE_SCORE_RE)].map((m) => Number(m[1]));
175
+ }
176
+
37
177
  /**
38
178
  * Score composto REAL da feature, do token gravado pelo avaliador independente.
39
179
  *
180
+ * Num relatório reavaliado (append-only), é o token do FIM — o veredito
181
+ * vigente. Ver o bloco "O TOKEN VIGENTE É O ÚLTIMO" no topo do arquivo.
182
+ *
40
183
  * @returns {number|undefined} undefined quando o relatório não existe, não é
41
184
  * legível, ou existe sem o token (relatório legado ou oco). `undefined` é
42
185
  * deliberado e não 0: quem chama distingue "sem veredito" de "veredito ruim".
43
186
  */
44
187
  export function evaluatorCompositeScore(cwd, featureName) {
45
- try {
46
- const p = evaluatorReportPath(cwd, featureName);
47
- if (!existsSync(p)) return undefined;
48
- const m = readFileSync(p, 'utf8').match(COMPOSITE_SCORE_RE);
49
- return m ? Number(m[1]) : undefined;
50
- } catch {
51
- return undefined;
52
- }
188
+ const text = readReport(cwd, featureName);
189
+ if (text === null) return undefined;
190
+ const scores = compositeScoresIn(text);
191
+ return scores.length === 0 ? undefined : scores[scores.length - 1];
53
192
  }
54
193
 
55
194
  /**
@@ -60,13 +199,128 @@ export function evaluatorCompositeScore(cwd, featureName) {
60
199
  * "o avaliador rodou mas o relatório está oco — ele não seguiu o contrato";
61
200
  * e só o terceiro caso é aprovável.
62
201
  *
63
- * @returns {{ status: 'ok'|'missing'|'untokenized', score?: number, path: string }}
202
+ * `scoreTokens`/`superseded` só aparecem no caso `ok` e existem para que uma
203
+ * reavaliação nunca passe silenciosa: o score é o último, mas quem consome sabe
204
+ * quantas rodadas o arquivo carrega e quais notas foram superadas.
205
+ *
206
+ * @returns {{ status: 'ok'|'missing'|'untokenized', score?: number,
207
+ * scoreTokens?: number, superseded?: number[], path: string }}
64
208
  */
65
209
  export function evaluatorReportState(cwd, featureName) {
66
210
  const path = evaluatorReportPath(cwd, featureName);
67
- if (!existsSync(path)) return { status: 'missing', path };
68
- const score = evaluatorCompositeScore(cwd, featureName);
69
- return score === undefined
70
- ? { status: 'untokenized', path }
71
- : { status: 'ok', score, path };
211
+ const text = readReport(cwd, featureName);
212
+ if (text === null) return { status: 'missing', path };
213
+ const scores = compositeScoresIn(text);
214
+ if (scores.length === 0) return { status: 'untokenized', path };
215
+ return {
216
+ status: 'ok',
217
+ score: scores[scores.length - 1],
218
+ scoreTokens: scores.length,
219
+ superseded: scores.slice(0, -1),
220
+ path,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Parseia o corpo de UMA linha `**Proof-Scores:**` — pares `chave=N` separados
226
+ * por espaço.
227
+ *
228
+ * @param {string} body
229
+ * @returns {Record<string, number>|undefined} undefined quando nenhum par
230
+ * VÁLIDO foi lido (linha vazia, só chaves desconhecidas, ou só valores
231
+ * não-numéricos). `undefined` e não `{}` pela mesma disciplina do
232
+ * `evaluatorCompositeScore`: "sem veredito" tem de ser distinguível de
233
+ * "veredito vazio".
234
+ */
235
+ function parseProofScores(body) {
236
+ const out = {};
237
+ for (const m of body.matchAll(/([A-Za-zÀ-ÿ][\w-]*)\s*=\s*([^\s]+)/g)) {
238
+ const key = m[1].toLowerCase();
239
+ if (!PROOF_SCORE_KEYS.includes(key)) continue;
240
+ if (!/^[0-9]+(?:\.[0-9]+)?$/.test(m[2])) continue;
241
+ out[key] = Number(m[2]);
242
+ }
243
+ return Object.keys(out).length === 0 ? undefined : out;
244
+ }
245
+
246
+ /**
247
+ * Estado de PROVA do relatório: a seção `## Mutações` está lá, e quais
248
+ * Proof-Scores o avaliador registrou.
249
+ *
250
+ * Deriva do disco e não julga: quem decide se a ausência pausa o Gate 3 é
251
+ * `risk-detector.js` (a divisão "gate-check coleta, risk-detector decide"), e
252
+ * quem decide o teto é a rubrica `test-coverage.md`. O `min` NÃO é gravado
253
+ * aqui — é derivado por quem consome, para que exista um só lugar onde
254
+ * "mínimo de quantas chaves" é definido.
255
+ *
256
+ * Arquivo ausente ou ilegível: `{ mutationSection: false, mediaSection: false,
257
+ * proofScores: undefined }`. Nunca lança, e nunca devolve `0` no lugar de
258
+ * `undefined`.
259
+ *
260
+ * `mediaSection` é lido do MESMO arquivo pelo MESMO módulo, de propósito: uma
261
+ * segunda leitura do relatório noutro lugar é como duas superfícies passam a
262
+ * discordar sobre o que ele diz.
263
+ *
264
+ * @param {string} cwd
265
+ * @param {string} featureName
266
+ * @returns {{ mutationSection: boolean, mediaSection: boolean, proofScores: Record<string, number>|undefined, path: string }}
267
+ */
268
+ export function evaluatorProofState(cwd, featureName) {
269
+ const path = evaluatorReportPath(cwd, featureName);
270
+ const text = readReport(cwd, featureName);
271
+ if (text === null) {
272
+ return { mutationSection: false, mediaSection: false, proofScores: undefined, path };
273
+ }
274
+
275
+ // Append-only outra vez: numa reavaliação a linha nova vai para o fim, e é
276
+ // ela que vale. A primeira linha descreveria a prova de uma rodada superada.
277
+ const lines = [...text.matchAll(PROOF_SCORES_RE)];
278
+ const proofScores = lines.length === 0
279
+ ? undefined
280
+ : parseProofScores(lines[lines.length - 1][1]);
281
+
282
+ return {
283
+ mutationSection: MUTATION_SECTION_RE.test(text),
284
+ mediaSection: MEDIA_SECTION_RE.test(text),
285
+ proofScores,
286
+ path,
287
+ };
288
+ }
289
+
290
+ /**
291
+ * O menor Proof-Score das provas de EVIDÊNCIA — o que impõe o teto em
292
+ * `test-coverage`.
293
+ *
294
+ * Calculado sobre {@link MUTATION_PROOF_KEYS} — que é
295
+ * {@link PROOF_SCORE_KEYS} MENOS {@link NON_MUTATION_PROOF_KEYS}, e não uma lista
296
+ * digitada. `media` responde a outra pergunta e limita outra dimensão
297
+ * (`architecture`); ler as duas famílias como um mínimo só faria um `media=4`
298
+ * derrubar o teto de teste sem que nada de teste tivesse acontecido. Uma chave
299
+ * NOVA de prova entra aqui sozinha: excluí-la exige uma linha deliberada em
300
+ * `NON_MUTATION_PROOF_KEYS`, e nunca acontece por esquecimento.
301
+ *
302
+ * @param {Record<string, number>|undefined} proofScores
303
+ * @returns {number|null} null quando não há nenhum score do mínimo: SEM TETO A
304
+ * APLICAR, jamais "teto 0". A mesma razão pela qual `count: null` do verify
305
+ * não é 0.
306
+ */
307
+ export function minProofScore(proofScores) {
308
+ if (!proofScores) return null;
309
+ const values = MUTATION_PROOF_KEYS
310
+ .map((key) => proofScores[key])
311
+ .filter((n) => typeof n === 'number' && Number.isFinite(n));
312
+ return values.length === 0 ? null : Math.min(...values);
313
+ }
314
+
315
+ /**
316
+ * O Proof-Score de MÍDIA, lido à parte. Impõe teto em `architecture` (tabela em
317
+ * `framework/evals/rubrics/architecture.md`, "Tetos de prova de mídia").
318
+ *
319
+ * @param {Record<string, number>|undefined} proofScores
320
+ * @returns {number|null} null quando a chave não foi registrada — o caso normal
321
+ * de uma feature que não gera mídia, e nunca "teto 0".
322
+ */
323
+ export function mediaProofScore(proofScores) {
324
+ const value = proofScores?.media;
325
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
72
326
  }
@@ -20,8 +20,9 @@ import { fileURLToPath } from 'url';
20
20
  import { existsSync, readFileSync } from 'fs';
21
21
  import { scanGraph } from '../graph/god-nodes.js';
22
22
  import { renderTemplate } from '../../core/templates/template-renderer.js';
23
+ import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
23
24
 
24
- const SKIP_DIRS = new Set(['node_modules', 'bin', 'obj', '.git', '.morph', '.next', 'dist', 'out', 'worktrees', '.worktrees']);
25
+ export const SKIP_DIRS = new Set(['node_modules', 'bin', 'obj', '.git', '.morph', '.next', 'dist', 'out', ...WORKTREE_DIR_NAMES]);
25
26
  const MAX_FILES_PER_SCANNER = 200;
26
27
 
27
28
  /**
@@ -126,12 +126,52 @@ function parseShortstat(text) {
126
126
  * @returns {number}
127
127
  */
128
128
  function countUntrackedFiles(cwd) {
129
+ return listUntrackedFiles(cwd).length;
130
+ }
131
+
132
+ /**
133
+ * Split git's stdout into clean, forward-slashed, non-empty path lines.
134
+ *
135
+ * One helper for both probes because the two disagreeing about what a "line" is
136
+ * — a stray `\r` on Windows, a trailing empty line — would make the count and
137
+ * the listing describe different sets from the same output.
138
+ *
139
+ * @param {string} text
140
+ * @returns {string[]}
141
+ */
142
+ function splitLines(text) {
143
+ return String(text)
144
+ .split('\n')
145
+ .map((line) => line.trim().replace(/\\/g, '/'))
146
+ .filter(Boolean);
147
+ }
148
+
149
+ /**
150
+ * The brand-new UNTRACKED files, by NAME — the listing behind
151
+ * {@link countUntrackedFiles}, and the second half of {@link changedPathsAgainst}.
152
+ *
153
+ * The two callers need the same fact at different resolutions, and this is the
154
+ * ONLY place the probe is spelled: `git diff` never shows a file that was never
155
+ * `git add`-ed, regardless of ref or working-tree mode, so anything that answers
156
+ * "what changed" has to ask twice. A second copy of this command is exactly how
157
+ * the invisible-new-file bug would come back on one caller and not the other.
158
+ *
159
+ * `--exclude-standard` honours gitignore the way `git status` does, so build
160
+ * output never inflates either answer.
161
+ *
162
+ * Fail-open to `[]`: this probe is secondary to the diff and must never take the
163
+ * whole result down.
164
+ *
165
+ * @param {string} cwd
166
+ * @returns {string[]}
167
+ */
168
+ function listUntrackedFiles(cwd) {
129
169
  try {
130
- const out = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], { cwd, stdio: 'pipe' })
131
- .toString();
132
- return out.split('\n').filter((l) => l.trim()).length;
170
+ return splitLines(
171
+ execFileSync('git', ['ls-files', '--others', '--exclude-standard'], { cwd, stdio: 'pipe' }).toString()
172
+ );
133
173
  } catch {
134
- return 0;
174
+ return [];
135
175
  }
136
176
  }
137
177
 
@@ -172,3 +212,51 @@ export function getDiffStat(baseRef, cwd = process.cwd()) {
172
212
  return null;
173
213
  }
174
214
  }
215
+
216
+ /**
217
+ * Every path this working tree CHANGED against `baseRef` — tracked edits plus
218
+ * brand-new untracked files.
219
+ *
220
+ * WHY IT LIVES BESIDE getDiffStat, AND REUSES ITS UNTRACKED PROBE
221
+ *
222
+ * The two answer the same question at different resolutions ("how much changed"
223
+ * versus "what changed"), and both depend on the SAME fragile fact: `git diff`
224
+ * is tracked-edits-only, so a brand-new file is invisible to it no matter which
225
+ * ref is named. `getDiffStat`'s docblock has documented that for a while; a
226
+ * second copy of the probe here is precisely how the "new file is invisible"
227
+ * bug comes back on one of the two call sites and not the other. So
228
+ * `countUntrackedFiles`'s command is not retyped — the listing is.
229
+ *
230
+ * `git diff --name-only <baseRef>` against the WORKING TREE, deliberately not
231
+ * `baseRef...HEAD`: a gate-check in the middle of implement must see work that
232
+ * is not committed yet. Someone who has only staged the prompt change has still
233
+ * changed the prompt.
234
+ *
235
+ * FAIL-OPEN, and the caller must know what that means. `[]` comes back both for
236
+ * "nothing changed" and for "the ref did not resolve", exactly like
237
+ * `getDiffStat` returning null. The distinction that MATTERS to the caller is a
238
+ * different one — "the project declares no watched path" versus "it declares
239
+ * some and none were touched" — and that one is answered by the caller's own
240
+ * `declared` flag, never by the emptiness of this array.
241
+ *
242
+ * @param {string|null} baseRef - typically `detectDefaultBranch(cwd)`
243
+ * @param {string} [cwd]
244
+ * @returns {string[]} Repo-relative paths with forward slashes, deduplicated,
245
+ * sorted. Empty when `baseRef` is falsy, cwd is not a repo, or the diff fails.
246
+ */
247
+ export function changedPathsAgainst(baseRef, cwd = process.cwd()) {
248
+ if (!baseRef) return [];
249
+
250
+ let tracked;
251
+ try {
252
+ tracked = splitLines(
253
+ execFileSync('git', ['diff', '--name-only', baseRef], { cwd, stdio: 'pipe' }).toString()
254
+ );
255
+ } catch {
256
+ // A baseRef that does not resolve is not a reason to claim "nothing
257
+ // changed" loudly — the caller's policy is written to be inert on `[]`.
258
+ return [];
259
+ }
260
+
261
+ return [...new Set([...tracked, ...listUntrackedFiles(cwd)])].sort();
262
+ }
@@ -17,9 +17,10 @@
17
17
  */
18
18
  import { writeFileSync, existsSync } from 'node:fs';
19
19
  import { join } from 'node:path';
20
+ import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
20
21
 
21
22
  export const ALWAYS_IGNORED = [
22
- '.git/', '.morph/', 'artifacts/', 'worktrees/', '.worktrees/',
23
+ '.git/', '.morph/', 'artifacts/', ...WORKTREE_DIR_NAMES.map((d) => `${d}/`),
23
24
  'coverage/', 'graphify-out/', 'dist/', 'out/',
24
25
  ];
25
26
 
@@ -0,0 +1,193 @@
1
+ /**
2
+ * effects.js — o coletor de efeitos ÚNICO do caminho de instalação/atualização.
3
+ *
4
+ * A propriedade que faz o `--dry-run` ser honesto está numa frase:
5
+ *
6
+ * > **o caminho real também emite.**
7
+ *
8
+ * Em modo normal cada verbo EXECUTA e registra; em `dryRun`, só registra. O
9
+ * resumo impresso ao fim de uma execução real sai de `list()`, não de
10
+ * contadores paralelos. A consequência é o ponto: um ponto de emissão
11
+ * esquecido some das DUAS saídas ao mesmo tempo, e o resumo de uma execução
12
+ * real fica visivelmente incompleto — o defeito aparece sem sequer usar o
13
+ * `--dry-run`.
14
+ *
15
+ * **Não há segunda travessia.** Nenhum filtro, walker ou lista de payload é
16
+ * reimplementado aqui. Os filtros que decidem o que instalar
17
+ * (`shouldInstallSkill`, `agentEligible`, `shouldInstall`) já são puros e
18
+ * continuam sendo os únicos. O anti-exemplar tem nome e mora neste
19
+ * repositório: `checkAgentsDrift` funciona como preditor porque re-deriva o
20
+ * conjunto elegível por conta própria e porque os dois lados chamam o mesmo
21
+ * renderer puro. Estender essa forma para 9 payloads + hooks + merge de
22
+ * settings + gitignore + índice do git é reimplementar o instalador inteiro e
23
+ * depender de disciplina humana para mantê-los sincronizados.
24
+ *
25
+ * **Os verbos são SÍNCRONOS de propósito.** Metade dos instaladores que eles
26
+ * atravessam (`installSkills`, `installAgents`, `installRulesForStack`) é
27
+ * síncrona; um coletor assíncrono obrigaria a tornar `installRulesForStack`
28
+ * async e a mudar a assinatura que `setup-infra`, `init` e os testes já usam.
29
+ *
30
+ * **O registro é a IDENTIDADE do efeito, nunca o conteúdo.** `writeFile` grava
31
+ * `{ verb, target }`, não os bytes: `installClaudeHooks` carimba um
32
+ * `generated_at` a cada execução, e comparar conteúdo faria o canário de
33
+ * igualdade dry × real reprovar por causa do relógio.
34
+ */
35
+
36
+ import fs from 'fs-extra';
37
+ import { spawnSync } from 'node:child_process';
38
+ import { dirname } from 'node:path';
39
+
40
+ /**
41
+ * @typedef {Object} Effect
42
+ * @property {string} verb - ensureDir | remove | copyDir | copyFile | writeFile | writeJson | mergeJson | spawn | gitIndex
43
+ * @property {string} target - o caminho (ou comando) sobre o qual o efeito age
44
+ * @property {string} [from] - origem, para os verbos de cópia
45
+ * @property {string} [detail] - qualificador estável (argumentos do spawn, operação do git)
46
+ */
47
+
48
+ /**
49
+ * Cria um coletor de efeitos.
50
+ *
51
+ * @param {{ dryRun?: boolean }} [options]
52
+ * @returns {{
53
+ * dryRun: boolean,
54
+ * ensureDir: (path: string) => void,
55
+ * remove: (path: string) => void,
56
+ * copyDir: (from: string, to: string) => void,
57
+ * copyFile: (from: string, to: string) => void,
58
+ * writeFile: (path: string, content: string) => void,
59
+ * writeJson: (path: string, data: object) => void,
60
+ * mergeJson: (path: string, patch: object) => void,
61
+ * spawn: (cmd: string, args: string[], options?: object) => object|null,
62
+ * gitIndex: (op: string, path: string, options?: object) => boolean,
63
+ * list: () => Effect[],
64
+ * }}
65
+ */
66
+ export function createEffects({ dryRun = false } = {}) {
67
+ /** @type {Effect[]} */
68
+ const effects = [];
69
+
70
+ const record = (verb, target, extra = {}) => {
71
+ const effect = { verb, target };
72
+ if (extra.from !== undefined) effect.from = extra.from;
73
+ if (extra.detail !== undefined) effect.detail = extra.detail;
74
+ if (extra.args !== undefined) effect.args = extra.args;
75
+ effects.push(effect);
76
+ return effect;
77
+ };
78
+
79
+ return {
80
+ dryRun,
81
+
82
+ ensureDir(path) {
83
+ record('ensureDir', path);
84
+ if (!dryRun) fs.ensureDirSync(path);
85
+ },
86
+
87
+ remove(path) {
88
+ record('remove', path);
89
+ if (!dryRun) fs.removeSync(path);
90
+ },
91
+
92
+ copyDir(from, to, options = {}) {
93
+ record('copyDir', to, { from });
94
+ if (!dryRun) fs.copySync(from, to, { overwrite: true, errorOnExist: false, ...options });
95
+ },
96
+
97
+ copyFile(from, to) {
98
+ record('copyFile', to, { from });
99
+ if (!dryRun) {
100
+ fs.ensureDirSync(dirname(to));
101
+ fs.copySync(from, to, { overwrite: true });
102
+ }
103
+ },
104
+
105
+ writeFile(path, content) {
106
+ record('writeFile', path);
107
+ if (!dryRun) {
108
+ fs.ensureDirSync(dirname(path));
109
+ fs.writeFileSync(path, content, 'utf8');
110
+ }
111
+ },
112
+
113
+ writeJson(path, data) {
114
+ record('writeJson', path);
115
+ if (!dryRun) {
116
+ fs.ensureDirSync(dirname(path));
117
+ fs.writeJsonSync(path, data, { spaces: 2 });
118
+ }
119
+ },
120
+
121
+ /**
122
+ * Leitura + fusão rasa + escrita, como UM efeito.
123
+ *
124
+ * A leitura acontece nos dois modos (ler não muda nada); só a escrita é
125
+ * suprimida em dry. É assim que `~/.claude/settings.json` — um arquivo do
126
+ * usuário, FORA do projeto — é atualizado sem que o dry o toque.
127
+ */
128
+ mergeJson(path, patch) {
129
+ record('mergeJson', path);
130
+ if (dryRun) return;
131
+ let current = {};
132
+ try {
133
+ if (fs.existsSync(path)) current = fs.readJsonSync(path);
134
+ } catch {
135
+ current = {};
136
+ }
137
+ fs.ensureDirSync(dirname(path));
138
+ fs.writeJsonSync(path, { ...current, ...patch }, { spaces: 2 });
139
+ },
140
+
141
+ /**
142
+ * @param {string} cmd
143
+ * @param {string[]} args
144
+ * @param {{ cwd?: string, dryRunArgs?: string[] }} [options]
145
+ * `dryRunArgs` é a variante SÓ-LEITURA do mesmo comando (por exemplo
146
+ * `--check`). Em dry ela é executada no lugar; sem ela, nada roda.
147
+ * O REGISTRO é o mesmo nos dois modos: o efeito previsto é o comando
148
+ * real, não a sua sonda.
149
+ */
150
+ spawn(cmd, args = [], options = {}) {
151
+ // `args` viaja no registro ALÉM do `detail` legível. O `detail` é para
152
+ // humano; quebrá-lo por espaço para descobrir onde o spawn escreve é o
153
+ // bug que um caminho como `R:\Polymorphism Tech\...` produz em silêncio,
154
+ // e que o relatório mostrou pondo um efeito do PACOTE da CLI no grupo
155
+ // "no projeto".
156
+ record('spawn', cmd, { detail: args.join(' '), args: [...args] });
157
+ const { cwd, dryRunArgs } = options;
158
+ if (dryRun) {
159
+ if (!dryRunArgs) return null;
160
+ return spawnSync(cmd, dryRunArgs, { cwd, encoding: 'utf8' });
161
+ }
162
+ return spawnSync(cmd, args, { cwd, encoding: 'utf8' });
163
+ },
164
+
165
+ /**
166
+ * Mutação do índice do git (`git rm --cached`, etc.). A SONDA que decide
167
+ * se há o que fazer fica fora daqui de propósito: sondar é ler, e ler
168
+ * acontece nos dois modos.
169
+ */
170
+ gitIndex(op, path, options = {}) {
171
+ record('gitIndex', path, { detail: op });
172
+ if (dryRun) return true;
173
+ const result = spawnSync('git', [...op.split(' '), path], {
174
+ cwd: options.cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
175
+ });
176
+ return result.status === 0;
177
+ },
178
+
179
+ list() {
180
+ return effects.map((e) => ({ ...e }));
181
+ },
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Coletor no-op-de-registro para chamadores que não passam um: executa
187
+ * normalmente e registra numa lista descartada. Existe para que assinaturas
188
+ * novas de `installSkills`/`installAgents`/`installRulesForStack` continuem
189
+ * funcionando para `setup-infra` e `init` sem mudança de comportamento.
190
+ */
191
+ export function realEffects() {
192
+ return createEffects({ dryRun: false });
193
+ }
@@ -16,8 +16,8 @@
16
16
  */
17
17
 
18
18
  import { existsSync } from 'fs';
19
- import fs from 'fs-extra';
20
19
  import { compareVersions } from '../../utils/version-checker.js';
20
+ import { realEffects } from './effects.js';
21
21
 
22
22
  /**
23
23
  * Last CLI version whose `update`/`setup-infra` still wrote
@@ -62,7 +62,7 @@ export const LAST_VERSION_THAT_COPIED_TEMPLATES = '8.17.0';
62
62
  * version BEFORE this run (e.g. `checkProjectOutdated().current`)
63
63
  * @returns {'absent'|'removed-legacy'|'preserved-override'}
64
64
  */
65
- export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion) {
65
+ export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion, fx = realEffects()) {
66
66
  if (!existsSync(templatesDest)) return 'absent';
67
67
 
68
68
  const isLegacy = !priorProjectVersion
@@ -71,7 +71,7 @@ export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion)
71
71
  if (!isLegacy) return 'preserved-override';
72
72
 
73
73
  try {
74
- fs.removeSync(templatesDest);
74
+ fx.remove(templatesDest);
75
75
  return 'removed-legacy';
76
76
  } catch {
77
77
  // Couldn't remove — don't risk a half-broken state; leave it and let