@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
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { existsSync } from 'fs';
|
|
19
|
-
import fs from 'fs-extra';
|
|
20
19
|
import { compareVersions } from '../../utils/version-checker.js';
|
|
20
|
+
import { realEffects } from './effects.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Last CLI version whose `update`/`setup-infra` still wrote
|
|
@@ -62,7 +62,7 @@ export const LAST_VERSION_THAT_COPIED_TEMPLATES = '8.17.0';
|
|
|
62
62
|
* version BEFORE this run (e.g. `checkProjectOutdated().current`)
|
|
63
63
|
* @returns {'absent'|'removed-legacy'|'preserved-override'}
|
|
64
64
|
*/
|
|
65
|
-
export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion) {
|
|
65
|
+
export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion, fx = realEffects()) {
|
|
66
66
|
if (!existsSync(templatesDest)) return 'absent';
|
|
67
67
|
|
|
68
68
|
const isLegacy = !priorProjectVersion
|
|
@@ -71,7 +71,7 @@ export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion)
|
|
|
71
71
|
if (!isLegacy) return 'preserved-override';
|
|
72
72
|
|
|
73
73
|
try {
|
|
74
|
-
|
|
74
|
+
fx.remove(templatesDest);
|
|
75
75
|
return 'removed-legacy';
|
|
76
76
|
} catch {
|
|
77
77
|
// Couldn't remove — don't risk a half-broken state; leave it and let
|
package/src/lib/risk-detector.js
CHANGED
|
@@ -9,10 +9,18 @@
|
|
|
9
9
|
* - designSystemDivergence: boolean — conflicting design sources detected
|
|
10
10
|
* - standardsViolations: string[] — violations of mandate.md proibições
|
|
11
11
|
* - verificationStatus: string — último `morph-spec verify` ('fail' pausa, qualquer gate)
|
|
12
|
+
* - e2eStatus + e2eReason + gate — nó e2e do último verify. Um `skip`
|
|
13
|
+
* cuja razão NÃO seja dispensa legítima pausa SÓ no ship (gate 'review')
|
|
14
|
+
* - verificationScope: string — escopo do stamp que carimbou esse
|
|
15
|
+
* `skip`. Dispensa legítima só conta vinda de 'feature' (ver abaixo)
|
|
12
16
|
* - verificationStale + gate: bool/str — stamp com sha != HEAD pausa SÓ no
|
|
13
17
|
* ship (gate 'review') — código mudou depois do último verify
|
|
14
18
|
* - evalScoreSource + gate: str — no ship, score que NÃO veio do avaliador
|
|
15
19
|
* independente pausa (auto-avaliação não decide o Gate 3)
|
|
20
|
+
* - mutationProof + gate: object — { section, min, productionCode } lido do
|
|
21
|
+
* evaluator-report.md. No ship, relatório SEM a seção `## Mutações` numa
|
|
22
|
+
* feature com código de produção pausa: score é opinião, mutação é prova
|
|
23
|
+
* - mediaClaims + gate: object — { section, score, featureGeneratesMedia }
|
|
16
24
|
* - evalScore: number — morph-eval score (pause if < 9)
|
|
17
25
|
* - taskCount: number — total tasks (pause if > 20)
|
|
18
26
|
*/
|
|
@@ -34,6 +42,95 @@ const TRUSTED_EVAL_SOURCE = 'evaluator-report';
|
|
|
34
42
|
// the work-type config object.
|
|
35
43
|
const FORCED_PAUSE_WORK_TYPES = ['hotfix'];
|
|
36
44
|
|
|
45
|
+
// As razões literais que `framework/hooks/shared/e2e-check.js` emite quando o
|
|
46
|
+
// nó e2e pula. Casadas por PADRÃO e não por igualdade porque duas delas
|
|
47
|
+
// carregam dado variável (o nome do grupo da task). Ordem irrelevante: os
|
|
48
|
+
// padrões são disjuntos.
|
|
49
|
+
const E2E_SKIP_PATTERNS = [
|
|
50
|
+
// "docker is not available in this environment — e2e could not run"
|
|
51
|
+
// (e2e-check.js). A app não foi exercida porque a MÁQUINA não podia.
|
|
52
|
+
[/docker is not available/i, 'environment'],
|
|
53
|
+
// `--skip-e2e` — o operador pediu para pular.
|
|
54
|
+
[/^requested$/i, 'requested'],
|
|
55
|
+
// "project does not containerize (no compose file)" — não há app para subir.
|
|
56
|
+
[/does not containerize/i, 'project'],
|
|
57
|
+
// 'task group "docs" does not touch runtime' — escopo da task, não da app.
|
|
58
|
+
[/does not touch runtime/i, 'scope'],
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// Classes de skip que NÃO são dispensa: o e2e não rodou e nada no repositório
|
|
62
|
+
// diz que ele não precisava rodar. `unknown` está aqui de propósito — inclui a
|
|
63
|
+
// razão vazia/nula, e uma dispensa sem motivo é o mesmo no-op silencioso que
|
|
64
|
+
// esta regra existe para eliminar.
|
|
65
|
+
const E2E_SKIP_CLASSES_THAT_PAUSE = ['environment', 'requested', 'unknown'];
|
|
66
|
+
|
|
67
|
+
// O ÚNICO escopo de stamp que responde pela feature inteira. `morph-spec verify
|
|
68
|
+
// {f} {task}` carimba `scope: 'task:{id}'`, e o nó e2e de um verify por task
|
|
69
|
+
// pula com a razão do GRUPO daquela task ('task group "docs" does not touch
|
|
70
|
+
// runtime') — uma afirmação sobre UMA task, nunca sobre a app. Sem este teste,
|
|
71
|
+
// a última task de uma feature (tipicamente `group: docs`) compra o Gate 3 da
|
|
72
|
+
// feature que ela mesma acabou de escrever, numa máquina onde a app nunca
|
|
73
|
+
// subiu. Ausente/desconhecido cai do mesmo lado seguro que `unknown`: recusar
|
|
74
|
+
// é barato, aprovar sem prova não é.
|
|
75
|
+
const FEATURE_SCOPE = 'feature';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Classifica a razão literal de um `skip` do nó e2e — de QUEM é a ausência de
|
|
79
|
+
* prova. Existe porque `verificationStatus` é o rollup do `verify` e um nó que
|
|
80
|
+
* pula NÃO o reprova: sem classificar a razão, "o e2e rodou e passou" e "o e2e
|
|
81
|
+
* nunca rodou" chegam ao gate como o mesmo `pass`.
|
|
82
|
+
*
|
|
83
|
+
* A assimetria é o ponto: `project`/`scope` descrevem o REPOSITÓRIO dizendo que
|
|
84
|
+
* não há runtime a exercer (dispensa legítima, registrada com motivo);
|
|
85
|
+
* `environment`/`requested` descrevem a MÁQUINA ou o operador, e nenhum dos
|
|
86
|
+
* dois é evidência sobre o código. `unknown` cai no lado seguro pelo mesmo
|
|
87
|
+
* motivo: recusar é barato, aprovar sem prova não é.
|
|
88
|
+
*
|
|
89
|
+
* @param {string|null|undefined} reason - `verification.e2eReason` (a razão
|
|
90
|
+
* literal que o `morph-spec verify` carimbou). Vazia/ausente ⇒ 'unknown' —
|
|
91
|
+
* DESCONHECIDO, jamais lido como dispensa.
|
|
92
|
+
* @returns {'environment'|'requested'|'project'|'scope'|'unknown'}
|
|
93
|
+
*/
|
|
94
|
+
export function classifyE2eSkip(reason) {
|
|
95
|
+
const text = typeof reason === 'string' ? reason.trim() : '';
|
|
96
|
+
if (text === '') return 'unknown';
|
|
97
|
+
for (const [pattern, klass] of E2E_SKIP_PATTERNS) {
|
|
98
|
+
if (pattern.test(text)) return klass;
|
|
99
|
+
}
|
|
100
|
+
return 'unknown';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Mensagem de pausa por classe — nomeia SEMPRE a razão literal do verify. */
|
|
104
|
+
function e2ePauseReason(klass, reason) {
|
|
105
|
+
const literal = typeof reason === 'string' && reason.trim() !== '' ? reason.trim() : null;
|
|
106
|
+
if (klass === 'environment') {
|
|
107
|
+
return `e2e não verificado (ambiente): ${literal} — a app não foi exercida;`
|
|
108
|
+
+ ' rode o e2e onde ele possa rodar, ou registre a dispensa com `morph-spec gate-decision`';
|
|
109
|
+
}
|
|
110
|
+
if (klass === 'requested') {
|
|
111
|
+
return 'e2e pulado a pedido (--skip-e2e) — pular a pedido não é dispensa;'
|
|
112
|
+
+ ' rode `morph-spec verify {feature}` sem a flag antes do ship';
|
|
113
|
+
}
|
|
114
|
+
if (literal === null) {
|
|
115
|
+
return 'e2e pulado SEM motivo registrado — uma dispensa sem motivo não é dispensa';
|
|
116
|
+
}
|
|
117
|
+
return `e2e pulado por razão não classificada: "${literal}" — sem classificação, não há dispensa`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Mensagem de recusa por ESCOPO do stamp: a razão do skip É uma dispensa
|
|
122
|
+
* legítima (`project`/`scope`), mas quem a carimbou foi um verify que não
|
|
123
|
+
* responde pela feature. Nomeia o escopo real para que o leitor saiba qual
|
|
124
|
+
* comando rodar, em vez de rodar o mesmo de novo.
|
|
125
|
+
*/
|
|
126
|
+
function e2eScopeRefusal(klass, scope, reason) {
|
|
127
|
+
const literal = typeof reason === 'string' && reason.trim() !== '' ? reason.trim() : 'sem motivo registrado';
|
|
128
|
+
const where = typeof scope === 'string' && scope.trim() !== '' ? scope.trim() : 'ausente';
|
|
129
|
+
return `dispensa de e2e [${klass}] veio de um verify de escopo "${where}", não da feature:`
|
|
130
|
+
+ ` "${literal}" — um stamp de task responde por UMA task, não pela app inteira;`
|
|
131
|
+
+ ' rode `morph-spec verify {feature}` (sem task) antes do ship';
|
|
132
|
+
}
|
|
133
|
+
|
|
37
134
|
/**
|
|
38
135
|
* @param {object} config - .morph/config/config.json contents (uses `trust`)
|
|
39
136
|
* @param {object} signals
|
|
@@ -75,6 +172,43 @@ export function shouldPauseGate(config, signals) {
|
|
|
75
172
|
};
|
|
76
173
|
}
|
|
77
174
|
|
|
175
|
+
// No gate de SHIP apenas: o nó e2e do último verify PULOU. Um skip não
|
|
176
|
+
// reprova o rollup, então `verificationStatus` sai 'pass' e o gate ficava
|
|
177
|
+
// cego — auto-passando um Gate 3 sobre uma app que ninguém subiu. A razão
|
|
178
|
+
// literal decide: dispensa legítima (o projeto não containeriza; o grupo da
|
|
179
|
+
// task não toca runtime) passa e fica REGISTRADA nos signals; ausência de
|
|
180
|
+
// prova (docker indisponível, `--skip-e2e`, razão nenhuma) pausa. É a prosa
|
|
181
|
+
// do morph-review virando regra — "docker indisponível não é dispensa" — e
|
|
182
|
+
// é o que dispensa o `gate-decision --pause false` manual que ela mandava
|
|
183
|
+
// rodar. Só no review: pausar o Gate 1/2 numa máquina sem docker travaria
|
|
184
|
+
// trabalho legítimo de planejamento.
|
|
185
|
+
if (signals.gate === 'review' && signals.e2eStatus === 'skip') {
|
|
186
|
+
const klass = classifyE2eSkip(signals.e2eReason);
|
|
187
|
+
if (E2E_SKIP_CLASSES_THAT_PAUSE.includes(klass)) {
|
|
188
|
+
return { pause: true, reason: e2ePauseReason(klass, signals.e2eReason) };
|
|
189
|
+
}
|
|
190
|
+
// Segunda pergunta, que a razão sozinha não responde: QUEM carimbou. O
|
|
191
|
+
// `verification` é um stamp SINGULAR sobrescrito por qualquer verify, de
|
|
192
|
+
// qualquer escopo — então um stamp de escopo `task:{id}` no momento do
|
|
193
|
+
// ship significa literalmente que o último verify NÃO foi o da feature. A
|
|
194
|
+
// razão que ele carrega descreve outra corrida: 'task group "docs" does
|
|
195
|
+
// not touch runtime' é dispensa legítima do que AQUELA task verificou, e a
|
|
196
|
+
// última task de uma feature tipicamente é `group: docs` — ela compraria o
|
|
197
|
+
// Gate 3 da feature que acabou de escrever, numa máquina onde a app nunca
|
|
198
|
+
// subiu. Vale para as duas classes de dispensa, e não só para `scope`:
|
|
199
|
+
// mesmo um "project does not containerize" carimbado por task é a resposta
|
|
200
|
+
// a uma pergunta que o gate não fez. O caminho de volta é barato e nomeado
|
|
201
|
+
// (rodar `verify {feature}`), e é o que morph-review já manda fazer antes
|
|
202
|
+
// do Gate 3. Narrow, nunca widen: nenhuma razão nova passa a valer aqui —
|
|
203
|
+
// uma que já valia deixa de valer fora do escopo que responde pela app.
|
|
204
|
+
if (signals.verificationScope !== FEATURE_SCOPE) {
|
|
205
|
+
return {
|
|
206
|
+
pause: true,
|
|
207
|
+
reason: e2eScopeRefusal(klass, signals.verificationScope, signals.e2eReason),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
78
212
|
// Furo 2: no gate de SHIP (review) apenas, um stamp de verification cujo
|
|
79
213
|
// sha diverge do HEAD atual significa que código mudou depois do último
|
|
80
214
|
// `morph-spec verify` — o resultado carimbado não cobre mais o que está
|
|
@@ -111,6 +245,103 @@ export function shouldPauseGate(config, signals) {
|
|
|
111
245
|
};
|
|
112
246
|
}
|
|
113
247
|
|
|
248
|
+
// No gate de SHIP: o relatório do avaliador chegou SEM a seção `## Mutações`.
|
|
249
|
+
// O score diz o que o avaliador ACHOU; a seção diz o que ele PROVOU — e um
|
|
250
|
+
// teste verde só prova que o código roda, nunca que o teste morde. Medido:
|
|
251
|
+
// 11 testes verdes sobre um artefato que o build recusava, porque nenhum
|
|
252
|
+
// deles CARREGAVA o arquivo; e um caminho de escrita reduzido a `return 0;`
|
|
253
|
+
// que passou 23 de 23, todos de recusa.
|
|
254
|
+
//
|
|
255
|
+
// DEPOIS da cláusula de procedência e ANTES da de score, e a ordem é
|
|
256
|
+
// POLÍTICA, não acaso: quando faltam avaliador E seção, a razão reportada tem
|
|
257
|
+
// de ser a mais fundamental ("sem avaliador independente"), porque exigir a
|
|
258
|
+
// prova de um relatório que nem devia decidir o gate manda o humano consertar
|
|
259
|
+
// a coisa errada.
|
|
260
|
+
//
|
|
261
|
+
// FAIL-OPEN POR AUSÊNCIA, em duas direções: `productionCode === true` é
|
|
262
|
+
// exigido EXPLICITAMENTE, então uma feature legada (sinal ausente, `undefined`)
|
|
263
|
+
// não pausa, e uma feature 100% `docs` — que não tem o que mutar — também
|
|
264
|
+
// não. Quem decide as duas coisas é gate-check.js, lendo o disco; aqui só se
|
|
265
|
+
// aplica a política sobre fatos já reunidos.
|
|
266
|
+
if (signals.gate === 'review'
|
|
267
|
+
&& signals.mutationProof?.productionCode === true
|
|
268
|
+
&& signals.mutationProof?.section !== true) {
|
|
269
|
+
return {
|
|
270
|
+
pause: true,
|
|
271
|
+
reason: 'relatório do avaliador sem a seção `## Mutações` — prova de mutação não registrada'
|
|
272
|
+
+ ' (rubrica evals/proof-rubrics/mutation-proof.md): um teste verde prova que o código'
|
|
273
|
+
+ ' roda, não que o teste morde',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// No gate de SHIP apenas: a feature mexeu num caminho que o PROJETO declarou
|
|
278
|
+
// como prompt ou modelo, e não há eval verde carimbado. Prompt e modelo são a
|
|
279
|
+
// única parte do produto cujo comportamento não é lido por build nem por teste
|
|
280
|
+
// unitário: os dois continuam verdes com o prompt trocado.
|
|
281
|
+
//
|
|
282
|
+
// DEPOIS da cláusula de procedência do score, de propósito: quando as duas
|
|
283
|
+
// valem, "Gate 3 sem score do avaliador independente" continua sendo a razão
|
|
284
|
+
// reportada — é o defeito mais grave dos dois.
|
|
285
|
+
//
|
|
286
|
+
// PAUSA, jamais bloqueia: `approve review` não ganha precondição nova. Uma
|
|
287
|
+
// segunda precondição dura ali tornaria inaprovável, inclusive por decisão
|
|
288
|
+
// humana, toda feature legada. Pausa é a severidade certa — o humano lê a
|
|
289
|
+
// razão e decide.
|
|
290
|
+
if (signals.gate === 'review' && (signals.promptModel?.touched?.length ?? 0) > 0) {
|
|
291
|
+
const touched = signals.promptModel.touched;
|
|
292
|
+
const where = `${touched.slice(0, 3).join(', ')}${touched.length > 3 ? ` (+${touched.length - 3})` : ''}`;
|
|
293
|
+
|
|
294
|
+
// `null` é "o projeto não declara nó de eval"; 'skip' é "declara, e esta
|
|
295
|
+
// corrida não o executou". Razões distintas porque as ações são distintas.
|
|
296
|
+
if (signals.evalsStatus === null || signals.evalsStatus === undefined) {
|
|
297
|
+
return {
|
|
298
|
+
pause: true,
|
|
299
|
+
reason: `mudou prompt/modelo (${where}) e o projeto não declara nó de eval`
|
|
300
|
+
+ ' — nenhum build ou teste unitário fica vermelho por um prompt trocado;'
|
|
301
|
+
+ ' declare um `runner: "evals"` em project.tests[] ou registre a dispensa com `morph-spec gate-decision`',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (signals.evalsStatus === 'skip') {
|
|
305
|
+
return {
|
|
306
|
+
pause: true,
|
|
307
|
+
reason: `mudou prompt/modelo (${where}) e o eval não foi executado neste verify`
|
|
308
|
+
+ ' — rode `morph-spec verify {feature}` (escopo de feature) antes do ship',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// 'fail' cai fora: um eval vermelho já avermelha o rollup e a regra de
|
|
312
|
+
// `verificationStatus` acima pausou antes daqui. 'pass' com stamp velho
|
|
313
|
+
// também já pausou, por `verificationStale`.
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// No gate de SHIP: uma feature que GERA MÍDIA cujo relatório chegou sem a
|
|
317
|
+
// seção `## Mídia gerada`. As quatro afirmações que essa seção carrega —
|
|
318
|
+
// custo, revisão humana, marcação e hot path não bloqueado — são fáceis de
|
|
319
|
+
// escrever e difíceis de provar, e a mais cara delas é a que ninguém percebe
|
|
320
|
+
// falhando: mídia gerada dentro de um turno de conversa, com latência
|
|
321
|
+
// documentada de até 2 min, custo por chamada e ZERO revisão possível.
|
|
322
|
+
//
|
|
323
|
+
// DEPOIS da cláusula de procedência e da de mutação, e a ordem é POLÍTICA,
|
|
324
|
+
// como lá: quando faltam avaliador, seção de mutação E seção de mídia, a razão
|
|
325
|
+
// reportada tem de ser a mais fundamental, porque mandar o humano consertar a
|
|
326
|
+
// terceira enquanto a primeira está aberta manda consertar a coisa errada.
|
|
327
|
+
//
|
|
328
|
+
// FAIL-OPEN POR AUSÊNCIA, igual: `featureGeneratesMedia === true` é exigido
|
|
329
|
+
// EXPLICITAMENTE, então uma feature legada (sinal ausente, `undefined`) não
|
|
330
|
+
// pausa, e a maioria das features — que não gera mídia — fica isenta
|
|
331
|
+
// automaticamente, sem ninguém precisar lembrar de aplicar a exceção. Quem
|
|
332
|
+
// decide isso é gate-check.js, lendo `standards[]` das tasks; aqui só se
|
|
333
|
+
// aplica a política sobre fatos já reunidos.
|
|
334
|
+
if (signals.gate === 'review'
|
|
335
|
+
&& signals.mediaClaims?.featureGeneratesMedia === true
|
|
336
|
+
&& signals.mediaClaims.section !== true) {
|
|
337
|
+
return {
|
|
338
|
+
pause: true,
|
|
339
|
+
reason: 'relatório do avaliador sem a seção `## Mídia gerada` — as quatro afirmações de mídia'
|
|
340
|
+
+ ' (custo, revisão humana, marcação, hot path) não foram provadas'
|
|
341
|
+
+ ' (rubrica evals/proof-rubrics/media-claims.md)',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
114
345
|
if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
|
|
115
346
|
return {
|
|
116
347
|
pause: true,
|
|
@@ -140,18 +371,70 @@ export function getRiskSignals(signals) {
|
|
|
140
371
|
out.push(`standards violations: ${signals.standardsViolations.join(', ')}`);
|
|
141
372
|
}
|
|
142
373
|
if (signals.verificationStatus === 'fail') out.push('verify vermelho');
|
|
374
|
+
// Listado mesmo quando NÃO pausa: uma dispensa que passa silenciosamente é
|
|
375
|
+
// indistinguível de uma omissão. A classe diz de quem é a ausência de prova.
|
|
376
|
+
if (signals.gate === 'review' && signals.e2eStatus === 'skip') {
|
|
377
|
+
const klass = classifyE2eSkip(signals.e2eReason);
|
|
378
|
+
const literal = typeof signals.e2eReason === 'string' && signals.e2eReason.trim() !== ''
|
|
379
|
+
? signals.e2eReason.trim()
|
|
380
|
+
: 'sem motivo registrado';
|
|
381
|
+
const where = typeof signals.verificationScope === 'string' && signals.verificationScope.trim() !== ''
|
|
382
|
+
? signals.verificationScope.trim()
|
|
383
|
+
: 'escopo ausente';
|
|
384
|
+
out.push(`e2e pulado [${klass}] (stamp de ${where}): ${literal}`);
|
|
385
|
+
}
|
|
143
386
|
if (signals.gate === 'review' && signals.verificationStale === true) {
|
|
144
387
|
out.push('verification stamp desatualizado (stale) no ship');
|
|
145
388
|
}
|
|
146
389
|
if (signals.gate === 'review' && signals.evalScoreSource !== TRUSTED_EVAL_SOURCE) {
|
|
147
390
|
out.push(`score do Gate 3 sem avaliador independente (fonte: ${signals.evalScoreSource ?? 'nenhuma'})`);
|
|
148
391
|
}
|
|
392
|
+
// Listado mesmo quando NÃO pausa (feature só de docs, ou prova presente com
|
|
393
|
+
// score baixo): a prova é o insumo do teto de `test-coverage.md`, e um teto
|
|
394
|
+
// que ninguém enxerga não é aplicado por ninguém.
|
|
395
|
+
if (signals.gate === 'review' && signals.mutationProof) {
|
|
396
|
+
const { section, min, productionCode } = signals.mutationProof;
|
|
397
|
+
if (section !== true) {
|
|
398
|
+
out.push(productionCode === true
|
|
399
|
+
? 'relatório sem a seção `## Mutações` (prova de mutação ausente)'
|
|
400
|
+
: 'relatório sem a seção `## Mutações` — feature sem código de produção, não pausa');
|
|
401
|
+
} else if (typeof min === 'number') {
|
|
402
|
+
out.push(`prova de mutação registrada (menor Proof-Score: ${min})`);
|
|
403
|
+
} else {
|
|
404
|
+
out.push('prova de mutação registrada (sem `**Proof-Scores:**` — nenhum teto a aplicar)');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Listado mesmo quando NÃO pausa, pela mesma razão do e2e: um projeto que não
|
|
408
|
+
// declara caminho de prompt/modelo é indistinguível, no relatório, de um que
|
|
409
|
+
// declara e não tocou em nada — e as duas coisas significam coisas diferentes.
|
|
410
|
+
if (signals.gate === 'review' && signals.promptModel) {
|
|
411
|
+
const { declared, touched } = signals.promptModel;
|
|
412
|
+
if (!declared) {
|
|
413
|
+
out.push('projeto não declara caminhos de prompt/modelo (project.evals.watch)');
|
|
414
|
+
} else if ((touched?.length ?? 0) > 0) {
|
|
415
|
+
out.push(`prompt/modelo tocado (${touched.length}) com eval ${signals.evalsStatus ?? 'não declarado'}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
149
418
|
if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
|
|
150
419
|
out.push(`eval score ${signals.evalScore}/10`);
|
|
151
420
|
}
|
|
152
421
|
if (typeof signals.taskCount === 'number' && signals.taskCount > SCOPE_TASK_THRESHOLD) {
|
|
153
422
|
out.push(`${signals.taskCount} tasks (over threshold)`);
|
|
154
423
|
}
|
|
424
|
+
// Listado mesmo quando NÃO pausa, pela mesma razão do sinal irmão: a isenção
|
|
425
|
+
// de uma feature que não gera mídia tem de ser VISÍVEL, senão é
|
|
426
|
+
// indistinguível de uma omissão.
|
|
427
|
+
if (signals.gate === 'review' && signals.mediaClaims?.featureGeneratesMedia === true) {
|
|
428
|
+
const { section, score } = signals.mediaClaims;
|
|
429
|
+
if (section !== true) {
|
|
430
|
+
out.push('relatório sem a seção `## Mídia gerada` (prova de mídia ausente)');
|
|
431
|
+
} else if (typeof score === 'number') {
|
|
432
|
+
out.push(`prova de mídia registrada (media=${score}; teto em arquitetura)`);
|
|
433
|
+
} else {
|
|
434
|
+
out.push('prova de mídia registrada (sem `media=` nos Proof-Scores — nenhum teto a aplicar)');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
155
438
|
return out;
|
|
156
439
|
}
|
|
157
440
|
|
|
@@ -160,4 +443,5 @@ export const RISK_CONSTANTS = {
|
|
|
160
443
|
MIN_EVAL_SCORE,
|
|
161
444
|
FORCED_PAUSE_WORK_TYPES,
|
|
162
445
|
TRUSTED_EVAL_SOURCE,
|
|
446
|
+
E2E_SKIP_CLASSES_THAT_PAUSE,
|
|
163
447
|
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/standards/verified-against.js
|
|
3
|
+
*
|
|
4
|
+
* THE parser for a standard's verification header and for its age against the
|
|
5
|
+
* AI package pin. Single source of truth, on purpose.
|
|
6
|
+
*
|
|
7
|
+
* A standard declares what it was checked against, and when:
|
|
8
|
+
*
|
|
9
|
+
* **Verified against:** Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08);
|
|
10
|
+
* provado por `templates/dotnet/ai-kit/.../AgentFactory.cs`. Last-verified: 2026-09-08.
|
|
11
|
+
*
|
|
12
|
+
* Three places need to read that same fact — `scripts/check-freshness.js`
|
|
13
|
+
* (the freshness gate), `test/framework/standard-header.test.js` (the header
|
|
14
|
+
* contract) and the "ai-agents vs ai-pin" report. Three copies of the regex
|
|
15
|
+
* would drift apart, which is exactly the failure mode CLAUDE.md's Dependency
|
|
16
|
+
* Matrix exists to catalogue. So the regex lives here, once.
|
|
17
|
+
*
|
|
18
|
+
* The age is measured against `framework/ai-pin.json`'s `pinnedAt`, never
|
|
19
|
+
* against "today": `pinnedAt` is the date a human checked the packages, so the
|
|
20
|
+
* obligation is tied to a human act. Measuring against today would make a
|
|
21
|
+
* parked repository change colour on its own.
|
|
22
|
+
*
|
|
23
|
+
* NOTE on the 90-day window: this module EXPOSES the age and the threshold; it
|
|
24
|
+
* enforces nothing — enforcement lives in its consumers. Issue #93 re-verified
|
|
25
|
+
* the `ai-agents` standards and turned the window into a HARD FAIL in
|
|
26
|
+
* scripts/check-freshness.js, so the number below is no longer only a warning.
|
|
27
|
+
* The name kept the `WARN` in it because renaming an export is a change to the
|
|
28
|
+
* module's surface, not to its documentation; scripts/check-freshness.js aliases
|
|
29
|
+
* it to `AI_PIN_STALE_DAYS` rather than re-declaring `90` — see that file's
|
|
30
|
+
* docblock and CONTRIBUTING.md's "two rulers" table.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readFileSync, existsSync } from 'fs';
|
|
34
|
+
import { join, dirname } from 'path';
|
|
35
|
+
import { fileURLToPath } from 'url';
|
|
36
|
+
|
|
37
|
+
/** Repository root, resolved from this file: src/lib/standards → ../../.. */
|
|
38
|
+
const DEFAULT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
39
|
+
|
|
40
|
+
/** Path of the pin relative to a root. */
|
|
41
|
+
const AI_PIN_RELATIVE = join('framework', 'ai-pin.json');
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The verification header, on ONE line.
|
|
45
|
+
*
|
|
46
|
+
* Group 1 is the subject (what the standard was checked against), group 2 the
|
|
47
|
+
* ISO date. `[^\n]*?` keeps the match inside a single line: a `Last-verified:`
|
|
48
|
+
* two paragraphs below belongs to another claim, and stitching the two would
|
|
49
|
+
* date a header that never carried a date.
|
|
50
|
+
*
|
|
51
|
+
* @type {RegExp}
|
|
52
|
+
*/
|
|
53
|
+
export const LAST_VERIFIED_RE = /\*\*Verified against:\*\*([^\n]*?)Last-verified:\s*(\d{4}-\d{2}-\d{2})/;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* An unresolved `// VERIFY` marker: a code detail the author could not confirm
|
|
57
|
+
* against the installed package.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately broader than the regex it replaces (`/^\s*\/\/\s*VERIFY:/`),
|
|
60
|
+
* which demanded line-start AND a colon. Every real marker in
|
|
61
|
+
* `framework/standards/ai-agents/` is an end-of-line comment without a colon —
|
|
62
|
+
*
|
|
63
|
+
* using OpenAI.Batch; // VERIFY namespace — confirme contra o pacote OpenAI 2.*
|
|
64
|
+
*
|
|
65
|
+
* — so the old counter reported 0 while two standards openly declared
|
|
66
|
+
* unconfirmed C# API. `\b` keeps `// VERIFYING` out; the fence requirement is
|
|
67
|
+
* NOT in the regex (a regex cannot see a fence) — it lives in
|
|
68
|
+
* {@link countVerifyMarkers}, so prose quoting `// VERIFY:` never counts.
|
|
69
|
+
*
|
|
70
|
+
* @type {RegExp}
|
|
71
|
+
*/
|
|
72
|
+
export const VERIFY_MARKER_RE = /(^|\s)\/\/\s*VERIFY\b/;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Age, in days against `pinnedAt`, above which an `ai-agents/**` standard is
|
|
76
|
+
* due for re-verification.
|
|
77
|
+
*
|
|
78
|
+
* READ UNDER TWO NAMES, ONE VALUE. `scripts/check-freshness.js` imports it as
|
|
79
|
+
* `AI_PIN_STALE_DAYS` and **hard-fails** the run above it (issue #93), for
|
|
80
|
+
* standards under `framework/standards/ai-agents/` measured against `pinnedAt`.
|
|
81
|
+
* The alias exists so the number is declared once: a second literal `90` is the
|
|
82
|
+
* same drift that once blinded the `// VERIFY` counter.
|
|
83
|
+
*
|
|
84
|
+
* The `WARN` in the name is now historical — it shipped in issue #92, when the
|
|
85
|
+
* window was a measurement. Kept because renaming an export changes the module's
|
|
86
|
+
* surface; this comment changes only what the reader is told.
|
|
87
|
+
*
|
|
88
|
+
* @type {number}
|
|
89
|
+
*/
|
|
90
|
+
export const AI_AGENTS_WARN_DAYS = 90;
|
|
91
|
+
|
|
92
|
+
/** ISO calendar date, the only date shape this module accepts. */
|
|
93
|
+
const ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse a standard's verification header.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} text - Full markdown text of the standard.
|
|
99
|
+
* @returns {{ verifiedAgainst: string, lastVerified: string }|null}
|
|
100
|
+
* `null` when the standard carries no parseable header — the caller decides
|
|
101
|
+
* whether that is a warning (the backfill backlog) or a failure
|
|
102
|
+
* (`ai-agents/**`, where a new standard must be born dated).
|
|
103
|
+
*/
|
|
104
|
+
export function parseVerifiedAgainst(text) {
|
|
105
|
+
if (typeof text !== 'string' || text.length === 0) return null;
|
|
106
|
+
const m = LAST_VERIFIED_RE.exec(text);
|
|
107
|
+
if (!m) return null;
|
|
108
|
+
return { verifiedAgainst: m[1].trim(), lastVerified: m[2] };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The `provado por` clause, and the path token that may follow it.
|
|
113
|
+
*
|
|
114
|
+
* THREE TYPESETTINGS EXIST IN THE ACERVO, and all three are real claims.
|
|
115
|
+
* Counted by re-deriving every one of the 78 standards on disk and summing the
|
|
116
|
+
* resulting paths — 27 paths across 16 entries:
|
|
117
|
+
*
|
|
118
|
+
* A ; provado por `templates/…/AgentFactory.cs` e `.../ConversationStore.cs`
|
|
119
|
+
* B ; `provado por templates/…/AgentFactory.cs` para a **ordem do pipeline**
|
|
120
|
+
* C . O timeout é **provado por** `.../AgentSpecOptions.cs` e `.../AgentFactory.cs`
|
|
121
|
+
*
|
|
122
|
+
* B opens the code span BEFORE the clause; C wraps the clause in bold and can
|
|
123
|
+
* appear MID-SENTENCE, after an earlier clause already closed. Each one was
|
|
124
|
+
* found by measuring, and each was missing from the rule that preceded it:
|
|
125
|
+
*
|
|
126
|
+
* - ignoring B dropped 4 of 16 claims (`middleware-patterns`,
|
|
127
|
+
* `observability-patterns`, `rag-custom-pgvector`, `structured-output`);
|
|
128
|
+
* - ignoring C dropped 2 paths from `llm-runtime-defaults` — 25 instead of 27.
|
|
129
|
+
*
|
|
130
|
+
* READ THE SHAPE OF THAT SECOND MISS, because it is why counting entries is not
|
|
131
|
+
* enough: `llm-runtime-defaults` had two OTHER paths from a clause of form A, so
|
|
132
|
+
* it stayed in the "16 entries with proof" bucket either way. No counter moved,
|
|
133
|
+
* `--check` stayed green, exit 0. The only ruler that saw it is the SUM of
|
|
134
|
+
* paths. A canary that pins entry counts would have gone green over it — which
|
|
135
|
+
* is why `standards-registry.test.js` pins the paths themselves, written out by
|
|
136
|
+
* hand, and not a total produced by this function.
|
|
137
|
+
*
|
|
138
|
+
* The DENIALS must stay empty, and they are the majority (61 of the 77 headers
|
|
139
|
+
* derive to `[]`; 16 say some variant of "sem cláusula `provado por`" or
|
|
140
|
+
* "NÃO carimba `provado por`"). Three properties keep them out without a
|
|
141
|
+
* blacklist of prose:
|
|
142
|
+
*
|
|
143
|
+
* 1. a path must contain a `/` — so "`provado por`. Os nomes" and
|
|
144
|
+
* "`provado por`: o ai-kit" match nothing;
|
|
145
|
+
* 2. a path must be CLOSED BY A BACKTICK — so prose that merely follows the
|
|
146
|
+
* clause ("para a **ordem do pipeline**") cannot be mistaken for a path;
|
|
147
|
+
* 3. the clause must not be the tail of a LONGER word — `aprovado por` and
|
|
148
|
+
* `comprovado por` are different claims. The corpus already contains
|
|
149
|
+
* "aprovado por lucas@…" inside a code sample, so this is a live shape,
|
|
150
|
+
* not a hypothetical.
|
|
151
|
+
*
|
|
152
|
+
* @type {RegExp}
|
|
153
|
+
*/
|
|
154
|
+
export const PROVED_BY_TRIGGER_RE = /(?:^|[^a-zA-Z])provado por/gi;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* One path token after the clause, plus the separator that may precede it:
|
|
158
|
+
* whitespace, backtick, `,`, `;`, the word `e`, and the markdown emphasis
|
|
159
|
+
* characters `*` and `_`. The emphasis characters are in the class because of
|
|
160
|
+
* typesetting C — without them, `**provado por**` armed the trigger and then
|
|
161
|
+
* matched no path at all, losing both of its files in silence.
|
|
162
|
+
*
|
|
163
|
+
* Anchored: it only ever reads from the cursor, so prose ends the scan instead
|
|
164
|
+
* of being searched through.
|
|
165
|
+
*
|
|
166
|
+
* @type {RegExp}
|
|
167
|
+
*/
|
|
168
|
+
export const PROVED_BY_PATH_RE =
|
|
169
|
+
/^[\s`,;*_]*(?:\be\b[\s`,;*_]*)?([A-Za-z0-9_.][A-Za-z0-9_.\-]*(?:\/[A-Za-z0-9_.\-]*)+)`/;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Derive the list of files a standard claims to be *proved by*.
|
|
173
|
+
*
|
|
174
|
+
* Reads the SAME header {@link parseVerifiedAgainst} reads — deliberately, and
|
|
175
|
+
* from this module, because a second regex over the same sentence in
|
|
176
|
+
* `scripts/generate-standards-registry.js` is precisely the drift the module
|
|
177
|
+
* docblock opens with.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} verifiedAgainst - The subject captured by
|
|
180
|
+
* {@link parseVerifiedAgainst} (group 1), not the whole markdown file.
|
|
181
|
+
* @returns {string[]} Paths in declaration order, de-duplicated. `[]` means
|
|
182
|
+
* "the header was read and declares no proof" — a documental verification —
|
|
183
|
+
* never "not asked". The caller distinguishes the two by whether the header
|
|
184
|
+
* parsed at all.
|
|
185
|
+
*/
|
|
186
|
+
export function deriveProvedBy(verifiedAgainst) {
|
|
187
|
+
if (typeof verifiedAgainst !== 'string' || verifiedAgainst.length === 0) return [];
|
|
188
|
+
const paths = [];
|
|
189
|
+
const trigger = new RegExp(PROVED_BY_TRIGGER_RE.source, PROVED_BY_TRIGGER_RE.flags);
|
|
190
|
+
let clause;
|
|
191
|
+
while ((clause = trigger.exec(verifiedAgainst)) !== null) {
|
|
192
|
+
let rest = verifiedAgainst.slice(clause.index + clause[0].length);
|
|
193
|
+
let hit;
|
|
194
|
+
while ((hit = PROVED_BY_PATH_RE.exec(rest)) !== null) {
|
|
195
|
+
if (!paths.includes(hit[1])) paths.push(hit[1]);
|
|
196
|
+
rest = rest.slice(hit[0].length);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return paths;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Count unresolved `// VERIFY` markers that sit INSIDE a fenced code block.
|
|
204
|
+
*
|
|
205
|
+
* The fence rule is what separates a real marker from prose that merely quotes
|
|
206
|
+
* one (`marcados \`// VERIFY:\` no código`). It lives here, next to the regex,
|
|
207
|
+
* so every consumer counts the same thing.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} text - Full markdown text of the standard.
|
|
210
|
+
* @returns {number}
|
|
211
|
+
*/
|
|
212
|
+
export function countVerifyMarkers(text) {
|
|
213
|
+
if (typeof text !== 'string' || text.length === 0) return 0;
|
|
214
|
+
let inFence = false;
|
|
215
|
+
let count = 0;
|
|
216
|
+
for (const line of text.split('\n')) {
|
|
217
|
+
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
|
|
218
|
+
if (inFence && VERIFY_MARKER_RE.test(line)) count++;
|
|
219
|
+
}
|
|
220
|
+
return count;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read and parse `framework/ai-pin.json`.
|
|
225
|
+
*
|
|
226
|
+
* Throws — never falls back to a default — when the pin is missing or invalid:
|
|
227
|
+
* without a pin there is no ruler, and pretending there is one is the original
|
|
228
|
+
* defect this feature removes. The message names the file so the failure is
|
|
229
|
+
* diagnosable from a CI log alone.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} [root=DEFAULT_ROOT] - Repository root that contains `framework/`.
|
|
232
|
+
* @returns {{ pinnedAt: string, source: string, targetFramework: string, sdk: string,
|
|
233
|
+
* packages: Record<string, {version: string, published: string, role: string}>,
|
|
234
|
+
* toolchain: Record<string, {version: string}> }}
|
|
235
|
+
* @throws {Error} When the file is absent or is not valid JSON.
|
|
236
|
+
*/
|
|
237
|
+
export function readAiPin(root = DEFAULT_ROOT) {
|
|
238
|
+
const pinPath = join(root, AI_PIN_RELATIVE);
|
|
239
|
+
if (!existsSync(pinPath)) {
|
|
240
|
+
throw new Error(`ai-pin.json não encontrado em ${pinPath} — sem pin não há régua de frescor.`);
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
return JSON.parse(readFileSync(pinPath, 'utf8'));
|
|
244
|
+
} catch (err) {
|
|
245
|
+
throw new Error(`ai-pin.json inválido em ${pinPath}: ${err.message}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Parse an ISO calendar date into a UTC epoch, timezone-independent. */
|
|
250
|
+
function isoToUtc(value, label) {
|
|
251
|
+
const m = typeof value === 'string' ? ISO_DATE_RE.exec(value) : null;
|
|
252
|
+
if (!m) {
|
|
253
|
+
throw new TypeError(`${label} deve ser uma data ISO YYYY-MM-DD, recebido ${JSON.stringify(value)}`);
|
|
254
|
+
}
|
|
255
|
+
return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Age of a standard's verification, in whole days, measured against the pin.
|
|
260
|
+
*
|
|
261
|
+
* Both dates are read as UTC calendar days, so the result does not depend on
|
|
262
|
+
* the runner's timezone. The value is NEGATIVE when the standard is newer than
|
|
263
|
+
* the pin — the caller must treat that as a PIN error (a `pinnedAt` in the
|
|
264
|
+
* future), never as a fresh standard: otherwise every standard could be
|
|
265
|
+
* "renewed" by editing one date.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} lastVerified - ISO date from the standard's header.
|
|
268
|
+
* @param {string} pinnedAt - ISO date from `framework/ai-pin.json`.
|
|
269
|
+
* @returns {number} Whole days between the two, possibly negative.
|
|
270
|
+
* @throws {TypeError} When either argument is not an ISO date.
|
|
271
|
+
*/
|
|
272
|
+
export function ageInDaysAgainstPin(lastVerified, pinnedAt) {
|
|
273
|
+
const verified = isoToUtc(lastVerified, 'lastVerified');
|
|
274
|
+
const pinned = isoToUtc(pinnedAt, 'pinnedAt');
|
|
275
|
+
return Math.round((pinned - verified) / 86400000);
|
|
276
|
+
}
|