@polymorphism-tech/morph-spec 8.34.2 → 8.35.1
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 -5
- 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 +236 -11
- 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/core/worktree-env-inject.js +43 -0
- 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/payload-utils.js +15 -2
- 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 +148 -6
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +76 -4
- 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 +379 -24
- 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/git/pr-files.js +56 -0
- 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
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Test Runner —
|
|
2
|
+
* Test Runner — the FALLBACK resolver and the executor of a single test node.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Scope note: a project has a LIST of test suites, and that list is resolved by
|
|
5
|
+
* `test-nodes.js` (`resolveTestNodes()`), which calls `resolveTestPlan()` below
|
|
6
|
+
* as its last fallback. What lives here is (a) the four-step guess for a
|
|
7
|
+
* project that declared nothing, and (b) `runTestSuite()`, which runs ONE node.
|
|
8
|
+
* The plural/singular split matters: a runner that resolved its own single
|
|
9
|
+
* command is what let a whole second suite sit outside the gate (defect F30).
|
|
10
|
+
*
|
|
11
|
+
* Resolution order for the fallback test command:
|
|
5
12
|
* 1. .morph/config/config.json → project.testCommand
|
|
6
13
|
* 2. .sln or .csproj present at project root → "dotnet test"
|
|
7
14
|
* 3. Nested *Tests.csproj anywhere in the tree (monorepo without a root
|
|
8
|
-
* .sln) → "dotnet test <proj>
|
|
15
|
+
* .sln) → "dotnet test <proj> -v q" for each match
|
|
9
16
|
* 4. package.json present AND has scripts.test → "npm test"
|
|
10
17
|
* 5. null (no test suite detected)
|
|
11
18
|
*
|
|
@@ -26,6 +33,8 @@ import { join, resolve, relative, isAbsolute } from 'path';
|
|
|
26
33
|
import { globSync } from 'glob';
|
|
27
34
|
|
|
28
35
|
import { WORKTREES_DIR, LEGACY_WORKTREES_DIR } from '../../commands/project/worktree.js';
|
|
36
|
+
import { getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
|
|
37
|
+
import { CONFIG_FILE_NAME } from '../../core/state/state-manager.js';
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
40
|
* Timeout budget granted to ONE test project. The chained `a && b` command is
|
|
@@ -44,7 +53,7 @@ export const DEFAULT_TEST_TIMEOUT_PER_PROJECT = 120_000;
|
|
|
44
53
|
*/
|
|
45
54
|
export function resolveTestPlan(projectPath = '.') {
|
|
46
55
|
// 1. Read from .morph/config/config.json
|
|
47
|
-
const config =
|
|
56
|
+
const config = readMorphConfig(projectPath);
|
|
48
57
|
const configured = config?.project?.testCommand;
|
|
49
58
|
if (configured && typeof configured === 'string') {
|
|
50
59
|
return { command: configured, projectCount: 1, source: 'config' };
|
|
@@ -70,7 +79,24 @@ export function resolveTestPlan(projectPath = '.') {
|
|
|
70
79
|
const testProjects = collectDotnetTestProjects(projectPath);
|
|
71
80
|
if (testProjects.length > 0) {
|
|
72
81
|
return {
|
|
73
|
-
|
|
82
|
+
// NO `--nologo`, and that omission is load-bearing. `--nologo` is a
|
|
83
|
+
// VSTest-MODE option of `dotnet test`. A project that opts into
|
|
84
|
+
// Microsoft.Testing.Platform (`"test": {"runner":
|
|
85
|
+
// "Microsoft.Testing.Platform"}` in global.json — what xunit.v3 4.x on
|
|
86
|
+
// .NET 10 requires, and what the ai-agents standards prescribe) runs
|
|
87
|
+
// `dotnet test` in MTP MODE, where `--nologo` is unknown and is forwarded
|
|
88
|
+
// verbatim to the test application. The application answers
|
|
89
|
+
// `error: unknown option: --nologo` and the session dies with MTP exit
|
|
90
|
+
// code 5 (InvalidCommandLine) reporting ZERO tests — a red `tests` node
|
|
91
|
+
// that looks exactly like a broken suite while every test passes.
|
|
92
|
+
// Measured on framework/templates/dotnet/ai-kit (2026-09-08): with the
|
|
93
|
+
// flag, "Zero testes executados" / exit 5; without it, 115 passed / exit 0.
|
|
94
|
+
// The rest of the command is mode-agnostic on purpose: the POSITIONAL
|
|
95
|
+
// project path and `-v q` are accepted by both modes, so this command
|
|
96
|
+
// stays correct whether the target project is VSTest or MTP. Do not
|
|
97
|
+
// "modernize" the path to `--project`: that spelling exists only in MTP
|
|
98
|
+
// mode and would break every VSTest project instead.
|
|
99
|
+
command: testProjects.map((p) => `dotnet test "${p}" -v q`).join(' && '),
|
|
74
100
|
projectCount: testProjects.length,
|
|
75
101
|
source: 'dotnet-scan',
|
|
76
102
|
};
|
|
@@ -93,16 +119,6 @@ export function resolveTestPlan(projectPath = '.') {
|
|
|
93
119
|
return { command: null, projectCount: 0, source: 'none' };
|
|
94
120
|
}
|
|
95
121
|
|
|
96
|
-
/**
|
|
97
|
-
* Resolve the test command for a project.
|
|
98
|
-
*
|
|
99
|
-
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
100
|
-
* @returns {string | null} The test command to run, or null if none found
|
|
101
|
-
*/
|
|
102
|
-
export function resolveTestCommand(projectPath = '.') {
|
|
103
|
-
return resolveTestPlan(projectPath).command;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
122
|
/**
|
|
107
123
|
* Find the *Tests.csproj files that belong to THIS tree — never to a linked
|
|
108
124
|
* worktree of the same repo.
|
|
@@ -192,14 +208,34 @@ function listLinkedWorktreePaths(projectPath) {
|
|
|
192
208
|
.filter((p) => p && p !== self);
|
|
193
209
|
}
|
|
194
210
|
|
|
195
|
-
/**
|
|
196
|
-
|
|
211
|
+
/**
|
|
212
|
+
* True when `child` is `parent` or lives under it.
|
|
213
|
+
*
|
|
214
|
+
* Exported because test-nodes.js asks the SAME question of a declared `cwd`
|
|
215
|
+
* ("does this node point outside the tree under verification?"). One spelling
|
|
216
|
+
* of containment, one place to fix it.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} child
|
|
219
|
+
* @param {string} parent
|
|
220
|
+
* @returns {boolean}
|
|
221
|
+
*/
|
|
222
|
+
export function isInside(child, parent) {
|
|
197
223
|
const rel = relative(parent, child);
|
|
198
224
|
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
199
225
|
}
|
|
200
226
|
|
|
201
|
-
|
|
202
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Read `.morph/config/config.json`, or null when it is absent or malformed.
|
|
229
|
+
*
|
|
230
|
+
* Exported so test-nodes.js reads the config through the SAME door: a second
|
|
231
|
+
* reader would be a second place for "absent" and "malformed" to drift apart —
|
|
232
|
+
* and both must keep meaning "fall back", never "fail".
|
|
233
|
+
*
|
|
234
|
+
* @param {string} projectPath
|
|
235
|
+
* @returns {object|null}
|
|
236
|
+
*/
|
|
237
|
+
export function readMorphConfig(projectPath) {
|
|
238
|
+
const configPath = join(projectPath, CONFIG_FILE_NAME);
|
|
203
239
|
if (!existsSync(configPath)) return null;
|
|
204
240
|
try {
|
|
205
241
|
return JSON.parse(readFileSync(configPath, 'utf8'));
|
|
@@ -210,24 +246,32 @@ function readConfig(projectPath) {
|
|
|
210
246
|
}
|
|
211
247
|
|
|
212
248
|
/**
|
|
213
|
-
* Resolve the timeout budget for
|
|
249
|
+
* Resolve the timeout budget for ONE test node.
|
|
214
250
|
*
|
|
215
|
-
* Precedence: explicit override (`morph-spec verify --timeout`) >
|
|
216
|
-
* `config.project.testTimeoutMs` > default scaled by the
|
|
217
|
-
* test projects.
|
|
251
|
+
* Precedence: explicit override (`morph-spec verify --timeout`) > the node's
|
|
252
|
+
* own `timeoutMs` > `config.project.testTimeoutMs` > default scaled by the
|
|
253
|
+
* number of test projects the node's command chains.
|
|
254
|
+
*
|
|
255
|
+
* The per-node ceiling sits above the project-wide one because the derived
|
|
256
|
+
* default (120s x chained projects) is shaped for `dotnet test`: it is hostile
|
|
257
|
+
* to a Node suite that takes twelve minutes, and a project with both runners
|
|
258
|
+
* cannot serve them with a single number. It sits BELOW the CLI override
|
|
259
|
+
* because what the user typed is the most explicit statement in the room.
|
|
218
260
|
*
|
|
219
261
|
* @param {string} projectPath
|
|
220
|
-
* @param {
|
|
262
|
+
* @param {import('./test-nodes.js').TestNode} node - The node about to run
|
|
221
263
|
* @param {number} [override] - Explicit override in ms, if the caller has one
|
|
222
264
|
* @returns {number} Timeout in ms
|
|
223
265
|
*/
|
|
224
|
-
export function resolveTestTimeout(projectPath,
|
|
266
|
+
export function resolveTestTimeout(projectPath, node, override) {
|
|
225
267
|
if (Number.isFinite(override) && override > 0) return override;
|
|
226
268
|
|
|
227
|
-
|
|
269
|
+
if (Number.isFinite(node?.timeoutMs) && node.timeoutMs > 0) return node.timeoutMs;
|
|
270
|
+
|
|
271
|
+
const configured = readMorphConfig(projectPath)?.project?.testTimeoutMs;
|
|
228
272
|
if (Number.isFinite(configured) && configured > 0) return configured;
|
|
229
273
|
|
|
230
|
-
return DEFAULT_TEST_TIMEOUT_PER_PROJECT * Math.max(1, projectCount);
|
|
274
|
+
return DEFAULT_TEST_TIMEOUT_PER_PROJECT * Math.max(1, node?.projectCount ?? 1);
|
|
231
275
|
}
|
|
232
276
|
|
|
233
277
|
/**
|
|
@@ -242,31 +286,364 @@ function countCompletedRuns(output) {
|
|
|
242
286
|
return matches ? matches.length : 0;
|
|
243
287
|
}
|
|
244
288
|
|
|
289
|
+
// ============================================================================
|
|
290
|
+
// Prebuild of the test projects, and the `--no-build` refusal (T5 / UC-08)
|
|
291
|
+
// ============================================================================
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* `--no-build` as a standalone token. Anchored on whitespace so `--no-build-x`
|
|
295
|
+
* (or a path that happens to contain the word) never matches.
|
|
296
|
+
*/
|
|
297
|
+
const NO_BUILD_FLAG = /(^|\s)--no-build(\s|$)/;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The `.csproj` targets a command names, quotes removed and duplicates dropped.
|
|
301
|
+
*
|
|
302
|
+
* Finding none yields `[]`, and `[]` is the SAFE answer, not a shrug: it means
|
|
303
|
+
* nothing was compiled here, which is precisely the condition that makes a
|
|
304
|
+
* `--no-build` in the same command a refusal. Refusing is cheap; approving a
|
|
305
|
+
* stale assembly is what F29 cost.
|
|
306
|
+
*
|
|
307
|
+
* @param {string|null|undefined} command
|
|
308
|
+
* @returns {string[]}
|
|
309
|
+
*/
|
|
310
|
+
export function extractCsprojTargets(command) {
|
|
311
|
+
if (typeof command !== 'string') return [];
|
|
312
|
+
const found = [];
|
|
313
|
+
const token = /"([^"]+\.csproj)"|'([^']+\.csproj)'|(\S+\.csproj)/gi;
|
|
314
|
+
let match;
|
|
315
|
+
while ((match = token.exec(command)) !== null) {
|
|
316
|
+
found.push(match[1] ?? match[2] ?? match[3]);
|
|
317
|
+
}
|
|
318
|
+
return [...new Set(found)];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Which test projects this node compiles before running: what it DECLARED, or
|
|
323
|
+
* else what its command names. Only `runner: 'dotnet'` has a prebuild at all —
|
|
324
|
+
* a vitest node has nothing to compile, and pretending otherwise would invent
|
|
325
|
+
* a build step nobody asked for.
|
|
326
|
+
*
|
|
327
|
+
* @param {import('./test-nodes.js').TestNode} node
|
|
328
|
+
* @returns {string[]}
|
|
329
|
+
*/
|
|
330
|
+
export function resolveTestBuildTargets(node) {
|
|
331
|
+
if (node?.runner !== 'dotnet') return [];
|
|
332
|
+
if (Array.isArray(node.build) && node.build.length > 0) return node.build;
|
|
333
|
+
return extractCsprojTargets(node.command);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* `dotnet build "<target>" …`, built from the SAME step build-check.js
|
|
338
|
+
* resolves for a .NET stack — the string "dotnet build" is not typed a second
|
|
339
|
+
* time anywhere in this repository.
|
|
340
|
+
*
|
|
341
|
+
* `--no-restore` is deliberately dropped: it is correct for the ROOT build
|
|
342
|
+
* (whose restore already happened) and wrong here, because the whole reason a
|
|
343
|
+
* test project needs its own prebuild is that it may live OUTSIDE the
|
|
344
|
+
* application's solution and may therefore never have been restored at all.
|
|
345
|
+
* F29's project was exactly that project.
|
|
346
|
+
*/
|
|
347
|
+
function dotnetBuildCommandFor(target, projectPath) {
|
|
348
|
+
const base = getBuildSteps('dotnet', { cwd: projectPath || '.' })[0]?.command;
|
|
349
|
+
if (!base) return null;
|
|
350
|
+
const [verb, subcommand, ...flags] = base.split(/\s+/);
|
|
351
|
+
const kept = flags.filter((flag) => flag !== '--no-restore');
|
|
352
|
+
return `${verb} ${subcommand} "${target}"${kept.length ? ` ${kept.join(' ')}` : ''}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Compile this node's test projects, and report whether a build ACTUALLY
|
|
357
|
+
* happened in this invocation — the fact the `--no-build` refusal keys on.
|
|
358
|
+
*
|
|
359
|
+
* WHEN it runs: when the node declared `build[]` (an explicit statement that
|
|
360
|
+
* these projects need compiling), or when the command carries `--no-build`
|
|
361
|
+
* (the only case where the binary would otherwise be whatever the last run
|
|
362
|
+
* left behind). A plain `dotnet test X.csproj` is NOT pre-compiled: `dotnet
|
|
363
|
+
* test` builds it itself and a compile error already reddens the node, so
|
|
364
|
+
* pre-compiling would only double the clock of the hot path — and the clock is
|
|
365
|
+
* what D13 is about.
|
|
366
|
+
*
|
|
367
|
+
* @returns {{ built: boolean, failure: object|null }} `failure` is a full
|
|
368
|
+
* runTestSuite result, ready to be returned as-is.
|
|
369
|
+
*/
|
|
370
|
+
function runTestPrebuild({ node, cwd, exec, timeout, projectPath }) {
|
|
371
|
+
if (node.runner !== 'dotnet') return { built: false, failure: null };
|
|
372
|
+
|
|
373
|
+
const declared = Array.isArray(node.build) ? node.build : [];
|
|
374
|
+
if (declared.length === 0 && !NO_BUILD_FLAG.test(node.command)) {
|
|
375
|
+
return { built: false, failure: null };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const targets = resolveTestBuildTargets(node);
|
|
379
|
+
const outputs = [];
|
|
380
|
+
|
|
381
|
+
for (const target of targets) {
|
|
382
|
+
const command = dotnetBuildCommandFor(target, projectPath);
|
|
383
|
+
if (!command) return { built: false, failure: null };
|
|
384
|
+
try {
|
|
385
|
+
exec(command, { cwd, timeout, encoding: 'utf8', stdio: 'pipe' });
|
|
386
|
+
} catch (err) {
|
|
387
|
+
outputs.push(`[${command}]\n${(err.stdout || '') + (err.stderr || '')}`);
|
|
388
|
+
return {
|
|
389
|
+
built: false,
|
|
390
|
+
failure: {
|
|
391
|
+
passed: false,
|
|
392
|
+
buildFailed: true,
|
|
393
|
+
reason: 'test-build-failed',
|
|
394
|
+
output: outputs.join('\n\n'),
|
|
395
|
+
exitCode: err.status ?? 1,
|
|
396
|
+
timeoutMs: timeout,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return { built: targets.length > 0, failure: null };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ============================================================================
|
|
406
|
+
// How many tests actually ran (T4 / UC-08)
|
|
407
|
+
// ============================================================================
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Sum every `Total:`/`Aprovado:`/`Com falha:` summary `dotnet test` printed.
|
|
411
|
+
*
|
|
412
|
+
* SUM, not "the last one": a chained `a && b` command prints one summary per
|
|
413
|
+
* project, and reading a single line would report half a suite as the whole of
|
|
414
|
+
* it — which, against a recorded baseline, is an invented count drop.
|
|
415
|
+
* `countCompletedRuns()` above counts the same lines for a different question
|
|
416
|
+
* (WHICH link was running when the clock died); this one reads their numbers.
|
|
417
|
+
*/
|
|
418
|
+
function parseDotnetCount(output) {
|
|
419
|
+
const lines = output.split('\n').filter((l) => /^\s*(?:Aprovado|Passed|Com falha|Failed|Reprovado|Erro)!/i.test(l));
|
|
420
|
+
let seen = false;
|
|
421
|
+
const sum = { total: 0, passed: 0, failed: 0 };
|
|
422
|
+
|
|
423
|
+
for (const line of lines) {
|
|
424
|
+
const total = /\b(?:Total de testes|Total)\s*:\s*(\d+)/i.exec(line);
|
|
425
|
+
if (!total) continue;
|
|
426
|
+
seen = true;
|
|
427
|
+
sum.total += Number(total[1]);
|
|
428
|
+
const passed = /\b(?:Aprovado|Passed)\s*:\s*(\d+)/i.exec(line);
|
|
429
|
+
const failed = /\b(?:Com falha|Failed|Reprovado)\s*:\s*(\d+)/i.exec(line);
|
|
430
|
+
sum.passed += passed ? Number(passed[1]) : 0;
|
|
431
|
+
sum.failed += failed ? Number(failed[1]) : 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return seen ? sum : null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Last value of a `# key N` (TAP) or `ℹ key N` (spec reporter) summary line. */
|
|
438
|
+
function lastSummaryValue(output, key) {
|
|
439
|
+
const re = new RegExp(`^\\s*[#ℹ]\\s*${key}\\s+(\\d+)\\s*$`, 'gim');
|
|
440
|
+
let match;
|
|
441
|
+
let value = null;
|
|
442
|
+
while ((match = re.exec(output)) !== null) value = Number(match[1]);
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** `node --test`, in either of the two reporters the runner may pick. */
|
|
447
|
+
function parseNodeTestCount(output) {
|
|
448
|
+
const total = lastSummaryValue(output, 'tests');
|
|
449
|
+
if (total === null) return null;
|
|
450
|
+
return {
|
|
451
|
+
total,
|
|
452
|
+
passed: lastSummaryValue(output, 'pass') ?? 0,
|
|
453
|
+
failed: lastSummaryValue(output, 'fail') ?? 0,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Vitest: `Tests 3082 passed (3082)` / `Tests 1 failed | 3081 passed (3082)`.
|
|
459
|
+
*
|
|
460
|
+
* The line must start with `Tests`, never `Test Files` — the file count sitting
|
|
461
|
+
* right above it has the same shape, and reading it would report 27 where 3082
|
|
462
|
+
* ran. A fabricated drop is worse than no count at all.
|
|
463
|
+
*/
|
|
464
|
+
function parseVitestCount(output) {
|
|
465
|
+
const line = output.split('\n').find((l) => /^\s*Tests\s{1,}\d/.test(l));
|
|
466
|
+
if (!line) return null;
|
|
467
|
+
const passed = /(\d+)\s+passed/i.exec(line);
|
|
468
|
+
const failed = /(\d+)\s+failed/i.exec(line);
|
|
469
|
+
if (!passed && !failed) return null;
|
|
470
|
+
const declaredTotal = /\((\d+)\)\s*$/.exec(line.trim());
|
|
471
|
+
const p = passed ? Number(passed[1]) : 0;
|
|
472
|
+
const f = failed ? Number(failed[1]) : 0;
|
|
473
|
+
return { total: declaredTotal ? Number(declaredTotal[1]) : p + f, passed: p, failed: f };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Jest: `Tests: 2 failed, 118 passed, 120 total`. */
|
|
477
|
+
function parseJestCount(output) {
|
|
478
|
+
const line = output.split('\n').find((l) => /^\s*Tests:\s/.test(l));
|
|
479
|
+
if (!line) return null;
|
|
480
|
+
const total = /(\d+)\s+total/i.exec(line);
|
|
481
|
+
if (!total) return null;
|
|
482
|
+
const passed = /(\d+)\s+passed/i.exec(line);
|
|
483
|
+
const failed = /(\d+)\s+failed/i.exec(line);
|
|
484
|
+
return {
|
|
485
|
+
total: Number(total[1]),
|
|
486
|
+
passed: passed ? Number(passed[1]) : 0,
|
|
487
|
+
failed: failed ? Number(failed[1]) : 0,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Playwright: `8 passed (12.4s)` and/or `2 failed`, on their own lines. */
|
|
492
|
+
function parsePlaywrightCount(output) {
|
|
493
|
+
const passed = /^\s*(\d+)\s+passed\b/im.exec(output);
|
|
494
|
+
const failed = /^\s*(\d+)\s+failed\b/im.exec(output);
|
|
495
|
+
if (!passed && !failed) return null;
|
|
496
|
+
const p = passed ? Number(passed[1]) : 0;
|
|
497
|
+
const f = failed ? Number(failed[1]) : 0;
|
|
498
|
+
return { total: p + f, passed: p, failed: f };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** One extractor per runner. An absent entry is an honest "I cannot read this". */
|
|
502
|
+
const COUNT_EXTRACTORS = {
|
|
503
|
+
dotnet: parseDotnetCount,
|
|
504
|
+
// An eval IS `dotnet test`, so it reads the same summary. No new extractor is
|
|
505
|
+
// invented here: the authoritative number for an eval node is the `scenarios`
|
|
506
|
+
// field of the fixture's summary FILE, which this function cannot see (it only
|
|
507
|
+
// gets the output). `verify-runner` prefers that file and falls back to this —
|
|
508
|
+
// and both paths keep `null` meaning UNKNOWN, never zero.
|
|
509
|
+
evals: parseDotnetCount,
|
|
510
|
+
'node-test': parseNodeTestCount,
|
|
511
|
+
vitest: parseVitestCount,
|
|
512
|
+
jest: parseJestCount,
|
|
513
|
+
playwright: parsePlaywrightCount,
|
|
514
|
+
none: () => null,
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* How many tests actually ran, read out of the runner's own summary.
|
|
519
|
+
*
|
|
520
|
+
* @param {string|null|undefined} output - Combined stdout+stderr of the run
|
|
521
|
+
* @param {string} runner - One of TEST_RUNNERS (see test-nodes.js)
|
|
522
|
+
* @returns {{ total: number, passed: number, failed: number }|null}
|
|
523
|
+
* **null is UNKNOWN, never zero.** A summary nobody could read is not a
|
|
524
|
+
* suite that ran zero tests: collapsing the two would manufacture a total
|
|
525
|
+
* count drop against every recorded baseline out of a missing regex. Same
|
|
526
|
+
* doctrine as `null` in pr-index.js.
|
|
527
|
+
*/
|
|
528
|
+
export function parseTestCount(output, runner) {
|
|
529
|
+
if (typeof output !== 'string' || !output) return null;
|
|
530
|
+
const extractor = COUNT_EXTRACTORS[runner];
|
|
531
|
+
if (!extractor) return null;
|
|
532
|
+
return extractor(output);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Compare an executed count against the recorded baseline — asymmetrically,
|
|
537
|
+
* and on purpose (D7).
|
|
538
|
+
*
|
|
539
|
+
* A count that DROPS is a finding: tests that used to run no longer do, which
|
|
540
|
+
* is exactly the silent damage this feature exists to catch. A count that
|
|
541
|
+
* RISES is the normal life of a suite that grows with every task. A symmetric
|
|
542
|
+
* rule would turn each new test into a red gate and would teach the agent to
|
|
543
|
+
* delete tests in order to "fix" it — the lesson MORPH.md already paid for
|
|
544
|
+
* ("mandate afirmando 2366 quando já eram 2375").
|
|
545
|
+
*
|
|
546
|
+
* @param {number|null} count - Executed count; null is UNKNOWN
|
|
547
|
+
* @param {number|null} expectedCount - Recorded baseline; null is "none recorded"
|
|
548
|
+
* @returns {{ verdict: 'drop'|'rise'|'match'|'unknown'|'no-baseline', reason: string|null, warning: string|null }}
|
|
549
|
+
* `reason` is non-null ONLY for a drop — it is the only verdict that fails a
|
|
550
|
+
* node by itself. Everything else leaves the verdict to the exit code.
|
|
551
|
+
*/
|
|
552
|
+
export function compareTestCount(count, expectedCount) {
|
|
553
|
+
const answer = (verdict, reason = null, warning = null) => ({ verdict, reason, warning });
|
|
554
|
+
|
|
555
|
+
// `null` is "no baseline recorded"; a declared `0` is a real baseline. The
|
|
556
|
+
// distinction has to survive here or "this suite runs zero tests" becomes
|
|
557
|
+
// inexpressible.
|
|
558
|
+
if (!Number.isInteger(expectedCount)) return answer('no-baseline');
|
|
559
|
+
|
|
560
|
+
if (!Number.isInteger(count)) {
|
|
561
|
+
return answer('unknown', null,
|
|
562
|
+
`contagem desconhecida — a baseline ${expectedCount} não pôde ser comparada`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (count < expectedCount) {
|
|
566
|
+
return answer('drop', `count-drop: esperado ${expectedCount}, executado ${count}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (count > expectedCount) {
|
|
570
|
+
return answer('rise', null,
|
|
571
|
+
`baseline desatualizada (${expectedCount} → ${count}); rode verify --record-baseline`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return answer('match');
|
|
575
|
+
}
|
|
576
|
+
|
|
245
577
|
/**
|
|
246
|
-
* Run
|
|
578
|
+
* Run ONE test node.
|
|
579
|
+
*
|
|
580
|
+
* The node is a parameter rather than something resolved here: a project has a
|
|
581
|
+
* LIST of suites (see test-nodes.js), and a runner that resolved its own single
|
|
582
|
+
* command is precisely what let 3.082 vitest tests sit outside the gate (F30).
|
|
583
|
+
* Resolution belongs to `resolveTestNodes()`; execution belongs here. One node,
|
|
584
|
+
* one command, one cwd, one clock.
|
|
247
585
|
*
|
|
248
586
|
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
249
587
|
* @param {object} [opts]
|
|
588
|
+
* @param {import('./test-nodes.js').TestNode} opts.node - The node to run; its
|
|
589
|
+
* `cwd` is resolved against `projectPath`
|
|
250
590
|
* @param {number} [opts.timeout] - Explicit timeout in ms (highest precedence)
|
|
591
|
+
* @param {Record<string,string>} [opts.env] - Extra environment for THIS node, layered
|
|
592
|
+
* over `process.env`. The eval runner's channel (`MORPH_EVAL_*`) travels here.
|
|
593
|
+
* Absent leaves the child on the parent's environment, byte for byte — every
|
|
594
|
+
* other runner keeps the exact behaviour it had.
|
|
251
595
|
* @param {Function} [opts._execSync] - Override execSync for unit testing
|
|
252
596
|
* @returns {{ passed?: boolean, output?: string, exitCode?: number, skipped?: boolean, reason?: string, timedOut?: boolean, timeoutMs?: number, message?: string }}
|
|
253
597
|
*/
|
|
254
598
|
export function runTestSuite(projectPath = '.', opts = {}) {
|
|
255
|
-
const
|
|
599
|
+
const node = opts.node;
|
|
600
|
+
const cmd = node?.command;
|
|
256
601
|
|
|
257
602
|
if (!cmd) {
|
|
258
|
-
return {
|
|
603
|
+
return {
|
|
604
|
+
skipped: true,
|
|
605
|
+
reason: node?.reason || 'No testCommand configured and no known project file detected',
|
|
606
|
+
};
|
|
259
607
|
}
|
|
260
608
|
|
|
261
609
|
const exec = opts._execSync ?? execSync;
|
|
262
|
-
const timeout = resolveTestTimeout(projectPath,
|
|
610
|
+
const timeout = resolveTestTimeout(projectPath, node, opts.timeout);
|
|
611
|
+
const projectCount = node.projectCount ?? 1;
|
|
612
|
+
const cwd = resolve(projectPath, node.cwd || '.');
|
|
613
|
+
|
|
614
|
+
// Compile the test projects FIRST, then decide whether `--no-build` has
|
|
615
|
+
// anything behind it. Order matters: the build is what makes the flag legal.
|
|
616
|
+
const prebuild = runTestPrebuild({ node, cwd, exec, timeout, projectPath });
|
|
617
|
+
if (prebuild.failure) return prebuild.failure;
|
|
618
|
+
|
|
619
|
+
// F29: `verify` reported `tests: pass` while the test csproj did not compile,
|
|
620
|
+
// because `--no-build` ran last build's assembly and seven new tests never
|
|
621
|
+
// executed. The refusal keys on whether a build HAPPENED here, never on the
|
|
622
|
+
// flag alone — `--no-build` after a real build is the fast path (D11). The
|
|
623
|
+
// command itself is never rewritten: a silent fix would hide the config
|
|
624
|
+
// defect and make the reported `command` a lie about what ran.
|
|
625
|
+
if (node.runner === 'dotnet' && NO_BUILD_FLAG.test(cmd) && !prebuild.built) {
|
|
626
|
+
return {
|
|
627
|
+
passed: false,
|
|
628
|
+
buildFailed: false,
|
|
629
|
+
reason: 'stale-binary: --no-build sem build dos projetos de teste nesta invocação',
|
|
630
|
+
output: '',
|
|
631
|
+
exitCode: null,
|
|
632
|
+
timeoutMs: timeout,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Layered OVER process.env, never replacing it: a child that lost PATH would
|
|
637
|
+
// fail in a way that has nothing to do with the suite under test.
|
|
638
|
+
const env = opts.env ? { ...process.env, ...opts.env } : undefined;
|
|
263
639
|
|
|
264
640
|
try {
|
|
265
641
|
const output = exec(cmd, {
|
|
266
|
-
cwd
|
|
642
|
+
cwd,
|
|
267
643
|
timeout,
|
|
268
644
|
encoding: 'utf8',
|
|
269
|
-
stdio: 'pipe'
|
|
645
|
+
stdio: 'pipe',
|
|
646
|
+
...(env ? { env } : {}),
|
|
270
647
|
});
|
|
271
648
|
return { passed: true, output: output ?? '', exitCode: 0, timeoutMs: timeout };
|
|
272
649
|
} catch (err) {
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checkpoint Validation Persistence
|
|
3
|
-
*
|
|
4
|
-
* Persists checkpoint validation results to disk for traceability.
|
|
5
|
-
* Each checkpoint writes a JSON file to:
|
|
6
|
-
* .morph/features/{feature}/3-implement/validations/checkpoint-{N}.json
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
10
|
-
import { join } from 'path';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Persist a checkpoint validation result to disk.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} projectPath - Project root
|
|
16
|
-
* @param {string} featureName - Feature name
|
|
17
|
-
* @param {{ checkpointNum: number, timestamp: string, passed: boolean, validators: Object, summary: string, feature?: string, phase?: string }} result - Validation result
|
|
18
|
-
* @returns {string} Path to the saved JSON file
|
|
19
|
-
*/
|
|
20
|
-
export function persistCheckpointValidation(projectPath, featureName, result) {
|
|
21
|
-
const dir = join(projectPath, `.morph/features/${featureName}/3-implement/validations`);
|
|
22
|
-
mkdirSync(dir, { recursive: true });
|
|
23
|
-
|
|
24
|
-
const filename = `checkpoint-${result.checkpointNum}.json`;
|
|
25
|
-
const filePath = join(dir, filename);
|
|
26
|
-
|
|
27
|
-
const output = {
|
|
28
|
-
checkpointNum: result.checkpointNum,
|
|
29
|
-
timestamp: result.timestamp || new Date().toISOString(),
|
|
30
|
-
passed: result.passed,
|
|
31
|
-
validators: result.validators || {},
|
|
32
|
-
summary: result.summary || '',
|
|
33
|
-
feature: result.feature || featureName,
|
|
34
|
-
phase: result.phase || 'implement',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf8');
|
|
38
|
-
return filePath;
|
|
39
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint Validation Persistence
|
|
3
|
+
*
|
|
4
|
+
* Persists checkpoint validation results to disk for traceability.
|
|
5
|
+
* Each checkpoint writes a JSON file to:
|
|
6
|
+
* .morph/features/{feature}/3-implement/validations/checkpoint-{N}.json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Persist a checkpoint validation result to disk.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} projectPath - Project root
|
|
16
|
+
* @param {string} featureName - Feature name
|
|
17
|
+
* @param {{ checkpointNum: number, timestamp: string, passed: boolean, validators: Object, summary: string, feature?: string, phase?: string }} result - Validation result
|
|
18
|
+
* @returns {string} Path to the saved JSON file
|
|
19
|
+
*/
|
|
20
|
+
export function persistCheckpointValidation(projectPath, featureName, result) {
|
|
21
|
+
const dir = join(projectPath, `.morph/features/${featureName}/3-implement/validations`);
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
const filename = `checkpoint-${result.checkpointNum}.json`;
|
|
25
|
+
const filePath = join(dir, filename);
|
|
26
|
+
|
|
27
|
+
const output = {
|
|
28
|
+
checkpointNum: result.checkpointNum,
|
|
29
|
+
timestamp: result.timestamp || new Date().toISOString(),
|
|
30
|
+
passed: result.passed,
|
|
31
|
+
validators: result.validators || {},
|
|
32
|
+
summary: result.summary || '',
|
|
33
|
+
feature: result.feature || featureName,
|
|
34
|
+
phase: result.phase || 'implement',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf8');
|
|
38
|
+
return filePath;
|
|
39
|
+
}
|