@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.
- package/CLAUDE.md +22 -2
- package/README.md +2 -2
- package/bin/morph-spec.js +43 -5
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +20 -22
- package/framework/agents.json +198 -44
- package/framework/ai-pin.json +22 -0
- package/framework/commands/morph-media.md +45 -0
- package/framework/docs/CLI.md +238 -10
- package/framework/evals/proof-rubrics/README.md +76 -0
- package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
- package/framework/evals/proof-rubrics/fiacao.md +54 -0
- package/framework/evals/proof-rubrics/guard-cego.md +52 -0
- package/framework/evals/proof-rubrics/media-claims.md +109 -0
- package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
- package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
- package/framework/evals/rubrics/architecture.md +30 -0
- package/framework/evals/rubrics/code-quality.md +26 -0
- package/framework/evals/rubrics/test-coverage.md +20 -0
- package/framework/hooks/README.md +11 -1
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
- package/framework/hooks/claude-code/core/chain.js +27 -7
- package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
- package/framework/hooks/claude-code/core/worktree-context.js +83 -14
- package/framework/hooks/claude-code/core/worktree-create.js +206 -0
- package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
- package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
- package/framework/hooks/shared/command-tokens.js +23 -0
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/git-paths.js +25 -0
- package/framework/hooks/shared/hook-response.js +17 -0
- package/framework/hooks/shared/morph-cli.js +134 -0
- package/framework/rules/ai-agents-standards.md +14 -2
- package/framework/rules/infrastructure-standards.md +3 -1
- package/framework/schemas/ai-pin.schema.json +97 -0
- package/framework/schemas/config.schema.json +107 -0
- package/framework/schemas/feature.schema.json +23 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +28 -7
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +20 -2
- package/framework/skills/core/morph-media/SKILL.md +215 -0
- package/framework/skills/core/morph-plan/SKILL.md +38 -4
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +166 -9
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/PROVENANCE.json +81 -3
- package/framework/standards/STANDARDS.json +1504 -830
- package/framework/standards/ai-agents/agent-archetypes.md +160 -0
- package/framework/standards/ai-agents/agent-session.md +256 -160
- package/framework/standards/ai-agents/agent-spec.md +196 -0
- package/framework/standards/ai-agents/batching.md +217 -193
- package/framework/standards/ai-agents/context-providers.md +261 -188
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
- package/framework/standards/ai-agents/cost-and-budget.md +256 -0
- package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
- package/framework/standards/ai-agents/evals-with-cache.md +289 -0
- package/framework/standards/ai-agents/guardrails.md +222 -0
- package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
- package/framework/standards/ai-agents/mcp-server.md +351 -250
- package/framework/standards/ai-agents/mcp-tools.md +250 -197
- package/framework/standards/ai-agents/media-pipeline.md +352 -0
- package/framework/standards/ai-agents/media-video.md +267 -0
- package/framework/standards/ai-agents/middleware-patterns.md +241 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +422 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +177 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +189 -181
- package/framework/standards/ai-agents/modalities-vision.md +205 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +211 -155
- package/framework/standards/ai-agents/observability-patterns.md +227 -17
- package/framework/standards/ai-agents/production.md +260 -231
- package/framework/standards/ai-agents/prompt-sources.md +389 -0
- package/framework/standards/ai-agents/providers/model-registry.md +293 -158
- package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
- package/framework/standards/ai-agents/service-tier-flex.md +143 -135
- package/framework/standards/ai-agents/setup.md +166 -50
- package/framework/standards/ai-agents/structured-output.md +280 -249
- package/framework/standards/ai-agents/sweet-spot.md +129 -109
- package/framework/standards/ai-agents/testing-ai.md +236 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
- package/framework/standards/ai-agents/workflows.md +27 -7
- package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +90 -0
- package/framework/templates/REGISTRY.json +14 -1
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
- package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
- package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
- package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
- package/framework/templates/dotnet/ai-kit/README.md +128 -0
- package/framework/templates/dotnet/ai-kit/global.json +9 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
- package/package.json +3 -3
- package/src/commands/e2e/e2e.js +91 -40
- package/src/commands/project/doctor.js +143 -6
- package/src/commands/project/env.js +95 -0
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +274 -53
- package/src/commands/state/advance.js +24 -2
- package/src/commands/state/approve.js +33 -1
- package/src/commands/state/create.js +72 -11
- package/src/commands/state/delete.js +8 -0
- package/src/commands/state/finish.js +360 -53
- package/src/commands/state/gate-check.js +190 -6
- package/src/commands/state/score.js +36 -1
- package/src/commands/validation/verify.js +134 -25
- package/src/core/state/state-manager.js +9 -1
- package/src/dashboard/app/pages/HookInspector.jsx +430 -418
- package/src/dashboard/scanner/hooks-scanner.js +5 -1
- package/src/lib/ai/ai-checks.js +182 -0
- package/src/lib/ai/ai-project-scan.js +347 -0
- package/src/lib/ai/code-audit.js +181 -0
- package/src/lib/ai/comment-stripper.js +184 -0
- package/src/lib/ai/pin-drift.js +138 -0
- package/src/lib/ai/registry-audit.js +285 -0
- package/src/lib/e2e/compose-env.js +55 -14
- package/src/lib/e2e/stack-teardown.js +115 -0
- package/src/lib/evaluator-report.js +269 -15
- package/src/lib/generators/architecture-scanner.js +2 -1
- package/src/lib/git/default-branch.js +92 -4
- package/src/lib/graph/graphifyignore.js +2 -1
- package/src/lib/installers/effects.js +193 -0
- package/src/lib/installers/templates-reconcile.js +3 -3
- package/src/lib/risk-detector.js +284 -0
- package/src/lib/standards/verified-against.js +276 -0
- package/src/lib/tasks/eval-node.js +485 -0
- package/src/lib/tasks/linux-runner.js +297 -0
- package/src/lib/tasks/test-nodes.js +486 -0
- package/src/lib/tasks/test-runner.js +431 -37
- package/src/lib/validators/verify-runner.js +590 -28
- package/src/lib/worktree/dirs.js +32 -0
- package/src/lib/worktree/docker-audit.js +287 -0
- package/src/lib/worktree/env-allocator.js +69 -11
- package/src/lib/worktree/worktree-audit.js +70 -3
- package/src/scripts/setup-infra.js +24 -11
- package/src/utils/agents-installer.js +18 -8
- package/src/utils/claude-md-injector.js +10 -5
- package/src/utils/file-copier.js +28 -16
- package/src/utils/hooks-installer.js +75 -29
- package/src/utils/skills-installer.js +194 -187
- package/src/utils/version-checker.js +68 -3
- package/framework/standards/ai-agents/prompts-in-db.md +0 -214
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
using Morph.AiKit.Media.Providers;
|
|
2
|
+
|
|
3
|
+
namespace Morph.AiKit.Media;
|
|
4
|
+
|
|
5
|
+
/// <summary>
|
|
6
|
+
/// A implementacao de <see cref="IMediaGenerator"/>. <b><c>internal</c></b>: o projeto
|
|
7
|
+
/// enxerga a interface, nunca o tipo.
|
|
8
|
+
/// </summary>
|
|
9
|
+
/// <remarks>
|
|
10
|
+
/// A ORDEM do <see cref="EnqueueAsync"/> e o contrato, e ela e economia, nao estilo:
|
|
11
|
+
/// validar, resolver alias, derivar a chave de PEDIDO, <b>consultar a chave antes de
|
|
12
|
+
/// qualquer coisa paga</b>, so entao estimar custo e reservar teto, e por ultimo gravar o
|
|
13
|
+
/// job. Um retry que chega aqui reencontra o job e nao paga de novo; um estouro de teto
|
|
14
|
+
/// recusa antes de o dinheiro sair.
|
|
15
|
+
/// </remarks>
|
|
16
|
+
internal sealed class MediaGenerator : IMediaGenerator
|
|
17
|
+
{
|
|
18
|
+
private readonly IMediaAliasResolver _aliases;
|
|
19
|
+
private readonly IMediaJobRepository _jobs;
|
|
20
|
+
private readonly IMediaBudget? _budget;
|
|
21
|
+
private readonly IMediaClock _clock;
|
|
22
|
+
|
|
23
|
+
public MediaGenerator(
|
|
24
|
+
IMediaAliasResolver aliases,
|
|
25
|
+
IMediaJobRepository jobs,
|
|
26
|
+
IMediaBudget? budget,
|
|
27
|
+
IMediaClock clock)
|
|
28
|
+
{
|
|
29
|
+
ArgumentNullException.ThrowIfNull(aliases);
|
|
30
|
+
ArgumentNullException.ThrowIfNull(jobs);
|
|
31
|
+
ArgumentNullException.ThrowIfNull(clock);
|
|
32
|
+
|
|
33
|
+
_aliases = aliases;
|
|
34
|
+
_jobs = jobs;
|
|
35
|
+
_budget = budget;
|
|
36
|
+
_clock = clock;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// <inheritdoc />
|
|
40
|
+
public async Task<MediaResult<MediaJobId>> EnqueueAsync(
|
|
41
|
+
MediaRequest request,
|
|
42
|
+
CancellationToken cancellationToken = default)
|
|
43
|
+
{
|
|
44
|
+
ArgumentNullException.ThrowIfNull(request);
|
|
45
|
+
|
|
46
|
+
var valid = request.Validate();
|
|
47
|
+
if (!valid.IsSuccess)
|
|
48
|
+
{
|
|
49
|
+
return MediaResult<MediaJobId>.FailLike(valid);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var binding = _aliases.Resolve(request.Alias);
|
|
53
|
+
if (!binding.IsSuccess)
|
|
54
|
+
{
|
|
55
|
+
return MediaResult<MediaJobId>.FailLike(binding);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (binding.Value.Kind != request.Kind)
|
|
59
|
+
{
|
|
60
|
+
return MediaResult<MediaJobId>.Fail(
|
|
61
|
+
MediaErrorCode.InvalidRequest,
|
|
62
|
+
$"alias '{request.Alias}' produz {binding.Value.Kind}, e o pedido e de {request.Kind}.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
var key = MediaIdempotencyKey.From(request, binding.Value.Provider, binding.Value.Model);
|
|
66
|
+
|
|
67
|
+
// ANTES de qualquer coisa paga. Um pedido identico a outro ja atendido reusa o job
|
|
68
|
+
// existente; so um job em Failed vale outra tentativa.
|
|
69
|
+
var existing = await _jobs.FindByIdempotencyKeyAsync(key, cancellationToken).ConfigureAwait(false);
|
|
70
|
+
if (existing is not null && existing.State != MediaJobState.Failed)
|
|
71
|
+
{
|
|
72
|
+
return MediaResult<MediaJobId>.Ok(existing.Id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var estimate = binding.Value.Pricing.EstimateUsd(request);
|
|
76
|
+
if (!estimate.IsSuccess)
|
|
77
|
+
{
|
|
78
|
+
return MediaResult<MediaJobId>.FailLike(estimate);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (_budget is not null)
|
|
82
|
+
{
|
|
83
|
+
var reserved = await _budget
|
|
84
|
+
.TryReserveAsync(request, estimate.Value, cancellationToken)
|
|
85
|
+
.ConfigureAwait(false);
|
|
86
|
+
|
|
87
|
+
if (!reserved)
|
|
88
|
+
{
|
|
89
|
+
return MediaResult<MediaJobId>.Fail(
|
|
90
|
+
MediaErrorCode.BudgetExceeded,
|
|
91
|
+
$"alias '{request.Alias}': o custo estimado de {estimate.Value:F4} USD estoura o teto. " +
|
|
92
|
+
"A recusa e no enqueue de proposito — depois da chamada, o dinheiro ja saiu.");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
var job = MediaJob.Create(request, key, _clock.UtcNow);
|
|
97
|
+
await _jobs.SaveAsync(job, cancellationToken).ConfigureAwait(false);
|
|
98
|
+
|
|
99
|
+
return MediaResult<MediaJobId>.Ok(job.Id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// <inheritdoc />
|
|
103
|
+
public async Task<MediaResult<MediaJob>> GetAsync(MediaJobId id, CancellationToken cancellationToken = default)
|
|
104
|
+
{
|
|
105
|
+
var job = await _jobs.FindAsync(id, cancellationToken).ConfigureAwait(false);
|
|
106
|
+
|
|
107
|
+
return job is null
|
|
108
|
+
? MediaResult<MediaJob>.Fail(MediaErrorCode.NotFound, $"job {id} nao existe.")
|
|
109
|
+
: MediaResult<MediaJob>.Ok(job);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// <summary>
|
|
114
|
+
/// Executa um job: chama o provider, guarda os bytes, move o estado. <b><c>internal</c></b>
|
|
115
|
+
/// — e este e o unico tipo do subsistema que segura um <see cref="IMediaProvider"/>.
|
|
116
|
+
/// </summary>
|
|
117
|
+
/// <remarks>
|
|
118
|
+
/// A ordem tambem e o contrato: <c>Generating</c> antes da chamada (para que um worker
|
|
119
|
+
/// morto no meio deixe rastro), os bytes GRAVADOS antes de <c>Generated</c> (um job em
|
|
120
|
+
/// <c>Generated</c> sem bytes no storage mente sobre o proprio estado), e o bloqueio de
|
|
121
|
+
/// moderacao indo para <c>RejectedByPolicy</c> e nao para <c>Failed</c>.
|
|
122
|
+
/// </remarks>
|
|
123
|
+
internal sealed class MediaWorker : IMediaJobRunner
|
|
124
|
+
{
|
|
125
|
+
private readonly IMediaAliasResolver _aliases;
|
|
126
|
+
private readonly IMediaJobRepository _jobs;
|
|
127
|
+
private readonly IMediaStore _store;
|
|
128
|
+
private readonly IReadOnlyList<IMediaProvider> _providers;
|
|
129
|
+
private readonly IMediaClock _clock;
|
|
130
|
+
|
|
131
|
+
public MediaWorker(
|
|
132
|
+
IMediaAliasResolver aliases,
|
|
133
|
+
IMediaJobRepository jobs,
|
|
134
|
+
IMediaStore store,
|
|
135
|
+
IReadOnlyList<IMediaProvider> providers,
|
|
136
|
+
IMediaClock clock)
|
|
137
|
+
{
|
|
138
|
+
ArgumentNullException.ThrowIfNull(aliases);
|
|
139
|
+
ArgumentNullException.ThrowIfNull(jobs);
|
|
140
|
+
ArgumentNullException.ThrowIfNull(store);
|
|
141
|
+
ArgumentNullException.ThrowIfNull(providers);
|
|
142
|
+
ArgumentNullException.ThrowIfNull(clock);
|
|
143
|
+
|
|
144
|
+
_aliases = aliases;
|
|
145
|
+
_jobs = jobs;
|
|
146
|
+
_store = store;
|
|
147
|
+
_providers = providers;
|
|
148
|
+
_clock = clock;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// <inheritdoc />
|
|
152
|
+
public async Task<MediaResult<MediaJob>> RunAsync(MediaJobId id, CancellationToken cancellationToken = default)
|
|
153
|
+
{
|
|
154
|
+
var job = await _jobs.FindAsync(id, cancellationToken).ConfigureAwait(false);
|
|
155
|
+
if (job is null)
|
|
156
|
+
{
|
|
157
|
+
return MediaResult<MediaJob>.Fail(MediaErrorCode.NotFound, $"job {id} nao existe.");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
var binding = _aliases.Resolve(job.Request.Alias);
|
|
161
|
+
if (!binding.IsSuccess)
|
|
162
|
+
{
|
|
163
|
+
return await PersistAsync(job.Fail(binding.Error!.Message), cancellationToken).ConfigureAwait(false);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var provider = _providers.FirstOrDefault(p =>
|
|
167
|
+
p.Provider == binding.Value.Provider && p.Supports(job.Request.Kind));
|
|
168
|
+
|
|
169
|
+
if (provider is null)
|
|
170
|
+
{
|
|
171
|
+
return await PersistAsync(
|
|
172
|
+
job.Fail($"nenhum adaptador atende provider '{binding.Value.Provider}' para {job.Request.Kind}."),
|
|
173
|
+
cancellationToken).ConfigureAwait(false);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
var generating = job.StartGenerating();
|
|
177
|
+
if (!generating.IsSuccess)
|
|
178
|
+
{
|
|
179
|
+
return generating;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
job = generating.Value;
|
|
183
|
+
await _jobs.SaveAsync(job, cancellationToken).ConfigureAwait(false);
|
|
184
|
+
|
|
185
|
+
var produced = await provider.ProduceAsync(job.Request, binding.Value, cancellationToken).ConfigureAwait(false);
|
|
186
|
+
|
|
187
|
+
if (!produced.IsSuccess)
|
|
188
|
+
{
|
|
189
|
+
// A distincao que vale dinheiro: bloqueio de politica e terminal, falha tecnica
|
|
190
|
+
// e retryable. Colapsar as duas em "erro" e como um laco de retry vira fatura.
|
|
191
|
+
var transition = produced.Error!.Code == MediaErrorCode.PolicyBlocked
|
|
192
|
+
? job.RejectByPolicy(produced.Error.Message)
|
|
193
|
+
: job.Fail(produced.Error.Message);
|
|
194
|
+
|
|
195
|
+
return await PersistAsync(transition, cancellationToken).ConfigureAwait(false);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
var artifact = produced.Value;
|
|
199
|
+
var entry = await _store
|
|
200
|
+
.PutAsync(artifact.Bytes, artifact.MediaType, cancellationToken)
|
|
201
|
+
.ConfigureAwait(false);
|
|
202
|
+
|
|
203
|
+
var estimate = binding.Value.Pricing.EstimateUsd(job.Request);
|
|
204
|
+
|
|
205
|
+
var provenance = new MediaProvenance(
|
|
206
|
+
binding.Value.Provider,
|
|
207
|
+
binding.Value.Model,
|
|
208
|
+
job.Request.Prompt,
|
|
209
|
+
artifact.RevisedPrompt,
|
|
210
|
+
[.. job.Request.References.Select(r => r.Sha256Hex)],
|
|
211
|
+
job.Request.Options.ToCanonicalString(),
|
|
212
|
+
artifact.ResponseId,
|
|
213
|
+
estimate.IsSuccess ? estimate.Value : 0m,
|
|
214
|
+
_clock.UtcNow,
|
|
215
|
+
artifact.C2paPreserved);
|
|
216
|
+
|
|
217
|
+
return await PersistAsync(
|
|
218
|
+
job.MarkGenerated(entry.Hash, entry.RelativePath, provenance),
|
|
219
|
+
cancellationToken).ConfigureAwait(false);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async Task<MediaResult<MediaJob>> PersistAsync(
|
|
223
|
+
MediaResult<MediaJob> transition,
|
|
224
|
+
CancellationToken cancellationToken)
|
|
225
|
+
{
|
|
226
|
+
if (transition.IsSuccess)
|
|
227
|
+
{
|
|
228
|
+
await _jobs.SaveAsync(transition.Value, cancellationToken).ConfigureAwait(false);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return transition;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// <summary>
|
|
236
|
+
/// Monta os adaptadores de provider. <b><c>internal</c></b>, e e por isso que nao existe
|
|
237
|
+
/// caminho publico ate um <see cref="IMediaProvider"/>.
|
|
238
|
+
/// </summary>
|
|
239
|
+
internal static class MediaProviderCatalog
|
|
240
|
+
{
|
|
241
|
+
/// <summary>Os tres adaptadores que o kit traz.</summary>
|
|
242
|
+
public static IReadOnlyList<IMediaProvider> Build(IMediaClientFactory clients)
|
|
243
|
+
{
|
|
244
|
+
ArgumentNullException.ThrowIfNull(clients);
|
|
245
|
+
|
|
246
|
+
return
|
|
247
|
+
[
|
|
248
|
+
new OpenAIImageProvider(clients),
|
|
249
|
+
new GoogleImageProvider(clients),
|
|
250
|
+
new VeoVideoProvider(clients),
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
using System.Security.Cryptography;
|
|
2
|
+
using System.Text;
|
|
3
|
+
|
|
4
|
+
namespace Morph.AiKit.Media;
|
|
5
|
+
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// A chave de idempotencia do <b>PEDIDO</b> — a que dedupe a CHAMADA PAGA.
|
|
8
|
+
/// </summary>
|
|
9
|
+
/// <remarks>
|
|
10
|
+
/// <para>
|
|
11
|
+
/// <b>Ela nao e o <see cref="MediaHash"/>, e a diferenca e o defeito caro deste
|
|
12
|
+
/// subsistema.</b> Esta chave e o sha256 do PEDIDO (provider + modelo + prompt normalizado
|
|
13
|
+
/// + sha256 de cada referencia + opcoes) e responde "ja pagamos por isto?". O
|
|
14
|
+
/// <see cref="MediaHash"/> e o sha256 dos BYTES e responde "ja guardamos isto?". Duas
|
|
15
|
+
/// perguntas diferentes: dois pedidos distintos podem produzir bytes identicos, e o mesmo
|
|
16
|
+
/// pedido sempre produz o mesmo pedido.
|
|
17
|
+
/// </para>
|
|
18
|
+
/// <para>
|
|
19
|
+
/// Ela e consultada <b>antes</b> de chamar o provider. E o que faz um retry — de fila, de
|
|
20
|
+
/// worker reiniciado, de webhook duplicado — custar zero.
|
|
21
|
+
/// </para>
|
|
22
|
+
/// <para>
|
|
23
|
+
/// <b>O prompt e normalizado, nao minusculado.</b> Espacos em branco colapsam e as pontas
|
|
24
|
+
/// sao aparadas, porque um espaco a mais nao muda o pedido; a caixa e preservada, porque
|
|
25
|
+
/// num prompt ela muda (nomes de marca, texto literal entre aspas que vai aparecer na
|
|
26
|
+
/// imagem).
|
|
27
|
+
/// </para>
|
|
28
|
+
/// </remarks>
|
|
29
|
+
/// <param name="Value">sha256 em hex minusculo.</param>
|
|
30
|
+
public readonly record struct MediaIdempotencyKey(string Value)
|
|
31
|
+
{
|
|
32
|
+
/// <summary>Separador de CAMPO na forma canonica: UNIT SEPARATOR (U+001F).</summary>
|
|
33
|
+
/// <remarks>
|
|
34
|
+
/// Um caractere de controle, e nao <c>|</c> ou <c>:</c>, porque o separador nao pode
|
|
35
|
+
/// aparecer dentro de um campo. Um prompt que contivesse o separador emendaria dois
|
|
36
|
+
/// campos numa chave so, e duas requisicoes diferentes passariam a compartilhar a
|
|
37
|
+
/// chave — o que faz uma reusar o artefato PAGO da outra. Pior que pagar duas vezes.
|
|
38
|
+
/// Escrito como <c>(char)0x1F</c> e nao como o caractere cru: um byte de controle
|
|
39
|
+
/// invisivel no fonte e uma armadilha para o proximo editor.
|
|
40
|
+
/// </remarks>
|
|
41
|
+
private const char FieldSeparator = (char)0x1F;
|
|
42
|
+
|
|
43
|
+
/// <summary>Separador de ITEM dentro de um campo: RECORD SEPARATOR (U+001E).</summary>
|
|
44
|
+
private const char ItemSeparator = (char)0x1E;
|
|
45
|
+
|
|
46
|
+
/// <summary>
|
|
47
|
+
/// Deriva a chave do pedido resolvido.
|
|
48
|
+
/// </summary>
|
|
49
|
+
/// <param name="request">O pedido.</param>
|
|
50
|
+
/// <param name="provider">A familia do SDK que vai atender (<c>openai</c>, <c>google</c>).</param>
|
|
51
|
+
/// <param name="model">O modelo resolvido pelo alias — nunca o alias, que pode ser repontado.</param>
|
|
52
|
+
/// <exception cref="ArgumentNullException">Pedido nulo.</exception>
|
|
53
|
+
public static MediaIdempotencyKey From(MediaRequest request, string provider, string model)
|
|
54
|
+
{
|
|
55
|
+
ArgumentNullException.ThrowIfNull(request);
|
|
56
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(provider);
|
|
57
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(model);
|
|
58
|
+
|
|
59
|
+
var canonical = new StringBuilder()
|
|
60
|
+
.Append(provider).Append(FieldSeparator)
|
|
61
|
+
.Append(model).Append(FieldSeparator)
|
|
62
|
+
.Append(request.Kind).Append(FieldSeparator)
|
|
63
|
+
.Append(NormalizePrompt(request.Prompt)).Append(FieldSeparator)
|
|
64
|
+
.Append(string.Join(ItemSeparator, request.References.Select(r => r.Sha256Hex)))
|
|
65
|
+
.Append(FieldSeparator)
|
|
66
|
+
.Append(request.Options.ToCanonicalString())
|
|
67
|
+
.ToString();
|
|
68
|
+
|
|
69
|
+
return new MediaIdempotencyKey(
|
|
70
|
+
Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(canonical))));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// <summary>Apara as pontas e colapsa espacos em branco. NAO minuscula.</summary>
|
|
74
|
+
public static string NormalizePrompt(string prompt)
|
|
75
|
+
{
|
|
76
|
+
ArgumentNullException.ThrowIfNull(prompt);
|
|
77
|
+
|
|
78
|
+
var builder = new StringBuilder(prompt.Length);
|
|
79
|
+
var pendingSpace = false;
|
|
80
|
+
|
|
81
|
+
foreach (var ch in prompt)
|
|
82
|
+
{
|
|
83
|
+
if (char.IsWhiteSpace(ch))
|
|
84
|
+
{
|
|
85
|
+
pendingSpace = builder.Length > 0;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (pendingSpace)
|
|
90
|
+
{
|
|
91
|
+
builder.Append(' ');
|
|
92
|
+
pendingSpace = false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
builder.Append(ch);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return builder.ToString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// <summary>A chave, para log.</summary>
|
|
102
|
+
public override string ToString() => Value;
|
|
103
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
namespace Morph.AiKit.Media;
|
|
2
|
+
|
|
3
|
+
/// <summary>
|
|
4
|
+
/// Um pedido de midia com estado. <b>E o unico objeto de dominio que a superficie publica
|
|
5
|
+
/// devolve</b> — nunca bytes.
|
|
6
|
+
/// </summary>
|
|
7
|
+
/// <remarks>
|
|
8
|
+
/// <para>
|
|
9
|
+
/// As transicoes sao metodos que devolvem <see cref="MediaResult{T}"/> e um job NOVO: o
|
|
10
|
+
/// registro e imutavel, e uma transicao invalida e informacao devolvida, nunca excecao.
|
|
11
|
+
/// </para>
|
|
12
|
+
/// <para>
|
|
13
|
+
/// <b>Duas invariantes carregam o standard inteiro:</b>
|
|
14
|
+
/// </para>
|
|
15
|
+
/// <list type="number">
|
|
16
|
+
/// <item>
|
|
17
|
+
/// <see cref="Approve"/> e a UNICA porta para <see cref="MediaJobState.Approved"/>, e
|
|
18
|
+
/// ela exige um revisor com nome e carimbo de tempo. E assim que "ha revisao humana"
|
|
19
|
+
/// deixa de ser uma frase no README e vira um caminho que o codigo nao tem.
|
|
20
|
+
/// </item>
|
|
21
|
+
/// <item>
|
|
22
|
+
/// <see cref="Publish"/> falha a partir de qualquer estado que nao seja
|
|
23
|
+
/// <c>Approved</c>. Nao ha atalho, nem flag, nem sobrecarga "force".
|
|
24
|
+
/// </item>
|
|
25
|
+
/// </list>
|
|
26
|
+
/// <para>
|
|
27
|
+
/// E ha uma terceira, que vale dinheiro: <see cref="RejectByPolicy"/> produz um estado
|
|
28
|
+
/// terminal com <see cref="IsRetryable"/> falso. Bloqueio de moderacao nao e erro tecnico —
|
|
29
|
+
/// retenta-lo gasta para receber o mesmo "nao".
|
|
30
|
+
/// </para>
|
|
31
|
+
/// </remarks>
|
|
32
|
+
public sealed record MediaJob
|
|
33
|
+
{
|
|
34
|
+
private MediaJob(MediaJobId id, MediaRequest request, MediaIdempotencyKey key, DateTimeOffset createdAt)
|
|
35
|
+
{
|
|
36
|
+
Id = id;
|
|
37
|
+
Request = request;
|
|
38
|
+
IdempotencyKey = key;
|
|
39
|
+
CreatedAt = createdAt;
|
|
40
|
+
State = MediaJobState.Pending;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// <summary>A identidade. O unico valor que sai pela superficie publica.</summary>
|
|
44
|
+
public MediaJobId Id { get; private init; }
|
|
45
|
+
|
|
46
|
+
/// <summary>O pedido, congelado.</summary>
|
|
47
|
+
public MediaRequest Request { get; private init; }
|
|
48
|
+
|
|
49
|
+
/// <summary>A chave que dedupe a chamada paga.</summary>
|
|
50
|
+
public MediaIdempotencyKey IdempotencyKey { get; private init; }
|
|
51
|
+
|
|
52
|
+
/// <summary>Onde o job esta.</summary>
|
|
53
|
+
public MediaJobState State { get; private init; }
|
|
54
|
+
|
|
55
|
+
/// <summary>Quando o pedido foi aceito.</summary>
|
|
56
|
+
public DateTimeOffset CreatedAt { get; private init; }
|
|
57
|
+
|
|
58
|
+
/// <summary>sha256 dos bytes, a partir de <see cref="MediaJobState.Generated"/>.</summary>
|
|
59
|
+
public MediaHash? Hash { get; private init; }
|
|
60
|
+
|
|
61
|
+
/// <summary>Caminho RELATIVO no blob do projeto. Nunca URL de provider.</summary>
|
|
62
|
+
public string? RelativePath { get; private init; }
|
|
63
|
+
|
|
64
|
+
/// <summary>A proveniencia, gravada na geracao.</summary>
|
|
65
|
+
public MediaProvenance? Provenance { get; private init; }
|
|
66
|
+
|
|
67
|
+
/// <summary>Quem decidiu, quando, e o que escreveu. Nulo enquanto ninguem revisou.</summary>
|
|
68
|
+
public MediaReview? Review { get; private init; }
|
|
69
|
+
|
|
70
|
+
/// <summary>Por que falhou ou foi bloqueado. Nulo nos estados saudaveis.</summary>
|
|
71
|
+
public string? FailureReason { get; private init; }
|
|
72
|
+
|
|
73
|
+
/// <summary>Quando foi publicado.</summary>
|
|
74
|
+
public DateTimeOffset? PublishedAt { get; private init; }
|
|
75
|
+
|
|
76
|
+
/// <summary>
|
|
77
|
+
/// A marcação "gerado por IA" registrada na publicação, ou nulo quando a finalidade não
|
|
78
|
+
/// a exige. Ver <see cref="MediaDisclosure"/> e <see cref="Publish"/>.
|
|
79
|
+
/// </summary>
|
|
80
|
+
public MediaDisclosure? Disclosure { get; private init; }
|
|
81
|
+
|
|
82
|
+
/// <summary>
|
|
83
|
+
/// Se vale tentar de novo. <b>So <see cref="MediaJobState.Failed"/> e retryable.</b>
|
|
84
|
+
/// <see cref="MediaJobState.RejectedByPolicy"/> nao e, e essa e a diferenca que impede
|
|
85
|
+
/// um laco de retry de virar uma fatura.
|
|
86
|
+
/// </summary>
|
|
87
|
+
public bool IsRetryable => State == MediaJobState.Failed;
|
|
88
|
+
|
|
89
|
+
/// <summary>Nasce em <see cref="MediaJobState.Pending"/>. Nada foi gasto ainda.</summary>
|
|
90
|
+
/// <exception cref="ArgumentNullException">Pedido nulo.</exception>
|
|
91
|
+
public static MediaJob Create(MediaRequest request, MediaIdempotencyKey key, DateTimeOffset createdAt)
|
|
92
|
+
{
|
|
93
|
+
ArgumentNullException.ThrowIfNull(request);
|
|
94
|
+
return new MediaJob(MediaJobId.New(), request, key, createdAt);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// <summary>Pending para Generating: a chamada paga esta em voo.</summary>
|
|
98
|
+
public MediaResult<MediaJob> StartGenerating()
|
|
99
|
+
=> State == MediaJobState.Pending || State == MediaJobState.Failed
|
|
100
|
+
? MediaResult<MediaJob>.Ok(this with { State = MediaJobState.Generating, FailureReason = null })
|
|
101
|
+
: Invalid(nameof(StartGenerating), MediaJobState.Generating);
|
|
102
|
+
|
|
103
|
+
/// <summary>
|
|
104
|
+
/// Generating para Generated. Exige hash, caminho e proveniencia <b>juntos</b>: um job
|
|
105
|
+
/// em <c>Generated</c> sem bytes no seu storage esta mentindo sobre o proprio estado.
|
|
106
|
+
/// </summary>
|
|
107
|
+
public MediaResult<MediaJob> MarkGenerated(MediaHash hash, string relativePath, MediaProvenance provenance)
|
|
108
|
+
{
|
|
109
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(relativePath);
|
|
110
|
+
ArgumentNullException.ThrowIfNull(provenance);
|
|
111
|
+
|
|
112
|
+
return State == MediaJobState.Generating
|
|
113
|
+
? MediaResult<MediaJob>.Ok(this with
|
|
114
|
+
{
|
|
115
|
+
State = MediaJobState.Generated,
|
|
116
|
+
Hash = hash,
|
|
117
|
+
RelativePath = relativePath,
|
|
118
|
+
Provenance = provenance,
|
|
119
|
+
})
|
|
120
|
+
: Invalid(nameof(MarkGenerated), MediaJobState.Generated);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// <summary>Falha tecnica. Retryable.</summary>
|
|
124
|
+
public MediaResult<MediaJob> Fail(string reason)
|
|
125
|
+
{
|
|
126
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(reason);
|
|
127
|
+
|
|
128
|
+
return State is MediaJobState.Pending or MediaJobState.Generating
|
|
129
|
+
? MediaResult<MediaJob>.Ok(this with { State = MediaJobState.Failed, FailureReason = reason })
|
|
130
|
+
: Invalid(nameof(Fail), MediaJobState.Failed);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// <summary>
|
|
134
|
+
/// Bloqueio de moderacao. Terminal e NAO retryable — ver <see cref="IsRetryable"/>.
|
|
135
|
+
/// </summary>
|
|
136
|
+
public MediaResult<MediaJob> RejectByPolicy(string reason)
|
|
137
|
+
{
|
|
138
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(reason);
|
|
139
|
+
|
|
140
|
+
return State is MediaJobState.Pending or MediaJobState.Generating or MediaJobState.Generated
|
|
141
|
+
? MediaResult<MediaJob>.Ok(this with
|
|
142
|
+
{
|
|
143
|
+
State = MediaJobState.RejectedByPolicy,
|
|
144
|
+
FailureReason = reason,
|
|
145
|
+
})
|
|
146
|
+
: Invalid(nameof(RejectByPolicy), MediaJobState.RejectedByPolicy);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// <summary>
|
|
150
|
+
/// A UNICA porta para <see cref="MediaJobState.Approved"/>, e ela exige um humano
|
|
151
|
+
/// nomeado com carimbo de tempo.
|
|
152
|
+
/// </summary>
|
|
153
|
+
public MediaResult<MediaJob> Approve(MediaReview review)
|
|
154
|
+
{
|
|
155
|
+
ArgumentNullException.ThrowIfNull(review);
|
|
156
|
+
|
|
157
|
+
if (review.Decision != MediaReviewDecision.Approved)
|
|
158
|
+
{
|
|
159
|
+
return MediaResult<MediaJob>.Fail(
|
|
160
|
+
MediaErrorCode.InvalidTransition,
|
|
161
|
+
$"job {Id}: Approve recebeu uma revisao com decisao '{review.Decision}'. " +
|
|
162
|
+
"Aprovar com um veredito de rejeicao seria registrar o oposto do que o humano decidiu.");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return State == MediaJobState.Generated
|
|
166
|
+
? MediaResult<MediaJob>.Ok(this with { State = MediaJobState.Approved, Review = review })
|
|
167
|
+
: Invalid(nameof(Approve), MediaJobState.Approved);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// <summary>Recusa humana. Terminal.</summary>
|
|
171
|
+
public MediaResult<MediaJob> Reject(MediaReview review)
|
|
172
|
+
{
|
|
173
|
+
ArgumentNullException.ThrowIfNull(review);
|
|
174
|
+
|
|
175
|
+
if (review.Decision != MediaReviewDecision.Rejected)
|
|
176
|
+
{
|
|
177
|
+
return MediaResult<MediaJob>.Fail(
|
|
178
|
+
MediaErrorCode.InvalidTransition,
|
|
179
|
+
$"job {Id}: Reject recebeu uma revisao com decisao '{review.Decision}'.");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return State == MediaJobState.Generated
|
|
183
|
+
? MediaResult<MediaJob>.Ok(this with { State = MediaJobState.Rejected, Review = review })
|
|
184
|
+
: Invalid(nameof(Reject), MediaJobState.Rejected);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// <summary>
|
|
188
|
+
/// Publica. <b>Tres portas, e cada uma fecha um caminho que existia so na prosa:</b>
|
|
189
|
+
/// </summary>
|
|
190
|
+
/// <remarks>
|
|
191
|
+
/// <list type="number">
|
|
192
|
+
/// <item>
|
|
193
|
+
/// Falha a partir de qualquer estado que nao seja <see cref="MediaJobState.Approved"/>
|
|
194
|
+
/// — inclusive de <c>Generated</c>, que e o atalho que alguem sempre quer. E o que
|
|
195
|
+
/// torna a REVISAO HUMANA um caminho que o codigo nao tem.
|
|
196
|
+
/// </item>
|
|
197
|
+
/// <item>
|
|
198
|
+
/// Falha quando a finalidade nao e publicavel. <see cref="MediaPurpose.Draft"/> se
|
|
199
|
+
/// declarava "nunca publicavel" num comentario XML que nada cobrava.
|
|
200
|
+
/// </item>
|
|
201
|
+
/// <item>
|
|
202
|
+
/// Falha quando a finalidade exige MARCACAO e ela nao foi passada. Era a unica das
|
|
203
|
+
/// quatro afirmacoes da rubrica <c>media-claims</c> presa por prosa — e e a que
|
|
204
|
+
/// carrega a obrigacao legal (AI Act art. 50).
|
|
205
|
+
/// </item>
|
|
206
|
+
/// </list>
|
|
207
|
+
/// <para>
|
|
208
|
+
/// O que isto prova e o que nao prova esta escrito em <see cref="MediaDisclosure"/>: o
|
|
209
|
+
/// kit garante que a DECISAO foi tomada e gravada, nao que o rotulo chegou aos olhos de
|
|
210
|
+
/// alguem — a renderizacao e do aplicativo consumidor.
|
|
211
|
+
/// </para>
|
|
212
|
+
/// </remarks>
|
|
213
|
+
/// <param name="disclosure">
|
|
214
|
+
/// A marcacao. Pode ser nula somente quando <see cref="MediaPurpose.RequiresDisclosure"/>
|
|
215
|
+
/// e falso para a finalidade do pedido.
|
|
216
|
+
/// </param>
|
|
217
|
+
/// <param name="publishedAt">Quando.</param>
|
|
218
|
+
public MediaResult<MediaJob> Publish(MediaDisclosure? disclosure, DateTimeOffset publishedAt)
|
|
219
|
+
{
|
|
220
|
+
if (State != MediaJobState.Approved)
|
|
221
|
+
{
|
|
222
|
+
return MediaResult<MediaJob>.Fail(
|
|
223
|
+
MediaErrorCode.InvalidTransition,
|
|
224
|
+
$"job {Id}: Publish a partir de '{State}'. So um job Approved pode ser publicado, e " +
|
|
225
|
+
"Approved so existe por Approve(revisor). Publicar sem revisao humana e o defeito que " +
|
|
226
|
+
"esta maquina de estados existe para impedir.");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!Request.Purpose.IsPublishable())
|
|
230
|
+
{
|
|
231
|
+
return MediaResult<MediaJob>.Fail(
|
|
232
|
+
MediaErrorCode.InvalidTransition,
|
|
233
|
+
$"job {Id}: finalidade '{Request.Purpose}' nao publica. Rascunho e material interno; " +
|
|
234
|
+
"promove-lo a publicado apaga a distincao que a finalidade existe para carregar.");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (Request.Purpose.RequiresDisclosure() && disclosure is null)
|
|
238
|
+
{
|
|
239
|
+
return MediaResult<MediaJob>.Fail(
|
|
240
|
+
MediaErrorCode.InvalidTransition,
|
|
241
|
+
$"job {Id}: finalidade '{Request.Purpose}' exige marcacao visivel e nenhuma foi " +
|
|
242
|
+
"registrada. C2PA e SynthID vem embutidos e CAEM com screenshot ou re-encode; a " +
|
|
243
|
+
"marcacao visivel e obrigacao separada, e ela e cobrada AQUI, no ponto de publicacao. " +
|
|
244
|
+
"Ver ai-agents/media-pipeline.md, secao Marcacao.");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return MediaResult<MediaJob>.Ok(this with
|
|
248
|
+
{
|
|
249
|
+
State = MediaJobState.Published,
|
|
250
|
+
PublishedAt = publishedAt,
|
|
251
|
+
Disclosure = disclosure,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private MediaResult<MediaJob> Invalid(string transition, MediaJobState target)
|
|
256
|
+
=> MediaResult<MediaJob>.Fail(
|
|
257
|
+
MediaErrorCode.InvalidTransition,
|
|
258
|
+
$"job {Id}: {transition} pediu '{target}' a partir de '{State}' — essa transicao nao existe.");
|
|
259
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
namespace Morph.AiKit.Media;
|
|
2
|
+
|
|
3
|
+
/// <summary>
|
|
4
|
+
/// A identidade de um job de midia. <b>O unico valor que a superficie publica devolve</b> —
|
|
5
|
+
/// nunca bytes.
|
|
6
|
+
/// </summary>
|
|
7
|
+
/// <remarks>
|
|
8
|
+
/// <c>Guid.CreateVersion7()</c> e nao <c>Guid.NewGuid()</c>: v7 e ordenavel por tempo, o
|
|
9
|
+
/// que mantem o indice do banco do projeto sequencial em vez de espalhar escrita por toda
|
|
10
|
+
/// a arvore. E a regra da casa, escrita aqui porque este e o unico lugar do subsistema que
|
|
11
|
+
/// gera identidade.
|
|
12
|
+
/// </remarks>
|
|
13
|
+
/// <param name="Value">O GUID v7.</param>
|
|
14
|
+
public readonly record struct MediaJobId(Guid Value)
|
|
15
|
+
{
|
|
16
|
+
/// <summary>Uma identidade nova, ordenavel por tempo.</summary>
|
|
17
|
+
public static MediaJobId New() => new(Guid.CreateVersion7());
|
|
18
|
+
|
|
19
|
+
/// <summary>Forma compacta, para log e URL.</summary>
|
|
20
|
+
public override string ToString() => Value.ToString("N");
|
|
21
|
+
}
|