@polymorphism-tech/morph-spec 8.34.2 → 8.35.1
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 -5
- package/README.md +2 -2
- package/bin/morph-spec.js +18 -4
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +16 -17
- package/framework/agents.json +197 -44
- package/framework/ai-pin.json +22 -0
- package/framework/commands/morph-media.md +45 -0
- package/framework/docs/CLI.md +236 -11
- 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/claude-code/core/protect-spec-files.js +41 -4
- package/framework/hooks/claude-code/core/worktree-env-inject.js +43 -0
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/e2e-check.js +135 -22
- package/framework/hooks/shared/payload-utils.js +15 -2
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rules/ai-agents-standards.md +14 -2
- 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 +26 -5
- 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 +18 -2
- package/framework/skills/core/morph-media/SKILL.md +215 -0
- package/framework/skills/core/morph-plan/SKILL.md +33 -2
- 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 +1483 -839
- package/framework/standards/ai-agents/agent-archetypes.md +160 -0
- package/framework/standards/ai-agents/agent-session.md +164 -68
- package/framework/standards/ai-agents/agent-spec.md +196 -0
- package/framework/standards/ai-agents/batching.md +61 -37
- package/framework/standards/ai-agents/context-providers.md +97 -24
- 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 +119 -18
- package/framework/standards/ai-agents/mcp-tools.md +85 -32
- 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 +39 -5
- package/framework/standards/ai-agents/modalities-image-gen.md +354 -105
- package/framework/standards/ai-agents/modalities-speech-to-text.md +8 -2
- package/framework/standards/ai-agents/modalities-text-to-speech.md +10 -2
- package/framework/standards/ai-agents/modalities-vision.md +6 -2
- package/framework/standards/ai-agents/multi-agent-patterns.md +61 -5
- package/framework/standards/ai-agents/observability-patterns.md +227 -17
- package/framework/standards/ai-agents/production.md +38 -9
- package/framework/standards/ai-agents/prompt-sources.md +389 -0
- package/framework/standards/ai-agents/providers/model-registry.md +174 -39
- package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
- package/framework/standards/ai-agents/service-tier-flex.md +27 -19
- package/framework/standards/ai-agents/setup.md +166 -50
- package/framework/standards/ai-agents/structured-output.md +34 -3
- package/framework/standards/ai-agents/sweet-spot.md +23 -3
- 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 +3 -1
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +13 -0
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +1 -1
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +1 -1
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- 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/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +3 -3
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/e2e/e2e.js +15 -2
- package/src/commands/index.js +13 -13
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +148 -6
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +76 -4
- 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/finish.js +379 -24
- 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/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +9 -1
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- 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/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/e2e/compose-env.js +50 -4
- package/src/lib/evaluator-report.js +269 -15
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/git/default-branch.js +92 -4
- package/src/lib/git/pr-files.js +56 -0
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/effects.js +193 -0
- package/src/lib/installers/mcp-installer.js +719 -719
- 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/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-nodes.js +486 -0
- package/src/lib/tasks/test-runner.js +410 -33
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/verify-runner.js +583 -28
- package/src/lib/worktree/docker-audit.js +287 -0
- package/src/lib/worktree/env-allocator.js +24 -0
- package/src/lib/worktree/worktree-audit.js +30 -1
- package/src/scripts/setup-infra.js +24 -11
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +18 -8
- package/src/utils/claude-md-injector.js +10 -5
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +11 -4
- package/src/utils/hooks-installer.js +29 -26
- package/src/utils/skills-installer.js +36 -29
- package/src/utils/version-checker.js +68 -3
- package/framework/standards/ai-agents/prompts-in-db.md +0 -214
|
@@ -0,0 +1,98 @@
|
|
|
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 <see cref="MediaStore"/>: endereco por conteudo, e gravacao unica.
|
|
10
|
+
/// </summary>
|
|
11
|
+
/// <remarks>
|
|
12
|
+
/// A armadilha deste arquivo, e a razao de <see cref="InMemoryMediaBlobs.Writes"/> existir:
|
|
13
|
+
/// um teste que so conferisse "o hash e o mesmo" <b>nao distingue dedupe de regravacao</b>,
|
|
14
|
+
/// porque os dois produzem o mesmo hash. A afirmacao "grava uma vez" so e verificavel
|
|
15
|
+
/// contando gravacoes no blob.
|
|
16
|
+
/// </remarks>
|
|
17
|
+
public sealed class MediaStoreTests
|
|
18
|
+
{
|
|
19
|
+
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
|
20
|
+
|
|
21
|
+
private static ReadOnlyMemory<byte> Bytes(string s) => Encoding.UTF8.GetBytes(s);
|
|
22
|
+
|
|
23
|
+
/// <summary>Mesmo conteudo, mesmo hash, mesmo caminho — e UMA gravacao.</summary>
|
|
24
|
+
[Fact]
|
|
25
|
+
public async Task TheSameContentIsStoredOnce()
|
|
26
|
+
{
|
|
27
|
+
var blobs = new InMemoryMediaBlobs();
|
|
28
|
+
var store = new MediaStore(blobs);
|
|
29
|
+
|
|
30
|
+
var first = await store.PutAsync(Bytes("os bytes"), "image/png", Ct);
|
|
31
|
+
var second = await store.PutAsync(Bytes("os bytes"), "image/png", Ct);
|
|
32
|
+
|
|
33
|
+
Assert.Equal(first.Hash, second.Hash);
|
|
34
|
+
Assert.Equal(first.RelativePath, second.RelativePath);
|
|
35
|
+
Assert.False(first.AlreadyExisted);
|
|
36
|
+
Assert.True(second.AlreadyExisted);
|
|
37
|
+
Assert.Equal(1, blobs.Writes);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// <summary>Conteudo diferente, endereco diferente, duas gravacoes.</summary>
|
|
41
|
+
[Fact]
|
|
42
|
+
public async Task DifferentContentGetsADifferentAddress()
|
|
43
|
+
{
|
|
44
|
+
var blobs = new InMemoryMediaBlobs();
|
|
45
|
+
var store = new MediaStore(blobs);
|
|
46
|
+
|
|
47
|
+
var a = await store.PutAsync(Bytes("um"), "image/png", Ct);
|
|
48
|
+
var b = await store.PutAsync(Bytes("outro"), "image/png", Ct);
|
|
49
|
+
|
|
50
|
+
Assert.NotEqual(a.Hash, b.Hash);
|
|
51
|
+
Assert.NotEqual(a.RelativePath, b.RelativePath);
|
|
52
|
+
Assert.Equal(2, blobs.Writes);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// <summary>
|
|
56
|
+
/// O hash e o do CONTEUDO, e nao do conteudo mais o MIME: o mesmo PNG servido como
|
|
57
|
+
/// <c>image/png</c> e como <c>application/octet-stream</c> e o mesmo arquivo.
|
|
58
|
+
/// </summary>
|
|
59
|
+
[Fact]
|
|
60
|
+
public void TheHashIsOfTheContentAlone()
|
|
61
|
+
=> Assert.Equal(MediaHash.Of(Bytes("x").Span), MediaHash.Of(Bytes("x").Span));
|
|
62
|
+
|
|
63
|
+
/// <summary>
|
|
64
|
+
/// O caminho tem dois niveis de prefixo. Um diretorio unico com centenas de milhares de
|
|
65
|
+
/// arquivos degrada listagem em praticamente todo sistema de arquivos.
|
|
66
|
+
/// </summary>
|
|
67
|
+
[Fact]
|
|
68
|
+
public void ThePathIsShardedByTheFirstFourHexDigits()
|
|
69
|
+
{
|
|
70
|
+
var hash = MediaHash.Of(Bytes("qualquer").Span);
|
|
71
|
+
var path = MediaStore.RelativePathFor(hash, "image/png");
|
|
72
|
+
|
|
73
|
+
Assert.Equal($"{hash.Value[..2]}/{hash.Value[2..4]}/{hash.Value}.png", path);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// <summary>
|
|
77
|
+
/// MIME desconhecido vira <c>.bin</c> e NAO recusa: perder a extensao e cosmetico,
|
|
78
|
+
/// perder os bytes pelos quais se pagou nao e.
|
|
79
|
+
/// </summary>
|
|
80
|
+
[Theory]
|
|
81
|
+
[InlineData("image/png", ".png")]
|
|
82
|
+
[InlineData("image/jpeg", ".jpg")]
|
|
83
|
+
[InlineData("image/webp", ".webp")]
|
|
84
|
+
[InlineData("video/mp4", ".mp4")]
|
|
85
|
+
[InlineData("application/x-inventado", ".bin")]
|
|
86
|
+
public void UnknownMediaTypesStillGetAPath(string mediaType, string expected)
|
|
87
|
+
=> Assert.Equal(expected, MediaStore.ExtensionFor(mediaType));
|
|
88
|
+
|
|
89
|
+
/// <summary>MIME em branco e defeito de chamada, e grita no lugar em que aconteceu.</summary>
|
|
90
|
+
[Fact]
|
|
91
|
+
public async Task ABlankMediaTypeIsRefused()
|
|
92
|
+
{
|
|
93
|
+
var store = new MediaStore(new InMemoryMediaBlobs());
|
|
94
|
+
|
|
95
|
+
await Assert.ThrowsAsync<ArgumentException>(
|
|
96
|
+
async () => await store.PutAsync(Bytes("x"), " ", Ct));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
using Microsoft.Extensions.AI;
|
|
2
|
+
using Morph.AiKit.Agents;
|
|
3
|
+
using Morph.AiKit.Providers;
|
|
4
|
+
using Morph.AiKit.Tests.Fakes;
|
|
5
|
+
using Xunit;
|
|
6
|
+
|
|
7
|
+
namespace Morph.AiKit.Tests;
|
|
8
|
+
|
|
9
|
+
/// <summary>
|
|
10
|
+
/// Canarios do <see cref="ModelRegistry"/> (T8). Tres coisas sao cobradas aqui, e as tres
|
|
11
|
+
/// sao as que o registry v1 nao tinha: <b>protocolo por alias</b>, <b>tarifa por alias</b>
|
|
12
|
+
/// e <b>falha explicita</b> onde antes havia nulo silencioso.
|
|
13
|
+
/// </summary>
|
|
14
|
+
public sealed class ModelRegistryTests
|
|
15
|
+
{
|
|
16
|
+
/// <summary>O exemplar de verdade, embutido do proprio arquivo que o kit publica.</summary>
|
|
17
|
+
private static string ExampleDocument()
|
|
18
|
+
{
|
|
19
|
+
using var stream = typeof(ModelRegistryTests).Assembly
|
|
20
|
+
.GetManifestResourceStream("model-registry.v2.json")
|
|
21
|
+
?? throw new InvalidOperationException(
|
|
22
|
+
"model-registry.v2.json nao foi embutido no assembly de teste.");
|
|
23
|
+
|
|
24
|
+
using var reader = new StreamReader(stream);
|
|
25
|
+
return reader.ReadToEnd();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private static string Document(string aliasesBody, string extraRoot = "")
|
|
29
|
+
=> $$"""
|
|
30
|
+
{
|
|
31
|
+
"version": "2.0"{{extraRoot}},
|
|
32
|
+
"aliases": { {{aliasesBody}} }
|
|
33
|
+
}
|
|
34
|
+
""";
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------------
|
|
37
|
+
// O exemplar publicado carrega — e expoe api e pricing por alias
|
|
38
|
+
// ---------------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
[Fact]
|
|
41
|
+
public void ThePublishedExampleLoadsAndExposesApiAndPricingPerAlias()
|
|
42
|
+
{
|
|
43
|
+
using var registry = ModelRegistry.FromJson(ExampleDocument(), new FakeProviderClientFactory());
|
|
44
|
+
|
|
45
|
+
Assert.Equal(
|
|
46
|
+
["local-dev", "text-default", "text-heavy", "text-heavy-chat", "vision"],
|
|
47
|
+
registry.Aliases.Order(StringComparer.Ordinal));
|
|
48
|
+
|
|
49
|
+
Assert.Equal("text-default", registry.DefaultAlias);
|
|
50
|
+
|
|
51
|
+
// 'api' omitido cai em Responses — a decisao da casa, verificada e nao suposta.
|
|
52
|
+
Assert.Equal(AgentApi.Responses, registry.Resolve("text-default").Api);
|
|
53
|
+
Assert.Equal(AgentApi.Responses, registry.Resolve("text-heavy").Api);
|
|
54
|
+
|
|
55
|
+
// ... e o MESMO modelo, sob outro alias, pode falar Chat Completions.
|
|
56
|
+
var chat = registry.Resolve("text-heavy-chat");
|
|
57
|
+
Assert.Equal(AgentApi.Chat, chat.Api);
|
|
58
|
+
Assert.Equal(registry.Resolve("text-heavy").Model, chat.Model);
|
|
59
|
+
|
|
60
|
+
var pricing = registry.Resolve("text-heavy").RequirePricing();
|
|
61
|
+
Assert.Equal(1.25m, pricing.InputPerMTok);
|
|
62
|
+
Assert.Equal(10.0m, pricing.OutputPerMTok);
|
|
63
|
+
Assert.Equal(0.125m, pricing.CachedInputPerMTok);
|
|
64
|
+
|
|
65
|
+
// cachedInputPerMTok explicitamente nulo = sem desconto declarado (nao "de graca").
|
|
66
|
+
Assert.Null(registry.Resolve("vision").RequirePricing().CachedInputPerMTok);
|
|
67
|
+
|
|
68
|
+
Assert.Equal("http://localhost:11434/v1", registry.Resolve("local-dev").Endpoint);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
[Fact]
|
|
72
|
+
public void TheExampleCarriesTheChatCompletionsLimitWithItsProvenance()
|
|
73
|
+
{
|
|
74
|
+
using var registry = ModelRegistry.FromJson(ExampleDocument(), new FakeProviderClientFactory());
|
|
75
|
+
|
|
76
|
+
var constraints = registry.Resolve("text-heavy-chat").Constraints;
|
|
77
|
+
|
|
78
|
+
Assert.True(constraints.ToolCallingRequiresReasoningEffortNone);
|
|
79
|
+
Assert.False(constraints.TemperatureUnsupported);
|
|
80
|
+
Assert.Contains("reasoning_effort", constraints.Provenance!, StringComparison.Ordinal);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------------
|
|
84
|
+
// Tarifa ausente e ALTO, nunca nulo silencioso
|
|
85
|
+
// ---------------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
[Fact]
|
|
88
|
+
public void AnAliasWithNeitherPricingNorWaiverIsRefusedAtLoadNamingTheAlias()
|
|
89
|
+
{
|
|
90
|
+
var document = Document("""
|
|
91
|
+
"orfao": { "provider": "openai", "model": "gpt-5.4" }
|
|
92
|
+
""");
|
|
93
|
+
|
|
94
|
+
var error = Assert.Throws<ModelRegistryException>(
|
|
95
|
+
() => ModelRegistry.FromJson(document, new FakeProviderClientFactory()));
|
|
96
|
+
|
|
97
|
+
Assert.Equal(ModelRegistryError.InvalidDocument, error.Reason);
|
|
98
|
+
Assert.Contains("alias 'orfao'", error.Message, StringComparison.Ordinal);
|
|
99
|
+
Assert.Contains("pricingWaiver", error.Message, StringComparison.Ordinal);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[Fact]
|
|
103
|
+
public void AnAliasWithBothPricingAndWaiverIsRefusedBecauseNeitherWins()
|
|
104
|
+
{
|
|
105
|
+
var document = Document("""
|
|
106
|
+
"ambiguo": {
|
|
107
|
+
"provider": "openai", "model": "gpt-5.4",
|
|
108
|
+
"pricing": { "inputPerMTok": 1, "outputPerMTok": 2 },
|
|
109
|
+
"pricingWaiver": "nao tem preco"
|
|
110
|
+
}
|
|
111
|
+
""");
|
|
112
|
+
|
|
113
|
+
var error = Assert.Throws<ModelRegistryException>(
|
|
114
|
+
() => ModelRegistry.FromJson(document, new FakeProviderClientFactory()));
|
|
115
|
+
|
|
116
|
+
Assert.Contains("alias 'ambiguo'", error.Message, StringComparison.Ordinal);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
[Fact]
|
|
120
|
+
public void AWaivedAliasLoadsAndSaysWhyItHasNoTariff()
|
|
121
|
+
{
|
|
122
|
+
using var registry = ModelRegistry.FromJson(ExampleDocument(), new FakeProviderClientFactory());
|
|
123
|
+
|
|
124
|
+
var local = registry.Resolve("local-dev");
|
|
125
|
+
|
|
126
|
+
Assert.False(local.HasPricing);
|
|
127
|
+
Assert.Contains("modelo local", local.PricingWaiver!, StringComparison.Ordinal);
|
|
128
|
+
|
|
129
|
+
var error = Assert.Throws<ModelRegistryException>(() => { _ = local.RequirePricing(); });
|
|
130
|
+
Assert.Equal(ModelRegistryError.MissingPricing, error.Reason);
|
|
131
|
+
Assert.Contains("local-dev", error.Message, StringComparison.Ordinal);
|
|
132
|
+
Assert.Contains("modelo local", error.Message, StringComparison.Ordinal);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[Fact]
|
|
136
|
+
public void ANegativeTariffIsRefused()
|
|
137
|
+
{
|
|
138
|
+
var document = Document("""
|
|
139
|
+
"barato-demais": {
|
|
140
|
+
"provider": "openai", "model": "gpt-5.4",
|
|
141
|
+
"pricing": { "inputPerMTok": -1, "outputPerMTok": 2 }
|
|
142
|
+
}
|
|
143
|
+
""");
|
|
144
|
+
|
|
145
|
+
var error = Assert.Throws<ModelRegistryException>(
|
|
146
|
+
() => ModelRegistry.FromJson(document, new FakeProviderClientFactory()));
|
|
147
|
+
|
|
148
|
+
Assert.Contains("inputPerMTok", error.Message, StringComparison.Ordinal);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
[Fact]
|
|
152
|
+
public void ATypoInAFieldNameIsRefusedInsteadOfSilentlyIgnored()
|
|
153
|
+
{
|
|
154
|
+
// "priceing" seria lido como "sem pricing" — e a ausencia silenciosa e o defeito.
|
|
155
|
+
var document = Document("""
|
|
156
|
+
"digitado-errado": {
|
|
157
|
+
"provider": "openai", "model": "gpt-5.4",
|
|
158
|
+
"priceing": { "inputPerMTok": 1, "outputPerMTok": 2 }
|
|
159
|
+
}
|
|
160
|
+
""");
|
|
161
|
+
|
|
162
|
+
var error = Assert.Throws<ModelRegistryException>(
|
|
163
|
+
() => ModelRegistry.FromJson(document, new FakeProviderClientFactory()));
|
|
164
|
+
|
|
165
|
+
Assert.Contains("priceing", error.Message, StringComparison.Ordinal);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------------
|
|
169
|
+
// api: por alias
|
|
170
|
+
// ---------------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
[Fact]
|
|
173
|
+
public void AnUnknownApiValueIsRefusedListingTheValidOnes()
|
|
174
|
+
{
|
|
175
|
+
var document = Document("""
|
|
176
|
+
"inventado": {
|
|
177
|
+
"provider": "openai", "model": "gpt-5.4", "api": "assistants",
|
|
178
|
+
"pricing": { "inputPerMTok": 1, "outputPerMTok": 2 }
|
|
179
|
+
}
|
|
180
|
+
""");
|
|
181
|
+
|
|
182
|
+
var error = Assert.Throws<ModelRegistryException>(
|
|
183
|
+
() => ModelRegistry.FromJson(document, new FakeProviderClientFactory()));
|
|
184
|
+
|
|
185
|
+
Assert.Contains("'assistants'", error.Message, StringComparison.Ordinal);
|
|
186
|
+
Assert.Contains("responses", error.Message, StringComparison.Ordinal);
|
|
187
|
+
Assert.Contains("chat", error.Message, StringComparison.Ordinal);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
[Fact]
|
|
191
|
+
public void ApiIsCaseInsensitiveOnTheWayIn()
|
|
192
|
+
{
|
|
193
|
+
var document = Document("""
|
|
194
|
+
"gritado": {
|
|
195
|
+
"provider": "openai", "model": "gpt-5.4", "api": "CHAT",
|
|
196
|
+
"pricing": { "inputPerMTok": 1, "outputPerMTok": 2 }
|
|
197
|
+
}
|
|
198
|
+
""");
|
|
199
|
+
|
|
200
|
+
using var registry = ModelRegistry.FromJson(document, new FakeProviderClientFactory());
|
|
201
|
+
|
|
202
|
+
Assert.Equal(AgentApi.Chat, registry.Resolve("gritado").Api);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
[Fact]
|
|
206
|
+
public void AV1DocumentIsRefusedWithAVersionMessageInsteadOfAConfusingFieldError()
|
|
207
|
+
{
|
|
208
|
+
var document = """
|
|
209
|
+
{
|
|
210
|
+
"version": "1.0",
|
|
211
|
+
"defaultAlias": "text-default",
|
|
212
|
+
"aliases": { "text-default": { "provider": "openai", "model": "gpt-4o-mini" } }
|
|
213
|
+
}
|
|
214
|
+
""";
|
|
215
|
+
|
|
216
|
+
var error = Assert.Throws<ModelRegistryException>(
|
|
217
|
+
() => ModelRegistry.FromJson(document, new FakeProviderClientFactory()));
|
|
218
|
+
|
|
219
|
+
Assert.Contains("'1.0'", error.Message, StringComparison.Ordinal);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------------
|
|
223
|
+
// Resolucao e memoizacao
|
|
224
|
+
// ---------------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
[Fact]
|
|
227
|
+
public void ResolvingAnUnknownAliasFailsExplicitlyAndListsWhatExists()
|
|
228
|
+
{
|
|
229
|
+
using var registry = ModelRegistry.FromJson(ExampleDocument(), new FakeProviderClientFactory());
|
|
230
|
+
|
|
231
|
+
var error = Assert.Throws<ModelRegistryException>(() => registry.Resolve("nao-existe"));
|
|
232
|
+
|
|
233
|
+
Assert.Equal(ModelRegistryError.UnknownAlias, error.Reason);
|
|
234
|
+
Assert.Contains("nao-existe", error.Message, StringComparison.Ordinal);
|
|
235
|
+
Assert.Contains("text-default", error.Message, StringComparison.Ordinal);
|
|
236
|
+
|
|
237
|
+
Assert.False(registry.TryResolve("nao-existe", out var silent));
|
|
238
|
+
Assert.Null(silent);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
[Fact]
|
|
242
|
+
public void TheChatClientIsMemoizedPerAliasAndTheFactoryRunsOnce()
|
|
243
|
+
{
|
|
244
|
+
var factory = new FakeProviderClientFactory();
|
|
245
|
+
using var registry = ModelRegistry.FromJson(ExampleDocument(), factory);
|
|
246
|
+
|
|
247
|
+
var first = registry.GetChatClient("text-default");
|
|
248
|
+
var second = registry.GetChatClient("text-default");
|
|
249
|
+
|
|
250
|
+
Assert.Same(first, second);
|
|
251
|
+
Assert.Equal(1, factory.CreateCount);
|
|
252
|
+
|
|
253
|
+
var other = registry.GetChatClient("text-heavy");
|
|
254
|
+
|
|
255
|
+
Assert.NotSame(first, other);
|
|
256
|
+
Assert.Equal(2, factory.CreateCount);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
[Fact]
|
|
260
|
+
public void AFactoryThatReturnsNullIsRefusedInsteadOfCachingNothing()
|
|
261
|
+
{
|
|
262
|
+
var factory = new FakeProviderClientFactory(_ => null);
|
|
263
|
+
using var registry = ModelRegistry.FromJson(ExampleDocument(), factory);
|
|
264
|
+
|
|
265
|
+
var error = Assert.Throws<ModelRegistryException>(() => registry.GetChatClient("text-default"));
|
|
266
|
+
|
|
267
|
+
Assert.Contains("devolveu nulo", error.Message, StringComparison.Ordinal);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
[Fact]
|
|
271
|
+
public void TheRegistryOwnsAndDisposesTheClientsItMemoized()
|
|
272
|
+
{
|
|
273
|
+
var factory = new FakeProviderClientFactory();
|
|
274
|
+
var registry = ModelRegistry.FromJson(ExampleDocument(), factory);
|
|
275
|
+
|
|
276
|
+
var client = (FakeChatClient)registry.GetChatClient("text-default");
|
|
277
|
+
Assert.False(client.IsDisposed);
|
|
278
|
+
|
|
279
|
+
registry.Dispose();
|
|
280
|
+
|
|
281
|
+
Assert.True(client.IsDisposed);
|
|
282
|
+
Assert.Throws<ObjectDisposedException>(() => registry.GetChatClient("text-default"));
|
|
283
|
+
|
|
284
|
+
registry.Dispose(); // idempotente
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
[Fact]
|
|
288
|
+
public void ADefaultAliasPointingNowhereIsRefused()
|
|
289
|
+
{
|
|
290
|
+
var document = Document(
|
|
291
|
+
"""
|
|
292
|
+
"existe": {
|
|
293
|
+
"provider": "openai", "model": "gpt-5.4",
|
|
294
|
+
"pricing": { "inputPerMTok": 1, "outputPerMTok": 2 }
|
|
295
|
+
}
|
|
296
|
+
""",
|
|
297
|
+
extraRoot: ""","defaultAlias": "fantasma" """);
|
|
298
|
+
|
|
299
|
+
var error = Assert.Throws<ModelRegistryException>(
|
|
300
|
+
() => ModelRegistry.FromJson(document, new FakeProviderClientFactory()));
|
|
301
|
+
|
|
302
|
+
Assert.Contains("fantasma", error.Message, StringComparison.Ordinal);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
[Fact]
|
|
306
|
+
public void BrokenJsonFailsNamingTheDocumentInsteadOfLeakingAJsonException()
|
|
307
|
+
{
|
|
308
|
+
var error = Assert.Throws<ModelRegistryException>(
|
|
309
|
+
() => ModelRegistry.FromJson("{ isto nao e json", new FakeProviderClientFactory()));
|
|
310
|
+
|
|
311
|
+
Assert.Contains("JSON invalido", error.Message, StringComparison.Ordinal);
|
|
312
|
+
Assert.IsAssignableFrom<System.Text.Json.JsonException>(error.InnerException);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------------
|
|
316
|
+
// Tarifa: a conta
|
|
317
|
+
// ---------------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
[Fact]
|
|
320
|
+
public void CostIsTheTariffTimesTheTokensDividedByAMillion()
|
|
321
|
+
{
|
|
322
|
+
var pricing = new ModelPricing(InputPerMTok: 1.25m, OutputPerMTok: 10m, CachedInputPerMTok: null);
|
|
323
|
+
|
|
324
|
+
// 1.000.000 de entrada = 1.25 USD; 100.000 de saida = 1.00 USD
|
|
325
|
+
Assert.Equal(2.25m, pricing.ComputeCostUsd(inputTokens: 1_000_000, outputTokens: 100_000));
|
|
326
|
+
Assert.Equal(0m, pricing.ComputeCostUsd(0, 0));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
[Fact]
|
|
330
|
+
public void CachedInputIsChargedApartOnlyWhenItsTariffExists()
|
|
331
|
+
{
|
|
332
|
+
var comCache = new ModelPricing(1.25m, 10m, CachedInputPerMTok: 0.125m);
|
|
333
|
+
var semCache = comCache with { CachedInputPerMTok = null };
|
|
334
|
+
|
|
335
|
+
// 1M de entrada, dos quais 800k vieram do cache:
|
|
336
|
+
// com tarifa de cache: 200k * 1.25 + 800k * 0.125 = 0.25 + 0.10 = 0.35
|
|
337
|
+
Assert.Equal(
|
|
338
|
+
0.35m,
|
|
339
|
+
comCache.ComputeCostUsd(inputTokens: 1_000_000, outputTokens: 0, cachedInputTokens: 800_000));
|
|
340
|
+
|
|
341
|
+
// sem tarifa de cache, a entrada inteira e cobrada como entrada — nunca de graca.
|
|
342
|
+
Assert.Equal(
|
|
343
|
+
1.25m,
|
|
344
|
+
semCache.ComputeCostUsd(inputTokens: 1_000_000, outputTokens: 0, cachedInputTokens: 800_000));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
[Fact]
|
|
348
|
+
public void CachedTokensCannotExceedInputTokens()
|
|
349
|
+
{
|
|
350
|
+
var pricing = new ModelPricing(1m, 1m, 0.1m);
|
|
351
|
+
|
|
352
|
+
Assert.Throws<ArgumentException>(
|
|
353
|
+
() => pricing.ComputeCostUsd(inputTokens: 10, outputTokens: 0, cachedInputTokens: 11));
|
|
354
|
+
Assert.Throws<ArgumentOutOfRangeException>(
|
|
355
|
+
() => pricing.ComputeCostUsd(inputTokens: -1, outputTokens: 0));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ---------------------------------------------------------------------------------
|
|
359
|
+
// Limites por alias — o limite REAL, nao o folclore
|
|
360
|
+
// ---------------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
[Fact]
|
|
363
|
+
public void TheChatToolLimitFiresOnlyWithAllFourFactsTogether()
|
|
364
|
+
{
|
|
365
|
+
using var registry = ModelRegistry.FromJson(ExampleDocument(), new FakeProviderClientFactory());
|
|
366
|
+
var alias = registry.Resolve("text-heavy-chat");
|
|
367
|
+
|
|
368
|
+
// os quatro fatos juntos: Chat + tools + effort enviado + effort != none
|
|
369
|
+
var violado = alias.CheckRequest(
|
|
370
|
+
AgentApi.Chat, hasTools: true, reasoningEffort: ReasoningEffort.High, temperatureIsSet: false);
|
|
371
|
+
|
|
372
|
+
Assert.False(violado.IsSatisfied);
|
|
373
|
+
Assert.Contains("reasoning effort", Assert.Single(violado.Violations), StringComparison.Ordinal);
|
|
374
|
+
|
|
375
|
+
// mesmo pedido, na Responses API: sem violacao
|
|
376
|
+
Assert.True(alias.CheckRequest(
|
|
377
|
+
AgentApi.Responses, hasTools: true, ReasoningEffort.High, temperatureIsSet: false).IsSatisfied);
|
|
378
|
+
|
|
379
|
+
// sem tools: sem violacao
|
|
380
|
+
Assert.True(alias.CheckRequest(
|
|
381
|
+
AgentApi.Chat, hasTools: false, ReasoningEffort.High, temperatureIsSet: false).IsSatisfied);
|
|
382
|
+
|
|
383
|
+
// effort 'none': e exatamente o que a documentacao permite
|
|
384
|
+
Assert.True(alias.CheckRequest(
|
|
385
|
+
AgentApi.Chat, hasTools: true, ReasoningEffort.None, temperatureIsSet: false).IsSatisfied);
|
|
386
|
+
|
|
387
|
+
// effort nao enviado: o default do provider vale, e o kit NAO afirma saber qual e
|
|
388
|
+
Assert.True(alias.CheckRequest(
|
|
389
|
+
AgentApi.Chat, hasTools: true, reasoningEffort: null, temperatureIsSet: false).IsSatisfied);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
[Fact]
|
|
393
|
+
public void AnAliasWithoutConstraintsNeverViolatesAnything()
|
|
394
|
+
{
|
|
395
|
+
using var registry = ModelRegistry.FromJson(ExampleDocument(), new FakeProviderClientFactory());
|
|
396
|
+
|
|
397
|
+
var check = registry.Resolve("text-default").CheckRequest(
|
|
398
|
+
AgentApi.Chat, hasTools: true, ReasoningEffort.High, temperatureIsSet: true);
|
|
399
|
+
|
|
400
|
+
Assert.True(check.IsSatisfied);
|
|
401
|
+
Assert.Empty(check.Violations);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
[Fact]
|
|
405
|
+
public void TheTemperatureLimitIsPerAliasAndOnlyBitesWhenTemperatureIsSent()
|
|
406
|
+
{
|
|
407
|
+
// A afirmacao "temperatura e obsoleta para modelo de raciocinio" NAO foi confirmada
|
|
408
|
+
// em fonte primaria (issue #93). Por isso ela e uma flag por alias com procedencia
|
|
409
|
+
// escrita — nunca uma regra universal no codigo. Aqui a procedencia diz, por extenso,
|
|
410
|
+
// que o exemplo e sintetico.
|
|
411
|
+
var document = Document("""
|
|
412
|
+
"sem-temperatura": {
|
|
413
|
+
"provider": "openai", "model": "modelo-hipotetico",
|
|
414
|
+
"pricing": { "inputPerMTok": 1, "outputPerMTok": 2 },
|
|
415
|
+
"constraints": {
|
|
416
|
+
"temperatureUnsupported": true,
|
|
417
|
+
"provenance": "EXEMPLO SINTETICO deste teste. Nenhuma fonte primaria foi encontrada para a afirmacao generica sobre temperatura em modelos de raciocinio."
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
""");
|
|
421
|
+
|
|
422
|
+
using var registry = ModelRegistry.FromJson(document, new FakeProviderClientFactory());
|
|
423
|
+
var alias = registry.Resolve("sem-temperatura");
|
|
424
|
+
|
|
425
|
+
var violado = alias.CheckRequest(
|
|
426
|
+
AgentApi.Responses, hasTools: false, reasoningEffort: null, temperatureIsSet: true);
|
|
427
|
+
|
|
428
|
+
Assert.False(violado.IsSatisfied);
|
|
429
|
+
Assert.Contains("EXEMPLO SINTETICO", Assert.Single(violado.Violations), StringComparison.Ordinal);
|
|
430
|
+
|
|
431
|
+
Assert.True(alias.CheckRequest(
|
|
432
|
+
AgentApi.Responses, hasTools: false, reasoningEffort: null, temperatureIsSet: false).IsSatisfied);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
[Fact]
|
|
436
|
+
public void AConstraintWithoutProvenanceIsRefusedAtLoad()
|
|
437
|
+
{
|
|
438
|
+
var document = Document("""
|
|
439
|
+
"folclore": {
|
|
440
|
+
"provider": "openai", "model": "gpt-5.4",
|
|
441
|
+
"pricing": { "inputPerMTok": 1, "outputPerMTok": 2 },
|
|
442
|
+
"constraints": { "temperatureUnsupported": true }
|
|
443
|
+
}
|
|
444
|
+
""");
|
|
445
|
+
|
|
446
|
+
var error = Assert.Throws<ModelRegistryException>(
|
|
447
|
+
() => ModelRegistry.FromJson(document, new FakeProviderClientFactory()));
|
|
448
|
+
|
|
449
|
+
Assert.Contains("provenance", error.Message, StringComparison.Ordinal);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
|
+
|
|
3
|
+
<PropertyGroup>
|
|
4
|
+
<RootNamespace>Morph.AiKit.Tests</RootNamespace>
|
|
5
|
+
<AssemblyName>Morph.AiKit.Tests</AssemblyName>
|
|
6
|
+
<IsTestProject>true</IsTestProject>
|
|
7
|
+
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
|
|
8
|
+
|
|
9
|
+
<!--
|
|
10
|
+
PISO DE DESCOBERTA. Este kit existe porque "markdown envelhece em silencio;
|
|
11
|
+
codigo compilado grita" — e um runner que descobre ZERO teste nao grita: ele
|
|
12
|
+
imprime uma linha curta e some. Foi exatamente o que aconteceu na primeira
|
|
13
|
+
invocacao real (a flag `nologo`, que so existe no modo VSTest, matou a
|
|
14
|
+
sessao: "Zero testes executados" com 115 testes passando por baixo).
|
|
15
|
+
|
|
16
|
+
A opcao `minimum-expected-tests N` (dois hifens a frente, como toda opcao da
|
|
17
|
+
plataforma) faz o runner sair com o codigo 9
|
|
18
|
+
(MinimumExpectedTestsPolicyViolation) sempre que MENOS de N casos rodarem,
|
|
19
|
+
inclusive quando rodaram zero — esse piso explicito SUPERA o veredito 8
|
|
20
|
+
(ZeroTests). Medido em 2026‑09‑08 contra o SDK .NET 10:
|
|
21
|
+
piso 221 → 221 aprovados, exit 0
|
|
22
|
+
piso 9999 → 115 aprovados, exit 9
|
|
23
|
+
|
|
24
|
+
Mora AQUI, e nao no passo do workflow, de proposito: assim o piso viaja com
|
|
25
|
+
o kit e vale para TODA invocacao de `dotnet test` — CI, maquina do dev e o
|
|
26
|
+
no `tests` do `morph-spec verify` — em vez de proteger so os dois arquivos
|
|
27
|
+
de workflow. Uma unica verdade: nenhum chamador repassa a flag na linha de
|
|
28
|
+
comando, porque repassa-la duplicaria a opcao.
|
|
29
|
+
|
|
30
|
+
O numero e o total de CASOS EXECUTADOS (275), nao de atributos nem de casos
|
|
31
|
+
DESCOBERTOS (276): os [Theory] expandem, e um caso IGNORADO nao conta para o piso — a
|
|
32
|
+
suite descobre 276 e executa 275, porque o semeador da cache exemplar de eval
|
|
33
|
+
(KitEvals.SeedTheCommittedCacheTree) so roda sob MORPH_EVAL_SEED=1. Medido: com piso
|
|
34
|
+
276 a sessao sai 9 mesmo com zero falhas. Adicionou teste? Suba o piso — o canario de
|
|
35
|
+
test/framework/ai-kit-ci-packaging.test.js cobra isso e falha se o piso
|
|
36
|
+
ficar para tras da contagem de `[Fact]`/`[Theory]` do diretorio.
|
|
37
|
+
|
|
38
|
+
E A SAIDA TAMBEM MUDA, nao so a invocacao. Sob MTP o sumario final e um
|
|
39
|
+
BLOCO DE 6 LINHAS, o veredito NAO comeca a linha ("Resumo da execucao de
|
|
40
|
+
teste: Aprovado!" / "Test run summary: Passed!") e a contagem vem em linha
|
|
41
|
+
propria, indentada, como `total:` MINUSCULO — nada disso e o sumario do
|
|
42
|
+
VSTest. Medido nos dois locales em 2026-09-08. Um parser do framework
|
|
43
|
+
escrito para VSTest (que procure uma linha COMECANDO em
|
|
44
|
+
"Aprovado!"/"Passed!" e contendo "Total de testes:"/"Total:") nao consegue
|
|
45
|
+
contar os testes deste projeto. O conserto do parser e da feature
|
|
46
|
+
`verify-por-stack`; aqui isto e registro da fronteira, nao conserto — o
|
|
47
|
+
piso acima ja garante que uma descoberta parcial saia com codigo 9, que e
|
|
48
|
+
o eixo que ESTE arquivo controla.
|
|
49
|
+
-->
|
|
50
|
+
<TestingPlatformCommandLineArguments>--minimum-expected-tests 275</TestingPlatformCommandLineArguments>
|
|
51
|
+
</PropertyGroup>
|
|
52
|
+
|
|
53
|
+
<!-- Sem atributo Version: as versoes vem de Directory.Packages.props (gerado do pin). -->
|
|
54
|
+
<ItemGroup>
|
|
55
|
+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
|
56
|
+
<PackageReference Include="xunit.v3" />
|
|
57
|
+
<PackageReference Include="xunit.runner.visualstudio" />
|
|
58
|
+
</ItemGroup>
|
|
59
|
+
|
|
60
|
+
<ItemGroup>
|
|
61
|
+
<ProjectReference Include="../../src/Morph.AiKit/Morph.AiKit.csproj" />
|
|
62
|
+
</ItemGroup>
|
|
63
|
+
|
|
64
|
+
<!--
|
|
65
|
+
O exemplar model-registry.v2.json entra no assembly de TESTE por link, e nao no da
|
|
66
|
+
biblioteca: ele e artefato de template (o projeto que copia o kit o poe em
|
|
67
|
+
config/model-registry.json), nao payload de runtime. Embutido aqui, o teste carrega o
|
|
68
|
+
MESMO arquivo que o usuario vai copiar — um exemplar que nao carrega deixa de passar
|
|
69
|
+
despercebido.
|
|
70
|
+
-->
|
|
71
|
+
<ItemGroup>
|
|
72
|
+
<EmbeddedResource Include="../../src/Morph.AiKit/Providers/model-registry.v2.json"
|
|
73
|
+
LogicalName="model-registry.v2.json" />
|
|
74
|
+
</ItemGroup>
|
|
75
|
+
|
|
76
|
+
</Project>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
using System.Reflection;
|
|
2
|
+
using System.Runtime.Versioning;
|
|
3
|
+
using Xunit;
|
|
4
|
+
|
|
5
|
+
namespace Morph.AiKit.Tests;
|
|
6
|
+
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Canario do esqueleto: prova que o harness de teste EXECUTA de verdade e que o
|
|
9
|
+
/// assembly do kit foi compilado no TFM do pin. Nao testa comportamento — os testes
|
|
10
|
+
/// de AgentFactory, ModelRegistry, middlewares, ConversationStore e DeltaIngest sao
|
|
11
|
+
/// das tasks T7-T14.
|
|
12
|
+
///
|
|
13
|
+
/// Por que ele existe em vez de um projeto de teste literalmente vazio: um projeto
|
|
14
|
+
/// sem nenhum teste NAO sai 0. Microsoft.Testing.Platform devolve exit code 8
|
|
15
|
+
/// ("zero tests ran") e o VSTest antigo devolvia 1 com "no test is available".
|
|
16
|
+
/// Um esqueleto que so "compila" e um CI verde que nunca executou nada — e um verde
|
|
17
|
+
/// desses e indistinguivel de um verde real, que e exatamente o silencio que esta
|
|
18
|
+
/// feature existe para quebrar.
|
|
19
|
+
/// </summary>
|
|
20
|
+
public sealed class SkeletonSmokeTests
|
|
21
|
+
{
|
|
22
|
+
[Fact]
|
|
23
|
+
public void KitAssemblyIsLoadableAndTargetsThePinnedFramework()
|
|
24
|
+
{
|
|
25
|
+
var kit = typeof(AiKitAssemblyMarker).Assembly;
|
|
26
|
+
|
|
27
|
+
Assert.Equal("Morph.AiKit", kit.GetName().Name);
|
|
28
|
+
|
|
29
|
+
var tfm = kit.GetCustomAttribute<TargetFrameworkAttribute>();
|
|
30
|
+
Assert.NotNull(tfm);
|
|
31
|
+
Assert.Equal(".NETCoreApp,Version=v10.0", tfm.FrameworkName);
|
|
32
|
+
}
|
|
33
|
+
}
|