@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 +51 -43
- package/README.md +39 -38
- package/bin/morph-spec.js +41 -113
- package/bin/validate.js +1 -147
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +86 -21
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-status.md +4 -4
- package/framework/docs/CLI.md +574 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +3 -17
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
- package/framework/hooks/claude-code/core/gate-guard.js +9 -78
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
- package/framework/hooks/claude-code/core/state-sync.js +15 -85
- package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
- package/framework/hooks/claude-code/core/validate-completion.js +37 -147
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
- package/framework/hooks/claude-code/core/worktree-context.js +15 -114
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +11 -76
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +168 -44
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +11 -43
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +59 -0
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +4 -0
- package/framework/hooks/shared/gate-question.js +12 -0
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +14 -72
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +99 -0
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +51 -247
- package/framework/hooks/shared/state-writer.js +9 -54
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +94 -152
- package/framework/standards/STANDARDS.json +109 -1219
- package/framework/standards/STANDARDS.meta.json +1319 -0
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +3 -3
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +7 -7
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +15 -179
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +18 -105
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +17 -111
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +53 -307
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +6 -35
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +8 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +65 -72
- package/src/commands/project/status.js +28 -121
- package/src/commands/project/update.js +31 -223
- package/src/commands/project/worktree.js +109 -883
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +65 -120
- package/src/commands/state/approve.js +87 -229
- package/src/commands/state/archive.js +23 -139
- package/src/commands/state/auto-approval.js +65 -0
- package/src/commands/state/create.js +49 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +105 -494
- package/src/commands/state/gate-check.js +74 -183
- package/src/commands/state/gate-decision.js +8 -42
- package/src/commands/state/handoff.js +4 -31
- package/src/commands/state/list.js +21 -91
- package/src/commands/state/report-check.js +134 -338
- package/src/commands/state/retype.js +12 -28
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +79 -19
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +52 -216
- package/src/commands/validation/verify.js +45 -160
- package/src/core/paths/output-schema.js +9 -155
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +2 -30
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +32 -395
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +21 -0
- package/src/core/work-types.js +10 -45
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
- package/src/dashboard/app/components/CommandPalette.jsx +4 -11
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +3 -9
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
- package/src/dashboard/app/pages/Features.jsx +9 -16
- package/src/dashboard/app/pages/Guide.jsx +2 -3
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +14 -83
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +1 -7
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +92 -113
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +4 -7
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +14 -13
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +8 -59
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +37 -112
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +11 -122
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +2 -1
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +17 -17
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +2 -6
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +8 -33
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/mutation/dotnet-introspection.js +281 -0
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +47 -13
- package/src/lib/plan/artifact-vocabulary.js +40 -0
- package/src/lib/plan/checks.js +208 -0
- package/src/lib/plan/plan-parser.js +204 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +204 -0
- package/src/lib/risk-detector.js +131 -274
- package/src/lib/specs/adr-index.js +38 -85
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +31 -0
- package/src/lib/specs/gfm-table.js +11 -0
- package/src/lib/specs/revocations.js +31 -102
- package/src/lib/specs/screen-checklist.js +56 -132
- package/src/lib/stack/dotnet-build-ignores.js +43 -0
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +260 -0
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +37 -0
- package/src/lib/standards/sections.js +47 -0
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +23 -179
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -313
- package/src/lib/tasks/test-runner.js +107 -447
- package/src/lib/telemetry/metrics.js +13 -103
- package/src/lib/telemetry/telemetry-reader.js +13 -238
- package/src/lib/telemetry/verify-event.js +4 -34
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +24 -137
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +54 -156
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +152 -575
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -137
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +31 -126
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -138
- package/src/utils/agents-installer.js +92 -272
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -87
- package/src/utils/file-copier.js +32 -90
- package/src/utils/hooks-installer.js +26 -296
- package/src/utils/skills-installer.js +4 -102
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -57
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/commands/utils/index.js +0 -3
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/dotnet-test-introspection.js +0 -632
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,41 +1,3 @@
|
|
|
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): a declaration — `tests[]` OR `testCommand` — still wins
|
|
17
|
-
* completely, and deeper auto-detection would only trade one silent false
|
|
18
|
-
* negative for another there. But with NOTHING declared, the guess stops
|
|
19
|
-
* being allowed to be a single answer: the old first-match fallback still
|
|
20
|
-
* picks the primary node exactly as before, and any vitest/jest suite found
|
|
21
|
-
* under a `package.json` (issue #87) is added BESIDE it, because that single
|
|
22
|
-
* guess is exactly what hid 3.082 vitest tests behind a green gate (F30) —
|
|
23
|
-
* the fallback found `.sln`, answered "dotnet test", and never looked twice.
|
|
24
|
-
*
|
|
25
|
-
* WHY VALIDATION IS FAIL-CLOSED HERE (D4)
|
|
26
|
-
*
|
|
27
|
-
* Every other config reader in this repo is fail-open — `JSON.parse` inside a
|
|
28
|
-
* `try/catch`, optional chaining, absent means "carry on". That is right for a
|
|
29
|
-
* hook: a guardrail that kills the session is worse than one that stays quiet.
|
|
30
|
-
* It is exactly WRONG for the verifier. A typo in `tests[]` treated fail-open
|
|
31
|
-
* falls through to the legacy resolver and reproduces F30 — green over a suite
|
|
32
|
-
* that never ran. So an invalid node is not ignored: it becomes a node with a
|
|
33
|
-
* pre-decided `status: 'fail'` and a reason naming the problem. It shows up in
|
|
34
|
-
* `runs[]` and it reddens the gate.
|
|
35
|
-
*
|
|
36
|
-
* @module test-nodes
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
1
|
import { existsSync, readFileSync } from 'fs';
|
|
40
2
|
import { join, resolve, relative, sep } from 'path';
|
|
41
3
|
|
|
@@ -44,71 +6,17 @@ import { EVAL_RUNNER, evalBlockProblem } from './eval-node.js';
|
|
|
44
6
|
import { atomicWriteJson, CONFIG_FILE_NAME } from '../../core/state/state-manager.js';
|
|
45
7
|
import { PACKAGE_LOCATIONS } from '../detectors/index.js';
|
|
46
8
|
|
|
47
|
-
/**
|
|
48
|
-
* The count extractors that exist (one per runner, see parseTestCount).
|
|
49
|
-
* `none` is the honest answer for a command whose summary nobody can read —
|
|
50
|
-
* it yields an UNKNOWN count, never a fabricated zero.
|
|
51
|
-
*/
|
|
9
|
+
/** `none` yields an UNKNOWN count, never a fabricated zero. */
|
|
52
10
|
export const TEST_RUNNERS = ['dotnet', 'vitest', 'jest', 'node-test', 'playwright', 'none', EVAL_RUNNER];
|
|
53
11
|
|
|
54
|
-
/**
|
|
55
|
-
* The task-group vocabulary a node can declare via `groups`, and that
|
|
56
|
-
* `tasks.json` entries declare via `group` (framework/schemas/tasks.schema.json
|
|
57
|
-
* `~:28`) — ONE enum, checked to agree by canary, never two that drift.
|
|
58
|
-
* Single home for B3's task-scope selection (`test-scope.js`) to import from.
|
|
59
|
-
*/
|
|
60
|
-
export const TASK_GROUPS = ['backend', 'frontend', 'tests', 'infra', 'docs'];
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Which group(s) a node serves when it does NOT declare `groups` itself,
|
|
64
|
-
* keyed by runner. `none` and `evals` are deliberately ABSENT — a command
|
|
65
|
-
* nobody could read (`none`) and a declared eval suite (`evals`) have no
|
|
66
|
-
* honest default group, so `effectiveGroups()` answers null for them rather
|
|
67
|
-
* than guessing one.
|
|
68
|
-
*/
|
|
69
|
-
export const RUNNER_DEFAULT_GROUPS = {
|
|
70
|
-
dotnet: ['backend'],
|
|
71
|
-
'node-test': ['backend'],
|
|
72
|
-
vitest: ['frontend'],
|
|
73
|
-
jest: ['frontend'],
|
|
74
|
-
playwright: ['frontend'],
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Which task group(s) this node answers to: what it DECLARED, or else its
|
|
79
|
-
* runner's default. **null is "no mapping"**, never an invented guess — B3's
|
|
80
|
-
* task-scope selection reads null as "this node is unscoped, always runs".
|
|
81
|
-
*
|
|
82
|
-
* @param {import('./test-nodes.js').TestNode} node
|
|
83
|
-
* @returns {string[]|null}
|
|
84
|
-
*/
|
|
85
|
-
export function effectiveGroups(node) {
|
|
86
|
-
return node?.groups ?? RUNNER_DEFAULT_GROUPS[node?.runner] ?? null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* A node id is what `--filter` matches and what keys `runs[]`. Lowercase and
|
|
91
|
-
* dash-only so it survives a shell argument and a JSON key without quoting.
|
|
92
|
-
*/
|
|
93
12
|
export const TEST_NODE_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
94
13
|
|
|
95
|
-
/** Id given to the single node the fallback synthesizes — it has no name of its own. */
|
|
96
14
|
const FALLBACK_NODE_ID = 'default';
|
|
97
15
|
|
|
98
16
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* a node id: the spelling rules for the id and the spelling rules for what
|
|
103
|
-
* selects it must never drift into two modules.
|
|
104
|
-
*
|
|
105
|
-
* An argument that yields NO tokens (`--filter ""`, `--filter " , "`) returns
|
|
106
|
-
* an empty token list rather than null. That distinction is the point: null
|
|
107
|
-
* means "the user did not filter", `[]` means "the user filtered and selected
|
|
108
|
-
* nothing" — and selecting nothing is a usage error, not a full run.
|
|
109
|
-
*
|
|
110
|
-
* @param {string|null|undefined} raw
|
|
111
|
-
* @returns {{ raw: string, tokens: string[] }|null} null when no filter was given
|
|
17
|
+
* null means "no filter given"; `{tokens: []}` means "filtered and selected nothing",
|
|
18
|
+
* which is a usage error, not a full run.
|
|
19
|
+
* @returns {{ raw: string, tokens: string[] }|null}
|
|
112
20
|
*/
|
|
113
21
|
export function parseTestFilter(raw) {
|
|
114
22
|
if (raw === undefined || raw === null || raw === false) return null;
|
|
@@ -119,64 +27,36 @@ export function parseTestFilter(raw) {
|
|
|
119
27
|
};
|
|
120
28
|
}
|
|
121
29
|
|
|
122
|
-
/**
|
|
123
|
-
* Does this node answer to the filter? Exact id, or the token as a substring
|
|
124
|
-
* of the id (`--filter dot` selects `dotnet`), case-insensitively.
|
|
125
|
-
*
|
|
126
|
-
* @param {TestNode} node
|
|
127
|
-
* @param {{ tokens: string[] }|null} filter - null (no filter) matches everything
|
|
128
|
-
* @returns {boolean}
|
|
129
|
-
*/
|
|
30
|
+
/** Exact id or substring of the id, case-insensitive; a null filter matches everything. */
|
|
130
31
|
export function nodeMatchesFilter(node, filter) {
|
|
131
32
|
if (!filter) return true;
|
|
132
33
|
const id = String(node?.id ?? '').toLowerCase();
|
|
133
34
|
return filter.tokens.some((token) => id === token || id.includes(token));
|
|
134
35
|
}
|
|
135
36
|
|
|
136
|
-
/**
|
|
37
|
+
/** Kept word for word from the singular resolver: readers already print it. */
|
|
137
38
|
const NO_SUITE_REASON = 'No testCommand configured and no known project file detected';
|
|
138
39
|
|
|
139
40
|
/**
|
|
140
|
-
* One declared (or synthesized) test suite.
|
|
141
|
-
*
|
|
142
41
|
* @typedef {Object} TestNode
|
|
143
|
-
* @property {string} id
|
|
144
|
-
* @property {string|null} command -
|
|
145
|
-
* @property {string} cwd - Relative to the project root
|
|
146
|
-
* @property {string} runner - One of TEST_RUNNERS
|
|
147
|
-
* @property {number|null} expectedCount -
|
|
148
|
-
*
|
|
149
|
-
* @property {
|
|
150
|
-
*
|
|
151
|
-
* @property {string
|
|
152
|
-
*
|
|
153
|
-
* @property {
|
|
154
|
-
* @property {string|null}
|
|
155
|
-
* @property {number} projectCount - How many test projects `command` chains — the input to
|
|
156
|
-
* the derived timeout. Always 1 for a declared node.
|
|
157
|
-
* @property {'fail'|'skip'|null} status - Pre-decided verdict. **Non-null means DO NOT
|
|
158
|
-
* EXECUTE**: the node is invalid config, or an explicit declaration of "no tests".
|
|
159
|
-
* null is the normal case: no verdict yet, the run decides.
|
|
160
|
-
* @property {string|null} reason - Why, when `status` is set.
|
|
42
|
+
* @property {string} id
|
|
43
|
+
* @property {string|null} command - null only on a node already decided.
|
|
44
|
+
* @property {string} cwd - Relative to the project root.
|
|
45
|
+
* @property {string} runner - One of TEST_RUNNERS.
|
|
46
|
+
* @property {number|null} expectedCount - null is UNKNOWN, never 0.
|
|
47
|
+
* @property {number|null} timeoutMs - null means "derive the ceiling", never 0.
|
|
48
|
+
* @property {string[]} build - Test projects to compile first (dotnet only).
|
|
49
|
+
* @property {boolean} e2e
|
|
50
|
+
* @property {string|null} linuxImage - null defers to the run's image.
|
|
51
|
+
* @property {number} projectCount - Input to the derived timeout; 1 for a declared node.
|
|
52
|
+
* @property {'fail'|'skip'|null} status - Non-null means DO NOT EXECUTE.
|
|
53
|
+
* @property {string|null} reason
|
|
161
54
|
*/
|
|
162
55
|
|
|
163
|
-
/**
|
|
164
|
-
* Infer which summary extractor can read a command's output.
|
|
165
|
-
*
|
|
166
|
-
* Deliberately conservative: `npm test` can front ANY runner, so it infers
|
|
167
|
-
* `none` (unknown count) rather than guessing. An extractor applied to the
|
|
168
|
-
* wrong output produces `null` anyway — but a WRONG guess that happens to
|
|
169
|
-
* match would invent a number, and numbers here fail gates.
|
|
170
|
-
*
|
|
171
|
-
* @param {string|null|undefined} command
|
|
172
|
-
* @returns {string} one of TEST_RUNNERS
|
|
173
|
-
*/
|
|
56
|
+
/** `npm test` can front any runner, so it infers `none`: a wrong guess would invent a number that fails gates. */
|
|
174
57
|
export function inferRunner(command) {
|
|
175
|
-
// `evals` is never inferred
|
|
176
|
-
//
|
|
177
|
-
// sniffing here would classify every dotnet suite as an eval — and, worse, the
|
|
178
|
-
// reverse mistake (an eval declared and read as plain dotnet) would silently
|
|
179
|
-
// drop the cache pre-flight.
|
|
58
|
+
// `evals` is never inferred: an eval command is literally `dotnet test`, and
|
|
59
|
+
// reading a declared eval as plain dotnet would drop the cache pre-flight.
|
|
180
60
|
if (typeof command !== 'string' || !command.trim()) return 'none';
|
|
181
61
|
if (/\bdotnet\s+test\b/.test(command)) return 'dotnet';
|
|
182
62
|
if (/\bvitest\b/.test(command)) return 'vitest';
|
|
@@ -186,10 +66,6 @@ export function inferRunner(command) {
|
|
|
186
66
|
return 'none';
|
|
187
67
|
}
|
|
188
68
|
|
|
189
|
-
/**
|
|
190
|
-
* Build a fully-shaped node. Every field is present on every node so that no
|
|
191
|
-
* reader has to distinguish "absent" from "unknown" by optional chaining.
|
|
192
|
-
*/
|
|
193
69
|
function makeNode(partial) {
|
|
194
70
|
return {
|
|
195
71
|
id: partial.id,
|
|
@@ -200,26 +76,15 @@ function makeNode(partial) {
|
|
|
200
76
|
timeoutMs: partial.timeoutMs ?? null,
|
|
201
77
|
build: partial.build ?? [],
|
|
202
78
|
e2e: partial.e2e ?? false,
|
|
203
|
-
// Per-node image for `verify --linux`. **null is UNKNOWN, never ''** — it
|
|
204
|
-
// means "use the run's image", not "use no image".
|
|
205
79
|
linuxImage: partial.linuxImage ?? null,
|
|
206
80
|
projectCount: partial.projectCount ?? 1,
|
|
207
|
-
//
|
|
208
|
-
// to config.json to learn where the cache lives. **null is "this is not an
|
|
209
|
-
// eval node"** — distinct from an eval node with an empty block, which
|
|
210
|
-
// `validateTestNodes` already refused.
|
|
81
|
+
// null is "not an eval node", distinct from an empty block (already refused).
|
|
211
82
|
evals: partial.evals ?? null,
|
|
212
|
-
// Declared task-group(s) this node serves (§B3 task-scope selection).
|
|
213
|
-
// **null is "not declared"**, distinct from an empty array (which
|
|
214
|
-
// `firstProblem` refuses outright) — effectiveGroups() falls back to the
|
|
215
|
-
// runner's default only when this is null.
|
|
216
|
-
groups: partial.groups ?? null,
|
|
217
83
|
status: partial.status ?? null,
|
|
218
84
|
reason: partial.reason ?? null,
|
|
219
85
|
};
|
|
220
86
|
}
|
|
221
87
|
|
|
222
|
-
/** A node that will not run, and says why in the vocabulary of §2.2 of the spec. */
|
|
223
88
|
function invalidNode(id, problem, raw = {}) {
|
|
224
89
|
return makeNode({
|
|
225
90
|
id,
|
|
@@ -238,15 +103,6 @@ function isNonNegativeInteger(value) {
|
|
|
238
103
|
return Number.isInteger(value) && value >= 0;
|
|
239
104
|
}
|
|
240
105
|
|
|
241
|
-
/**
|
|
242
|
-
* First problem with one raw `tests[]` entry, or null when it is well formed.
|
|
243
|
-
* First problem only: a list of complaints about a single node buries the one
|
|
244
|
-
* that has to be fixed first.
|
|
245
|
-
*
|
|
246
|
-
* @param {*} raw
|
|
247
|
-
* @param {string|null} projectPath - When given, `cwd` is also checked against the disk.
|
|
248
|
-
* @returns {string|null}
|
|
249
|
-
*/
|
|
250
106
|
function firstProblem(raw, projectPath) {
|
|
251
107
|
if (!isPlainObject(raw)) return 'must be an object';
|
|
252
108
|
|
|
@@ -271,10 +127,7 @@ function firstProblem(raw, projectPath) {
|
|
|
271
127
|
if (raw.build !== undefined && (!Array.isArray(raw.build) || raw.build.some((b) => typeof b !== 'string'))) {
|
|
272
128
|
return 'build must be an array of strings';
|
|
273
129
|
}
|
|
274
|
-
//
|
|
275
|
-
// on a vitest node it would be silently ignored — and a declaration the
|
|
276
|
-
// verifier ignores is the same fail-open shape as the typo in `tests[]`:
|
|
277
|
-
// the author believes something is being compiled that never is.
|
|
130
|
+
// A declaration the verifier would silently ignore is refused, not dropped.
|
|
278
131
|
if (Array.isArray(raw.build) && raw.build.length > 0
|
|
279
132
|
&& (raw.runner ?? inferRunner(raw.command)) !== 'dotnet') {
|
|
280
133
|
return 'build applies only to runner "dotnet"';
|
|
@@ -285,10 +138,6 @@ function firstProblem(raw, projectPath) {
|
|
|
285
138
|
if (raw.linuxImage !== undefined && (typeof raw.linuxImage !== 'string' || !raw.linuxImage.trim())) {
|
|
286
139
|
return 'linuxImage must be a non-empty string';
|
|
287
140
|
}
|
|
288
|
-
// `runner: "evals"` REQUIRES its block; the block on any other runner is a
|
|
289
|
-
// declaration the verifier would silently ignore — the same fail-open shape as
|
|
290
|
-
// a typo in `tests[]`, where the author believes something is being checked
|
|
291
|
-
// that never is.
|
|
292
141
|
if ((raw.runner ?? inferRunner(raw.command)) === EVAL_RUNNER) {
|
|
293
142
|
const problem = evalBlockProblem(raw, projectPath);
|
|
294
143
|
if (problem) return problem;
|
|
@@ -296,41 +145,14 @@ function firstProblem(raw, projectPath) {
|
|
|
296
145
|
return `evals applies only to runner "${EVAL_RUNNER}"`;
|
|
297
146
|
}
|
|
298
147
|
|
|
299
|
-
if (raw.groups !== undefined) {
|
|
300
|
-
if (!Array.isArray(raw.groups)) {
|
|
301
|
-
return 'groups must be a non-empty array';
|
|
302
|
-
}
|
|
303
|
-
if (raw.groups.length === 0) {
|
|
304
|
-
return 'groups must be a non-empty array';
|
|
305
|
-
}
|
|
306
|
-
const invalid = raw.groups.filter((g) => !TASK_GROUPS.includes(g));
|
|
307
|
-
if (invalid.length > 0) {
|
|
308
|
-
return `groups must contain only ${TASK_GROUPS.join(' | ')} (invalid: ${invalid.join(', ')})`;
|
|
309
|
-
}
|
|
310
|
-
const seenGroups = new Set();
|
|
311
|
-
const duplicates = new Set();
|
|
312
|
-
for (const g of raw.groups) {
|
|
313
|
-
if (seenGroups.has(g)) duplicates.add(g);
|
|
314
|
-
seenGroups.add(g);
|
|
315
|
-
}
|
|
316
|
-
if (duplicates.size > 0) {
|
|
317
|
-
return `groups must not contain duplicates (repeated: ${[...duplicates].join(', ')})`;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// The cwd checks come last because they are the expensive ones, and because
|
|
322
|
-
// a node that is malformed in shape has a better error to report first.
|
|
323
148
|
const cwd = typeof raw.cwd === 'string' ? raw.cwd.trim() : '.';
|
|
324
149
|
const root = resolve(projectPath ?? '.');
|
|
325
150
|
const abs = resolve(root, cwd);
|
|
326
151
|
if (!isInside(abs, root)) {
|
|
327
|
-
// A cwd that leaves the root addresses ANOTHER tree — the same damage the
|
|
328
|
-
// worktree exclusion in collectDotnetTestProjects exists to prevent.
|
|
329
152
|
return `cwd "${cwd}" resolves outside the project root`;
|
|
330
153
|
}
|
|
331
154
|
if (projectPath && !existsSync(abs)) {
|
|
332
|
-
//
|
|
333
|
-
// would hide it behind a green gate.
|
|
155
|
+
// Named failure, never a skip: a skip would hide a wrong cwd behind a green gate.
|
|
334
156
|
return `cwd "${cwd}" does not exist`;
|
|
335
157
|
}
|
|
336
158
|
|
|
@@ -338,16 +160,9 @@ function firstProblem(raw, projectPath) {
|
|
|
338
160
|
}
|
|
339
161
|
|
|
340
162
|
/**
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* could not detect anything (v8.9/v8.11).
|
|
345
|
-
*
|
|
346
|
-
* Fail-closed by design (D4): a bad entry becomes a node with
|
|
347
|
-
* `status: 'fail'`, never a node that disappears.
|
|
348
|
-
*
|
|
349
|
-
* @param {object|null} config - Parsed `.morph/config/config.json`
|
|
350
|
-
* @param {string|null} [projectPath] - Project root; enables the on-disk `cwd` check
|
|
163
|
+
* Fail-CLOSED, unlike the repo's other config readers: a typo that falls through
|
|
164
|
+
* to the fallback resolver gives a green gate over a suite that never ran.
|
|
165
|
+
* A bad entry becomes a `status: 'fail'` node, never a node that disappears.
|
|
351
166
|
* @returns {TestNode[]} One node per declared entry, in declaration order
|
|
352
167
|
*/
|
|
353
168
|
export function validateTestNodes(config, projectPath = null) {
|
|
@@ -357,9 +172,7 @@ export function validateTestNodes(config, projectPath = null) {
|
|
|
357
172
|
return [invalidNode('project.tests', 'must be an array of test nodes')];
|
|
358
173
|
}
|
|
359
174
|
|
|
360
|
-
//
|
|
361
|
-
// `runs[]` ambiguous, and picking a winner would silence one declared suite.
|
|
362
|
-
// Both sides fail — the ambiguity is the defect, not either entry.
|
|
175
|
+
// Duplicate ids fail on both sides: picking a winner would silence a declared suite.
|
|
363
176
|
const seen = new Map();
|
|
364
177
|
for (const entry of raw) {
|
|
365
178
|
if (isPlainObject(entry) && typeof entry.id === 'string') {
|
|
@@ -390,13 +203,11 @@ export function validateTestNodes(config, projectPath = null) {
|
|
|
390
203
|
e2e: entry.e2e ?? false,
|
|
391
204
|
linuxImage: entry.linuxImage ?? null,
|
|
392
205
|
evals: entry.evals ?? null,
|
|
393
|
-
groups: entry.groups ?? null,
|
|
394
206
|
projectCount: 1,
|
|
395
207
|
});
|
|
396
208
|
});
|
|
397
209
|
}
|
|
398
210
|
|
|
399
|
-
/** Turn a relative path into an id-safe slug matching TEST_NODE_ID_PATTERN. */
|
|
400
211
|
function slugifyForId(text) {
|
|
401
212
|
return text
|
|
402
213
|
.toLowerCase()
|
|
@@ -405,20 +216,8 @@ function slugifyForId(text) {
|
|
|
405
216
|
}
|
|
406
217
|
|
|
407
218
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
* fallback of `resolveTestNodes()`, and only when nothing was declared: a
|
|
411
|
-
* `package.json` that names `vitest`/`jest` in `dependencies` or
|
|
412
|
-
* `devDependencies` becomes one candidate suite, exactly the shape F30
|
|
413
|
-
* exploited when a repo had .NET AND a frontend and the first-match fallback
|
|
414
|
-
* only ever answered "dotnet test".
|
|
415
|
-
*
|
|
416
|
-
* Fail-open by construction (it IS a guess): an unreadable or malformed
|
|
417
|
-
* `package.json` is skipped silently, never a `fail` node — that fail-closed
|
|
418
|
-
* treatment is reserved for a DECLARATION (`tests[]`), which this is not.
|
|
419
|
-
*
|
|
420
|
-
* @param {string} projectPath - Project root
|
|
421
|
-
* @param {object|null} config - Parsed `.morph/config/config.json`, or null
|
|
219
|
+
* A GUESS from `package.json` (vitest/jest in deps), never a declaration — so it
|
|
220
|
+
* is fail-open: an unreadable `package.json` is skipped, never a `fail` node.
|
|
422
221
|
* @returns {Array<{ id: string, cwd: string, runner: 'vitest'|'jest', command: string }>}
|
|
423
222
|
*/
|
|
424
223
|
export function discoverFrontendPackages(projectPath, config) {
|
|
@@ -428,9 +227,6 @@ export function discoverFrontendPackages(projectPath, config) {
|
|
|
428
227
|
const frontendPath = config?.project?.frontendPath;
|
|
429
228
|
if (typeof frontendPath === 'string' && frontendPath.trim()) {
|
|
430
229
|
const abs = resolve(root, frontendPath.trim());
|
|
431
|
-
// Outside the root is IGNORED, not refused — this is a guess reusing
|
|
432
|
-
// another guess (detectProject's own frontendPath), and neither one gets
|
|
433
|
-
// to address another tree.
|
|
434
230
|
if (isInside(abs, root)) candidates.push(frontendPath.trim());
|
|
435
231
|
}
|
|
436
232
|
|
|
@@ -450,7 +246,7 @@ export function discoverFrontendPackages(projectPath, config) {
|
|
|
450
246
|
try {
|
|
451
247
|
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
452
248
|
} catch {
|
|
453
|
-
continue;
|
|
249
|
+
continue;
|
|
454
250
|
}
|
|
455
251
|
|
|
456
252
|
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
@@ -482,40 +278,19 @@ export function discoverFrontendPackages(projectPath, config) {
|
|
|
482
278
|
}
|
|
483
279
|
|
|
484
280
|
/**
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
* config at all, because this repository (the framework source) has none and
|
|
489
|
-
* every project installed before this feature has none either:
|
|
490
|
-
*
|
|
491
|
-
* 1. `project.tests[]` declared → one node per entry, source 'config-tests'
|
|
492
|
-
* 2. `project.testCommand` declared → ONE synthesized node, source 'config'
|
|
493
|
-
* 3. the four-step fallback of → ONE synthesized node, source
|
|
494
|
-
* resolveTestPlan() (root .sln, 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'
|
|
495
|
-
* *Tests.csproj scan, npm test)
|
|
496
|
-
*
|
|
497
|
-
* Steps 2 and 3 are `resolveTestPlan()` itself — CALLED, never re-implemented.
|
|
498
|
-
* That matters twice over: it is the only place that knows the fallback order,
|
|
499
|
-
* and it is the only place that excludes linked worktrees from the
|
|
500
|
-
* `*Tests.csproj` scan (a worktree is another feature's checkout; scanning it
|
|
501
|
-
* doubles the clock and reports on code outside the tree being judged).
|
|
502
|
-
*
|
|
503
|
-
* The list is never empty: "nothing to run" is expressed as ONE node already
|
|
504
|
-
* decided as `skip`, so the reason travels to the report instead of vanishing.
|
|
505
|
-
*
|
|
506
|
-
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
281
|
+
* Precedence: `project.tests[]` → `project.testCommand` → `resolveTestPlan()`'s
|
|
282
|
+
* fallback (called, never re-implemented: it owns the order and the worktree
|
|
283
|
+
* exclusion). Never empty: "nothing to run" is one node decided as `skip`.
|
|
507
284
|
* @returns {{ nodes: TestNode[], source: string }}
|
|
508
285
|
*/
|
|
509
286
|
export function resolveTestNodes(projectPath = '.') {
|
|
510
287
|
const config = readMorphConfig(projectPath);
|
|
511
288
|
|
|
512
|
-
// `tests`
|
|
513
|
-
// one: falling back from a typo is how F30 comes back.
|
|
289
|
+
// `tests` in ANY shape is a declaration: falling back from a typo hides a suite.
|
|
514
290
|
if (config?.project?.tests !== undefined) {
|
|
515
291
|
const nodes = validateTestNodes(config, projectPath);
|
|
516
292
|
if (Array.isArray(config.project.tests) && config.project.tests.length === 0) {
|
|
517
|
-
// `[]`
|
|
518
|
-
// from an absent key, which means "guess for me".
|
|
293
|
+
// `[]` declares "no suites"; an absent key means "guess for me".
|
|
519
294
|
return {
|
|
520
295
|
nodes: [makeNode({ id: 'none', status: 'skip', reason: 'no test nodes declared' })],
|
|
521
296
|
source: 'config-tests',
|
|
@@ -532,16 +307,11 @@ export function resolveTestNodes(projectPath = '.') {
|
|
|
532
307
|
: makeNode({ id: FALLBACK_NODE_ID, status: 'skip', reason: NO_SUITE_REASON }),
|
|
533
308
|
];
|
|
534
309
|
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
// 'config'` means `project.testCommand` IS a declaration (it lives inside
|
|
538
|
-
// resolveTestPlan's own step 1), and a declaration wins completely: nothing
|
|
539
|
-
// is added beside it, same as the `project.tests[]` branch above.
|
|
310
|
+
// Discovered frontend suites go BESIDE the fallback node, never beside a
|
|
311
|
+
// declaration (`plan.source === 'config'` is `project.testCommand`).
|
|
540
312
|
if (plan.source !== 'config') {
|
|
541
313
|
const discovered = discoverFrontendPackages(projectPath, config)
|
|
542
|
-
// The root package is already run by `npm test
|
|
543
|
-
// fallback's own answer — adding it again would run the same suite
|
|
544
|
-
// twice under two different commands.
|
|
314
|
+
// The root package is already run by the fallback's `npm test`.
|
|
545
315
|
.filter((pkg) => !(pkg.cwd === '.' && plan.source === 'npm'))
|
|
546
316
|
.map((pkg) => makeNode({ id: pkg.id, command: pkg.command, cwd: pkg.cwd, runner: pkg.runner }));
|
|
547
317
|
nodes.push(...discovered);
|
|
@@ -550,50 +320,15 @@ export function resolveTestNodes(projectPath = '.') {
|
|
|
550
320
|
return { nodes, source: plan.source };
|
|
551
321
|
}
|
|
552
322
|
|
|
553
|
-
/**
|
|
554
|
-
* Path of the config the baseline lives in. One spelling, so the writer and
|
|
555
|
-
* `readMorphConfig()` can never disagree about which file is meant — and that
|
|
556
|
-
* is now literally true: both derive from `CONFIG_FILE_NAME`
|
|
557
|
-
* (`core/state/state-manager.js`), the same constant `state-manager.js` itself
|
|
558
|
-
* reads the project identity through. It used to be a promise made by a JSDoc
|
|
559
|
-
* over two hand-typed copies of the same string.
|
|
560
|
-
*/
|
|
561
323
|
export function morphConfigPath(projectPath = '.') {
|
|
562
324
|
return join(projectPath, CONFIG_FILE_NAME);
|
|
563
325
|
}
|
|
564
326
|
|
|
565
327
|
/**
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
* `MORPH.md` says a baseline is a COMMAND, never a number — and this feature
|
|
571
|
-
* stores a number. The rule survives because nobody is allowed to type it: the
|
|
572
|
-
* only writer is `verify --record-baseline`, and it writes what a real, green
|
|
573
|
-
* run measured. A hand-typed number describes what someone believed; the
|
|
574
|
-
* divergence between belief and the suite is exactly the failure the rule was
|
|
575
|
-
* written after.
|
|
576
|
-
*
|
|
577
|
-
* The write is a POINTED mutation: read → set one field per declared node →
|
|
578
|
-
* atomic `tmp.{pid}` + rename via `atomicWriteJson` (the same door
|
|
579
|
-
* `state-manager.js` uses, not a second copy of it). Every other key of
|
|
580
|
-
* `config.json` survives, because this command owns one field and nothing else.
|
|
581
|
-
*
|
|
582
|
-
* It refuses rather than improvises:
|
|
583
|
-
* - a measurement whose `count` is not an integer (**null is UNKNOWN, never
|
|
584
|
-
* 0** — a written 0 would fabricate a drop against every later run);
|
|
585
|
-
* - a measured id that is not declared (appending a node would be inventing
|
|
586
|
-
* the declaration the command came to update);
|
|
587
|
-
* - a declared node with no measurement (a baseline recorded for part of a
|
|
588
|
-
* fleet says nothing true about the rest of it);
|
|
589
|
-
* - a measurement carrying a task-scope `selection` (#87): narrowed, scoped
|
|
590
|
-
* out or run whole INSIDE a task-scoped verify, it measured a recorte, and
|
|
591
|
-
* `expectedCount` is the whole suite's number. `verify` already refuses
|
|
592
|
-
* `--record-baseline` with a task id before the build; this is the second
|
|
593
|
-
* barrier, at the writer, so no caller can route around it.
|
|
594
|
-
*
|
|
595
|
-
* @param {string} projectPath - Project root
|
|
596
|
-
* @param {Array<{id: string, count: number|null, selection?: object|null}>} measurements - One per node that ran
|
|
328
|
+
* The only writer of `expectedCount`: a baseline is measured by a green run, never typed.
|
|
329
|
+
* Refuses a non-integer count (null is UNKNOWN, never 0), an undeclared id, or a
|
|
330
|
+
* declared node with no measurement. Every other config key survives.
|
|
331
|
+
* @param {Array<{id: string, count: number|null}>} measurements
|
|
597
332
|
* @returns {{ recorded: boolean, reason: string|null, updates: Array<{id: string, from: number|null, to: number}>, path: string }}
|
|
598
333
|
*/
|
|
599
334
|
export function recordTestBaselines(projectPath, measurements) {
|
|
@@ -614,11 +349,6 @@ export function recordTestBaselines(projectPath, measurements) {
|
|
|
614
349
|
return refuse('config.json não declara project.tests[] — não há entrada onde gravar a baseline');
|
|
615
350
|
}
|
|
616
351
|
|
|
617
|
-
const recortes = measurements.filter((m) => m.selection != null).map((m) => m.id);
|
|
618
|
-
if (recortes.length > 0) {
|
|
619
|
-
return refuse(`medição de recorte de task em: ${recortes.join(', ')} — baseline só de execução inteira, sem {task}`);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
352
|
const unreadable = measurements.filter((m) => !Number.isInteger(m.count)).map((m) => m.id);
|
|
623
353
|
if (unreadable.length > 0) {
|
|
624
354
|
return refuse(`contagem desconhecida em: ${unreadable.join(', ')} — desconhecido nunca vira 0`);
|