@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
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Test Nodes — the PLURAL resolver for "which test suites does this project have?".
3
+ *
4
+ * WHY THIS MODULE EXISTS
5
+ *
6
+ * `test-runner.js` answers a singular question: ONE command, resolved by a
7
+ * four-step fallback. That shape is what defect F30 exploited — the resolver
8
+ * found a `.sln`, answered "dotnet test", and 3.082 vitest tests in the same
9
+ * repository never ran while the gate reported green. A project with two
10
+ * runners had no way to say so, and a verifier that cannot be told about a
11
+ * suite cannot fail because of it.
12
+ *
13
+ * So the unit of verification becomes a LIST of nodes. `project.tests[]` in
14
+ * `.morph/config/config.json` declares them; when it is absent, the four-step
15
+ * fallback still runs and is synthesized into exactly one node. Declared beats
16
+ * guessed (D3): deeper auto-detection would only trade one silent false
17
+ * negative for another, and the cost of the error here is a green gate over
18
+ * untested code.
19
+ *
20
+ * WHY VALIDATION IS FAIL-CLOSED HERE (D4)
21
+ *
22
+ * Every other config reader in this repo is fail-open — `JSON.parse` inside a
23
+ * `try/catch`, optional chaining, absent means "carry on". That is right for a
24
+ * hook: a guardrail that kills the session is worse than one that stays quiet.
25
+ * It is exactly WRONG for the verifier. A typo in `tests[]` treated fail-open
26
+ * falls through to the legacy resolver and reproduces F30 — green over a suite
27
+ * that never ran. So an invalid node is not ignored: it becomes a node with a
28
+ * pre-decided `status: 'fail'` and a reason naming the problem. It shows up in
29
+ * `runs[]` and it reddens the gate.
30
+ *
31
+ * @module test-nodes
32
+ */
33
+
34
+ import { existsSync, readFileSync } from 'fs';
35
+ import { join, resolve } from 'path';
36
+
37
+ import { resolveTestPlan, readMorphConfig, isInside } from './test-runner.js';
38
+ import { EVAL_RUNNER, evalBlockProblem } from './eval-node.js';
39
+ import { atomicWriteJson, CONFIG_FILE_NAME } from '../../core/state/state-manager.js';
40
+
41
+ /**
42
+ * The count extractors that exist (one per runner, see parseTestCount).
43
+ * `none` is the honest answer for a command whose summary nobody can read —
44
+ * it yields an UNKNOWN count, never a fabricated zero.
45
+ */
46
+ export const TEST_RUNNERS = ['dotnet', 'vitest', 'jest', 'node-test', 'playwright', 'none', EVAL_RUNNER];
47
+
48
+ /**
49
+ * A node id is what `--filter` matches and what keys `runs[]`. Lowercase and
50
+ * dash-only so it survives a shell argument and a JSON key without quoting.
51
+ */
52
+ export const TEST_NODE_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
53
+
54
+ /** Id given to the single node the fallback synthesizes — it has no name of its own. */
55
+ const FALLBACK_NODE_ID = 'default';
56
+
57
+ /**
58
+ * Parse the `--filter` argument into the token list `nodeMatchesFilter` reads.
59
+ *
60
+ * Lives here, beside TEST_NODE_ID_PATTERN, because the thing being matched is
61
+ * a node id: the spelling rules for the id and the spelling rules for what
62
+ * selects it must never drift into two modules.
63
+ *
64
+ * An argument that yields NO tokens (`--filter ""`, `--filter " , "`) returns
65
+ * an empty token list rather than null. That distinction is the point: null
66
+ * means "the user did not filter", `[]` means "the user filtered and selected
67
+ * nothing" — and selecting nothing is a usage error, not a full run.
68
+ *
69
+ * @param {string|null|undefined} raw
70
+ * @returns {{ raw: string, tokens: string[] }|null} null when no filter was given
71
+ */
72
+ export function parseTestFilter(raw) {
73
+ if (raw === undefined || raw === null || raw === false) return null;
74
+ const text = String(raw);
75
+ return {
76
+ raw: text,
77
+ tokens: text.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean),
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Does this node answer to the filter? Exact id, or the token as a substring
83
+ * of the id (`--filter dot` selects `dotnet`), case-insensitively.
84
+ *
85
+ * @param {TestNode} node
86
+ * @param {{ tokens: string[] }|null} filter - null (no filter) matches everything
87
+ * @returns {boolean}
88
+ */
89
+ export function nodeMatchesFilter(node, filter) {
90
+ if (!filter) return true;
91
+ const id = String(node?.id ?? '').toLowerCase();
92
+ return filter.tokens.some((token) => id === token || id.includes(token));
93
+ }
94
+
95
+ /** Reason kept WORD FOR WORD from the singular resolver: readers already print it. */
96
+ const NO_SUITE_REASON = 'No testCommand configured and no known project file detected';
97
+
98
+ /**
99
+ * One declared (or synthesized) test suite.
100
+ *
101
+ * @typedef {Object} TestNode
102
+ * @property {string} id - Unique within the list; what `--filter` matches.
103
+ * @property {string|null} command - Literal shell command; null only on a node already decided.
104
+ * @property {string} cwd - Relative to the project root; '.' by default.
105
+ * @property {string} runner - One of TEST_RUNNERS; inferred from `command` when not declared.
106
+ * @property {number|null} expectedCount - Recorded baseline. **null is UNKNOWN, never 0**:
107
+ * a 0 here would fabricate a count drop out of "no baseline recorded yet".
108
+ * @property {number|null} timeoutMs - Per-node ceiling. **null is UNKNOWN, never 0** — it
109
+ * means "derive the ceiling", not "no time allowed".
110
+ * @property {string[]} build - Test projects to compile first (dotnet only); `[]` is
111
+ * "nothing declared", which is a real answer, not an unknown.
112
+ * @property {boolean} e2e - Needs the stack up; feature scope only.
113
+ * @property {string|null} linuxImage - Container image for `verify --linux`; null defers to the run's.
114
+ * @property {number} projectCount - How many test projects `command` chains — the input to
115
+ * the derived timeout. Always 1 for a declared node.
116
+ * @property {'fail'|'skip'|null} status - Pre-decided verdict. **Non-null means DO NOT
117
+ * EXECUTE**: the node is invalid config, or an explicit declaration of "no tests".
118
+ * null is the normal case: no verdict yet, the run decides.
119
+ * @property {string|null} reason - Why, when `status` is set.
120
+ */
121
+
122
+ /**
123
+ * Infer which summary extractor can read a command's output.
124
+ *
125
+ * Deliberately conservative: `npm test` can front ANY runner, so it infers
126
+ * `none` (unknown count) rather than guessing. An extractor applied to the
127
+ * wrong output produces `null` anyway — but a WRONG guess that happens to
128
+ * match would invent a number, and numbers here fail gates.
129
+ *
130
+ * @param {string|null|undefined} command
131
+ * @returns {string} one of TEST_RUNNERS
132
+ */
133
+ export function inferRunner(command) {
134
+ // `evals` is never inferred, on purpose: it is a DECLARATION, not a shape a
135
+ // command line can betray. An eval command is literally `dotnet test`, so any
136
+ // sniffing here would classify every dotnet suite as an eval — and, worse, the
137
+ // reverse mistake (an eval declared and read as plain dotnet) would silently
138
+ // drop the cache pre-flight.
139
+ if (typeof command !== 'string' || !command.trim()) return 'none';
140
+ if (/\bdotnet\s+test\b/.test(command)) return 'dotnet';
141
+ if (/\bvitest\b/.test(command)) return 'vitest';
142
+ if (/\bplaywright\b/.test(command)) return 'playwright';
143
+ if (/\bjest\b/.test(command)) return 'jest';
144
+ if (/\bnode\b[^&|]*\s--test\b/.test(command) || /run-tests\.mjs\b/.test(command)) return 'node-test';
145
+ return 'none';
146
+ }
147
+
148
+ /**
149
+ * Build a fully-shaped node. Every field is present on every node so that no
150
+ * reader has to distinguish "absent" from "unknown" by optional chaining.
151
+ */
152
+ function makeNode(partial) {
153
+ return {
154
+ id: partial.id,
155
+ command: partial.command ?? null,
156
+ cwd: partial.cwd ?? '.',
157
+ runner: partial.runner ?? inferRunner(partial.command),
158
+ expectedCount: partial.expectedCount ?? null,
159
+ timeoutMs: partial.timeoutMs ?? null,
160
+ build: partial.build ?? [],
161
+ e2e: partial.e2e ?? false,
162
+ // Per-node image for `verify --linux`. **null is UNKNOWN, never ''** — it
163
+ // means "use the run's image", not "use no image".
164
+ linuxImage: partial.linuxImage ?? null,
165
+ projectCount: partial.projectCount ?? 1,
166
+ // The eval block travels on the node so `verify-runner` never has to go back
167
+ // to config.json to learn where the cache lives. **null is "this is not an
168
+ // eval node"** — distinct from an eval node with an empty block, which
169
+ // `validateTestNodes` already refused.
170
+ evals: partial.evals ?? null,
171
+ status: partial.status ?? null,
172
+ reason: partial.reason ?? null,
173
+ };
174
+ }
175
+
176
+ /** A node that will not run, and says why in the vocabulary of §2.2 of the spec. */
177
+ function invalidNode(id, problem, raw = {}) {
178
+ return makeNode({
179
+ id,
180
+ command: typeof raw.command === 'string' ? raw.command : null,
181
+ cwd: typeof raw.cwd === 'string' ? raw.cwd : '.',
182
+ status: 'fail',
183
+ reason: `invalid-test-node: ${id}: ${problem}`,
184
+ });
185
+ }
186
+
187
+ function isPlainObject(value) {
188
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
189
+ }
190
+
191
+ function isNonNegativeInteger(value) {
192
+ return Number.isInteger(value) && value >= 0;
193
+ }
194
+
195
+ /**
196
+ * First problem with one raw `tests[]` entry, or null when it is well formed.
197
+ * First problem only: a list of complaints about a single node buries the one
198
+ * that has to be fixed first.
199
+ *
200
+ * @param {*} raw
201
+ * @param {string|null} projectPath - When given, `cwd` is also checked against the disk.
202
+ * @returns {string|null}
203
+ */
204
+ function firstProblem(raw, projectPath) {
205
+ if (!isPlainObject(raw)) return 'must be an object';
206
+
207
+ if (typeof raw.id !== 'string' || !TEST_NODE_ID_PATTERN.test(raw.id)) {
208
+ return `id must be a string matching ${TEST_NODE_ID_PATTERN.source}`;
209
+ }
210
+ if (typeof raw.command !== 'string' || !raw.command.trim()) {
211
+ return 'command must be a non-empty string';
212
+ }
213
+ if (raw.cwd !== undefined && (typeof raw.cwd !== 'string' || !raw.cwd.trim())) {
214
+ return 'cwd must be a non-empty string';
215
+ }
216
+ if (raw.runner !== undefined && !TEST_RUNNERS.includes(raw.runner)) {
217
+ return `runner must be one of ${TEST_RUNNERS.join(' | ')}`;
218
+ }
219
+ if (raw.expectedCount !== undefined && !isNonNegativeInteger(raw.expectedCount)) {
220
+ return 'expectedCount must be an integer >= 0';
221
+ }
222
+ if (raw.timeoutMs !== undefined && !(Number.isInteger(raw.timeoutMs) && raw.timeoutMs > 0)) {
223
+ return 'timeoutMs must be an integer > 0';
224
+ }
225
+ if (raw.build !== undefined && (!Array.isArray(raw.build) || raw.build.some((b) => typeof b !== 'string'))) {
226
+ return 'build must be an array of strings';
227
+ }
228
+ // `build[]` is compiled by the dotnet prebuild and by nothing else. Declared
229
+ // on a vitest node it would be silently ignored — and a declaration the
230
+ // verifier ignores is the same fail-open shape as the typo in `tests[]`:
231
+ // the author believes something is being compiled that never is.
232
+ if (Array.isArray(raw.build) && raw.build.length > 0
233
+ && (raw.runner ?? inferRunner(raw.command)) !== 'dotnet') {
234
+ return 'build applies only to runner "dotnet"';
235
+ }
236
+ if (raw.e2e !== undefined && typeof raw.e2e !== 'boolean') {
237
+ return 'e2e must be a boolean';
238
+ }
239
+ if (raw.linuxImage !== undefined && (typeof raw.linuxImage !== 'string' || !raw.linuxImage.trim())) {
240
+ return 'linuxImage must be a non-empty string';
241
+ }
242
+ // `runner: "evals"` REQUIRES its block; the block on any other runner is a
243
+ // declaration the verifier would silently ignore — the same fail-open shape as
244
+ // a typo in `tests[]`, where the author believes something is being checked
245
+ // that never is.
246
+ if ((raw.runner ?? inferRunner(raw.command)) === EVAL_RUNNER) {
247
+ const problem = evalBlockProblem(raw, projectPath);
248
+ if (problem) return problem;
249
+ } else if (raw.evals !== undefined) {
250
+ return `evals applies only to runner "${EVAL_RUNNER}"`;
251
+ }
252
+
253
+ // The cwd checks come last because they are the expensive ones, and because
254
+ // a node that is malformed in shape has a better error to report first.
255
+ const cwd = typeof raw.cwd === 'string' ? raw.cwd.trim() : '.';
256
+ const root = resolve(projectPath ?? '.');
257
+ const abs = resolve(root, cwd);
258
+ if (!isInside(abs, root)) {
259
+ // A cwd that leaves the root addresses ANOTHER tree — the same damage the
260
+ // worktree exclusion in collectDotnetTestProjects exists to prevent.
261
+ return `cwd "${cwd}" resolves outside the project root`;
262
+ }
263
+ if (projectPath && !existsSync(abs)) {
264
+ // P5: named failure, never a skip. A wrong cwd is a plan error, and a skip
265
+ // would hide it behind a green gate.
266
+ return `cwd "${cwd}" does not exist`;
267
+ }
268
+
269
+ return null;
270
+ }
271
+
272
+ /**
273
+ * Validate `config.project.tests[]` IN USE — this is called by
274
+ * resolveTestNodes(), not merely declared in a schema. A schema that nothing
275
+ * calls is decoration; this repo already paid for fifteen guardrails that
276
+ * could not detect anything (v8.9/v8.11).
277
+ *
278
+ * Fail-closed by design (D4): a bad entry becomes a node with
279
+ * `status: 'fail'`, never a node that disappears.
280
+ *
281
+ * @param {object|null} config - Parsed `.morph/config/config.json`
282
+ * @param {string|null} [projectPath] - Project root; enables the on-disk `cwd` check
283
+ * @returns {TestNode[]} One node per declared entry, in declaration order
284
+ */
285
+ export function validateTestNodes(config, projectPath = null) {
286
+ const raw = config?.project?.tests;
287
+
288
+ if (!Array.isArray(raw)) {
289
+ return [invalidNode('project.tests', 'must be an array of test nodes')];
290
+ }
291
+
292
+ // Count ids BEFORE judging: two nodes sharing an id make `--filter` and
293
+ // `runs[]` ambiguous, and picking a winner would silence one declared suite.
294
+ // Both sides fail — the ambiguity is the defect, not either entry.
295
+ const seen = new Map();
296
+ for (const entry of raw) {
297
+ if (isPlainObject(entry) && typeof entry.id === 'string') {
298
+ seen.set(entry.id, (seen.get(entry.id) ?? 0) + 1);
299
+ }
300
+ }
301
+
302
+ return raw.map((entry, index) => {
303
+ const id = isPlainObject(entry) && typeof entry.id === 'string' && entry.id
304
+ ? entry.id
305
+ : `tests[${index}]`;
306
+
307
+ const problem = firstProblem(entry, projectPath);
308
+ if (problem) return invalidNode(id, problem, isPlainObject(entry) ? entry : {});
309
+
310
+ if (seen.get(entry.id) > 1) {
311
+ return invalidNode(id, `duplicate id "${entry.id}" — ids must be unique within tests[]`, entry);
312
+ }
313
+
314
+ return makeNode({
315
+ id: entry.id,
316
+ command: entry.command,
317
+ cwd: typeof entry.cwd === 'string' ? entry.cwd.trim() : '.',
318
+ runner: entry.runner ?? inferRunner(entry.command),
319
+ expectedCount: entry.expectedCount ?? null,
320
+ timeoutMs: entry.timeoutMs ?? null,
321
+ build: entry.build ?? [],
322
+ e2e: entry.e2e ?? false,
323
+ linuxImage: entry.linuxImage ?? null,
324
+ evals: entry.evals ?? null,
325
+ projectCount: 1,
326
+ });
327
+ });
328
+ }
329
+
330
+ /**
331
+ * Resolve every test suite of a project, in the order the verifier must run them.
332
+ *
333
+ * Precedence — the whole point is that the last three steps still work with NO
334
+ * config at all, because this repository (the framework source) has none and
335
+ * every project installed before this feature has none either:
336
+ *
337
+ * 1. `project.tests[]` declared → one node per entry, source 'config-tests'
338
+ * 2. `project.testCommand` declared → ONE synthesized node, source 'config'
339
+ * 3. the four-step fallback of → ONE synthesized node, source
340
+ * resolveTestPlan() (root .sln, 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'
341
+ * *Tests.csproj scan, npm test)
342
+ *
343
+ * Steps 2 and 3 are `resolveTestPlan()` itself — CALLED, never re-implemented.
344
+ * That matters twice over: it is the only place that knows the fallback order,
345
+ * and it is the only place that excludes linked worktrees from the
346
+ * `*Tests.csproj` scan (a worktree is another feature's checkout; scanning it
347
+ * doubles the clock and reports on code outside the tree being judged).
348
+ *
349
+ * The list is never empty: "nothing to run" is expressed as ONE node already
350
+ * decided as `skip`, so the reason travels to the report instead of vanishing.
351
+ *
352
+ * @param {string} [projectPath] - Project root directory (defaults to '.')
353
+ * @returns {{ nodes: TestNode[], source: string }}
354
+ */
355
+ export function resolveTestNodes(projectPath = '.') {
356
+ const config = readMorphConfig(projectPath);
357
+
358
+ // `tests` present in ANY shape is a declaration of intent. Even a malformed
359
+ // one: falling back from a typo is how F30 comes back.
360
+ if (config?.project?.tests !== undefined) {
361
+ const nodes = validateTestNodes(config, projectPath);
362
+ if (Array.isArray(config.project.tests) && config.project.tests.length === 0) {
363
+ // `[]` is an explicit "this project declares no test suites" — distinct
364
+ // from an absent key, which means "guess for me".
365
+ return {
366
+ nodes: [makeNode({ id: 'none', status: 'skip', reason: 'no test nodes declared' })],
367
+ source: 'config-tests',
368
+ };
369
+ }
370
+ return { nodes, source: 'config-tests' };
371
+ }
372
+
373
+ const plan = resolveTestPlan(projectPath);
374
+
375
+ if (!plan.command) {
376
+ return {
377
+ nodes: [makeNode({ id: FALLBACK_NODE_ID, status: 'skip', reason: NO_SUITE_REASON })],
378
+ source: plan.source,
379
+ };
380
+ }
381
+
382
+ return {
383
+ nodes: [makeNode({
384
+ id: FALLBACK_NODE_ID,
385
+ command: plan.command,
386
+ cwd: '.',
387
+ projectCount: plan.projectCount,
388
+ })],
389
+ source: plan.source,
390
+ };
391
+ }
392
+
393
+ /**
394
+ * Path of the config the baseline lives in. One spelling, so the writer and
395
+ * `readMorphConfig()` can never disagree about which file is meant — and that
396
+ * is now literally true: both derive from `CONFIG_FILE_NAME`
397
+ * (`core/state/state-manager.js`), the same constant `state-manager.js` itself
398
+ * reads the project identity through. It used to be a promise made by a JSDoc
399
+ * over two hand-typed copies of the same string.
400
+ */
401
+ export function morphConfigPath(projectPath = '.') {
402
+ return join(projectPath, CONFIG_FILE_NAME);
403
+ }
404
+
405
+ /**
406
+ * Write measured `expectedCount` values back into `project.tests[]`.
407
+ *
408
+ * WHY THIS IS A FUNCTION AND NOT AN EDIT (D8)
409
+ *
410
+ * `MORPH.md` says a baseline is a COMMAND, never a number — and this feature
411
+ * stores a number. The rule survives because nobody is allowed to type it: the
412
+ * only writer is `verify --record-baseline`, and it writes what a real, green
413
+ * run measured. A hand-typed number describes what someone believed; the
414
+ * divergence between belief and the suite is exactly the failure the rule was
415
+ * written after ("mandate afirmando 2366 quando já eram 2375").
416
+ *
417
+ * The write is a POINTED mutation: read → set one field per declared node →
418
+ * atomic `tmp.{pid}` + rename via `atomicWriteJson` (the same door
419
+ * `state-manager.js` uses, not a second copy of it). Every other key of
420
+ * `config.json` survives, because this command owns one field and nothing else.
421
+ *
422
+ * It refuses rather than improvises:
423
+ * - a measurement whose `count` is not an integer (**null is UNKNOWN, never
424
+ * 0** — a written 0 would fabricate a drop against every later run);
425
+ * - a measured id that is not declared (appending a node would be inventing
426
+ * the declaration the command came to update);
427
+ * - a declared node with no measurement (a baseline recorded for part of a
428
+ * fleet says nothing true about the rest of it).
429
+ *
430
+ * @param {string} projectPath - Project root
431
+ * @param {Array<{id: string, count: number|null}>} measurements - One per node that ran
432
+ * @returns {{ recorded: boolean, reason: string|null, updates: Array<{id: string, from: number|null, to: number}>, path: string }}
433
+ */
434
+ export function recordTestBaselines(projectPath, measurements) {
435
+ const path = morphConfigPath(projectPath);
436
+ const refuse = (reason) => ({ recorded: false, reason, updates: [], path });
437
+
438
+ if (!existsSync(path)) return refuse('não existe .morph/config/config.json — não há onde gravar');
439
+
440
+ let config;
441
+ try {
442
+ config = JSON.parse(readFileSync(path, 'utf8'));
443
+ } catch (err) {
444
+ return refuse(`config.json ilegível (${err.message})`);
445
+ }
446
+
447
+ const declared = config?.project?.tests;
448
+ if (!Array.isArray(declared)) {
449
+ return refuse('config.json não declara project.tests[] — não há entrada onde gravar a baseline');
450
+ }
451
+
452
+ const unreadable = measurements.filter((m) => !Number.isInteger(m.count)).map((m) => m.id);
453
+ if (unreadable.length > 0) {
454
+ return refuse(`contagem desconhecida em: ${unreadable.join(', ')} — desconhecido nunca vira 0`);
455
+ }
456
+
457
+ const byId = new Map(measurements.map((m) => [m.id, m.count]));
458
+ const undeclared = [...byId.keys()].filter(
459
+ (id) => !declared.some((entry) => entry && entry.id === id)
460
+ );
461
+ if (undeclared.length > 0) {
462
+ return refuse(`nó medido não declarado em project.tests[]: ${undeclared.join(', ')}`);
463
+ }
464
+
465
+ const unmeasured = declared
466
+ .map((entry) => entry?.id)
467
+ .filter((id) => typeof id === 'string' && !byId.has(id));
468
+ if (unmeasured.length > 0) {
469
+ return refuse(`nó declarado sem medição: ${unmeasured.join(', ')} — baseline parcial não é baseline`);
470
+ }
471
+
472
+ const updates = [];
473
+ for (const entry of declared) {
474
+ if (!entry || !byId.has(entry.id)) continue;
475
+ const to = byId.get(entry.id);
476
+ updates.push({
477
+ id: entry.id,
478
+ from: Number.isInteger(entry.expectedCount) ? entry.expectedCount : null,
479
+ to,
480
+ });
481
+ entry.expectedCount = to;
482
+ }
483
+
484
+ atomicWriteJson(path, config);
485
+ return { recorded: true, reason: null, updates, path };
486
+ }