@polymorphism-tech/morph-spec 8.34.2 → 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 (325) hide show
  1. package/CLAUDE.md +20 -3
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +18 -4
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +5 -4
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/MORPH.md +16 -17
  9. package/framework/agents.json +197 -44
  10. package/framework/ai-pin.json +22 -0
  11. package/framework/commands/morph-media.md +45 -0
  12. package/framework/docs/CLI.md +234 -9
  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/claude-code/core/protect-spec-files.js +41 -4
  25. package/framework/hooks/claude-code/statusline.sh +11 -11
  26. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  27. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  28. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  29. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  30. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  31. package/framework/hooks/shared/e2e-check.js +135 -22
  32. package/framework/hooks/shared/stdin-reader.js +26 -26
  33. package/framework/rules/ai-agents-standards.md +14 -2
  34. package/framework/schemas/ai-pin.schema.json +97 -0
  35. package/framework/schemas/config.schema.json +107 -0
  36. package/framework/schemas/feature.schema.json +23 -4
  37. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  38. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  39. package/framework/skills/core/morph-apply/SKILL.md +26 -5
  40. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  41. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  42. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  43. package/framework/skills/core/morph-implement/SKILL.md +18 -2
  44. package/framework/skills/core/morph-media/SKILL.md +215 -0
  45. package/framework/skills/core/morph-plan/SKILL.md +33 -2
  46. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  47. package/framework/skills/core/morph-review/SKILL.md +166 -9
  48. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  49. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  50. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  51. package/framework/standards/PROVENANCE.json +81 -3
  52. package/framework/standards/STANDARDS.json +1483 -839
  53. package/framework/standards/ai-agents/agent-archetypes.md +160 -0
  54. package/framework/standards/ai-agents/agent-session.md +164 -68
  55. package/framework/standards/ai-agents/agent-spec.md +196 -0
  56. package/framework/standards/ai-agents/batching.md +61 -37
  57. package/framework/standards/ai-agents/context-providers.md +97 -24
  58. package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
  59. package/framework/standards/ai-agents/cost-and-budget.md +256 -0
  60. package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
  61. package/framework/standards/ai-agents/evals-with-cache.md +289 -0
  62. package/framework/standards/ai-agents/guardrails.md +222 -0
  63. package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
  64. package/framework/standards/ai-agents/mcp-server.md +119 -18
  65. package/framework/standards/ai-agents/mcp-tools.md +85 -32
  66. package/framework/standards/ai-agents/media-pipeline.md +352 -0
  67. package/framework/standards/ai-agents/media-video.md +267 -0
  68. package/framework/standards/ai-agents/middleware-patterns.md +39 -5
  69. package/framework/standards/ai-agents/modalities-image-gen.md +354 -105
  70. package/framework/standards/ai-agents/modalities-speech-to-text.md +8 -2
  71. package/framework/standards/ai-agents/modalities-text-to-speech.md +10 -2
  72. package/framework/standards/ai-agents/modalities-vision.md +6 -2
  73. package/framework/standards/ai-agents/multi-agent-patterns.md +61 -5
  74. package/framework/standards/ai-agents/observability-patterns.md +227 -17
  75. package/framework/standards/ai-agents/production.md +38 -9
  76. package/framework/standards/ai-agents/prompt-sources.md +389 -0
  77. package/framework/standards/ai-agents/providers/model-registry.md +174 -39
  78. package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
  79. package/framework/standards/ai-agents/service-tier-flex.md +27 -19
  80. package/framework/standards/ai-agents/setup.md +166 -50
  81. package/framework/standards/ai-agents/structured-output.md +34 -3
  82. package/framework/standards/ai-agents/sweet-spot.md +23 -3
  83. package/framework/standards/ai-agents/testing-ai.md +236 -0
  84. package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
  85. package/framework/standards/ai-agents/workflows.md +27 -7
  86. package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
  87. package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
  88. package/framework/standards/infrastructure/docker/local-compose-isolation.md +3 -1
  89. package/framework/templates/.idea/morph-templates.xml +92 -0
  90. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  91. package/framework/templates/IDE-SNIPPETS.md +262 -262
  92. package/framework/templates/REGISTRY.json +13 -0
  93. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  94. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  95. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  96. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  97. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +1 -1
  98. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  99. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  100. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  101. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  102. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  103. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  104. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  105. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  106. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  107. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +1 -1
  108. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  109. package/framework/templates/code/typescript/contracts.ts +167 -167
  110. package/framework/templates/docs/clarifications.md +253 -253
  111. package/framework/templates/docs/schema-analysis.md +119 -119
  112. package/framework/templates/docs/ui-components.md +52 -52
  113. package/framework/templates/docs/ui-design-system.md +76 -76
  114. package/framework/templates/docs/ui-flows.md +167 -167
  115. package/framework/templates/docs/ui-mockups.md +98 -98
  116. package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
  117. package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
  118. package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
  119. package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
  120. package/framework/templates/dotnet/ai-kit/README.md +128 -0
  121. package/framework/templates/dotnet/ai-kit/global.json +9 -0
  122. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
  123. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
  124. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
  125. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
  126. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
  127. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
  128. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
  129. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
  130. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
  131. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
  132. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
  133. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
  134. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
  135. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
  136. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
  137. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
  138. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
  139. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
  140. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
  141. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
  142. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
  143. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
  144. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
  145. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
  146. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
  147. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
  148. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
  149. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
  150. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
  151. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
  152. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
  153. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
  154. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
  155. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
  156. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
  157. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
  158. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
  159. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
  160. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
  161. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
  162. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
  163. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
  164. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
  165. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
  166. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
  167. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
  168. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
  169. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
  170. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
  171. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
  172. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
  173. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
  174. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
  175. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
  176. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
  177. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
  178. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
  179. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
  180. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
  181. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
  182. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
  183. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
  184. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
  185. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
  186. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
  187. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
  188. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
  189. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
  190. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
  191. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
  192. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
  193. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
  194. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
  195. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
  196. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
  197. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
  198. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
  199. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
  200. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
  201. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
  202. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
  203. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
  204. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
  205. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
  206. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
  207. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
  208. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
  209. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
  210. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
  211. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
  212. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
  213. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
  214. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
  215. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
  216. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
  217. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
  218. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
  219. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
  220. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
  221. package/framework/templates/feature/decisions.md +122 -122
  222. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  223. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  224. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  225. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  226. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  227. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  228. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  229. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  230. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  231. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  232. package/framework/templates/infrastructure/github/README.md +56 -56
  233. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  234. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  235. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  236. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  237. package/framework/templates/integrations/asaas-client.cs +387 -387
  238. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  239. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  240. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  241. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  242. package/framework/templates/project/validate.js +120 -120
  243. package/framework/templates/state.template.json +17 -17
  244. package/framework/templates/ui/design-system.css +226 -226
  245. package/package.json +3 -3
  246. package/src/commands/dashboard/dashboard.js +53 -53
  247. package/src/commands/e2e/e2e.js +15 -2
  248. package/src/commands/index.js +13 -13
  249. package/src/commands/project/checkpoint.js +209 -209
  250. package/src/commands/project/doctor.js +140 -5
  251. package/src/commands/project/update.js +469 -153
  252. package/src/commands/project/worktree.js +8 -1
  253. package/src/commands/state/advance.js +24 -2
  254. package/src/commands/state/approve.js +33 -1
  255. package/src/commands/state/create.js +72 -11
  256. package/src/commands/state/finish.js +333 -21
  257. package/src/commands/state/gate-check.js +190 -6
  258. package/src/commands/state/score.js +36 -1
  259. package/src/commands/validation/verify.js +134 -25
  260. package/src/core/state/phase-state-machine.js +197 -197
  261. package/src/core/state/state-manager.js +9 -1
  262. package/src/core/templates/template-renderer.js +477 -477
  263. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  264. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  265. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  266. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  267. package/src/dashboard/app/main.jsx +13 -13
  268. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  269. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  270. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  271. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  272. package/src/dashboard/app/pages/Standards.jsx +332 -332
  273. package/src/dashboard/app/styles/index.css +47 -47
  274. package/src/dashboard/index.html +12 -12
  275. package/src/dashboard/package.json +24 -24
  276. package/src/dashboard/scanner/phases-scanner.js +15 -15
  277. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  278. package/src/dashboard/scanner/rules-scanner.js +37 -37
  279. package/src/dashboard/scanner/skills-scanner.js +43 -43
  280. package/src/dashboard/scanner/squads-scanner.js +13 -13
  281. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  282. package/src/dashboard/vite.config.js +24 -24
  283. package/src/lib/ai/ai-checks.js +182 -0
  284. package/src/lib/ai/ai-project-scan.js +347 -0
  285. package/src/lib/ai/code-audit.js +181 -0
  286. package/src/lib/ai/comment-stripper.js +184 -0
  287. package/src/lib/ai/pin-drift.js +138 -0
  288. package/src/lib/ai/registry-audit.js +285 -0
  289. package/src/lib/detectors/design-system-detector.js +188 -188
  290. package/src/lib/detectors/index.js +217 -217
  291. package/src/lib/e2e/compose-env.js +50 -4
  292. package/src/lib/evaluator-report.js +269 -15
  293. package/src/lib/generators/design-system-generator.js +508 -508
  294. package/src/lib/generators/settings-generator.js +209 -209
  295. package/src/lib/git/default-branch.js +92 -4
  296. package/src/lib/hooks/hook-executor.js +167 -167
  297. package/src/lib/installers/effects.js +193 -0
  298. package/src/lib/installers/mcp-installer.js +719 -719
  299. package/src/lib/installers/templates-reconcile.js +3 -3
  300. package/src/lib/risk-detector.js +284 -0
  301. package/src/lib/standards/verified-against.js +276 -0
  302. package/src/lib/tasks/eval-node.js +485 -0
  303. package/src/lib/tasks/linux-runner.js +297 -0
  304. package/src/lib/tasks/task-classifier.js +30 -30
  305. package/src/lib/tasks/task-parser.js +209 -209
  306. package/src/lib/tasks/test-nodes.js +486 -0
  307. package/src/lib/tasks/test-runner.js +410 -33
  308. package/src/lib/validators/checkpoint-persistence.js +39 -39
  309. package/src/lib/validators/css/css-validator.js +340 -340
  310. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  311. package/src/lib/validators/shared/issue-counter.js +14 -14
  312. package/src/lib/validators/verify-runner.js +583 -28
  313. package/src/lib/worktree/docker-audit.js +287 -0
  314. package/src/lib/worktree/env-allocator.js +24 -0
  315. package/src/lib/worktree/worktree-audit.js +30 -1
  316. package/src/scripts/setup-infra.js +24 -11
  317. package/src/utils/agent-id-mapper.js +11 -11
  318. package/src/utils/agents-installer.js +18 -8
  319. package/src/utils/claude-md-injector.js +10 -5
  320. package/src/utils/claude-settings-manager.js +300 -300
  321. package/src/utils/file-copier.js +11 -4
  322. package/src/utils/hooks-installer.js +29 -26
  323. package/src/utils/skills-installer.js +36 -29
  324. package/src/utils/version-checker.js +68 -3
  325. package/framework/standards/ai-agents/prompts-in-db.md +0 -214
