@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +11 -63
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +3 -3
- package/framework/agents.json +19 -11
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +22 -31
- package/framework/evals/review-checklist.md +12 -2
- package/framework/hooks/README.md +8 -44
- 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 +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- 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 +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +12 -68
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- 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.sh +11 -11
- 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 +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- 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 +1 -42
- 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 +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- 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/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/rules/nextjs-standards.md +1 -1
- 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 +4 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +9 -7
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +36 -14
- package/framework/standards/STANDARDS.meta.json +16 -2
- 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/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 +2 -2
- 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 +1 -1
- 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/testing.md +10 -4
- 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/frontend/nextjs/testing.md +10 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- 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/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +3 -1
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- 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/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/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- 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 +17 -104
- 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 +5 -57
- 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.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +15 -97
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +11 -11
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +10 -97
- package/src/commands/validation/verify.js +30 -113
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- 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 +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- 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 +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- 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 +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- 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 +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/dashboard/vite.config.js +24 -24
- 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 +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +14 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/applicability.js +37 -0
- 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 +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- 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 +1 -0
- 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 +3 -15
- 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 +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +34 -33
- package/src/lib/plan/plan-parser.js +7 -8
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +11 -26
- package/src/lib/risk-detector.js +41 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/tests-audit/audit.js +96 -0
- 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 +2 -24
- 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 +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +21 -158
- package/src/lib/validators/verify-runner.js +85 -376
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- 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 +13 -95
- 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 -147
- package/src/utils/agents-installer.js +29 -276
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- 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,36 +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
|
-
* A node id is what `--filter` matches and what keys `runs[]`. Lowercase and
|
|
56
|
-
* dash-only so it survives a shell argument and a JSON key without quoting.
|
|
57
|
-
*/
|
|
58
12
|
export const TEST_NODE_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
59
13
|
|
|
60
|
-
/** Id given to the single node the fallback synthesizes — it has no name of its own. */
|
|
61
14
|
const FALLBACK_NODE_ID = 'default';
|
|
62
15
|
|
|
63
16
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* a node id: the spelling rules for the id and the spelling rules for what
|
|
68
|
-
* selects it must never drift into two modules.
|
|
69
|
-
*
|
|
70
|
-
* An argument that yields NO tokens (`--filter ""`, `--filter " , "`) returns
|
|
71
|
-
* an empty token list rather than null. That distinction is the point: null
|
|
72
|
-
* means "the user did not filter", `[]` means "the user filtered and selected
|
|
73
|
-
* nothing" — and selecting nothing is a usage error, not a full run.
|
|
74
|
-
*
|
|
75
|
-
* @param {string|null|undefined} raw
|
|
76
|
-
* @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}
|
|
77
20
|
*/
|
|
78
21
|
export function parseTestFilter(raw) {
|
|
79
22
|
if (raw === undefined || raw === null || raw === false) return null;
|
|
@@ -84,64 +27,36 @@ export function parseTestFilter(raw) {
|
|
|
84
27
|
};
|
|
85
28
|
}
|
|
86
29
|
|
|
87
|
-
/**
|
|
88
|
-
* Does this node answer to the filter? Exact id, or the token as a substring
|
|
89
|
-
* of the id (`--filter dot` selects `dotnet`), case-insensitively.
|
|
90
|
-
*
|
|
91
|
-
* @param {TestNode} node
|
|
92
|
-
* @param {{ tokens: string[] }|null} filter - null (no filter) matches everything
|
|
93
|
-
* @returns {boolean}
|
|
94
|
-
*/
|
|
30
|
+
/** Exact id or substring of the id, case-insensitive; a null filter matches everything. */
|
|
95
31
|
export function nodeMatchesFilter(node, filter) {
|
|
96
32
|
if (!filter) return true;
|
|
97
33
|
const id = String(node?.id ?? '').toLowerCase();
|
|
98
34
|
return filter.tokens.some((token) => id === token || id.includes(token));
|
|
99
35
|
}
|
|
100
36
|
|
|
101
|
-
/**
|
|
37
|
+
/** Kept word for word from the singular resolver: readers already print it. */
|
|
102
38
|
const NO_SUITE_REASON = 'No testCommand configured and no known project file detected';
|
|
103
39
|
|
|
104
40
|
/**
|
|
105
|
-
* One declared (or synthesized) test suite.
|
|
106
|
-
*
|
|
107
41
|
* @typedef {Object} TestNode
|
|
108
|
-
* @property {string} id
|
|
109
|
-
* @property {string|null} command -
|
|
110
|
-
* @property {string} cwd - Relative to the project root
|
|
111
|
-
* @property {string} runner - One of TEST_RUNNERS
|
|
112
|
-
* @property {number|null} expectedCount -
|
|
113
|
-
*
|
|
114
|
-
* @property {
|
|
115
|
-
*
|
|
116
|
-
* @property {string
|
|
117
|
-
*
|
|
118
|
-
* @property {
|
|
119
|
-
* @property {string|null}
|
|
120
|
-
* @property {number} projectCount - How many test projects `command` chains — the input to
|
|
121
|
-
* the derived timeout. Always 1 for a declared node.
|
|
122
|
-
* @property {'fail'|'skip'|null} status - Pre-decided verdict. **Non-null means DO NOT
|
|
123
|
-
* EXECUTE**: the node is invalid config, or an explicit declaration of "no tests".
|
|
124
|
-
* null is the normal case: no verdict yet, the run decides.
|
|
125
|
-
* @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
|
|
126
54
|
*/
|
|
127
55
|
|
|
128
|
-
/**
|
|
129
|
-
* Infer which summary extractor can read a command's output.
|
|
130
|
-
*
|
|
131
|
-
* Deliberately conservative: `npm test` can front ANY runner, so it infers
|
|
132
|
-
* `none` (unknown count) rather than guessing. An extractor applied to the
|
|
133
|
-
* wrong output produces `null` anyway — but a WRONG guess that happens to
|
|
134
|
-
* match would invent a number, and numbers here fail gates.
|
|
135
|
-
*
|
|
136
|
-
* @param {string|null|undefined} command
|
|
137
|
-
* @returns {string} one of TEST_RUNNERS
|
|
138
|
-
*/
|
|
56
|
+
/** `npm test` can front any runner, so it infers `none`: a wrong guess would invent a number that fails gates. */
|
|
139
57
|
export function inferRunner(command) {
|
|
140
|
-
// `evals` is never inferred
|
|
141
|
-
//
|
|
142
|
-
// sniffing here would classify every dotnet suite as an eval — and, worse, the
|
|
143
|
-
// reverse mistake (an eval declared and read as plain dotnet) would silently
|
|
144
|
-
// 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.
|
|
145
60
|
if (typeof command !== 'string' || !command.trim()) return 'none';
|
|
146
61
|
if (/\bdotnet\s+test\b/.test(command)) return 'dotnet';
|
|
147
62
|
if (/\bvitest\b/.test(command)) return 'vitest';
|
|
@@ -151,10 +66,6 @@ export function inferRunner(command) {
|
|
|
151
66
|
return 'none';
|
|
152
67
|
}
|
|
153
68
|
|
|
154
|
-
/**
|
|
155
|
-
* Build a fully-shaped node. Every field is present on every node so that no
|
|
156
|
-
* reader has to distinguish "absent" from "unknown" by optional chaining.
|
|
157
|
-
*/
|
|
158
69
|
function makeNode(partial) {
|
|
159
70
|
return {
|
|
160
71
|
id: partial.id,
|
|
@@ -165,21 +76,15 @@ function makeNode(partial) {
|
|
|
165
76
|
timeoutMs: partial.timeoutMs ?? null,
|
|
166
77
|
build: partial.build ?? [],
|
|
167
78
|
e2e: partial.e2e ?? false,
|
|
168
|
-
// Per-node image for `verify --linux`. **null is UNKNOWN, never ''** — it
|
|
169
|
-
// means "use the run's image", not "use no image".
|
|
170
79
|
linuxImage: partial.linuxImage ?? null,
|
|
171
80
|
projectCount: partial.projectCount ?? 1,
|
|
172
|
-
//
|
|
173
|
-
// to config.json to learn where the cache lives. **null is "this is not an
|
|
174
|
-
// eval node"** — distinct from an eval node with an empty block, which
|
|
175
|
-
// `validateTestNodes` already refused.
|
|
81
|
+
// null is "not an eval node", distinct from an empty block (already refused).
|
|
176
82
|
evals: partial.evals ?? null,
|
|
177
83
|
status: partial.status ?? null,
|
|
178
84
|
reason: partial.reason ?? null,
|
|
179
85
|
};
|
|
180
86
|
}
|
|
181
87
|
|
|
182
|
-
/** A node that will not run, and says why in the vocabulary of §2.2 of the spec. */
|
|
183
88
|
function invalidNode(id, problem, raw = {}) {
|
|
184
89
|
return makeNode({
|
|
185
90
|
id,
|
|
@@ -198,15 +103,6 @@ function isNonNegativeInteger(value) {
|
|
|
198
103
|
return Number.isInteger(value) && value >= 0;
|
|
199
104
|
}
|
|
200
105
|
|
|
201
|
-
/**
|
|
202
|
-
* First problem with one raw `tests[]` entry, or null when it is well formed.
|
|
203
|
-
* First problem only: a list of complaints about a single node buries the one
|
|
204
|
-
* that has to be fixed first.
|
|
205
|
-
*
|
|
206
|
-
* @param {*} raw
|
|
207
|
-
* @param {string|null} projectPath - When given, `cwd` is also checked against the disk.
|
|
208
|
-
* @returns {string|null}
|
|
209
|
-
*/
|
|
210
106
|
function firstProblem(raw, projectPath) {
|
|
211
107
|
if (!isPlainObject(raw)) return 'must be an object';
|
|
212
108
|
|
|
@@ -231,10 +127,7 @@ function firstProblem(raw, projectPath) {
|
|
|
231
127
|
if (raw.build !== undefined && (!Array.isArray(raw.build) || raw.build.some((b) => typeof b !== 'string'))) {
|
|
232
128
|
return 'build must be an array of strings';
|
|
233
129
|
}
|
|
234
|
-
//
|
|
235
|
-
// on a vitest node it would be silently ignored — and a declaration the
|
|
236
|
-
// verifier ignores is the same fail-open shape as the typo in `tests[]`:
|
|
237
|
-
// the author believes something is being compiled that never is.
|
|
130
|
+
// A declaration the verifier would silently ignore is refused, not dropped.
|
|
238
131
|
if (Array.isArray(raw.build) && raw.build.length > 0
|
|
239
132
|
&& (raw.runner ?? inferRunner(raw.command)) !== 'dotnet') {
|
|
240
133
|
return 'build applies only to runner "dotnet"';
|
|
@@ -245,10 +138,6 @@ function firstProblem(raw, projectPath) {
|
|
|
245
138
|
if (raw.linuxImage !== undefined && (typeof raw.linuxImage !== 'string' || !raw.linuxImage.trim())) {
|
|
246
139
|
return 'linuxImage must be a non-empty string';
|
|
247
140
|
}
|
|
248
|
-
// `runner: "evals"` REQUIRES its block; the block on any other runner is a
|
|
249
|
-
// declaration the verifier would silently ignore — the same fail-open shape as
|
|
250
|
-
// a typo in `tests[]`, where the author believes something is being checked
|
|
251
|
-
// that never is.
|
|
252
141
|
if ((raw.runner ?? inferRunner(raw.command)) === EVAL_RUNNER) {
|
|
253
142
|
const problem = evalBlockProblem(raw, projectPath);
|
|
254
143
|
if (problem) return problem;
|
|
@@ -256,19 +145,14 @@ function firstProblem(raw, projectPath) {
|
|
|
256
145
|
return `evals applies only to runner "${EVAL_RUNNER}"`;
|
|
257
146
|
}
|
|
258
147
|
|
|
259
|
-
// The cwd checks come last because they are the expensive ones, and because
|
|
260
|
-
// a node that is malformed in shape has a better error to report first.
|
|
261
148
|
const cwd = typeof raw.cwd === 'string' ? raw.cwd.trim() : '.';
|
|
262
149
|
const root = resolve(projectPath ?? '.');
|
|
263
150
|
const abs = resolve(root, cwd);
|
|
264
151
|
if (!isInside(abs, root)) {
|
|
265
|
-
// A cwd that leaves the root addresses ANOTHER tree — the same damage the
|
|
266
|
-
// worktree exclusion in collectDotnetTestProjects exists to prevent.
|
|
267
152
|
return `cwd "${cwd}" resolves outside the project root`;
|
|
268
153
|
}
|
|
269
154
|
if (projectPath && !existsSync(abs)) {
|
|
270
|
-
//
|
|
271
|
-
// would hide it behind a green gate.
|
|
155
|
+
// Named failure, never a skip: a skip would hide a wrong cwd behind a green gate.
|
|
272
156
|
return `cwd "${cwd}" does not exist`;
|
|
273
157
|
}
|
|
274
158
|
|
|
@@ -276,16 +160,9 @@ function firstProblem(raw, projectPath) {
|
|
|
276
160
|
}
|
|
277
161
|
|
|
278
162
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* could not detect anything (v8.9/v8.11).
|
|
283
|
-
*
|
|
284
|
-
* Fail-closed by design (D4): a bad entry becomes a node with
|
|
285
|
-
* `status: 'fail'`, never a node that disappears.
|
|
286
|
-
*
|
|
287
|
-
* @param {object|null} config - Parsed `.morph/config/config.json`
|
|
288
|
-
* @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.
|
|
289
166
|
* @returns {TestNode[]} One node per declared entry, in declaration order
|
|
290
167
|
*/
|
|
291
168
|
export function validateTestNodes(config, projectPath = null) {
|
|
@@ -295,9 +172,7 @@ export function validateTestNodes(config, projectPath = null) {
|
|
|
295
172
|
return [invalidNode('project.tests', 'must be an array of test nodes')];
|
|
296
173
|
}
|
|
297
174
|
|
|
298
|
-
//
|
|
299
|
-
// `runs[]` ambiguous, and picking a winner would silence one declared suite.
|
|
300
|
-
// 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.
|
|
301
176
|
const seen = new Map();
|
|
302
177
|
for (const entry of raw) {
|
|
303
178
|
if (isPlainObject(entry) && typeof entry.id === 'string') {
|
|
@@ -333,7 +208,6 @@ export function validateTestNodes(config, projectPath = null) {
|
|
|
333
208
|
});
|
|
334
209
|
}
|
|
335
210
|
|
|
336
|
-
/** Turn a relative path into an id-safe slug matching TEST_NODE_ID_PATTERN. */
|
|
337
211
|
function slugifyForId(text) {
|
|
338
212
|
return text
|
|
339
213
|
.toLowerCase()
|
|
@@ -342,20 +216,8 @@ function slugifyForId(text) {
|
|
|
342
216
|
}
|
|
343
217
|
|
|
344
218
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* fallback of `resolveTestNodes()`, and only when nothing was declared: a
|
|
348
|
-
* `package.json` that names `vitest`/`jest` in `dependencies` or
|
|
349
|
-
* `devDependencies` becomes one candidate suite, exactly the shape F30
|
|
350
|
-
* exploited when a repo had .NET AND a frontend and the first-match fallback
|
|
351
|
-
* only ever answered "dotnet test".
|
|
352
|
-
*
|
|
353
|
-
* Fail-open by construction (it IS a guess): an unreadable or malformed
|
|
354
|
-
* `package.json` is skipped silently, never a `fail` node — that fail-closed
|
|
355
|
-
* treatment is reserved for a DECLARATION (`tests[]`), which this is not.
|
|
356
|
-
*
|
|
357
|
-
* @param {string} projectPath - Project root
|
|
358
|
-
* @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.
|
|
359
221
|
* @returns {Array<{ id: string, cwd: string, runner: 'vitest'|'jest', command: string }>}
|
|
360
222
|
*/
|
|
361
223
|
export function discoverFrontendPackages(projectPath, config) {
|
|
@@ -365,9 +227,6 @@ export function discoverFrontendPackages(projectPath, config) {
|
|
|
365
227
|
const frontendPath = config?.project?.frontendPath;
|
|
366
228
|
if (typeof frontendPath === 'string' && frontendPath.trim()) {
|
|
367
229
|
const abs = resolve(root, frontendPath.trim());
|
|
368
|
-
// Outside the root is IGNORED, not refused — this is a guess reusing
|
|
369
|
-
// another guess (detectProject's own frontendPath), and neither one gets
|
|
370
|
-
// to address another tree.
|
|
371
230
|
if (isInside(abs, root)) candidates.push(frontendPath.trim());
|
|
372
231
|
}
|
|
373
232
|
|
|
@@ -387,7 +246,7 @@ export function discoverFrontendPackages(projectPath, config) {
|
|
|
387
246
|
try {
|
|
388
247
|
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
389
248
|
} catch {
|
|
390
|
-
continue;
|
|
249
|
+
continue;
|
|
391
250
|
}
|
|
392
251
|
|
|
393
252
|
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
@@ -419,40 +278,19 @@ export function discoverFrontendPackages(projectPath, config) {
|
|
|
419
278
|
}
|
|
420
279
|
|
|
421
280
|
/**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* config at all, because this repository (the framework source) has none and
|
|
426
|
-
* every project installed before this feature has none either:
|
|
427
|
-
*
|
|
428
|
-
* 1. `project.tests[]` declared → one node per entry, source 'config-tests'
|
|
429
|
-
* 2. `project.testCommand` declared → ONE synthesized node, source 'config'
|
|
430
|
-
* 3. the four-step fallback of → ONE synthesized node, source
|
|
431
|
-
* resolveTestPlan() (root .sln, 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'
|
|
432
|
-
* *Tests.csproj scan, npm test)
|
|
433
|
-
*
|
|
434
|
-
* Steps 2 and 3 are `resolveTestPlan()` itself — CALLED, never re-implemented.
|
|
435
|
-
* That matters twice over: it is the only place that knows the fallback order,
|
|
436
|
-
* and it is the only place that excludes linked worktrees from the
|
|
437
|
-
* `*Tests.csproj` scan (a worktree is another feature's checkout; scanning it
|
|
438
|
-
* doubles the clock and reports on code outside the tree being judged).
|
|
439
|
-
*
|
|
440
|
-
* The list is never empty: "nothing to run" is expressed as ONE node already
|
|
441
|
-
* decided as `skip`, so the reason travels to the report instead of vanishing.
|
|
442
|
-
*
|
|
443
|
-
* @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`.
|
|
444
284
|
* @returns {{ nodes: TestNode[], source: string }}
|
|
445
285
|
*/
|
|
446
286
|
export function resolveTestNodes(projectPath = '.') {
|
|
447
287
|
const config = readMorphConfig(projectPath);
|
|
448
288
|
|
|
449
|
-
// `tests`
|
|
450
|
-
// 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.
|
|
451
290
|
if (config?.project?.tests !== undefined) {
|
|
452
291
|
const nodes = validateTestNodes(config, projectPath);
|
|
453
292
|
if (Array.isArray(config.project.tests) && config.project.tests.length === 0) {
|
|
454
|
-
// `[]`
|
|
455
|
-
// from an absent key, which means "guess for me".
|
|
293
|
+
// `[]` declares "no suites"; an absent key means "guess for me".
|
|
456
294
|
return {
|
|
457
295
|
nodes: [makeNode({ id: 'none', status: 'skip', reason: 'no test nodes declared' })],
|
|
458
296
|
source: 'config-tests',
|
|
@@ -469,16 +307,11 @@ export function resolveTestNodes(projectPath = '.') {
|
|
|
469
307
|
: makeNode({ id: FALLBACK_NODE_ID, status: 'skip', reason: NO_SUITE_REASON }),
|
|
470
308
|
];
|
|
471
309
|
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
// 'config'` means `project.testCommand` IS a declaration (it lives inside
|
|
475
|
-
// resolveTestPlan's own step 1), and a declaration wins completely: nothing
|
|
476
|
-
// 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`).
|
|
477
312
|
if (plan.source !== 'config') {
|
|
478
313
|
const discovered = discoverFrontendPackages(projectPath, config)
|
|
479
|
-
// The root package is already run by `npm test
|
|
480
|
-
// fallback's own answer — adding it again would run the same suite
|
|
481
|
-
// twice under two different commands.
|
|
314
|
+
// The root package is already run by the fallback's `npm test`.
|
|
482
315
|
.filter((pkg) => !(pkg.cwd === '.' && plan.source === 'npm'))
|
|
483
316
|
.map((pkg) => makeNode({ id: pkg.id, command: pkg.command, cwd: pkg.cwd, runner: pkg.runner }));
|
|
484
317
|
nodes.push(...discovered);
|
|
@@ -487,45 +320,15 @@ export function resolveTestNodes(projectPath = '.') {
|
|
|
487
320
|
return { nodes, source: plan.source };
|
|
488
321
|
}
|
|
489
322
|
|
|
490
|
-
/**
|
|
491
|
-
* Path of the config the baseline lives in. One spelling, so the writer and
|
|
492
|
-
* `readMorphConfig()` can never disagree about which file is meant — and that
|
|
493
|
-
* is now literally true: both derive from `CONFIG_FILE_NAME`
|
|
494
|
-
* (`core/state/state-manager.js`), the same constant `state-manager.js` itself
|
|
495
|
-
* reads the project identity through. It used to be a promise made by a JSDoc
|
|
496
|
-
* over two hand-typed copies of the same string.
|
|
497
|
-
*/
|
|
498
323
|
export function morphConfigPath(projectPath = '.') {
|
|
499
324
|
return join(projectPath, CONFIG_FILE_NAME);
|
|
500
325
|
}
|
|
501
326
|
|
|
502
327
|
/**
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* `MORPH.md` says a baseline is a COMMAND, never a number — and this feature
|
|
508
|
-
* stores a number. The rule survives because nobody is allowed to type it: the
|
|
509
|
-
* only writer is `verify --record-baseline`, and it writes what a real, green
|
|
510
|
-
* run measured. A hand-typed number describes what someone believed; the
|
|
511
|
-
* divergence between belief and the suite is exactly the failure the rule was
|
|
512
|
-
* written after.
|
|
513
|
-
*
|
|
514
|
-
* The write is a POINTED mutation: read → set one field per declared node →
|
|
515
|
-
* atomic `tmp.{pid}` + rename via `atomicWriteJson` (the same door
|
|
516
|
-
* `state-manager.js` uses, not a second copy of it). Every other key of
|
|
517
|
-
* `config.json` survives, because this command owns one field and nothing else.
|
|
518
|
-
*
|
|
519
|
-
* It refuses rather than improvises:
|
|
520
|
-
* - a measurement whose `count` is not an integer (**null is UNKNOWN, never
|
|
521
|
-
* 0** — a written 0 would fabricate a drop against every later run);
|
|
522
|
-
* - a measured id that is not declared (appending a node would be inventing
|
|
523
|
-
* the declaration the command came to update);
|
|
524
|
-
* - a declared node with no measurement (a baseline recorded for part of a
|
|
525
|
-
* fleet says nothing true about the rest of it);
|
|
526
|
-
*
|
|
527
|
-
* @param {string} projectPath - Project root
|
|
528
|
-
* @param {Array<{id: string, count: number|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
|
|
529
332
|
* @returns {{ recorded: boolean, reason: string|null, updates: Array<{id: string, from: number|null, to: number}>, path: string }}
|
|
530
333
|
*/
|
|
531
334
|
export function recordTestBaselines(projectPath, measurements) {
|