@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
@@ -13,7 +13,8 @@
13
13
  *
14
14
  * Pure orchestration — reuses (never reimplements):
15
15
  * - runBuildCheck() from framework/hooks/shared/build-check.js
16
- * - runTestSuite() from src/lib/tasks/test-runner.js
16
+ * - runTestSuite() from src/lib/tasks/test-runner.js (one node)
17
+ * - resolveTestNodes() from src/lib/tasks/test-nodes.js (the LIST of suites)
17
18
  * - runValidation() from ./validation-runner.js
18
19
  * - runE2eCheck() from framework/hooks/shared/e2e-check.js
19
20
  * - loadRubric() + aggregateScores() from ./score-aggregator.js
@@ -28,7 +29,16 @@ import { join, resolve } from 'path';
28
29
  import { runBuildCheck, getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
29
30
  import { runE2eCheck } from '../../../framework/hooks/shared/e2e-check.js';
30
31
  import { buildComposeEnv } from '../e2e/compose-env.js';
31
- import { runTestSuite, resolveTestCommand } from '../tasks/test-runner.js';
32
+ import { runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout } from '../tasks/test-runner.js';
33
+ import {
34
+ resolveTestNodes, parseTestFilter, nodeMatchesFilter, recordTestBaselines, morphConfigPath,
35
+ } from '../tasks/test-nodes.js';
36
+ import { runLinuxSuite, checkLinuxPreconditions } from '../tasks/linux-runner.js';
37
+ import {
38
+ EVAL_RUNNER, evalPreflight, evalNodeEnv, readEvalSummary, evalScenarioCount,
39
+ evalFailureLines,
40
+ evalEmptyRunProblem, evalColdCacheWarning,
41
+ } from '../tasks/eval-node.js';
32
42
  import { runValidation } from './validation-runner.js';
33
43
  import { loadRubric, aggregateScores } from './score-aggregator.js';
34
44
  import { checkOutputsTraceability } from './traceability-check.js';
@@ -40,6 +50,23 @@ const DEFAULT_BUILD_TIMEOUT = 120_000;
40
50
  /** Number of trailing output lines kept for feedback/reporting. */
41
51
  const OUTPUT_TAIL_LINES = 20;
42
52
 
53
+ /**
54
+ * The caller asked for something impossible — not a red verification, a wrong
55
+ * invocation. The distinction is the exit code: `verify` answers 1 for "the
56
+ * code is not ready" and 2 for "this command cannot mean what you typed".
57
+ *
58
+ * It exists because `--filter` can select nothing. Running every suite (or,
59
+ * far worse, none of them) and reporting green would be F30 restated as a
60
+ * one-word typo — so the runner refuses instead of guessing.
61
+ */
62
+ export class VerifyUsageError extends Error {
63
+ constructor(message) {
64
+ super(message);
65
+ this.name = 'VerifyUsageError';
66
+ this.code = 'verify-usage';
67
+ }
68
+ }
69
+
43
70
  /**
44
71
  * Run the deterministic verify cadence for a feature (or a single task within it).
45
72
  *
@@ -54,6 +81,11 @@ const OUTPUT_TAIL_LINES = 20;
54
81
  * @param {boolean} [options.skipBuild] - Skip the build node
55
82
  * @param {boolean} [options.skipE2e] - Skip the e2e node
56
83
  * @param {boolean} [options.freshE2e] - Force a stack rebuild instead of reusing a warm one
84
+ * @param {boolean} [options.recordBaseline] - Measure and write `expectedCount` back into
85
+ * `project.tests[]` — only from a run whose every test node is green (D8)
86
+ * @param {boolean} [options.linux] - Run the test nodes inside a Linux container, on a COPY
87
+ * of the tree (see linux-runner.js for why a bind mount would prove nothing)
88
+ * @param {string} [options.linuxImage] - Image override for --linux
57
89
  * @param {Function} [options._runBuildCheck] - Injectable override for runBuildCheck (testing)
58
90
  * @param {Function} [options._runTestSuite] - Injectable override for runTestSuite (testing)
59
91
  * @param {Function} [options._runValidation] - Injectable override for runValidation (testing)
@@ -69,6 +101,12 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
69
101
  skipBuild = false,
70
102
  skipE2e = false,
71
103
  freshE2e = false,
104
+ recordBaseline = false,
105
+ refreshCache = false,
106
+ linux = false,
107
+ linuxImage = null,
108
+ _runLinuxSuite = runLinuxSuite,
109
+ _exec = undefined,
72
110
  _runBuildCheck = runBuildCheck,
73
111
  _runTestSuite = runTestSuite,
74
112
  _runValidation = runValidation,
@@ -79,6 +117,64 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
79
117
  const stack = readStack(projectPath);
80
118
  const paths = readProjectPaths(projectPath);
81
119
 
120
+ // The suites are resolved ONCE, before anything is executed, because
121
+ // `--filter` has to be judged before the first second of build time is
122
+ // spent: a filter that matches nothing is a typo, and paying for a build to
123
+ // discover a typo is the worst of both outcomes.
124
+ const testPlan = resolveTestNodes(projectPath);
125
+ const filter = parseTestFilter(options.filter);
126
+ if (filter && !testPlan.nodes.some((node) => nodeMatchesFilter(node, filter))) {
127
+ const declared = testPlan.nodes.map((node) => node.id).join(', ') || '(none)';
128
+ throw new VerifyUsageError(
129
+ `--filter "${filter.raw}" matched no test node — declared nodes: ${declared}`
130
+ );
131
+ }
132
+
133
+ // `--record-baseline` is judged in the same breath, and for the same reason:
134
+ // discovering that there is nowhere to write AFTER running the whole fleet
135
+ // would charge the user a full suite for a usage error. `config.json` is
136
+ // never created here — this repository deliberately has none, and inventing
137
+ // one would break `doctor --full`.
138
+ if (recordBaseline) {
139
+ const problem = baselineTargetProblem(projectPath, testPlan, { skipTests });
140
+ if (problem) throw new VerifyUsageError(problem);
141
+ }
142
+
143
+ // `--refresh-cache` is judged in the same breath as `--filter` and
144
+ // `--record-baseline`, and for the same reason — but here the stake is higher
145
+ // than a wasted clock: a refusal AFTER the cache was touched would leave a
146
+ // half-rewritten tree behind. Same discipline as `finish --merge`, which
147
+ // validates the root BEFORE archiving.
148
+ if (refreshCache) {
149
+ const problem = refreshCacheProblem(projectPath, testPlan, filter, { skipTests });
150
+ if (problem) throw new VerifyUsageError(problem);
151
+ }
152
+
153
+ // `--linux` fails FAST and LOUD when Docker is missing (exit 2). Never a
154
+ // skip: "we could not check on Linux" reading as green is F35 itself.
155
+ if (linux) {
156
+ const problem = checkLinuxPreconditions({
157
+ projectPath, skipTests, ...(_exec ? { exec: _exec } : {}),
158
+ });
159
+ if (problem) throw new VerifyUsageError(problem);
160
+
161
+ // The MORPH_EVAL_* channel carries ABSOLUTE host paths, and the container
162
+ // runs a COPY of the tree at /work — so those paths do not exist inside it.
163
+ // Refusing by name is the only honest answer: running the node anyway would
164
+ // fail with an unrelated message, and skipping it would be the green-over-
165
+ // nothing shape this feature exists to remove. The escape is named.
166
+ const evalNode = testPlan.nodes.find(
167
+ (node) => node.runner === EVAL_RUNNER && nodeMatchesFilter(node, filter)
168
+ );
169
+ if (evalNode) {
170
+ throw new VerifyUsageError(
171
+ `--linux não executa o nó de eval "${evalNode.id}": o canal MORPH_EVAL_* carrega caminhos absolutos do host, `
172
+ + 'e o container roda uma CÓPIA da árvore em /work. Rode o eval fora do container, '
173
+ + `ou exclua-o com --filter (nós declarados: ${testPlan.nodes.map((n) => n.id).join(', ')}).`
174
+ );
175
+ }
176
+ }
177
+
82
178
  const nodes = {};
83
179
  const feedback = [];
84
180
 
@@ -94,10 +190,18 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
94
190
  const explicitTimeout = Number.isFinite(options.timeout) ? options.timeout : undefined;
95
191
  nodes.tests = await runTestsNode({
96
192
  projectPath,
193
+ testPlan,
194
+ filter,
97
195
  skipTests,
98
196
  buildNode: nodes.build,
99
197
  timeout: explicitTimeout,
198
+ taskId,
199
+ refreshCache,
200
+ linux,
201
+ linuxImage,
100
202
  _runTestSuite,
203
+ _runLinuxSuite,
204
+ _exec,
101
205
  });
102
206
  appendTestsFeedback(feedback, nodes.tests);
103
207
 
@@ -132,6 +236,13 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
132
236
 
133
237
  const status = Object.values(nodes).some((n) => n.status === 'fail') ? 'fail' : 'pass';
134
238
 
239
+ // Recording happens LAST and only from what actually ran — the number is a
240
+ // measurement of this invocation, never a value carried in from anywhere.
241
+ const baseline = recordBaseline ? recordBaselineFrom(projectPath, nodes.tests) : null;
242
+ if (baseline && !baseline.recorded) {
243
+ feedback.push(`[baseline] não gravada: ${baseline.reason}`);
244
+ }
245
+
135
246
  return {
136
247
  version: 1,
137
248
  feature: featureName,
@@ -143,6 +254,7 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
143
254
  nodes,
144
255
  feedback,
145
256
  traceability,
257
+ ...(baseline ? { baseline } : {}),
146
258
  };
147
259
  }
148
260
 
@@ -186,35 +298,276 @@ function appendBuildFeedback(feedback, buildNode) {
186
298
  // Tests node
187
299
  // ============================================================================
188
300
 
189
- async function runTestsNode({ projectPath, skipTests, buildNode, timeout, _runTestSuite }) {
190
- const command = resolveTestCommand(projectPath);
301
+ /**
302
+ * Run EVERY test suite the project declares, and roll the results up.
303
+ *
304
+ * Why a rollup instead of one node: defect F30. The resolver answered
305
+ * "dotnet test", the 3.082 vitest tests of the same repository never ran, and
306
+ * the gate reported green — a suite nobody could name is a suite that cannot
307
+ * fail. `resolveTestNodes()` now returns the LIST, each entry runs with its own
308
+ * command, cwd and clock, and every one of them appears in `runs[]`.
309
+ *
310
+ * `nodes.tests` deliberately stays an OBJECT with `status` (D6): verify.js and
311
+ * the persisted stamp read `nodes.tests.status`, and turning a verification
312
+ * defect into a shape defect is not progress. The plural detail lives in
313
+ * `runs[]` beside it.
314
+ *
315
+ * @returns {Object} The rollup: `status`, joined `command`, summed `durationMs`,
316
+ * `source`, `environment` (null outside a container) and `runs[]`.
317
+ */
318
+ async function runTestsNode({
319
+ projectPath, testPlan, filter, skipTests, buildNode, timeout, taskId, refreshCache = false,
320
+ linux = false, linuxImage = null, _runTestSuite, _runLinuxSuite = runLinuxSuite, _exec,
321
+ }) {
322
+ const { nodes, source } = testPlan;
323
+ // Kept readable for whoever only prints the string — the same join the
324
+ // fallback scan already used for a chain of test projects.
325
+ const command = nodes.map((n) => n.command).filter(Boolean).join(' && ') || null;
326
+ // A fresh `runs: []` per return — two rollups must never alias one array.
327
+ const nothingRan = (reason) => ({
328
+ status: 'skip', command, source, environment: null, runs: [], warnings: [],
329
+ durationMs: 0, reason, output: '',
330
+ });
331
+
332
+ if (skipTests) return nothingRan('requested');
191
333
 
192
- if (skipTests) {
193
- return { status: 'skip', command, durationMs: 0, reason: 'requested', output: '' };
334
+ if (buildNode.status === 'fail') {
335
+ return nothingRan('build failed — test run would only add noise');
194
336
  }
195
337
 
196
- if (buildNode.status === 'fail') {
197
- return {
198
- status: 'skip',
199
- command,
200
- durationMs: 0,
201
- reason: 'build failed — test run would only add noise',
202
- output: '',
203
- };
338
+ if (linux) {
339
+ const { runs, environment } = await runNodesInLinux({
340
+ projectPath, nodes, filter, taskId, timeout, linuxImage, _runLinuxSuite, _exec,
341
+ });
342
+ return rollUpTestRuns(runs, { command, source, environment });
343
+ }
344
+
345
+ const runs = [];
346
+ for (const node of nodes) {
347
+ runs.push(await runOneTestNode({
348
+ projectPath, node, filter, taskId, timeout, refreshCache, _runTestSuite,
349
+ }));
350
+ }
351
+
352
+ return rollUpTestRuns(runs, { command, source });
353
+ }
354
+
355
+ /**
356
+ * Run the executable nodes inside ONE Linux container and judge them with the
357
+ * SAME rules the local path uses.
358
+ *
359
+ * The pre-decisions (invalid config, `--filter`, an e2e node at task scope)
360
+ * happen HERE, before the container exists: a node that must not run must not
361
+ * be written into the container script either, or the flag would quietly run
362
+ * what the user filtered out. What the container returns — an exit code and an
363
+ * output — is then handed to `judgeRun()`, the same function the host path
364
+ * calls, so a count drop means a count drop in both environments.
365
+ *
366
+ * The tree the container executes is a COPY on its own filesystem, never the
367
+ * bind mount: see linux-runner.js. That is the whole reason this path exists.
368
+ */
369
+ async function runNodesInLinux({
370
+ projectPath, nodes, filter, taskId, timeout, linuxImage, _runLinuxSuite, _exec,
371
+ }) {
372
+ const decided = new Map();
373
+ const toRun = [];
374
+ for (const node of nodes) {
375
+ const pre = preDecideRun({ node, filter, taskId, projectPath });
376
+ if (pre) decided.set(node.id, pre);
377
+ else toRun.push(node);
204
378
  }
205
379
 
380
+ // One container, one clock: the ceiling is the sum of what each node would
381
+ // have been given locally, since they share the run.
382
+ const containerTimeout = toRun.reduce(
383
+ (sum, node) => sum + resolveTestTimeout(projectPath, node, timeout), 0
384
+ ) || undefined;
385
+
386
+ const suite = await _runLinuxSuite({
387
+ projectPath,
388
+ nodes: toRun,
389
+ image: linuxImage || undefined,
390
+ timeout: containerTimeout,
391
+ ...(_exec ? { exec: _exec } : {}),
392
+ });
393
+
394
+ const byId = new Map((suite.results || []).map((r) => [r.id, r]));
395
+ const runs = nodes.map((node) => {
396
+ if (decided.has(node.id)) return decided.get(node.id);
397
+ const raw = byId.get(node.id);
398
+ if (!raw) {
399
+ // The container answered about a fleet that does not include this node.
400
+ // Silence here would be a suite vanishing from the report.
401
+ return { ...emptyRunEntry(node), status: 'fail', reason: 'o container não reportou este nó' };
402
+ }
403
+ if (raw.skipped) {
404
+ return { ...emptyRunEntry(node), reason: raw.reason };
405
+ }
406
+ const result = raw.exitCode === null
407
+ // No exit code is NOT a pass: the container died before this node
408
+ // reported, and a green verdict over an unobserved suite is F30's shape.
409
+ ? { passed: false, output: raw.output || '', reason: raw.reason, exitCode: null }
410
+ : { passed: raw.exitCode === 0, output: raw.output || '', exitCode: raw.exitCode };
411
+ return judgeRun({ node, result, durationMs: raw.durationMs ?? 0 });
412
+ });
413
+
414
+ return { runs, environment: suite.environment };
415
+ }
416
+
417
+ /**
418
+ * Execute (or decline to execute) one node, always producing a `runs[]` entry.
419
+ *
420
+ * Nothing is ever dropped: a node that must not run says so with a reason, in
421
+ * the place the reader is already looking. A suite that silently disappears
422
+ * from the report is the defect this whole feature exists to close.
423
+ */
424
+ async function runOneTestNode({
425
+ projectPath, node, filter, taskId, timeout, refreshCache = false, _runTestSuite,
426
+ }) {
427
+ const decided = preDecideRun({ node, filter, taskId, projectPath, refreshCache });
428
+ if (decided) return decided;
429
+
430
+ // The eval channel: the CLI DICTATES where the cache, the dataset and the
431
+ // summary live, so the same xunit suite is runnable by `verify` without a
432
+ // second configuration path. `env` is undefined for every other runner, which
433
+ // keeps `runTestSuite` on exactly the path it had before.
434
+ const evalChannel = node.runner === EVAL_RUNNER
435
+ ? evalNodeEnv(node, projectPath, { refresh: refreshCache })
436
+ : null;
437
+
206
438
  const start = Date.now();
207
- const result = await _runTestSuite(projectPath, { timeout });
208
- const durationMs = Date.now() - start;
439
+ const result = await _runTestSuite(projectPath, {
440
+ timeout, node, ...(evalChannel ? { env: evalChannel.env } : {}),
441
+ });
442
+ const entry = judgeRun({ node, result, durationMs: Date.now() - start });
443
+
444
+ return evalChannel ? judgeEvalRun(entry, node, evalChannel.summaryPath) : entry;
445
+ }
446
+
447
+ /**
448
+ * Second half of an eval verdict: what the SUMMARY says, on top of what the exit
449
+ * code said.
450
+ *
451
+ * Two things the exit code cannot see:
452
+ * - the scenario COUNT, which is what `expectedCount` is compared against.
453
+ * The summary wins when readable; otherwise the `dotnet` extractor's number
454
+ * stands, because an eval IS `dotnet test`. **null stays UNKNOWN, never 0** —
455
+ * an unreadable summary must not manufacture a count drop;
456
+ * - a suite that evaluated ZERO scenarios and exited 0. Nothing failed, because
457
+ * nothing ran: the guardrail-inert shape this repository has already paid to
458
+ * learn to detect.
459
+ */
460
+ function judgeEvalRun(entry, node, summaryPath) {
461
+ const summary = readEvalSummary(summaryPath);
462
+ const warnings = [...(entry.warnings || [])];
463
+
464
+ const cold = evalColdCacheWarning(summary);
465
+ if (cold) warnings.push(cold);
466
+
467
+ const fromSummary = evalScenarioCount(summary);
468
+ const count = fromSummary ?? entry.count;
469
+ const verdict = compareTestCount(count, node.expectedCount);
470
+ if (verdict.warning) warnings.push(verdict.warning);
471
+
472
+ // As falhas NOMEADAS entram no buffer de saida do no, e entram na FRENTE.
473
+ // `appendTestsFeedback` prefere `output` a `reason`, entao um nome so no
474
+ // `reason` nunca chegaria ao `feedback[]`; e a cauda do `dotnet` e ruido
475
+ // truncado exatamente onde o sumario ja tem a frase certa. Prepend, e nao
476
+ // substituicao, porque um crash do runner tambem vive nessa cauda.
477
+ const named = evalFailureLines(summary);
478
+ const withNames = named.length > 0
479
+ ? { ...entry, output: [...named, ...(entry.output ? [entry.output] : [])].join('\n') }
480
+ : entry;
481
+
482
+ const empty = evalEmptyRunProblem(summary);
483
+ if (empty) {
484
+ return { ...withNames, status: 'fail', count, warnings, reason: empty };
485
+ }
486
+
487
+ if (entry.status === 'pass' && verdict.verdict === 'drop') {
488
+ return { ...withNames, status: 'fail', count, warnings, reason: verdict.reason };
489
+ }
490
+ if (verdict.verdict === 'drop') warnings.push(verdict.reason);
491
+
492
+ return { ...withNames, count, warnings };
493
+ }
494
+
495
+ /** The `runs[]` entry every path starts from — every field present, nothing implied. */
496
+ function emptyRunEntry(node) {
497
+ return {
498
+ id: node.id,
499
+ command: node.command,
500
+ cwd: node.cwd,
501
+ runner: node.runner,
502
+ expectedCount: node.expectedCount,
503
+ // Always present, so no reader has to tell "absent" from "unknown" by
504
+ // optional chaining. **null is UNKNOWN, never 0** (D7).
505
+ count: null,
506
+ status: 'skip',
507
+ durationMs: 0,
508
+ reason: null,
509
+ warnings: [],
510
+ output: '',
511
+ };
512
+ }
513
+
514
+ /**
515
+ * The verdicts that exist BEFORE anything is executed, or null when the node
516
+ * has to run to be judged.
517
+ *
518
+ * Shared by the local loop and by `--linux`, so a node that must not run is
519
+ * refused for the same reason and with the same words in both environments —
520
+ * two copies of this ordering is how a filtered node ends up invisible in one
521
+ * of them.
522
+ */
523
+ function preDecideRun({ node, filter, taskId, projectPath = '.', refreshCache = false }) {
524
+ const entry = emptyRunEntry(node);
525
+
526
+ // The resolver already decided: invalid config (fail-closed, D4) or an
527
+ // explicit "no suites declared". Executing it would be asking a question that
528
+ // has been answered — and, for an invalid node, answering it wrongly.
529
+ if (node.status) {
530
+ return { ...entry, status: node.status, reason: node.reason };
531
+ }
532
+
533
+ // Deliberately AFTER the pre-decided verdict above: `--filter` says "do not
534
+ // spend time on this suite", not "do not tell me my config is broken". An
535
+ // invalid node costs no clock and still reddens the gate (D4).
536
+ if (!nodeMatchesFilter(node, filter)) {
537
+ return { ...entry, reason: `filtered out (--filter ${filter.raw})` };
538
+ }
539
+
540
+ if (node.e2e && taskId) {
541
+ return { ...entry, reason: 'e2e node runs at feature scope only' };
542
+ }
543
+
544
+ // The eval pre-flight lives HERE, beside the other pre-decisions, so the host
545
+ // loop and the `--linux` path refuse for the same reason with the same words.
546
+ // Its three cache verdicts are FAILURES, never skips: a skip over an unusable
547
+ // cache is a green gate above a verification that never happened. The only
548
+ // skip it produces is scope — an eval suite does not pay for itself inside the
549
+ // per-task loop, and the result Gate 3 reads is the feature-scoped one.
550
+ if (node.runner === EVAL_RUNNER) {
551
+ const pre = evalPreflight(projectPath, node, { taskId, refresh: refreshCache });
552
+ if (pre.status) return { ...entry, status: pre.status, reason: pre.reason };
553
+ }
554
+
555
+ return null;
556
+ }
557
+
558
+ /**
559
+ * Turn ONE executor result into a `runs[]` entry — the count rule included.
560
+ *
561
+ * Called by both the local loop and the `--linux` container path on purpose:
562
+ * a drop, a rise and an unknown count must mean the same thing whether the
563
+ * suite ran on this host or inside a container. A second copy of this judgement
564
+ * is how two environments start disagreeing about the same suite.
565
+ */
566
+ function judgeRun({ node, result, durationMs }) {
567
+ const entry = emptyRunEntry(node);
209
568
 
210
569
  if (result.skipped) {
211
- return {
212
- status: 'skip',
213
- command,
214
- durationMs,
215
- reason: result.reason || 'no test command detected',
216
- output: '',
217
- };
570
+ return { ...entry, durationMs, reason: result.reason || 'no test command detected' };
218
571
  }
219
572
 
220
573
  // A timeout has no verdict to report: the tail of a truncated log reads like
@@ -222,8 +575,8 @@ async function runTestsNode({ projectPath, skipTests, buildNode, timeout, _runTe
222
575
  if (result.timedOut) {
223
576
  const tail = tailLines(result.output, OUTPUT_TAIL_LINES);
224
577
  return {
578
+ ...entry,
225
579
  status: 'fail',
226
- command,
227
580
  durationMs,
228
581
  timedOut: true,
229
582
  reason: result.message,
@@ -231,22 +584,224 @@ async function runTestsNode({ projectPath, skipTests, buildNode, timeout, _runTe
231
584
  };
232
585
  }
233
586
 
587
+ // How many tests actually ran, and how that compares to the recorded
588
+ // baseline. Read from the FULL output, before it is truncated to a tail —
589
+ // the summary line is the last thing a runner prints, but a red run's tail
590
+ // is chosen for the failures, not for the summary.
591
+ const parsed = parseTestCount(result.output, node.runner);
592
+ const count = parsed ? parsed.total : null;
593
+ const verdict = compareTestCount(count, node.expectedCount);
594
+ const warnings = verdict.warning ? [verdict.warning] : [];
595
+
596
+ // A drop is the one count verdict that fails a node by itself: tests that
597
+ // used to run no longer do. A RISE never fails (D7) — the suite grows with
598
+ // every task, and failing on growth teaches deleting tests.
599
+ if (verdict.verdict === 'drop' && result.passed) {
600
+ return { ...entry, status: 'fail', durationMs, count, warnings, reason: verdict.reason, output: '' };
601
+ }
602
+ // Already red: the failing test is the better lead, so the drop travels
603
+ // beside it instead of overwriting the reason.
604
+ if (verdict.verdict === 'drop') warnings.push(verdict.reason);
605
+
234
606
  return {
607
+ ...entry,
235
608
  status: result.passed ? 'pass' : 'fail',
236
- command,
237
609
  durationMs,
238
- reason: null,
610
+ count,
611
+ warnings,
612
+ // `test-build-failed` / `stale-binary`: the executor refused BEFORE any
613
+ // test ran, and the reason is the only thing worth reading. Without this
614
+ // the node would go red with a null reason and the reader would go hunting
615
+ // through a compiler log for a suite that never started.
616
+ reason: result.reason ?? null,
239
617
  output: result.passed ? '' : tailLines(result.output, OUTPUT_TAIL_LINES),
240
618
  };
241
619
  }
242
620
 
621
+ /**
622
+ * Aggregate the per-node runs into the single node `verify` publishes.
623
+ *
624
+ * The rollup is pessimistic on purpose: ANY red run reddens it. A green
625
+ * aggregate covering a red suite is F30 restated, so `some(fail)` is checked
626
+ * before anything else. `skip` only survives when there is nothing else to
627
+ * report — one green run among skips is a pass.
628
+ */
629
+ function rollUpTestRuns(runs, { command, source, environment = null }) {
630
+ const failed = runs.filter((r) => r.status === 'fail');
631
+ const status = failed.length > 0
632
+ ? 'fail'
633
+ : runs.some((r) => r.status === 'pass') ? 'pass' : 'skip';
634
+
635
+ const node = {
636
+ status,
637
+ command,
638
+ durationMs: runs.reduce((sum, r) => sum + (r.durationMs || 0), 0),
639
+ reason: null,
640
+ output: failed.map((r) => r.output).filter(Boolean).join('\n'),
641
+ source,
642
+ // Where these suites ran. `null` is the local host; a container run says so
643
+ // by name and image, because a green from a container and a green from the
644
+ // host are otherwise indistinguishable in feature.json — and the next
645
+ // person would not know what was proven.
646
+ environment,
647
+ // Named by suite: a warning that does not say WHICH baseline went stale
648
+ // sends the reader to read every one of them.
649
+ warnings: runs.flatMap((r) => (r.warnings || []).map((w) => `${r.id}: ${w}`)),
650
+ runs,
651
+ };
652
+
653
+ if (status === 'fail') node.reason = failed[0].reason ?? null;
654
+ else if (status === 'skip') {
655
+ node.reason = runs.length === 1 ? runs[0].reason : 'every test node was skipped';
656
+ }
657
+ if (runs.some((r) => r.timedOut)) node.timedOut = true;
658
+
659
+ return node;
660
+ }
661
+
662
+ /**
663
+ * One feedback line per RED node, named by node id.
664
+ *
665
+ * The `[tests/{id}]` prefix replaces the old bare `[tests]`: with N suites, a
666
+ * line that does not say which one failed sends the reader to the wrong repo
667
+ * half.
668
+ */
243
669
  function appendTestsFeedback(feedback, testsNode) {
670
+ // Warnings are emitted whatever the verdict — a stale baseline lives on a
671
+ // GREEN node by construction (the count rose), so a warning that only
672
+ // printed on failure would never print at all.
673
+ for (const run of testsNode.runs || []) {
674
+ for (const warning of run.warnings || []) {
675
+ feedback.push(`[tests/${run.id}] warning: ${warning}`);
676
+ }
677
+ }
244
678
  if (testsNode.status !== 'fail') return;
245
- for (const line of linesOf(testsNode.output)) {
246
- feedback.push(`[tests] ${line}`);
679
+ for (const run of testsNode.runs || []) {
680
+ if (run.status !== 'fail') continue;
681
+ for (const line of linesOf(run.output || run.reason || '')) {
682
+ feedback.push(`[tests/${run.id}] ${line}`);
683
+ }
247
684
  }
248
685
  }
249
686
 
687
+ /**
688
+ * Why `--refresh-cache` cannot mean what it says, or null when it can.
689
+ *
690
+ * Judged BEFORE the build node, and the ordering is the whole point: a refusal
691
+ * that arrived after the cache was touched would leave a half-rewritten tree
692
+ * behind. Same discipline `finish --merge` learned — validate the precondition
693
+ * before the mutation, never after.
694
+ *
695
+ * Three refusals, all usage errors (exit 2), never verdicts:
696
+ * - no eval node declared: succeeding silently would teach the user to type a
697
+ * flag that does nothing;
698
+ * - `--skip-tests`: the two flags cancel each other, and the winner would be
699
+ * whichever the code happened to check first;
700
+ * - a filter that excludes every eval node: same shape as the first.
701
+ *
702
+ * The MISSING CREDENTIAL is deliberately NOT judged here: it is per node (each
703
+ * declares its own `envFile`), so `evalPreflight` answers it — still before the
704
+ * command runs, and still without touching the tree.
705
+ *
706
+ * @param {string} projectPath
707
+ * @param {{nodes: Array}} testPlan
708
+ * @param {{tokens: string[], raw: string}|null} filter
709
+ * @param {{skipTests: boolean}} opts
710
+ * @returns {string|null}
711
+ */
712
+ function refreshCacheProblem(projectPath, testPlan, filter, { skipTests }) {
713
+ if (skipTests) {
714
+ return '--refresh-cache com --skip-tests: as duas flags se anulam — a regravação da cache É a suíte de eval';
715
+ }
716
+
717
+ const evalNodes = testPlan.nodes.filter((node) => node.runner === EVAL_RUNNER);
718
+ if (evalNodes.length === 0) {
719
+ const declared = testPlan.nodes.map((node) => node.id).join(', ') || '(nenhum)';
720
+ return `--refresh-cache sem nenhum nó \`runner: "evals"\` declarado em project.tests[] — nós declarados: ${declared}`;
721
+ }
722
+
723
+ if (!evalNodes.some((node) => nodeMatchesFilter(node, filter))) {
724
+ return `--refresh-cache com --filter "${filter.raw}", que não casa nenhum nó de eval`
725
+ + ` (nós de eval: ${evalNodes.map((node) => node.id).join(', ')})`;
726
+ }
727
+
728
+ return null;
729
+ }
730
+
731
+ // ============================================================================
732
+ // Baseline recording (--record-baseline)
733
+ // ============================================================================
734
+
735
+ /**
736
+ * Why `--record-baseline` cannot run here, or null when it can.
737
+ *
738
+ * Judged BEFORE the build node, beside the `--filter` check, because both are
739
+ * statements about a command that cannot mean what it says. Paying for a full
740
+ * suite to be told there was nowhere to write the result is the worst possible
741
+ * ordering.
742
+ *
743
+ * The refusal for a project with no `project.tests[]` is deliberate rather
744
+ * than helpful: the nodes the fallback synthesizes are not declared anywhere,
745
+ * so there is no entry to update, and writing one would mean this command
746
+ * inventing the declaration it came to maintain. `config.json` is likewise
747
+ * never created — the framework repository has none on purpose (creating it
748
+ * breaks `doctor --full`), so the honest answer is a named usage error.
749
+ *
750
+ * @param {string} projectPath
751
+ * @param {{ nodes: Array, source: string }} testPlan
752
+ * @param {{ skipTests: boolean }} opts
753
+ * @returns {string|null}
754
+ */
755
+ function baselineTargetProblem(projectPath, testPlan, { skipTests }) {
756
+ if (skipTests) {
757
+ return '--record-baseline com --skip-tests não mede nada — uma baseline vem de uma execução, não de uma omissão';
758
+ }
759
+ if (!existsSync(morphConfigPath(projectPath))) {
760
+ return `--record-baseline não tem onde gravar: ${morphConfigPath(projectPath)} não existe `
761
+ + '(e não será criado — declare project.tests[] antes)';
762
+ }
763
+ if (testPlan.source !== 'config-tests') {
764
+ return '--record-baseline exige project.tests[] declarado em .morph/config/config.json — '
765
+ + `os nós de "${testPlan.source}" são sintetizados pelo fallback e não têm entrada onde gravar`;
766
+ }
767
+ return null;
768
+ }
769
+
770
+ /**
771
+ * Turn a finished tests rollup into a baseline write, or into a refusal.
772
+ *
773
+ * D8 lives here: the number is only ever what a REAL run measured, and only
774
+ * when every node of that run is green. A red run records nothing — writing a
775
+ * lower count over a regression is how the regression becomes the new normal.
776
+ * A PARTIAL run records nothing either: `--filter` and skips leave nodes
777
+ * unmeasured, and a baseline covering half the fleet is a claim about the
778
+ * other half that nobody made.
779
+ *
780
+ * @param {string} projectPath
781
+ * @param {Object} testsNode - The rollup produced by runTestsNode()
782
+ * @returns {{ recorded: boolean, reason: string|null, updates: Array, path?: string }}
783
+ */
784
+ function recordBaselineFrom(projectPath, testsNode) {
785
+ const runs = testsNode.runs || [];
786
+ const refuse = (reason) => ({ recorded: false, reason, updates: [] });
787
+
788
+ if (runs.length === 0) return refuse('nenhum nó de teste executou');
789
+
790
+ const notGreen = runs.filter((r) => r.status !== 'pass');
791
+ if (notGreen.length > 0) {
792
+ const named = notGreen.map((r) => `${r.id} (${r.status}${r.reason ? `: ${r.reason}` : ''})`).join('; ');
793
+ return refuse(
794
+ `execução não está inteiramente verde — ${named}. `
795
+ + 'Gravar baseline de execução vermelha ou parcial congela a regressão'
796
+ );
797
+ }
798
+
799
+ return recordTestBaselines(
800
+ projectPath,
801
+ runs.map((r) => ({ id: r.id, count: r.count }))
802
+ );
803
+ }
804
+
250
805
  // ============================================================================
251
806
  // Validators node
252
807
  // ============================================================================