@polymorphism-tech/morph-spec 8.34.2 → 8.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +20 -3
- package/README.md +2 -2
- package/bin/morph-spec.js +18 -4
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +16 -17
- package/framework/agents.json +197 -44
- package/framework/ai-pin.json +22 -0
- package/framework/commands/morph-media.md +45 -0
- package/framework/docs/CLI.md +234 -9
- package/framework/evals/proof-rubrics/README.md +76 -0
- package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
- package/framework/evals/proof-rubrics/fiacao.md +54 -0
- package/framework/evals/proof-rubrics/guard-cego.md +52 -0
- package/framework/evals/proof-rubrics/media-claims.md +109 -0
- package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
- package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
- package/framework/evals/rubrics/architecture.md +30 -0
- package/framework/evals/rubrics/code-quality.md +26 -0
- package/framework/evals/rubrics/test-coverage.md +20 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/e2e-check.js +135 -22
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rules/ai-agents-standards.md +14 -2
- package/framework/schemas/ai-pin.schema.json +97 -0
- package/framework/schemas/config.schema.json +107 -0
- package/framework/schemas/feature.schema.json +23 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +26 -5
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +18 -2
- package/framework/skills/core/morph-media/SKILL.md +215 -0
- package/framework/skills/core/morph-plan/SKILL.md +33 -2
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +166 -9
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/PROVENANCE.json +81 -3
- package/framework/standards/STANDARDS.json +1483 -839
- package/framework/standards/ai-agents/agent-archetypes.md +160 -0
- package/framework/standards/ai-agents/agent-session.md +164 -68
- package/framework/standards/ai-agents/agent-spec.md +196 -0
- package/framework/standards/ai-agents/batching.md +61 -37
- package/framework/standards/ai-agents/context-providers.md +97 -24
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
- package/framework/standards/ai-agents/cost-and-budget.md +256 -0
- package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
- package/framework/standards/ai-agents/evals-with-cache.md +289 -0
- package/framework/standards/ai-agents/guardrails.md +222 -0
- package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
- package/framework/standards/ai-agents/mcp-server.md +119 -18
- package/framework/standards/ai-agents/mcp-tools.md +85 -32
- package/framework/standards/ai-agents/media-pipeline.md +352 -0
- package/framework/standards/ai-agents/media-video.md +267 -0
- package/framework/standards/ai-agents/middleware-patterns.md +39 -5
- package/framework/standards/ai-agents/modalities-image-gen.md +354 -105
- package/framework/standards/ai-agents/modalities-speech-to-text.md +8 -2
- package/framework/standards/ai-agents/modalities-text-to-speech.md +10 -2
- package/framework/standards/ai-agents/modalities-vision.md +6 -2
- package/framework/standards/ai-agents/multi-agent-patterns.md +61 -5
- package/framework/standards/ai-agents/observability-patterns.md +227 -17
- package/framework/standards/ai-agents/production.md +38 -9
- package/framework/standards/ai-agents/prompt-sources.md +389 -0
- package/framework/standards/ai-agents/providers/model-registry.md +174 -39
- package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
- package/framework/standards/ai-agents/service-tier-flex.md +27 -19
- package/framework/standards/ai-agents/setup.md +166 -50
- package/framework/standards/ai-agents/structured-output.md +34 -3
- package/framework/standards/ai-agents/sweet-spot.md +23 -3
- package/framework/standards/ai-agents/testing-ai.md +236 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
- package/framework/standards/ai-agents/workflows.md +27 -7
- package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +3 -1
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +13 -0
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +1 -1
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +1 -1
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
- package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
- package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
- package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
- package/framework/templates/dotnet/ai-kit/README.md +128 -0
- package/framework/templates/dotnet/ai-kit/global.json +9 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +3 -3
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/e2e/e2e.js +15 -2
- package/src/commands/index.js +13 -13
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +140 -5
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +8 -1
- package/src/commands/state/advance.js +24 -2
- package/src/commands/state/approve.js +33 -1
- package/src/commands/state/create.js +72 -11
- package/src/commands/state/finish.js +333 -21
- package/src/commands/state/gate-check.js +190 -6
- package/src/commands/state/score.js +36 -1
- package/src/commands/validation/verify.js +134 -25
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +9 -1
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/ai/ai-checks.js +182 -0
- package/src/lib/ai/ai-project-scan.js +347 -0
- package/src/lib/ai/code-audit.js +181 -0
- package/src/lib/ai/comment-stripper.js +184 -0
- package/src/lib/ai/pin-drift.js +138 -0
- package/src/lib/ai/registry-audit.js +285 -0
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/e2e/compose-env.js +50 -4
- package/src/lib/evaluator-report.js +269 -15
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/git/default-branch.js +92 -4
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/effects.js +193 -0
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/installers/templates-reconcile.js +3 -3
- package/src/lib/risk-detector.js +284 -0
- package/src/lib/standards/verified-against.js +276 -0
- package/src/lib/tasks/eval-node.js +485 -0
- package/src/lib/tasks/linux-runner.js +297 -0
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-nodes.js +486 -0
- package/src/lib/tasks/test-runner.js +410 -33
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/verify-runner.js +583 -28
- package/src/lib/worktree/docker-audit.js +287 -0
- package/src/lib/worktree/env-allocator.js +24 -0
- package/src/lib/worktree/worktree-audit.js +30 -1
- package/src/scripts/setup-infra.js +24 -11
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +18 -8
- package/src/utils/claude-md-injector.js +10 -5
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +11 -4
- package/src/utils/hooks-installer.js +29 -26
- package/src/utils/skills-installer.js +36 -29
- package/src/utils/version-checker.js +68 -3
- package/framework/standards/ai-agents/prompts-in-db.md +0 -214
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Nodes — the PLURAL resolver for "which test suites does this project have?".
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS MODULE EXISTS
|
|
5
|
+
*
|
|
6
|
+
* `test-runner.js` answers a singular question: ONE command, resolved by a
|
|
7
|
+
* four-step fallback. That shape is what defect F30 exploited — the resolver
|
|
8
|
+
* found a `.sln`, answered "dotnet test", and 3.082 vitest tests in the same
|
|
9
|
+
* repository never ran while the gate reported green. A project with two
|
|
10
|
+
* runners had no way to say so, and a verifier that cannot be told about a
|
|
11
|
+
* suite cannot fail because of it.
|
|
12
|
+
*
|
|
13
|
+
* So the unit of verification becomes a LIST of nodes. `project.tests[]` in
|
|
14
|
+
* `.morph/config/config.json` declares them; when it is absent, the four-step
|
|
15
|
+
* fallback still runs and is synthesized into exactly one node. Declared beats
|
|
16
|
+
* guessed (D3): deeper auto-detection would only trade one silent false
|
|
17
|
+
* negative for another, and the cost of the error here is a green gate over
|
|
18
|
+
* untested code.
|
|
19
|
+
*
|
|
20
|
+
* WHY VALIDATION IS FAIL-CLOSED HERE (D4)
|
|
21
|
+
*
|
|
22
|
+
* Every other config reader in this repo is fail-open — `JSON.parse` inside a
|
|
23
|
+
* `try/catch`, optional chaining, absent means "carry on". That is right for a
|
|
24
|
+
* hook: a guardrail that kills the session is worse than one that stays quiet.
|
|
25
|
+
* It is exactly WRONG for the verifier. A typo in `tests[]` treated fail-open
|
|
26
|
+
* falls through to the legacy resolver and reproduces F30 — green over a suite
|
|
27
|
+
* that never ran. So an invalid node is not ignored: it becomes a node with a
|
|
28
|
+
* pre-decided `status: 'fail'` and a reason naming the problem. It shows up in
|
|
29
|
+
* `runs[]` and it reddens the gate.
|
|
30
|
+
*
|
|
31
|
+
* @module test-nodes
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { existsSync, readFileSync } from 'fs';
|
|
35
|
+
import { join, resolve } from 'path';
|
|
36
|
+
|
|
37
|
+
import { resolveTestPlan, readMorphConfig, isInside } from './test-runner.js';
|
|
38
|
+
import { EVAL_RUNNER, evalBlockProblem } from './eval-node.js';
|
|
39
|
+
import { atomicWriteJson, CONFIG_FILE_NAME } from '../../core/state/state-manager.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The count extractors that exist (one per runner, see parseTestCount).
|
|
43
|
+
* `none` is the honest answer for a command whose summary nobody can read —
|
|
44
|
+
* it yields an UNKNOWN count, never a fabricated zero.
|
|
45
|
+
*/
|
|
46
|
+
export const TEST_RUNNERS = ['dotnet', 'vitest', 'jest', 'node-test', 'playwright', 'none', EVAL_RUNNER];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A node id is what `--filter` matches and what keys `runs[]`. Lowercase and
|
|
50
|
+
* dash-only so it survives a shell argument and a JSON key without quoting.
|
|
51
|
+
*/
|
|
52
|
+
export const TEST_NODE_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
53
|
+
|
|
54
|
+
/** Id given to the single node the fallback synthesizes — it has no name of its own. */
|
|
55
|
+
const FALLBACK_NODE_ID = 'default';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse the `--filter` argument into the token list `nodeMatchesFilter` reads.
|
|
59
|
+
*
|
|
60
|
+
* Lives here, beside TEST_NODE_ID_PATTERN, because the thing being matched is
|
|
61
|
+
* a node id: the spelling rules for the id and the spelling rules for what
|
|
62
|
+
* selects it must never drift into two modules.
|
|
63
|
+
*
|
|
64
|
+
* An argument that yields NO tokens (`--filter ""`, `--filter " , "`) returns
|
|
65
|
+
* an empty token list rather than null. That distinction is the point: null
|
|
66
|
+
* means "the user did not filter", `[]` means "the user filtered and selected
|
|
67
|
+
* nothing" — and selecting nothing is a usage error, not a full run.
|
|
68
|
+
*
|
|
69
|
+
* @param {string|null|undefined} raw
|
|
70
|
+
* @returns {{ raw: string, tokens: string[] }|null} null when no filter was given
|
|
71
|
+
*/
|
|
72
|
+
export function parseTestFilter(raw) {
|
|
73
|
+
if (raw === undefined || raw === null || raw === false) return null;
|
|
74
|
+
const text = String(raw);
|
|
75
|
+
return {
|
|
76
|
+
raw: text,
|
|
77
|
+
tokens: text.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Does this node answer to the filter? Exact id, or the token as a substring
|
|
83
|
+
* of the id (`--filter dot` selects `dotnet`), case-insensitively.
|
|
84
|
+
*
|
|
85
|
+
* @param {TestNode} node
|
|
86
|
+
* @param {{ tokens: string[] }|null} filter - null (no filter) matches everything
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
export function nodeMatchesFilter(node, filter) {
|
|
90
|
+
if (!filter) return true;
|
|
91
|
+
const id = String(node?.id ?? '').toLowerCase();
|
|
92
|
+
return filter.tokens.some((token) => id === token || id.includes(token));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Reason kept WORD FOR WORD from the singular resolver: readers already print it. */
|
|
96
|
+
const NO_SUITE_REASON = 'No testCommand configured and no known project file detected';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One declared (or synthesized) test suite.
|
|
100
|
+
*
|
|
101
|
+
* @typedef {Object} TestNode
|
|
102
|
+
* @property {string} id - Unique within the list; what `--filter` matches.
|
|
103
|
+
* @property {string|null} command - Literal shell command; null only on a node already decided.
|
|
104
|
+
* @property {string} cwd - Relative to the project root; '.' by default.
|
|
105
|
+
* @property {string} runner - One of TEST_RUNNERS; inferred from `command` when not declared.
|
|
106
|
+
* @property {number|null} expectedCount - Recorded baseline. **null is UNKNOWN, never 0**:
|
|
107
|
+
* a 0 here would fabricate a count drop out of "no baseline recorded yet".
|
|
108
|
+
* @property {number|null} timeoutMs - Per-node ceiling. **null is UNKNOWN, never 0** — it
|
|
109
|
+
* means "derive the ceiling", not "no time allowed".
|
|
110
|
+
* @property {string[]} build - Test projects to compile first (dotnet only); `[]` is
|
|
111
|
+
* "nothing declared", which is a real answer, not an unknown.
|
|
112
|
+
* @property {boolean} e2e - Needs the stack up; feature scope only.
|
|
113
|
+
* @property {string|null} linuxImage - Container image for `verify --linux`; null defers to the run's.
|
|
114
|
+
* @property {number} projectCount - How many test projects `command` chains — the input to
|
|
115
|
+
* the derived timeout. Always 1 for a declared node.
|
|
116
|
+
* @property {'fail'|'skip'|null} status - Pre-decided verdict. **Non-null means DO NOT
|
|
117
|
+
* EXECUTE**: the node is invalid config, or an explicit declaration of "no tests".
|
|
118
|
+
* null is the normal case: no verdict yet, the run decides.
|
|
119
|
+
* @property {string|null} reason - Why, when `status` is set.
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Infer which summary extractor can read a command's output.
|
|
124
|
+
*
|
|
125
|
+
* Deliberately conservative: `npm test` can front ANY runner, so it infers
|
|
126
|
+
* `none` (unknown count) rather than guessing. An extractor applied to the
|
|
127
|
+
* wrong output produces `null` anyway — but a WRONG guess that happens to
|
|
128
|
+
* match would invent a number, and numbers here fail gates.
|
|
129
|
+
*
|
|
130
|
+
* @param {string|null|undefined} command
|
|
131
|
+
* @returns {string} one of TEST_RUNNERS
|
|
132
|
+
*/
|
|
133
|
+
export function inferRunner(command) {
|
|
134
|
+
// `evals` is never inferred, on purpose: it is a DECLARATION, not a shape a
|
|
135
|
+
// command line can betray. An eval command is literally `dotnet test`, so any
|
|
136
|
+
// sniffing here would classify every dotnet suite as an eval — and, worse, the
|
|
137
|
+
// reverse mistake (an eval declared and read as plain dotnet) would silently
|
|
138
|
+
// drop the cache pre-flight.
|
|
139
|
+
if (typeof command !== 'string' || !command.trim()) return 'none';
|
|
140
|
+
if (/\bdotnet\s+test\b/.test(command)) return 'dotnet';
|
|
141
|
+
if (/\bvitest\b/.test(command)) return 'vitest';
|
|
142
|
+
if (/\bplaywright\b/.test(command)) return 'playwright';
|
|
143
|
+
if (/\bjest\b/.test(command)) return 'jest';
|
|
144
|
+
if (/\bnode\b[^&|]*\s--test\b/.test(command) || /run-tests\.mjs\b/.test(command)) return 'node-test';
|
|
145
|
+
return 'none';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build a fully-shaped node. Every field is present on every node so that no
|
|
150
|
+
* reader has to distinguish "absent" from "unknown" by optional chaining.
|
|
151
|
+
*/
|
|
152
|
+
function makeNode(partial) {
|
|
153
|
+
return {
|
|
154
|
+
id: partial.id,
|
|
155
|
+
command: partial.command ?? null,
|
|
156
|
+
cwd: partial.cwd ?? '.',
|
|
157
|
+
runner: partial.runner ?? inferRunner(partial.command),
|
|
158
|
+
expectedCount: partial.expectedCount ?? null,
|
|
159
|
+
timeoutMs: partial.timeoutMs ?? null,
|
|
160
|
+
build: partial.build ?? [],
|
|
161
|
+
e2e: partial.e2e ?? false,
|
|
162
|
+
// Per-node image for `verify --linux`. **null is UNKNOWN, never ''** — it
|
|
163
|
+
// means "use the run's image", not "use no image".
|
|
164
|
+
linuxImage: partial.linuxImage ?? null,
|
|
165
|
+
projectCount: partial.projectCount ?? 1,
|
|
166
|
+
// The eval block travels on the node so `verify-runner` never has to go back
|
|
167
|
+
// to config.json to learn where the cache lives. **null is "this is not an
|
|
168
|
+
// eval node"** — distinct from an eval node with an empty block, which
|
|
169
|
+
// `validateTestNodes` already refused.
|
|
170
|
+
evals: partial.evals ?? null,
|
|
171
|
+
status: partial.status ?? null,
|
|
172
|
+
reason: partial.reason ?? null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A node that will not run, and says why in the vocabulary of §2.2 of the spec. */
|
|
177
|
+
function invalidNode(id, problem, raw = {}) {
|
|
178
|
+
return makeNode({
|
|
179
|
+
id,
|
|
180
|
+
command: typeof raw.command === 'string' ? raw.command : null,
|
|
181
|
+
cwd: typeof raw.cwd === 'string' ? raw.cwd : '.',
|
|
182
|
+
status: 'fail',
|
|
183
|
+
reason: `invalid-test-node: ${id}: ${problem}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isPlainObject(value) {
|
|
188
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isNonNegativeInteger(value) {
|
|
192
|
+
return Number.isInteger(value) && value >= 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* First problem with one raw `tests[]` entry, or null when it is well formed.
|
|
197
|
+
* First problem only: a list of complaints about a single node buries the one
|
|
198
|
+
* that has to be fixed first.
|
|
199
|
+
*
|
|
200
|
+
* @param {*} raw
|
|
201
|
+
* @param {string|null} projectPath - When given, `cwd` is also checked against the disk.
|
|
202
|
+
* @returns {string|null}
|
|
203
|
+
*/
|
|
204
|
+
function firstProblem(raw, projectPath) {
|
|
205
|
+
if (!isPlainObject(raw)) return 'must be an object';
|
|
206
|
+
|
|
207
|
+
if (typeof raw.id !== 'string' || !TEST_NODE_ID_PATTERN.test(raw.id)) {
|
|
208
|
+
return `id must be a string matching ${TEST_NODE_ID_PATTERN.source}`;
|
|
209
|
+
}
|
|
210
|
+
if (typeof raw.command !== 'string' || !raw.command.trim()) {
|
|
211
|
+
return 'command must be a non-empty string';
|
|
212
|
+
}
|
|
213
|
+
if (raw.cwd !== undefined && (typeof raw.cwd !== 'string' || !raw.cwd.trim())) {
|
|
214
|
+
return 'cwd must be a non-empty string';
|
|
215
|
+
}
|
|
216
|
+
if (raw.runner !== undefined && !TEST_RUNNERS.includes(raw.runner)) {
|
|
217
|
+
return `runner must be one of ${TEST_RUNNERS.join(' | ')}`;
|
|
218
|
+
}
|
|
219
|
+
if (raw.expectedCount !== undefined && !isNonNegativeInteger(raw.expectedCount)) {
|
|
220
|
+
return 'expectedCount must be an integer >= 0';
|
|
221
|
+
}
|
|
222
|
+
if (raw.timeoutMs !== undefined && !(Number.isInteger(raw.timeoutMs) && raw.timeoutMs > 0)) {
|
|
223
|
+
return 'timeoutMs must be an integer > 0';
|
|
224
|
+
}
|
|
225
|
+
if (raw.build !== undefined && (!Array.isArray(raw.build) || raw.build.some((b) => typeof b !== 'string'))) {
|
|
226
|
+
return 'build must be an array of strings';
|
|
227
|
+
}
|
|
228
|
+
// `build[]` is compiled by the dotnet prebuild and by nothing else. Declared
|
|
229
|
+
// on a vitest node it would be silently ignored — and a declaration the
|
|
230
|
+
// verifier ignores is the same fail-open shape as the typo in `tests[]`:
|
|
231
|
+
// the author believes something is being compiled that never is.
|
|
232
|
+
if (Array.isArray(raw.build) && raw.build.length > 0
|
|
233
|
+
&& (raw.runner ?? inferRunner(raw.command)) !== 'dotnet') {
|
|
234
|
+
return 'build applies only to runner "dotnet"';
|
|
235
|
+
}
|
|
236
|
+
if (raw.e2e !== undefined && typeof raw.e2e !== 'boolean') {
|
|
237
|
+
return 'e2e must be a boolean';
|
|
238
|
+
}
|
|
239
|
+
if (raw.linuxImage !== undefined && (typeof raw.linuxImage !== 'string' || !raw.linuxImage.trim())) {
|
|
240
|
+
return 'linuxImage must be a non-empty string';
|
|
241
|
+
}
|
|
242
|
+
// `runner: "evals"` REQUIRES its block; the block on any other runner is a
|
|
243
|
+
// declaration the verifier would silently ignore — the same fail-open shape as
|
|
244
|
+
// a typo in `tests[]`, where the author believes something is being checked
|
|
245
|
+
// that never is.
|
|
246
|
+
if ((raw.runner ?? inferRunner(raw.command)) === EVAL_RUNNER) {
|
|
247
|
+
const problem = evalBlockProblem(raw, projectPath);
|
|
248
|
+
if (problem) return problem;
|
|
249
|
+
} else if (raw.evals !== undefined) {
|
|
250
|
+
return `evals applies only to runner "${EVAL_RUNNER}"`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// The cwd checks come last because they are the expensive ones, and because
|
|
254
|
+
// a node that is malformed in shape has a better error to report first.
|
|
255
|
+
const cwd = typeof raw.cwd === 'string' ? raw.cwd.trim() : '.';
|
|
256
|
+
const root = resolve(projectPath ?? '.');
|
|
257
|
+
const abs = resolve(root, cwd);
|
|
258
|
+
if (!isInside(abs, root)) {
|
|
259
|
+
// A cwd that leaves the root addresses ANOTHER tree — the same damage the
|
|
260
|
+
// worktree exclusion in collectDotnetTestProjects exists to prevent.
|
|
261
|
+
return `cwd "${cwd}" resolves outside the project root`;
|
|
262
|
+
}
|
|
263
|
+
if (projectPath && !existsSync(abs)) {
|
|
264
|
+
// P5: named failure, never a skip. A wrong cwd is a plan error, and a skip
|
|
265
|
+
// would hide it behind a green gate.
|
|
266
|
+
return `cwd "${cwd}" does not exist`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Validate `config.project.tests[]` IN USE — this is called by
|
|
274
|
+
* resolveTestNodes(), not merely declared in a schema. A schema that nothing
|
|
275
|
+
* calls is decoration; this repo already paid for fifteen guardrails that
|
|
276
|
+
* could not detect anything (v8.9/v8.11).
|
|
277
|
+
*
|
|
278
|
+
* Fail-closed by design (D4): a bad entry becomes a node with
|
|
279
|
+
* `status: 'fail'`, never a node that disappears.
|
|
280
|
+
*
|
|
281
|
+
* @param {object|null} config - Parsed `.morph/config/config.json`
|
|
282
|
+
* @param {string|null} [projectPath] - Project root; enables the on-disk `cwd` check
|
|
283
|
+
* @returns {TestNode[]} One node per declared entry, in declaration order
|
|
284
|
+
*/
|
|
285
|
+
export function validateTestNodes(config, projectPath = null) {
|
|
286
|
+
const raw = config?.project?.tests;
|
|
287
|
+
|
|
288
|
+
if (!Array.isArray(raw)) {
|
|
289
|
+
return [invalidNode('project.tests', 'must be an array of test nodes')];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Count ids BEFORE judging: two nodes sharing an id make `--filter` and
|
|
293
|
+
// `runs[]` ambiguous, and picking a winner would silence one declared suite.
|
|
294
|
+
// Both sides fail — the ambiguity is the defect, not either entry.
|
|
295
|
+
const seen = new Map();
|
|
296
|
+
for (const entry of raw) {
|
|
297
|
+
if (isPlainObject(entry) && typeof entry.id === 'string') {
|
|
298
|
+
seen.set(entry.id, (seen.get(entry.id) ?? 0) + 1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return raw.map((entry, index) => {
|
|
303
|
+
const id = isPlainObject(entry) && typeof entry.id === 'string' && entry.id
|
|
304
|
+
? entry.id
|
|
305
|
+
: `tests[${index}]`;
|
|
306
|
+
|
|
307
|
+
const problem = firstProblem(entry, projectPath);
|
|
308
|
+
if (problem) return invalidNode(id, problem, isPlainObject(entry) ? entry : {});
|
|
309
|
+
|
|
310
|
+
if (seen.get(entry.id) > 1) {
|
|
311
|
+
return invalidNode(id, `duplicate id "${entry.id}" — ids must be unique within tests[]`, entry);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return makeNode({
|
|
315
|
+
id: entry.id,
|
|
316
|
+
command: entry.command,
|
|
317
|
+
cwd: typeof entry.cwd === 'string' ? entry.cwd.trim() : '.',
|
|
318
|
+
runner: entry.runner ?? inferRunner(entry.command),
|
|
319
|
+
expectedCount: entry.expectedCount ?? null,
|
|
320
|
+
timeoutMs: entry.timeoutMs ?? null,
|
|
321
|
+
build: entry.build ?? [],
|
|
322
|
+
e2e: entry.e2e ?? false,
|
|
323
|
+
linuxImage: entry.linuxImage ?? null,
|
|
324
|
+
evals: entry.evals ?? null,
|
|
325
|
+
projectCount: 1,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Resolve every test suite of a project, in the order the verifier must run them.
|
|
332
|
+
*
|
|
333
|
+
* Precedence — the whole point is that the last three steps still work with NO
|
|
334
|
+
* config at all, because this repository (the framework source) has none and
|
|
335
|
+
* every project installed before this feature has none either:
|
|
336
|
+
*
|
|
337
|
+
* 1. `project.tests[]` declared → one node per entry, source 'config-tests'
|
|
338
|
+
* 2. `project.testCommand` declared → ONE synthesized node, source 'config'
|
|
339
|
+
* 3. the four-step fallback of → ONE synthesized node, source
|
|
340
|
+
* resolveTestPlan() (root .sln, 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'
|
|
341
|
+
* *Tests.csproj scan, npm test)
|
|
342
|
+
*
|
|
343
|
+
* Steps 2 and 3 are `resolveTestPlan()` itself — CALLED, never re-implemented.
|
|
344
|
+
* That matters twice over: it is the only place that knows the fallback order,
|
|
345
|
+
* and it is the only place that excludes linked worktrees from the
|
|
346
|
+
* `*Tests.csproj` scan (a worktree is another feature's checkout; scanning it
|
|
347
|
+
* doubles the clock and reports on code outside the tree being judged).
|
|
348
|
+
*
|
|
349
|
+
* The list is never empty: "nothing to run" is expressed as ONE node already
|
|
350
|
+
* decided as `skip`, so the reason travels to the report instead of vanishing.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
353
|
+
* @returns {{ nodes: TestNode[], source: string }}
|
|
354
|
+
*/
|
|
355
|
+
export function resolveTestNodes(projectPath = '.') {
|
|
356
|
+
const config = readMorphConfig(projectPath);
|
|
357
|
+
|
|
358
|
+
// `tests` present in ANY shape is a declaration of intent. Even a malformed
|
|
359
|
+
// one: falling back from a typo is how F30 comes back.
|
|
360
|
+
if (config?.project?.tests !== undefined) {
|
|
361
|
+
const nodes = validateTestNodes(config, projectPath);
|
|
362
|
+
if (Array.isArray(config.project.tests) && config.project.tests.length === 0) {
|
|
363
|
+
// `[]` is an explicit "this project declares no test suites" — distinct
|
|
364
|
+
// from an absent key, which means "guess for me".
|
|
365
|
+
return {
|
|
366
|
+
nodes: [makeNode({ id: 'none', status: 'skip', reason: 'no test nodes declared' })],
|
|
367
|
+
source: 'config-tests',
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return { nodes, source: 'config-tests' };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const plan = resolveTestPlan(projectPath);
|
|
374
|
+
|
|
375
|
+
if (!plan.command) {
|
|
376
|
+
return {
|
|
377
|
+
nodes: [makeNode({ id: FALLBACK_NODE_ID, status: 'skip', reason: NO_SUITE_REASON })],
|
|
378
|
+
source: plan.source,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
nodes: [makeNode({
|
|
384
|
+
id: FALLBACK_NODE_ID,
|
|
385
|
+
command: plan.command,
|
|
386
|
+
cwd: '.',
|
|
387
|
+
projectCount: plan.projectCount,
|
|
388
|
+
})],
|
|
389
|
+
source: plan.source,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Path of the config the baseline lives in. One spelling, so the writer and
|
|
395
|
+
* `readMorphConfig()` can never disagree about which file is meant — and that
|
|
396
|
+
* is now literally true: both derive from `CONFIG_FILE_NAME`
|
|
397
|
+
* (`core/state/state-manager.js`), the same constant `state-manager.js` itself
|
|
398
|
+
* reads the project identity through. It used to be a promise made by a JSDoc
|
|
399
|
+
* over two hand-typed copies of the same string.
|
|
400
|
+
*/
|
|
401
|
+
export function morphConfigPath(projectPath = '.') {
|
|
402
|
+
return join(projectPath, CONFIG_FILE_NAME);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Write measured `expectedCount` values back into `project.tests[]`.
|
|
407
|
+
*
|
|
408
|
+
* WHY THIS IS A FUNCTION AND NOT AN EDIT (D8)
|
|
409
|
+
*
|
|
410
|
+
* `MORPH.md` says a baseline is a COMMAND, never a number — and this feature
|
|
411
|
+
* stores a number. The rule survives because nobody is allowed to type it: the
|
|
412
|
+
* only writer is `verify --record-baseline`, and it writes what a real, green
|
|
413
|
+
* run measured. A hand-typed number describes what someone believed; the
|
|
414
|
+
* divergence between belief and the suite is exactly the failure the rule was
|
|
415
|
+
* written after ("mandate afirmando 2366 quando já eram 2375").
|
|
416
|
+
*
|
|
417
|
+
* The write is a POINTED mutation: read → set one field per declared node →
|
|
418
|
+
* atomic `tmp.{pid}` + rename via `atomicWriteJson` (the same door
|
|
419
|
+
* `state-manager.js` uses, not a second copy of it). Every other key of
|
|
420
|
+
* `config.json` survives, because this command owns one field and nothing else.
|
|
421
|
+
*
|
|
422
|
+
* It refuses rather than improvises:
|
|
423
|
+
* - a measurement whose `count` is not an integer (**null is UNKNOWN, never
|
|
424
|
+
* 0** — a written 0 would fabricate a drop against every later run);
|
|
425
|
+
* - a measured id that is not declared (appending a node would be inventing
|
|
426
|
+
* the declaration the command came to update);
|
|
427
|
+
* - a declared node with no measurement (a baseline recorded for part of a
|
|
428
|
+
* fleet says nothing true about the rest of it).
|
|
429
|
+
*
|
|
430
|
+
* @param {string} projectPath - Project root
|
|
431
|
+
* @param {Array<{id: string, count: number|null}>} measurements - One per node that ran
|
|
432
|
+
* @returns {{ recorded: boolean, reason: string|null, updates: Array<{id: string, from: number|null, to: number}>, path: string }}
|
|
433
|
+
*/
|
|
434
|
+
export function recordTestBaselines(projectPath, measurements) {
|
|
435
|
+
const path = morphConfigPath(projectPath);
|
|
436
|
+
const refuse = (reason) => ({ recorded: false, reason, updates: [], path });
|
|
437
|
+
|
|
438
|
+
if (!existsSync(path)) return refuse('não existe .morph/config/config.json — não há onde gravar');
|
|
439
|
+
|
|
440
|
+
let config;
|
|
441
|
+
try {
|
|
442
|
+
config = JSON.parse(readFileSync(path, 'utf8'));
|
|
443
|
+
} catch (err) {
|
|
444
|
+
return refuse(`config.json ilegível (${err.message})`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const declared = config?.project?.tests;
|
|
448
|
+
if (!Array.isArray(declared)) {
|
|
449
|
+
return refuse('config.json não declara project.tests[] — não há entrada onde gravar a baseline');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const unreadable = measurements.filter((m) => !Number.isInteger(m.count)).map((m) => m.id);
|
|
453
|
+
if (unreadable.length > 0) {
|
|
454
|
+
return refuse(`contagem desconhecida em: ${unreadable.join(', ')} — desconhecido nunca vira 0`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const byId = new Map(measurements.map((m) => [m.id, m.count]));
|
|
458
|
+
const undeclared = [...byId.keys()].filter(
|
|
459
|
+
(id) => !declared.some((entry) => entry && entry.id === id)
|
|
460
|
+
);
|
|
461
|
+
if (undeclared.length > 0) {
|
|
462
|
+
return refuse(`nó medido não declarado em project.tests[]: ${undeclared.join(', ')}`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const unmeasured = declared
|
|
466
|
+
.map((entry) => entry?.id)
|
|
467
|
+
.filter((id) => typeof id === 'string' && !byId.has(id));
|
|
468
|
+
if (unmeasured.length > 0) {
|
|
469
|
+
return refuse(`nó declarado sem medição: ${unmeasured.join(', ')} — baseline parcial não é baseline`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const updates = [];
|
|
473
|
+
for (const entry of declared) {
|
|
474
|
+
if (!entry || !byId.has(entry.id)) continue;
|
|
475
|
+
const to = byId.get(entry.id);
|
|
476
|
+
updates.push({
|
|
477
|
+
id: entry.id,
|
|
478
|
+
from: Number.isInteger(entry.expectedCount) ? entry.expectedCount : null,
|
|
479
|
+
to,
|
|
480
|
+
});
|
|
481
|
+
entry.expectedCount = to;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
atomicWriteJson(path, config);
|
|
485
|
+
return { recorded: true, reason: null, updates, path };
|
|
486
|
+
}
|