@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,264 @@
1
+ using System.Text;
2
+ using Morph.AiKit.Media;
3
+ using Morph.AiKit.Tests.Fakes;
4
+ using Xunit;
5
+
6
+ namespace Morph.AiKit.Tests;
7
+
8
+ /// <summary>
9
+ /// Canarios do enqueue e do worker. <b>Nenhum deles toca API paga</b> — o unico ponto que
10
+ /// gastaria dinheiro e o <see cref="FakeMediaProvider"/>, e a suite conta as chamadas dele.
11
+ /// </summary>
12
+ /// <remarks>
13
+ /// As afirmacoes aqui sao as tres que o dinheiro cobra: um retry nao paga de novo, um
14
+ /// estouro de teto recusa <b>antes</b> da chamada, e um bloqueio de moderacao vai para
15
+ /// <c>RejectedByPolicy</c> em vez de virar retry.
16
+ /// </remarks>
17
+ public sealed class MediaGeneratorTests
18
+ {
19
+ private static readonly DateTimeOffset T0 = new(2026, 9, 8, 12, 0, 0, TimeSpan.Zero);
20
+
21
+ private static CancellationToken Ct => TestContext.Current.CancellationToken;
22
+
23
+ private static MediaAliasBinding ImageBinding(decimal low = 0.006m) => new(
24
+ "catalogo", "fake", "gpt-image-2", MediaKind.Image, MediaPricing.PerImage(low, 0.053m, 0.211m));
25
+
26
+ private static MediaRequest Request(MediaOptions? options = null)
27
+ => new("catalogo", "um sofa verde", MediaPurpose.CatalogPhoto) { Options = options ?? MediaOptions.None };
28
+
29
+ private static (MediaGenerator Generator, MediaWorker Worker, InMemoryMediaJobs Jobs,
30
+ InMemoryMediaStore Store, FakeMediaProvider Provider) Build(
31
+ IMediaBudget? budget = null, MediaAliasBinding? binding = null)
32
+ {
33
+ var aliases = new StubAliasResolver().With(binding ?? ImageBinding());
34
+ var jobs = new InMemoryMediaJobs();
35
+ var store = new InMemoryMediaStore();
36
+ var provider = new FakeMediaProvider(provider: "fake");
37
+ var clock = new FixedMediaClock(T0);
38
+
39
+ return (
40
+ new MediaGenerator(aliases, jobs, budget, clock),
41
+ new MediaWorker(aliases, jobs, store, [provider], clock),
42
+ jobs, store, provider);
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------------
46
+ // Enqueue: id de job, nunca bytes
47
+ // ---------------------------------------------------------------------------------
48
+
49
+ /// <summary>Um pedido valido vira job em <c>Pending</c>, e o retorno e um id.</summary>
50
+ [Fact]
51
+ public async Task EnqueueReturnsAJobIdAndTheJobStartsPending()
52
+ {
53
+ var (generator, _, _, _, provider) = Build();
54
+
55
+ var enqueued = await generator.EnqueueAsync(Request(), Ct);
56
+
57
+ Assert.True(enqueued.IsSuccess);
58
+ var job = await generator.GetAsync(enqueued.Value, Ct);
59
+ Assert.Equal(MediaJobState.Pending, job.Value.State);
60
+
61
+ // Nada foi gasto: o enqueue NAO chama provider.
62
+ Assert.Equal(0, provider.Calls);
63
+ }
64
+
65
+ /// <summary>Pedido sem prompt e recusa nomeada, nao excecao.</summary>
66
+ [Fact]
67
+ public async Task AnInvalidRequestIsRefusedByName()
68
+ {
69
+ var (generator, _, _, _, _) = Build();
70
+
71
+ var result = await generator.EnqueueAsync(
72
+ new MediaRequest("catalogo", " ", MediaPurpose.Draft), Ct);
73
+
74
+ Assert.False(result.IsSuccess);
75
+ Assert.Equal(MediaErrorCode.InvalidRequest, result.Error!.Code);
76
+ }
77
+
78
+ /// <summary>Alias desconhecido: recusa, e nenhum job criado.</summary>
79
+ [Fact]
80
+ public async Task AnUnknownAliasIsRefused()
81
+ {
82
+ var (generator, _, jobs, _, _) = Build();
83
+
84
+ var result = await generator.EnqueueAsync(
85
+ new MediaRequest("inexistente", "p", MediaPurpose.Draft), Ct);
86
+
87
+ Assert.False(result.IsSuccess);
88
+ Assert.Equal(0, jobs.Saves);
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------------
92
+ // Idempotencia: o retry nao paga de novo
93
+ // ---------------------------------------------------------------------------------
94
+
95
+ /// <summary>
96
+ /// O MESMO pedido, enfileirado duas vezes, devolve o MESMO job. E o que faz um retry de
97
+ /// fila, de worker reiniciado ou de webhook duplicado custar zero.
98
+ /// </summary>
99
+ [Fact]
100
+ public async Task TheSameRequestEnqueuedTwiceReusesTheSameJob()
101
+ {
102
+ var (generator, _, jobs, _, _) = Build();
103
+
104
+ var first = await generator.EnqueueAsync(Request(), Ct);
105
+ var second = await generator.EnqueueAsync(Request(), Ct);
106
+
107
+ Assert.Equal(first.Value, second.Value);
108
+ Assert.Equal(1, jobs.Saves);
109
+ }
110
+
111
+ /// <summary>Pedido diferente, job diferente — a dedupe nao pode engolir trabalho novo.</summary>
112
+ [Fact]
113
+ public async Task ADifferentRequestGetsItsOwnJob()
114
+ {
115
+ var (generator, _, _, _, _) = Build();
116
+
117
+ var first = await generator.EnqueueAsync(Request(), Ct);
118
+ var second = await generator.EnqueueAsync(
119
+ new MediaRequest("catalogo", "um sofa azul", MediaPurpose.CatalogPhoto), Ct);
120
+
121
+ Assert.NotEqual(first.Value, second.Value);
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------------
125
+ // Teto de custo: recusa ANTES da chamada
126
+ // ---------------------------------------------------------------------------------
127
+
128
+ /// <summary>
129
+ /// O teto e confrontado com o custo estimado, e a recusa acontece no enqueue — antes de
130
+ /// existir qualquer chamada. Depois dela, o dinheiro ja saiu.
131
+ /// </summary>
132
+ [Fact]
133
+ public async Task ABudgetOverrunIsRefusedAtEnqueueBeforeAnyCall()
134
+ {
135
+ var budget = new CappedBudget(0.01m);
136
+ var (generator, _, jobs, _, provider) = Build(budget);
137
+
138
+ var result = await generator.EnqueueAsync(
139
+ Request(new MediaOptions { Quality = MediaQuality.High }), Ct);
140
+
141
+ Assert.False(result.IsSuccess);
142
+ Assert.Equal(MediaErrorCode.BudgetExceeded, result.Error!.Code);
143
+ Assert.Equal(0, jobs.Saves);
144
+ Assert.Equal(0, provider.Calls);
145
+ Assert.Equal([0.211m], budget.Seen);
146
+ }
147
+
148
+ /// <summary>O custo estimado que chega ao teto e o da tarifa, na quantidade pedida.</summary>
149
+ [Fact]
150
+ public async Task TheEstimateSentToTheBudgetIsTariffTimesCount()
151
+ {
152
+ var budget = new CappedBudget(100m);
153
+ var (generator, _, _, _, _) = Build(budget);
154
+
155
+ _ = await generator.EnqueueAsync(Request(new MediaOptions { Count = 4 }), Ct);
156
+
157
+ Assert.Equal([0.024m], budget.Seen);
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------------
161
+ // Worker: estados, storage e a distincao que vale dinheiro
162
+ // ---------------------------------------------------------------------------------
163
+
164
+ /// <summary>Caminho feliz: os bytes vao para o storage ANTES de o job virar Generated.</summary>
165
+ [Fact]
166
+ public async Task TheWorkerStoresTheBytesAndThenMarksGenerated()
167
+ {
168
+ var (generator, worker, _, store, provider) = Build();
169
+ provider.Returns(Encoding.UTF8.GetBytes("os bytes"), "image/png", "prompt reescrito");
170
+
171
+ var id = (await generator.EnqueueAsync(Request(), Ct)).Value;
172
+ var ran = await worker.RunAsync(id, Ct);
173
+
174
+ Assert.True(ran.IsSuccess);
175
+ Assert.Equal(MediaJobState.Generated, ran.Value.State);
176
+ Assert.Equal(1, store.Blobs.Writes);
177
+ Assert.Equal(MediaHash.Of(Encoding.UTF8.GetBytes("os bytes")), ran.Value.Hash);
178
+ Assert.EndsWith(".png", ran.Value.RelativePath, StringComparison.Ordinal);
179
+ }
180
+
181
+ /// <summary>A proveniencia e gravada com o modelo, o prompt reescrito e o custo estimado.</summary>
182
+ [Fact]
183
+ public async Task TheWorkerRecordsProvenanceWithTheEstimatedCost()
184
+ {
185
+ var (generator, worker, _, _, provider) = Build();
186
+ provider.Returns(Encoding.UTF8.GetBytes("x"), "image/png", "prompt reescrito");
187
+
188
+ var id = (await generator.EnqueueAsync(Request(), Ct)).Value;
189
+ var provenance = (await worker.RunAsync(id, Ct)).Value.Provenance;
190
+
191
+ Assert.NotNull(provenance);
192
+ Assert.Equal("gpt-image-2", provenance.ModelId);
193
+ Assert.Equal("fake", provenance.Provider);
194
+ Assert.Equal("prompt reescrito", provenance.RevisedPrompt);
195
+ Assert.Equal(0.006m, provenance.EstimatedCostUsd);
196
+ Assert.Equal(T0, provenance.CreatedAt);
197
+ Assert.True(provenance.C2paPreserved);
198
+ }
199
+
200
+ /// <summary>
201
+ /// Bloqueio de moderacao vira <c>RejectedByPolicy</c>, terminal. Um erro tecnico vira
202
+ /// <c>Failed</c>, retryable. Colapsar os dois e como um laco de retry vira fatura.
203
+ /// </summary>
204
+ [Theory]
205
+ [InlineData(MediaErrorCode.PolicyBlocked, MediaJobState.RejectedByPolicy, false)]
206
+ [InlineData(MediaErrorCode.ProviderFailure, MediaJobState.Failed, true)]
207
+ public async Task TheWorkerSeparatesPolicyBlocksFromTechnicalFailures(
208
+ MediaErrorCode produced, MediaJobState expected, bool retryable)
209
+ {
210
+ var (generator, worker, _, store, provider) = Build();
211
+ provider.Fails(produced);
212
+
213
+ var id = (await generator.EnqueueAsync(Request(), Ct)).Value;
214
+ var ran = await worker.RunAsync(id, Ct);
215
+
216
+ Assert.Equal(expected, ran.Value.State);
217
+ Assert.Equal(retryable, ran.Value.IsRetryable);
218
+ Assert.Equal(0, store.Blobs.Writes);
219
+ }
220
+
221
+ /// <summary>Job inexistente e recusa nomeada, nunca uma excecao de referencia nula.</summary>
222
+ [Fact]
223
+ public async Task RunningAnUnknownJobIsRefusedByName()
224
+ {
225
+ var (_, worker, _, _, _) = Build();
226
+
227
+ var ran = await worker.RunAsync(MediaJobId.New(), Ct);
228
+
229
+ Assert.False(ran.IsSuccess);
230
+ Assert.Equal(MediaErrorCode.NotFound, ran.Error!.Code);
231
+ }
232
+
233
+ /// <summary>
234
+ /// Um alias de VIDEO pedido como imagem nao vira job: as unidades de cobranca sao
235
+ /// diferentes, e um pedido com a tarifa errada estima o custo errado em silencio.
236
+ /// </summary>
237
+ [Fact]
238
+ public async Task AVideoAliasCannotServeAnImageRequest()
239
+ {
240
+ var (generator, _, _, _, _) = Build(binding: new MediaAliasBinding(
241
+ "catalogo", "fake", "veo-3.1-fast-generate-preview", MediaKind.Video, MediaPricing.PerSecond(0.10m)));
242
+
243
+ var result = await generator.EnqueueAsync(Request(), Ct);
244
+
245
+ Assert.False(result.IsSuccess);
246
+ Assert.Equal(MediaErrorCode.InvalidRequest, result.Error!.Code);
247
+ }
248
+
249
+ /// <summary>
250
+ /// Video sem duracao nao tem custo estimavel — e arbitrar segundos e arbitrar dinheiro.
251
+ /// </summary>
252
+ [Fact]
253
+ public async Task AVideoRequestWithoutDurationCannotBePriced()
254
+ {
255
+ var (generator, _, _, _, _) = Build(binding: new MediaAliasBinding(
256
+ "catalogo", "fake", "veo", MediaKind.Video, MediaPricing.PerSecond(0.10m)));
257
+
258
+ var result = await generator.EnqueueAsync(
259
+ new MediaRequest("catalogo", "um reel", MediaPurpose.Marketing) { Kind = MediaKind.Video }, Ct);
260
+
261
+ Assert.False(result.IsSuccess);
262
+ Assert.Equal(MediaErrorCode.PricingMissing, result.Error!.Code);
263
+ }
264
+ }
@@ -0,0 +1,148 @@
1
+ using System.Text;
2
+ using Morph.AiKit.Media;
3
+ using Xunit;
4
+
5
+ namespace Morph.AiKit.Tests;
6
+
7
+ /// <summary>
8
+ /// Canarios de <see cref="MediaIdempotencyKey"/>: a chave que dedupe a CHAMADA PAGA.
9
+ /// </summary>
10
+ /// <remarks>
11
+ /// Duas propriedades, e as duas custam dinheiro quando falham. <b>Estabilidade</b>: o mesmo
12
+ /// pedido tem de dar a mesma chave, senao todo retry paga de novo. <b>Sensibilidade</b>:
13
+ /// qualquer campo participante que mude tem de mudar a chave, senao um pedido reusa o
14
+ /// artefato pago de OUTRO — pior que pagar duas vezes.
15
+ /// </remarks>
16
+ public sealed class MediaIdempotencyKeyTests
17
+ {
18
+ private static MediaRequest Request(
19
+ string prompt = "um sofa verde",
20
+ MediaOptions? options = null,
21
+ params string[] references)
22
+ => new("catalogo", prompt, MediaPurpose.CatalogPhoto)
23
+ {
24
+ Options = options ?? MediaOptions.None,
25
+ References = [.. references.Select(r =>
26
+ new MediaReference(r, "image/png", Encoding.UTF8.GetBytes(r)))],
27
+ };
28
+
29
+ private static string KeyOf(MediaRequest request)
30
+ => MediaIdempotencyKey.From(request, "openai", "gpt-image-2").Value;
31
+
32
+ // ---------------------------------------------------------------------------------
33
+ // Estabilidade
34
+ // ---------------------------------------------------------------------------------
35
+
36
+ /// <summary>Duas construcoes do mesmo pedido dao a mesma chave.</summary>
37
+ [Fact]
38
+ public void TheSameRequestAlwaysGivesTheSameKey()
39
+ => Assert.Equal(KeyOf(Request()), KeyOf(Request()));
40
+
41
+ /// <summary>
42
+ /// Espaco em branco a mais nao muda o pedido, entao nao muda a chave. E o que impede um
43
+ /// prompt re-serializado com quebra de linha diferente de virar outra fatura.
44
+ /// </summary>
45
+ [Fact]
46
+ public void WhitespaceIsNormalisedAway()
47
+ => Assert.Equal(KeyOf(Request("um sofa verde")), KeyOf(Request(" um sofa\n verde ")));
48
+
49
+ /// <summary>
50
+ /// A caixa NAO e normalizada, e e deliberado: num prompt ela muda o resultado (nome de
51
+ /// marca, texto literal que vai aparecer na imagem).
52
+ /// </summary>
53
+ [Fact]
54
+ public void CaseIsPreservedBecauseItChangesThePicture()
55
+ => Assert.NotEqual(KeyOf(Request("um sofa VERDE")), KeyOf(Request("um sofa verde")));
56
+
57
+ /// <summary>A chave e um sha256 em hex minusculo: 64 digitos.</summary>
58
+ [Fact]
59
+ public void TheKeyIsASha256InLowercaseHex()
60
+ {
61
+ var key = KeyOf(Request());
62
+
63
+ Assert.Equal(64, key.Length);
64
+ Assert.Matches("^[0-9a-f]{64}$", key);
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------------
68
+ // Sensibilidade — uma rodada POR CAMPO participante
69
+ // ---------------------------------------------------------------------------------
70
+
71
+ /// <summary>Prompt diferente, chave diferente.</summary>
72
+ [Fact]
73
+ public void ADifferentPromptChangesTheKey()
74
+ => Assert.NotEqual(KeyOf(Request("um sofa azul")), KeyOf(Request()));
75
+
76
+ /// <summary>Modelo diferente, chave diferente — o alias pode ser repontado.</summary>
77
+ [Fact]
78
+ public void ADifferentModelChangesTheKey()
79
+ => Assert.NotEqual(
80
+ MediaIdempotencyKey.From(Request(), "openai", "gpt-image-2").Value,
81
+ MediaIdempotencyKey.From(Request(), "openai", "outro-modelo").Value);
82
+
83
+ /// <summary>Provider diferente, chave diferente.</summary>
84
+ [Fact]
85
+ public void ADifferentProviderChangesTheKey()
86
+ => Assert.NotEqual(
87
+ MediaIdempotencyKey.From(Request(), "openai", "m").Value,
88
+ MediaIdempotencyKey.From(Request(), "google", "m").Value);
89
+
90
+ /// <summary>
91
+ /// <b>Trocar a foto de referencia E outro pedido</b>, mesmo com o prompt identico. Por
92
+ /// isso o sha256 de CADA referencia participa, e nao a contagem delas.
93
+ /// </summary>
94
+ [Fact]
95
+ public void ADifferentReferenceChangesTheKey()
96
+ => Assert.NotEqual(
97
+ KeyOf(Request(references: ["foto-a"])),
98
+ KeyOf(Request(references: ["foto-b"])));
99
+
100
+ /// <summary>Mesma quantidade de referencias, conteudo trocado de ordem: outro pedido.</summary>
101
+ [Fact]
102
+ public void TheOrderOfReferencesIsPartOfTheRequest()
103
+ => Assert.NotEqual(
104
+ KeyOf(Request(references: ["a", "b"])),
105
+ KeyOf(Request(references: ["b", "a"])));
106
+
107
+ /// <summary>Sem referencia e com referencia sao pedidos diferentes.</summary>
108
+ [Fact]
109
+ public void AddingAReferenceChangesTheKey()
110
+ => Assert.NotEqual(KeyOf(Request()), KeyOf(Request(references: ["foto-a"])));
111
+
112
+ /// <summary>Cada opcao participante, uma rodada.</summary>
113
+ [Theory]
114
+ [MemberData(nameof(OptionVariations))]
115
+ public void EveryParticipatingOptionChangesTheKey(MediaOptions variation)
116
+ => Assert.NotEqual(KeyOf(Request()), KeyOf(Request(options: variation)));
117
+
118
+ /// <summary>As variacoes de opcao exercitadas, uma por campo canonico.</summary>
119
+ public static TheoryData<MediaOptions> OptionVariations() =>
120
+ [
121
+ new MediaOptions { Quality = MediaQuality.High },
122
+ new MediaOptions { Count = 4 },
123
+ new MediaOptions { Width = 1024, Height = 1024 },
124
+ new MediaOptions { MediaType = "image/webp" },
125
+ new MediaOptions { DurationSeconds = 8 },
126
+ new MediaOptions { AspectRatio = "9:16" },
127
+ ];
128
+
129
+ /// <summary>
130
+ /// O separador e um caractere de CONTROLE, e nao <c>|</c>. Um prompt que contivesse o
131
+ /// separador visivel emendaria campos e faria dois pedidos diferentes colidirem — que
132
+ /// e o defeito que faz um pedido reusar o artefato pago do outro.
133
+ /// </summary>
134
+ [Fact]
135
+ public void APromptFullOfPipesDoesNotCollideWithAnotherRequest()
136
+ => Assert.NotEqual(
137
+ KeyOf(Request("gpt-image-2|Image|um sofa")),
138
+ KeyOf(Request("um sofa")));
139
+
140
+ /// <summary>Normalizacao de prompt e pura e conferivel sozinha.</summary>
141
+ [Theory]
142
+ [InlineData(" a b ", "a b")]
143
+ [InlineData("a\n\nb", "a b")]
144
+ [InlineData("", "")]
145
+ [InlineData(" ", "")]
146
+ public void NormalizePromptCollapsesWhitespace(string input, string expected)
147
+ => Assert.Equal(expected, MediaIdempotencyKey.NormalizePrompt(input));
148
+ }
@@ -0,0 +1,184 @@
1
+ using Morph.AiKit.Media;
2
+ using Xunit;
3
+
4
+ namespace Morph.AiKit.Tests;
5
+
6
+ /// <summary>
7
+ /// Canarios da maquina de estados de <see cref="MediaJob"/>.
8
+ /// </summary>
9
+ /// <remarks>
10
+ /// Tres afirmacoes carregam o standard <c>ai-agents/media-pipeline.md</c> inteiro, e cada
11
+ /// uma e o inverso literal de um defeito caro:
12
+ /// <list type="number">
13
+ /// <item><c>Publish</c> falha fora de <c>Approved</c> — publicar sem revisao humana.</item>
14
+ /// <item><c>Approved</c> so existe por <c>Approve(revisor)</c> — "aprovado" sem quem.</item>
15
+ /// <item><c>RejectedByPolicy</c> nao e retryable — retry que vira fatura.</item>
16
+ /// </list>
17
+ /// </remarks>
18
+ public sealed class MediaJobStateTests
19
+ {
20
+ private static readonly DateTimeOffset T0 = new(2026, 9, 8, 12, 0, 0, TimeSpan.Zero);
21
+
22
+ /// <summary>Uma marcacao valida, para os casos que nao estao medindo a marcacao.</summary>
23
+ private static MediaDisclosure Label => MediaDisclosure.Create("Imagem gerada por IA", "legenda", T0);
24
+
25
+ private static MediaJob Pending() => MediaJob.Create(
26
+ new MediaRequest("catalogo", "um sofa verde", MediaPurpose.CatalogPhoto),
27
+ new MediaIdempotencyKey("chave"),
28
+ T0);
29
+
30
+ private static MediaJob Generated()
31
+ {
32
+ var job = Pending().StartGenerating().Value;
33
+ return job.MarkGenerated(
34
+ new MediaHash("abc"),
35
+ "ab/c/abc.png",
36
+ new MediaProvenance("openai", "gpt-image-2", "um sofa verde", null, [], "-", null, 0.05m, T0, true))
37
+ .Value;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------------
41
+ // 1. Publish so a partir de Approved
42
+ // ---------------------------------------------------------------------------------
43
+
44
+ /// <summary>
45
+ /// O atalho que alguem sempre quer: publicar direto de <c>Generated</c>. Ele nao
46
+ /// existe, e a mensagem diz por que.
47
+ /// </summary>
48
+ [Fact]
49
+ public void PublishFailsFromGeneratedBecauseNobodyReviewedIt()
50
+ {
51
+ var result = Generated().Publish(Label, T0);
52
+
53
+ Assert.False(result.IsSuccess);
54
+ Assert.Equal(MediaErrorCode.InvalidTransition, result.Error!.Code);
55
+ Assert.Contains("Approved", result.Error.Message, StringComparison.Ordinal);
56
+ }
57
+
58
+ /// <summary>Publish falha a partir de TODO estado que nao seja Approved — nao so de Generated.</summary>
59
+ [Theory]
60
+ [InlineData(MediaJobState.Pending)]
61
+ [InlineData(MediaJobState.Generating)]
62
+ [InlineData(MediaJobState.Generated)]
63
+ [InlineData(MediaJobState.Rejected)]
64
+ [InlineData(MediaJobState.RejectedByPolicy)]
65
+ [InlineData(MediaJobState.Failed)]
66
+ public void PublishFailsFromEveryStateThatIsNotApproved(MediaJobState state)
67
+ {
68
+ var job = JobIn(state);
69
+
70
+ Assert.NotEqual(MediaJobState.Approved, job.State);
71
+ Assert.False(job.Publish(Label, T0).IsSuccess);
72
+ }
73
+
74
+ /// <summary>E, com a revisao humana no lugar, publica.</summary>
75
+ [Fact]
76
+ public void PublishSucceedsFromApproved()
77
+ {
78
+ var approved = Generated().Approve(MediaReview.ApprovedBy("lucas", T0)).Value;
79
+
80
+ var published = approved.Publish(Label, T0);
81
+
82
+ Assert.True(published.IsSuccess);
83
+ Assert.Equal(MediaJobState.Published, published.Value.State);
84
+ Assert.Equal(T0, published.Value.PublishedAt);
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------------
88
+ // 2. Approved so por Approve(revisor)
89
+ // ---------------------------------------------------------------------------------
90
+
91
+ /// <summary>A aprovacao carrega quem e quando — sem isso e um campo booleano.</summary>
92
+ [Fact]
93
+ public void ApproveStampsTheReviewerAndTheTime()
94
+ {
95
+ var approved = Generated().Approve(MediaReview.ApprovedBy("lucas", T0, "cor confere")).Value;
96
+
97
+ Assert.Equal(MediaJobState.Approved, approved.State);
98
+ Assert.Equal("lucas", approved.Review!.Reviewer);
99
+ Assert.Equal(T0, approved.Review.DecidedAt);
100
+ Assert.Equal("cor confere", approved.Review.Note);
101
+ }
102
+
103
+ /// <summary>Revisor em branco nao carimba nada, e a recusa e na construcao da revisao.</summary>
104
+ [Fact]
105
+ public void AnAnonymousReviewIsRefusedAtConstruction()
106
+ => Assert.Throws<ArgumentException>(() => MediaReview.ApprovedBy(" ", T0));
107
+
108
+ /// <summary>
109
+ /// Aprovar com um veredito de REJEICAO seria registrar o oposto do que o humano
110
+ /// decidiu. O tipo recusa.
111
+ /// </summary>
112
+ [Fact]
113
+ public void ApproveRefusesAReviewThatSaysRejected()
114
+ {
115
+ var result = Generated().Approve(MediaReview.RejectedBy("lucas", T0));
116
+
117
+ Assert.False(result.IsSuccess);
118
+ Assert.Equal(MediaErrorCode.InvalidTransition, result.Error!.Code);
119
+ }
120
+
121
+ /// <summary>Aprovar antes de existirem bytes nao e transicao.</summary>
122
+ [Fact]
123
+ public void ApproveFailsBeforeTheBytesExist()
124
+ => Assert.False(Pending().Approve(MediaReview.ApprovedBy("lucas", T0)).IsSuccess);
125
+
126
+ // ---------------------------------------------------------------------------------
127
+ // 3. RejectedByPolicy nao e retryable; Failed e
128
+ // ---------------------------------------------------------------------------------
129
+
130
+ /// <summary>
131
+ /// A distincao que vale dinheiro. Bloqueio de moderacao e resposta definitiva; erro
132
+ /// tecnico e tentativa perdida.
133
+ /// </summary>
134
+ [Fact]
135
+ public void PolicyBlockIsTerminalAndTechnicalFailureIsNot()
136
+ {
137
+ var blocked = Pending().StartGenerating().Value.RejectByPolicy("moderacao").Value;
138
+ var failed = Pending().StartGenerating().Value.Fail("HTTP 503").Value;
139
+
140
+ Assert.Equal(MediaJobState.RejectedByPolicy, blocked.State);
141
+ Assert.False(blocked.IsRetryable);
142
+ Assert.False(blocked.StartGenerating().IsSuccess);
143
+
144
+ Assert.Equal(MediaJobState.Failed, failed.State);
145
+ Assert.True(failed.IsRetryable);
146
+ Assert.True(failed.StartGenerating().IsSuccess);
147
+ }
148
+
149
+ /// <summary>Um job em <c>Generated</c> carrega hash, caminho e proveniencia — os tres juntos.</summary>
150
+ [Fact]
151
+ public void GeneratedCarriesHashPathAndProvenanceTogether()
152
+ {
153
+ var job = Generated();
154
+
155
+ Assert.Equal(MediaJobState.Generated, job.State);
156
+ Assert.Equal("abc", job.Hash!.Value.Value);
157
+ Assert.Equal("ab/c/abc.png", job.RelativePath);
158
+ Assert.Equal("gpt-image-2", job.Provenance!.ModelId);
159
+ }
160
+
161
+ /// <summary>Uma transicao recusada NAO muda o job: o registro e imutavel.</summary>
162
+ [Fact]
163
+ public void ARefusedTransitionLeavesTheJobUntouched()
164
+ {
165
+ var job = Generated();
166
+
167
+ _ = job.Publish(Label, T0);
168
+
169
+ Assert.Equal(MediaJobState.Generated, job.State);
170
+ Assert.Null(job.PublishedAt);
171
+ }
172
+
173
+ private static MediaJob JobIn(MediaJobState state) => state switch
174
+ {
175
+ MediaJobState.Pending => Pending(),
176
+ MediaJobState.Generating => Pending().StartGenerating().Value,
177
+ MediaJobState.Generated => Generated(),
178
+ MediaJobState.Approved => Generated().Approve(MediaReview.ApprovedBy("lucas", T0)).Value,
179
+ MediaJobState.Rejected => Generated().Reject(MediaReview.RejectedBy("lucas", T0)).Value,
180
+ MediaJobState.RejectedByPolicy => Pending().StartGenerating().Value.RejectByPolicy("moderacao").Value,
181
+ MediaJobState.Failed => Pending().StartGenerating().Value.Fail("HTTP 503").Value,
182
+ _ => throw new ArgumentOutOfRangeException(nameof(state)),
183
+ };
184
+ }
@@ -0,0 +1,89 @@
1
+ using Morph.AiKit.Media;
2
+ using Xunit;
3
+
4
+ namespace Morph.AiKit.Tests;
5
+
6
+ /// <summary>
7
+ /// Canarios de <see cref="MediaReview"/>.
8
+ /// </summary>
9
+ /// <remarks>
10
+ /// A afirmacao que este arquivo protege nao e "aprovar funciona" — e que
11
+ /// <see cref="MediaReview"/> <b>delega</b> ao <see cref="MediaJob"/> em vez de reimplementar
12
+ /// a regra. Uma segunda copia de "so Approved publica" e exatamente como as duas divergem, e
13
+ /// e o padrao de falha que esta epic catalogou sete vezes: o teste mede uma COPIA do objeto.
14
+ /// </remarks>
15
+ public sealed class MediaReviewTests
16
+ {
17
+ private static readonly DateTimeOffset T0 = new(2026, 9, 8, 12, 0, 0, TimeSpan.Zero);
18
+
19
+ private static MediaDisclosure Label => MediaDisclosure.Create("Imagem gerada por IA", "legenda", T0);
20
+
21
+ private static MediaJob Generated()
22
+ {
23
+ var job = MediaJob.Create(
24
+ new MediaRequest("catalogo", "um sofa verde", MediaPurpose.CatalogPhoto),
25
+ new MediaIdempotencyKey("chave"),
26
+ T0).StartGenerating().Value;
27
+
28
+ return job.MarkGenerated(
29
+ new MediaHash("abc"),
30
+ "ab/c/abc.png",
31
+ new MediaProvenance("openai", "gpt-image-2", "p", null, [], "-", null, 0m, T0, true)).Value;
32
+ }
33
+
34
+ /// <summary>O atalho aprova e carimba.</summary>
35
+ [Fact]
36
+ public void ApproveDelegatesAndStamps()
37
+ {
38
+ var result = MediaReview.Approve(Generated(), "lucas", T0, "ok");
39
+
40
+ Assert.True(result.IsSuccess);
41
+ Assert.Equal(MediaJobState.Approved, result.Value.State);
42
+ Assert.Equal("lucas", result.Value.Review!.Reviewer);
43
+ }
44
+
45
+ /// <summary>Rejeitar e terminal e tambem carimba.</summary>
46
+ [Fact]
47
+ public void RejectDelegatesAndStamps()
48
+ {
49
+ var result = MediaReview.Reject(Generated(), "lucas", T0, "cor errada");
50
+
51
+ Assert.True(result.IsSuccess);
52
+ Assert.Equal(MediaJobState.Rejected, result.Value.State);
53
+ Assert.Equal(MediaReviewDecision.Rejected, result.Value.Review!.Decision);
54
+ }
55
+
56
+ /// <summary>
57
+ /// O atalho de publicar NAO tem uma regra propria: ele recusa exatamente onde
58
+ /// <see cref="MediaJob.Publish"/> recusa. Confere-se comparando as DUAS chamadas sobre o
59
+ /// MESMO job — se <c>MediaReview</c> tivesse copiado a regra, aqui e onde a copia
60
+ /// divergiria.
61
+ /// </summary>
62
+ [Fact]
63
+ public void PublishThroughTheReviewFacadeAgreesWithTheJobItself()
64
+ {
65
+ var generated = Generated();
66
+ var approved = generated.Approve(MediaReview.ApprovedBy("lucas", T0)).Value;
67
+
68
+ Assert.Equal(generated.Publish(Label, T0).IsSuccess, MediaReview.Publish(generated, Label, T0).IsSuccess);
69
+ Assert.Equal(approved.Publish(Label, T0).IsSuccess, MediaReview.Publish(approved, Label, T0).IsSuccess);
70
+
71
+ Assert.False(MediaReview.Publish(generated, Label, T0).IsSuccess);
72
+ Assert.True(MediaReview.Publish(approved, Label, T0).IsSuccess);
73
+ }
74
+
75
+ /// <summary>Job nulo e falha de composicao, nao Result: quem chama errou o codigo.</summary>
76
+ [Fact]
77
+ public void ANullJobIsAProgrammingError()
78
+ => Assert.Throws<ArgumentNullException>(() => MediaReview.Approve(null!, "lucas", T0));
79
+
80
+ /// <summary>Um juiz LLM cabe na nota, nunca no campo do revisor de uma foto de catalogo.</summary>
81
+ [Fact]
82
+ public void TheNoteCarriesThePreFilterVerdict()
83
+ {
84
+ var review = MediaReview.ApprovedBy("lucas", T0, "juiz LLM: 8/10 em fidelidade de cor");
85
+
86
+ Assert.Equal("lucas", review.Reviewer);
87
+ Assert.Contains("juiz LLM", review.Note, StringComparison.Ordinal);
88
+ }
89
+ }