@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
@@ -0,0 +1,379 @@
1
+ using Morph.AiKit.Rag;
2
+ using Xunit;
3
+
4
+ namespace Morph.AiKit.Tests;
5
+
6
+ /// <summary>
7
+ /// Canarios do <see cref="DeltaIngest"/> (T14). A afirmacao central e uma so, e ela e
8
+ /// <b>demonstrada</b> contra um acervo, nao afirmada: <b>ingerir a mesma coisa duas vezes nao
9
+ /// duplica nada, e mudar um campo atualiza em vez de acrescentar</b>.
10
+ /// </summary>
11
+ public sealed class DeltaIngestTests
12
+ {
13
+ /// <summary>Um documento de acervo: identidade estavel + conteudo que muda.</summary>
14
+ private sealed record Doc(string Slug, string Body);
15
+
16
+ private static string Key(Doc d) => d.Slug;
17
+
18
+ /// <summary>
19
+ /// Hash de conteudo do teste. Nao precisa ser criptografico — precisa mudar quando o
20
+ /// conteudo muda, que e a unica propriedade que o <see cref="DeltaIngest"/> usa.
21
+ /// </summary>
22
+ private static string Hash(Doc d) => $"h:{d.Body.Length}:{d.Body.GetHashCode(StringComparison.Ordinal)}";
23
+
24
+ /// <summary>
25
+ /// Um acervo minusculo que <b>aplica</b> o plano — e a unica forma de a idempotencia ser
26
+ /// medida no que ficou guardado, em vez de nos nomes das listas do plano.
27
+ /// </summary>
28
+ /// <remarks>
29
+ /// Modela um acervo de verdade: <c>Added</c> vira <b>INSERT</b> numa lista de linhas,
30
+ /// <c>Updated</c> vira UPDATE da linha existente e <c>Removed</c> vira DELETE. O indice de
31
+ /// hashes e mantido a parte, como um indice de banco. Isso importa: se o acervo fizesse
32
+ /// upsert de tudo, uma re-ingestao que classificasse errado sairia invisivel — aqui ela
33
+ /// aparece como linha duplicada, que e o defeito real que esta task combate.
34
+ /// </remarks>
35
+ private sealed class Corpus
36
+ {
37
+ private readonly List<Doc> _rows = [];
38
+ private readonly Dictionary<string, string> _index = new(StringComparer.Ordinal);
39
+
40
+ /// <summary>Quantos embeddings foram pagos ao todo. Nao sobe numa ingestao no-op.</summary>
41
+ public int EmbeddingCalls { get; private set; }
42
+
43
+ /// <summary>As linhas guardadas, na ordem em que entraram.</summary>
44
+ public IReadOnlyList<Doc> Rows => _rows;
45
+
46
+ /// <summary>O indice lookup-key para hash, que alimenta o proximo plano.</summary>
47
+ public IReadOnlyDictionary<string, string> Hashes => _index;
48
+
49
+ public DeltaPlan<Doc> Ingest(IEnumerable<Doc> incoming)
50
+ {
51
+ var plan = DeltaIngest.Plan(incoming, Key, Hash, _index);
52
+
53
+ foreach (var doc in plan.Added)
54
+ {
55
+ _rows.Add(doc); // INSERT
56
+ _index[doc.Slug] = Hash(doc);
57
+ EmbeddingCalls++;
58
+ }
59
+
60
+ foreach (var doc in plan.Updated)
61
+ {
62
+ _rows[_rows.FindIndex(r => r.Slug == doc.Slug)] = doc; // UPDATE
63
+ _index[doc.Slug] = Hash(doc);
64
+ EmbeddingCalls++;
65
+ }
66
+
67
+ foreach (var slug in plan.Removed)
68
+ {
69
+ _rows.RemoveAll(r => r.Slug == slug); // DELETE
70
+ _index.Remove(slug);
71
+ }
72
+
73
+ return plan;
74
+ }
75
+ }
76
+
77
+ private static Doc[] ThreeDocs() =>
78
+ [
79
+ new("politica-troca", "Trocas em ate 7 dias."),
80
+ new("prazo-entrega", "Entrega em 3 dias uteis."),
81
+ new("garantia", "Garantia de 12 meses."),
82
+ ];
83
+
84
+ // ---------------------------------------------------------------------------------
85
+ // doneCriteria: os quatro baldes
86
+ // ---------------------------------------------------------------------------------
87
+
88
+ /// <summary>
89
+ /// Os quatro baldes numa unica passada: um item novo, um com hash mudado, um com hash
90
+ /// igual e uma chave que sumiu da origem.
91
+ /// </summary>
92
+ [Fact]
93
+ public void TheFourBucketsAreSortedInASinglePass()
94
+ {
95
+ var existing = new Dictionary<string, string>(StringComparer.Ordinal)
96
+ {
97
+ ["prazo-entrega"] = Hash(new Doc("prazo-entrega", "Entrega em 5 dias uteis.")), // vai mudar
98
+ ["garantia"] = Hash(new Doc("garantia", "Garantia de 12 meses.")), // igual
99
+ ["pagina-morta"] = "h:qualquer", // sumiu
100
+ };
101
+
102
+ var plan = DeltaIngest.Plan(ThreeDocs(), Key, Hash, existing);
103
+
104
+ Assert.Equal(["politica-troca"], plan.Added.Select(d => d.Slug));
105
+ Assert.Equal(["prazo-entrega"], plan.Updated.Select(d => d.Slug));
106
+ Assert.Equal(["garantia"], plan.Unchanged.Select(d => d.Slug));
107
+ Assert.Equal(["pagina-morta"], plan.Removed);
108
+ Assert.False(plan.IsNoOp);
109
+ Assert.Equal(3, plan.WriteCount);
110
+ }
111
+
112
+ /// <summary>Acervo vazio: tudo e novo, nada e removido.</summary>
113
+ [Fact]
114
+ public void AnEmptyCorpusMakesEverythingNew()
115
+ {
116
+ var plan = DeltaIngest.Plan(ThreeDocs(), Key, Hash, new Dictionary<string, string>());
117
+
118
+ Assert.Equal(3, plan.Added.Count);
119
+ Assert.Empty(plan.Updated);
120
+ Assert.Empty(plan.Unchanged);
121
+ Assert.Empty(plan.Removed);
122
+ }
123
+
124
+ /// <summary>
125
+ /// Remessa vazia com acervo cheio: <b>tudo</b> sai em <c>Removed</c>. E o comportamento
126
+ /// correto e tambem o mais perigoso — e por isso o plano e um plano: quem decide se
127
+ /// aplica um <c>Removed</c> de 100% e o projeto, que sabe se a origem realmente esvaziou
128
+ /// ou se o crawler falhou.
129
+ /// </summary>
130
+ [Fact]
131
+ public void AnEmptyShipmentAgainstAFullCorpusRemovesEverything()
132
+ {
133
+ var corpus = new Corpus();
134
+ corpus.Ingest(ThreeDocs());
135
+
136
+ var plan = DeltaIngest.Plan(Array.Empty<Doc>(), Key, Hash, corpus.Hashes);
137
+
138
+ Assert.Equal(3, plan.Removed.Count);
139
+ Assert.Empty(plan.Added);
140
+ Assert.Empty(plan.Updated);
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------------
144
+ // doneCriteria: IDEMPOTENCIA, medida no acervo
145
+ // ---------------------------------------------------------------------------------
146
+
147
+ /// <summary>
148
+ /// <b>A prova principal.</b> Ingerir a mesma remessa duas vezes: na segunda o plano e
149
+ /// vazio nos tres baldes de escrita, o acervo tem exatamente os mesmos documentos e
150
+ /// <b>nenhum embedding a mais foi pago</b>.
151
+ /// </summary>
152
+ [Fact]
153
+ public void IngestingTheSameShipmentTwiceLeavesTheCorpusUntouched()
154
+ {
155
+ var corpus = new Corpus();
156
+ var shipment = ThreeDocs();
157
+
158
+ var first = corpus.Ingest(shipment);
159
+
160
+ Assert.Equal(3, first.Added.Count);
161
+ Assert.Equal(3, corpus.EmbeddingCalls);
162
+
163
+ var before = corpus.Rows.OrderBy(d => d.Slug, StringComparer.Ordinal).ToArray();
164
+
165
+ var second = corpus.Ingest(shipment);
166
+
167
+ Assert.True(second.IsNoOp);
168
+ Assert.Equal(0, second.WriteCount);
169
+ Assert.Equal(3, second.Unchanged.Count);
170
+
171
+ // O acervo e o custo nao se mexeram: 3 documentos, os mesmos, 3 embeddings ao todo.
172
+ Assert.Equal(3, corpus.Rows.Count); // <- a linha que morre se a mesma coisa duplicar
173
+ Assert.Equal(before, corpus.Rows.OrderBy(d => d.Slug, StringComparer.Ordinal));
174
+ Assert.Equal(3, corpus.EmbeddingCalls);
175
+
176
+ // E uma terceira, para que "duas vezes" nao pareca coincidencia.
177
+ Assert.True(corpus.Ingest(shipment).IsNoOp);
178
+ Assert.Equal(3, corpus.EmbeddingCalls);
179
+ }
180
+
181
+ /// <summary>
182
+ /// <b>Mudar um campo atualiza, nao acrescenta.</b> A contagem do acervo nao sobe, o slug
183
+ /// continua unico e o corpo e o novo — que e a diferenca entre uma ingestao delta e a
184
+ /// ingestao apaga-tudo que este tipo existe para substituir.
185
+ /// </summary>
186
+ [Fact]
187
+ public void ChangingOneFieldUpdatesInPlaceInsteadOfAppending()
188
+ {
189
+ var corpus = new Corpus();
190
+ corpus.Ingest(ThreeDocs());
191
+
192
+ Doc[] revised =
193
+ [
194
+ new("politica-troca", "Trocas em ate 7 dias."),
195
+ new("prazo-entrega", "Entrega em 1 dia util."), // <- unico campo alterado
196
+ new("garantia", "Garantia de 12 meses."),
197
+ ];
198
+
199
+ var plan = corpus.Ingest(revised);
200
+
201
+ Assert.Equal(["prazo-entrega"], plan.Updated.Select(d => d.Slug));
202
+ Assert.Empty(plan.Added);
203
+ Assert.Empty(plan.Removed);
204
+ Assert.Equal(2, plan.Unchanged.Count);
205
+
206
+ Assert.Equal(3, corpus.Rows.Count); // nao apendou
207
+ Assert.Single(corpus.Rows, d => d.Slug == "prazo-entrega"); // uma so linha
208
+ Assert.Equal("Entrega em 1 dia util.", corpus.Rows.Single(d => d.Slug == "prazo-entrega").Body);
209
+ Assert.Equal(4, corpus.EmbeddingCalls); // 3 iniciais + 1 mudanca
210
+
211
+ // E a re-ingestao da remessa revisada volta a ser no-op.
212
+ Assert.True(corpus.Ingest(revised).IsNoOp);
213
+ Assert.Equal(4, corpus.EmbeddingCalls);
214
+ }
215
+
216
+ /// <summary>
217
+ /// A mesma afirmacao dita como <b>contagem de linhas</b>, e antes de qualquer outra
218
+ /// asserçao: re-ingerir nao insere uma segunda copia. E a linha que fica vermelha com
219
+ /// "6" no lugar de "3" quando a comparacao por lookup-key se quebra — a duplicacao
220
+ /// literal, e nao um nome de balde trocado.
221
+ /// </summary>
222
+ [Fact]
223
+ public void ReIngestingDoesNotInsertASecondCopyOfTheSameDocument()
224
+ {
225
+ var corpus = new Corpus();
226
+ var shipment = ThreeDocs();
227
+
228
+ corpus.Ingest(shipment);
229
+ corpus.Ingest(shipment);
230
+
231
+ Assert.Equal(3, corpus.Rows.Count);
232
+ Assert.Equal(3, corpus.Rows.Select(d => d.Slug).Distinct(StringComparer.Ordinal).Count());
233
+ Assert.Equal(3, corpus.EmbeddingCalls);
234
+ }
235
+
236
+ /// <summary>
237
+ /// Idempotencia sobrevive a reordenacao: a mesma remessa embaralhada continua sendo
238
+ /// no-op. A identidade e a chave, nunca a posicao na lista.
239
+ /// </summary>
240
+ [Fact]
241
+ public void OrderDoesNotMakeAShipmentLookDifferent()
242
+ {
243
+ var corpus = new Corpus();
244
+ corpus.Ingest(ThreeDocs());
245
+
246
+ var shuffled = ThreeDocs().Reverse().ToArray();
247
+
248
+ Assert.True(corpus.Ingest(shuffled).IsNoOp);
249
+ Assert.Equal(3, corpus.EmbeddingCalls);
250
+ }
251
+
252
+ // ---------------------------------------------------------------------------------
253
+ // doneCriteria: chave duplicada falha de forma explicita
254
+ // ---------------------------------------------------------------------------------
255
+
256
+ /// <summary>
257
+ /// Chave repetida derruba o plano dizendo a chave <b>e as duas posicoes</b>. Ficar com o
258
+ /// ultimo gravaria um dos dois ao acaso; deduplicar sozinho esconderia o defeito da
259
+ /// origem, que e onde ele tem conserto.
260
+ /// </summary>
261
+ [Fact]
262
+ public void ADuplicateLookupKeyFailsNamingTheKeyAndBothPositions()
263
+ {
264
+ Doc[] shipment =
265
+ [
266
+ new("politica-troca", "primeira versao"),
267
+ new("prazo-entrega", "irrelevante"),
268
+ new("politica-troca", "segunda versao"),
269
+ ];
270
+
271
+ var failure = Assert.Throws<DuplicateLookupKeyException>(
272
+ () => DeltaIngest.Plan(shipment, Key, Hash, new Dictionary<string, string>()));
273
+
274
+ Assert.Equal("politica-troca", failure.LookupKey);
275
+ Assert.Equal(0, failure.FirstIndex);
276
+ Assert.Equal(2, failure.DuplicateIndex);
277
+ Assert.Contains("deixa de ser deterministico", failure.Message, StringComparison.Ordinal);
278
+ }
279
+
280
+ /// <summary>Chave em branco colapsaria itens distintos num so — e recusada.</summary>
281
+ [Theory]
282
+ [InlineData("")]
283
+ [InlineData(" ")]
284
+ public void ABlankLookupKeyIsRefused(string blank)
285
+ {
286
+ var failure = Assert.Throws<ArgumentException>(
287
+ () => DeltaIngest.Plan([new Doc(blank, "corpo")], Key, Hash, new Dictionary<string, string>()));
288
+
289
+ Assert.Contains("posicao 0", failure.Message, StringComparison.Ordinal);
290
+ }
291
+
292
+ /// <summary>
293
+ /// Hash em branco e recusado: sem versao nao ha como separar "nao mudou" de "mudou", e
294
+ /// tratar a ausencia como "mudou" reintroduz o re-embedding de tudo — o defeito original.
295
+ /// </summary>
296
+ [Fact]
297
+ public void ABlankContentHashIsRefusedInsteadOfMeaningChanged()
298
+ {
299
+ var failure = Assert.Throws<ArgumentException>(
300
+ () => DeltaIngest.Plan(ThreeDocs(), Key, _ => string.Empty, new Dictionary<string, string>()));
301
+
302
+ Assert.Contains("politica-troca", failure.Message, StringComparison.Ordinal);
303
+ }
304
+
305
+ /// <summary>Argumento nulo e defeito de chamada, e falha na chamada.</summary>
306
+ [Fact]
307
+ public void TheRequiredArgumentsAreNotOptional()
308
+ {
309
+ var empty = new Dictionary<string, string>();
310
+
311
+ Assert.Throws<ArgumentNullException>(() => DeltaIngest.Plan<Doc>(null!, Key, Hash, empty));
312
+ Assert.Throws<ArgumentNullException>(() => DeltaIngest.Plan(ThreeDocs(), null!, Hash, empty));
313
+ Assert.Throws<ArgumentNullException>(() => DeltaIngest.Plan(ThreeDocs(), Key, null!, empty));
314
+ Assert.Throws<ArgumentNullException>(() => DeltaIngest.Plan(ThreeDocs(), Key, Hash, null!));
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------------
318
+ // Identidade e versao sao perguntas diferentes
319
+ // ---------------------------------------------------------------------------------
320
+
321
+ /// <summary>
322
+ /// A comparacao de chave e <b>ordinal</b>: <c>Politica</c> e <c>politica</c> sao dois
323
+ /// documentos. Deixar a cultura decidir faria o mesmo corpus produzir planos diferentes
324
+ /// conforme a maquina que roda a ingestao.
325
+ /// </summary>
326
+ [Fact]
327
+ public void KeysAreComparedOrdinallySoThePlanDoesNotDependOnTheMachine()
328
+ {
329
+ var existing = new Dictionary<string, string>(StringComparer.Ordinal)
330
+ {
331
+ ["politica-troca"] = Hash(new Doc("politica-troca", "Trocas em ate 7 dias.")),
332
+ };
333
+
334
+ var plan = DeltaIngest.Plan(
335
+ [new Doc("Politica-Troca", "Trocas em ate 7 dias.")],
336
+ Key,
337
+ Hash,
338
+ existing);
339
+
340
+ Assert.Equal(["Politica-Troca"], plan.Added.Select(d => d.Slug));
341
+ Assert.Equal(["politica-troca"], plan.Removed);
342
+ }
343
+
344
+ /// <summary>
345
+ /// O plano nao le o corpo do item: um documento cuja chave e hash nao mudaram e
346
+ /// <c>Unchanged</c> ainda que o objeto seja outra instancia. E o que permite ao acervo
347
+ /// reprocessar so o que mudou de verdade.
348
+ /// </summary>
349
+ [Fact]
350
+ public void EqualityIsDecidedByKeyAndHashNotByInstance()
351
+ {
352
+ var corpus = new Corpus();
353
+ corpus.Ingest(ThreeDocs());
354
+
355
+ var reconstructed = ThreeDocs().Select(d => new Doc(d.Slug, d.Body)).ToArray();
356
+
357
+ Assert.True(corpus.Ingest(reconstructed).IsNoOp);
358
+ }
359
+
360
+ /// <summary>A remessa e enumerada uma unica vez — importa quando ela vem de um stream.</summary>
361
+ [Fact]
362
+ public void TheShipmentIsEnumeratedExactlyOnce()
363
+ {
364
+ var passes = 0;
365
+
366
+ IEnumerable<Doc> Once()
367
+ {
368
+ passes++;
369
+ foreach (var doc in ThreeDocs())
370
+ {
371
+ yield return doc;
372
+ }
373
+ }
374
+
375
+ DeltaIngest.Plan(Once(), Key, Hash, new Dictionary<string, string>());
376
+
377
+ Assert.Equal(1, passes);
378
+ }
379
+ }
@@ -0,0 +1,197 @@
1
+ using Microsoft.Extensions.AI;
2
+ using Morph.AiKit.Evals;
3
+ using Xunit;
4
+
5
+ namespace Morph.AiKit.Tests.Evals;
6
+
7
+ /// <summary>
8
+ /// A REGUA, testada contra a resposta ERRADA.
9
+ /// </summary>
10
+ /// <remarks>
11
+ /// <para>
12
+ /// Este arquivo nasceu de um achado do Gate 3: das seis <see cref="EvalAssertionKind"/>,
13
+ /// afrouxar CINCO para sempre-passar nao matava um unico teste. A suite continuava
14
+ /// "152 total, 0 falhou" com o motor de assercao praticamente inerte — e duas das cinco
15
+ /// (<c>NoPii</c> e <c>JsonHasKey</c>) sao usadas pelo dataset commitado, ou seja, os quatro
16
+ /// cenarios exemplares dependiam de reguas que nunca haviam sido provadas.
17
+ /// </para>
18
+ /// <para>
19
+ /// A causa e a mesma de todo canario inerte desta base: testava-se que a assercao APROVA o
20
+ /// que deve aprovar, nunca que ela REPROVA o que deve reprovar. Uma regua so separa duas
21
+ /// coisas se for exercitada dos DOIS lados — e a que so viu o lado bom e indistinguivel de
22
+ /// <c>return true</c>.
23
+ /// </para>
24
+ /// <para>
25
+ /// Por isso cada teste aqui carrega o PAR: a resposta que passa e a resposta que deveria
26
+ /// reprovar. Afrouxar qualquer <see cref="EvalAssertionKind"/> mata o teste que a nomeia.
27
+ /// </para>
28
+ /// </remarks>
29
+ public sealed class EvalAssertionTests
30
+ {
31
+ /// <summary>Resposta so de texto — a superficie que as assercoes de texto leem.</summary>
32
+ private static ChatResponse Text(string text)
33
+ => new(new ChatMessage(ChatRole.Assistant, text));
34
+
35
+ /// <summary>Resposta que escolheu uma ferramenta, com argumentos.</summary>
36
+ private static ChatResponse Tool(string name, params (string Key, object? Value)[] args)
37
+ {
38
+ var arguments = new Dictionary<string, object?>(StringComparer.Ordinal);
39
+ foreach (var (key, value) in args)
40
+ {
41
+ arguments[key] = value;
42
+ }
43
+
44
+ var message = new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call-1", name, arguments)]);
45
+ return new ChatResponse(message);
46
+ }
47
+
48
+ // ---------------------------------------------------------------- Contains
49
+
50
+ [Fact]
51
+ public void ContainsSeparatesTheAnswerThatCarriesTheTextFromTheOneThatDoesNot()
52
+ {
53
+ var assertion = new EvalAssertion { Name = "saudacao", Kind = EvalAssertionKind.Contains, Value = "Bom dia" };
54
+
55
+ Assert.True(assertion.Evaluate(Text("Bom dia! Como posso ajudar?")).Passed);
56
+
57
+ var reproved = assertion.Evaluate(Text("Boa noite! Como posso ajudar?"));
58
+ Assert.False(reproved.Passed, "Contains aceitou uma resposta que NAO contem o texto");
59
+ Assert.Contains("Bom dia", reproved.Reason, StringComparison.Ordinal);
60
+ }
61
+
62
+ [Fact]
63
+ public void ContainsIgnoresCaseButNotAbsence()
64
+ {
65
+ var assertion = new EvalAssertion { Name = "saudacao", Kind = EvalAssertionKind.Contains, Value = "bom dia" };
66
+
67
+ Assert.True(assertion.Evaluate(Text("BOM DIA")).Passed);
68
+ Assert.False(assertion.Evaluate(Text("ola")).Passed);
69
+ }
70
+
71
+ // ------------------------------------------------------------- NotContains
72
+
73
+ [Fact]
74
+ public void NotContainsSeparatesTheCleanAnswerFromTheOneThatLeaksTheForbiddenText()
75
+ {
76
+ var assertion = new EvalAssertion { Name = "sem-preco", Kind = EvalAssertionKind.NotContains, Value = "desconto" };
77
+
78
+ Assert.True(assertion.Evaluate(Text("Segue o orcamento solicitado.")).Passed);
79
+
80
+ var reproved = assertion.Evaluate(Text("Segue o orcamento com desconto de 20%."));
81
+ Assert.False(reproved.Passed, "NotContains aceitou uma resposta que CONTEM o texto proibido");
82
+ Assert.Contains("desconto", reproved.Reason, StringComparison.Ordinal);
83
+ }
84
+
85
+ // -------------------------------------------------------------- JsonHasKey
86
+
87
+ [Fact]
88
+ public void JsonHasKeySeparatesTheObjectThatCarriesTheKeyFromTheOneThatDoesNot()
89
+ {
90
+ var assertion = new EvalAssertion { Name = "tem-total", Kind = EvalAssertionKind.JsonHasKey, Target = "total" };
91
+
92
+ Assert.True(assertion.Evaluate(Text("""{"total": 1200, "moeda": "BRL"}""")).Passed);
93
+
94
+ var reproved = assertion.Evaluate(Text("""{"moeda": "BRL"}"""));
95
+ Assert.False(reproved.Passed, "JsonHasKey aceitou um objeto SEM a chave exigida");
96
+ Assert.Contains("total", reproved.Reason, StringComparison.Ordinal);
97
+ }
98
+
99
+ [Fact]
100
+ public void JsonHasKeyReprovesWhatIsNotJsonAtAll()
101
+ {
102
+ var assertion = new EvalAssertion { Name = "tem-total", Kind = EvalAssertionKind.JsonHasKey, Target = "total" };
103
+
104
+ var reproved = assertion.Evaluate(Text("O total e mil e duzentos reais."));
105
+ Assert.False(reproved.Passed, "JsonHasKey aceitou uma resposta que nem JSON e");
106
+ Assert.Contains("JSON", reproved.Reason, StringComparison.Ordinal);
107
+ }
108
+
109
+ [Fact]
110
+ public void JsonHasKeyReprovesAJsonArrayEvenWhenItParses()
111
+ {
112
+ // Um array parseia como JSON valido: se a regua so perguntasse "parseia?", ela
113
+ // aprovaria uma resposta em que a chave nao pode nem existir.
114
+ var assertion = new EvalAssertion { Name = "tem-total", Kind = EvalAssertionKind.JsonHasKey, Target = "total" };
115
+
116
+ Assert.False(assertion.Evaluate(Text("""[1, 2, 3]""")).Passed);
117
+ }
118
+
119
+ // ------------------------------------------------------------------- NoPii
120
+
121
+ [Theory]
122
+ [InlineData("Entre em contato pelo lucas@example.com quando puder.", "e-mail")]
123
+ [InlineData("O CPF do titular e 123.456.789-01.", "CPF")]
124
+ [InlineData("Ligue para (11) 98765-4321.", "telefone")]
125
+ public void NoPiiReprovesEachFormOfPersonalDataItClaimsToRecognize(string leaky, string label)
126
+ {
127
+ var assertion = new EvalAssertion { Name = "sem-pii", Kind = EvalAssertionKind.NoPii };
128
+
129
+ var reproved = assertion.Evaluate(Text(leaky));
130
+ Assert.False(reproved.Passed, $"NoPii aceitou uma resposta que carrega {label}");
131
+ Assert.Contains(label, reproved.Reason, StringComparison.Ordinal);
132
+ }
133
+
134
+ [Fact]
135
+ public void NoPiiApprovesTheAnswerWithoutPersonalData()
136
+ {
137
+ var assertion = new EvalAssertion { Name = "sem-pii", Kind = EvalAssertionKind.NoPii };
138
+
139
+ Assert.True(assertion.Evaluate(Text("Bom dia! Como posso ajudar voce hoje?")).Passed);
140
+ }
141
+
142
+ // -------------------------------------------------------------- ToolCalled
143
+
144
+ [Fact]
145
+ public void ToolCalledSeparatesTheRightToolFromAnotherToolAndFromNoToolAtAll()
146
+ {
147
+ var assertion = new EvalAssertion { Name = "escalou", Kind = EvalAssertionKind.ToolCalled, Target = "escalar_humano" };
148
+
149
+ Assert.True(assertion.Evaluate(Tool("escalar_humano")).Passed);
150
+
151
+ var outraFerramenta = assertion.Evaluate(Tool("consultar_estoque"));
152
+ Assert.False(outraFerramenta.Passed, "ToolCalled aceitou a escolha de OUTRA ferramenta");
153
+ Assert.Contains("escalar_humano", outraFerramenta.Reason, StringComparison.Ordinal);
154
+
155
+ Assert.False(assertion.Evaluate(Text("vou escalar para um humano")).Passed);
156
+ }
157
+
158
+ // ------------------------------------------------------------ ToolArgument
159
+
160
+ [Fact]
161
+ public void ToolArgumentSeparatesTheRightArgumentFromTheWrongValueAndTheMissingKey()
162
+ {
163
+ var assertion = new EvalAssertion
164
+ {
165
+ Name = "prioridade-alta",
166
+ Kind = EvalAssertionKind.ToolArgument,
167
+ Target = "escalar_humano",
168
+ Value = "prioridade=alta",
169
+ };
170
+
171
+ Assert.True(assertion.Evaluate(Tool("escalar_humano", ("prioridade", "alta"))).Passed);
172
+
173
+ var valorErrado = assertion.Evaluate(Tool("escalar_humano", ("prioridade", "baixa")));
174
+ Assert.False(valorErrado.Passed, "ToolArgument aceitou o argumento com o VALOR errado");
175
+ Assert.Contains("prioridade=alta", valorErrado.Reason, StringComparison.Ordinal);
176
+
177
+ Assert.False(
178
+ assertion.Evaluate(Tool("escalar_humano", ("motivo", "alta"))).Passed,
179
+ "ToolArgument aceitou uma chamada sem a CHAVE exigida");
180
+ }
181
+
182
+ // ------------------------------------- o veredito diz QUEM julgou (B2)
183
+
184
+ [Fact]
185
+ public void EveryOutcomeCarriesTheNameAndTheKindOfTheAssertionThatProducedIt()
186
+ {
187
+ // Sem este carimbo, uma falha chega a jusante como contador e o sumario nao consegue
188
+ // nomear nada — foi assim que o eval mordia anonimamente.
189
+ var assertion = new EvalAssertion { Name = "saudacao", Kind = EvalAssertionKind.Contains, Value = "Bom dia" };
190
+
191
+ foreach (var outcome in new[] { assertion.Evaluate(Text("Bom dia")), assertion.Evaluate(Text("nada")) })
192
+ {
193
+ Assert.Equal("saudacao", outcome.Assertion);
194
+ Assert.Equal(EvalAssertionKind.Contains, outcome.Kind);
195
+ }
196
+ }
197
+ }