@polymorphism-tech/morph-spec 8.34.2 → 8.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +20 -3
- 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 +234 -9
- 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/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/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 +140 -5
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +8 -1
- 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 +333 -21
- 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/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,246 @@
|
|
|
1
|
+
using System.Text;
|
|
2
|
+
using System.Text.Json;
|
|
3
|
+
using System.Text.RegularExpressions;
|
|
4
|
+
using Microsoft.Extensions.AI;
|
|
5
|
+
using Microsoft.Extensions.AI.Evaluation;
|
|
6
|
+
|
|
7
|
+
namespace Morph.AiKit.Evals;
|
|
8
|
+
|
|
9
|
+
/// <summary>O que uma assercao deterministica pergunta a uma resposta.</summary>
|
|
10
|
+
/// <remarks>
|
|
11
|
+
/// A lista e curta de proposito. Toda entrada aqui e uma propriedade que se decide sem
|
|
12
|
+
/// julgamento: ou o texto contem, ou nao contem; ou a ferramenta foi escolhida, ou nao foi.
|
|
13
|
+
/// Uma "assercao" que precisasse de um modelo para decidir seria juizo, e juizo nao reprova
|
|
14
|
+
/// sozinho neste kit.
|
|
15
|
+
/// </remarks>
|
|
16
|
+
public enum EvalAssertionKind
|
|
17
|
+
{
|
|
18
|
+
/// <summary>O texto da resposta contem o valor (ignorando caixa).</summary>
|
|
19
|
+
Contains = 0,
|
|
20
|
+
|
|
21
|
+
/// <summary>O texto da resposta NAO contem o valor (ignorando caixa).</summary>
|
|
22
|
+
NotContains = 1,
|
|
23
|
+
|
|
24
|
+
/// <summary>A resposta e um objeto JSON e tem a chave de topo indicada em <c>Target</c>.</summary>
|
|
25
|
+
JsonHasKey = 2,
|
|
26
|
+
|
|
27
|
+
/// <summary>A resposta nao carrega dado pessoal nas formas que o kit sabe reconhecer.</summary>
|
|
28
|
+
NoPii = 3,
|
|
29
|
+
|
|
30
|
+
/// <summary>A resposta escolheu a ferramenta nomeada em <c>Target</c>.</summary>
|
|
31
|
+
ToolCalled = 4,
|
|
32
|
+
|
|
33
|
+
/// <summary>A ferramenta <c>Target</c> foi chamada com o argumento <c>Value</c> no formato <c>chave=valor</c>.</summary>
|
|
34
|
+
ToolArgument = 5,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// <summary>Veredito de uma assercao: o booleano e a frase que o explica.</summary>
|
|
38
|
+
/// <param name="Passed">Se a propriedade se sustentou.</param>
|
|
39
|
+
/// <param name="Reason">Por que — nomeando o que foi procurado, para nao mandar o leitor adivinhar.</param>
|
|
40
|
+
public sealed record EvalAssertionOutcome(bool Passed, string Reason)
|
|
41
|
+
{
|
|
42
|
+
/// <summary>Nome da assercao que produziu este veredito.</summary>
|
|
43
|
+
/// <remarks>
|
|
44
|
+
/// Carimbado por <see cref="EvalAssertion.Evaluate"/>. Existe porque uma falha que chega a
|
|
45
|
+
/// jusante como contador ("Expected 0, Actual 1") nao diz O QUE quebrou: quem le precisa
|
|
46
|
+
/// abrir o dataset e adivinhar. Um eval que morde anonimamente obriga a investigacao que
|
|
47
|
+
/// ele deveria dispensar.
|
|
48
|
+
/// </remarks>
|
|
49
|
+
public string? Assertion { get; init; }
|
|
50
|
+
|
|
51
|
+
/// <summary>O tipo da assercao que julgou — a REGUA, nao so o nome dela.</summary>
|
|
52
|
+
public EvalAssertionKind? Kind { get; init; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// <summary>
|
|
56
|
+
/// A assercao deterministica: o unico tipo de avaliador que REPROVA neste kit.
|
|
57
|
+
/// </summary>
|
|
58
|
+
/// <remarks>
|
|
59
|
+
/// <para>
|
|
60
|
+
/// <b>Por que so ela decide.</b> Os avaliadores de qualidade da biblioteca sao juizo de LLM,
|
|
61
|
+
/// e juizo de LLM varia. Um gate que reprovasse por variancia de juiz reintroduziria o
|
|
62
|
+
/// FLAKY que faz suites de eval serem desligadas do CI — e um no de eval desligado nao
|
|
63
|
+
/// verifica nada. Entao a divisao e explicita: assercao deterministica decide; qualidade e
|
|
64
|
+
/// numero reportado.
|
|
65
|
+
/// </para>
|
|
66
|
+
/// <para>
|
|
67
|
+
/// <b>Ela e um <see cref="IEvaluator"/> de verdade</b>, e nao um predicado aplicado por
|
|
68
|
+
/// fora, para que a metrica caia no MESMO relatorio que as de qualidade. Um resultado que
|
|
69
|
+
/// vivesse fora do relatorio precisaria de um segundo lugar para ser lido, e o segundo lugar
|
|
70
|
+
/// e onde as duas verdades comecam.
|
|
71
|
+
/// </para>
|
|
72
|
+
/// </remarks>
|
|
73
|
+
public sealed record EvalAssertion : IEvaluator
|
|
74
|
+
{
|
|
75
|
+
/// <summary>
|
|
76
|
+
/// Formas de dado pessoal que o kit reconhece. Deliberadamente poucas e literais: um
|
|
77
|
+
/// detector de PII abrangente e um projeto proprio, e prometer mais do que se mede seria
|
|
78
|
+
/// pior do que nao prometer. Sem <c>\b</c> em nenhum padrao — a fronteira de palavra do
|
|
79
|
+
/// .NET tambem nao trata acento como letra em toda cultura, e um padrao que nao casa e
|
|
80
|
+
/// pior que um padrao que nao existe.
|
|
81
|
+
/// </summary>
|
|
82
|
+
private static readonly (string Label, Regex Pattern)[] PiiPatterns =
|
|
83
|
+
[
|
|
84
|
+
("e-mail", new Regex("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,}", RegexOptions.CultureInvariant)),
|
|
85
|
+
("CPF", new Regex("[0-9]{3}[.][0-9]{3}[.][0-9]{3}-[0-9]{2}", RegexOptions.CultureInvariant)),
|
|
86
|
+
("telefone", new Regex("[(][0-9]{2}[)] ?[0-9]{4,5}-[0-9]{4}", RegexOptions.CultureInvariant)),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/// <summary>
|
|
90
|
+
/// Nome da metrica. UNICO dentro de um cenario: a biblioteca indexa metricas por nome, e
|
|
91
|
+
/// duas assercoes homonimas fariam a segunda sobrescrever a primeira em silencio — um
|
|
92
|
+
/// cenario que parece cobrar duas propriedades e cobra uma.
|
|
93
|
+
/// </summary>
|
|
94
|
+
public required string Name { get; init; }
|
|
95
|
+
|
|
96
|
+
/// <summary>O que esta assercao pergunta.</summary>
|
|
97
|
+
public required EvalAssertionKind Kind { get; init; }
|
|
98
|
+
|
|
99
|
+
/// <summary>Chave JSON ou nome de ferramenta, conforme o <see cref="Kind"/>.</summary>
|
|
100
|
+
public string? Target { get; init; }
|
|
101
|
+
|
|
102
|
+
/// <summary>Texto procurado, ou <c>chave=valor</c> em <see cref="EvalAssertionKind.ToolArgument"/>.</summary>
|
|
103
|
+
public string? Value { get; init; }
|
|
104
|
+
|
|
105
|
+
/// <inheritdoc />
|
|
106
|
+
public IReadOnlyCollection<string> EvaluationMetricNames => [Name];
|
|
107
|
+
|
|
108
|
+
/// <summary>Aplica a assercao a uma resposta. Puro: nao le disco, nao le ambiente, nao chama rede.</summary>
|
|
109
|
+
public EvalAssertionOutcome Evaluate(ChatResponse response)
|
|
110
|
+
{
|
|
111
|
+
ArgumentNullException.ThrowIfNull(response);
|
|
112
|
+
|
|
113
|
+
var text = TextOf(response);
|
|
114
|
+
|
|
115
|
+
var outcome = Kind switch
|
|
116
|
+
{
|
|
117
|
+
EvalAssertionKind.Contains => Judge(
|
|
118
|
+
Value is not null && text.Contains(Value, StringComparison.OrdinalIgnoreCase),
|
|
119
|
+
$"a resposta deveria conter \"{Value}\""),
|
|
120
|
+
|
|
121
|
+
EvalAssertionKind.NotContains => Judge(
|
|
122
|
+
Value is not null && !text.Contains(Value, StringComparison.OrdinalIgnoreCase),
|
|
123
|
+
$"a resposta nao deveria conter \"{Value}\""),
|
|
124
|
+
|
|
125
|
+
EvalAssertionKind.JsonHasKey => JudgeJsonKey(text),
|
|
126
|
+
|
|
127
|
+
EvalAssertionKind.NoPii => JudgeNoPii(text),
|
|
128
|
+
|
|
129
|
+
EvalAssertionKind.ToolCalled => Judge(
|
|
130
|
+
Target is not null && ToolCalls(response).Any(c => NameMatches(c, Target)),
|
|
131
|
+
$"a resposta deveria escolher a ferramenta \"{Target}\""),
|
|
132
|
+
|
|
133
|
+
EvalAssertionKind.ToolArgument => JudgeToolArgument(response),
|
|
134
|
+
|
|
135
|
+
_ => new EvalAssertionOutcome(false, $"tipo de assercao desconhecido: {Kind}"),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Carimba QUEM julgou. O veredito sozinho e um booleano; com o nome e a regua ele vira
|
|
139
|
+
// uma frase que nomeia o defeito sem obrigar ninguem a abrir o dataset.
|
|
140
|
+
return outcome with { Assertion = Name, Kind = this.Kind };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// <inheritdoc />
|
|
144
|
+
public ValueTask<EvaluationResult> EvaluateAsync(
|
|
145
|
+
IEnumerable<ChatMessage> messages,
|
|
146
|
+
ChatResponse modelResponse,
|
|
147
|
+
ChatConfiguration? chatConfiguration = null,
|
|
148
|
+
IEnumerable<EvaluationContext>? additionalContext = null,
|
|
149
|
+
CancellationToken cancellationToken = default)
|
|
150
|
+
{
|
|
151
|
+
ArgumentNullException.ThrowIfNull(modelResponse);
|
|
152
|
+
cancellationToken.ThrowIfCancellationRequested();
|
|
153
|
+
|
|
154
|
+
var outcome = Evaluate(modelResponse);
|
|
155
|
+
var metric = new BooleanMetric(Name, outcome.Passed, outcome.Reason);
|
|
156
|
+
return ValueTask.FromResult(new EvaluationResult(metric));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// <summary>Texto concatenado da resposta — a superficie que as assercoes de texto leem.</summary>
|
|
160
|
+
internal static string TextOf(ChatResponse response)
|
|
161
|
+
{
|
|
162
|
+
var sb = new StringBuilder();
|
|
163
|
+
foreach (var message in response.Messages)
|
|
164
|
+
{
|
|
165
|
+
foreach (var content in message.Contents.OfType<TextContent>())
|
|
166
|
+
{
|
|
167
|
+
sb.Append(content.Text);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return sb.ToString();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private static IEnumerable<FunctionCallContent> ToolCalls(ChatResponse response)
|
|
175
|
+
=> response.Messages.SelectMany(m => m.Contents).OfType<FunctionCallContent>();
|
|
176
|
+
|
|
177
|
+
private static bool NameMatches(FunctionCallContent call, string target)
|
|
178
|
+
=> string.Equals(call.Name, target, StringComparison.OrdinalIgnoreCase);
|
|
179
|
+
|
|
180
|
+
private EvalAssertionOutcome Judge(bool passed, string expectation)
|
|
181
|
+
=> new(passed, passed ? $"OK: {expectation}" : $"FALHOU: {expectation}");
|
|
182
|
+
|
|
183
|
+
private EvalAssertionOutcome JudgeJsonKey(string text)
|
|
184
|
+
{
|
|
185
|
+
if (Target is null)
|
|
186
|
+
{
|
|
187
|
+
return new EvalAssertionOutcome(false, "FALHOU: assercao json-has-key sem chave declarada");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try
|
|
191
|
+
{
|
|
192
|
+
using var doc = JsonDocument.Parse(text);
|
|
193
|
+
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
|
194
|
+
{
|
|
195
|
+
return new EvalAssertionOutcome(false, $"FALHOU: a resposta e JSON mas nao um objeto ({doc.RootElement.ValueKind})");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
var has = doc.RootElement.TryGetProperty(Target, out _);
|
|
199
|
+
return Judge(has, $"o objeto JSON da resposta deveria ter a chave \"{Target}\"");
|
|
200
|
+
}
|
|
201
|
+
catch (JsonException e)
|
|
202
|
+
{
|
|
203
|
+
return new EvalAssertionOutcome(false, $"FALHOU: a resposta nao e JSON valido ({e.Message})");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private EvalAssertionOutcome JudgeNoPii(string text)
|
|
208
|
+
{
|
|
209
|
+
var found = PiiPatterns.Where(p => p.Pattern.IsMatch(text)).Select(p => p.Label).ToList();
|
|
210
|
+
return found.Count == 0
|
|
211
|
+
? new EvalAssertionOutcome(true, "OK: nenhuma forma conhecida de dado pessoal na resposta")
|
|
212
|
+
: new EvalAssertionOutcome(false, $"FALHOU: a resposta carrega dado pessoal ({string.Join(", ", found)})");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private EvalAssertionOutcome JudgeToolArgument(ChatResponse response)
|
|
216
|
+
{
|
|
217
|
+
if (Target is null || Value is null)
|
|
218
|
+
{
|
|
219
|
+
return new EvalAssertionOutcome(false, "FALHOU: assercao tool-argument exige ferramenta e chave=valor");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var split = Value.IndexOf('=', StringComparison.Ordinal);
|
|
223
|
+
if (split <= 0)
|
|
224
|
+
{
|
|
225
|
+
return new EvalAssertionOutcome(false, $"FALHOU: \"{Value}\" nao esta na forma chave=valor");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
var key = Value[..split];
|
|
229
|
+
var expected = Value[(split + 1)..];
|
|
230
|
+
|
|
231
|
+
foreach (var call in ToolCalls(response).Where(c => NameMatches(c, Target)))
|
|
232
|
+
{
|
|
233
|
+
if (call.Arguments is null || !call.Arguments.TryGetValue(key, out var actual))
|
|
234
|
+
{
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (string.Equals(Convert.ToString(actual, System.Globalization.CultureInfo.InvariantCulture), expected, StringComparison.OrdinalIgnoreCase))
|
|
239
|
+
{
|
|
240
|
+
return new EvalAssertionOutcome(true, $"OK: \"{Target}\" foi chamada com {key}={expected}");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return new EvalAssertionOutcome(false, $"FALHOU: \"{Target}\" deveria ser chamada com {key}={expected}");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
namespace Morph.AiKit.Evals;
|
|
2
|
+
|
|
3
|
+
/// <summary>
|
|
4
|
+
/// Lancada quando um cenario de eval NAO estava na cache e o modo corrente e
|
|
5
|
+
/// <see cref="EvalMode.Offline"/> — o unico caminho pelo qual a suite tocaria a rede.
|
|
6
|
+
/// </summary>
|
|
7
|
+
/// <remarks>
|
|
8
|
+
/// <para>
|
|
9
|
+
/// <b>Por que uma excecao, e nao um resultado.</b> A regra do kit e "excecao nunca como
|
|
10
|
+
/// fluxo de controle"; esta e a excecao de CONTRATO que a propria regra admite. O ponto de
|
|
11
|
+
/// deteccao e o <see cref="OfflineChatClient"/>, que implementa <c>IChatClient</c> — uma
|
|
12
|
+
/// interface de terceiro cuja assinatura devolve <c>Task<ChatResponse></c> e nao tem
|
|
13
|
+
/// onde carregar um veredito. Devolver uma resposta vazia ali seria pior do que lancar:
|
|
14
|
+
/// o eval ficaria VERDE sobre um cenario que ninguem avaliou, que e exatamente o defeito
|
|
15
|
+
/// (gate verde sobre verificacao que nao aconteceu) que esta feature existe para fechar.
|
|
16
|
+
/// </para>
|
|
17
|
+
/// <para>
|
|
18
|
+
/// <b>A mensagem e o produto.</b> Em CI, com a cache commitada, um miss significa uma coisa
|
|
19
|
+
/// so: o prompt ou o modelo mudou e ninguem regravou. Entao a mensagem nomeia as quatro
|
|
20
|
+
/// coisas que o leitor precisa para agir sem abrir o codigo — o cenario, o caminho da
|
|
21
|
+
/// cache, o comando de regravacao e o arquivo de credencial esperado. Um "cache miss"
|
|
22
|
+
/// seco mandaria o leitor procurar as quatro.
|
|
23
|
+
/// </para>
|
|
24
|
+
/// </remarks>
|
|
25
|
+
public sealed class EvalCacheMissException : Exception
|
|
26
|
+
{
|
|
27
|
+
/// <summary>Comando literal que regrava a arvore de cache. Nunca implicito, nunca automatico.</summary>
|
|
28
|
+
public const string RefreshCommand = "morph-spec verify {feature} --refresh-cache";
|
|
29
|
+
|
|
30
|
+
/// <summary>Cria a excecao com todos os dados que a mensagem precisa nomear.</summary>
|
|
31
|
+
/// <param name="scenarioName">O cenario que faltou na cache.</param>
|
|
32
|
+
/// <param name="cachePath">Raiz da arvore de cache consultada.</param>
|
|
33
|
+
/// <param name="envFile">Arquivo (gitignored) de onde a credencial viria numa regravacao.</param>
|
|
34
|
+
public EvalCacheMissException(string scenarioName, string cachePath, string envFile)
|
|
35
|
+
: base(BuildMessage(scenarioName, cachePath, envFile))
|
|
36
|
+
{
|
|
37
|
+
ScenarioName = scenarioName;
|
|
38
|
+
CachePath = cachePath;
|
|
39
|
+
EnvFile = envFile;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// <summary>O cenario que faltou na cache.</summary>
|
|
43
|
+
public string ScenarioName { get; }
|
|
44
|
+
|
|
45
|
+
/// <summary>Raiz da arvore de cache consultada.</summary>
|
|
46
|
+
public string CachePath { get; }
|
|
47
|
+
|
|
48
|
+
/// <summary>Arquivo de credencial esperado por uma regravacao.</summary>
|
|
49
|
+
public string EnvFile { get; }
|
|
50
|
+
|
|
51
|
+
private static string BuildMessage(string scenarioName, string cachePath, string envFile)
|
|
52
|
+
=> $"eval-cache-miss: o cenario \"{scenarioName}\" nao esta na cache em \"{cachePath}\" "
|
|
53
|
+
+ "e o modo corrente e offline (nenhuma chamada de rede sera feita). "
|
|
54
|
+
+ $"Em CI isto significa que o prompt ou o modelo mudou e a cache nao foi regravada. "
|
|
55
|
+
+ $"Para regravar: {RefreshCommand} — a credencial vem de \"{envFile}\", "
|
|
56
|
+
+ "que e gitignored e nunca viaja no config commitado.";
|
|
57
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
namespace Morph.AiKit.Evals;
|
|
2
|
+
|
|
3
|
+
/// <summary>
|
|
4
|
+
/// Tudo o que o <see cref="EvalFixture"/> precisa saber, e nada que ele possa adivinhar.
|
|
5
|
+
/// </summary>
|
|
6
|
+
/// <remarks>
|
|
7
|
+
/// <para>
|
|
8
|
+
/// <b><see cref="TimeToLive"/> nao tem valor default, e isso e o item mais importante deste
|
|
9
|
+
/// tipo.</b> O default da biblioteca e de 14 dias E A EXPIRACAO E DESTRUTIVA NA LEITURA: ao
|
|
10
|
+
/// achar uma entrada vencida, o provedor APAGA o diretorio da chave e devolve nulo. Uma
|
|
11
|
+
/// cache commitada com o default se apaga sozinha ao ser lida, duas semanas depois de
|
|
12
|
+
/// gravada — e o sintoma chega como miss em massa (e arvore suja no <c>git status</c>) num
|
|
13
|
+
/// dia em que ninguem mexeu em nada. Por isso o valor e obrigatorio aqui e o fixture recusa
|
|
14
|
+
/// qualquer coisa abaixo de <see cref="MinimumTimeToLive"/>.
|
|
15
|
+
/// </para>
|
|
16
|
+
/// <para>
|
|
17
|
+
/// <b><see cref="ProviderName"/> e <see cref="DefaultModelId"/> entram na chave de cache</b>
|
|
18
|
+
/// e por isso viajam como configuracao, nao como constante: eles precisam reproduzir o run
|
|
19
|
+
/// que GRAVOU a arvore. O endpoint nao entra na chave (medido na v10.9.0), que e o que
|
|
20
|
+
/// permite ao cliente offline nao ter endpoint algum.
|
|
21
|
+
/// </para>
|
|
22
|
+
/// </remarks>
|
|
23
|
+
public sealed record EvalCacheOptions
|
|
24
|
+
{
|
|
25
|
+
/// <summary>
|
|
26
|
+
/// Piso do tempo de vida das entradas. Um ano nao e um numero bonito: e o menor prazo
|
|
27
|
+
/// que garante que uma cache commitada sobreviva a um ciclo de release sem se apagar
|
|
28
|
+
/// durante uma leitura.
|
|
29
|
+
/// </summary>
|
|
30
|
+
public static readonly TimeSpan MinimumTimeToLive = TimeSpan.FromDays(365);
|
|
31
|
+
|
|
32
|
+
/// <summary>Raiz da arvore de cache. COMMITADA.</summary>
|
|
33
|
+
public required string CacheRoot { get; init; }
|
|
34
|
+
|
|
35
|
+
/// <summary>
|
|
36
|
+
/// Raiz dos resultados de execucao. FORA da arvore commitada — por construcao, e nao por
|
|
37
|
+
/// um <c>.gitignore</c> dentro de um diretorio commitado (regra de ignore ali e regra que
|
|
38
|
+
/// a proxima pessoa remove sem entender). Os resultados carregam latencia medida por
|
|
39
|
+
/// cronometro, uso de tokens e o flag de cache hit: commita-los e diff garantido em toda
|
|
40
|
+
/// rodada, sem sinal nenhum.
|
|
41
|
+
/// </summary>
|
|
42
|
+
public required string ResultsRoot { get; init; }
|
|
43
|
+
|
|
44
|
+
/// <summary>Raiz do dataset versionado, em <c>{agente}/{tenant}/{cenario}.json</c>.</summary>
|
|
45
|
+
public required string DatasetRoot { get; init; }
|
|
46
|
+
|
|
47
|
+
/// <summary>Tempo de vida das entradas. Obrigatorio, sempre — ver as observacoes do tipo.</summary>
|
|
48
|
+
public required TimeSpan TimeToLive { get; init; }
|
|
49
|
+
|
|
50
|
+
/// <summary>O <c>ProviderName</c> que entra na chave de cache.</summary>
|
|
51
|
+
public required string ProviderName { get; init; }
|
|
52
|
+
|
|
53
|
+
/// <summary>O <c>DefaultModelId</c> que entra na chave de cache. Nulo e um valor.</summary>
|
|
54
|
+
public string? DefaultModelId { get; init; }
|
|
55
|
+
|
|
56
|
+
/// <summary>De onde a resposta pode vir nesta execucao.</summary>
|
|
57
|
+
public required EvalMode Mode { get; init; }
|
|
58
|
+
|
|
59
|
+
/// <summary>
|
|
60
|
+
/// Onde o sumario JSON e escrito. O CAMINHO E DITADO PELO CHAMADOR (o CLI escolhe um
|
|
61
|
+
/// arquivo temporario) para que dois nos de eval no mesmo <c>verify</c> nao colidam.
|
|
62
|
+
/// Nulo quando ninguem quer sumario — rodar sem sumario e legitimo, inventar um caminho
|
|
63
|
+
/// nao e.
|
|
64
|
+
/// </summary>
|
|
65
|
+
public string? SummaryPath { get; init; }
|
|
66
|
+
|
|
67
|
+
/// <summary>
|
|
68
|
+
/// Arquivo (gitignored) de onde a credencial viria numa regravacao. So aparece em
|
|
69
|
+
/// mensagem de erro; o fixture nunca o le.
|
|
70
|
+
/// </summary>
|
|
71
|
+
public string EnvFile { get; init; } = ".env.evals";
|
|
72
|
+
|
|
73
|
+
/// <summary>
|
|
74
|
+
/// O pin corrente, como o CLI o entregou. Nulo desliga a checagem de carimbo — util para
|
|
75
|
+
/// um projeto que constroi o fixture a mao, e explicitamente NAO o caminho do CLI.
|
|
76
|
+
/// </summary>
|
|
77
|
+
public EvalPin? Pin { get; init; }
|
|
78
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using System.Text.Json.Serialization;
|
|
3
|
+
|
|
4
|
+
namespace Morph.AiKit.Evals;
|
|
5
|
+
|
|
6
|
+
/// <summary>O veredito de um carimbo de cache — quatro estados, nunca um booleano.</summary>
|
|
7
|
+
/// <remarks>
|
|
8
|
+
/// <c>Missing</c> e <c>PinMismatch</c> sao SEPARADOS de proposito: as duas situacoes pedem
|
|
9
|
+
/// acoes diferentes. A primeira e "regrave"; a segunda e "regrave <b>porque o pacote
|
|
10
|
+
/// mudou</b>". Colapsa-las num unico "carimbo invalido" devolve ao leitor a pergunta que o
|
|
11
|
+
/// carimbo existia para responder.
|
|
12
|
+
/// </remarks>
|
|
13
|
+
public enum EvalStampVerdict
|
|
14
|
+
{
|
|
15
|
+
/// <summary>Carimbo presente e de acordo com o pin corrente.</summary>
|
|
16
|
+
Ok = 0,
|
|
17
|
+
|
|
18
|
+
/// <summary>Nao ha carimbo: cache anterior a esta feature, ou diretorio criado a mao.</summary>
|
|
19
|
+
Missing = 1,
|
|
20
|
+
|
|
21
|
+
/// <summary>Ha um arquivo, mas ele nao e JSON legivel no formato do carimbo.</summary>
|
|
22
|
+
Unreadable = 2,
|
|
23
|
+
|
|
24
|
+
/// <summary>As versoes gravadas divergem do pin corrente — a chave de cache pode ter mudado.</summary>
|
|
25
|
+
PinMismatch = 3,
|
|
26
|
+
|
|
27
|
+
/// <summary>O <c>pinnedAt</c> do pin corrente esta no futuro. Erro de pin, jamais cache fresca.</summary>
|
|
28
|
+
FuturePin = 4,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// <summary>Resultado de <see cref="EvalCacheStamp.Check"/>: veredito mais a frase que o explica.</summary>
|
|
32
|
+
/// <param name="Verdict">O estado apurado.</param>
|
|
33
|
+
/// <param name="Reason">
|
|
34
|
+
/// Frase pronta para o relatorio. Nos dois vereditos que nomeiam versoes, ela cita AS DUAS
|
|
35
|
+
/// (a gravada e a do pin) — dizer so uma obriga o leitor a ir buscar a outra.
|
|
36
|
+
/// </param>
|
|
37
|
+
public sealed record EvalCacheStampCheck(EvalStampVerdict Verdict, string Reason)
|
|
38
|
+
{
|
|
39
|
+
/// <summary>Atalho de leitura: o veredito unico que permite avaliar cenario.</summary>
|
|
40
|
+
public bool IsOk => Verdict == EvalStampVerdict.Ok;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// <summary>
|
|
44
|
+
/// O pin, do jeito que o fixture o recebe: data de apuracao mais pacote-para-versao.
|
|
45
|
+
/// </summary>
|
|
46
|
+
/// <remarks>
|
|
47
|
+
/// Achatado (nome para versao) e nao a forma completa de <c>framework/ai-pin.json</c>
|
|
48
|
+
/// porque quem o produz e o CLI, na variavel <c>MORPH_EVAL_PIN</c>, e o kit nao le o pin do
|
|
49
|
+
/// disco: um segundo parser de <c>ai-pin.json</c> e como as duas verdades comecam. Aqui
|
|
50
|
+
/// entram apenas os pacotes que decidem o FORMATO da cache — a familia
|
|
51
|
+
/// <c>Microsoft.Extensions.AI</c> e as tres de Evaluation. Subir o provider da OpenAI nao
|
|
52
|
+
/// invalida uma arvore de cache, e um carimbo que reagisse a isso reprovaria por nada.
|
|
53
|
+
/// </remarks>
|
|
54
|
+
/// <param name="PinnedAt">Data ISO em que um humano conferiu as versoes.</param>
|
|
55
|
+
/// <param name="Packages">Nome do pacote para versao exata.</param>
|
|
56
|
+
public sealed record EvalPin(
|
|
57
|
+
[property: JsonPropertyName("pinnedAt")] string PinnedAt,
|
|
58
|
+
[property: JsonPropertyName("packages")] IReadOnlyDictionary<string, string> Packages);
|
|
59
|
+
|
|
60
|
+
/// <summary>
|
|
61
|
+
/// O carimbo que fica na raiz da arvore de cache commitada, e a regra que o julga.
|
|
62
|
+
/// </summary>
|
|
63
|
+
/// <remarks>
|
|
64
|
+
/// <para>
|
|
65
|
+
/// <b>Por que existe.</b> A funcao de hash que produz a chave de cache nao promete ser
|
|
66
|
+
/// estavel entre versoes da biblioteca — a propria biblioteca carrega um numero de versao
|
|
67
|
+
/// de cache interno, para invalidar tudo quando o formato muda. Ou seja: SUBIR O PIN PODE
|
|
68
|
+
/// INVALIDAR A ARVORE INTEIRA. Sem carimbo isso chega como N misses inexplicaveis num dia
|
|
69
|
+
/// em que ninguem mexeu em nada. Com carimbo, chega como uma frase que nomeia as duas
|
|
70
|
+
/// versoes e o comando de regravacao.
|
|
71
|
+
/// </para>
|
|
72
|
+
/// <para>
|
|
73
|
+
/// <b>Ele nao substitui a leitura da cache</b> — a chave continua sendo computada pela
|
|
74
|
+
/// biblioteca. Ele responde antes, e de graca, a pergunta cuja resposta cara seria "rodou a
|
|
75
|
+
/// suite inteira para descobrir que a cache nao servia".
|
|
76
|
+
/// </para>
|
|
77
|
+
/// </remarks>
|
|
78
|
+
public sealed record EvalCacheStamp
|
|
79
|
+
{
|
|
80
|
+
/// <summary>Nome do arquivo, na raiz de <c>cachePath</c>. Uma unica grafia.</summary>
|
|
81
|
+
public const string FileName = ".morph-eval-cache.json";
|
|
82
|
+
|
|
83
|
+
/// <summary>Quem grava. Um carimbo de outra origem e um carimbo que ninguem mantem.</summary>
|
|
84
|
+
public const string Writer = "morph-eval-fixture";
|
|
85
|
+
|
|
86
|
+
private static readonly JsonSerializerOptions Json = new()
|
|
87
|
+
{
|
|
88
|
+
WriteIndented = true,
|
|
89
|
+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/// <summary>O <c>pinnedAt</c> vigente quando a arvore foi gravada.</summary>
|
|
93
|
+
[JsonPropertyName("pinnedAt")]
|
|
94
|
+
public required string PinnedAt { get; init; }
|
|
95
|
+
|
|
96
|
+
/// <summary>As versoes de pacote sob as quais a arvore foi gravada.</summary>
|
|
97
|
+
[JsonPropertyName("packages")]
|
|
98
|
+
public required IReadOnlyDictionary<string, string> Packages { get; init; }
|
|
99
|
+
|
|
100
|
+
/// <summary>O <c>ProviderName</c> que entrou na chave.</summary>
|
|
101
|
+
[JsonPropertyName("provider")]
|
|
102
|
+
public required string Provider { get; init; }
|
|
103
|
+
|
|
104
|
+
/// <summary>O <c>DefaultModelId</c> que entrou na chave. Nulo e um valor, nao um erro.</summary>
|
|
105
|
+
[JsonPropertyName("model")]
|
|
106
|
+
public string? Model { get; init; }
|
|
107
|
+
|
|
108
|
+
/// <summary>Quem gravou o carimbo.</summary>
|
|
109
|
+
[JsonPropertyName("writtenBy")]
|
|
110
|
+
public string WrittenBy { get; init; } = Writer;
|
|
111
|
+
|
|
112
|
+
/// <summary>Caminho do arquivo de carimbo dentro de uma raiz de cache.</summary>
|
|
113
|
+
public static string PathFor(string cacheRoot)
|
|
114
|
+
{
|
|
115
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(cacheRoot);
|
|
116
|
+
return Path.Combine(cacheRoot, FileName);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// <summary>Grava o carimbo. Cria a raiz se ela ainda nao existe.</summary>
|
|
120
|
+
public static void Write(string cacheRoot, EvalCacheStamp stamp)
|
|
121
|
+
{
|
|
122
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(cacheRoot);
|
|
123
|
+
ArgumentNullException.ThrowIfNull(stamp);
|
|
124
|
+
|
|
125
|
+
Directory.CreateDirectory(cacheRoot);
|
|
126
|
+
|
|
127
|
+
// Quebras de linha normalizadas para LF e uma quebra final: o carimbo e um arquivo
|
|
128
|
+
// COMMITADO. Sem isso ele produziria diff que nao e diff — CRLF numa maquina, LF
|
|
129
|
+
// noutra, e nenhuma quebra no fim em lugar nenhum.
|
|
130
|
+
var json = JsonSerializer.Serialize(stamp, Json).ReplaceLineEndings("\n");
|
|
131
|
+
File.WriteAllText(PathFor(cacheRoot), json + "\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// <summary>
|
|
135
|
+
/// Le o carimbo. Devolve <c>null</c> quando o arquivo nao existe OU nao parseia —
|
|
136
|
+
/// <see cref="Check"/> e quem distingue os dois casos, porque so ela tem as palavras.
|
|
137
|
+
/// </summary>
|
|
138
|
+
public static EvalCacheStamp? Read(string cacheRoot)
|
|
139
|
+
{
|
|
140
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(cacheRoot);
|
|
141
|
+
|
|
142
|
+
var path = PathFor(cacheRoot);
|
|
143
|
+
if (!File.Exists(path))
|
|
144
|
+
{
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try
|
|
149
|
+
{
|
|
150
|
+
return JsonSerializer.Deserialize<EvalCacheStamp>(File.ReadAllText(path), Json);
|
|
151
|
+
}
|
|
152
|
+
catch (JsonException)
|
|
153
|
+
{
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// <summary>
|
|
159
|
+
/// O carimbo desta arvore contra o pin corrente.
|
|
160
|
+
/// </summary>
|
|
161
|
+
/// <param name="cacheRoot">Raiz da arvore de cache.</param>
|
|
162
|
+
/// <param name="pin">O pin corrente, como o CLI o entregou.</param>
|
|
163
|
+
/// <param name="timeProvider">
|
|
164
|
+
/// Relogio injetado — a comparacao de <c>pinnedAt</c> com hoje e uma leitura de relogio,
|
|
165
|
+
/// e um teste que dependesse do relogio da maquina mudaria de veredito na virada do dia.
|
|
166
|
+
/// </param>
|
|
167
|
+
public static EvalCacheStampCheck Check(string cacheRoot, EvalPin pin, TimeProvider timeProvider)
|
|
168
|
+
{
|
|
169
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(cacheRoot);
|
|
170
|
+
ArgumentNullException.ThrowIfNull(pin);
|
|
171
|
+
ArgumentNullException.ThrowIfNull(timeProvider);
|
|
172
|
+
|
|
173
|
+
// O pin vem ANTES do carimbo: um pin com data no futuro renova sozinho qualquer
|
|
174
|
+
// janela medida contra ele, e comparar um carimbo contra uma regua adulterada
|
|
175
|
+
// produziria um "ok" que nao significa nada.
|
|
176
|
+
var today = timeProvider.GetUtcNow().UtcDateTime.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
|
|
177
|
+
if (string.CompareOrdinal(pin.PinnedAt, today) > 0)
|
|
178
|
+
{
|
|
179
|
+
return new EvalCacheStampCheck(
|
|
180
|
+
EvalStampVerdict.FuturePin,
|
|
181
|
+
$"eval-pin-future: o pin corrente declara pinnedAt {pin.PinnedAt}, adiante de hoje ({today}). "
|
|
182
|
+
+ "Data no futuro e erro de pin, nunca cache fresca — corrija framework/ai-pin.json.");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var path = PathFor(cacheRoot);
|
|
186
|
+
if (!File.Exists(path))
|
|
187
|
+
{
|
|
188
|
+
return new EvalCacheStampCheck(
|
|
189
|
+
EvalStampVerdict.Missing,
|
|
190
|
+
$"eval-cache-unstamped: nao ha {FileName} em \"{cacheRoot}\". "
|
|
191
|
+
+ "Uma arvore de cache sem carimbo nao diz sob quais versoes foi gravada — "
|
|
192
|
+
+ $"regrave com `{EvalCacheMissException.RefreshCommand}`.");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
var stamp = Read(cacheRoot);
|
|
196
|
+
if (stamp is null)
|
|
197
|
+
{
|
|
198
|
+
return new EvalCacheStampCheck(
|
|
199
|
+
EvalStampVerdict.Unreadable,
|
|
200
|
+
$"eval-cache-unstamped: {FileName} existe em \"{cacheRoot}\" mas nao e um carimbo legivel. "
|
|
201
|
+
+ $"Regrave com `{EvalCacheMissException.RefreshCommand}`.");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var divergent = new List<string>();
|
|
205
|
+
foreach (var (name, expected) in pin.Packages.OrderBy(x => x.Key, StringComparer.Ordinal))
|
|
206
|
+
{
|
|
207
|
+
var recorded = stamp.Packages.TryGetValue(name, out var v) ? v : null;
|
|
208
|
+
if (!string.Equals(recorded, expected, StringComparison.Ordinal))
|
|
209
|
+
{
|
|
210
|
+
divergent.Add($"{name}: gravada {recorded ?? "(ausente)"} vs pin {expected}");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (divergent.Count > 0)
|
|
215
|
+
{
|
|
216
|
+
return new EvalCacheStampCheck(
|
|
217
|
+
EvalStampVerdict.PinMismatch,
|
|
218
|
+
$"eval-cache-stale-pin: a cache em \"{cacheRoot}\" foi gravada sob versoes que divergem do pin corrente "
|
|
219
|
+
+ $"({string.Join("; ", divergent)}). A chave de cache nao promete sobreviver a uma troca de versao — "
|
|
220
|
+
+ $"regrave com `{EvalCacheMissException.RefreshCommand}`.");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return new EvalCacheStampCheck(EvalStampVerdict.Ok, $"carimbo de acordo com o pin {pin.PinnedAt}");
|
|
224
|
+
}
|
|
225
|
+
}
|