@polymorphism-tech/morph-spec 8.33.0 → 8.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/CLAUDE.md +22 -2
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +43 -5
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +5 -4
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +2 -2
  8. package/framework/MORPH.md +20 -22
  9. package/framework/agents.json +198 -44
  10. package/framework/ai-pin.json +22 -0
  11. package/framework/commands/morph-media.md +45 -0
  12. package/framework/docs/CLI.md +238 -10
  13. package/framework/evals/proof-rubrics/README.md +76 -0
  14. package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
  15. package/framework/evals/proof-rubrics/fiacao.md +54 -0
  16. package/framework/evals/proof-rubrics/guard-cego.md +52 -0
  17. package/framework/evals/proof-rubrics/media-claims.md +109 -0
  18. package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
  19. package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
  20. package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
  21. package/framework/evals/rubrics/architecture.md +30 -0
  22. package/framework/evals/rubrics/code-quality.md +26 -0
  23. package/framework/evals/rubrics/test-coverage.md +20 -0
  24. package/framework/hooks/README.md +11 -1
  25. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
  26. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
  27. package/framework/hooks/claude-code/core/chain.js +27 -7
  28. package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
  29. package/framework/hooks/claude-code/core/worktree-context.js +83 -14
  30. package/framework/hooks/claude-code/core/worktree-create.js +206 -0
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
  32. package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
  33. package/framework/hooks/shared/command-tokens.js +23 -0
  34. package/framework/hooks/shared/e2e-check.js +0 -0
  35. package/framework/hooks/shared/git-paths.js +25 -0
  36. package/framework/hooks/shared/hook-response.js +17 -0
  37. package/framework/hooks/shared/morph-cli.js +134 -0
  38. package/framework/rules/ai-agents-standards.md +14 -2
  39. package/framework/rules/infrastructure-standards.md +3 -1
  40. package/framework/schemas/ai-pin.schema.json +97 -0
  41. package/framework/schemas/config.schema.json +107 -0
  42. package/framework/schemas/feature.schema.json +23 -4
  43. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  44. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  45. package/framework/skills/core/morph-apply/SKILL.md +28 -7
  46. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  47. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  48. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  49. package/framework/skills/core/morph-implement/SKILL.md +20 -2
  50. package/framework/skills/core/morph-media/SKILL.md +215 -0
  51. package/framework/skills/core/morph-plan/SKILL.md +38 -4
  52. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  53. package/framework/skills/core/morph-review/SKILL.md +166 -9
  54. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  55. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  56. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  57. package/framework/standards/PROVENANCE.json +81 -3
  58. package/framework/standards/STANDARDS.json +1504 -830
  59. package/framework/standards/ai-agents/agent-archetypes.md +160 -0
  60. package/framework/standards/ai-agents/agent-session.md +256 -160
  61. package/framework/standards/ai-agents/agent-spec.md +196 -0
  62. package/framework/standards/ai-agents/batching.md +217 -193
  63. package/framework/standards/ai-agents/context-providers.md +261 -188
  64. package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
  65. package/framework/standards/ai-agents/cost-and-budget.md +256 -0
  66. package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
  67. package/framework/standards/ai-agents/evals-with-cache.md +289 -0
  68. package/framework/standards/ai-agents/guardrails.md +222 -0
  69. package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
  70. package/framework/standards/ai-agents/mcp-server.md +351 -250
  71. package/framework/standards/ai-agents/mcp-tools.md +250 -197
  72. package/framework/standards/ai-agents/media-pipeline.md +352 -0
  73. package/framework/standards/ai-agents/media-video.md +267 -0
  74. package/framework/standards/ai-agents/middleware-patterns.md +241 -207
  75. package/framework/standards/ai-agents/modalities-image-gen.md +422 -173
  76. package/framework/standards/ai-agents/modalities-speech-to-text.md +177 -171
  77. package/framework/standards/ai-agents/modalities-text-to-speech.md +189 -181
  78. package/framework/standards/ai-agents/modalities-vision.md +205 -201
  79. package/framework/standards/ai-agents/multi-agent-patterns.md +211 -155
  80. package/framework/standards/ai-agents/observability-patterns.md +227 -17
  81. package/framework/standards/ai-agents/production.md +260 -231
  82. package/framework/standards/ai-agents/prompt-sources.md +389 -0
  83. package/framework/standards/ai-agents/providers/model-registry.md +293 -158
  84. package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
  85. package/framework/standards/ai-agents/service-tier-flex.md +143 -135
  86. package/framework/standards/ai-agents/setup.md +166 -50
  87. package/framework/standards/ai-agents/structured-output.md +280 -249
  88. package/framework/standards/ai-agents/sweet-spot.md +129 -109
  89. package/framework/standards/ai-agents/testing-ai.md +236 -0
  90. package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
  91. package/framework/standards/ai-agents/workflows.md +27 -7
  92. package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
  93. package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
  94. package/framework/standards/infrastructure/docker/local-compose-isolation.md +90 -0
  95. package/framework/templates/REGISTRY.json +14 -1
  96. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  97. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  98. package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
  99. package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
  100. package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
  101. package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
  102. package/framework/templates/dotnet/ai-kit/README.md +128 -0
  103. package/framework/templates/dotnet/ai-kit/global.json +9 -0
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
  118. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
  119. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
  120. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
  121. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
  122. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
  123. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
  124. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
  125. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
  126. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
  127. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
  128. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
  129. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
  130. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
  131. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
  132. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
  133. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
  134. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
  135. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
  136. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
  137. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
  138. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
  139. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
  140. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
  141. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
  142. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
  143. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
  144. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
  145. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
  146. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
  147. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
  148. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
  149. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
  150. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
  151. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
  152. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
  153. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
  154. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
  155. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
  156. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
  157. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
  158. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
  159. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
  160. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
  161. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
  162. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
  163. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
  164. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
  165. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
  166. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
  167. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
  168. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
  169. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
  170. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
  171. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
  172. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
  173. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
  174. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
  175. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
  176. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
  177. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
  178. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
  179. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
  180. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
  181. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
  182. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
  183. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
  184. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
  185. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
  186. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
  187. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
  188. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
  189. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
  190. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
  191. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
  192. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
  193. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
  194. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
  195. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
  196. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
  197. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
  198. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
  199. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
  200. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
  201. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
  202. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
  203. package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
  204. package/package.json +3 -3
  205. package/src/commands/e2e/e2e.js +91 -40
  206. package/src/commands/project/doctor.js +143 -6
  207. package/src/commands/project/env.js +95 -0
  208. package/src/commands/project/update.js +469 -153
  209. package/src/commands/project/worktree.js +274 -53
  210. package/src/commands/state/advance.js +24 -2
  211. package/src/commands/state/approve.js +33 -1
  212. package/src/commands/state/create.js +72 -11
  213. package/src/commands/state/delete.js +8 -0
  214. package/src/commands/state/finish.js +360 -53
  215. package/src/commands/state/gate-check.js +190 -6
  216. package/src/commands/state/score.js +36 -1
  217. package/src/commands/validation/verify.js +134 -25
  218. package/src/core/state/state-manager.js +9 -1
  219. package/src/dashboard/app/pages/HookInspector.jsx +430 -418
  220. package/src/dashboard/scanner/hooks-scanner.js +5 -1
  221. package/src/lib/ai/ai-checks.js +182 -0
  222. package/src/lib/ai/ai-project-scan.js +347 -0
  223. package/src/lib/ai/code-audit.js +181 -0
  224. package/src/lib/ai/comment-stripper.js +184 -0
  225. package/src/lib/ai/pin-drift.js +138 -0
  226. package/src/lib/ai/registry-audit.js +285 -0
  227. package/src/lib/e2e/compose-env.js +55 -14
  228. package/src/lib/e2e/stack-teardown.js +115 -0
  229. package/src/lib/evaluator-report.js +269 -15
  230. package/src/lib/generators/architecture-scanner.js +2 -1
  231. package/src/lib/git/default-branch.js +92 -4
  232. package/src/lib/graph/graphifyignore.js +2 -1
  233. package/src/lib/installers/effects.js +193 -0
  234. package/src/lib/installers/templates-reconcile.js +3 -3
  235. package/src/lib/risk-detector.js +284 -0
  236. package/src/lib/standards/verified-against.js +276 -0
  237. package/src/lib/tasks/eval-node.js +485 -0
  238. package/src/lib/tasks/linux-runner.js +297 -0
  239. package/src/lib/tasks/test-nodes.js +486 -0
  240. package/src/lib/tasks/test-runner.js +431 -37
  241. package/src/lib/validators/verify-runner.js +590 -28
  242. package/src/lib/worktree/dirs.js +32 -0
  243. package/src/lib/worktree/docker-audit.js +287 -0
  244. package/src/lib/worktree/env-allocator.js +69 -11
  245. package/src/lib/worktree/worktree-audit.js +70 -3
  246. package/src/scripts/setup-infra.js +24 -11
  247. package/src/utils/agents-installer.js +18 -8
  248. package/src/utils/claude-md-injector.js +10 -5
  249. package/src/utils/file-copier.js +28 -16
  250. package/src/utils/hooks-installer.js +75 -29
  251. package/src/utils/skills-installer.js +194 -187
  252. package/src/utils/version-checker.js +68 -3
  253. package/framework/standards/ai-agents/prompts-in-db.md +0 -214
