@polymorphism-tech/morph-spec 8.33.0 → 8.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +22 -2
- package/README.md +2 -2
- package/bin/morph-spec.js +43 -5
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +20 -22
- package/framework/agents.json +198 -44
- package/framework/ai-pin.json +22 -0
- package/framework/commands/morph-media.md +45 -0
- package/framework/docs/CLI.md +238 -10
- package/framework/evals/proof-rubrics/README.md +76 -0
- package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
- package/framework/evals/proof-rubrics/fiacao.md +54 -0
- package/framework/evals/proof-rubrics/guard-cego.md +52 -0
- package/framework/evals/proof-rubrics/media-claims.md +109 -0
- package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
- package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
- package/framework/evals/rubrics/architecture.md +30 -0
- package/framework/evals/rubrics/code-quality.md +26 -0
- package/framework/evals/rubrics/test-coverage.md +20 -0
- package/framework/hooks/README.md +11 -1
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
- package/framework/hooks/claude-code/core/chain.js +27 -7
- package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
- package/framework/hooks/claude-code/core/worktree-context.js +83 -14
- package/framework/hooks/claude-code/core/worktree-create.js +206 -0
- package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
- package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
- package/framework/hooks/shared/command-tokens.js +23 -0
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/git-paths.js +25 -0
- package/framework/hooks/shared/hook-response.js +17 -0
- package/framework/hooks/shared/morph-cli.js +134 -0
- package/framework/rules/ai-agents-standards.md +14 -2
- package/framework/rules/infrastructure-standards.md +3 -1
- package/framework/schemas/ai-pin.schema.json +97 -0
- package/framework/schemas/config.schema.json +107 -0
- package/framework/schemas/feature.schema.json +23 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +28 -7
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +20 -2
- package/framework/skills/core/morph-media/SKILL.md +215 -0
- package/framework/skills/core/morph-plan/SKILL.md +38 -4
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +166 -9
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/PROVENANCE.json +81 -3
- package/framework/standards/STANDARDS.json +1504 -830
- package/framework/standards/ai-agents/agent-archetypes.md +160 -0
- package/framework/standards/ai-agents/agent-session.md +256 -160
- package/framework/standards/ai-agents/agent-spec.md +196 -0
- package/framework/standards/ai-agents/batching.md +217 -193
- package/framework/standards/ai-agents/context-providers.md +261 -188
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
- package/framework/standards/ai-agents/cost-and-budget.md +256 -0
- package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
- package/framework/standards/ai-agents/evals-with-cache.md +289 -0
- package/framework/standards/ai-agents/guardrails.md +222 -0
- package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
- package/framework/standards/ai-agents/mcp-server.md +351 -250
- package/framework/standards/ai-agents/mcp-tools.md +250 -197
- package/framework/standards/ai-agents/media-pipeline.md +352 -0
- package/framework/standards/ai-agents/media-video.md +267 -0
- package/framework/standards/ai-agents/middleware-patterns.md +241 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +422 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +177 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +189 -181
- package/framework/standards/ai-agents/modalities-vision.md +205 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +211 -155
- package/framework/standards/ai-agents/observability-patterns.md +227 -17
- package/framework/standards/ai-agents/production.md +260 -231
- package/framework/standards/ai-agents/prompt-sources.md +389 -0
- package/framework/standards/ai-agents/providers/model-registry.md +293 -158
- package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
- package/framework/standards/ai-agents/service-tier-flex.md +143 -135
- package/framework/standards/ai-agents/setup.md +166 -50
- package/framework/standards/ai-agents/structured-output.md +280 -249
- package/framework/standards/ai-agents/sweet-spot.md +129 -109
- package/framework/standards/ai-agents/testing-ai.md +236 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
- package/framework/standards/ai-agents/workflows.md +27 -7
- package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +90 -0
- package/framework/templates/REGISTRY.json +14 -1
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
- package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
- package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
- package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
- package/framework/templates/dotnet/ai-kit/README.md +128 -0
- package/framework/templates/dotnet/ai-kit/global.json +9 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
- package/package.json +3 -3
- package/src/commands/e2e/e2e.js +91 -40
- package/src/commands/project/doctor.js +143 -6
- package/src/commands/project/env.js +95 -0
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +274 -53
- package/src/commands/state/advance.js +24 -2
- package/src/commands/state/approve.js +33 -1
- package/src/commands/state/create.js +72 -11
- package/src/commands/state/delete.js +8 -0
- package/src/commands/state/finish.js +360 -53
- package/src/commands/state/gate-check.js +190 -6
- package/src/commands/state/score.js +36 -1
- package/src/commands/validation/verify.js +134 -25
- package/src/core/state/state-manager.js +9 -1
- package/src/dashboard/app/pages/HookInspector.jsx +430 -418
- package/src/dashboard/scanner/hooks-scanner.js +5 -1
- package/src/lib/ai/ai-checks.js +182 -0
- package/src/lib/ai/ai-project-scan.js +347 -0
- package/src/lib/ai/code-audit.js +181 -0
- package/src/lib/ai/comment-stripper.js +184 -0
- package/src/lib/ai/pin-drift.js +138 -0
- package/src/lib/ai/registry-audit.js +285 -0
- package/src/lib/e2e/compose-env.js +55 -14
- package/src/lib/e2e/stack-teardown.js +115 -0
- package/src/lib/evaluator-report.js +269 -15
- package/src/lib/generators/architecture-scanner.js +2 -1
- package/src/lib/git/default-branch.js +92 -4
- package/src/lib/graph/graphifyignore.js +2 -1
- package/src/lib/installers/effects.js +193 -0
- package/src/lib/installers/templates-reconcile.js +3 -3
- package/src/lib/risk-detector.js +284 -0
- package/src/lib/standards/verified-against.js +276 -0
- package/src/lib/tasks/eval-node.js +485 -0
- package/src/lib/tasks/linux-runner.js +297 -0
- package/src/lib/tasks/test-nodes.js +486 -0
- package/src/lib/tasks/test-runner.js +431 -37
- package/src/lib/validators/verify-runner.js +590 -28
- package/src/lib/worktree/dirs.js +32 -0
- package/src/lib/worktree/docker-audit.js +287 -0
- package/src/lib/worktree/env-allocator.js +69 -11
- package/src/lib/worktree/worktree-audit.js +70 -3
- package/src/scripts/setup-infra.js +24 -11
- package/src/utils/agents-installer.js +18 -8
- package/src/utils/claude-md-injector.js +10 -5
- package/src/utils/file-copier.js +28 -16
- package/src/utils/hooks-installer.js +75 -29
- package/src/utils/skills-installer.js +194 -187
- package/src/utils/version-checker.js +68 -3
- package/framework/standards/ai-agents/prompts-in-db.md +0 -214
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Evals com cache — regressão de prompt e de modelo sem chave no CI
|
|
2
|
+
|
|
3
|
+
> **Scope:** stacks=["dotnet"]
|
|
4
|
+
> **Layer:** 1 (on-keyword)
|
|
5
|
+
> **Keywords:** eval, evals, response cache, cache de respostas, dataset de eval, regressão de prompt, troca de modelo, model swap, prompt regression, EvalFixture, OfflineChatClient, DiskBasedResponseCacheProvider, nó evals, --refresh-cache
|
|
6
|
+
> **Read by Claude in:** implement (quando a feature muda prompt, troca modelo, ou o projeto vai declarar o nó `evals` no `verify`)
|
|
7
|
+
|
|
8
|
+
**Verified against:** Microsoft.Extensions.AI.Evaluation 10.9.0 + `.Quality` + `.Reporting` (ai-pin 2026-09-08); provado por `templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs`, que compila e roda no CI contra exatamente essas versões. A superfície de `DiskBasedReportingConfiguration`, `DiskBasedResponseCacheProvider`, `ReportingConfiguration` e dos avaliadores de `.Quality` foi **medida por reflexão sobre as assemblies `Microsoft.Extensions.AI.Evaluation*` 10.9.0** em 2026-09-08. Last-verified: 2026-09-08.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Por que eval não é teste unitário
|
|
13
|
+
|
|
14
|
+
Um teste unitário responde "esta função ainda faz o que fazia?". Um eval responde
|
|
15
|
+
"este **produto** ainda responde o que respondia?" — e a diferença não é de grau.
|
|
16
|
+
|
|
17
|
+
Trocar o prompt de sistema de um agente, ou trocar `gpt-4.1` por `gpt-5`, deixa **build
|
|
18
|
+
verde e suíte unitária verde**. Nenhuma das duas coisas lê o comportamento do modelo; as
|
|
19
|
+
duas leem o código em volta dele. O prompt e o modelo são a única parte do produto cujo
|
|
20
|
+
comportamento não é coberto por nada do que já existe no gate.
|
|
21
|
+
|
|
22
|
+
O precedente é de campo, e é caro: um repositório real manteve o script de eval **fora do
|
|
23
|
+
CI** por decisão escrita (`ci.yml`, decisão registrada D-F5-2) porque *"variância de LLM
|
|
24
|
+
real não pertence a pipeline determinístico"*. A decisão estava certa **para aquele
|
|
25
|
+
desenho**. Um eval que chama o modelo a cada PR é lento, caro e flaky — e um eval flaky é
|
|
26
|
+
um eval desligado, que verifica exatamente nada.
|
|
27
|
+
|
|
28
|
+
Este standard descreve o desenho em que a decisão muda: **o eval lê de uma cache de
|
|
29
|
+
respostas commitada**, e por isso é rápido, gratuito e determinístico.
|
|
30
|
+
|
|
31
|
+
## O que se ganha, em uma frase
|
|
32
|
+
|
|
33
|
+
Com a cache quente, a suíte de eval roda **sem credencial e sem rede**: a biblioteca
|
|
34
|
+
embrulha o cliente interno num cliente de cache, e no *hit* o cliente interno nunca é
|
|
35
|
+
invocado. Trocando o cliente interno por um que **recusa** (`OfflineChatClient`), o único
|
|
36
|
+
caminho para a rede fica fechado — ou tudo resolve pela cache, ou a suíte fica vermelha
|
|
37
|
+
nomeando o cenário que faltou.
|
|
38
|
+
|
|
39
|
+
Isso é o que permite o nó de eval existir no CI de um repositório que **não tem uma única
|
|
40
|
+
chave de LLM nos seus workflows**. Um mecanismo que exigisse uma chave seria adotado por
|
|
41
|
+
ninguém.
|
|
42
|
+
|
|
43
|
+
## Dataset por agente e por tenant
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
eval-datasets/
|
|
47
|
+
├── triagem/acme/a-saudacao.json
|
|
48
|
+
├── triagem/acme/b-escalonamento.json
|
|
49
|
+
├── triagem/globex/a-saudacao.json
|
|
50
|
+
└── orcamento/acme/a-resposta-estruturada.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Os dois primeiros segmentos existem porque a pergunta útil quase nunca é "o produto
|
|
54
|
+
regrediu?", e sim **"o agente X regrediu para o tenant Y?"**. Um dataset plano não responde
|
|
55
|
+
isso sem convenção de nome, e convenção de nome é a forma mais frágil de estrutura.
|
|
56
|
+
|
|
57
|
+
O caminho **é** a identidade do cenário: ele vira a chave do cenário na cache. Dois tenants
|
|
58
|
+
têm cenários com o mesmo nome de propósito (`saudacao`, `handoff`), e uma chave que os
|
|
59
|
+
confundisse devolveria a resposta de um tenant para o outro.
|
|
60
|
+
|
|
61
|
+
> **Dado real de tenant é proibido no dataset.** A cache guarda **mensagens inteiras** e é
|
|
62
|
+
> commitada: um segredo que entre no dataset entra no git, para sempre. O fixture só lê
|
|
63
|
+
> cenário do diretório versionado — nunca de banco, nunca de variável de ambiente.
|
|
64
|
+
|
|
65
|
+
## O que entra na chave de cache
|
|
66
|
+
|
|
67
|
+
Medido no pacote 10.9.0, e o achado importa:
|
|
68
|
+
|
|
69
|
+
| Entra na chave | Não entra |
|
|
70
|
+
|---|---|
|
|
71
|
+
| As mensagens e as `ChatOptions` | **O endpoint (`ProviderUri`)** |
|
|
72
|
+
| `ProviderName` do `ChatClientMetadata` | |
|
|
73
|
+
| `DefaultModelId` do `ChatClientMetadata` | |
|
|
74
|
+
| A versão interna de formato de cache da biblioteca | |
|
|
75
|
+
|
|
76
|
+
**A documentação afirma que o endpoint entra. Ele não entra.** A divergência entre o
|
|
77
|
+
comentário XML da API e o comportamento medido é o que torna possível um cliente offline
|
|
78
|
+
sem endpoint algum: ele não tem para onde ligar, e não precisa ter.
|
|
79
|
+
|
|
80
|
+
A consequência prática: o cliente que **lê** a cache tem de replicar `ProviderName` e
|
|
81
|
+
`DefaultModelId` do run que a **gravou**. Se divergirem, a chave computada na leitura é
|
|
82
|
+
outra e **todo** cenário dá miss — uma cache quente indistinguível de uma cache vazia.
|
|
83
|
+
|
|
84
|
+
## TTL explícito, sempre — e por quê
|
|
85
|
+
|
|
86
|
+
`Defaults.DefaultTimeToLiveForCacheEntries` são **14 dias**, e a expiração é **destrutiva na
|
|
87
|
+
leitura**: ao encontrar uma entrada vencida, o provedor **apaga o diretório da chave** antes
|
|
88
|
+
de devolver `null`.
|
|
89
|
+
|
|
90
|
+
Uma cache commitada com o default **se apaga sozinha ao ser lida**, duas semanas depois de
|
|
91
|
+
gravada. O sintoma chega como CI verde por duas semanas e então miss em massa num dia em
|
|
92
|
+
que ninguém mexeu em nada — com a árvore apagada no working tree, sujando o `git status`.
|
|
93
|
+
|
|
94
|
+
Um TTL implícito aqui não é preguiça de configuração; é uma bomba-relógio. Declare-o, e
|
|
95
|
+
declare-o **longo** (o kit recusa qualquer valor abaixo de um ano, com a razão na mensagem).
|
|
96
|
+
|
|
97
|
+
## O carimbo, e o que ele responde de graça
|
|
98
|
+
|
|
99
|
+
A função de hash que produz a chave **não promete ser estável entre versões da biblioteca**
|
|
100
|
+
— a própria biblioteca carrega um número de versão de cache interno, justamente para
|
|
101
|
+
invalidar tudo quando o formato muda. Ou seja: **subir o pin pode invalidar a árvore
|
|
102
|
+
inteira**.
|
|
103
|
+
|
|
104
|
+
Sem carimbo, isso chega como N misses inexplicáveis. Com carimbo, chega como uma frase:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
eval-cache-stale-pin: Microsoft.Extensions.AI.Evaluation: gravada 10.9.0 vs pin 10.11.0
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`.morph-eval-cache.json` fica na raiz da árvore e guarda o `pinnedAt` e as versões da
|
|
111
|
+
família `Microsoft.Extensions.AI` sob as quais a árvore foi gravada. **Carimbo ausente tem
|
|
112
|
+
motivo próprio**, distinto de carimbo divergente: as duas situações pedem ações diferentes
|
|
113
|
+
— a primeira é "regrave", a segunda é "regrave **porque o pacote mudou**".
|
|
114
|
+
|
|
115
|
+
## O que se commita, e o que não
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
<cachePath>/ COMMITADO
|
|
119
|
+
├── .morph-eval-cache.json o carimbo
|
|
120
|
+
└── cache/<cenário>/<iteração>/<chave-curta>/{entry.json, contents.data}
|
|
121
|
+
|
|
122
|
+
<cachePath>/../eval-results/ NUNCA COMMITADO
|
|
123
|
+
└── results/<execução>/<cenário>/<iteração>.json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Os resultados de execução carregam **latência medida por cronômetro, uso de tokens e o flag
|
|
127
|
+
de cache hit**: são não-determinísticos por natureza, e commitá-los é diff garantido em toda
|
|
128
|
+
rodada, sem sinal nenhum.
|
|
129
|
+
|
|
130
|
+
Eles ficam fora da árvore commitada **por construção** — outra raiz de armazenamento — e
|
|
131
|
+
não por um `.gitignore` dentro de um diretório commitado. Uma regra de ignore ali é uma
|
|
132
|
+
regra que a próxima pessoa remove sem entender.
|
|
133
|
+
|
|
134
|
+
### Por que a chave é CURTA, e por que isso não é detalhe
|
|
135
|
+
|
|
136
|
+
O provedor de cache do kit (`ShortPathResponseCacheProvider`) nomeia o diretório da entrada
|
|
137
|
+
com os **16 primeiros caracteres** do hash, e não com o hash inteiro. A chave completa é
|
|
138
|
+
verificada dentro do `entry.json`: se dois cenários colidirem no prefixo, a leitura vira
|
|
139
|
+
**miss** — nunca a resposta errada.
|
|
140
|
+
|
|
141
|
+
O motivo é medido, e custou uma rodada inteira. Com o hash inteiro (96 caracteres em hex), o
|
|
142
|
+
pior caminho relativo desta árvore dava **225**. Somado à raiz do repositório nesta máquina
|
|
143
|
+
(73), o absoluto dava **298** — contra os 260 do `MAX_PATH` do Windows. O git **não recusou
|
|
144
|
+
barulhento**: avisou `Filename too long` no stderr, commitou **1 de 9 arquivos**, e saiu com
|
|
145
|
+
código 0.
|
|
146
|
+
|
|
147
|
+
E o dano real não foi o arquivo faltando. Foi que o canário de CI — um `git diff --exit-code`
|
|
148
|
+
sobre a árvore — passou a **reportar limpo**, não porque a árvore estivesse limpa, mas porque
|
|
149
|
+
o git nunca enxergou aqueles arquivos. **Um guarda que não consegue observar o objeto reporta
|
|
150
|
+
verde por construção.**
|
|
151
|
+
|
|
152
|
+
Com a chave curta o pior caminho é **145**, e o teto que o cobra é 160 — a diferença é
|
|
153
|
+
orçamento explícito, não sobra por acaso:
|
|
154
|
+
|
|
155
|
+
| Parcela | Chars |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `MAX_PATH` utilizável do Windows | 259 |
|
|
158
|
+
| − `node_modules\@polymorphism-tech\morph-spec\` (estes arquivos viajam no tarball npm) | 43 |
|
|
159
|
+
| − teto do caminho relativo | 160 |
|
|
160
|
+
| = folga para a raiz do projeto de quem instala | **56** |
|
|
161
|
+
|
|
162
|
+
Quem cobra os dois fatos — que cada arquivo é **endereçável pelo git** e que o pior caminho
|
|
163
|
+
cabe no teto — é `test/framework/eval-cache-portability.test.js`, e ele recusa passar sobre
|
|
164
|
+
um conjunto vazio: um canário que roda sobre zero arquivos é exatamente o canário cego que
|
|
165
|
+
o defeito original tinha.
|
|
166
|
+
|
|
167
|
+
### O payload é opaco: `-text` no `.gitattributes`
|
|
168
|
+
|
|
169
|
+
A árvore de cache precisa de uma linha própria em `.gitattributes`:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
<cachePath>/** -text
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Sem ela, o `text=auto eol=lf` do repositório **normaliza** os `contents.data` — medido: avisos
|
|
176
|
+
de CRLF→LF nos quatro payloads, e bytes que não sobreviveriam a um clone. `-text` (e não
|
|
177
|
+
`binary`) porque ele preserva a fidelidade de byte **sem** perder o diff legível: quando o
|
|
178
|
+
canário acusa, ele mostra o quê mudou, e não apenas que mudou.
|
|
179
|
+
|
|
180
|
+
E o invariante que sustenta a commitabilidade é barato de cobrar: **um run offline não
|
|
181
|
+
escreve um byte na árvore de cache.** Um canário que compara a árvore antes e depois (por
|
|
182
|
+
conteúdo, não por data de modificação) morde no dia em que a biblioteca passar a tocar disco
|
|
183
|
+
no caminho de hit — e não meses depois, quando alguém estranhar um diff que aparece sozinho.
|
|
184
|
+
|
|
185
|
+
## Gate duro só em propriedade determinística
|
|
186
|
+
|
|
187
|
+
| Papel | Quem | Reprova sozinho? |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| Asserção determinística | contém / não contém, chave de JSON, ferramenta escolhida, argumento de ferramenta, ausência de PII | **SIM** — é o gate |
|
|
190
|
+
| Juízo de LLM | `RelevanceEvaluator`, `CompletenessEvaluator` | **NÃO** — número reportado |
|
|
191
|
+
|
|
192
|
+
Um juízo de LLM que reprovasse sozinho reintroduziria exatamente o `FLAKY` que fez a decisão
|
|
193
|
+
D-F5-2 manter o eval fora do CI. A métrica de juízo continua útil (dá tendência entre
|
|
194
|
+
versões de prompt) sem precisar ser autoritativa — e, com a cache quente, ela é
|
|
195
|
+
determinística também, porque a resposta do juiz está na cache.
|
|
196
|
+
|
|
197
|
+
> **Dois dos quatro avaliadores de qualidade são `[Experimental("AIEVAL001")]` na 10.9.0**
|
|
198
|
+
> (`TaskAdherenceEvaluator` e `ToolCallAccuracyEvaluator`). Sob `TreatWarningsAsErrors`,
|
|
199
|
+
> usá-los exige suprimir um diagnóstico — uma decisão consciente, nunca um efeito colateral.
|
|
200
|
+
> `.Safety` e `.NLP` ficam fora por outro motivo: ambos são preview, e `.Safety` exige
|
|
201
|
+
> `TokenCredential` + Azure AI Foundry **na construção** — o oposto de "roda em CI sem
|
|
202
|
+
> credencial".
|
|
203
|
+
|
|
204
|
+
Um cenário **sem** asserção determinística não passa: ele não cobra nada. Deixá-lo verde é o
|
|
205
|
+
mesmo defeito de um teste vazio, e aqui seria pior, porque o cenário apareceria na contagem
|
|
206
|
+
como se tivesse verificado algo.
|
|
207
|
+
|
|
208
|
+
## Quando o eval roda
|
|
209
|
+
|
|
210
|
+
Declare-o como um nó a mais em `project.tests[]` — não como uma estrutura paralela:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"project": {
|
|
215
|
+
"tests": [
|
|
216
|
+
{ "id": "unit", "command": "dotnet test tests/App.Tests/App.Tests.csproj" },
|
|
217
|
+
{
|
|
218
|
+
"id": "evals",
|
|
219
|
+
"runner": "evals",
|
|
220
|
+
"command": "dotnet test tests/App.Tests/App.Tests.csproj --filter-trait Category=Eval",
|
|
221
|
+
"evals": { "cachePath": "tests/App.Tests/eval-cache", "envFile": ".env.evals" }
|
|
222
|
+
}
|
|
223
|
+
],
|
|
224
|
+
"evals": { "watch": ["tenants/**", "product/**", "config/model-registry.json"] }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
| Quando | O que acontece |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `verify {feature} {task}` (loop de task) | **pulado**, com motivo registrado — o relógio de uma task não paga uma suíte de eval |
|
|
232
|
+
| `verify {feature}` (escopo de feature) | **roda**, offline, contra a cache commitada |
|
|
233
|
+
| miss de cache sem credencial | **reprova** nomeando o cenário, o caminho, o comando de regravação e o arquivo de credencial |
|
|
234
|
+
| `--refresh-cache` | regrava a árvore contra o provider real. **Nunca implícito**: é uma chamada paga |
|
|
235
|
+
|
|
236
|
+
**Miss jamais vira `skip`.** A tentação é real ("não dá para avaliar, então não avalio") e
|
|
237
|
+
recria o buraco que este harness já fechou duas vezes: gate verde sobre verificação que não
|
|
238
|
+
aconteceu. E há razão positiva: em CI, com cache commitada, um miss significa **uma coisa
|
|
239
|
+
só** — o prompt ou o modelo mudou e ninguém regravou. O miss é o veredito, não o obstáculo.
|
|
240
|
+
|
|
241
|
+
## `evals.watch[]`: o que conta como "mudei o prompt"
|
|
242
|
+
|
|
243
|
+
Os globs declaram, **para o projeto**, quais caminhos são prompt ou modelo. Eles são casados
|
|
244
|
+
contra o que a feature mudou (diff contra a branch default **mais** arquivos novos ainda não
|
|
245
|
+
rastreados) e alimentam a regra de Gate 3: tocou caminho declarado e não há eval verde
|
|
246
|
+
carimbado no mesmo commit ⇒ o gate **pausa** e nomeia o caminho.
|
|
247
|
+
|
|
248
|
+
Exclua `<cachePath>/**` e `<datasetPath>/**` dos globs. Sem isso, regravar a cache dispara a
|
|
249
|
+
própria regra.
|
|
250
|
+
|
|
251
|
+
O detector é por caminho, e as três formas conhecidas de escapar dele estão ditas em voz
|
|
252
|
+
alta, porque um detector que se apresenta como completo e não é vale menos que nenhum:
|
|
253
|
+
|
|
254
|
+
1. **eval trivialmente verde** — um cenário só, asserção vazia. É a mesma classe de contorno
|
|
255
|
+
de um teste vazio, e a resposta é a rubrica de prova, não uma segunda regra determinística
|
|
256
|
+
que também seria contornável;
|
|
257
|
+
2. **prompt publicado direto no banco**, sem tocar arquivo;
|
|
258
|
+
3. **modelo trocado por variável de ambiente** no provedor de deploy — invisível ao
|
|
259
|
+
repositório por construção.
|
|
260
|
+
|
|
261
|
+
## Semeando a cache exemplar sem gastar dinheiro
|
|
262
|
+
|
|
263
|
+
A cache commitada de um kit ou de um projeto novo é gravada rodando o fixture em modo
|
|
264
|
+
`Refresh` contra um **fake roteirizado**, cujo `ChatClientMetadata` é configurado com o
|
|
265
|
+
mesmo `providerName`/`defaultModelId` que o cliente offline vai replicar.
|
|
266
|
+
|
|
267
|
+
Isto não é um atalho: o caminho de **escrita** exercitado é o da biblioteca, e a chave é
|
|
268
|
+
computada pela mesma função que a leitura vai usar. A leitura offline lê exatamente o que
|
|
269
|
+
aquela escrita produziu.
|
|
270
|
+
|
|
271
|
+
**A lacuna residual, declarada:** isso não prova que uma cache gravada contra um provider
|
|
272
|
+
real é legível offline. O que a mitiga é o fato medido acima — a chave depende de
|
|
273
|
+
`ProviderName` e `DefaultModelId`, e não do endpoint, e ambos estão sob nosso controle.
|
|
274
|
+
|
|
275
|
+
## Fronteira com `testing-ai`
|
|
276
|
+
|
|
277
|
+
| Standard | Cobre |
|
|
278
|
+
|---|---|
|
|
279
|
+
| `testing-ai` | Fake roteirizado rodando o loop real do agente; prova ferramenta escolhida, argumentos e guard. **Sem rede, sem modelo** |
|
|
280
|
+
| `evals-with-cache` (este) | Modelo **real** com cache em disco; dataset por tenant/agente; regressão de prompt e de modelo; o nó `evals` no `verify` |
|
|
281
|
+
|
|
282
|
+
Em uma frase: `testing-ai` prova que **o código em volta do modelo** se comporta;
|
|
283
|
+
`evals-with-cache` prova que **o modelo** ainda responde o que respondia.
|
|
284
|
+
|
|
285
|
+
## Se precisar mapear o blast radius de um prompt
|
|
286
|
+
|
|
287
|
+
`morph-spec graph explain <símbolo>` ajuda a achar quem lê um prompt ou um alias de modelo.
|
|
288
|
+
**Sem grafo construído, caia para `grep -rn` e siga em frente** — a ausência do grafo nunca
|
|
289
|
+
bloqueia este ciclo, e o fallback é a resposta correta, não um plano B envergonhado.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Guardrails — o que o código garante quando o prompt já falhou
|
|
2
|
+
|
|
3
|
+
> **Scope:** stacks=["dotnet"]
|
|
4
|
+
> **Layer:** 1 (on-keyword)
|
|
5
|
+
> **Keywords:** guardrail, guard de tool, regra proibida, validacao de pertinencia, fail-open guard, argumento nao confiavel, choke-point, veredito ternario
|
|
6
|
+
> **Read by Claude in:** implement (ao escrever regra de negócio que o modelo poderia violar) e review (ao avaliar o que garante o comportamento)
|
|
7
|
+
|
|
8
|
+
**Verified against:** Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08). **Verificação documental**, sem cláusula `provado por` — o ai-kit não traz um guard de exemplo; o padrão é destilado de dois repositórios em produção, com o local de cada evidência citado na seção correspondente. Last-verified: 2026-09-08.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Regra proibida vira código
|
|
13
|
+
|
|
14
|
+
**Instrução que já falhou ao vivo não se reforça escrevendo-a de novo em maiúsculas.**
|
|
15
|
+
|
|
16
|
+
Quando uma regra tem consequência real — não falar de um assunto, não prometer um prazo, não citar
|
|
17
|
+
um valor — ela sai do prompt e vira uma condição em C#. O prompt continua existindo para *orientar*;
|
|
18
|
+
o código existe para *garantir*. São papéis diferentes, e confundi-los é como se descobre em
|
|
19
|
+
produção que "o modelo geralmente obedece".
|
|
20
|
+
|
|
21
|
+
Três formas medidas em campo, em ordem crescente de força:
|
|
22
|
+
|
|
23
|
+
| Forma | Onde acontece | O que garante |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| **Gatilho literal avaliado antes do turno** | No compositor, sobre o texto de entrada | O assunto proibido nunca chega ao modelo com as tools que o executariam |
|
|
26
|
+
| **Veredito calculado em .NET antes do turno e reescrito depois sobre a resposta** | Antes e depois da chamada | O número que o produto publica é o número que o backend calculou, não o que o modelo escreveu |
|
|
27
|
+
| **Dado resolvido e pontuado em .NET, com o agente apenas copiando** | Fora do modelo | O modelo não tem oportunidade de errar a conta |
|
|
28
|
+
|
|
29
|
+
**O motivo empírico está escrito no próprio código de campo**, e é a frase mais útil desta seção:
|
|
30
|
+
*"o modelo obedece bem a 'uma entrada por elemento desta lista', mas erra feio quando precisa julgar
|
|
31
|
+
sozinho quais blocos têm dado"* — com a medição ao lado: uma saída esperada de dez itens saía com
|
|
32
|
+
dois.
|
|
33
|
+
|
|
34
|
+
A leitura correta disso não é "o modelo é ruim". É: **o modelo é bom em transformação enumerada e
|
|
35
|
+
ruim em julgamento de suficiência.** Dê-lhe a lista; faça o julgamento você.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Onde fica o choke-point
|
|
40
|
+
|
|
41
|
+
Três lugares legítimos, e cada um pega uma classe diferente de problema. Escolher o errado é o que
|
|
42
|
+
faz um guard parecer instalado sem estar.
|
|
43
|
+
|
|
44
|
+
**(a) Antes do turno, no compositor.** Pega o que não deve nem ser oferecido: a fase errada, a tool
|
|
45
|
+
que este tenant não tem, o assunto proibido. É o mais barato — o modelo nem é chamado — e o único
|
|
46
|
+
que evita gastar dinheiro com uma chamada que já nasceu errada.
|
|
47
|
+
|
|
48
|
+
**(b) No middleware de invocação de função.** Pega o que o modelo pediu e não deveria ter pedido, e
|
|
49
|
+
decide **o que volta ao modelo**. Duas exigências de forma:
|
|
50
|
+
|
|
51
|
+
- **Vocabulário fechado de desfecho.** `ok` · `error` · `invalid_args` · `limit` · `unavailable`.
|
|
52
|
+
Fechado porque a alternativa — devolver a mensagem da exceção — dá ao modelo texto livre para
|
|
53
|
+
interpretar, e o modelo interpreta.
|
|
54
|
+
- **Argumento obrigatório ausente é barrado sem executar a tool**, lendo `required` do próprio
|
|
55
|
+
schema JSON da função. Não se escreve a lista de obrigatórios num segundo lugar: ela já existe, e
|
|
56
|
+
a segunda cópia é a que diverge.
|
|
57
|
+
|
|
58
|
+
**(c) Depois do run, como backstop.** É o único caminho em que o **código** invoca a implementação
|
|
59
|
+
da tool diretamente, quando a resposta indica que algo obrigatório não foi feito. Serve como rede,
|
|
60
|
+
nunca como mecanismo principal: um guard que só existe em (c) deixa o modelo decidir e depois
|
|
61
|
+
conserta, pagando as duas coisas.
|
|
62
|
+
|
|
63
|
+
O **mecanismo** de middleware — os três níveis, o registro no builder, a ordem — é de
|
|
64
|
+
`ai-agents-middleware-patterns`. Este standard usa esse mecanismo e não o repete.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Argumento de tool é input não confiável
|
|
69
|
+
|
|
70
|
+
**Argumento vindo do modelo é entrada de usuário, com a agravante de ser gerada por algo que não
|
|
71
|
+
tem intenção.**
|
|
72
|
+
|
|
73
|
+
A regra: **identificadores e valores com efeito vêm da closure do turno, nunca do modelo.** A tool é
|
|
74
|
+
construída já sabendo em qual conversa, em qual tenant e para qual registro ela opera; o modelo só
|
|
75
|
+
fornece o que é genuinamente decisão dele.
|
|
76
|
+
|
|
77
|
+
Os dois lados existem no acervo, e vale ver os dois:
|
|
78
|
+
|
|
79
|
+
- **Padrão:** as tools de um produto são construídas sobre closures, com a regra escrita no código —
|
|
80
|
+
*"o modelo nunca fornece o identificador da conta, o identificador do contato nem valores
|
|
81
|
+
monetários"*.
|
|
82
|
+
- **Padrão, no outro projeto:** o filtro de tenant da busca vetorial *"sempre vem do caller, nunca
|
|
83
|
+
do LLM"*.
|
|
84
|
+
|
|
85
|
+
O caso que essa regra impede é direto: uma tool que aceita o identificador do registro como
|
|
86
|
+
argumento é uma tool que, com o prompt certo na conversa errada, opera sobre o registro de outra
|
|
87
|
+
pessoa. Não é um risco hipotético de segurança — é um bug de correção que aparece como suporte.
|
|
88
|
+
|
|
89
|
+
Corolário: **valide o que sobra.** O que o modelo legitimamente decide (um texto, uma categoria, um
|
|
90
|
+
valor dentro de uma faixa) ainda passa pelo schema e pelas regras de domínio, como qualquer entrada.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Pertinência, não presença
|
|
95
|
+
|
|
96
|
+
Esta é a seção difícil, e a que separa um guard de um `!= null`.
|
|
97
|
+
|
|
98
|
+
O guard fraco pergunta: *o campo veio preenchido?*
|
|
99
|
+
O guard forte pergunta: **o que veio se sustenta?**
|
|
100
|
+
|
|
101
|
+
Três formas medidas, em ordem crescente de custo e de força:
|
|
102
|
+
|
|
103
|
+
**(1) Rubrica determinística que soma 100.** As bandas espelham o que o prompt pediu, há uma
|
|
104
|
+
whitelist explícita de "sem dados" (para que ausência legítima não seja punida como erro), e — o
|
|
105
|
+
detalhe que faz a diferença — **os pontos que o backend já calculou são conferidos por igualdade**,
|
|
106
|
+
não por plausibilidade. O validador **acumula todos os erros** em vez de parar no primeiro, e
|
|
107
|
+
classifica o resultado em **incompleto × inválido**, que são coisas diferentes: o primeiro pede mais
|
|
108
|
+
dado, o segundo pede rejeição. Referência de campo: o maior arquivo da camada de agentes daquele
|
|
109
|
+
projeto, com 343 linhas, é justamente esse validador.
|
|
110
|
+
|
|
111
|
+
**(2) Trava anti-alucinação por proveniência.** A afirmação do modelo só sobrevive se houver
|
|
112
|
+
**rastro de origem**: um "encontrei" só permanece verdadeiro se o identificador correspondente
|
|
113
|
+
estiver no sink de captura — isto é, se alguma tool de fato o raspou. Sem rastro, o resultado é
|
|
114
|
+
**rebaixado** para "não encontrado", **preservando o texto de raciocínio original para auditoria**.
|
|
115
|
+
Preservar é parte do padrão: apagar o raciocínio destrói a única pista de por que o modelo
|
|
116
|
+
alucinou.
|
|
117
|
+
|
|
118
|
+
**(3) Corte de confiança, com o caso real que o justifica.** O modelo casou um perfil apenas pelo
|
|
119
|
+
nome e devolveu confiança 85, usando **a ausência de concorrentes como evidência a favor** — um
|
|
120
|
+
raciocínio que soa razoável e é inválido. O remédio foi um mínimo de confiança **por rede**, não um
|
|
121
|
+
mínimo global: a mesma pontuação significa coisas diferentes em fontes diferentes.
|
|
122
|
+
|
|
123
|
+
O que as três têm em comum: **elas comparam com algo que não veio do modelo** — uma conta do
|
|
124
|
+
backend, um rastro de execução, um limiar calibrado por fonte. Um guard que só olha para a resposta
|
|
125
|
+
do modelo está pedindo ao modelo que se avalie.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Quando o guard não consegue decidir
|
|
130
|
+
|
|
131
|
+
A seção que o campo obriga, porque é o caso que mais acontece e o menos escrito.
|
|
132
|
+
|
|
133
|
+
**O acervo responde fail-open, sempre.** Configuração ausente resolve para "pode"; configuração
|
|
134
|
+
ilegível resolve para "pode"; os caminhos auxiliares (persona, sumarização, análise de memória, base
|
|
135
|
+
de conhecimento, multimodal) devolvem vazio ou nulo em qualquer falha, e o turno segue.
|
|
136
|
+
|
|
137
|
+
**Este standard ratifica o fail-open — com uma condição dura: o escape é registrado, nunca
|
|
138
|
+
silencioso.**
|
|
139
|
+
|
|
140
|
+
Ratifica porque a alternativa é pior no caso concreto: um guard que fecha quando não sabe derruba a
|
|
141
|
+
conversa por causa de uma configuração ausente, e o custo de um falso bloqueio numa conversa ao vivo
|
|
142
|
+
é maior que o de uma permissão a mais que alguém vai auditar.
|
|
143
|
+
|
|
144
|
+
A condição é dura porque sem ela o fail-open é indistinguível de um bug.
|
|
145
|
+
|
|
146
|
+
**Precedente que mostra a forma certa:** um guard **deliberadamente permeável** — rejeita a primeira
|
|
147
|
+
tentativa, a segunda passa — **com o escape instrumentado**, num contador dedicado. A permeabilidade
|
|
148
|
+
foi uma decisão de produto; a instrumentação é o que a torna revisável. *Guard permeável e medido é
|
|
149
|
+
engenharia; guard permeável e mudo é bug.*
|
|
150
|
+
|
|
151
|
+
**O anti-padrão, com número do campo:** **81 de 83 blocos `catch (Exception ex)` sem leitura do
|
|
152
|
+
`ex`**, no mesmo projeto. Isso é fail-open sem instrumentação em escala — e está registrado como
|
|
153
|
+
dívida pelo próprio time, o que significa que nem eles conseguem responder o que está sendo
|
|
154
|
+
engolido.
|
|
155
|
+
|
|
156
|
+
### Veredito ternário
|
|
157
|
+
|
|
158
|
+
O resultado de um guard é `pass` / `reject` / `not_evaluated`, com **`reject` vencendo `pass` na
|
|
159
|
+
mesma execução**.
|
|
160
|
+
|
|
161
|
+
Um booleano perde exatamente a informação que o fail-open produz: *não deu para avaliar*. Com
|
|
162
|
+
booleano, "não avaliado" vira "aprovado", e a métrica de aprovação passa a incluir tudo o que o
|
|
163
|
+
guard não conseguiu olhar — que é a métrica errada com a aparência da certa.
|
|
164
|
+
|
|
165
|
+
`not_evaluated` também é o que permite responder "quantas vezes este guard sequer rodou?", que é a
|
|
166
|
+
primeira pergunta a fazer quando um guard "nunca reprova nada".
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Anti-patterns
|
|
171
|
+
|
|
172
|
+
| Anti-pattern | Por quê é problema | Solução |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| Regra crítica reforçada em maiúsculas no prompt | O modelo geralmente obedece; "geralmente" não é garantia | Vira condição em código |
|
|
175
|
+
| Pedir ao modelo que julgue quais blocos têm dado | Medido: dez itens esperados, dois entregues | Enumere você; peça transformação, não julgamento |
|
|
176
|
+
| Guard só depois do run | Paga a chamada errada e depois conserta | Choke-point antes do turno, quando possível |
|
|
177
|
+
| Devolver a mensagem da exceção ao modelo | Texto livre que o modelo interpreta | Vocabulário fechado de desfecho |
|
|
178
|
+
| Lista de argumentos obrigatórios escrita num segundo lugar | A segunda cópia diverge do schema | Leia `required` do próprio schema da função |
|
|
179
|
+
| Identificador ou valor vindo do modelo | Com o prompt certo, opera no registro de outra pessoa | Closure do turno |
|
|
180
|
+
| Guard que checa presença (`!= null`) | Aprova qualquer coisa preenchida, inclusive alucinação | Compare com algo que não veio do modelo |
|
|
181
|
+
| Validador que para no primeiro erro | Uma correção por rodada; o custo vira o número de rodadas | Acumule todos, e separe incompleto de inválido |
|
|
182
|
+
| Rebaixar uma alucinação apagando o raciocínio | Destrói a única pista de por que aconteceu | Rebaixe preservando o raciocínio para auditoria |
|
|
183
|
+
| Limiar de confiança global | A mesma pontuação significa coisas diferentes por fonte | Limiar por fonte |
|
|
184
|
+
| Fail-open silencioso | Indistinguível de bug; 81 de 83 `catch` sem leitura | Fail-open **registrado** |
|
|
185
|
+
| Veredito booleano | "Não avaliado" vira "aprovado", e a métrica mente | `pass` / `reject` / `not_evaluated` |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Checklist (verifiable by morph-eval)
|
|
190
|
+
|
|
191
|
+
- [ ] Toda regra com consequência real existe como condição em código, não apenas no prompt
|
|
192
|
+
- [ ] O choke-point escolhido está nomeado no `decisions.md`, com o que ele pega
|
|
193
|
+
- [ ] O middleware de tool devolve **vocabulário fechado** de desfecho
|
|
194
|
+
- [ ] Argumento obrigatório ausente é barrado **sem** executar a tool, lendo o schema da própria função
|
|
195
|
+
- [ ] Nenhum identificador com efeito vem do modelo — todos vêm da closure do turno
|
|
196
|
+
- [ ] Ao menos um guard compara a saída com algo que **não** veio do modelo
|
|
197
|
+
- [ ] Validador acumula erros e distingue incompleto de inválido
|
|
198
|
+
- [ ] Todo caminho fail-open **registra** o escape num contador ou span
|
|
199
|
+
- [ ] O veredito é ternário, e `reject` vence `pass` na mesma execução
|
|
200
|
+
- [ ] Existe forma de responder "quantas vezes este guard rodou, e quantas ele não conseguiu avaliar"
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## References
|
|
205
|
+
|
|
206
|
+
- `ai-agents-middleware-patterns` — **dono do mecanismo** de middleware de invocação de função
|
|
207
|
+
(os três níveis, o registro, a ordem). Este standard o usa e não o repete
|
|
208
|
+
- `ai-agents-conversational-agent-with-phases` — o choke-point (a) na prática: compositor,
|
|
209
|
+
allow-list por fase e teto de tool calls
|
|
210
|
+
- `ai-agents-agent-spec` — a spec do agente, onde as tools do turno são declaradas
|
|
211
|
+
- `ai-agents-structured-output` — o schema cuja seção `required` o guard lê
|
|
212
|
+
- `ai-agents-observability-patterns` — onde o escape registrado vira span e métrica
|
|
213
|
+
- `ai-agents-production` — o panorama que hoje cita guardrails como um item de checklist
|
|
214
|
+
|
|
215
|
+
> **Lacuna declarada.** O par compilável deste standard seria um guard de exemplo em
|
|
216
|
+
> `templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/`, com o teste que o vê barrar um argumento
|
|
217
|
+
> obrigatório ausente **sem** executar a tool. **Ele ainda não existe** no ai-kit. Por isso o
|
|
218
|
+
> cabeçalho declara verificação documental e não carimba `provado por`.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
*MORPH-SPEC by Polymorphism Tech — ai-agents/guardrails.md v1.0 (2026-09-08)*
|