@@ -1,11 +1,18 @@
1
1
  /**
2
- * Test Runner — Resolves and executes the project's test suite.
2
+ * Test Runner — the FALLBACK resolver and the executor of a single test node.
3
3
  *
4
- * Resolution order for test command:
4
+ * Scope note: a project has a LIST of test suites, and that list is resolved by
5
+ * `test-nodes.js` (`resolveTestNodes()`), which calls `resolveTestPlan()` below
6
+ * as its last fallback. What lives here is (a) the four-step guess for a
7
+ * project that declared nothing, and (b) `runTestSuite()`, which runs ONE node.
8
+ * The plural/singular split matters: a runner that resolved its own single
9
+ * command is what let a whole second suite sit outside the gate (defect F30).
10
+ *
11
+ * Resolution order for the fallback test command:
5
12
  * 1. .morph/config/config.json → project.testCommand
6
13
  * 2. .sln or .csproj present at project root → "dotnet test"
7
14
  * 3. Nested *Tests.csproj anywhere in the tree (monorepo without a root
8
- * .sln) → "dotnet test <proj> --nologo -v q" for each match
15
+ * .sln) → "dotnet test <proj> -v q" for each match
9
16
  * 4. package.json present AND has scripts.test → "npm test"
10
17
  * 5. null (no test suite detected)
11
18
  *
@@ -26,6 +33,8 @@ import { join, resolve, relative, isAbsolute } from 'path';
26
33
  import { globSync } from 'glob';
27
34
 
28
35
  import { WORKTREES_DIR, LEGACY_WORKTREES_DIR } from '../../commands/project/worktree.js';
36
+ import { getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
37
+ import { CONFIG_FILE_NAME } from '../../core/state/state-manager.js';
29
38
 
30
39
  /**
31
40
  * Timeout budget granted to ONE test project. The chained `a && b` command is
@@ -44,7 +53,7 @@ export const DEFAULT_TEST_TIMEOUT_PER_PROJECT = 120_000;
44
53
  */
