@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,260 @@
1
+ using System.Collections;
2
+ using System.Reflection;
3
+ using System.Text.Json;
4
+ using Microsoft.Extensions.AI;
5
+ using Morph.AiKit.Agents;
6
+ using Xunit;
7
+
8
+ namespace Morph.AiKit.Tests;
9
+
10
+ /// <summary>
11
+ /// Canarios do agente-como-dado (T7). O que estes testes protegem nao e a existencia dos
12
+ /// tipos — o compilador ja faz isso — e sim a UNICA regra estrutural que
13
+ /// <see cref="AgentSpecOptions"/> tem: <b>nulo nao envia nada ao provider</b>.
14
+ /// </summary>
15
+ /// <remarks>
16
+ /// A comparacao e feita por reflexao contra um <see cref="ChatOptions"/> virgem, e nao
17
+ /// contra uma lista de propriedades escrita a mao, de proposito: uma propriedade nova no
18
+ /// <c>Microsoft.Extensions.AI</c> entra na comparacao sozinha. Uma lista escrita a mao
19
+ /// envelheceria em silencio no proximo bump do pin — que e exatamente o defeito que a
20
+ /// feature inteira existe para matar.
21
+ /// </remarks>
22
+ public sealed class AgentSpecTests
23
+ {
24
+ // ---------------------------------------------------------------------------------
25
+ // A regra estrutural: nulo = nao envia
26
+ // ---------------------------------------------------------------------------------
27
+
28
+ [Fact]
29
+ public void FreshOptionsProduceChatOptionsIndistinguishableFromPristine()
30
+ {
31
+ var produced = new AgentSpecOptions().ToChatOptions();
32
+
33
+ var differing = DiffAgainstPristine(produced);
34
+
35
+ Assert.True(
36
+ differing.Count == 0,
37
+ $"AgentSpecOptions recem-construida NAO pode enviar opcao nenhuma. Enviou: {string.Join(", ", differing)}");
38
+ }
39
+
40
+ [Fact]
41
+ public void EveryAgentSpecOptionsPropertyIsNullOnAFreshInstance()
42
+ {
43
+ var fresh = new AgentSpecOptions();
44
+
45
+ var nonNull = typeof(AgentSpecOptions)
46
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance)
47
+ .Where(p => p.GetIndexParameters().Length == 0)
48
+ .Where(p => p.GetValue(fresh) is not null)
49
+ .Select(p => p.Name)
50
+ .ToList();
51
+
52
+ Assert.True(
53
+ nonNull.Count == 0,
54
+ $"Toda propriedade de AgentSpecOptions nasce nula. Nasceram preenchidas: {string.Join(", ", nonNull)}");
55
+ }
56
+
57
+ [Fact]
58
+ public void SettingOnlyTemperatureSendsOnlyTemperature()
59
+ {
60
+ var produced = new AgentSpecOptions { Temperature = 0.2f }.ToChatOptions();
61
+
62
+ var differing = DiffAgainstPristine(produced);
63
+
64
+ Assert.Equal(new[] { nameof(ChatOptions.Temperature) }, differing);
65
+ Assert.Equal(0.2f, produced.Temperature);
66
+ }
67
+
68
+ [Fact]
69
+ public void AgentLevelKnobsNeverLeakIntoChatOptions()
70
+ {
71
+ // As quatro opcoes de nivel de agente. Nenhuma tem casa em ChatOptions: elas sao
72
+ // lidas pela AgentFactory (T10) e pela telemetria (T12). Se alguem "resolver" isso
73
+ // despejando-as em AdditionalProperties, este teste morre — que e o ponto.
74
+ var produced = new AgentSpecOptions
75
+ {
76
+ MaxToolIterations = 7,
77
+ AllowConcurrentToolInvocation = true,
78
+ TimeoutSeconds = 30,
79
+ EnableSensitiveTelemetry = true,
80
+ }.ToChatOptions();
81
+
82
+ var differing = DiffAgainstPristine(produced);
83
+
84
+ Assert.True(
85
+ differing.Count == 0,
86
+ $"Opcao de nivel de agente vazou para ChatOptions: {string.Join(", ", differing)}");
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------------
90
+ // Mapeamento de cada opcao de nivel de requisicao para a sua casa
91
+ // ---------------------------------------------------------------------------------
92
+
93
+ [Fact]
94
+ public void EachRequestLevelOptionLandsInItsOwnChatOptionsHome()
95
+ {
96
+ var options = new AgentSpecOptions
97
+ {
98
+ Temperature = 0.35f,
99
+ TopP = 0.9f,
100
+ MaxOutputTokens = 512,
101
+ Seed = 1234,
102
+ StopSequences = ["<END>"],
103
+ ToolMode = ChatToolMode.RequireAny,
104
+ ReasoningEffort = ReasoningEffort.Medium,
105
+ ServiceTier = "flex",
106
+ };
107
+
108
+ var produced = options.ToChatOptions();
109
+
110
+ Assert.Equal(0.35f, produced.Temperature);
111
+ Assert.Equal(0.9f, produced.TopP);
112
+ Assert.Equal(512, produced.MaxOutputTokens);
113
+ Assert.Equal(1234L, produced.Seed);
114
+ Assert.Equal(["<END>"], produced.StopSequences);
115
+ Assert.Same(ChatToolMode.RequireAny, produced.ToolMode);
116
+ Assert.Equal(ReasoningEffort.Medium, produced.Reasoning?.Effort);
117
+
118
+ // service tier NAO existe em ChatOptions no Microsoft.Extensions.AI 10.9.0 (apurado
119
+ // por reflexao sobre o assembly do pin) — viaja em AdditionalProperties.
120
+ Assert.NotNull(produced.AdditionalProperties);
121
+ Assert.True(produced.AdditionalProperties.TryGetValue(
122
+ AgentSpecOptions.ServiceTierPropertyName,
123
+ out var serviceTier));
124
+ Assert.Equal("flex", serviceTier);
125
+ }
126
+
127
+ [Fact]
128
+ public void AnExplicitlyEmptyStopSequenceListIsSentAsWritten()
129
+ {
130
+ // "Nulo nao envia; nao-nulo envia o que voce escreveu." Tratar lista vazia como
131
+ // nula seria uma excecao escondida na unica regra do tipo.
132
+ var produced = new AgentSpecOptions { StopSequences = [] }.ToChatOptions();
133
+
134
+ Assert.Equal([nameof(ChatOptions.StopSequences)], DiffAgainstPristine(produced));
135
+ Assert.NotNull(produced.StopSequences);
136
+ Assert.Empty(produced.StopSequences);
137
+ }
138
+
139
+ [Fact]
140
+ public void ApplyToNeverClobbersWhatTheCallerAlreadySet()
141
+ {
142
+ // A AgentFactory (T10) preenche Tools e ResponseFormat; ApplyTo nao pode apaga-los.
143
+ var destination = new ChatOptions
144
+ {
145
+ ResponseFormat = ChatResponseFormat.Json,
146
+ ModelId = "already-chosen",
147
+ };
148
+
149
+ var returned = new AgentSpecOptions { Temperature = 0.1f }.ApplyTo(destination);
150
+
151
+ Assert.Same(destination, returned);
152
+ Assert.Same(ChatResponseFormat.Json, destination.ResponseFormat);
153
+ Assert.Equal("already-chosen", destination.ModelId);
154
+ Assert.Equal(0.1f, destination.Temperature);
155
+ }
156
+
157
+ [Fact]
158
+ public void ApplyToRejectsANullDestination()
159
+ => Assert.Throws<ArgumentNullException>(() => new AgentSpecOptions().ApplyTo(null!));
160
+
161
+ // ---------------------------------------------------------------------------------
162
+ // AgentSpec
163
+ // ---------------------------------------------------------------------------------
164
+
165
+ [Fact]
166
+ public void AFreshSpecCarriesNoToolsNoSchemaAndSendsNoOptions()
167
+ {
168
+ var spec = new AgentSpec { Alias = "text-default", Instructions = "Seja util." };
169
+
170
+ Assert.Empty(spec.Tools);
171
+ Assert.Null(spec.OutputSchema);
172
+ Assert.Null(spec.Api);
173
+ Assert.NotNull(spec.Options);
174
+ Assert.Empty(DiffAgainstPristine(spec.Options.ToChatOptions()));
175
+ }
176
+
177
+ [Theory]
178
+ [InlineData("")]
179
+ [InlineData(" ")]
180
+ public void ABlankAliasIsRejectedAtConstruction(string blank)
181
+ => Assert.Throws<ArgumentException>(
182
+ () => new AgentSpec { Alias = blank, Instructions = "Seja util." });
183
+
184
+ [Theory]
185
+ [InlineData("")]
186
+ [InlineData(" ")]
187
+ public void BlankInstructionsAreRejectedAtConstruction(string blank)
188
+ => Assert.Throws<ArgumentException>(
189
+ () => new AgentSpec { Alias = "text-default", Instructions = blank });
190
+
191
+ [Fact]
192
+ public void TheSpecApiWinsWhenPresentAndTheAliasWinsWhenItIsNull()
193
+ {
194
+ var withoutOverride = new AgentSpec { Alias = "a", Instructions = "i" };
195
+ var withOverride = withoutOverride with { Api = AgentApi.Chat };
196
+
197
+ // alias manda quando a spec nao opinou
198
+ Assert.Equal(AgentApi.Responses, withoutOverride.ResolveApi(AgentApi.Responses));
199
+ Assert.Equal(AgentApi.Chat, withoutOverride.ResolveApi(AgentApi.Chat));
200
+
201
+ // spec manda quando opinou — inclusive contra o alias
202
+ Assert.Equal(AgentApi.Chat, withOverride.ResolveApi(AgentApi.Responses));
203
+ }
204
+
205
+ [Fact]
206
+ public void ResponsesIsTheDefaultOfTheEnumBecauseItIsTheHouseDefault()
207
+ => Assert.Equal(AgentApi.Responses, default(AgentApi));
208
+
209
+ [Fact]
210
+ public void AnOutputSchemaSurvivesAsLongAsItsDocumentDoes()
211
+ {
212
+ using var document = JsonDocument.Parse("""{"type":"object"}""");
213
+
214
+ var spec = new AgentSpec
215
+ {
216
+ Alias = "text-default",
217
+ Instructions = "Responda em JSON.",
218
+ OutputSchema = document.RootElement.Clone(),
219
+ };
220
+
221
+ Assert.Equal(JsonValueKind.Object, spec.OutputSchema!.Value.ValueKind);
222
+ Assert.Equal("object", spec.OutputSchema.Value.GetProperty("type").GetString());
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------------
226
+ // Helpers
227
+ // ---------------------------------------------------------------------------------
228
+
229
+ /// <summary>
230
+ /// Nomes das propriedades de <see cref="ChatOptions"/> em que <paramref name="actual"/>
231
+ /// difere de um <see cref="ChatOptions"/> virgem — ou seja, o conjunto exato do que
232
+ /// seria enviado ao provider.
233
+ /// </summary>
234
+ private static IReadOnlyList<string> DiffAgainstPristine(ChatOptions actual)
235
+ {
236
+ var pristine = new ChatOptions();
237
+
238
+ return [.. typeof(ChatOptions)
239
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance)
240
+ .Where(p => p.GetIndexParameters().Length == 0 && p.CanRead)
241
+ .Where(p => !ValuesEqual(p.GetValue(actual), p.GetValue(pristine)))
242
+ .Select(p => p.Name)
243
+ .OrderBy(name => name, StringComparer.Ordinal)];
244
+ }
245
+
246
+ private static bool ValuesEqual(object? left, object? right)
247
+ {
248
+ if (left is null || right is null)
249
+ {
250
+ return left is null && right is null;
251
+ }
252
+
253
+ if (left is IEnumerable leftItems and not string && right is IEnumerable rightItems and not string)
254
+ {
255
+ return leftItems.Cast<object?>().SequenceEqual(rightItems.Cast<object?>());
256
+ }
257
+
258
+ return Equals(left, right);
259
+ }
260
+ }
@@ -0,0 +1,405 @@
1
+ using System.Collections.Concurrent;
2
+ using Microsoft.Agents.AI;
3
+ using Microsoft.Extensions.AI;
4
+ using Morph.AiKit.Conversation;
5
+ using Morph.AiKit.Tests.Fakes;
6
+ using Xunit;
7
+
8
+ namespace Morph.AiKit.Tests;
9
+
10
+ /// <summary>
11
+ /// Canarios do <see cref="ConversationStore"/> (T13). A afirmacao central: <b>uma unica
12
+ /// instancia de provider atende todas as sessoes, e o historico de uma conversa nunca
13
+ /// aparece em outra</b> — porque o estado por sessao vive na
14
+ /// <see cref="AgentSession"/> (via <see cref="ProviderSessionState{TState}"/>), jamais num
15
+ /// campo do provider.
16
+ /// </summary>
17
+ /// <remarks>
18
+ /// Os testes rodam pelo caminho <b>real</b> do MAF: um <see cref="ChatClientAgent"/> de
19
+ /// verdade, com <c>ChatClientAgentOptions.ChatHistoryProvider</c> apontando para o
20
+ /// <see cref="ConversationStore"/>, sessoes criadas por <c>CreateSessionAsync</c> e voltas
21
+ /// disparadas por <c>RunAsync</c>. O unico fake e o <see cref="FakeChatClient"/> no lugar do
22
+ /// provider de modelo — e por isso "o provider e chamado" e um fato observado, e nao uma
23
+ /// chamada manual ao metodo protegido fingindo ser o framework.
24
+ /// </remarks>
25
+ public sealed class ConversationStoreTests
26
+ {
27
+ private static ChatClientAgent Agent(FakeChatClient client, ConversationStore history)
28
+ => new(client, new ChatClientAgentOptions
29
+ {
30
+ Name = "atendente",
31
+ ChatOptions = new ChatOptions { Instructions = "Voce e um atendente." },
32
+ ChatHistoryProvider = history,
33
+ });
34
+
35
+ private static CancellationToken Ct => TestContext.Current.CancellationToken;
36
+
37
+ private static string[] TextsOf(IEnumerable<ChatMessage> messages)
38
+ => [.. messages.Select(m => m.Text)];
39
+
40
+ // ---------------------------------------------------------------------------------
41
+ // doneCriteria 2: AppendAsync recebe a volta; LoadAsync alimenta a volta seguinte
42
+ // ---------------------------------------------------------------------------------
43
+
44
+ /// <summary>
45
+ /// Uma volta e gravada com pedido e resposta, nessa ordem — e a volta seguinte chega ao
46
+ /// modelo ja com essa historia na frente. As duas metades do contrato, medidas nos dois
47
+ /// lados: no acervo e no que o cliente de modelo recebeu.
48
+ /// </summary>
49
+ [Fact]
50
+ public async Task TheRoundIsAppendedAndFeedsTheHistoryOfTheNextRound()
51
+ {
52
+ var store = new InMemoryConversationStore();
53
+ var history = new ConversationStore(store);
54
+ using var client = new FakeChatClient()
55
+ .EnqueueText("Vou verificar o pedido 1234.")
56
+ .EnqueueText("Foi despachado ontem.");
57
+
58
+ var agent = Agent(client, history);
59
+ var session = await agent.CreateSessionAsync(Ct);
60
+ history.BindConversation(session, "conversa-A");
61
+
62
+ await agent.RunAsync("Meu pedido 1234 esta atrasado.", session, cancellationToken: Ct);
63
+
64
+ // Depois da 1a volta o acervo tem exatamente o pedido e a resposta daquela volta.
65
+ var afterFirst = await store.LoadAsync("conversa-A", Ct);
66
+ Assert.Equal(
67
+ ["Meu pedido 1234 esta atrasado.", "Vou verificar o pedido 1234."],
68
+ TextsOf(afterFirst));
69
+
70
+ await agent.RunAsync("E quando foi despachado?", session, cancellationToken: Ct);
71
+
72
+ // A 2a ida ao modelo levou o historico carregado do acervo, e so uma vez cada
73
+ // mensagem — o turno corrente nao vem duplicado.
74
+ var secondCall = TextsOf(client.ReceivedMessages[1]);
75
+ Assert.Equal(
76
+ [
77
+ "Meu pedido 1234 esta atrasado.",
78
+ "Vou verificar o pedido 1234.",
79
+ "E quando foi despachado?",
80
+ ],
81
+ [.. secondCall.Where(t => t.Length > 0 && t != "Voce e um atendente.")]);
82
+
83
+ var afterSecond = await store.LoadAsync("conversa-A", Ct);
84
+ Assert.Equal(4, afterSecond.Count);
85
+ }
86
+
87
+ /// <summary>
88
+ /// A primeira volta nao carrega historico nenhum: conversa inexistente e lista vazia, nao
89
+ /// erro. E o caso mais comum de todos.
90
+ /// </summary>
91
+ [Fact]
92
+ public async Task AConversationThatDoesNotExistYetLoadsEmptyInsteadOfFailing()
93
+ {
94
+ var store = new InMemoryConversationStore();
95
+ var history = new ConversationStore(store);
96
+ using var client = new FakeChatClient().EnqueueText("ola");
97
+
98
+ var agent = Agent(client, history);
99
+ var session = await agent.CreateSessionAsync(Ct);
100
+ history.BindConversation(session, "conversa-nova");
101
+
102
+ Assert.Empty(await store.LoadAsync("conversa-nova", Ct));
103
+
104
+ await agent.RunAsync("oi", session, cancellationToken: Ct);
105
+
106
+ Assert.Equal(["oi", "ola"], TextsOf(await store.LoadAsync("conversa-nova", Ct)));
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------------
110
+ // doneCriteria 1: DUAS sessoes concorrentes na MESMA instancia, sem vazamento
111
+ // ---------------------------------------------------------------------------------
112
+
113
+ /// <summary>
114
+ /// <b>O canario da invariante.</b> Uma instancia de <see cref="ConversationStore"/>, duas
115
+ /// sessoes, muitas voltas intercaladas em paralelo: nenhuma mensagem de uma conversa
116
+ /// aparece na outra, e cada uma tem exatamente as suas.
117
+ /// </summary>
118
+ /// <remarks>
119
+ /// Este e o teste que morre se alguem guardar "a conversa atual" num campo do provider —
120
+ /// o defeito que em producao aparece como historico de um usuario na tela de outro.
121
+ /// </remarks>
122
+ [Fact]
123
+ public async Task TwoConcurrentSessionsOnTheSameProviderNeverSeeEachOthersMessages()
124
+ {
125
+ const int Rounds = 25;
126
+
127
+ var store = new InMemoryConversationStore();
128
+ var history = new ConversationStore(store);
129
+
130
+ using var clientA = new FakeChatClient();
131
+ using var clientB = new FakeChatClient();
132
+
133
+ for (var i = 0; i < Rounds; i++)
134
+ {
135
+ clientA.EnqueueText($"resposta-A-{i}");
136
+ clientB.EnqueueText($"resposta-B-{i}");
137
+ }
138
+
139
+ // Duas conversas distintas, o MESMO provider de historico nos dois agentes.
140
+ var agentA = Agent(clientA, history);
141
+ var agentB = Agent(clientB, history);
142
+
143
+ var sessionA = await agentA.CreateSessionAsync(Ct);
144
+ var sessionB = await agentB.CreateSessionAsync(Ct);
145
+ history.BindConversation(sessionA, "conversa-A");
146
+ history.BindConversation(sessionB, "conversa-B");
147
+
148
+ // Sequencial DENTRO de cada sessao (uma conversa e uma fila), concorrente ENTRE elas.
149
+ var driveA = Task.Run(async () =>
150
+ {
151
+ for (var i = 0; i < Rounds; i++)
152
+ {
153
+ await agentA.RunAsync($"pergunta-A-{i}", sessionA, cancellationToken: Ct);
154
+ }
155
+ }, Ct);
156
+
157
+ var driveB = Task.Run(async () =>
158
+ {
159
+ for (var i = 0; i < Rounds; i++)
160
+ {
161
+ await agentB.RunAsync($"pergunta-B-{i}", sessionB, cancellationToken: Ct);
162
+ }
163
+ }, Ct);
164
+
165
+ await Task.WhenAll(driveA, driveB);
166
+
167
+ var conversationA = TextsOf(await store.LoadAsync("conversa-A", Ct));
168
+ var conversationB = TextsOf(await store.LoadAsync("conversa-B", Ct));
169
+
170
+ Assert.Equal(Rounds * 2, conversationA.Length);
171
+ Assert.Equal(Rounds * 2, conversationB.Length);
172
+
173
+ // Nenhum texto do outro lado atravessou.
174
+ Assert.DoesNotContain(conversationA, t => t.Contains("-B-", StringComparison.Ordinal));
175
+ Assert.DoesNotContain(conversationB, t => t.Contains("-A-", StringComparison.Ordinal));
176
+
177
+ // E cada conversa tem as suas, na ordem.
178
+ Assert.Equal(
179
+ [.. Enumerable.Range(0, Rounds).SelectMany(i => new[] { $"pergunta-A-{i}", $"resposta-A-{i}" })],
180
+ conversationA);
181
+
182
+ // O que o modelo VIU tambem nao se misturou: a ultima ida de A nao carrega nada de B.
183
+ Assert.DoesNotContain(
184
+ TextsOf(clientA.ReceivedMessages[^1]),
185
+ t => t.Contains("-B-", StringComparison.Ordinal));
186
+ }
187
+
188
+ /// <summary>
189
+ /// A prova direta de que o estado por sessao <b>mora na sessao</b>: o mesmo provider
190
+ /// devolve ids diferentes para sessoes diferentes, e o id de cada sessao esta gravado
191
+ /// no <c>StateBag</c> dela — que e onde o MAF o serializa e o retoma.
192
+ /// </summary>
193
+ [Fact]
194
+ public async Task TheConversationIdLivesInTheSessionStateNotInAProviderField()
195
+ {
196
+ var history = new ConversationStore(new InMemoryConversationStore());
197
+ using var client = new FakeChatClient();
198
+ var agent = Agent(client, history);
199
+
200
+ var first = await agent.CreateSessionAsync(Ct);
201
+ var second = await agent.CreateSessionAsync(Ct);
202
+
203
+ var firstId = history.GetConversationId(first);
204
+ var secondId = history.GetConversationId(second);
205
+
206
+ Assert.NotEqual(firstId, secondId);
207
+ Assert.Equal(firstId, history.GetConversationId(first)); // estavel na mesma sessao
208
+
209
+ // Esta gravado na sessao, sob a chave que o provider declara em StateKeys.
210
+ Assert.True(first.StateBag.TryGetValue<string>(ConversationStore.ConversationIdStateKey, out var stored));
211
+ Assert.Equal(firstId, stored);
212
+ Assert.Contains(ConversationStore.ConversationIdStateKey, history.StateKeys);
213
+ }
214
+
215
+ /// <summary>
216
+ /// Sem amarracao explicita o id e um <see cref="Guid.CreateVersion7"/> — ordenavel no
217
+ /// tempo, ao contrario de <c>Guid.NewGuid</c>. A versao 7 e legivel no proprio valor
218
+ /// (o nibble de versao), entao a regra da casa e verificavel e nao so declarada.
219
+ /// </summary>
220
+ [Fact]
221
+ public async Task AnUnboundSessionGetsATimeOrderedIdentifier()
222
+ {
223
+ var history = new ConversationStore(new InMemoryConversationStore());
224
+ using var client = new FakeChatClient();
225
+ var agent = Agent(client, history);
226
+
227
+ var session = await agent.CreateSessionAsync(Ct);
228
+ var id = history.GetConversationId(session);
229
+
230
+ Assert.True(Guid.TryParseExact(id, "N", out var parsed));
231
+ Assert.Equal(7, parsed.ToByteArray(bigEndian: true)[6] >> 4);
232
+ }
233
+
234
+ /// <summary>
235
+ /// O caminho do CRM: a conversa ja existe como registro, e a sessao entra nela — o
236
+ /// historico anterior aparece na primeira volta desta sessao. E o que torna a decisao
237
+ /// "conversa no acervo do projeto" util em vez de teorica.
238
+ /// </summary>
239
+ [Fact]
240
+ public async Task ASessionCanJoinAConversationThatAlreadyExistsInTheProjectStore()
241
+ {
242
+ var store = new InMemoryConversationStore();
243
+ await store.AppendAsync(
244
+ "crm-conversa-42",
245
+ [
246
+ new ChatMessage(ChatRole.User, "Ontem eu pedi um orcamento."),
247
+ new ChatMessage(ChatRole.Assistant, "Enviei por email."),
248
+ ],
249
+ Ct);
250
+
251
+ var history = new ConversationStore(store);
252
+ using var client = new FakeChatClient().EnqueueText("Reenviei agora.");
253
+ var agent = Agent(client, history);
254
+
255
+ var session = await agent.CreateSessionAsync(Ct);
256
+ history.BindConversation(session, "crm-conversa-42");
257
+
258
+ await agent.RunAsync("Nao chegou.", session, cancellationToken: Ct);
259
+
260
+ Assert.Contains("Ontem eu pedi um orcamento.", TextsOf(client.ReceivedMessages[0]));
261
+ // 2 do acervo anterior + o pedido e a resposta desta volta. A volta grava SO a si
262
+ // mesma: regravar o historico inteiro faria a conversa crescer em progressao
263
+ // geometrica a cada turno.
264
+ Assert.Equal(4, (await store.LoadAsync("crm-conversa-42", Ct)).Count);
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------------
268
+ // O acervo e plugavel: o kit nao conhece banco
269
+ // ---------------------------------------------------------------------------------
270
+
271
+ /// <summary>
272
+ /// A costura e substituivel: um acervo de teste que apenas anota as chamadas prova que o
273
+ /// provider fala com <see cref="IConversationStore"/> e com mais nada — e um projeto que
274
+ /// troque memoria por <c>jsonb</c> so implementa esta interface.
275
+ /// </summary>
276
+ [Fact]
277
+ public async Task TheProviderTalksOnlyToTheSeamSoPersistenceIsReplaceable()
278
+ {
279
+ var spy = new RecordingConversationStore();
280
+ var history = new ConversationStore(spy);
281
+ using var client = new FakeChatClient().EnqueueText("pronto");
282
+
283
+ var agent = Agent(client, history);
284
+ var session = await agent.CreateSessionAsync(Ct);
285
+ history.BindConversation(session, "c-1");
286
+
287
+ await agent.RunAsync("faz", session, cancellationToken: Ct);
288
+
289
+ Assert.Equal(["load:c-1", "append:c-1"], spy.Calls);
290
+ }
291
+
292
+ /// <summary>
293
+ /// Volta que falhou nao vira historico. Persistir o pedido sem resposta deixaria a
294
+ /// conversa terminando numa pergunta nunca respondida — e a volta seguinte a repetiria,
295
+ /// com o usuario se vendo duplicado.
296
+ /// </summary>
297
+ [Fact]
298
+ public async Task AFailedRoundIsNotPersisted()
299
+ {
300
+ var store = new InMemoryConversationStore();
301
+ var history = new ConversationStore(store);
302
+ using var client = new FakeChatClient(); // fila vazia: a volta estoura
303
+ var agent = Agent(client, history);
304
+
305
+ var session = await agent.CreateSessionAsync(Ct);
306
+ history.BindConversation(session, "conversa-falha");
307
+
308
+ await Assert.ThrowsAsync<InvalidOperationException>(
309
+ () => agent.RunAsync("oi", session, cancellationToken: Ct));
310
+
311
+ Assert.Empty(await store.LoadAsync("conversa-falha", Ct));
312
+ }
313
+
314
+ /// <summary>
315
+ /// <b>Uma volta sem sessao.</b> O MAF declara <c>InvokingContext.Session</c> como
316
+ /// anulavel, e o provider recusa esse caso: inventar um id gravaria conversa orfa e usar
317
+ /// uma chave fixa juntaria tenants no mesmo balde — as duas saidas silenciosas sao
318
+ /// piores que a recusa. Este teste mede o que o framework realmente faz por esse
319
+ /// caminho, em vez de supor: e o comportamento observado que fica pinado aqui.
320
+ /// </summary>
321
+ /// <remarks>
322
+ /// O caminho de construir <c>InvokingContext</c> a mao — que seria a forma direta de
323
+ /// exercitar a guarda — <b>nao esta disponivel</b>: no MAF 1.20.0 os construtores de
324
+ /// <c>InvokingContext</c> e <c>InvokedContext</c> sao os unicos membros da area de
325
+ /// <c>ChatHistoryProvider</c> marcados <c>[Experimental("MAAI001")]</c>, e com
326
+ /// <c>TreatWarningsAsErrors</c> isso e erro de compilacao. Consequencia registrada: um
327
+ /// provider de historico so pode ser testado <b>atraves de um agente real</b>, que e
328
+ /// como esta classe inteira o testa.
329
+ /// </remarks>
330
+ [Fact]
331
+ public async Task ARoundWithoutAnExplicitSessionStillHasAConversationOfItsOwn()
332
+ {
333
+ var store = new InMemoryConversationStore();
334
+ var history = new ConversationStore(store);
335
+ using var client = new FakeChatClient().EnqueueText("respondido");
336
+ var agent = Agent(client, history);
337
+
338
+ await agent.RunAsync("sem sessao", cancellationToken: Ct);
339
+
340
+ // O agente cria uma sessao efemera para a volta: a guarda de sessao ausente nao e
341
+ // alcancada por este caminho, e a conversa nasce com id proprio em vez de cair num
342
+ // balde compartilhado.
343
+ var conversation = Assert.Single(store.ConversationIds);
344
+ Assert.Equal(["sem sessao", "respondido"], TextsOf(await store.LoadAsync(conversation, Ct)));
345
+
346
+ // E a volta seguinte, tambem sem sessao, NAO herda a conversa anterior — sem sessao
347
+ // nao ha continuidade, que e a semantica correta de uma chamada avulsa.
348
+ client.EnqueueText("de novo");
349
+ await agent.RunAsync("outra avulsa", cancellationToken: Ct);
350
+
351
+ Assert.Equal(2, store.ConversationIds.Count);
352
+ Assert.DoesNotContain("sem sessao", TextsOf(client.ReceivedMessages[^1]));
353
+ }
354
+
355
+ /// <summary>Provider sem acervo e defeito de composicao, e falha na construcao.</summary>
356
+ [Fact]
357
+ public void TheProviderRefusesToBeBuiltWithoutAStore()
358
+ => Assert.Throws<ArgumentNullException>(() => new ConversationStore(null!));
359
+
360
+ /// <summary>
361
+ /// O acervo em memoria e ele proprio seguro sob concorrencia — senao o canario das duas
362
+ /// sessoes mediria a falta de trava aqui, e nao o isolamento do provider.
363
+ /// </summary>
364
+ [Fact]
365
+ public async Task TheInMemoryStoreDoesNotLoseWritesUnderConcurrency()
366
+ {
367
+ var store = new InMemoryConversationStore();
368
+
369
+ await Task.WhenAll(Enumerable.Range(0, 100).Select(i => Task.Run(
370
+ async () => await store.AppendAsync(
371
+ i % 2 == 0 ? "par" : "impar",
372
+ [new ChatMessage(ChatRole.User, $"m{i}")],
373
+ Ct),
374
+ Ct)));
375
+
376
+ Assert.Equal(50, (await store.LoadAsync("par", Ct)).Count);
377
+ Assert.Equal(50, (await store.LoadAsync("impar", Ct)).Count);
378
+ Assert.Empty(await store.LoadAsync("inexistente", Ct));
379
+ }
380
+
381
+ /// <summary>Acervo de teste que so anota o que lhe pediram, na ordem.</summary>
382
+ private sealed class RecordingConversationStore : IConversationStore
383
+ {
384
+ private readonly ConcurrentQueue<string> _calls = new();
385
+
386
+ public IReadOnlyList<string> Calls => [.. _calls];
387
+
388
+ public ValueTask<IReadOnlyList<ChatMessage>> LoadAsync(
389
+ string conversationId,
390
+ CancellationToken cancellationToken)
391
+ {
392
+ _calls.Enqueue($"load:{conversationId}");
393
+ return ValueTask.FromResult<IReadOnlyList<ChatMessage>>([]);
394
+ }
395
+
396
+ public ValueTask AppendAsync(
397
+ string conversationId,
398
+ IReadOnlyList<ChatMessage> messages,
399
+ CancellationToken cancellationToken)
400
+ {
401
+ _calls.Enqueue($"append:{conversationId}");
402
+ return ValueTask.CompletedTask;
403
+ }
404
+ }
405
+ }