@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
|
@@ -2,10 +2,12 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { getFeature } from '../../core/state/state-manager.js';
|
|
4
4
|
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
5
|
-
import { shouldPauseGate, getRiskSignals } from '../../lib/risk-detector.js';
|
|
6
|
-
import { evaluatorCompositeScore } from '../../lib/evaluator-report.js';
|
|
5
|
+
import { shouldPauseGate, getRiskSignals, classifyE2eSkip } from '../../lib/risk-detector.js';
|
|
6
|
+
import { evaluatorCompositeScore, evaluatorProofState, minProofScore, mediaProofScore } from '../../lib/evaluator-report.js';
|
|
7
7
|
import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
|
|
8
|
-
import {
|
|
8
|
+
import { minimatch } from 'minimatch';
|
|
9
|
+
import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
|
|
10
|
+
import { readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
9
11
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -27,6 +29,59 @@ import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/featu
|
|
|
27
29
|
*/
|
|
28
30
|
const OVERRIDABLE_SIGNAL_KEYS = ['standardsViolations', 'designSystemDivergence', 'evalScore'];
|
|
29
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Os paths de standard que fazem uma feature ser "de midia".
|
|
34
|
+
*
|
|
35
|
+
* Escritos aqui, uma vez, e lidos do campo `standards[]` das tasks — que o Gate
|
|
36
|
+
* 2 congelou. Nao ha julgamento envolvido: ou a task pinou o standard, ou nao
|
|
37
|
+
* pinou. Um sinal derivado de opiniao seria sobrescrivivel na pratica ainda que
|
|
38
|
+
* nao estivesse em OVERRIDABLE_SIGNAL_KEYS.
|
|
39
|
+
*
|
|
40
|
+
* A forma dos valores e a de `tasks.json` (`standards/...`), nao a do disco
|
|
41
|
+
* (`framework/standards/...`): e o vocabulario que o plano usa.
|
|
42
|
+
*/
|
|
43
|
+
export const MEDIA_STANDARD_PATHS = [
|
|
44
|
+
'standards/ai-agents/modalities-image-gen.md',
|
|
45
|
+
'standards/ai-agents/media-video.md',
|
|
46
|
+
'standards/ai-agents/media-pipeline.md',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Uma feature "gera midia" quando ALGUMA task pinou um dos tres standards de
|
|
51
|
+
* midia no seu array `standards[]`.
|
|
52
|
+
*
|
|
53
|
+
* Isto e uma FUNCAO EXPORTADA, e nao uma expressao dentro de `collectSignals`,
|
|
54
|
+
* porque um predicado que decide se um guardrail arma tem de ser testavel
|
|
55
|
+
* sozinho — o irmao `productionCode` tem quatro casos, e este nao tinha nenhum.
|
|
56
|
+
* Um predicado que sempre devolve `false` desarma a clausula em silencio, e
|
|
57
|
+
* silencio e o defeito que esta epic catalogou sete vezes.
|
|
58
|
+
*
|
|
59
|
+
* **O prefixo `framework/` e normalizado, e essa e a razao de existir da
|
|
60
|
+
* funcao.** O vocabulario do plano e `standards/...` (o exemplo do
|
|
61
|
+
* `tasks.schema.json`, e a unica forma medida hoje nos `standards[]` desta
|
|
62
|
+
* arvore), mas o disco e `framework/standards/...` — e a mesma prosa das skills
|
|
63
|
+
* cita as duas formas. Um plano que escrevesse a forma do disco desarmaria o
|
|
64
|
+
* guardrail sem erro nenhum: `standards --find`, `validate` e o proprio Gate 3
|
|
65
|
+
* continuariam verdes. Aceitar as duas grafias custa uma linha; recusar uma
|
|
66
|
+
* delas em silencio custa o gate.
|
|
67
|
+
*
|
|
68
|
+
* Fail-open por AUSENCIA (nao por opiniao): lista vazia, `standards` ausente ou
|
|
69
|
+
* de tipo errado ⇒ `false` ⇒ nao pausa. Sem plano legivel nao ha como AFIRMAR
|
|
70
|
+
* que a feature gera midia, e uma regra nova nao trava feature legada.
|
|
71
|
+
*
|
|
72
|
+
* @param {Array<object>} tasks - `tasks.json` → `tasks[]` (qualquer coisa nao-array vira []).
|
|
73
|
+
* @returns {boolean} true se ao menos uma task pinou um standard de midia.
|
|
74
|
+
*/
|
|
75
|
+
export function featureGeneratesMedia(tasks) {
|
|
76
|
+
if (!Array.isArray(tasks)) return false;
|
|
77
|
+
|
|
78
|
+
return tasks.some((t) =>
|
|
79
|
+
Array.isArray(t?.standards) &&
|
|
80
|
+
t.standards.some((s) =>
|
|
81
|
+
typeof s === 'string' &&
|
|
82
|
+
MEDIA_STANDARD_PATHS.includes(s.replace(/^framework\//, ''))));
|
|
83
|
+
}
|
|
84
|
+
|
|
30
85
|
/** Read tasks.json's task list for a feature; [] if missing/corrupt. Mirrors verify.js's readTasks(). */
|
|
31
86
|
function readTasks(featureName) {
|
|
32
87
|
try {
|
|
@@ -63,6 +118,41 @@ function compositeEvalScore(feature) {
|
|
|
63
118
|
return Math.round(avg * 10) / 10;
|
|
64
119
|
}
|
|
65
120
|
|
|
121
|
+
/**
|
|
122
|
+
* "Esta feature mexeu em prompt ou em modelo?" — em duas metades, porque uma só
|
|
123
|
+
* não distingue as duas situações que pedem ações diferentes.
|
|
124
|
+
*
|
|
125
|
+
* `declared: false` é "o projeto não declara caminho algum de prompt/modelo" —
|
|
126
|
+
* a regra é inerte por construção e a razão registrada diz isso. `declared:
|
|
127
|
+
* true` com `touched: []` é "declara, e nada foi tocado" — a regra rodou e não
|
|
128
|
+
* viu nada. Colapsar os dois num array vazio faria um projeto que nunca
|
|
129
|
+
* configurou `watch[]` parecer um projeto que foi verificado.
|
|
130
|
+
*
|
|
131
|
+
* O predicado NÃO é invenção deste framework: o repositório de referência já
|
|
132
|
+
* roda exatamente ele em produção, disparando publicação por `paths:` sobre os
|
|
133
|
+
* mesmos globs e decidindo o escopo com `git diff --name-only`.
|
|
134
|
+
*
|
|
135
|
+
* COLETA, NUNCA DECIDE. Nada aqui julga se o que foi tocado deveria pausar um
|
|
136
|
+
* gate — política escrita no coletor cria duas fontes para o mesmo fato.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} cwd
|
|
139
|
+
* @param {string|null} defaultBranch
|
|
140
|
+
* @returns {{declared: boolean, watch: string[], touched: string[]}}
|
|
141
|
+
*/
|
|
142
|
+
function collectPromptModel(cwd, defaultBranch) {
|
|
143
|
+
const watch = readMorphConfig(cwd)?.project?.evals?.watch;
|
|
144
|
+
const globs = Array.isArray(watch) ? watch.filter((g) => typeof g === 'string' && g.trim()) : [];
|
|
145
|
+
|
|
146
|
+
if (globs.length === 0) return { declared: false, watch: [], touched: [] };
|
|
147
|
+
|
|
148
|
+
const changed = changedPathsAgainst(defaultBranch, cwd);
|
|
149
|
+
return {
|
|
150
|
+
declared: true,
|
|
151
|
+
watch: globs,
|
|
152
|
+
touched: changed.filter((path) => globs.some((glob) => minimatch(path, glob))),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
66
156
|
/**
|
|
67
157
|
* Collect every signal `shouldPauseGate()` (risk-detector.js) is documented
|
|
68
158
|
* to consume, straight off disk — the deterministic call site risk-detector
|
|
@@ -72,9 +162,15 @@ function compositeEvalScore(feature) {
|
|
|
72
162
|
* política adicionada em risk-detector.js no fecho do R4, qualquer gate).
|
|
73
163
|
* `verificationStale` também alimenta uma regra própria, mas só no ship
|
|
74
164
|
* (`gate === 'review'`) — sha do stamp divergindo do HEAD atual. `gate` em si
|
|
75
|
-
* é o que permite risk-detector.js diferenciar as duas. `
|
|
76
|
-
*
|
|
77
|
-
*
|
|
165
|
+
* é o que permite risk-detector.js diferenciar as duas. `e2eStatus`/`e2eReason`
|
|
166
|
+
* alimentam a regra da dispensa de e2e (também só no ship): um nó e2e que
|
|
167
|
+
* PULOU não reprova o rollup, logo `verificationStatus` sai 'pass' e a razão
|
|
168
|
+
* literal é o único fato que distingue dispensa legítima de ausência de prova.
|
|
169
|
+
* `verificationScope` é o quarto sinal da mesma regra: um `verify {f} {task}`
|
|
170
|
+
* carimba `scope: 'task:{id}'` e o e2e dele pula com a razão do GRUPO daquela
|
|
171
|
+
* task, que risk-detector.js recusa como dispensa da feature.
|
|
172
|
+
* `verification` completo, `e2eSkipClass`, diffstat e sha seguem como contexto
|
|
173
|
+
* de auditoria persistido, sem regra própria.
|
|
78
174
|
*
|
|
79
175
|
* @param {string} featureName
|
|
80
176
|
* @param {object} feature
|
|
@@ -113,6 +209,61 @@ function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
113
209
|
signals.evalScore = trueComposite;
|
|
114
210
|
signals.evalScoreSource = 'evaluator-report';
|
|
115
211
|
}
|
|
212
|
+
|
|
213
|
+
// PROVA DE MUTAÇÃO, derivada do MESMO módulo leitor do relatório. Um score
|
|
214
|
+
// alto diz o que o avaliador ACHOU; a seção `## Mutações` diz se ele
|
|
215
|
+
// PROVOU. Sem ela, um Gate 3 auto-passa sobre uma suíte que ninguém mostrou
|
|
216
|
+
// que morde — o defeito que as rubricas de `evals/proof-rubrics/` existem
|
|
217
|
+
// para nomear.
|
|
218
|
+
//
|
|
219
|
+
// `productionCode` viaja DENTRO do sinal para que risk-detector.js continue
|
|
220
|
+
// puro sobre fatos já reunidos: a divisão é "gate-check COLETA do disco,
|
|
221
|
+
// risk-detector DECIDE". Uma feature 100% `docs` não tem o que mutar, e
|
|
222
|
+
// exigir prova dela seria pedir uma cerimônia vazia — que é como uma regra
|
|
223
|
+
// vira ruído e depois é desligada.
|
|
224
|
+
//
|
|
225
|
+
// Task SEM `group` conta como código de produção: `group` é opcional no
|
|
226
|
+
// schema e o default documentado é `backend`. O conservador aqui é assumir
|
|
227
|
+
// que HÁ código — errar para o lado de pedir prova custa uma pausa; errar
|
|
228
|
+
// para o outro lado devolve o gate cego.
|
|
229
|
+
//
|
|
230
|
+
// `tasks` vazio (plano ausente ou ilegível) ⇒ `productionCode: false` ⇒ NÃO
|
|
231
|
+
// pausa. Fail-open deliberado: sem plano, não há como AFIRMAR que existe
|
|
232
|
+
// código de produção, e uma regra nova não pode travar feature legada.
|
|
233
|
+
//
|
|
234
|
+
// Nada disto entra em OVERRIDABLE_SIGNAL_KEYS: são fatos de disco, como
|
|
235
|
+
// `gate`/`workType`/`e2e*`. Um `--signals '{"mutationProof":{"section":true}}'`
|
|
236
|
+
// compraria o Gate 3 exatamente como um `e2eStatus: "pass"` forjado.
|
|
237
|
+
const proof = evaluatorProofState(cwd, featureName);
|
|
238
|
+
signals.mutationProof = {
|
|
239
|
+
section: proof.mutationSection,
|
|
240
|
+
min: minProofScore(proof.proofScores),
|
|
241
|
+
productionCode: tasks.some((t) => (t?.group ?? 'backend') !== 'docs'),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// PROVA DE MÍDIA, mesmo módulo leitor e mesma disciplina anti-spoof. As
|
|
245
|
+
// quatro afirmações de uma feature de mídia (custo, revisão humana,
|
|
246
|
+
// marcação, hot path não bloqueado) são propriedades ESTRUTURAIS, e por isso
|
|
247
|
+
// `score` limita `architecture` e não `test-coverage` — daí `media` ficar
|
|
248
|
+
// FORA do `min` acima.
|
|
249
|
+
//
|
|
250
|
+
// `featureGeneratesMedia` é determinístico e vem do DISCO, nunca de
|
|
251
|
+
// julgamento: existe ao menos uma task cujo array `standards` cita um dos
|
|
252
|
+
// três paths de standard de mídia. É o mesmo formato do `productionCode`
|
|
253
|
+
// acima e usa um campo que o Gate 2 congelou. Mora numa função exportada
|
|
254
|
+
// (acima) porque um predicado que decide se um guardrail arma precisa de
|
|
255
|
+
// canário próprio — e porque o prefixo `framework/` precisa ser normalizado
|
|
256
|
+
// ali, num lugar só.
|
|
257
|
+
//
|
|
258
|
+
// A isenção é automática e é o comportamento certo: a maioria das features
|
|
259
|
+
// não gera mídia, e exigir a seção delas seria cerimônia vazia — que é como
|
|
260
|
+
// uma regra vira ruído e depois é desligada. `tasks` vazio ⇒ false ⇒ não
|
|
261
|
+
// pausa, o mesmo fail-open por ausência do sinal irmão.
|
|
262
|
+
signals.mediaClaims = {
|
|
263
|
+
section: proof.mediaSection,
|
|
264
|
+
score: mediaProofScore(proof.proofScores),
|
|
265
|
+
featureGeneratesMedia: featureGeneratesMedia(tasks),
|
|
266
|
+
};
|
|
116
267
|
}
|
|
117
268
|
|
|
118
269
|
// verificationStatus alimenta a regra "verify vermelho pausa"; o resto do
|
|
@@ -121,6 +272,39 @@ function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
121
272
|
signals.verification = verification;
|
|
122
273
|
if (verification?.status) signals.verificationStatus = verification.status;
|
|
123
274
|
signals.verificationStale = Boolean(verification?.sha && sha && verification.sha !== sha);
|
|
275
|
+
|
|
276
|
+
// O nó e2e, separado do rollup de propósito: um `skip` NÃO reprova
|
|
277
|
+
// `verification.status`, então sem estas duas chaves o gate lê 'pass' tanto
|
|
278
|
+
// quando o e2e rodou e passou quanto quando ele nunca rodou. `null` aqui é
|
|
279
|
+
// DESCONHECIDO (stamp ausente, ou anterior ao nó e2e), nunca 'skip'.
|
|
280
|
+
// `e2eSkipClass` é DERIVADO pela fonte única (classifyE2eSkip, em
|
|
281
|
+
// risk-detector.js) e persistido só como auditoria — quem decide continua
|
|
282
|
+
// sendo shouldPauseGate, aqui não há política.
|
|
283
|
+
//
|
|
284
|
+
// Nenhuma das três entra em OVERRIDABLE_SIGNAL_KEYS: descrevem O QUE
|
|
285
|
+
// ACONTECEU no disco, como `gate`/`workType`, não uma opinião que o chamador
|
|
286
|
+
// possa afirmar. Um `--signals '{"e2eStatus":"pass"}'` compraria o Gate 3.
|
|
287
|
+
signals.e2eStatus = verification?.nodes?.e2e ?? null;
|
|
288
|
+
signals.e2eReason = verification?.e2eReason ?? null;
|
|
289
|
+
signals.e2eSkipClass = signals.e2eStatus === 'skip' ? classifyE2eSkip(signals.e2eReason) : null;
|
|
290
|
+
// De QUEM é o carimbo. `verify {f} {task}` grava 'task:{id}'; só 'feature'
|
|
291
|
+
// responde pela app inteira, e é por isso que este sinal existe: a razão
|
|
292
|
+
// 'task group "docs" does not touch runtime' é dispensa legítima do que
|
|
293
|
+
// AQUELA task verificou, e sem o escopo ela dispensaria o Gate 3 da feature.
|
|
294
|
+
// Fato de disco como os outros três — jamais sobrescrevível por --signals.
|
|
295
|
+
signals.verificationScope = verification?.scope ?? null;
|
|
296
|
+
|
|
297
|
+
// O nó de eval, separado do rollup pela mesma razão que o e2e: um eval que
|
|
298
|
+
// PULOU (escopo de task) não avermelha `verification.status`, então o gate
|
|
299
|
+
// leria 'pass' tanto para "o eval rodou e passou" quanto para "o eval nunca
|
|
300
|
+
// rodou". `null` aqui é "este projeto não declara nó de eval" — DISTINTO de
|
|
301
|
+
// 'skip', que é "declara, e esta corrida não o executou".
|
|
302
|
+
signals.evalsStatus = verification?.nodes?.evals ?? null;
|
|
303
|
+
// O que a feature TOCOU, contra os globs que o projeto declarou como
|
|
304
|
+
// prompt/modelo. Fato de disco: `declared` vem do config commitado, `touched`
|
|
305
|
+
// do git. Nenhuma política mora aqui — quem decide é shouldPauseGate.
|
|
306
|
+
signals.promptModel = collectPromptModel(cwd, defaultBranch);
|
|
307
|
+
|
|
124
308
|
signals.diffstat = getDiffStat(defaultBranch, cwd);
|
|
125
309
|
signals.sha = sha;
|
|
126
310
|
|
|
@@ -7,8 +7,28 @@ import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/featu
|
|
|
7
7
|
/** Fixed --dims order: a,c,q,t (mirrors the morph-eval dimension weights). */
|
|
8
8
|
const DIM_KEYS = ['architecture', 'contracts', 'codeQuality', 'testCoverage'];
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* taskScores keys are pinned to `^T\d+$` by feature.schema.json, and tasks.schema.json
|
|
12
|
+
* pins the same shape for the task ids themselves. Anything else — a dotted `T3.2`
|
|
13
|
+
* above all — is split on the dot by `updateFeature` and lands as a NESTED key that
|
|
14
|
+
* neither the schema nor the gate-check proxy can see.
|
|
15
|
+
*/
|
|
16
|
+
const TASK_ID_PATTERN = /^T\d+$/;
|
|
17
|
+
|
|
18
|
+
const USAGE_LINE = 'Usage: morph-spec score <feature> <task> <score> [--dims a,c,q,t] e.g. morph-spec score my-feature T1 9.2 --dims "9,10,9,9"';
|
|
19
|
+
|
|
10
20
|
function parseScoreValue(raw, label) {
|
|
11
|
-
|
|
21
|
+
// `Number('')` and `Number(' ')` are 0 — finite and inside 0-10, so the range check
|
|
22
|
+
// below waves them through and a blank value gets recorded as a real zero. Refuse the
|
|
23
|
+
// empty shape BEFORE Number() so no score can ever be fabricated.
|
|
24
|
+
const text = raw === undefined || raw === null ? '' : String(raw).trim();
|
|
25
|
+
if (text === '') {
|
|
26
|
+
const got = raw === undefined ? 'no value was passed' : `got "${raw}"`;
|
|
27
|
+
console.error(chalk.red(`\n❌ Missing ${label}: ${got} — a blank value is refused, never recorded as 0`));
|
|
28
|
+
console.error(chalk.gray(` ${USAGE_LINE}\n`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const value = Number(text);
|
|
12
32
|
if (!Number.isFinite(value) || value < 0 || value > 10) {
|
|
13
33
|
console.error(chalk.red(`\n❌ Invalid ${label}: "${raw}" — expected a number between 0 and 10\n`));
|
|
14
34
|
process.exit(1);
|
|
@@ -16,6 +36,18 @@ function parseScoreValue(raw, label) {
|
|
|
16
36
|
return value;
|
|
17
37
|
}
|
|
18
38
|
|
|
39
|
+
function assertTaskIdShape(taskId) {
|
|
40
|
+
const id = taskId === undefined || taskId === null ? '' : String(taskId);
|
|
41
|
+
if (TASK_ID_PATTERN.test(id)) return;
|
|
42
|
+
console.error(chalk.red(`\n❌ Invalid task id: "${id}" — expected the pattern T<n> (T1, T7, T12)`));
|
|
43
|
+
console.error(chalk.gray(
|
|
44
|
+
` taskScores keys are pinned by feature.schema.json (propertyNames.pattern "^T\\d+$").\n` +
|
|
45
|
+
` A dotted id is split on the "." and written as a nested key, invisible to the gate-check proxy.\n` +
|
|
46
|
+
` ${USAGE_LINE}\n`
|
|
47
|
+
));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
/**
|
|
20
52
|
* `morph-spec score <feature> <task> <score> [--dims a,c,q,t]` — record a
|
|
21
53
|
* task eval score under `taskScores` in the authoritative feature.json.
|
|
@@ -26,6 +58,9 @@ function parseScoreValue(raw, label) {
|
|
|
26
58
|
*/
|
|
27
59
|
export async function scoreCommand(featureName, taskId, scoreArg, options = {}) {
|
|
28
60
|
try {
|
|
61
|
+
// Argument validation runs before any state read or write: a refusal must never
|
|
62
|
+
// leave a fabricated or nested taskScores entry behind.
|
|
63
|
+
assertTaskIdShape(taskId);
|
|
29
64
|
const score = parseScoreValue(scoreArg, 'score');
|
|
30
65
|
|
|
31
66
|
const feature = getFeature(featureName);
|
|
@@ -3,9 +3,10 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
3
3
|
import { resolve, join } from 'path';
|
|
4
4
|
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
5
5
|
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
6
|
-
import { runVerify } from '../../lib/validators/verify-runner.js';
|
|
6
|
+
import { runVerify, VerifyUsageError } from '../../lib/validators/verify-runner.js';
|
|
7
7
|
import { resolvePrimaryRoot } from '../project/worktree.js';
|
|
8
8
|
import { getHeadSha } from '../../lib/git/default-branch.js';
|
|
9
|
+
import { EVAL_RUNNER } from '../../lib/tasks/eval-node.js';
|
|
9
10
|
|
|
10
11
|
/** ✓ pass · ✗ fail · – skip */
|
|
11
12
|
function statusIcon(status) {
|
|
@@ -29,6 +30,11 @@ function formatVerifyResult(result) {
|
|
|
29
30
|
lines.push('');
|
|
30
31
|
lines.push(result.status === 'pass' ? chalk.green.bold('✓ PASS') : chalk.red.bold('✗ FAIL'));
|
|
31
32
|
lines.push('');
|
|
33
|
+
const env = result.nodes.tests?.environment;
|
|
34
|
+
if (env) {
|
|
35
|
+
lines.push(chalk.cyan(`environment: ${env.kind} (${env.image})`));
|
|
36
|
+
lines.push('');
|
|
37
|
+
}
|
|
32
38
|
lines.push('Nodes:');
|
|
33
39
|
for (const [name, node] of Object.entries(result.nodes)) {
|
|
34
40
|
const icon = statusIcon(node.status);
|
|
@@ -112,6 +118,54 @@ function decidePersistence({ cwd, featureName, noPersist }) {
|
|
|
112
118
|
return { persist: false, reason: 'unregistered-worktree' };
|
|
113
119
|
}
|
|
114
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The per-node status map, in the shape `feature.schema.json` fixes for BOTH
|
|
123
|
+
* writers of it — the per-scope history entry (`verifications.*`) and the
|
|
124
|
+
* singular gate stamp (`verification.nodes`). One function because they were
|
|
125
|
+
* two literal copies twenty-five lines apart: a fifth node meant remembering
|
|
126
|
+
* both, and a stamp missing a node the history has is precisely the kind of
|
|
127
|
+
* half-truth the gate reads as fact.
|
|
128
|
+
*
|
|
129
|
+
* @param {{ nodes: Record<string, { status: string }> }} result - runVerify() output
|
|
130
|
+
* @returns {{ build: string, tests: string, validators: string, e2e: string }}
|
|
131
|
+
*/
|
|
132
|
+
function nodeStatuses(result) {
|
|
133
|
+
return {
|
|
134
|
+
build: result.nodes.build.status,
|
|
135
|
+
tests: result.nodes.tests.status,
|
|
136
|
+
validators: result.nodes.validators.status,
|
|
137
|
+
e2e: result.nodes.e2e.status,
|
|
138
|
+
evals: evalsStatusOf(result),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The status of the eval suites, pulled OUT of the tests rollup.
|
|
144
|
+
*
|
|
145
|
+
* The rollup cannot answer the question Gate 3 asks. A red eval and a red unit
|
|
146
|
+
* test both arrive as `tests: 'fail'`, and — worse — an eval node that was
|
|
147
|
+
* SKIPPED (task scope) leaves the rollup green, so a gate reading `tests` alone
|
|
148
|
+
* cannot tell "the eval ran and passed" from "the eval never ran".
|
|
149
|
+
*
|
|
150
|
+
* Four values, and the distinction between the last two is the whole point:
|
|
151
|
+
* - `'pass'` / `'fail'`: every declared eval node agreed, or at least one did
|
|
152
|
+
* not. Same pessimistic rollup rule the tests node already uses;
|
|
153
|
+
* - `'skip'`: eval nodes ARE declared, but this run did not execute them;
|
|
154
|
+
* - `null`: this project declares NO eval node. Not a skip — there is nothing
|
|
155
|
+
* to skip. `risk-detector.js` pauses on the two for different reasons,
|
|
156
|
+
* because they call for different actions.
|
|
157
|
+
*
|
|
158
|
+
* @param {{nodes: {tests: {runs?: Array}}}} result - runVerify() output
|
|
159
|
+
* @returns {'pass'|'fail'|'skip'|null}
|
|
160
|
+
*/
|
|
161
|
+
function evalsStatusOf(result) {
|
|
162
|
+
const runs = (result.nodes.tests?.runs || []).filter((run) => run.runner === EVAL_RUNNER);
|
|
163
|
+
if (runs.length === 0) return null;
|
|
164
|
+
if (runs.some((run) => run.status === 'fail')) return 'fail';
|
|
165
|
+
if (runs.some((run) => run.status === 'pass')) return 'pass';
|
|
166
|
+
return 'skip';
|
|
167
|
+
}
|
|
168
|
+
|
|
115
169
|
/** Read tasks.json's task list for a feature; [] if missing/corrupt. */
|
|
116
170
|
function readTasks(featureName) {
|
|
117
171
|
try {
|
|
@@ -129,10 +183,20 @@ function readTasks(featureName) {
|
|
|
129
183
|
* for the node cadence; this command wraps it with CLI ergonomics
|
|
130
184
|
* (human/JSON output, exit codes) and context-aware persistence.
|
|
131
185
|
*
|
|
132
|
-
* Exit codes: 0 = pass (including all-skip) · 1 = fail (>=1 red node
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
186
|
+
* Exit codes: 0 = pass (including all-skip) · 1 = fail (>=1 red node, or a
|
|
187
|
+
* `--record-baseline` the run refused to write) · 2 = usage error.
|
|
188
|
+
*
|
|
189
|
+
* Exit 2 is "the command cannot mean what you typed", and there are four
|
|
190
|
+
* paths to it — never a verdict, because a verdict about nothing is worse
|
|
191
|
+
* than a refusal:
|
|
192
|
+
* - unknown feature, or unknown task id (the task's `group` decides the
|
|
193
|
+
* rubric, so it is a hard usage error here, unlike the advisory warning
|
|
194
|
+
* `morph-spec score` gives for the same situation);
|
|
195
|
+
* - `--filter` that matches no declared node (a filtered-to-empty run is
|
|
196
|
+
* green and proves nothing);
|
|
197
|
+
* - `--record-baseline` with nowhere to write (no `config.json`);
|
|
198
|
+
* - `--linux` without Docker.
|
|
199
|
+
* The last three arrive as `VerifyUsageError` from the runner.
|
|
136
200
|
*/
|
|
137
201
|
export async function verifyCommand(featureName, taskId, options = {}) {
|
|
138
202
|
let feature;
|
|
@@ -154,21 +218,40 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
154
218
|
}
|
|
155
219
|
}
|
|
156
220
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
221
|
+
let result;
|
|
222
|
+
try {
|
|
223
|
+
result = await runVerify(process.cwd(), featureName, taskId || null, {
|
|
224
|
+
rubric: options.rubric,
|
|
225
|
+
timeout: options.timeout,
|
|
226
|
+
skipTests: options.skipTests,
|
|
227
|
+
skipBuild: options.skipBuild,
|
|
228
|
+
skipE2e: options.skipE2e,
|
|
229
|
+
freshE2e: options.freshE2e,
|
|
230
|
+
filter: options.filter,
|
|
231
|
+
recordBaseline: options.recordBaseline,
|
|
232
|
+
refreshCache: options.refreshCache,
|
|
233
|
+
linux: options.linux,
|
|
234
|
+
linuxImage: options.linuxImage,
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
// A wrong invocation is exit 2, like an unknown feature or task — never a
|
|
238
|
+
// verdict. `--filter` that selects nothing must not print green (or red):
|
|
239
|
+
// there is no verification to report, only a command to fix.
|
|
240
|
+
if (err instanceof VerifyUsageError) {
|
|
241
|
+
console.error(chalk.red(`\n❌ ${err.message}\n`));
|
|
242
|
+
process.exit(2);
|
|
243
|
+
}
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
165
246
|
|
|
166
247
|
const scopeKey = taskId ? `taskVerifications.${taskId}` : 'verifications.feature';
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
248
|
+
// `--record-baseline` is config maintenance, not gate evidence: it measures
|
|
249
|
+
// the suite in order to write `expectedCount`. Persisting a `verification`
|
|
250
|
+
// stamp from it would let a maintenance run stand in for the proof a gate
|
|
251
|
+
// asks for — the same class of confusion as a hand-typed baseline.
|
|
252
|
+
const { persist, reason } = options.recordBaseline
|
|
253
|
+
? { persist: false, reason: 'record-baseline' }
|
|
254
|
+
: decidePersistence({ cwd: process.cwd(), featureName, noPersist: options.noPersist });
|
|
172
255
|
|
|
173
256
|
let compact = null;
|
|
174
257
|
if (persist) {
|
|
@@ -176,12 +259,7 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
176
259
|
const now = new Date().toISOString();
|
|
177
260
|
compact = {
|
|
178
261
|
status: result.status,
|
|
179
|
-
nodes:
|
|
180
|
-
build: result.nodes.build.status,
|
|
181
|
-
tests: result.nodes.tests.status,
|
|
182
|
-
validators: result.nodes.validators.status,
|
|
183
|
-
e2e: result.nodes.e2e.status,
|
|
184
|
-
},
|
|
262
|
+
nodes: nodeStatuses(result),
|
|
185
263
|
score: result.nodes.validators.score ?? null,
|
|
186
264
|
attempts: (prevEntry?.attempts ?? 0) + 1,
|
|
187
265
|
updatedAt: now,
|
|
@@ -195,8 +273,19 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
195
273
|
// `**Reviewed-SHA:**` token lets approve.js detect a stale review. null
|
|
196
274
|
// in a non-git checkout (or before the first commit) — fail-open, never
|
|
197
275
|
// blocks persistence.
|
|
276
|
+
//
|
|
277
|
+
// `nodes` + `e2eReason` são acréscimo de campo, não mudança de forma: o
|
|
278
|
+
// rollup `status` continua onde estava. Existem porque um nó e2e que PULA
|
|
279
|
+
// não reprova o rollup — sem o status do nó e a razão LITERAL do skip, o
|
|
280
|
+
// gate-check lê 'pass' tanto quando o e2e rodou e passou quanto quando ele
|
|
281
|
+
// nunca rodou, e não tem como separar dispensa legítima ("o projeto não
|
|
282
|
+
// containeriza") de ausência de prova ("docker indisponível"). `e2eReason`
|
|
283
|
+
// é null quando o nó não pulou — e um null NUM SKIP é lido como dispensa
|
|
284
|
+
// sem motivo, que risk-detector.js recusa.
|
|
198
285
|
const stamp = {
|
|
199
286
|
status: result.status,
|
|
287
|
+
nodes: nodeStatuses(result),
|
|
288
|
+
e2eReason: result.nodes.e2e.status === 'skip' ? (result.nodes.e2e.reason ?? null) : null,
|
|
200
289
|
sha: getHeadSha(process.cwd()),
|
|
201
290
|
ts: now,
|
|
202
291
|
scope: taskId ? `task:${taskId}` : 'feature',
|
|
@@ -204,11 +293,26 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
204
293
|
await updateFeature(featureName, 'verification', stamp);
|
|
205
294
|
}
|
|
206
295
|
|
|
296
|
+
// The refusal always goes to stderr, so it survives --json (whose stdout has
|
|
297
|
+
// to stay parseable) and is never something the reader has to go looking for.
|
|
298
|
+
if (result.baseline && !result.baseline.recorded) {
|
|
299
|
+
console.error(chalk.red(`
|
|
300
|
+
❌ baseline não gravada: ${result.baseline.reason}
|
|
301
|
+
`));
|
|
302
|
+
}
|
|
303
|
+
|
|
207
304
|
if (options.json) {
|
|
208
305
|
const output = { ...result, persisted: persist, ...(persist ? {} : { reason }) };
|
|
209
306
|
console.log(JSON.stringify(output, null, 2));
|
|
210
307
|
} else {
|
|
211
308
|
console.log(formatVerifyResult(result));
|
|
309
|
+
if (result.baseline?.recorded) {
|
|
310
|
+
for (const u of result.baseline.updates) {
|
|
311
|
+
console.log(chalk.green(` baseline ${u.id}: ${u.from ?? '—'} → ${u.to}`));
|
|
312
|
+
}
|
|
313
|
+
console.log(chalk.gray(`Gravada em ${result.baseline.path}
|
|
314
|
+
`));
|
|
315
|
+
}
|
|
212
316
|
if (persist) {
|
|
213
317
|
console.log(chalk.gray(`Persisted: ${scopeKey} (attempts: ${compact.attempts})\n`));
|
|
214
318
|
} else {
|
|
@@ -216,5 +320,10 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
216
320
|
}
|
|
217
321
|
}
|
|
218
322
|
|
|
219
|
-
|
|
323
|
+
// A recording the user asked for and did not get is a failure of the
|
|
324
|
+
// invocation even when the run itself was green (a filtered run is green and
|
|
325
|
+
// unrecordable): exiting 0 there would report success for something that did
|
|
326
|
+
// not happen.
|
|
327
|
+
const baselineRefused = Boolean(result.baseline && !result.baseline.recorded);
|
|
328
|
+
process.exit(result.status === 'pass' && !baselineRefused ? 0 : 1);
|
|
220
329
|
}
|
|
@@ -12,7 +12,15 @@ import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
|
|
|
12
12
|
const STATE_FILE_NAME = '.morph/state.json';
|
|
13
13
|
const FEATURES_DIR_NAME = '.morph/features';
|
|
14
14
|
const FEATURE_FILE_NAME = 'feature.json';
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The one spelling of the committed project config's location, relative to a
|
|
17
|
+
* project root. Exported because three modules used to write it out by hand
|
|
18
|
+
* (`readProjectFromConfig` here, `readMorphConfig` and `morphConfigPath` in
|
|
19
|
+
* src/lib/tasks/) — and `morphConfigPath`'s own JSDoc promised the reader that
|
|
20
|
+
* the writer and the reader "can never disagree about which file is meant",
|
|
21
|
+
* a guarantee three copies of a string cannot give. Now they can't.
|
|
22
|
+
*/
|
|
23
|
+
export const CONFIG_FILE_NAME = '.morph/config/config.json';
|
|
16
24
|
|
|
17
25
|
// The on-disk state version. NOTE: this tracks the *feature-object* schema, which
|
|
18
26
|
// is unchanged by the per-feature storage split — features are now persisted to
|