45
54
  export function resolveTestPlan(projectPath = '.') {
46
55
  // 1. Read from .morph/config/config.json
47
- const config = readConfig(projectPath);
56
+ const config = readMorphConfig(projectPath);
48
57
  const configured = config?.project?.testCommand;
49
58
  if (configured && typeof configured === 'string') {
50
59
  return { command: configured, projectCount: 1, source: 'config' };
@@ -70,7 +79,24 @@ export function resolveTestPlan(projectPath = '.') {
70
79
  const testProjects = collectDotnetTestProjects(projectPath);
71
80
  if (testProjects.length > 0) {
72
81
  return {
73
- command: testProjects.map((p) => `dotnet test "${p}" --nologo -v q`).join(' && '),
82
+ // NO `--nologo`, and that omission is load-bearing. `--nologo` is a
83
+ // VSTest-MODE option of `dotnet test`. A project that opts into
84
+ // Microsoft.Testing.Platform (`"test": {"runner":
85
+ // "Microsoft.Testing.Platform"}` in global.json — what xunit.v3 4.x on
86
+ // .NET 10 requires, and what the ai-agents standards prescribe) runs
87
+ // `dotnet test` in MTP MODE, where `--nologo` is unknown and is forwarded
88
+ // verbatim to the test application. The application answers
89
+ // `error: unknown option: --nologo` and the session dies with MTP exit
90
+ // code 5 (InvalidCommandLine) reporting ZERO tests — a red `tests` node
91
+ // that looks exactly like a broken suite while every test passes.
92
+ // Measured on framework/templates/dotnet/ai-kit (2026-09-08): with the
93
+ // flag, "Zero testes executados" / exit 5; without it, 115 passed / exit 0.
94
+ // The rest of the command is mode-agnostic on purpose: the POSITIONAL
95
+ // project path and `-v q` are accepted by both modes, so this command
96
+ // stays correct whether the target project is VSTest or MTP. Do not
97
+ // "modernize" the path to `--project`: that spelling exists only in MTP
98
+ // mode and would break every VSTest project instead.
99
+ command: testProjects.map((p) => `dotnet test "${p}" -v q`).join(' && '),
74
100
  projectCount: testProjects.length,
75
101
  source: 'dotnet-scan',
76
102
  };
@@ -93,16 +119,6 @@ export function resolveTestPlan(projectPath = '.') {
93
119
  return { command: null, projectCount: 0, source: 'none' };
94
120
  }
95
121
 
96
- /**
97
- * Resolve the test command for a project.
98
- *
99
- * @param {string} [projectPath] - Project root directory (defaults to '.')
100
- * @returns {string | null} The test command to run, or null if none found
101
- */
102
- export function resolveTestCommand(projectPath = '.') {
103
- return resolveTestPlan(projectPath).command;
104
- }
105
-
106
122
  /**
107
123
  * Find the *Tests.csproj files that belong to THIS tree — never to a linked
108
124
  * worktree of the same repo.
@@ -192,14 +208,34 @@ function listLinkedWorktreePaths(projectPath) {
192
208
  .filter((p) => p && p !== self);
193
209
  }
194
210
 
195
- /** True when `child` is `parent` or lives under it. */
196
- function isInside(child, parent) {
211
+ /**
212
+ * True when `child` is `parent` or lives under it.
213
+ *
214
+ * Exported because test-nodes.js asks the SAME question of a declared `cwd`
215
+ * ("does this node point outside the tree under verification?"). One spelling
216
+ * of containment, one place to fix it.
217
+ *
218
+ * @param {string} child
219
+ * @param {string} parent
220
+ * @returns {boolean}
221
+ */
222
+ export function isInside(child, parent) {
197
223
  const rel = relative(parent, child);
198
224
  return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
199
225
  }
200
226
 
201
- function readConfig(projectPath) {
202
- const configPath = join(projectPath, '.morph', 'config', 'config.json');
227
+ /**
228
+ * Read `.morph/config/config.json`, or null when it is absent or malformed.
229
+ *
230
+ * Exported so test-nodes.js reads the config through the SAME door: a second
231
+ * reader would be a second place for "absent" and "malformed" to drift apart —
232
+ * and both must keep meaning "fall back", never "fail".
233
+ *
234
+ * @param {string} projectPath
235
+ * @returns {object|null}
236
+ */
237
+ export function readMorphConfig(projectPath) {
238
+ const configPath = join(projectPath, CONFIG_FILE_NAME);
203
239
  if (!existsSync(configPath)) return null;
204
240
  try {
205
241
  return JSON.parse(readFileSync(configPath, 'utf8'));
@@ -210,24 +246,32 @@ function readConfig(projectPath) {
210
246
  }
211
247
 
212
248
  /**
213
- * Resolve the timeout budget for the test node.
249
+ * Resolve the timeout budget for ONE test node.
214
250
  *
215
- * Precedence: explicit override (`morph-spec verify --timeout`) >
216
- * `config.project.testTimeoutMs` > default scaled by the number of chained
217
- * test projects.
251
+ * Precedence: explicit override (`morph-spec verify --timeout`) > the node's
252
+ * own `timeoutMs` > `config.project.testTimeoutMs` > default scaled by the
253
+ * number of test projects the node's command chains.
254
+ *
255
+ * The per-node ceiling sits above the project-wide one because the derived
256
+ * default (120s x chained projects) is shaped for `dotnet test`: it is hostile
257
+ * to a Node suite that takes twelve minutes, and a project with both runners
258
+ * cannot serve them with a single number. It sits BELOW the CLI override
259
+ * because what the user typed is the most explicit statement in the room.
218
260
  *
219
261
  * @param {string} projectPath
220
- * @param {number} projectCount - How many test projects the command chains
262
+ * @param {import('./test-nodes.js').TestNode} node - The node about to run
221
263
  * @param {number} [override] - Explicit override in ms, if the caller has one
222
264
  * @returns {number} Timeout in ms
223
265
  */
224
- export function resolveTestTimeout(projectPath, projectCount, override) {
266
+ export function resolveTestTimeout(projectPath, node, override) {
225
267
  if (Number.isFinite(override) && override > 0) return override;
226
268
 
227
- const configured = readConfig(projectPath)?.project?.testTimeoutMs;
269
+ if (Number.isFinite(node?.timeoutMs) && node.timeoutMs > 0) return node.timeoutMs;
270
+
271
+ const configured = readMorphConfig(projectPath)?.project?.testTimeoutMs;
228
272
  if (Number.isFinite(configured) && configured > 0) return configured;
229
273
 
230
- return DEFAULT_TEST_TIMEOUT_PER_PROJECT * Math.max(1, projectCount);
274
+ return DEFAULT_TEST_TIMEOUT_PER_PROJECT * Math.max(1, node?.projectCount ?? 1);
231
275
  }
232
276
 
233
277
  /**
@@ -242,31 +286,364 @@ function countCompletedRuns(output) {
242
286
  return matches ? matches.length : 0;
243
287
  }
244
288
 
289
+ // ============================================================================
290
+ // Prebuild of the test projects, and the `--no-build` refusal (T5 / UC-08)
291
+ // ============================================================================
292
+
293
+ /**
294
+ * `--no-build` as a standalone token. Anchored on whitespace so `--no-build-x`
295
+ * (or a path that happens to contain the word) never matches.
296
+ */
297
+ const NO_BUILD_FLAG = /(^|\s)--no-build(\s|$)/;
298
+
299
+ /**
300
+ * The `.csproj` targets a command names, quotes removed and duplicates dropped.
301
+ *
302
+ * Finding none yields `[]`, and `[]` is the SAFE answer, not a shrug: it means
303
+ * nothing was compiled here, which is precisely the condition that makes a
304
+ * `--no-build` in the same command a refusal. Refusing is cheap; approving a
305
+ * stale assembly is what F29 cost.
306
+ *
307
+ * @param {string|null|undefined} command
308
+ * @returns {string[]}
309
+ */
310
+ export function extractCsprojTargets(command) {
311
+ if (typeof command !== 'string') return [];
312
+ const found = [];
313
+ const token = /"([^"]+\.csproj)"|'([^']+\.csproj)'|(\S+\.csproj)/gi;
314
+ let match;
315
+ while ((match = token.exec(command)) !== null) {
316
+ found.push(match[1] ?? match[2] ?? match[3]);
317
+ }
318
+ return [...new Set(found)];
319
+ }
320
+
321
+ /**
322
+ * Which test projects this node compiles before running: what it DECLARED, or
323
+ * else what its command names. Only `runner: 'dotnet'` has a prebuild at all —
324
+ * a vitest node has nothing to compile, and pretending otherwise would invent
325
+ * a build step nobody asked for.
326
+ *
327
+ * @param {import('./test-nodes.js').TestNode} node
328
+ * @returns {string[]}
329
+ */
330
+ export function resolveTestBuildTargets(node) {
331
+ if (node?.runner !== 'dotnet') return [];
332
+ if (Array.isArray(node.build) && node.build.length > 0) return node.build;
333
+ return extractCsprojTargets(node.command);
334
+ }
335
+
336
+ /**
337
+ * `dotnet build "<target>" …`, built from the SAME step build-check.js
338
+ * resolves for a .NET stack — the string "dotnet build" is not typed a second
339
+ * time anywhere in this repository.
340
+ *
341
+ * `--no-restore` is deliberately dropped: it is correct for the ROOT build
342
+ * (whose restore already happened) and wrong here, because the whole reason a
343
+ * test project needs its own prebuild is that it may live OUTSIDE the
344
+ * application's solution and may therefore never have been restored at all.
345
+ * F29's project was exactly that project.
346
+ */
347
+ function dotnetBuildCommandFor(target, projectPath) {
348
+ const base = getBuildSteps('dotnet', { cwd: projectPath || '.' })[0]?.command;
349
+ if (!base) return null;
350
+ const [verb, subcommand, ...flags] = base.split(/\s+/);
351
+ const kept = flags.filter((flag) => flag !== '--no-restore');
352
+ return `${verb} ${subcommand} "${target}"${kept.length ? ` ${kept.join(' ')}` : ''}`;
353
+ }
354
+
355
+ /**
356
+ * Compile this node's test projects, and report whether a build ACTUALLY
357
+ * happened in this invocation — the fact the `--no-build` refusal keys on.
358
+ *
359
+ * WHEN it runs: when the node declared `build[]` (an explicit statement that
360
+ * these projects need compiling), or when the command carries `--no-build`
361
+ * (the only case where the binary would otherwise be whatever the last run
362
+ * left behind). A plain `dotnet test X.csproj` is NOT pre-compiled: `dotnet
363
+ * test` builds it itself and a compile error already reddens the node, so
364
+ * pre-compiling would only double the clock of the hot path — and the clock is
365
+ * what D13 is about.
366
+ *
367
+ * @returns {{ built: boolean, failure: object|null }} `failure` is a full
368
+ * runTestSuite result, ready to be returned as-is.
369
+ */
370
+ function runTestPrebuild({ node, cwd, exec, timeout, projectPath }) {
371
+ if (node.runner !== 'dotnet') return { built: false, failure: null };
372
+
373
+ const declared = Array.isArray(node.build) ? node.build : [];
374
+ if (declared.length === 0 && !NO_BUILD_FLAG.test(node.command)) {
375
+ return { built: false, failure: null };
376
+ }
377
+
378
+ const targets = resolveTestBuildTargets(node);
379
+ const outputs = [];
380
+
381
+ for (const target of targets) {
382
+ const command = dotnetBuildCommandFor(target, projectPath);
383
+ if (!command) return { built: false, failure: null };
384
+ try {
385
+ exec(command, { cwd, timeout, encoding: 'utf8', stdio: 'pipe' });
386
+ } catch (err) {
387
+ outputs.push(`[${command}]\n${(err.stdout || '') + (err.stderr || '')}`);
388
+ return {
389
+ built: false,
390
+ failure: {
391
+ passed: false,
392
+ buildFailed: true,
393
+ reason: 'test-build-failed',
394
+ output: outputs.join('\n\n'),
395
+ exitCode: err.status ?? 1,
396
+ timeoutMs: timeout,
397
+ },
398
+ };
399
+ }
400
+ }
401
+
402
+ return { built: targets.length > 0, failure: null };
403
+ }
404
+
405
+ // ============================================================================
406
+ // How many tests actually ran (T4 / UC-08)
407
+ // ============================================================================
408
+
409
+ /**
410
+ * Sum every `Total:`/`Aprovado:`/`Com falha:` summary `dotnet test` printed.
411
+ *
412
+ * SUM, not "the last one": a chained `a && b` command prints one summary per
413
+ * project, and reading a single line would report half a suite as the whole of
414
+ * it — which, against a recorded baseline, is an invented count drop.
415
+ * `countCompletedRuns()` above counts the same lines for a different question
416
+ * (WHICH link was running when the clock died); this one reads their numbers.
417
+ */
418
+ function parseDotnetCount(output) {
419
+ const lines = output.split('\n').filter((l) => /^\s*(?:Aprovado|Passed|Com falha|Failed|Reprovado|Erro)!/i.test(l));
420
+ let seen = false;
421
+ const sum = { total: 0, passed: 0, failed: 0 };
422
+
423
+ for (const line of lines) {
424
+ const total = /\b(?:Total de testes|Total)\s*:\s*(\d+)/i.exec(line);
425
+ if (!total) continue;
426
+ seen = true;
427
+ sum.total += Number(total[1]);
428
+ const passed = /\b(?:Aprovado|Passed)\s*:\s*(\d+)/i.exec(line);
429
+ const failed = /\b(?:Com falha|Failed|Reprovado)\s*:\s*(\d+)/i.exec(line);
430
+ sum.passed += passed ? Number(passed[1]) : 0;
431
+ sum.failed += failed ? Number(failed[1]) : 0;
432
+ }
433
+
434
+ return seen ? sum : null;
435
+ }
436
+
437
+ /** Last value of a `# key N` (TAP) or `ℹ key N` (spec reporter) summary line. */
438
+ function lastSummaryValue(output, key) {
439
+ const re = new RegExp(`^\\s*[#ℹ]\\s*${key}\\s+(\\d+)\\s*$`, 'gim');
440
+ let match;
441
+ let value = null;
442
+ while ((match = re.exec(output)) !== null) value = Number(match[1]);
443
+ return value;
444
+ }
445
+
446
+ /** `node --test`, in either of the two reporters the runner may pick. */
447
+ function parseNodeTestCount(output) {
448
+ const total = lastSummaryValue(output, 'tests');
449
+ if (total === null) return null;
450
+ return {
451
+ total,
452
+ passed: lastSummaryValue(output, 'pass') ?? 0,
453
+ failed: lastSummaryValue(output, 'fail') ?? 0,
454
+ };
455
+ }
456
+
457
+ /**
458
+ * Vitest: `Tests 3082 passed (3082)` / `Tests 1 failed | 3081 passed (3082)`.
459
+ *
460
+ * The line must start with `Tests`, never `Test Files` — the file count sitting
461
+ * right above it has the same shape, and reading it would report 27 where 3082
462
+ * ran. A fabricated drop is worse than no count at all.
463
+ */
464
+ function parseVitestCount(output) {
465
+ const line = output.split('\n').find((l) => /^\s*Tests\s{1,}\d/.test(l));
466
+ if (!line) return null;
467
+ const passed = /(\d+)\s+passed/i.exec(line);
468
+ const failed = /(\d+)\s+failed/i.exec(line);
469
+ if (!passed && !failed) return null;
470
+ const declaredTotal = /\((\d+)\)\s*$/.exec(line.trim());
471
+ const p = passed ? Number(passed[1]) : 0;
472
+ const f = failed ? Number(failed[1]) : 0;
473
+ return { total: declaredTotal ? Number(declaredTotal[1]) : p + f, passed: p, failed: f };
474
+ }
475
+
476
+ /** Jest: `Tests: 2 failed, 118 passed, 120 total`. */
477
+ function parseJestCount(output) {
478
+ const line = output.split('\n').find((l) => /^\s*Tests:\s/.test(l));
479
+ if (!line) return null;
480
+ const total = /(\d+)\s+total/i.exec(line);
481
+ if (!total) return null;
482
+ const passed = /(\d+)\s+passed/i.exec(line);
483
+ const failed = /(\d+)\s+failed/i.exec(line);
484
+ return {
485
+ total: Number(total[1]),
486
+ passed: passed ? Number(passed[1]) : 0,
487
+ failed: failed ? Number(failed[1]) : 0,
488
+ };
489
+ }
490
+
491
+ /** Playwright: `8 passed (12.4s)` and/or `2 failed`, on their own lines. */
492
+ function parsePlaywrightCount(output) {
493
+ const passed = /^\s*(\d+)\s+passed\b/im.exec(output);
494
+ const failed = /^\s*(\d+)\s+failed\b/im.exec(output);
495
+ if (!passed && !failed) return null;
496
+ const p = passed ? Number(passed[1]) : 0;
497
+ const f = failed ? Number(failed[1]) : 0;
498
+ return { total: p + f, passed: p, failed: f };
499
+ }
500
+
501
+ /** One extractor per runner. An absent entry is an honest "I cannot read this". */
502
+ const COUNT_EXTRACTORS = {
503
+ dotnet: parseDotnetCount,
504
+ // An eval IS `dotnet test`, so it reads the same summary. No new extractor is
505
+ // invented here: the authoritative number for an eval node is the `scenarios`
506
+ // field of the fixture's summary FILE, which this function cannot see (it only
507
+ // gets the output). `verify-runner` prefers that file and falls back to this —
508
+ // and both paths keep `null` meaning UNKNOWN, never zero.
509
+ evals: parseDotnetCount,
510
+ 'node-test': parseNodeTestCount,
511
+ vitest: parseVitestCount,
512
+ jest: parseJestCount,
513
+ playwright: parsePlaywrightCount,
514
+ none: () => null,
515
+ };
516
+
517
+ /**
518
+ * How many tests actually ran, read out of the runner's own summary.
519
+ *
520
+ * @param {string|null|undefined} output - Combined stdout+stderr of the run
521
+ * @param {string} runner - One of TEST_RUNNERS (see test-nodes.js)
522
+ * @returns {{ total: number, passed: number, failed: number }|null}
523
+ * **null is UNKNOWN, never zero.** A summary nobody could read is not a
524
+ * suite that ran zero tests: collapsing the two would manufacture a total
525
+ * count drop against every recorded baseline out of a missing regex. Same
526
+ * doctrine as `null` in pr-index.js.
527
+ */
528
+ export function parseTestCount(output, runner) {
529
+ if (typeof output !== 'string' || !output) return null;
530
+ const extractor = COUNT_EXTRACTORS[runner];
531
+ if (!extractor) return null;
532
+ return extractor(output);
533
+ }
534
+
535
+ /**
536
+ * Compare an executed count against the recorded baseline — asymmetrically,
537
+ * and on purpose (D7).
538
+ *
539
+ * A count that DROPS is a finding: tests that used to run no longer do, which
540
+ * is exactly the silent damage this feature exists to catch. A count that
541
+ * RISES is the normal life of a suite that grows with every task. A symmetric
542
+ * rule would turn each new test into a red gate and would teach the agent to
543
+ * delete tests in order to "fix" it — the lesson MORPH.md already paid for
544
+ * ("mandate afirmando 2366 quando já eram 2375").
545
+ *
546
+ * @param {number|null} count - Executed count; null is UNKNOWN
547
+ * @param {number|null} expectedCount - Recorded baseline; null is "none recorded"
548
+ * @returns {{ verdict: 'drop'|'rise'|'match'|'unknown'|'no-baseline', reason: string|null, warning: string|null }}
549
+ * `reason` is non-null ONLY for a drop — it is the only verdict that fails a
550
+ * node by itself. Everything else leaves the verdict to the exit code.
551
+ */
552
+ export function compareTestCount(count, expectedCount) {
553
+ const answer = (verdict, reason = null, warning = null) => ({ verdict, reason, warning });
554
+
555
+ // `null` is "no baseline recorded"; a declared `0` is a real baseline. The
556
+ // distinction has to survive here or "this suite runs zero tests" becomes
557
+ // inexpressible.
558
+ if (!Number.isInteger(expectedCount)) return answer('no-baseline');
559
+
560
+ if (!Number.isInteger(count)) {
561
+ return answer('unknown', null,
562
+ `contagem desconhecida — a baseline ${expectedCount} não pôde ser comparada`);
563
+ }
564
+
565
+ if (count < expectedCount) {
566
+ return answer('drop', `count-drop: esperado ${expectedCount}, executado ${count}`);
567
+ }
568
+
569
+ if (count > expectedCount) {
570
+ return answer('rise', null,
571
+ `baseline desatualizada (${expectedCount} → ${count}); rode verify --record-baseline`);
572
+ }
573
+
574
+ return answer('match');
575
+ }
576
+
245
577
  /**
246
- * Run the project's test suite.
578
+ * Run ONE test node.
579
+ *
580
+ * The node is a parameter rather than something resolved here: a project has a
581
+ * LIST of suites (see test-nodes.js), and a runner that resolved its own single
582
+ * command is precisely what let 3.082 vitest tests sit outside the gate (F30).
583
+ * Resolution belongs to `resolveTestNodes()`; execution belongs here. One node,
584
+ * one command, one cwd, one clock.
247
585
  *
248
586
  * @param {string} [projectPath] - Project root directory (defaults to '.')
249
587
  * @param {object} [opts]
588
+ * @param {import('./test-nodes.js').TestNode} opts.node - The node to run; its
589
+ * `cwd` is resolved against `projectPath`
250
590
  * @param {number} [opts.timeout] - Explicit timeout in ms (highest precedence)
591
+ * @param {Record<string,string>} [opts.env] - Extra environment for THIS node, layered
592
+ * over `process.env`. The eval runner's channel (`MORPH_EVAL_*`) travels here.
593
+ * Absent leaves the child on the parent's environment, byte for byte — every
594
+ * other runner keeps the exact behaviour it had.
251
595
  * @param {Function} [opts._execSync] - Override execSync for unit testing
252
596
  * @returns {{ passed?: boolean, output?: string, exitCode?: number, skipped?: boolean, reason?: string, timedOut?: boolean, timeoutMs?: number, message?: string }}
253
597
  */
254
598
  export function runTestSuite(projectPath = '.', opts = {}) {
255
- const { command: cmd, projectCount } = resolveTestPlan(projectPath);
599
+ const node = opts.node;
600
+ const cmd = node?.command;
256
601
 
257
602
  if (!cmd) {
258
- return { skipped: true, reason: 'No testCommand configured and no known project file detected' };
603
+ return {
604
+ skipped: true,
605
+ reason: node?.reason || 'No testCommand configured and no known project file detected',
606
+ };
259
607
  }
260
608
 
261
609
  const exec = opts._execSync ?? execSync;
262
- const timeout = resolveTestTimeout(projectPath, projectCount, opts.timeout);
610
+ const timeout = resolveTestTimeout(projectPath, node, opts.timeout);
611
+ const projectCount = node.projectCount ?? 1;
612
+ const cwd = resolve(projectPath, node.cwd || '.');
613
+
614
+ // Compile the test projects FIRST, then decide whether `--no-build` has
615
+ // anything behind it. Order matters: the build is what makes the flag legal.
616
+ const prebuild = runTestPrebuild({ node, cwd, exec, timeout, projectPath });
617
+ if (prebuild.failure) return prebuild.failure;
618
+
619
+ // F29: `verify` reported `tests: pass` while the test csproj did not compile,
620
+ // because `--no-build` ran last build's assembly and seven new tests never
621
+ // executed. The refusal keys on whether a build HAPPENED here, never on the
622
+ // flag alone — `--no-build` after a real build is the fast path (D11). The
623
+ // command itself is never rewritten: a silent fix would hide the config
624
+ // defect and make the reported `command` a lie about what ran.
625
+ if (node.runner === 'dotnet' && NO_BUILD_FLAG.test(cmd) && !prebuild.built) {
626
+ return {
627
+ passed: false,
628
+ buildFailed: false,
629
+ reason: 'stale-binary: --no-build sem build dos projetos de teste nesta invocação',
630
+ output: '',
631
+ exitCode: null,
632
+ timeoutMs: timeout,
633
+ };
634
+ }
635
+
636
+ // Layered OVER process.env, never replacing it: a child that lost PATH would
637
+ // fail in a way that has nothing to do with the suite under test.
638
+ const env = opts.env ? { ...process.env, ...opts.env } : undefined;
263
639
 
264
640
  try {
265
641
  const output = exec(cmd, {
266
- cwd: projectPath,
642
+ cwd,
267
643
  timeout,
268
644
  encoding: 'utf8',
269
- stdio: 'pipe'
645
+ stdio: 'pipe',
646
+ ...(env ? { env } : {}),
270
647
  });
271
648
  return { passed: true, output: output ?? '', exitCode: 0, timeoutMs: timeout };
272
649
  } catch (err) {
@@ -1,39 +1,39 @@
1
- /**
2
- * Checkpoint Validation Persistence
3
- *
4
- * Persists checkpoint validation results to disk for traceability.
5
- * Each checkpoint writes a JSON file to:
6
- * .morph/features/{feature}/3-implement/validations/checkpoint-{N}.json
7
- */
8
-
9
- import { mkdirSync, writeFileSync } from 'fs';
10
- import { join } from 'path';
11
-
12
- /**
13
- * Persist a checkpoint validation result to disk.
14
- *
15
- * @param {string} projectPath - Project root
16
- * @param {string} featureName - Feature name
17
- * @param {{ checkpointNum: number, timestamp: string, passed: boolean, validators: Object, summary: string, feature?: string, phase?: string }} result - Validation result
18
- * @returns {string} Path to the saved JSON file
19
- */
20
- export function persistCheckpointValidation(projectPath, featureName, result) {
21
- const dir = join(projectPath, `.morph/features/${featureName}/3-implement/validations`);
22
- mkdirSync(dir, { recursive: true });
23
-
24
- const filename = `checkpoint-${result.checkpointNum}.json`;
25
- const filePath = join(dir, filename);
26
-
27
- const output = {
28
- checkpointNum: result.checkpointNum,
29
- timestamp: result.timestamp || new Date().toISOString(),
30
- passed: result.passed,
31
- validators: result.validators || {},
32
- summary: result.summary || '',
33
- feature: result.feature || featureName,
34
- phase: result.phase || 'implement',
35
- };
36
-
37
- writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf8');
38
- return filePath;
39
- }
1
+ /**
2
+ * Checkpoint Validation Persistence
3
+ *
4
+ * Persists checkpoint validation results to disk for traceability.
5
+ * Each checkpoint writes a JSON file to:
6
+ * .morph/features/{feature}/3-implement/validations/checkpoint-{N}.json
7
+ */
8
+
9
+ import { mkdirSync, writeFileSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ /**
13
+ * Persist a checkpoint validation result to disk.
14
+ *
15
+ * @param {string} projectPath - Project root
16
+ * @param {string} featureName - Feature name
17
+ * @param {{ checkpointNum: number, timestamp: string, passed: boolean, validators: Object, summary: string, feature?: string, phase?: string }} result - Validation result
18
+ * @returns {string} Path to the saved JSON file
19
+ */
20
+ export function persistCheckpointValidation(projectPath, featureName, result) {
21
+ const dir = join(projectPath, `.morph/features/${featureName}/3-implement/validations`);
22
+ mkdirSync(dir, { recursive: true });
23
+
24
+ const filename = `checkpoint-${result.checkpointNum}.json`;
25
+ const filePath = join(dir, filename);
26
+
27
+ const output = {
28
+ checkpointNum: result.checkpointNum,
29
+ timestamp: result.timestamp || new Date().toISOString(),
30
+ passed: result.passed,
31
+ validators: result.validators || {},
32
+ summary: result.summary || '',
33
+ feature: result.feature || featureName,
34
+ phase: result.phase || 'implement',
35
+ };
36
+
37
+ writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf8');
38
+ return filePath;
39
+ }