@@ -0,0 +1,139 @@
1
+ using Morph.AiKit.Evals;
2
+ using Morph.AiKit.Tests.Fakes;
3
+ using Xunit;
4
+
5
+ namespace Morph.AiKit.Tests.Evals;
6
+
7
+ /// <summary>
8
+ /// Miss de cache sem credencial REPROVA, nomeado. Jamais <c>skip</c>.
9
+ /// </summary>
10
+ /// <remarks>
11
+ /// A tentacao do <c>skip</c> e real ("nao da para avaliar, entao nao avalio") e recria
12
+ /// exatamente o buraco que este harness fechou duas vezes: gate verde sobre verificacao que
13
+ /// nao aconteceu. E ha razao positiva: em CI, com cache commitada, um miss significa UMA
14
+ /// coisa so — o prompt ou o modelo mudou e ninguem regravou. O miss e o veredito, nao o
15
+ /// obstaculo.
16
+ /// </remarks>
17
+ public sealed class EvalCacheMissTests
18
+ {
19
+ private static async Task<EvalWorkspace> SeededAsync(params string[] scenarioNames)
20
+ {
21
+ var ws = new EvalWorkspace();
22
+ using var fake = new FakeChatClient
23
+ {
24
+ MetadataProviderName = "morph-fake",
25
+ MetadataDefaultModelId = "modelo-exemplar",
26
+ };
27
+
28
+ foreach (var name in scenarioNames)
29
+ {
30
+ ws.WriteScenario("triagem", "acme", name, $"pergunta de {name}", """[ { "kind": "contains", "value": "resposta" } ]""");
31
+ fake.EnqueueText($"resposta de {name}");
32
+ }
33
+
34
+ await using var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake);
35
+ await refresh.RunAllAsync(TestContext.Current.CancellationToken);
36
+ return ws;
37
+ }
38
+
39
+ [Fact]
40
+ public async Task AScenarioMissingFromTheCacheFailsByNameInsteadOfCallingTheProvider()
41
+ {
42
+ using var ws = await SeededAsync("a-primeiro");
43
+
44
+ // Cenario NOVO, que a cache nunca viu — a situacao literal de "mudou o prompt e
45
+ // ninguem regravou". O prefixo "b-" e deliberado: os cenarios rodam em ordem ordinal
46
+ // de caminho, e o teste abaixo depende de o cenario BOM ter rodado antes do miss.
47
+ ws.WriteScenario("triagem", "acme", "b-novo", "pergunta inedita", """[ { "kind": "contains", "value": "resposta" } ]""");
48
+
49
+ await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
50
+ var error = await Assert.ThrowsAsync<EvalCacheMissException>(
51
+ () => offline.RunAllAsync(TestContext.Current.CancellationToken));
52
+
53
+ // O cenario que faltou, e nao "algum cenario".
54
+ Assert.Equal("triagem.acme.b-novo", error.ScenarioName);
55
+ Assert.Contains("eval-cache-miss", error.Message, StringComparison.Ordinal);
56
+
57
+ // P3: a mensagem carrega o comando de regravacao e o arquivo de credencial esperado.
58
+ // Um "cache miss" seco mandaria o leitor procurar os dois.
59
+ Assert.Contains(EvalCacheMissException.RefreshCommand, error.Message, StringComparison.Ordinal);
60
+ Assert.Contains(".env.evals", error.Message, StringComparison.Ordinal);
61
+ Assert.Contains(ws.CacheRoot, error.Message, StringComparison.Ordinal);
62
+ }
63
+
64
+ [Fact]
65
+ public async Task TheMissTravelsAsAFAILURE_NeverAsASkippedScenario()
66
+ {
67
+ using var ws = await SeededAsync("a-primeiro");
68
+ ws.WriteScenario("triagem", "acme", "b-novo", "pergunta inedita", """[ { "kind": "contains", "value": "resposta" } ]""");
69
+
70
+ // O sumario da SEMEADURA, byte a byte. Ele e a linha de base: o que este teste cobra
71
+ // e que o run que morreu por miss nao escreva NADA por cima dele.
72
+ var summaryBeforeTheMiss = File.ReadAllBytes(ws.SummaryPath);
73
+
74
+ await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
75
+ await Assert.ThrowsAsync<EvalCacheMissException>(
76
+ () => offline.RunAllAsync(TestContext.Current.CancellationToken));
77
+
78
+ // Nao ha caminho pelo qual o cenario ausente apareca como avaliado: a contagem so tem
79
+ // o que de fato resolveu, e o sumario NAO foi reescrito. Um sumario dizendo
80
+ // "1 cenario, 1 passou, cache warm" aqui seria exatamente a mentira que a excecao
81
+ // existe para impedir — e o CLI, que le o ARQUIVO, acreditaria nela.
82
+ Assert.Single(offline.Results);
83
+ Assert.Equal("triagem.acme.a-primeiro", offline.Results[0].Scenario);
84
+ Assert.Equal(summaryBeforeTheMiss, File.ReadAllBytes(ws.SummaryPath));
85
+
86
+ // E o contador do cliente interno registra a tentativa: a arvore de cache nao
87
+ // respondeu, e isso e um FATO medido, nao uma inferencia.
88
+ Assert.Equal(1, offline.Offline!.Invocations);
89
+ }
90
+
91
+ [Fact]
92
+ public async Task AFailedAssertionMakesTheScenarioREDWithoutAnyCacheMiss()
93
+ {
94
+ // A outra metade do contrato: reprovar por conteudo e diferente de reprovar por
95
+ // cache. Uma so mensagem para os dois casos mandaria o leitor procurar no lugar
96
+ // errado metade das vezes.
97
+ using var ws = new EvalWorkspace();
98
+ ws.WriteScenario("triagem", "acme", "saudacao", "bom dia", """[ { "kind": "contains", "value": "jamais aparece" } ]""");
99
+
100
+ using var fake = new FakeChatClient { MetadataProviderName = "morph-fake", MetadataDefaultModelId = "modelo-exemplar" };
101
+ fake.EnqueueText("Ola!");
102
+ await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
103
+ {
104
+ await refresh.RunAllAsync(TestContext.Current.CancellationToken);
105
+ }
106
+
107
+ await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
108
+ var summary = await offline.RunAllAsync(TestContext.Current.CancellationToken);
109
+
110
+ Assert.Equal(1, summary.Scenarios);
111
+ Assert.Equal(1, summary.Failed);
112
+ Assert.Equal(EvalCacheState.Warm, summary.Cache);
113
+ Assert.Equal(0, summary.Misses);
114
+ Assert.Contains("FALHOU", offline.Results[0].Outcomes[0].Reason, StringComparison.Ordinal);
115
+ }
116
+
117
+ [Fact]
118
+ public async Task AScenarioWithNoDeterministicAssertionIsNeverGreenByVacuity()
119
+ {
120
+ using var ws = new EvalWorkspace();
121
+ ws.WriteScenario("triagem", "acme", "vazio", "bom dia", "[]");
122
+
123
+ using var fake = new FakeChatClient { MetadataProviderName = "morph-fake", MetadataDefaultModelId = "modelo-exemplar" };
124
+ fake.EnqueueText("Ola!");
125
+ await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
126
+ {
127
+ await refresh.RunAllAsync(TestContext.Current.CancellationToken);
128
+ }
129
+
130
+ await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
131
+ var summary = await offline.RunAllAsync(TestContext.Current.CancellationToken);
132
+
133
+ // Um cenario que nao cobra nada nao pode contar como cobertura. Deixa-lo verde e o
134
+ // mesmo defeito de um teste vazio — pior aqui, porque ele apareceria na contagem
135
+ // como se tivesse verificado algo.
136
+ Assert.Equal(1, summary.Failed);
137
+ Assert.Equal(0, summary.Passed);
138
+ }
139
+ }
@@ -0,0 +1,157 @@
1
+ using Morph.AiKit.Evals;
2
+ using Morph.AiKit.Tests.Fakes;
3
+ using Xunit;
4
+
5
+ namespace Morph.AiKit.Tests.Evals;
6
+
7
+ /// <summary>
8
+ /// O carimbo: por que a arvore de cache precisa dizer sob quais versoes foi gravada.
9
+ /// </summary>
10
+ /// <remarks>
11
+ /// A funcao de hash que produz a chave nao promete estabilidade entre versoes da biblioteca.
12
+ /// Sem carimbo, subir o pin chega como N misses inexplicaveis num dia em que ninguem mexeu
13
+ /// em nada; com carimbo, chega como uma frase que nomeia as DUAS versoes.
14
+ /// </remarks>
15
+ public sealed class EvalCacheStampTests
16
+ {
17
+ private static readonly FakeClock Today = new(new DateTimeOffset(2026, 9, 8, 12, 0, 0, TimeSpan.Zero));
18
+
19
+ private static EvalPin Pin(string pinnedAt = "2026-09-08", string evaluation = "10.9.0") => new(
20
+ pinnedAt,
21
+ new Dictionary<string, string>(StringComparer.Ordinal)
22
+ {
23
+ ["Microsoft.Extensions.AI"] = "10.9.0",
24
+ ["Microsoft.Extensions.AI.Evaluation"] = evaluation,
25
+ });
26
+
27
+ private static void Stamp(string root, string pinnedAt = "2026-09-08", string evaluation = "10.9.0")
28
+ => EvalCacheStamp.Write(root, new EvalCacheStamp
29
+ {
30
+ PinnedAt = pinnedAt,
31
+ Packages = Pin(pinnedAt, evaluation).Packages,
32
+ Provider = "morph-fake",
33
+ Model = "modelo-exemplar",
34
+ });
35
+
36
+ [Fact]
37
+ public void AStampedTreeThatMatchesThePinIsAccepted()
38
+ {
39
+ using var ws = new EvalWorkspace();
40
+ Stamp(ws.CacheRoot);
41
+
42
+ var check = EvalCacheStamp.Check(ws.CacheRoot, Pin(), Today);
43
+
44
+ Assert.True(check.IsOk);
45
+ Assert.Equal(EvalStampVerdict.Ok, check.Verdict);
46
+ }
47
+
48
+ [Fact]
49
+ public void TheStampCarriesThePinDateAndTheEvaluationVersionsItWasWrittenUnder()
50
+ {
51
+ using var ws = new EvalWorkspace();
52
+ Stamp(ws.CacheRoot);
53
+
54
+ // Ler de VOLTA do disco, e nao comparar o objeto com ele mesmo: o carimbo e um
55
+ // artefato, e um teste que so olhasse a instancia em memoria seria um espelho.
56
+ var read = EvalCacheStamp.Read(ws.CacheRoot);
57
+
58
+ Assert.NotNull(read);
59
+ Assert.Equal("2026-09-08", read!.PinnedAt);
60
+ Assert.Equal("10.9.0", read.Packages["Microsoft.Extensions.AI.Evaluation"]);
61
+ Assert.Equal("morph-fake", read.Provider);
62
+ Assert.Equal(EvalCacheStamp.Writer, read.WrittenBy);
63
+ }
64
+
65
+ [Fact]
66
+ public void ADivergentStampIsRefusedNamingBOTHVersions()
67
+ {
68
+ using var ws = new EvalWorkspace();
69
+ Stamp(ws.CacheRoot, evaluation: "10.9.0");
70
+
71
+ var check = EvalCacheStamp.Check(ws.CacheRoot, Pin(evaluation: "10.11.0"), Today);
72
+
73
+ Assert.Equal(EvalStampVerdict.PinMismatch, check.Verdict);
74
+ Assert.Contains("eval-cache-stale-pin", check.Reason, StringComparison.Ordinal);
75
+ // Dizer so uma das versoes obriga o leitor a ir buscar a outra.
76
+ Assert.Contains("gravada 10.9.0", check.Reason, StringComparison.Ordinal);
77
+ Assert.Contains("pin 10.11.0", check.Reason, StringComparison.Ordinal);
78
+ }
79
+
80
+ [Fact]
81
+ public void AMissingStampHasItsOWNReason_DistinctFromADivergentOne()
82
+ {
83
+ using var ws = new EvalWorkspace();
84
+
85
+ var missing = EvalCacheStamp.Check(ws.CacheRoot, Pin(), Today);
86
+ Stamp(ws.CacheRoot, evaluation: "10.9.0");
87
+ var divergent = EvalCacheStamp.Check(ws.CacheRoot, Pin(evaluation: "10.11.0"), Today);
88
+
89
+ Assert.Equal(EvalStampVerdict.Missing, missing.Verdict);
90
+ Assert.Contains("eval-cache-unstamped", missing.Reason, StringComparison.Ordinal);
91
+
92
+ // As duas situacoes pedem acoes diferentes: "regrave" versus "regrave PORQUE o
93
+ // pacote mudou". Um veredito unico devolveria ao leitor a pergunta que o carimbo
94
+ // existia para responder.
95
+ Assert.NotEqual(missing.Verdict, divergent.Verdict);
96
+ Assert.DoesNotContain("eval-cache-stale-pin", missing.Reason, StringComparison.Ordinal);
97
+ }
98
+
99
+ [Fact]
100
+ public void AnUnreadableStampIsNotSilentlyTreatedAsAbsent()
101
+ {
102
+ using var ws = new EvalWorkspace();
103
+ File.WriteAllText(EvalCacheStamp.PathFor(ws.CacheRoot), "{ isto nao e json");
104
+
105
+ var check = EvalCacheStamp.Check(ws.CacheRoot, Pin(), Today);
106
+
107
+ Assert.Equal(EvalStampVerdict.Unreadable, check.Verdict);
108
+ Assert.Null(EvalCacheStamp.Read(ws.CacheRoot));
109
+ }
110
+
111
+ [Fact]
112
+ public void APinDatedInTheFutureIsAPinERROR_NeverAFreshCache()
113
+ {
114
+ using var ws = new EvalWorkspace();
115
+ Stamp(ws.CacheRoot, pinnedAt: "2027-01-01");
116
+
117
+ var check = EvalCacheStamp.Check(ws.CacheRoot, Pin(pinnedAt: "2027-01-01"), Today);
118
+
119
+ // O carimbo CASA com o pin — e mesmo assim reprova, porque a regua esta adulterada.
120
+ // Uma data no futuro renovaria sozinha qualquer janela medida contra ela.
121
+ Assert.Equal(EvalStampVerdict.FuturePin, check.Verdict);
122
+ Assert.Contains("eval-pin-future", check.Reason, StringComparison.Ordinal);
123
+ }
124
+
125
+ [Fact]
126
+ public void TheFixtureRefusesToEvaluateASINGLEScenarioUnderABadStamp()
127
+ {
128
+ using var ws = new EvalWorkspace();
129
+ ws.WriteScenario("triagem", "acme", "saudacao", "bom dia", """[ { "kind": "contains", "value": "Ola" } ]""");
130
+ Stamp(ws.CacheRoot, evaluation: "10.9.0");
131
+
132
+ var pin = new EvalPin("2026-09-08", new Dictionary<string, string>(StringComparer.Ordinal)
133
+ {
134
+ ["Microsoft.Extensions.AI"] = "10.9.0",
135
+ ["Microsoft.Extensions.AI.Evaluation"] = "10.11.0",
136
+ });
137
+
138
+ // A recusa acontece na CONSTRUCAO — antes de qualquer cenario. Nao faz sentido pagar
139
+ // o relogio de uma suite para descobrir no fim que a cache nao servia.
140
+ var error = Assert.Throws<InvalidOperationException>(
141
+ () => EvalFixture.Create(ws.Options(EvalMode.Offline, pin), timeProvider: Today));
142
+
143
+ Assert.Contains("eval-cache-stale-pin", error.Message, StringComparison.Ordinal);
144
+ }
145
+
146
+ [Fact]
147
+ public void RefreshDoesNotDemandAStamp_ItWritesOne()
148
+ {
149
+ using var ws = new EvalWorkspace();
150
+
151
+ // Cobrar carimbo em modo de escrita tornaria impossivel criar a PRIMEIRA cache.
152
+ var fixture = EvalFixture.Create(ws.Options(EvalMode.Refresh), new FakeChatClient());
153
+
154
+ Assert.True(fixture.StampCheck.IsOk);
155
+ Assert.False(File.Exists(EvalCacheStamp.PathFor(ws.CacheRoot)));
156
+ }
157
+ }
@@ -0,0 +1,178 @@
1
+ using Morph.AiKit.Evals;
2
+ using Morph.AiKit.Tests.Fakes;
3
+ using Xunit;
4
+
5
+ namespace Morph.AiKit.Tests.Evals;
6
+
7
+ /// <summary>
8
+ /// A camada que NOMEIA a falha, testada pelo conteudo do que ela produz.
9
+ /// </summary>
10
+ /// <remarks>
11
+ /// <para>
12
+ /// Este arquivo fecha a setima ocorrencia da mesma classe nesta epic, e a mais ironica: o
13
+ /// mecanismo de <c>Failures</c> nasceu para consertar um eval que mordia ANONIMAMENTE, e
14
+ /// nasceu ele proprio sem par. As tres partes podiam ser desligadas sem matar um teste —
15
+ /// <c>CollectFailures</c> devolvendo lista vazia, o ramo da vacuidade nunca disparando, e o
16
+ /// nome da assercao trocado por um literal — e a suite seguia "165 total, 0 falhou".
17
+ /// </para>
18
+ /// <para>
19
+ /// A causa foi uma unica testemunha automatizada: <c>Assert.Empty(summary.Failures)</c> num
20
+ /// run VERDE. Essa asserção é verdadeira exatamente quando o mecanismo esta morto — ela nao
21
+ /// distingue "nao houve falha" de "falha nenhuma e registrada". Uma lista vazia prova a
22
+ /// ausencia de conteudo, jamais a presenca de capacidade.
23
+ /// </para>
24
+ /// <para>
25
+ /// Por isso aqui nao se assere sobre <c>Failures</c> VAZIA: assere-se sobre o CONTEUDO de
26
+ /// <c>Failures</c> num run que falha de verdade — o par, a mesma forma que fechou o motor de
27
+ /// assercao um andar abaixo.
28
+ /// </para>
29
+ /// </remarks>
30
+ public sealed class EvalFailuresTests
31
+ {
32
+ /// <summary>Grava a cache do cenario com o texto dado, e devolve o fixture offline pronto.</summary>
33
+ /// <remarks>
34
+ /// A cache e semeada pelo <see cref="FakeChatClient"/>, NUNCA por chamada paga: o caminho
35
+ /// de escrita exercitado e o da biblioteca, e a leitura offline le exatamente o que aquela
36
+ /// escrita produziu.
37
+ /// </remarks>
38
+ private static async Task<EvalRunSummary> RunOfflineAsync(EvalWorkspace ws, params string[] responses)
39
+ {
40
+ using var fake = new FakeChatClient
41
+ {
42
+ MetadataProviderName = "morph-fake",
43
+ MetadataDefaultModelId = "modelo-exemplar",
44
+ };
45
+
46
+ foreach (var text in responses)
47
+ {
48
+ fake.EnqueueText(text);
49
+ }
50
+
51
+ await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
52
+ {
53
+ await refresh.RunAllAsync(TestContext.Current.CancellationToken);
54
+ }
55
+
56
+ await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
57
+ return await offline.RunAllAsync(TestContext.Current.CancellationToken);
58
+ }
59
+
60
+ [Fact]
61
+ public async Task AFailedScenarioIsRegisteredWithScenarioAssertionKindAndReason()
62
+ {
63
+ // O caso central: uma assercao reprova, e a falha chega ao sumario COM NOME. Sem isso
64
+ // o veredito viaja como contador — "Expected 0, Actual 1" — e quem le tem de abrir o
65
+ // dataset para descobrir o que quebrou.
66
+ using var ws = new EvalWorkspace();
67
+ ws.WriteScenario(
68
+ "triagem", "acme", "saudacao", "bom dia",
69
+ """[ { "name": "cumprimenta", "kind": "contains", "value": "bom dia" } ]""");
70
+
71
+ var summary = await RunOfflineAsync(ws, "Boa noite!");
72
+
73
+ Assert.Equal(1, summary.Failed);
74
+
75
+ var failure = Assert.Single(summary.Failures);
76
+ Assert.Equal("triagem.acme.saudacao", failure.Scenario);
77
+ Assert.Equal("cumprimenta", failure.Assertion);
78
+ Assert.Equal("Contains", failure.Kind);
79
+ Assert.Contains("FALHOU", failure.Reason, StringComparison.Ordinal);
80
+ Assert.Contains("bom dia", failure.Reason, StringComparison.Ordinal);
81
+
82
+ // A linha legivel, no formato que a rubrica eval-de-modelo.md cita como exemplo.
83
+ Assert.Contains(
84
+ "eval-fail: triagem.acme.saudacao",
85
+ summary.DescribeFailures(),
86
+ StringComparison.Ordinal);
87
+ }
88
+
89
+ [Fact]
90
+ public async Task TwoReprovedAssertionsInOneScenarioProduceTWOLinesEachNamingItsOwnRule()
91
+ {
92
+ // `Failures` e LISTA, e nao contador, porque as duas linhas dizem coisas diferentes:
93
+ // uma resposta pode quebrar em mais de uma regua, e isso separa "o prompt mudou" de
94
+ // "o modelo parou de responder". Se o tipo fosse um booleano, esta distincao morreria.
95
+ using var ws = new EvalWorkspace();
96
+ ws.WriteScenario(
97
+ "triagem", "acme", "saudacao", "bom dia",
98
+ """
99
+ [
100
+ { "name": "cumprimenta", "kind": "contains", "value": "bom dia" },
101
+ { "name": "sem-dado-pessoal", "kind": "no-pii" }
102
+ ]
103
+ """);
104
+
105
+ var summary = await RunOfflineAsync(ws, "Boa noite! Fale com lucas@example.com");
106
+
107
+ Assert.Equal(1, summary.Failed);
108
+ Assert.Equal(2, summary.Failures.Count);
109
+
110
+ Assert.Contains(summary.Failures, f => f.Assertion == "cumprimenta" && f.Kind == "Contains");
111
+ Assert.Contains(summary.Failures, f => f.Assertion == "sem-dado-pessoal" && f.Kind == "NoPii");
112
+
113
+ // Toda linha nomeia o cenario: uma falha sem cenario obriga a mesma busca que ela
114
+ // deveria dispensar.
115
+ Assert.All(summary.Failures, f => Assert.Equal("triagem.acme.saudacao", f.Scenario));
116
+ }
117
+
118
+ [Fact]
119
+ public async Task AScenarioWithNOAssertionsIsCountedANDNamed()
120
+ {
121
+ // O cenario sem assercao nenhuma reprova por VACUIDADE: ele nao cobra nada. Nesse caso
122
+ // nao existe outcome reprovado para nomear, e sem um ramo proprio ele seria contado em
123
+ // `Failed` e invisivel em `Failures` — um numero sem frase, que e metade exata do
124
+ // defeito que este tipo existe para fechar.
125
+ using var ws = new EvalWorkspace();
126
+ ws.WriteScenario("triagem", "acme", "vazio", "bom dia", "[]");
127
+
128
+ var summary = await RunOfflineAsync(ws, "Bom dia!");
129
+
130
+ Assert.Equal(1, summary.Failed);
131
+
132
+ var failure = Assert.Single(summary.Failures);
133
+ Assert.Equal("triagem.acme.vazio", failure.Scenario);
134
+ Assert.Contains("sem assercao deterministica", failure.Reason, StringComparison.Ordinal);
135
+
136
+ // Contado E nomeado: o numero e a frase tem de concordar sempre.
137
+ Assert.Equal(summary.Failed, summary.Failures.Count);
138
+ }
139
+
140
+ [Fact]
141
+ public async Task AGreenRunRegistersNoFailureAndSaysSoInWords()
142
+ {
143
+ // A reciproca vive AO LADO dos casos que falham de verdade, nunca sozinha: uma suite
144
+ // cuja unica testemunha e `Assert.Empty` num run verde e verdadeira precisamente
145
+ // quando o mecanismo esta morto.
146
+ using var ws = new EvalWorkspace();
147
+ ws.WriteScenario(
148
+ "triagem", "acme", "saudacao", "bom dia",
149
+ """[ { "name": "cumprimenta", "kind": "contains", "value": "bom dia" } ]""");
150
+
151
+ var summary = await RunOfflineAsync(ws, "Bom dia! Como posso ajudar?");
152
+
153
+ Assert.Equal(0, summary.Failed);
154
+ Assert.Empty(summary.Failures);
155
+ Assert.Equal("nenhuma falha de eval registrada", summary.DescribeFailures());
156
+ }
157
+
158
+ [Fact]
159
+ public async Task TheNamedFailuresSurviveTheROUNDTRIPThroughTheSummaryFileTheCLIReads()
160
+ {
161
+ // O sumario e um ARQUIVO: quem consome as falhas e o CLI, do outro lado do disco. Uma
162
+ // lista que existisse so em memoria seria invisivel exatamente onde precisa ser lida.
163
+ using var ws = new EvalWorkspace();
164
+ ws.WriteScenario(
165
+ "triagem", "acme", "saudacao", "bom dia",
166
+ """[ { "name": "cumprimenta", "kind": "contains", "value": "bom dia" } ]""");
167
+
168
+ await RunOfflineAsync(ws, "Boa noite!");
169
+
170
+ var reread = EvalRunSummary.ReadFrom(ws.SummaryPath);
171
+
172
+ Assert.NotNull(reread);
173
+ var failure = Assert.Single(reread!.Failures);
174
+ Assert.Equal("triagem.acme.saudacao", failure.Scenario);
175
+ Assert.Equal("cumprimenta", failure.Assertion);
176
+ Assert.Equal("Contains", failure.Kind);
177
+ }
178
+ }
@@ -0,0 +1,153 @@
1
+ using Microsoft.Extensions.AI.Evaluation.Reporting;
2
+ using Morph.AiKit.Evals;
3
+ using Morph.AiKit.Tests.Fakes;
4
+ using Xunit;
5
+
6
+ namespace Morph.AiKit.Tests.Evals;
7
+
8
+ /// <summary>
9
+ /// A prova que decide a feature: <b>com a cache quente, o eval roda sem credencial e sem
10
+ /// rede</b>.
11
+ /// </summary>
12
+ /// <remarks>
13
+ /// Essa frase foi derivada de leitura de fonte antes de existir codigo. Aqui ela vira
14
+ /// medicao: uma arvore de cache e gravada por um fake, o processo troca o cliente interno
15
+ /// por um que RECUSA qualquer chamada, e a suite termina verde. Se o cliente interno fosse
16
+ /// tocado uma unica vez, o contador dele deixaria de ser zero — e nenhuma das assercoes
17
+ /// abaixo passaria.
18
+ /// </remarks>
19
+ public sealed class EvalFixtureTests
20
+ {
21
+ [Fact]
22
+ public async Task RefreshWritesTheCacheAndOfflineReadsItBackWithoutInvokingTheInnerClient()
23
+ {
24
+ using var ws = new EvalWorkspace();
25
+ ws.WriteScenario("triagem", "acme", "saudacao", "bom dia", """[ { "kind": "contains", "value": "Ola" } ]""");
26
+ ws.WriteScenario("triagem", "acme", "handoff", "quero falar com humano", """[ { "kind": "contains", "value": "humano" } ]""");
27
+
28
+ // 1) GRAVA a arvore contra o fake — nunca contra uma API paga. O caminho de escrita
29
+ // exercitado e o da biblioteca, e a chave e computada pela mesma funcao que a
30
+ // leitura vai usar.
31
+ using var fake = new FakeChatClient
32
+ {
33
+ MetadataProviderName = "morph-fake",
34
+ MetadataDefaultModelId = "modelo-exemplar",
35
+ };
36
+ // A ORDEM importa e e a do dataset (ordinal por caminho): "handoff" vem antes de
37
+ // "saudacao". Enfileirar na ordem alfabetica errada foi como esta suite descobriu
38
+ // que LoadAll ordena — e a ordem estavel e o que faz duas maquinas gravarem a mesma
39
+ // arvore de cache.
40
+ fake.EnqueueText("Encaminhando para um humano agora.").EnqueueText("Ola! Bom dia.");
41
+
42
+ await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
43
+ {
44
+ var written = await refresh.RunAllAsync(TestContext.Current.CancellationToken);
45
+ Assert.Equal(2, written.Scenarios);
46
+ Assert.Equal(2, written.Passed);
47
+ Assert.Equal(EvalCacheState.Cold, written.Cache);
48
+ }
49
+
50
+ Assert.Equal(2, fake.CallCount);
51
+ Assert.True(File.Exists(EvalCacheStamp.PathFor(ws.CacheRoot)), "o refresh tem de deixar o carimbo na raiz da cache");
52
+
53
+ // 2) LE offline. O cliente interno agora RECUSA: se a cache nao respondesse, este
54
+ // bloco lancaria EvalCacheMissException em vez de terminar verde.
55
+ await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
56
+ var summary = await offline.RunAllAsync(TestContext.Current.CancellationToken);
57
+
58
+ Assert.Equal(2, summary.Scenarios);
59
+ Assert.Equal(2, summary.Passed);
60
+ Assert.Equal(0, summary.Failed);
61
+ Assert.Equal(EvalCacheState.Warm, summary.Cache);
62
+ Assert.Equal(0, summary.Misses);
63
+ Assert.Equal("ok", summary.Stamp);
64
+ Assert.Equal(0, offline.Offline!.Invocations);
65
+
66
+ // 3) O fake nao foi tocado de novo: a leitura offline nao passou por ele nem por
67
+ // acidente (ele esta noutro processo logico, mas a contagem prova a intencao).
68
+ Assert.Equal(2, fake.CallCount);
69
+ }
70
+
71
+ [Fact]
72
+ public async Task TheSummaryIsWrittenToThePathTheCallerDictates()
73
+ {
74
+ using var ws = new EvalWorkspace();
75
+ ws.WriteScenario("triagem", "acme", "saudacao", "bom dia", """[ { "kind": "contains", "value": "Ola" } ]""");
76
+
77
+ using var fake = new FakeChatClient { MetadataProviderName = "morph-fake", MetadataDefaultModelId = "modelo-exemplar" };
78
+ fake.EnqueueText("Ola!");
79
+ await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
80
+ {
81
+ await refresh.RunAllAsync(TestContext.Current.CancellationToken);
82
+ }
83
+
84
+ await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
85
+ await offline.RunAllAsync(TestContext.Current.CancellationToken);
86
+
87
+ // Ler de volta o ARQUIVO, e nao o objeto em memoria: o contrato com o CLI e o
88
+ // arquivo, e um teste que so olhasse o objeto seria um espelho do gerador.
89
+ var reread = EvalRunSummary.ReadFrom(ws.SummaryPath);
90
+ Assert.NotNull(reread);
91
+ Assert.Equal(1, reread!.Scenarios);
92
+ Assert.Equal(EvalCacheState.Warm, reread.Cache);
93
+ Assert.Equal("morph-fake", reread.Provider);
94
+ Assert.Equal("modelo-exemplar", reread.Model);
95
+ }
96
+
97
+ [Fact]
98
+ public void ATimeToLiveBelowOneYearIsRefusedWithTheReasonInTheMessage()
99
+ {
100
+ using var ws = new EvalWorkspace();
101
+ var options = ws.Options(EvalMode.Offline) with { TimeToLive = TimeSpan.FromDays(14) };
102
+
103
+ var error = Assert.Throws<ArgumentOutOfRangeException>(() => EvalFixture.Create(options));
104
+
105
+ Assert.Contains("eval-cache-ttl-too-short", error.Message, StringComparison.Ordinal);
106
+ // A mensagem tem de dizer POR QUE, e nao so que o numero e pequeno: a expiracao apaga
107
+ // o diretorio da chave durante a LEITURA.
108
+ Assert.Contains("DESTRUTIVA NA LEITURA", error.Message, StringComparison.Ordinal);
109
+ }
110
+
111
+ [Fact]
112
+ public void TheLibraryDefaultTimeToLiveWouldExpireACommittedCacheWithinWeeks()
113
+ {
114
+ // MEDIDO, nao lembrado: e este fato que torna o TTL explicito obrigatorio. Se um dia
115
+ // a biblioteca subir o default para alem do piso do kit, este teste morre e a
116
+ // exigencia pode ser rediscutida com um numero na mao.
117
+ Assert.True(
118
+ Defaults.DefaultTimeToLiveForCacheEntries < EvalCacheOptions.MinimumTimeToLive,
119
+ $"o default da biblioteca e {Defaults.DefaultTimeToLiveForCacheEntries.TotalDays:0} dias, "
120
+ + $"o piso do kit e {EvalCacheOptions.MinimumTimeToLive.TotalDays:0}");
121
+ Assert.Equal(14, Defaults.DefaultTimeToLiveForCacheEntries.TotalDays);
122
+ }
123
+
124
+ [Fact]
125
+ public void AWriteModeWithoutARealClientIsRefusedByName()
126
+ {
127
+ using var ws = new EvalWorkspace();
128
+
129
+ var error = Assert.Throws<InvalidOperationException>(
130
+ () => EvalFixture.Create(ws.Options(EvalMode.Refresh), liveClient: null));
131
+
132
+ Assert.Contains("eval-live-client-missing", error.Message, StringComparison.Ordinal);
133
+ }
134
+
135
+ [Fact]
136
+ public async Task AnEmptyDatasetIsAFailureAndNeverAGreenRunOfZeroScenarios()
137
+ {
138
+ using var ws = new EvalWorkspace();
139
+ EvalCacheStamp.Write(ws.CacheRoot, new EvalCacheStamp
140
+ {
141
+ PinnedAt = EvalWorkspace.Pin.PinnedAt,
142
+ Packages = EvalWorkspace.Pin.Packages,
143
+ Provider = "morph-fake",
144
+ Model = "modelo-exemplar",
145
+ });
146
+
147
+ await using var fixture = EvalFixture.Create(ws.Options(EvalMode.Offline));
148
+ var error = await Assert.ThrowsAsync<InvalidOperationException>(
149
+ () => fixture.RunAllAsync(TestContext.Current.CancellationToken));
150
+
151
+ Assert.Contains("eval-empty-run", error.Message, StringComparison.Ordinal);
152
+ }
153
+ }