@polymorphism-tech/morph-spec 9.0.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +4 -62
- 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 +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +12 -4
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +6 -17
- 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 +11 -67
- 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/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/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 +2 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- 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 +2 -2
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- 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 +34 -12
- 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/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/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/docs/ui-mockups.md +4 -0
- 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 +14 -96
- 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 +2 -5
- package/src/commands/validation/validate-feature.js +5 -94
- package/src/commands/validation/verify.js +15 -98
- 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/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/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 +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- 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/dotnet-introspection.js +20 -106
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +5 -31
- package/src/lib/plan/plan-parser.js +1 -6
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +5 -23
- package/src/lib/risk-detector.js +39 -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/validators/code-budget/code-budget-validator.js +3 -43
- 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 +20 -125
- package/src/lib/validators/verify-runner.js +52 -358
- 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 +28 -275
- 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/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,26 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verify Runner — deterministic build+tests+validators+e2e cadence.
|
|
3
|
-
*
|
|
4
|
-
* v8 adds a DETERMINISTIC verification node that runs BEFORE the LLM judge
|
|
5
|
-
* in the implement loop: build + tests + validators (code, free) first; the
|
|
6
|
-
* LLM judge is only invoked once verify passes. This is a BINARY gate
|
|
7
|
-
* (pass/fail per node).
|
|
8
|
-
*
|
|
9
|
-
* The `e2e` node closes the last gap in that chain: build proves the code
|
|
10
|
-
* COMPILES, tests prove units behave, validators read structure — none of
|
|
11
|
-
* them prove the application RUNS. See e2e-check.js for why that distinction
|
|
12
|
-
* is load-bearing rather than academic.
|
|
13
|
-
*
|
|
14
|
-
* Pure orchestration — reuses (never reimplements):
|
|
15
|
-
* - runBuildCheck() from framework/hooks/shared/build-check.js
|
|
16
|
-
* - runTestSuite() from src/lib/tasks/test-runner.js (one node)
|
|
17
|
-
* - resolveTestNodes() from src/lib/tasks/test-nodes.js (the LIST of suites)
|
|
18
|
-
* - runValidation() from ./validation-runner.js
|
|
19
|
-
* - runE2eCheck() from framework/hooks/shared/e2e-check.js
|
|
20
|
-
*
|
|
21
|
-
* @module verify-runner
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
1
|
import { execFileSync } from 'child_process';
|
|
25
2
|
import { existsSync, readFileSync, rmSync } from 'fs';
|
|
26
3
|
import { join, resolve } from 'path';
|
|
@@ -44,24 +21,14 @@ import { runValidation } from './validation-runner.js';
|
|
|
44
21
|
import { checkOutputsTraceability } from './traceability-check.js';
|
|
45
22
|
import { createHeavyGate } from '../heavy/gate.js';
|
|
46
23
|
|
|
47
|
-
|
|
24
|
+
// A dotnet cold build is slow; the Stop-hook default of 30s is too short.
|
|
48
25
|
const DEFAULT_BUILD_TIMEOUT = 120_000;
|
|
49
26
|
|
|
50
|
-
// Validator issues surfaced in `feedback`, errors first; the full list stays on the node.
|
|
51
27
|
const VALIDATOR_FEEDBACK_LIMIT = 10;
|
|
52
28
|
|
|
53
|
-
/** Number of trailing output lines kept for feedback/reporting. */
|
|
54
29
|
const OUTPUT_TAIL_LINES = 20;
|
|
55
30
|
|
|
56
|
-
/**
|
|
57
|
-
* The caller asked for something impossible — not a red verification, a wrong
|
|
58
|
-
* invocation. The distinction is the exit code: `verify` answers 1 for "the
|
|
59
|
-
* code is not ready" and 2 for "this command cannot mean what you typed".
|
|
60
|
-
*
|
|
61
|
-
* It exists because `--filter` can select nothing. Running every suite (or,
|
|
62
|
-
* far worse, none of them) and reporting green would be F30 restated as a
|
|
63
|
-
* one-word typo — so the runner refuses instead of guessing.
|
|
64
|
-
*/
|
|
31
|
+
/** A wrong invocation, not a red verification: `verify` exits 2 for it, 1 for "code not ready". */
|
|
65
32
|
export class VerifyUsageError extends Error {
|
|
66
33
|
constructor(message) {
|
|
67
34
|
super(message);
|
|
@@ -71,29 +38,7 @@ export class VerifyUsageError extends Error {
|
|
|
71
38
|
}
|
|
72
39
|
|
|
73
40
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* @param {string} projectPath - Project root directory
|
|
77
|
-
* @param {string} featureName - Feature name
|
|
78
|
-
* @param {Object} [options]
|
|
79
|
-
* @param {number} [options.timeout] - Timeout in ms for the build AND tests nodes
|
|
80
|
-
* (default: 120000 for build; for tests, see resolveTestTimeout())
|
|
81
|
-
* @param {boolean} [options.skipTests] - Skip the tests node
|
|
82
|
-
* @param {boolean} [options.skipBuild] - Skip the build node
|
|
83
|
-
* @param {boolean} [options.skipE2e] - Skip the e2e node
|
|
84
|
-
* @param {boolean} [options.freshE2e] - Force a stack rebuild instead of reusing a warm one
|
|
85
|
-
* @param {boolean} [options.recordBaseline] - Measure and write `expectedCount` back into
|
|
86
|
-
* `project.tests[]` — only from a run whose every test node is green (D8)
|
|
87
|
-
* @param {boolean} [options.linux] - Run the test nodes inside a Linux container, on a COPY
|
|
88
|
-
* of the tree (see linux-runner.js for why a bind mount would prove nothing)
|
|
89
|
-
* @param {string} [options.linuxImage] - Image override for --linux
|
|
90
|
-
* @param {Function} [options._runBuildCheck] - Injectable override for runBuildCheck (testing)
|
|
91
|
-
* @param {Function} [options._runTestSuite] - Injectable override for runTestSuite (testing)
|
|
92
|
-
* @param {Function} [options._runValidation] - Injectable override for runValidation (testing)
|
|
93
|
-
* @param {Function} [options._runE2eCheck] - Injectable override for runE2eCheck (testing)
|
|
94
|
-
* @param {Function} [options._checkOutputsTraceability] - Injectable override for checkOutputsTraceability (testing)
|
|
95
|
-
* @param {Object} [options._heavyGate] - Injectable heavy-slot gate (testing); default is one per run
|
|
96
|
-
* @returns {Promise<Object>} Aggregated verify result — see module docs for shape
|
|
41
|
+
* @throws {VerifyUsageError} on an invocation that cannot mean what was typed (exit 2)
|
|
97
42
|
* @throws {HeavyBusyError} when no heavy slot (or RAM) freed within `heavy.waitTimeoutSec`
|
|
98
43
|
*/
|
|
99
44
|
export async function runVerify(projectPath, featureName, options = {}) {
|
|
@@ -128,10 +73,8 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
128
73
|
const stack = readStack(projectPath);
|
|
129
74
|
const paths = readProjectPaths(projectPath);
|
|
130
75
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// spent: a filter that matches nothing is a typo, and paying for a build to
|
|
134
|
-
// discover a typo is the worst of both outcomes.
|
|
76
|
+
// Usage errors are judged before the build: paying for a build to discover a typo
|
|
77
|
+
// is the worst ordering, and a refusal after the cache was touched leaves a half-rewritten tree.
|
|
135
78
|
const testPlan = resolveTestNodes(projectPath);
|
|
136
79
|
const filter = parseTestFilter(options.filter);
|
|
137
80
|
if (filter && !testPlan.nodes.some((node) => nodeMatchesFilter(node, filter))) {
|
|
@@ -141,39 +84,23 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
141
84
|
);
|
|
142
85
|
}
|
|
143
86
|
|
|
144
|
-
// `--record-baseline` is judged in the same breath, and for the same reason:
|
|
145
|
-
// discovering that there is nowhere to write AFTER running the whole fleet
|
|
146
|
-
// would charge the user a full suite for a usage error. `config.json` is
|
|
147
|
-
// never created here — this repository deliberately has none, and inventing
|
|
148
|
-
// one would break `doctor --full`.
|
|
149
87
|
if (recordBaseline) {
|
|
150
88
|
const problem = baselineTargetProblem(projectPath, testPlan, { skipTests });
|
|
151
89
|
if (problem) throw new VerifyUsageError(problem);
|
|
152
90
|
}
|
|
153
91
|
|
|
154
|
-
// `--refresh-cache` is judged in the same breath as `--filter` and
|
|
155
|
-
// `--record-baseline`, and for the same reason — but here the stake is higher
|
|
156
|
-
// than a wasted clock: a refusal AFTER the cache was touched would leave a
|
|
157
|
-
// half-rewritten tree behind. Same discipline as `finish --merge`, which
|
|
158
|
-
// validates the root BEFORE archiving.
|
|
159
92
|
if (refreshCache) {
|
|
160
93
|
const problem = refreshCacheProblem(projectPath, testPlan, filter, { skipTests });
|
|
161
94
|
if (problem) throw new VerifyUsageError(problem);
|
|
162
95
|
}
|
|
163
96
|
|
|
164
|
-
//
|
|
165
|
-
// skip: "we could not check on Linux" reading as green is F35 itself.
|
|
97
|
+
// Missing Docker is a usage error, never a skip: "could not check on Linux" must not read as green.
|
|
166
98
|
if (linux) {
|
|
167
99
|
const problem = checkLinuxPreconditions({
|
|
168
100
|
projectPath, skipTests, ...(_exec ? { exec: _exec } : {}),
|
|
169
101
|
});
|
|
170
102
|
if (problem) throw new VerifyUsageError(problem);
|
|
171
103
|
|
|
172
|
-
// The MORPH_EVAL_* channel carries ABSOLUTE host paths, and the container
|
|
173
|
-
// runs a COPY of the tree at /work — so those paths do not exist inside it.
|
|
174
|
-
// Refusing by name is the only honest answer: running the node anyway would
|
|
175
|
-
// fail with an unrelated message, and skipping it would be the green-over-
|
|
176
|
-
// nothing shape this feature exists to remove. The escape is named.
|
|
177
104
|
const evalNode = testPlan.nodes.find(
|
|
178
105
|
(node) => node.runner === EVAL_RUNNER && nodeMatchesFilter(node, filter)
|
|
179
106
|
);
|
|
@@ -189,15 +116,11 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
189
116
|
const nodes = {};
|
|
190
117
|
const feedback = [];
|
|
191
118
|
|
|
192
|
-
// 1. build
|
|
193
119
|
nodes.build = await runBuildNode({ projectPath, stack, paths, timeout, skipBuild, gate, _runBuildCheck });
|
|
194
120
|
appendBuildFeedback(feedback, nodes.build);
|
|
195
121
|
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// typed one: `timeout` above is defaulted for the build, and passing that
|
|
199
|
-
// default down would silently outrank config.project.testTimeoutMs and the
|
|
200
|
-
// per-project scaling (see resolveTestTimeout()).
|
|
122
|
+
// Only a TYPED --timeout reaches the tests: the build default would outrank
|
|
123
|
+
// config.project.testTimeoutMs and the per-project scaling.
|
|
201
124
|
const explicitTimeout = Number.isFinite(options.timeout) ? options.timeout : undefined;
|
|
202
125
|
const stackNodes = [];
|
|
203
126
|
nodes.tests = await runTestsNode({
|
|
@@ -218,13 +141,9 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
218
141
|
});
|
|
219
142
|
if (stackNodes.length === 0) appendTestsFeedback(feedback, nodes.tests);
|
|
220
143
|
|
|
221
|
-
// 3. validators — ALWAYS runs, even on a red build (free static feedback).
|
|
222
144
|
nodes.validators = await runValidatorsNode({ projectPath, featureName, _runValidation });
|
|
223
145
|
appendValidatorsFeedback(feedback, nodes.validators);
|
|
224
146
|
|
|
225
|
-
// 4. e2e — the only node that runs the application. Like tests, it is not
|
|
226
|
-
// invoked at all on a red build: a stack that cannot compile cannot come up,
|
|
227
|
-
// and the docker output would bury the actual compile error.
|
|
228
147
|
const e2e = await runE2eNode({
|
|
229
148
|
projectPath,
|
|
230
149
|
featureName,
|
|
@@ -237,7 +156,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
237
156
|
nodes.e2e = e2e.node;
|
|
238
157
|
appendE2eFeedback(feedback, nodes.e2e);
|
|
239
158
|
|
|
240
|
-
// `e2e: true` suites need the stack the e2e node just brought up (and left up).
|
|
241
159
|
if (stackNodes.length > 0) {
|
|
242
160
|
nodes.tests = await runStackTestNodes({
|
|
243
161
|
projectPath, testsNode: nodes.tests, stackNodes, e2eNode: nodes.e2e, stackEnv: e2e.stackEnv,
|
|
@@ -246,14 +164,12 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
246
164
|
appendTestsFeedback(feedback, nodes.tests);
|
|
247
165
|
}
|
|
248
166
|
|
|
249
|
-
//
|
|
250
|
-
//
|
|
167
|
+
// Outside `nodes` on purpose: status comes only from the nodes, so a traceability
|
|
168
|
+
// warning never turns red.
|
|
251
169
|
const traceability = _checkOutputsTraceability(projectPath, featureName);
|
|
252
170
|
|
|
253
171
|
const status = Object.values(nodes).some((n) => n.status === 'fail') ? 'fail' : 'pass';
|
|
254
172
|
|
|
255
|
-
// Recording happens LAST and only from what actually ran — the number is a
|
|
256
|
-
// measurement of this invocation, never a value carried in from anywhere.
|
|
257
173
|
const baseline = recordBaseline ? recordBaselineFrom(projectPath, nodes.tests) : null;
|
|
258
174
|
if (baseline && !baseline.recorded) {
|
|
259
175
|
feedback.push(`[baseline] não gravada: ${baseline.reason}`);
|
|
@@ -271,10 +187,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
271
187
|
};
|
|
272
188
|
}
|
|
273
189
|
|
|
274
|
-
// ============================================================================
|
|
275
|
-
// Build node
|
|
276
|
-
// ============================================================================
|
|
277
|
-
|
|
278
190
|
/**
|
|
279
191
|
* A root `dotnet build` with no solution at the root fails MSB1003: build the
|
|
280
192
|
* repository's single solution instead, or say which candidates and which key decide.
|
|
@@ -325,34 +237,13 @@ function appendBuildFeedback(feedback, buildNode) {
|
|
|
325
237
|
}
|
|
326
238
|
}
|
|
327
239
|
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
// ============================================================================
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Run EVERY test suite the project declares, and roll the results up.
|
|
334
|
-
*
|
|
335
|
-
* Why a rollup instead of one node: defect F30. The resolver answered
|
|
336
|
-
* "dotnet test", the 3.082 vitest tests of the same repository never ran, and
|
|
337
|
-
* the gate reported green — a suite nobody could name is a suite that cannot
|
|
338
|
-
* fail. `resolveTestNodes()` now returns the LIST, each entry runs with its own
|
|
339
|
-
* command, cwd and clock, and every one of them appears in `runs[]`.
|
|
340
|
-
*
|
|
341
|
-
* `nodes.tests` deliberately stays an OBJECT with `status` (D6): verify.js and
|
|
342
|
-
* the persisted stamp read `nodes.tests.status`, and turning a verification
|
|
343
|
-
* defect into a shape defect is not progress. The plural detail lives in
|
|
344
|
-
* `runs[]` beside it.
|
|
345
|
-
*
|
|
346
|
-
* @returns {Object} The rollup: `status`, joined `command`, summed `durationMs`,
|
|
347
|
-
* `source`, `environment` (null outside a container) and `runs[]`.
|
|
348
|
-
*/
|
|
240
|
+
// Every declared suite runs and appears in `runs[]`; the rollup stays an object
|
|
241
|
+
// with `status` because verify.js and the persisted stamp read `nodes.tests.status`.
|
|
349
242
|
async function runTestsNode({
|
|
350
243
|
projectPath, testPlan, filter, skipTests, buildNode, timeout, refreshCache = false,
|
|
351
244
|
linux = false, linuxImage = null, gate, stackNodes = [], _runTestSuite, _runLinuxSuite = runLinuxSuite, _exec,
|
|
352
245
|
}) {
|
|
353
246
|
const { nodes, source } = testPlan;
|
|
354
|
-
// Kept readable for whoever only prints the string — the same join the
|
|
355
|
-
// fallback scan already used for a chain of test projects.
|
|
356
247
|
const command = nodes.map((n) => n.command).filter(Boolean).join(' && ') || null;
|
|
357
248
|
// A fresh `runs: []` per return — two rollups must never alias one array.
|
|
358
249
|
const nothingRan = (reason) => ({
|
|
@@ -406,19 +297,8 @@ async function runStackTestNodes({ projectPath, testsNode, stackNodes, e2eNode,
|
|
|
406
297
|
return rollUpTestRuns(runs, { command: testsNode.command, source: testsNode.source, environment: testsNode.environment });
|
|
407
298
|
}
|
|
408
299
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
* SAME rules the local path uses.
|
|
412
|
-
*
|
|
413
|
-
* The pre-decisions (invalid config, `--filter`, the eval pre-flight) happen HERE, before the container exists: a node that must not run must not
|
|
414
|
-
* be written into the container script either, or the flag would quietly run
|
|
415
|
-
* what the user filtered out. What the container returns — an exit code and an
|
|
416
|
-
* output — is then handed to `judgeRun()`, the same function the host path
|
|
417
|
-
* calls, so a count drop means a count drop in both environments.
|
|
418
|
-
*
|
|
419
|
-
* The tree the container executes is a COPY on its own filesystem, never the
|
|
420
|
-
* bind mount: see linux-runner.js. That is the whole reason this path exists.
|
|
421
|
-
*/
|
|
300
|
+
// Pre-decisions run before the container exists, so a filtered node is never written
|
|
301
|
+
// into its script; results go through the same `judgeRun()` as the host path.
|
|
422
302
|
async function runNodesInLinux({
|
|
423
303
|
projectPath, nodes, filter, timeout, linuxImage, gate, _runLinuxSuite, _exec,
|
|
424
304
|
}) {
|
|
@@ -430,8 +310,7 @@ async function runNodesInLinux({
|
|
|
430
310
|
else toRun.push(node);
|
|
431
311
|
}
|
|
432
312
|
|
|
433
|
-
// One container, one clock: the
|
|
434
|
-
// have been given locally, since they share the run.
|
|
313
|
+
// One container, one clock: the sum of each node's local timeout.
|
|
435
314
|
const containerTimeout = toRun.reduce(
|
|
436
315
|
(sum, node) => sum + resolveTestTimeout(projectPath, node, timeout), 0
|
|
437
316
|
) || undefined;
|
|
@@ -450,16 +329,13 @@ async function runNodesInLinux({
|
|
|
450
329
|
if (decided.has(node.id)) return decided.get(node.id);
|
|
451
330
|
const raw = byId.get(node.id);
|
|
452
331
|
if (!raw) {
|
|
453
|
-
// The container answered about a fleet that does not include this node.
|
|
454
|
-
// Silence here would be a suite vanishing from the report.
|
|
455
332
|
return { ...emptyRunEntry(node), status: 'fail', reason: 'o container não reportou este nó' };
|
|
456
333
|
}
|
|
457
334
|
if (raw.skipped) {
|
|
458
335
|
return { ...emptyRunEntry(node), reason: raw.reason };
|
|
459
336
|
}
|
|
460
337
|
const result = raw.exitCode === null
|
|
461
|
-
// No exit code is NOT a pass: the container died before this node
|
|
462
|
-
// reported, and a green verdict over an unobserved suite is F30's shape.
|
|
338
|
+
// No exit code is NOT a pass: the container died before this node reported.
|
|
463
339
|
? { passed: false, output: raw.output || '', reason: raw.reason, exitCode: null }
|
|
464
340
|
: { passed: raw.exitCode === 0, output: raw.output || '', exitCode: raw.exitCode };
|
|
465
341
|
return judgeRun({ node, result, durationMs: raw.durationMs ?? 0 });
|
|
@@ -468,30 +344,17 @@ async function runNodesInLinux({
|
|
|
468
344
|
return { runs, environment: suite.environment };
|
|
469
345
|
}
|
|
470
346
|
|
|
471
|
-
/**
|
|
472
|
-
* Execute (or decline to execute) one node, always producing a `runs[]` entry.
|
|
473
|
-
*
|
|
474
|
-
* Nothing is ever dropped: a node that must not run says so with a reason, in
|
|
475
|
-
* the place the reader is already looking. A suite that silently disappears
|
|
476
|
-
* from the report is the defect this whole feature exists to close.
|
|
477
|
-
*/
|
|
478
347
|
async function runOneTestNode({
|
|
479
348
|
projectPath, node, filter, timeout, refreshCache = false, gate, env = null, _runTestSuite,
|
|
480
349
|
}) {
|
|
481
350
|
const decided = preDecideRun({ node, filter, projectPath, refreshCache });
|
|
482
351
|
if (decided) return decided;
|
|
483
352
|
|
|
484
|
-
// The eval channel: the CLI DICTATES where the cache, the dataset and the
|
|
485
|
-
// summary live, so the same xunit suite is runnable by `verify` without a
|
|
486
|
-
// second configuration path. `env` is undefined for every other runner, which
|
|
487
|
-
// keeps `runTestSuite` on exactly the path it had before.
|
|
488
353
|
const evalChannel = node.runner === EVAL_RUNNER
|
|
489
354
|
? evalNodeEnv(node, projectPath, { refresh: refreshCache })
|
|
490
355
|
: null;
|
|
491
|
-
// The summary path is fixed per node id and pid
|
|
492
|
-
//
|
|
493
|
-
// process, a recycled pid) was read as THIS run's, handing a harness that
|
|
494
|
-
// wrote nothing the old scenario count — and hiding an empty run behind it.
|
|
356
|
+
// The summary path is fixed per node id and pid: a file left by an earlier run
|
|
357
|
+
// would be read as this run's and hide an empty run behind the old count.
|
|
495
358
|
if (evalChannel) discardFile(evalChannel.summaryPath);
|
|
496
359
|
|
|
497
360
|
gate.enter('tests');
|
|
@@ -508,26 +371,12 @@ async function runOneTestNode({
|
|
|
508
371
|
return judged;
|
|
509
372
|
}
|
|
510
373
|
|
|
511
|
-
/** Remove a scratch file the CLI owns; a file that is not there (or will not go) changes nothing. */
|
|
512
374
|
function discardFile(path) {
|
|
513
|
-
try { rmSync(path, { force: true }); } catch { /*
|
|
375
|
+
try { rmSync(path, { force: true }); } catch { /* the next run removes it first */ }
|
|
514
376
|
}
|
|
515
377
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
* code said.
|
|
519
|
-
*
|
|
520
|
-
* Two things the exit code cannot see:
|
|
521
|
-
* - the scenario COUNT, which is what `expectedCount` is compared against and
|
|
522
|
-
* what `--record-baseline` writes. Read ONLY from the summary: the `dotnet`
|
|
523
|
-
* extractor counts xunit cases, another unit — compared with (or recorded as)
|
|
524
|
-
* a scenario baseline it failed green runs as drops (#110, #117 review). No
|
|
525
|
-
* readable summary → **null, UNKNOWN, never 0** — an unreadable summary must
|
|
526
|
-
* not manufacture a count drop;
|
|
527
|
-
* - a suite that evaluated ZERO scenarios and exited 0. Nothing failed, because
|
|
528
|
-
* nothing ran: the guardrail-inert shape this repository has already paid to
|
|
529
|
-
* learn to detect.
|
|
530
|
-
*/
|
|
378
|
+
// The scenario count comes ONLY from the summary (the dotnet extractor counts xunit
|
|
379
|
+
// cases, another unit); no readable summary is null, UNKNOWN, never 0.
|
|
531
380
|
function judgeEvalRun(entry, node, summaryPath) {
|
|
532
381
|
const summary = readEvalSummary(summaryPath);
|
|
533
382
|
const warnings = [...(entry.warnings || [])];
|
|
@@ -539,11 +388,8 @@ function judgeEvalRun(entry, node, summaryPath) {
|
|
|
539
388
|
const verdict = compareTestCount(count, node.expectedCount);
|
|
540
389
|
if (verdict.warning) warnings.push(verdict.warning);
|
|
541
390
|
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
// `reason` nunca chegaria ao `feedback[]`; e a cauda do `dotnet` e ruido
|
|
545
|
-
// truncado exatamente onde o sumario ja tem a frase certa. Prepend, e nao
|
|
546
|
-
// substituicao, porque um crash do runner tambem vive nessa cauda.
|
|
391
|
+
// Named failures go into `output` (feedback prefers it to `reason`), prepended
|
|
392
|
+
// rather than replacing it because a runner crash also lives in that tail.
|
|
547
393
|
const named = evalFailureLines(summary);
|
|
548
394
|
const withNames = named.length > 0
|
|
549
395
|
? { ...entry, output: [...named, ...(entry.output ? [entry.output] : [])].join('\n') }
|
|
@@ -562,7 +408,6 @@ function judgeEvalRun(entry, node, summaryPath) {
|
|
|
562
408
|
return { ...withNames, count, warnings };
|
|
563
409
|
}
|
|
564
410
|
|
|
565
|
-
/** The `runs[]` entry every path starts from — every field present, nothing implied. */
|
|
566
411
|
function emptyRunEntry(node) {
|
|
567
412
|
return {
|
|
568
413
|
id: node.id,
|
|
@@ -570,8 +415,7 @@ function emptyRunEntry(node) {
|
|
|
570
415
|
cwd: node.cwd,
|
|
571
416
|
runner: node.runner,
|
|
572
417
|
expectedCount: node.expectedCount,
|
|
573
|
-
//
|
|
574
|
-
// optional chaining. **null is UNKNOWN, never 0** (D7).
|
|
418
|
+
// null is UNKNOWN, never 0.
|
|
575
419
|
count: null,
|
|
576
420
|
status: 'skip',
|
|
577
421
|
durationMs: 0,
|
|
@@ -581,36 +425,22 @@ function emptyRunEntry(node) {
|
|
|
581
425
|
};
|
|
582
426
|
}
|
|
583
427
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
* has to run to be judged.
|
|
587
|
-
*
|
|
588
|
-
* Shared by the local loop and by `--linux`, so a node that must not run is
|
|
589
|
-
* refused for the same reason and with the same words in both environments —
|
|
590
|
-
* two copies of this ordering is how a filtered node ends up invisible in one
|
|
591
|
-
* of them.
|
|
592
|
-
*/
|
|
428
|
+
// Verdicts decided before execution, shared by the host loop and `--linux` so both
|
|
429
|
+
// refuse for the same reason with the same words; null means the node must run.
|
|
593
430
|
function preDecideRun({ node, filter, projectPath = '.', refreshCache = false }) {
|
|
594
431
|
const entry = emptyRunEntry(node);
|
|
595
432
|
|
|
596
|
-
// The resolver already decided: invalid config (fail-closed, D4) or an
|
|
597
|
-
// explicit "no suites declared". Executing it would be asking a question that
|
|
598
|
-
// has been answered — and, for an invalid node, answering it wrongly.
|
|
599
433
|
if (node.status) {
|
|
600
434
|
return { ...entry, status: node.status, reason: node.reason };
|
|
601
435
|
}
|
|
602
436
|
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
// invalid node costs no clock and still reddens the gate (D4).
|
|
437
|
+
// After the pre-decided verdict on purpose: an invalid node costs no clock and
|
|
438
|
+
// still reddens the gate even when filtered out.
|
|
606
439
|
if (!nodeMatchesFilter(node, filter)) {
|
|
607
440
|
return { ...entry, reason: `filtered out (--filter ${filter.raw})` };
|
|
608
441
|
}
|
|
609
442
|
|
|
610
|
-
//
|
|
611
|
-
// loop and the `--linux` path refuse for the same reason with the same words.
|
|
612
|
-
// Its three cache verdicts are FAILURES, never skips: a skip over an unusable
|
|
613
|
-
// cache is a green gate above a verification that never happened.
|
|
443
|
+
// A cache miss is a fail, never a skip: a skip would be green over nothing.
|
|
614
444
|
if (node.runner === EVAL_RUNNER) {
|
|
615
445
|
const pre = evalPreflight(projectPath, node, { refresh: refreshCache });
|
|
616
446
|
if (pre.status) return { ...entry, status: pre.status, reason: pre.reason };
|
|
@@ -619,14 +449,7 @@ function preDecideRun({ node, filter, projectPath = '.', refreshCache = false })
|
|
|
619
449
|
return null;
|
|
620
450
|
}
|
|
621
451
|
|
|
622
|
-
|
|
623
|
-
* Turn ONE executor result into a `runs[]` entry — the count rule included.
|
|
624
|
-
*
|
|
625
|
-
* Called by both the local loop and the `--linux` container path on purpose:
|
|
626
|
-
* a drop, a rise and an unknown count must mean the same thing whether the
|
|
627
|
-
* suite ran on this host or inside a container. A second copy of this judgement
|
|
628
|
-
* is how two environments start disagreeing about the same suite.
|
|
629
|
-
*/
|
|
452
|
+
// The single count rule for host and container runs; a second copy makes them disagree.
|
|
630
453
|
function judgeRun({ node, result, durationMs }) {
|
|
631
454
|
const entry = emptyRunEntry(node);
|
|
632
455
|
|
|
@@ -634,8 +457,7 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
634
457
|
return { ...entry, durationMs, reason: result.reason || 'no test command detected' };
|
|
635
458
|
}
|
|
636
459
|
|
|
637
|
-
// A
|
|
638
|
-
// a passing run cut mid-sentence. Lead with what actually happened.
|
|
460
|
+
// A truncated log tail reads like a passing run cut mid-sentence: lead with the timeout.
|
|
639
461
|
if (result.timedOut) {
|
|
640
462
|
const tail = tailLines(result.output, OUTPUT_TAIL_LINES);
|
|
641
463
|
return {
|
|
@@ -648,29 +470,19 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
648
470
|
};
|
|
649
471
|
}
|
|
650
472
|
|
|
651
|
-
//
|
|
652
|
-
// baseline. Read from the FULL output, before it is truncated to a tail —
|
|
653
|
-
// the summary line is the last thing a runner prints, but a red run's tail
|
|
654
|
-
// is chosen for the failures, not for the summary.
|
|
473
|
+
// Parsed from the FULL output, before truncation to a tail.
|
|
655
474
|
const parsed = parseTestCount(result.output, node.runner);
|
|
656
475
|
const count = parsed ? parsed.total : null;
|
|
657
476
|
|
|
658
|
-
// An eval node's unit is the SCENARIO
|
|
659
|
-
//
|
|
660
|
-
// Comparing here too, with the extractor's count of xunit cases, failed a
|
|
661
|
-
// 24/24 run as "esperado 24, executado 2" — and `judgeEvalRun` only turns a
|
|
662
|
-
// pass into a fail, never the reverse.
|
|
477
|
+
// An eval node's unit is the SCENARIO, compared only by `judgeEvalRun`; the
|
|
478
|
+
// extractor's xunit-case count against that baseline would fail green runs.
|
|
663
479
|
const verdict = compareTestCount(count, node.runner === EVAL_RUNNER ? null : node.expectedCount);
|
|
664
480
|
const warnings = verdict.warning ? [verdict.warning] : [];
|
|
665
481
|
|
|
666
|
-
//
|
|
667
|
-
// used to run no longer do. A RISE never fails (D7) — the suite grows with
|
|
668
|
-
// every task, and failing on growth teaches deleting tests.
|
|
482
|
+
// Only a drop fails a node; a rise never does, or failing on growth teaches deleting tests.
|
|
669
483
|
if (verdict.verdict === 'drop' && result.passed) {
|
|
670
484
|
return { ...entry, status: 'fail', durationMs, count, warnings, reason: verdict.reason, output: '' };
|
|
671
485
|
}
|
|
672
|
-
// Already red: the failing test is the better lead, so the drop travels
|
|
673
|
-
// beside it instead of overwriting the reason.
|
|
674
486
|
if (verdict.verdict === 'drop') warnings.push(verdict.reason);
|
|
675
487
|
|
|
676
488
|
return {
|
|
@@ -679,23 +491,13 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
679
491
|
durationMs,
|
|
680
492
|
count,
|
|
681
493
|
warnings,
|
|
682
|
-
// `test-build-failed` / `stale-binary`:
|
|
683
|
-
// test ran, and the reason is the only thing worth reading. Without this
|
|
684
|
-
// the node would go red with a null reason and the reader would go hunting
|
|
685
|
-
// through a compiler log for a suite that never started.
|
|
494
|
+
// `test-build-failed` / `stale-binary`: refused before any test ran.
|
|
686
495
|
reason: result.reason ?? null,
|
|
687
496
|
output: result.passed ? '' : tailLines(result.output, OUTPUT_TAIL_LINES),
|
|
688
497
|
};
|
|
689
498
|
}
|
|
690
499
|
|
|
691
|
-
|
|
692
|
-
* Aggregate the per-node runs into the single node `verify` publishes.
|
|
693
|
-
*
|
|
694
|
-
* The rollup is pessimistic on purpose: ANY red run reddens it. A green
|
|
695
|
-
* aggregate covering a red suite is F30 restated, so `some(fail)` is checked
|
|
696
|
-
* before anything else. `skip` only survives when there is nothing else to
|
|
697
|
-
* report — one green run among skips is a pass.
|
|
698
|
-
*/
|
|
500
|
+
// Any red run reddens the rollup; one green run among skips is a pass.
|
|
699
501
|
function rollUpTestRuns(runs, { command, source, environment = null }) {
|
|
700
502
|
const failed = runs.filter((r) => r.status === 'fail');
|
|
701
503
|
const status = failed.length > 0
|
|
@@ -709,13 +511,8 @@ function rollUpTestRuns(runs, { command, source, environment = null }) {
|
|
|
709
511
|
reason: null,
|
|
710
512
|
output: failed.map((r) => r.output).filter(Boolean).join('\n'),
|
|
711
513
|
source,
|
|
712
|
-
//
|
|
713
|
-
// by name and image, because a green from a container and a green from the
|
|
714
|
-
// host are otherwise indistinguishable in feature.json — and the next
|
|
715
|
-
// person would not know what was proven.
|
|
514
|
+
// null is the local host; a container run is named so feature.json says what was proven.
|
|
716
515
|
environment,
|
|
717
|
-
// Named by suite: a warning that does not say WHICH baseline went stale
|
|
718
|
-
// sends the reader to read every one of them.
|
|
719
516
|
warnings: runs.flatMap((r) => (r.warnings || []).map((w) => `${r.id}: ${w}`)),
|
|
720
517
|
runs,
|
|
721
518
|
};
|
|
@@ -729,17 +526,8 @@ function rollUpTestRuns(runs, { command, source, environment = null }) {
|
|
|
729
526
|
return node;
|
|
730
527
|
}
|
|
731
528
|
|
|
732
|
-
/**
|
|
733
|
-
* One feedback line per RED node, named by node id.
|
|
734
|
-
*
|
|
735
|
-
* The `[tests/{id}]` prefix replaces the old bare `[tests]`: with N suites, a
|
|
736
|
-
* line that does not say which one failed sends the reader to the wrong repo
|
|
737
|
-
* half.
|
|
738
|
-
*/
|
|
739
529
|
function appendTestsFeedback(feedback, testsNode) {
|
|
740
|
-
// Warnings
|
|
741
|
-
// GREEN node by construction (the count rose), so a warning that only
|
|
742
|
-
// printed on failure would never print at all.
|
|
530
|
+
// Warnings print whatever the verdict: a stale baseline lives on a GREEN node.
|
|
743
531
|
for (const run of testsNode.runs || []) {
|
|
744
532
|
for (const warning of run.warnings || []) {
|
|
745
533
|
feedback.push(`[tests/${run.id}] warning: ${warning}`);
|
|
@@ -754,31 +542,7 @@ function appendTestsFeedback(feedback, testsNode) {
|
|
|
754
542
|
}
|
|
755
543
|
}
|
|
756
544
|
|
|
757
|
-
|
|
758
|
-
* Why `--refresh-cache` cannot mean what it says, or null when it can.
|
|
759
|
-
*
|
|
760
|
-
* Judged BEFORE the build node, and the ordering is the whole point: a refusal
|
|
761
|
-
* that arrived after the cache was touched would leave a half-rewritten tree
|
|
762
|
-
* behind. Same discipline `finish --merge` learned — validate the precondition
|
|
763
|
-
* before the mutation, never after.
|
|
764
|
-
*
|
|
765
|
-
* Three refusals, all usage errors (exit 2), never verdicts:
|
|
766
|
-
* - no eval node declared: succeeding silently would teach the user to type a
|
|
767
|
-
* flag that does nothing;
|
|
768
|
-
* - `--skip-tests`: the two flags cancel each other, and the winner would be
|
|
769
|
-
* whichever the code happened to check first;
|
|
770
|
-
* - a filter that excludes every eval node: same shape as the first.
|
|
771
|
-
*
|
|
772
|
-
* The MISSING CREDENTIAL is deliberately NOT judged here: it is per node (each
|
|
773
|
-
* declares its own `envFile`), so `evalPreflight` answers it — still before the
|
|
774
|
-
* command runs, and still without touching the tree.
|
|
775
|
-
*
|
|
776
|
-
* @param {string} projectPath
|
|
777
|
-
* @param {{nodes: Array}} testPlan
|
|
778
|
-
* @param {{tokens: string[], raw: string}|null} filter
|
|
779
|
-
* @param {{skipTests: boolean}} opts
|
|
780
|
-
* @returns {string|null}
|
|
781
|
-
*/
|
|
545
|
+
// The missing credential is judged per node by `evalPreflight`, not here.
|
|
782
546
|
function refreshCacheProblem(projectPath, testPlan, filter, { skipTests }) {
|
|
783
547
|
if (skipTests) {
|
|
784
548
|
return '--refresh-cache com --skip-tests: as duas flags se anulam — a regravação da cache É a suíte de eval';
|
|
@@ -798,30 +562,8 @@ function refreshCacheProblem(projectPath, testPlan, filter, { skipTests }) {
|
|
|
798
562
|
return null;
|
|
799
563
|
}
|
|
800
564
|
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
// ============================================================================
|
|
804
|
-
|
|
805
|
-
/**
|
|
806
|
-
* Why `--record-baseline` cannot run here, or null when it can.
|
|
807
|
-
*
|
|
808
|
-
* Judged BEFORE the build node, beside the `--filter` check, because both are
|
|
809
|
-
* statements about a command that cannot mean what it says. Paying for a full
|
|
810
|
-
* suite to be told there was nowhere to write the result is the worst possible
|
|
811
|
-
* ordering.
|
|
812
|
-
*
|
|
813
|
-
* The refusal for a project with no `project.tests[]` is deliberate rather
|
|
814
|
-
* than helpful: the nodes the fallback synthesizes are not declared anywhere,
|
|
815
|
-
* so there is no entry to update, and writing one would mean this command
|
|
816
|
-
* inventing the declaration it came to maintain. `config.json` is likewise
|
|
817
|
-
* never created — the framework repository has none on purpose (creating it
|
|
818
|
-
* breaks `doctor --full`), so the honest answer is a named usage error.
|
|
819
|
-
*
|
|
820
|
-
* @param {string} projectPath
|
|
821
|
-
* @param {{ nodes: Array, source: string }} testPlan
|
|
822
|
-
* @param {{ skipTests: boolean }} opts
|
|
823
|
-
* @returns {string|null}
|
|
824
|
-
*/
|
|
565
|
+
// Never creates config.json nor a `project.tests[]` entry: fallback nodes have no
|
|
566
|
+
// declaration to update, and inventing one is not this command's job.
|
|
825
567
|
function baselineTargetProblem(projectPath, testPlan, { skipTests }) {
|
|
826
568
|
if (skipTests) {
|
|
827
569
|
return '--record-baseline com --skip-tests não mede nada — uma baseline vem de uma execução, não de uma omissão';
|
|
@@ -837,20 +579,8 @@ function baselineTargetProblem(projectPath, testPlan, { skipTests }) {
|
|
|
837
579
|
return null;
|
|
838
580
|
}
|
|
839
581
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
*
|
|
843
|
-
* D8 lives here: the number is only ever what a REAL run measured, and only
|
|
844
|
-
* when every node of that run is green. A red run records nothing — writing a
|
|
845
|
-
* lower count over a regression is how the regression becomes the new normal.
|
|
846
|
-
* A PARTIAL run records nothing either: `--filter` and skips leave nodes
|
|
847
|
-
* unmeasured, and a baseline covering half the fleet is a claim about the
|
|
848
|
-
* other half that nobody made.
|
|
849
|
-
*
|
|
850
|
-
* @param {string} projectPath
|
|
851
|
-
* @param {Object} testsNode - The rollup produced by runTestsNode()
|
|
852
|
-
* @returns {{ recorded: boolean, reason: string|null, updates: Array, path?: string }}
|
|
853
|
-
*/
|
|
582
|
+
// Only a run with EVERY node green records: a red run would freeze the regression,
|
|
583
|
+
// a partial one would claim counts for nodes nobody measured.
|
|
854
584
|
function recordBaselineFrom(projectPath, testsNode) {
|
|
855
585
|
const runs = testsNode.runs || [];
|
|
856
586
|
const refuse = (reason) => ({ recorded: false, reason, updates: [] });
|
|
@@ -869,16 +599,10 @@ function recordBaselineFrom(projectPath, testsNode) {
|
|
|
869
599
|
return recordTestBaselines(projectPath, runs.map((r) => ({ id: r.id, count: r.count })));
|
|
870
600
|
}
|
|
871
601
|
|
|
872
|
-
// ============================================================================
|
|
873
|
-
// Validators node
|
|
874
|
-
// ============================================================================
|
|
875
|
-
|
|
876
602
|
async function runValidatorsNode({ projectPath, featureName, _runValidation }) {
|
|
877
603
|
const start = Date.now();
|
|
878
604
|
|
|
879
|
-
// runValidation() prints
|
|
880
|
-
// JSON — silence console during the call and ALWAYS restore it, even if
|
|
881
|
-
// runValidation throws.
|
|
605
|
+
// runValidation() prints progress to stdout, which would corrupt verify's JSON.
|
|
882
606
|
const originalLog = console.log;
|
|
883
607
|
const originalWarn = console.warn;
|
|
884
608
|
let validationResult;
|
|
@@ -917,8 +641,7 @@ async function runValidatorsNode({ projectPath, featureName, _runValidation }) {
|
|
|
917
641
|
}
|
|
918
642
|
|
|
919
643
|
function appendValidatorsFeedback(feedback, validatorsNode) {
|
|
920
|
-
// A validator that could not check
|
|
921
|
-
// green node; when the whole node skipped, its own line already says why.
|
|
644
|
+
// A validator that could not check is invisible in a green node otherwise.
|
|
922
645
|
if (validatorsNode.status !== 'skip') {
|
|
923
646
|
for (const s of validatorsNode.skipped || []) {
|
|
924
647
|
if (s.unavailable) feedback.push(`[validators/${s.validator}] não rodou: ${s.reason}`);
|
|
@@ -935,10 +658,6 @@ function appendValidatorsFeedback(feedback, validatorsNode) {
|
|
|
935
658
|
}
|
|
936
659
|
}
|
|
937
660
|
|
|
938
|
-
// ============================================================================
|
|
939
|
-
// E2E node
|
|
940
|
-
// ============================================================================
|
|
941
|
-
|
|
942
661
|
async function runE2eNode({ projectPath, featureName, skipE2e, freshE2e, buildNode, gate, _runE2eCheck }) {
|
|
943
662
|
if (buildNode.status === 'fail') {
|
|
944
663
|
return {
|
|
@@ -955,10 +674,7 @@ async function runE2eNode({ projectPath, featureName, skipE2e, freshE2e, buildNo
|
|
|
955
674
|
|
|
956
675
|
const config = readConfig(projectPath);
|
|
957
676
|
const gitCommonDir = resolveGitCommonDir(projectPath);
|
|
958
|
-
//
|
|
959
|
-
// runs in the loop, and two worktrees verifying at once would otherwise get
|
|
960
|
-
// distinct container names and then collide on host port 3000 — a red gate
|
|
961
|
-
// over an infrastructure clash instead of a defect.
|
|
677
|
+
// Without the port block, two worktrees verifying at once collide on host ports.
|
|
962
678
|
const env = buildComposeEnv({ cwd: projectPath, config, featureName, gitCommonDir });
|
|
963
679
|
|
|
964
680
|
const start = Date.now();
|
|
@@ -971,9 +687,7 @@ async function runE2eNode({ projectPath, featureName, skipE2e, freshE2e, buildNo
|
|
|
971
687
|
forceFresh: freshE2e,
|
|
972
688
|
beforeHeavy: () => gate.enter('e2e'),
|
|
973
689
|
countSpecs: (output) => parseTestCount(output, 'playwright')?.total ?? null,
|
|
974
|
-
//
|
|
975
|
-
// container per verify is the difference between a usable loop and one
|
|
976
|
-
// nobody runs. `morph-spec e2e down` (and `finish`) tear it down.
|
|
690
|
+
// Cold-starting a .NET container per verify is too slow; `e2e down` and `finish` tear it down.
|
|
977
691
|
keepUp: true,
|
|
978
692
|
});
|
|
979
693
|
const durationMs = Date.now() - start;
|
|
@@ -985,11 +699,7 @@ async function runE2eNode({ projectPath, featureName, skipE2e, freshE2e, buildNo
|
|
|
985
699
|
durationMs,
|
|
986
700
|
reason: result.reason ?? null,
|
|
987
701
|
output: result.output || '',
|
|
988
|
-
// Specs Playwright counted in this run; null = no specs ran or unreadable summary.
|
|
989
702
|
count: Number.isInteger(result.count) ? result.count : null,
|
|
990
|
-
// Non-fatal findings (e.g. a compose with fixed ports at the ROOT, where
|
|
991
|
-
// nothing collides yet). Surfaced so the same compose does not fail
|
|
992
|
-
// unannounced the day the feature moves into a worktree.
|
|
993
703
|
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
994
704
|
},
|
|
995
705
|
stackEnv: { COMPOSE_PROJECT_NAME: composeProjectName(featureName), ...env },
|
|
@@ -1007,13 +717,8 @@ function appendE2eFeedback(feedback, e2eNode) {
|
|
|
1007
717
|
}
|
|
1008
718
|
}
|
|
1009
719
|
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
// ============================================================================
|
|
1013
|
-
|
|
1014
|
-
/** Read the project stack directly from config.json — never the stack-profile
|
|
1015
|
-
* default (which falls back to 'unknown'→dotnet and would wrongly fire
|
|
1016
|
-
* `dotnet build` on a greenfield JS project). */
|
|
720
|
+
// Never the stack-profile default: it falls back to dotnet and would fire
|
|
721
|
+
// `dotnet build` on a greenfield JS project.
|
|
1017
722
|
function readStack(projectPath) {
|
|
1018
723
|
try {
|
|
1019
724
|
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
@@ -1025,10 +730,6 @@ function readStack(projectPath) {
|
|
|
1025
730
|
}
|
|
1026
731
|
}
|
|
1027
732
|
|
|
1028
|
-
/** Read frontend/backend sub-paths for a monorepo, so the build node can run
|
|
1029
|
-
* each stack's build command from its own directory instead of assuming a
|
|
1030
|
-
* single root (see getBuildSteps() in build-check.js). Absent in single-stack
|
|
1031
|
-
* projects — every consumer treats missing paths as "use projectPath". */
|
|
1032
733
|
function readProjectPaths(projectPath) {
|
|
1033
734
|
try {
|
|
1034
735
|
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
@@ -1041,7 +742,6 @@ function readProjectPaths(projectPath) {
|
|
|
1041
742
|
}
|
|
1042
743
|
}
|
|
1043
744
|
|
|
1044
|
-
/** Read the whole config.json — the e2e node needs `e2e` and `project` together. */
|
|
1045
745
|
function readConfig(projectPath) {
|
|
1046
746
|
try {
|
|
1047
747
|
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
@@ -1052,14 +752,8 @@ function readConfig(projectPath) {
|
|
|
1052
752
|
}
|
|
1053
753
|
}
|
|
1054
754
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
* location and rationale as the fleet env-registry (see env-allocator.js): it
|
|
1058
|
-
* is the one place every linked worktree of a repo shares, and writing there
|
|
1059
|
-
* can never dirty the `git status` the worktree-finish gates read.
|
|
1060
|
-
*
|
|
1061
|
-
* Fail-open: no git, no stamp — the cost is a rebuild, never a crash.
|
|
1062
|
-
*/
|
|
755
|
+
// The e2e freshness stamp lives in the common dir: shared by every worktree and
|
|
756
|
+
// never dirties `git status`. Fail-open: no git costs a rebuild, never a crash.
|
|
1063
757
|
function resolveGitCommonDir(projectPath) {
|
|
1064
758
|
try {
|
|
1065
759
|
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
|