@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,139 @@
|
|
|
1
|
+
using Morph.AiKit.Evals;
|
|
2
|
+
using Morph.AiKit.Tests.Fakes;
|
|
3
|
+
using Xunit;
|
|
4
|
+
|
|
5
|
+
namespace Morph.AiKit.Tests.Evals;
|
|
6
|
+
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Miss de cache sem credencial REPROVA, nomeado. Jamais <c>skip</c>.
|
|
9
|
+
/// </summary>
|
|
10
|
+
/// <remarks>
|
|
11
|
+
/// A tentacao do <c>skip</c> e real ("nao da para avaliar, entao nao avalio") e recria
|
|
12
|
+
/// exatamente o buraco que este harness fechou duas vezes: gate verde sobre verificacao que
|
|
13
|
+
/// nao aconteceu. E ha razao positiva: em CI, com cache commitada, um miss significa UMA
|
|
14
|
+
/// coisa so — o prompt ou o modelo mudou e ninguem regravou. O miss e o veredito, nao o
|
|
15
|
+
/// obstaculo.
|
|
16
|
+
/// </remarks>
|
|
17
|
+
public sealed class EvalCacheMissTests
|
|
18
|
+
{
|
|
19
|
+
private static async Task<EvalWorkspace> SeededAsync(params string[] scenarioNames)
|
|
20
|
+
{
|
|
21
|
+
var ws = new EvalWorkspace();
|
|
22
|
+
using var fake = new FakeChatClient
|
|
23
|
+
{
|
|
24
|
+
MetadataProviderName = "morph-fake",
|
|
25
|
+
MetadataDefaultModelId = "modelo-exemplar",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
foreach (var name in scenarioNames)
|
|
29
|
+
{
|
|
30
|
+
ws.WriteScenario("triagem", "acme", name, $"pergunta de {name}", """[ { "kind": "contains", "value": "resposta" } ]""");
|
|
31
|
+
fake.EnqueueText($"resposta de {name}");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await using var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake);
|
|
35
|
+
await refresh.RunAllAsync(TestContext.Current.CancellationToken);
|
|
36
|
+
return ws;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
[Fact]
|
|
40
|
+
public async Task AScenarioMissingFromTheCacheFailsByNameInsteadOfCallingTheProvider()
|
|
41
|
+
{
|
|
42
|
+
using var ws = await SeededAsync("a-primeiro");
|
|
43
|
+
|
|
44
|
+
// Cenario NOVO, que a cache nunca viu — a situacao literal de "mudou o prompt e
|
|
45
|
+
// ninguem regravou". O prefixo "b-" e deliberado: os cenarios rodam em ordem ordinal
|
|
46
|
+
// de caminho, e o teste abaixo depende de o cenario BOM ter rodado antes do miss.
|
|
47
|
+
ws.WriteScenario("triagem", "acme", "b-novo", "pergunta inedita", """[ { "kind": "contains", "value": "resposta" } ]""");
|
|
48
|
+
|
|
49
|
+
await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
|
|
50
|
+
var error = await Assert.ThrowsAsync<EvalCacheMissException>(
|
|
51
|
+
() => offline.RunAllAsync(TestContext.Current.CancellationToken));
|
|
52
|
+
|
|
53
|
+
// O cenario que faltou, e nao "algum cenario".
|
|
54
|
+
Assert.Equal("triagem.acme.b-novo", error.ScenarioName);
|
|
55
|
+
Assert.Contains("eval-cache-miss", error.Message, StringComparison.Ordinal);
|
|
56
|
+
|
|
57
|
+
// P3: a mensagem carrega o comando de regravacao e o arquivo de credencial esperado.
|
|
58
|
+
// Um "cache miss" seco mandaria o leitor procurar os dois.
|
|
59
|
+
Assert.Contains(EvalCacheMissException.RefreshCommand, error.Message, StringComparison.Ordinal);
|
|
60
|
+
Assert.Contains(".env.evals", error.Message, StringComparison.Ordinal);
|
|
61
|
+
Assert.Contains(ws.CacheRoot, error.Message, StringComparison.Ordinal);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[Fact]
|
|
65
|
+
public async Task TheMissTravelsAsAFAILURE_NeverAsASkippedScenario()
|
|
66
|
+
{
|
|
67
|
+
using var ws = await SeededAsync("a-primeiro");
|
|
68
|
+
ws.WriteScenario("triagem", "acme", "b-novo", "pergunta inedita", """[ { "kind": "contains", "value": "resposta" } ]""");
|
|
69
|
+
|
|
70
|
+
// O sumario da SEMEADURA, byte a byte. Ele e a linha de base: o que este teste cobra
|
|
71
|
+
// e que o run que morreu por miss nao escreva NADA por cima dele.
|
|
72
|
+
var summaryBeforeTheMiss = File.ReadAllBytes(ws.SummaryPath);
|
|
73
|
+
|
|
74
|
+
await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
|
|
75
|
+
await Assert.ThrowsAsync<EvalCacheMissException>(
|
|
76
|
+
() => offline.RunAllAsync(TestContext.Current.CancellationToken));
|
|
77
|
+
|
|
78
|
+
// Nao ha caminho pelo qual o cenario ausente apareca como avaliado: a contagem so tem
|
|
79
|
+
// o que de fato resolveu, e o sumario NAO foi reescrito. Um sumario dizendo
|
|
80
|
+
// "1 cenario, 1 passou, cache warm" aqui seria exatamente a mentira que a excecao
|
|
81
|
+
// existe para impedir — e o CLI, que le o ARQUIVO, acreditaria nela.
|
|
82
|
+
Assert.Single(offline.Results);
|
|
83
|
+
Assert.Equal("triagem.acme.a-primeiro", offline.Results[0].Scenario);
|
|
84
|
+
Assert.Equal(summaryBeforeTheMiss, File.ReadAllBytes(ws.SummaryPath));
|
|
85
|
+
|
|
86
|
+
// E o contador do cliente interno registra a tentativa: a arvore de cache nao
|
|
87
|
+
// respondeu, e isso e um FATO medido, nao uma inferencia.
|
|
88
|
+
Assert.Equal(1, offline.Offline!.Invocations);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
[Fact]
|
|
92
|
+
public async Task AFailedAssertionMakesTheScenarioREDWithoutAnyCacheMiss()
|
|
93
|
+
{
|
|
94
|
+
// A outra metade do contrato: reprovar por conteudo e diferente de reprovar por
|
|
95
|
+
// cache. Uma so mensagem para os dois casos mandaria o leitor procurar no lugar
|
|
96
|
+
// errado metade das vezes.
|
|
97
|
+
using var ws = new EvalWorkspace();
|
|
98
|
+
ws.WriteScenario("triagem", "acme", "saudacao", "bom dia", """[ { "kind": "contains", "value": "jamais aparece" } ]""");
|
|
99
|
+
|
|
100
|
+
using var fake = new FakeChatClient { MetadataProviderName = "morph-fake", MetadataDefaultModelId = "modelo-exemplar" };
|
|
101
|
+
fake.EnqueueText("Ola!");
|
|
102
|
+
await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
|
|
103
|
+
{
|
|
104
|
+
await refresh.RunAllAsync(TestContext.Current.CancellationToken);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
|
|
108
|
+
var summary = await offline.RunAllAsync(TestContext.Current.CancellationToken);
|
|
109
|
+
|
|
110
|
+
Assert.Equal(1, summary.Scenarios);
|
|
111
|
+
Assert.Equal(1, summary.Failed);
|
|
112
|
+
Assert.Equal(EvalCacheState.Warm, summary.Cache);
|
|
113
|
+
Assert.Equal(0, summary.Misses);
|
|
114
|
+
Assert.Contains("FALHOU", offline.Results[0].Outcomes[0].Reason, StringComparison.Ordinal);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
[Fact]
|
|
118
|
+
public async Task AScenarioWithNoDeterministicAssertionIsNeverGreenByVacuity()
|
|
119
|
+
{
|
|
120
|
+
using var ws = new EvalWorkspace();
|
|
121
|
+
ws.WriteScenario("triagem", "acme", "vazio", "bom dia", "[]");
|
|
122
|
+
|
|
123
|
+
using var fake = new FakeChatClient { MetadataProviderName = "morph-fake", MetadataDefaultModelId = "modelo-exemplar" };
|
|
124
|
+
fake.EnqueueText("Ola!");
|
|
125
|
+
await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
|
|
126
|
+
{
|
|
127
|
+
await refresh.RunAllAsync(TestContext.Current.CancellationToken);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
|
|
131
|
+
var summary = await offline.RunAllAsync(TestContext.Current.CancellationToken);
|
|
132
|
+
|
|
133
|
+
// Um cenario que nao cobra nada nao pode contar como cobertura. Deixa-lo verde e o
|
|
134
|
+
// mesmo defeito de um teste vazio — pior aqui, porque ele apareceria na contagem
|
|
135
|
+
// como se tivesse verificado algo.
|
|
136
|
+
Assert.Equal(1, summary.Failed);
|
|
137
|
+
Assert.Equal(0, summary.Passed);
|
|
138
|
+
}
|
|
139
|
+
}
|
package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
using Morph.AiKit.Evals;
|
|
2
|
+
using Morph.AiKit.Tests.Fakes;
|
|
3
|
+
using Xunit;
|
|
4
|
+
|
|
5
|
+
namespace Morph.AiKit.Tests.Evals;
|
|
6
|
+
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// O carimbo: por que a arvore de cache precisa dizer sob quais versoes foi gravada.
|
|
9
|
+
/// </summary>
|
|
10
|
+
/// <remarks>
|
|
11
|
+
/// A funcao de hash que produz a chave nao promete estabilidade entre versoes da biblioteca.
|
|
12
|
+
/// Sem carimbo, subir o pin chega como N misses inexplicaveis num dia em que ninguem mexeu
|
|
13
|
+
/// em nada; com carimbo, chega como uma frase que nomeia as DUAS versoes.
|
|
14
|
+
/// </remarks>
|
|
15
|
+
public sealed class EvalCacheStampTests
|
|
16
|
+
{
|
|
17
|
+
private static readonly FakeClock Today = new(new DateTimeOffset(2026, 9, 8, 12, 0, 0, TimeSpan.Zero));
|
|
18
|
+
|
|
19
|
+
private static EvalPin Pin(string pinnedAt = "2026-09-08", string evaluation = "10.9.0") => new(
|
|
20
|
+
pinnedAt,
|
|
21
|
+
new Dictionary<string, string>(StringComparer.Ordinal)
|
|
22
|
+
{
|
|
23
|
+
["Microsoft.Extensions.AI"] = "10.9.0",
|
|
24
|
+
["Microsoft.Extensions.AI.Evaluation"] = evaluation,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
private static void Stamp(string root, string pinnedAt = "2026-09-08", string evaluation = "10.9.0")
|
|
28
|
+
=> EvalCacheStamp.Write(root, new EvalCacheStamp
|
|
29
|
+
{
|
|
30
|
+
PinnedAt = pinnedAt,
|
|
31
|
+
Packages = Pin(pinnedAt, evaluation).Packages,
|
|
32
|
+
Provider = "morph-fake",
|
|
33
|
+
Model = "modelo-exemplar",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
[Fact]
|
|
37
|
+
public void AStampedTreeThatMatchesThePinIsAccepted()
|
|
38
|
+
{
|
|
39
|
+
using var ws = new EvalWorkspace();
|
|
40
|
+
Stamp(ws.CacheRoot);
|
|
41
|
+
|
|
42
|
+
var check = EvalCacheStamp.Check(ws.CacheRoot, Pin(), Today);
|
|
43
|
+
|
|
44
|
+
Assert.True(check.IsOk);
|
|
45
|
+
Assert.Equal(EvalStampVerdict.Ok, check.Verdict);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[Fact]
|
|
49
|
+
public void TheStampCarriesThePinDateAndTheEvaluationVersionsItWasWrittenUnder()
|
|
50
|
+
{
|
|
51
|
+
using var ws = new EvalWorkspace();
|
|
52
|
+
Stamp(ws.CacheRoot);
|
|
53
|
+
|
|
54
|
+
// Ler de VOLTA do disco, e nao comparar o objeto com ele mesmo: o carimbo e um
|
|
55
|
+
// artefato, e um teste que so olhasse a instancia em memoria seria um espelho.
|
|
56
|
+
var read = EvalCacheStamp.Read(ws.CacheRoot);
|
|
57
|
+
|
|
58
|
+
Assert.NotNull(read);
|
|
59
|
+
Assert.Equal("2026-09-08", read!.PinnedAt);
|
|
60
|
+
Assert.Equal("10.9.0", read.Packages["Microsoft.Extensions.AI.Evaluation"]);
|
|
61
|
+
Assert.Equal("morph-fake", read.Provider);
|
|
62
|
+
Assert.Equal(EvalCacheStamp.Writer, read.WrittenBy);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
[Fact]
|
|
66
|
+
public void ADivergentStampIsRefusedNamingBOTHVersions()
|
|
67
|
+
{
|
|
68
|
+
using var ws = new EvalWorkspace();
|
|
69
|
+
Stamp(ws.CacheRoot, evaluation: "10.9.0");
|
|
70
|
+
|
|
71
|
+
var check = EvalCacheStamp.Check(ws.CacheRoot, Pin(evaluation: "10.11.0"), Today);
|
|
72
|
+
|
|
73
|
+
Assert.Equal(EvalStampVerdict.PinMismatch, check.Verdict);
|
|
74
|
+
Assert.Contains("eval-cache-stale-pin", check.Reason, StringComparison.Ordinal);
|
|
75
|
+
// Dizer so uma das versoes obriga o leitor a ir buscar a outra.
|
|
76
|
+
Assert.Contains("gravada 10.9.0", check.Reason, StringComparison.Ordinal);
|
|
77
|
+
Assert.Contains("pin 10.11.0", check.Reason, StringComparison.Ordinal);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[Fact]
|
|
81
|
+
public void AMissingStampHasItsOWNReason_DistinctFromADivergentOne()
|
|
82
|
+
{
|
|
83
|
+
using var ws = new EvalWorkspace();
|
|
84
|
+
|
|
85
|
+
var missing = EvalCacheStamp.Check(ws.CacheRoot, Pin(), Today);
|
|
86
|
+
Stamp(ws.CacheRoot, evaluation: "10.9.0");
|
|
87
|
+
var divergent = EvalCacheStamp.Check(ws.CacheRoot, Pin(evaluation: "10.11.0"), Today);
|
|
88
|
+
|
|
89
|
+
Assert.Equal(EvalStampVerdict.Missing, missing.Verdict);
|
|
90
|
+
Assert.Contains("eval-cache-unstamped", missing.Reason, StringComparison.Ordinal);
|
|
91
|
+
|
|
92
|
+
// As duas situacoes pedem acoes diferentes: "regrave" versus "regrave PORQUE o
|
|
93
|
+
// pacote mudou". Um veredito unico devolveria ao leitor a pergunta que o carimbo
|
|
94
|
+
// existia para responder.
|
|
95
|
+
Assert.NotEqual(missing.Verdict, divergent.Verdict);
|
|
96
|
+
Assert.DoesNotContain("eval-cache-stale-pin", missing.Reason, StringComparison.Ordinal);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[Fact]
|
|
100
|
+
public void AnUnreadableStampIsNotSilentlyTreatedAsAbsent()
|
|
101
|
+
{
|
|
102
|
+
using var ws = new EvalWorkspace();
|
|
103
|
+
File.WriteAllText(EvalCacheStamp.PathFor(ws.CacheRoot), "{ isto nao e json");
|
|
104
|
+
|
|
105
|
+
var check = EvalCacheStamp.Check(ws.CacheRoot, Pin(), Today);
|
|
106
|
+
|
|
107
|
+
Assert.Equal(EvalStampVerdict.Unreadable, check.Verdict);
|
|
108
|
+
Assert.Null(EvalCacheStamp.Read(ws.CacheRoot));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
[Fact]
|
|
112
|
+
public void APinDatedInTheFutureIsAPinERROR_NeverAFreshCache()
|
|
113
|
+
{
|
|
114
|
+
using var ws = new EvalWorkspace();
|
|
115
|
+
Stamp(ws.CacheRoot, pinnedAt: "2027-01-01");
|
|
116
|
+
|
|
117
|
+
var check = EvalCacheStamp.Check(ws.CacheRoot, Pin(pinnedAt: "2027-01-01"), Today);
|
|
118
|
+
|
|
119
|
+
// O carimbo CASA com o pin — e mesmo assim reprova, porque a regua esta adulterada.
|
|
120
|
+
// Uma data no futuro renovaria sozinha qualquer janela medida contra ela.
|
|
121
|
+
Assert.Equal(EvalStampVerdict.FuturePin, check.Verdict);
|
|
122
|
+
Assert.Contains("eval-pin-future", check.Reason, StringComparison.Ordinal);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
[Fact]
|
|
126
|
+
public void TheFixtureRefusesToEvaluateASINGLEScenarioUnderABadStamp()
|
|
127
|
+
{
|
|
128
|
+
using var ws = new EvalWorkspace();
|
|
129
|
+
ws.WriteScenario("triagem", "acme", "saudacao", "bom dia", """[ { "kind": "contains", "value": "Ola" } ]""");
|
|
130
|
+
Stamp(ws.CacheRoot, evaluation: "10.9.0");
|
|
131
|
+
|
|
132
|
+
var pin = new EvalPin("2026-09-08", new Dictionary<string, string>(StringComparer.Ordinal)
|
|
133
|
+
{
|
|
134
|
+
["Microsoft.Extensions.AI"] = "10.9.0",
|
|
135
|
+
["Microsoft.Extensions.AI.Evaluation"] = "10.11.0",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// A recusa acontece na CONSTRUCAO — antes de qualquer cenario. Nao faz sentido pagar
|
|
139
|
+
// o relogio de uma suite para descobrir no fim que a cache nao servia.
|
|
140
|
+
var error = Assert.Throws<InvalidOperationException>(
|
|
141
|
+
() => EvalFixture.Create(ws.Options(EvalMode.Offline, pin), timeProvider: Today));
|
|
142
|
+
|
|
143
|
+
Assert.Contains("eval-cache-stale-pin", error.Message, StringComparison.Ordinal);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
[Fact]
|
|
147
|
+
public void RefreshDoesNotDemandAStamp_ItWritesOne()
|
|
148
|
+
{
|
|
149
|
+
using var ws = new EvalWorkspace();
|
|
150
|
+
|
|
151
|
+
// Cobrar carimbo em modo de escrita tornaria impossivel criar a PRIMEIRA cache.
|
|
152
|
+
var fixture = EvalFixture.Create(ws.Options(EvalMode.Refresh), new FakeChatClient());
|
|
153
|
+
|
|
154
|
+
Assert.True(fixture.StampCheck.IsOk);
|
|
155
|
+
Assert.False(File.Exists(EvalCacheStamp.PathFor(ws.CacheRoot)));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
using Morph.AiKit.Evals;
|
|
2
|
+
using Morph.AiKit.Tests.Fakes;
|
|
3
|
+
using Xunit;
|
|
4
|
+
|
|
5
|
+
namespace Morph.AiKit.Tests.Evals;
|
|
6
|
+
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// A camada que NOMEIA a falha, testada pelo conteudo do que ela produz.
|
|
9
|
+
/// </summary>
|
|
10
|
+
/// <remarks>
|
|
11
|
+
/// <para>
|
|
12
|
+
/// Este arquivo fecha a setima ocorrencia da mesma classe nesta epic, e a mais ironica: o
|
|
13
|
+
/// mecanismo de <c>Failures</c> nasceu para consertar um eval que mordia ANONIMAMENTE, e
|
|
14
|
+
/// nasceu ele proprio sem par. As tres partes podiam ser desligadas sem matar um teste —
|
|
15
|
+
/// <c>CollectFailures</c> devolvendo lista vazia, o ramo da vacuidade nunca disparando, e o
|
|
16
|
+
/// nome da assercao trocado por um literal — e a suite seguia "165 total, 0 falhou".
|
|
17
|
+
/// </para>
|
|
18
|
+
/// <para>
|
|
19
|
+
/// A causa foi uma unica testemunha automatizada: <c>Assert.Empty(summary.Failures)</c> num
|
|
20
|
+
/// run VERDE. Essa asserção é verdadeira exatamente quando o mecanismo esta morto — ela nao
|
|
21
|
+
/// distingue "nao houve falha" de "falha nenhuma e registrada". Uma lista vazia prova a
|
|
22
|
+
/// ausencia de conteudo, jamais a presenca de capacidade.
|
|
23
|
+
/// </para>
|
|
24
|
+
/// <para>
|
|
25
|
+
/// Por isso aqui nao se assere sobre <c>Failures</c> VAZIA: assere-se sobre o CONTEUDO de
|
|
26
|
+
/// <c>Failures</c> num run que falha de verdade — o par, a mesma forma que fechou o motor de
|
|
27
|
+
/// assercao um andar abaixo.
|
|
28
|
+
/// </para>
|
|
29
|
+
/// </remarks>
|
|
30
|
+
public sealed class EvalFailuresTests
|
|
31
|
+
{
|
|
32
|
+
/// <summary>Grava a cache do cenario com o texto dado, e devolve o fixture offline pronto.</summary>
|
|
33
|
+
/// <remarks>
|
|
34
|
+
/// A cache e semeada pelo <see cref="FakeChatClient"/>, NUNCA por chamada paga: o caminho
|
|
35
|
+
/// de escrita exercitado e o da biblioteca, e a leitura offline le exatamente o que aquela
|
|
36
|
+
/// escrita produziu.
|
|
37
|
+
/// </remarks>
|
|
38
|
+
private static async Task<EvalRunSummary> RunOfflineAsync(EvalWorkspace ws, params string[] responses)
|
|
39
|
+
{
|
|
40
|
+
using var fake = new FakeChatClient
|
|
41
|
+
{
|
|
42
|
+
MetadataProviderName = "morph-fake",
|
|
43
|
+
MetadataDefaultModelId = "modelo-exemplar",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
foreach (var text in responses)
|
|
47
|
+
{
|
|
48
|
+
fake.EnqueueText(text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
|
|
52
|
+
{
|
|
53
|
+
await refresh.RunAllAsync(TestContext.Current.CancellationToken);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
|
|
57
|
+
return await offline.RunAllAsync(TestContext.Current.CancellationToken);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[Fact]
|
|
61
|
+
public async Task AFailedScenarioIsRegisteredWithScenarioAssertionKindAndReason()
|
|
62
|
+
{
|
|
63
|
+
// O caso central: uma assercao reprova, e a falha chega ao sumario COM NOME. Sem isso
|
|
64
|
+
// o veredito viaja como contador — "Expected 0, Actual 1" — e quem le tem de abrir o
|
|
65
|
+
// dataset para descobrir o que quebrou.
|
|
66
|
+
using var ws = new EvalWorkspace();
|
|
67
|
+
ws.WriteScenario(
|
|
68
|
+
"triagem", "acme", "saudacao", "bom dia",
|
|
69
|
+
"""[ { "name": "cumprimenta", "kind": "contains", "value": "bom dia" } ]""");
|
|
70
|
+
|
|
71
|
+
var summary = await RunOfflineAsync(ws, "Boa noite!");
|
|
72
|
+
|
|
73
|
+
Assert.Equal(1, summary.Failed);
|
|
74
|
+
|
|
75
|
+
var failure = Assert.Single(summary.Failures);
|
|
76
|
+
Assert.Equal("triagem.acme.saudacao", failure.Scenario);
|
|
77
|
+
Assert.Equal("cumprimenta", failure.Assertion);
|
|
78
|
+
Assert.Equal("Contains", failure.Kind);
|
|
79
|
+
Assert.Contains("FALHOU", failure.Reason, StringComparison.Ordinal);
|
|
80
|
+
Assert.Contains("bom dia", failure.Reason, StringComparison.Ordinal);
|
|
81
|
+
|
|
82
|
+
// A linha legivel, no formato que a rubrica eval-de-modelo.md cita como exemplo.
|
|
83
|
+
Assert.Contains(
|
|
84
|
+
"eval-fail: triagem.acme.saudacao",
|
|
85
|
+
summary.DescribeFailures(),
|
|
86
|
+
StringComparison.Ordinal);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
[Fact]
|
|
90
|
+
public async Task TwoReprovedAssertionsInOneScenarioProduceTWOLinesEachNamingItsOwnRule()
|
|
91
|
+
{
|
|
92
|
+
// `Failures` e LISTA, e nao contador, porque as duas linhas dizem coisas diferentes:
|
|
93
|
+
// uma resposta pode quebrar em mais de uma regua, e isso separa "o prompt mudou" de
|
|
94
|
+
// "o modelo parou de responder". Se o tipo fosse um booleano, esta distincao morreria.
|
|
95
|
+
using var ws = new EvalWorkspace();
|
|
96
|
+
ws.WriteScenario(
|
|
97
|
+
"triagem", "acme", "saudacao", "bom dia",
|
|
98
|
+
"""
|
|
99
|
+
[
|
|
100
|
+
{ "name": "cumprimenta", "kind": "contains", "value": "bom dia" },
|
|
101
|
+
{ "name": "sem-dado-pessoal", "kind": "no-pii" }
|
|
102
|
+
]
|
|
103
|
+
""");
|
|
104
|
+
|
|
105
|
+
var summary = await RunOfflineAsync(ws, "Boa noite! Fale com lucas@example.com");
|
|
106
|
+
|
|
107
|
+
Assert.Equal(1, summary.Failed);
|
|
108
|
+
Assert.Equal(2, summary.Failures.Count);
|
|
109
|
+
|
|
110
|
+
Assert.Contains(summary.Failures, f => f.Assertion == "cumprimenta" && f.Kind == "Contains");
|
|
111
|
+
Assert.Contains(summary.Failures, f => f.Assertion == "sem-dado-pessoal" && f.Kind == "NoPii");
|
|
112
|
+
|
|
113
|
+
// Toda linha nomeia o cenario: uma falha sem cenario obriga a mesma busca que ela
|
|
114
|
+
// deveria dispensar.
|
|
115
|
+
Assert.All(summary.Failures, f => Assert.Equal("triagem.acme.saudacao", f.Scenario));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
[Fact]
|
|
119
|
+
public async Task AScenarioWithNOAssertionsIsCountedANDNamed()
|
|
120
|
+
{
|
|
121
|
+
// O cenario sem assercao nenhuma reprova por VACUIDADE: ele nao cobra nada. Nesse caso
|
|
122
|
+
// nao existe outcome reprovado para nomear, e sem um ramo proprio ele seria contado em
|
|
123
|
+
// `Failed` e invisivel em `Failures` — um numero sem frase, que e metade exata do
|
|
124
|
+
// defeito que este tipo existe para fechar.
|
|
125
|
+
using var ws = new EvalWorkspace();
|
|
126
|
+
ws.WriteScenario("triagem", "acme", "vazio", "bom dia", "[]");
|
|
127
|
+
|
|
128
|
+
var summary = await RunOfflineAsync(ws, "Bom dia!");
|
|
129
|
+
|
|
130
|
+
Assert.Equal(1, summary.Failed);
|
|
131
|
+
|
|
132
|
+
var failure = Assert.Single(summary.Failures);
|
|
133
|
+
Assert.Equal("triagem.acme.vazio", failure.Scenario);
|
|
134
|
+
Assert.Contains("sem assercao deterministica", failure.Reason, StringComparison.Ordinal);
|
|
135
|
+
|
|
136
|
+
// Contado E nomeado: o numero e a frase tem de concordar sempre.
|
|
137
|
+
Assert.Equal(summary.Failed, summary.Failures.Count);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
[Fact]
|
|
141
|
+
public async Task AGreenRunRegistersNoFailureAndSaysSoInWords()
|
|
142
|
+
{
|
|
143
|
+
// A reciproca vive AO LADO dos casos que falham de verdade, nunca sozinha: uma suite
|
|
144
|
+
// cuja unica testemunha e `Assert.Empty` num run verde e verdadeira precisamente
|
|
145
|
+
// quando o mecanismo esta morto.
|
|
146
|
+
using var ws = new EvalWorkspace();
|
|
147
|
+
ws.WriteScenario(
|
|
148
|
+
"triagem", "acme", "saudacao", "bom dia",
|
|
149
|
+
"""[ { "name": "cumprimenta", "kind": "contains", "value": "bom dia" } ]""");
|
|
150
|
+
|
|
151
|
+
var summary = await RunOfflineAsync(ws, "Bom dia! Como posso ajudar?");
|
|
152
|
+
|
|
153
|
+
Assert.Equal(0, summary.Failed);
|
|
154
|
+
Assert.Empty(summary.Failures);
|
|
155
|
+
Assert.Equal("nenhuma falha de eval registrada", summary.DescribeFailures());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
[Fact]
|
|
159
|
+
public async Task TheNamedFailuresSurviveTheROUNDTRIPThroughTheSummaryFileTheCLIReads()
|
|
160
|
+
{
|
|
161
|
+
// O sumario e um ARQUIVO: quem consome as falhas e o CLI, do outro lado do disco. Uma
|
|
162
|
+
// lista que existisse so em memoria seria invisivel exatamente onde precisa ser lida.
|
|
163
|
+
using var ws = new EvalWorkspace();
|
|
164
|
+
ws.WriteScenario(
|
|
165
|
+
"triagem", "acme", "saudacao", "bom dia",
|
|
166
|
+
"""[ { "name": "cumprimenta", "kind": "contains", "value": "bom dia" } ]""");
|
|
167
|
+
|
|
168
|
+
await RunOfflineAsync(ws, "Boa noite!");
|
|
169
|
+
|
|
170
|
+
var reread = EvalRunSummary.ReadFrom(ws.SummaryPath);
|
|
171
|
+
|
|
172
|
+
Assert.NotNull(reread);
|
|
173
|
+
var failure = Assert.Single(reread!.Failures);
|
|
174
|
+
Assert.Equal("triagem.acme.saudacao", failure.Scenario);
|
|
175
|
+
Assert.Equal("cumprimenta", failure.Assertion);
|
|
176
|
+
Assert.Equal("Contains", failure.Kind);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
using Microsoft.Extensions.AI.Evaluation.Reporting;
|
|
2
|
+
using Morph.AiKit.Evals;
|
|
3
|
+
using Morph.AiKit.Tests.Fakes;
|
|
4
|
+
using Xunit;
|
|
5
|
+
|
|
6
|
+
namespace Morph.AiKit.Tests.Evals;
|
|
7
|
+
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// A prova que decide a feature: <b>com a cache quente, o eval roda sem credencial e sem
|
|
10
|
+
/// rede</b>.
|
|
11
|
+
/// </summary>
|
|
12
|
+
/// <remarks>
|
|
13
|
+
/// Essa frase foi derivada de leitura de fonte antes de existir codigo. Aqui ela vira
|
|
14
|
+
/// medicao: uma arvore de cache e gravada por um fake, o processo troca o cliente interno
|
|
15
|
+
/// por um que RECUSA qualquer chamada, e a suite termina verde. Se o cliente interno fosse
|
|
16
|
+
/// tocado uma unica vez, o contador dele deixaria de ser zero — e nenhuma das assercoes
|
|
17
|
+
/// abaixo passaria.
|
|
18
|
+
/// </remarks>
|
|
19
|
+
public sealed class EvalFixtureTests
|
|
20
|
+
{
|
|
21
|
+
[Fact]
|
|
22
|
+
public async Task RefreshWritesTheCacheAndOfflineReadsItBackWithoutInvokingTheInnerClient()
|
|
23
|
+
{
|
|
24
|
+
using var ws = new EvalWorkspace();
|
|
25
|
+
ws.WriteScenario("triagem", "acme", "saudacao", "bom dia", """[ { "kind": "contains", "value": "Ola" } ]""");
|
|
26
|
+
ws.WriteScenario("triagem", "acme", "handoff", "quero falar com humano", """[ { "kind": "contains", "value": "humano" } ]""");
|
|
27
|
+
|
|
28
|
+
// 1) GRAVA a arvore contra o fake — nunca contra uma API paga. O caminho de escrita
|
|
29
|
+
// exercitado e o da biblioteca, e a chave e computada pela mesma funcao que a
|
|
30
|
+
// leitura vai usar.
|
|
31
|
+
using var fake = new FakeChatClient
|
|
32
|
+
{
|
|
33
|
+
MetadataProviderName = "morph-fake",
|
|
34
|
+
MetadataDefaultModelId = "modelo-exemplar",
|
|
35
|
+
};
|
|
36
|
+
// A ORDEM importa e e a do dataset (ordinal por caminho): "handoff" vem antes de
|
|
37
|
+
// "saudacao". Enfileirar na ordem alfabetica errada foi como esta suite descobriu
|
|
38
|
+
// que LoadAll ordena — e a ordem estavel e o que faz duas maquinas gravarem a mesma
|
|
39
|
+
// arvore de cache.
|
|
40
|
+
fake.EnqueueText("Encaminhando para um humano agora.").EnqueueText("Ola! Bom dia.");
|
|
41
|
+
|
|
42
|
+
await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
|
|
43
|
+
{
|
|
44
|
+
var written = await refresh.RunAllAsync(TestContext.Current.CancellationToken);
|
|
45
|
+
Assert.Equal(2, written.Scenarios);
|
|
46
|
+
Assert.Equal(2, written.Passed);
|
|
47
|
+
Assert.Equal(EvalCacheState.Cold, written.Cache);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Assert.Equal(2, fake.CallCount);
|
|
51
|
+
Assert.True(File.Exists(EvalCacheStamp.PathFor(ws.CacheRoot)), "o refresh tem de deixar o carimbo na raiz da cache");
|
|
52
|
+
|
|
53
|
+
// 2) LE offline. O cliente interno agora RECUSA: se a cache nao respondesse, este
|
|
54
|
+
// bloco lancaria EvalCacheMissException em vez de terminar verde.
|
|
55
|
+
await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
|
|
56
|
+
var summary = await offline.RunAllAsync(TestContext.Current.CancellationToken);
|
|
57
|
+
|
|
58
|
+
Assert.Equal(2, summary.Scenarios);
|
|
59
|
+
Assert.Equal(2, summary.Passed);
|
|
60
|
+
Assert.Equal(0, summary.Failed);
|
|
61
|
+
Assert.Equal(EvalCacheState.Warm, summary.Cache);
|
|
62
|
+
Assert.Equal(0, summary.Misses);
|
|
63
|
+
Assert.Equal("ok", summary.Stamp);
|
|
64
|
+
Assert.Equal(0, offline.Offline!.Invocations);
|
|
65
|
+
|
|
66
|
+
// 3) O fake nao foi tocado de novo: a leitura offline nao passou por ele nem por
|
|
67
|
+
// acidente (ele esta noutro processo logico, mas a contagem prova a intencao).
|
|
68
|
+
Assert.Equal(2, fake.CallCount);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
[Fact]
|
|
72
|
+
public async Task TheSummaryIsWrittenToThePathTheCallerDictates()
|
|
73
|
+
{
|
|
74
|
+
using var ws = new EvalWorkspace();
|
|
75
|
+
ws.WriteScenario("triagem", "acme", "saudacao", "bom dia", """[ { "kind": "contains", "value": "Ola" } ]""");
|
|
76
|
+
|
|
77
|
+
using var fake = new FakeChatClient { MetadataProviderName = "morph-fake", MetadataDefaultModelId = "modelo-exemplar" };
|
|
78
|
+
fake.EnqueueText("Ola!");
|
|
79
|
+
await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
|
|
80
|
+
{
|
|
81
|
+
await refresh.RunAllAsync(TestContext.Current.CancellationToken);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
|
|
85
|
+
await offline.RunAllAsync(TestContext.Current.CancellationToken);
|
|
86
|
+
|
|
87
|
+
// Ler de volta o ARQUIVO, e nao o objeto em memoria: o contrato com o CLI e o
|
|
88
|
+
// arquivo, e um teste que so olhasse o objeto seria um espelho do gerador.
|
|
89
|
+
var reread = EvalRunSummary.ReadFrom(ws.SummaryPath);
|
|
90
|
+
Assert.NotNull(reread);
|
|
91
|
+
Assert.Equal(1, reread!.Scenarios);
|
|
92
|
+
Assert.Equal(EvalCacheState.Warm, reread.Cache);
|
|
93
|
+
Assert.Equal("morph-fake", reread.Provider);
|
|
94
|
+
Assert.Equal("modelo-exemplar", reread.Model);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
[Fact]
|
|
98
|
+
public void ATimeToLiveBelowOneYearIsRefusedWithTheReasonInTheMessage()
|
|
99
|
+
{
|
|
100
|
+
using var ws = new EvalWorkspace();
|
|
101
|
+
var options = ws.Options(EvalMode.Offline) with { TimeToLive = TimeSpan.FromDays(14) };
|
|
102
|
+
|
|
103
|
+
var error = Assert.Throws<ArgumentOutOfRangeException>(() => EvalFixture.Create(options));
|
|
104
|
+
|
|
105
|
+
Assert.Contains("eval-cache-ttl-too-short", error.Message, StringComparison.Ordinal);
|
|
106
|
+
// A mensagem tem de dizer POR QUE, e nao so que o numero e pequeno: a expiracao apaga
|
|
107
|
+
// o diretorio da chave durante a LEITURA.
|
|
108
|
+
Assert.Contains("DESTRUTIVA NA LEITURA", error.Message, StringComparison.Ordinal);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
[Fact]
|
|
112
|
+
public void TheLibraryDefaultTimeToLiveWouldExpireACommittedCacheWithinWeeks()
|
|
113
|
+
{
|
|
114
|
+
// MEDIDO, nao lembrado: e este fato que torna o TTL explicito obrigatorio. Se um dia
|
|
115
|
+
// a biblioteca subir o default para alem do piso do kit, este teste morre e a
|
|
116
|
+
// exigencia pode ser rediscutida com um numero na mao.
|
|
117
|
+
Assert.True(
|
|
118
|
+
Defaults.DefaultTimeToLiveForCacheEntries < EvalCacheOptions.MinimumTimeToLive,
|
|
119
|
+
$"o default da biblioteca e {Defaults.DefaultTimeToLiveForCacheEntries.TotalDays:0} dias, "
|
|
120
|
+
+ $"o piso do kit e {EvalCacheOptions.MinimumTimeToLive.TotalDays:0}");
|
|
121
|
+
Assert.Equal(14, Defaults.DefaultTimeToLiveForCacheEntries.TotalDays);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
[Fact]
|
|
125
|
+
public void AWriteModeWithoutARealClientIsRefusedByName()
|
|
126
|
+
{
|
|
127
|
+
using var ws = new EvalWorkspace();
|
|
128
|
+
|
|
129
|
+
var error = Assert.Throws<InvalidOperationException>(
|
|
130
|
+
() => EvalFixture.Create(ws.Options(EvalMode.Refresh), liveClient: null));
|
|
131
|
+
|
|
132
|
+
Assert.Contains("eval-live-client-missing", error.Message, StringComparison.Ordinal);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[Fact]
|
|
136
|
+
public async Task AnEmptyDatasetIsAFailureAndNeverAGreenRunOfZeroScenarios()
|
|
137
|
+
{
|
|
138
|
+
using var ws = new EvalWorkspace();
|
|
139
|
+
EvalCacheStamp.Write(ws.CacheRoot, new EvalCacheStamp
|
|
140
|
+
{
|
|
141
|
+
PinnedAt = EvalWorkspace.Pin.PinnedAt,
|
|
142
|
+
Packages = EvalWorkspace.Pin.Packages,
|
|
143
|
+
Provider = "morph-fake",
|
|
144
|
+
Model = "modelo-exemplar",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await using var fixture = EvalFixture.Create(ws.Options(EvalMode.Offline));
|
|
148
|
+
var error = await Assert.ThrowsAsync<InvalidOperationException>(
|
|
149
|
+
() => fixture.RunAllAsync(TestContext.Current.CancellationToken));
|
|
150
|
+
|
|
151
|
+
Assert.Contains("eval-empty-run", error.Message, StringComparison.Ordinal);
|
|
152
|
+
}
|
|
153
|
+
}
|