@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,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';
|
|
@@ -28,6 +5,7 @@ import { join, resolve } from 'path';
|
|
|
28
5
|
import { runBuildCheck, getBuildSteps, isDotnetStack } from '../../../framework/hooks/shared/build-check.js';
|
|
29
6
|
import { composeProjectName, runE2eCheck } from '../../../framework/hooks/shared/e2e-check.js';
|
|
30
7
|
import { buildComposeEnv } from '../e2e/compose-env.js';
|
|
8
|
+
import { e2eApplicability } from '../e2e/applicability.js';
|
|
31
9
|
import {
|
|
32
10
|
runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout, discoverDotnetSolution,
|
|
33
11
|
} from '../tasks/test-runner.js';
|
|
@@ -44,24 +22,14 @@ import { runValidation } from './validation-runner.js';
|
|
|
44
22
|
import { checkOutputsTraceability } from './traceability-check.js';
|
|
45
23
|
import { createHeavyGate } from '../heavy/gate.js';
|
|
46
24
|
|
|
47
|
-
|
|
25
|
+
// A cold dotnet build takes minutes, not seconds.
|
|
48
26
|
const DEFAULT_BUILD_TIMEOUT = 120_000;
|
|
49
27
|
|
|
50
|
-
// Validator issues surfaced in `feedback`, errors first; the full list stays on the node.
|
|
51
28
|
const VALIDATOR_FEEDBACK_LIMIT = 10;
|
|
52
29
|
|
|
53
|
-
/** Number of trailing output lines kept for feedback/reporting. */
|
|
54
30
|
const OUTPUT_TAIL_LINES = 20;
|
|
55
31
|
|
|
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
|
-
*/
|
|
32
|
+
/** A wrong invocation, not a red verification: `verify` exits 2 for it, 1 for "code not ready". */
|
|
65
33
|
export class VerifyUsageError extends Error {
|
|
66
34
|
constructor(message) {
|
|
67
35
|
super(message);
|
|
@@ -71,29 +39,7 @@ export class VerifyUsageError extends Error {
|
|
|
71
39
|
}
|
|
72
40
|
|
|
73
41
|
/**
|
|
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
|
|
42
|
+
* @throws {VerifyUsageError} on an invocation that cannot mean what was typed (exit 2)
|
|
97
43
|
* @throws {HeavyBusyError} when no heavy slot (or RAM) freed within `heavy.waitTimeoutSec`
|
|
98
44
|
*/
|
|
99
45
|
export async function runVerify(projectPath, featureName, options = {}) {
|
|
@@ -122,16 +68,15 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
122
68
|
_runTestSuite = runTestSuite,
|
|
123
69
|
_runValidation = runValidation,
|
|
124
70
|
_runE2eCheck = runE2eCheck,
|
|
71
|
+
_e2eApplicability = e2eApplicability,
|
|
125
72
|
_checkOutputsTraceability = checkOutputsTraceability,
|
|
126
73
|
} = options;
|
|
127
74
|
|
|
128
75
|
const stack = readStack(projectPath);
|
|
129
76
|
const paths = readProjectPaths(projectPath);
|
|
130
77
|
|
|
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.
|
|
78
|
+
// Usage errors are judged before the build: paying for a build to discover a typo
|
|
79
|
+
// is the worst ordering, and a refusal after the cache was touched leaves a half-rewritten tree.
|
|
135
80
|
const testPlan = resolveTestNodes(projectPath);
|
|
136
81
|
const filter = parseTestFilter(options.filter);
|
|
137
82
|
if (filter && !testPlan.nodes.some((node) => nodeMatchesFilter(node, filter))) {
|
|
@@ -141,39 +86,23 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
141
86
|
);
|
|
142
87
|
}
|
|
143
88
|
|
|
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
89
|
if (recordBaseline) {
|
|
150
90
|
const problem = baselineTargetProblem(projectPath, testPlan, { skipTests });
|
|
151
91
|
if (problem) throw new VerifyUsageError(problem);
|
|
152
92
|
}
|
|
153
93
|
|
|
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
94
|
if (refreshCache) {
|
|
160
95
|
const problem = refreshCacheProblem(projectPath, testPlan, filter, { skipTests });
|
|
161
96
|
if (problem) throw new VerifyUsageError(problem);
|
|
162
97
|
}
|
|
163
98
|
|
|
164
|
-
//
|
|
165
|
-
// skip: "we could not check on Linux" reading as green is F35 itself.
|
|
99
|
+
// Missing Docker is a usage error, never a skip: "could not check on Linux" must not read as green.
|
|
166
100
|
if (linux) {
|
|
167
101
|
const problem = checkLinuxPreconditions({
|
|
168
102
|
projectPath, skipTests, ...(_exec ? { exec: _exec } : {}),
|
|
169
103
|
});
|
|
170
104
|
if (problem) throw new VerifyUsageError(problem);
|
|
171
105
|
|
|
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
106
|
const evalNode = testPlan.nodes.find(
|
|
178
107
|
(node) => node.runner === EVAL_RUNNER && nodeMatchesFilter(node, filter)
|
|
179
108
|
);
|
|
@@ -189,15 +118,11 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
189
118
|
const nodes = {};
|
|
190
119
|
const feedback = [];
|
|
191
120
|
|
|
192
|
-
// 1. build
|
|
193
121
|
nodes.build = await runBuildNode({ projectPath, stack, paths, timeout, skipBuild, gate, _runBuildCheck });
|
|
194
122
|
appendBuildFeedback(feedback, nodes.build);
|
|
195
123
|
|
|
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()).
|
|
124
|
+
// Only a TYPED --timeout reaches the tests: the build default would outrank
|
|
125
|
+
// config.project.testTimeoutMs and the per-project scaling.
|
|
201
126
|
const explicitTimeout = Number.isFinite(options.timeout) ? options.timeout : undefined;
|
|
202
127
|
const stackNodes = [];
|
|
203
128
|
nodes.tests = await runTestsNode({
|
|
@@ -218,26 +143,24 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
218
143
|
});
|
|
219
144
|
if (stackNodes.length === 0) appendTestsFeedback(feedback, nodes.tests);
|
|
220
145
|
|
|
221
|
-
// 3. validators — ALWAYS runs, even on a red build (free static feedback).
|
|
222
146
|
nodes.validators = await runValidatorsNode({ projectPath, featureName, _runValidation });
|
|
223
147
|
appendValidatorsFeedback(feedback, nodes.validators);
|
|
224
148
|
|
|
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
149
|
const e2e = await runE2eNode({
|
|
229
150
|
projectPath,
|
|
230
151
|
featureName,
|
|
231
152
|
skipE2e,
|
|
232
153
|
freshE2e,
|
|
233
154
|
buildNode: nodes.build,
|
|
155
|
+
testsNode: nodes.tests,
|
|
156
|
+
hasStackNodes: stackNodes.length > 0,
|
|
234
157
|
gate,
|
|
235
158
|
_runE2eCheck,
|
|
159
|
+
_e2eApplicability,
|
|
236
160
|
});
|
|
237
161
|
nodes.e2e = e2e.node;
|
|
238
162
|
appendE2eFeedback(feedback, nodes.e2e);
|
|
239
163
|
|
|
240
|
-
// `e2e: true` suites need the stack the e2e node just brought up (and left up).
|
|
241
164
|
if (stackNodes.length > 0) {
|
|
242
165
|
nodes.tests = await runStackTestNodes({
|
|
243
166
|
projectPath, testsNode: nodes.tests, stackNodes, e2eNode: nodes.e2e, stackEnv: e2e.stackEnv,
|
|
@@ -246,18 +169,21 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
246
169
|
appendTestsFeedback(feedback, nodes.tests);
|
|
247
170
|
}
|
|
248
171
|
|
|
249
|
-
//
|
|
250
|
-
//
|
|
172
|
+
// Outside `nodes` on purpose: status comes only from the nodes, so a traceability
|
|
173
|
+
// warning never turns red.
|
|
251
174
|
const traceability = _checkOutputsTraceability(projectPath, featureName);
|
|
252
175
|
|
|
253
|
-
const status = Object.values(nodes).some((n) => n.status === 'fail') ? 'fail' : 'pass';
|
|
254
|
-
|
|
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
176
|
const baseline = recordBaseline ? recordBaselineFrom(projectPath, nodes.tests) : null;
|
|
258
177
|
if (baseline && !baseline.recorded) {
|
|
259
178
|
feedback.push(`[baseline] não gravada: ${baseline.reason}`);
|
|
260
179
|
}
|
|
180
|
+
const lowered = baseline?.recorded ? baseline.updates.filter((u) => u.from !== null && u.to < u.from) : [];
|
|
181
|
+
if (lowered.length > 0) {
|
|
182
|
+
feedback.push(`[baseline] rebaixada: ${lowered.map((u) => `${u.id} (${u.from} → ${u.to})`).join(', ')}`);
|
|
183
|
+
nodes.tests = acceptRecordedDrops(nodes.tests);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const status = Object.values(nodes).some((n) => n.status === 'fail') ? 'fail' : 'pass';
|
|
261
187
|
|
|
262
188
|
return {
|
|
263
189
|
version: 1,
|
|
@@ -271,10 +197,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
271
197
|
};
|
|
272
198
|
}
|
|
273
199
|
|
|
274
|
-
// ============================================================================
|
|
275
|
-
// Build node
|
|
276
|
-
// ============================================================================
|
|
277
|
-
|
|
278
200
|
/**
|
|
279
201
|
* A root `dotnet build` with no solution at the root fails MSB1003: build the
|
|
280
202
|
* repository's single solution instead, or say which candidates and which key decide.
|
|
@@ -325,34 +247,13 @@ function appendBuildFeedback(feedback, buildNode) {
|
|
|
325
247
|
}
|
|
326
248
|
}
|
|
327
249
|
|
|
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
|
-
*/
|
|
250
|
+
// Every declared suite runs and appears in `runs[]`; the rollup stays an object
|
|
251
|
+
// with `status` because verify.js and the persisted stamp read `nodes.tests.status`.
|
|
349
252
|
async function runTestsNode({
|
|
350
253
|
projectPath, testPlan, filter, skipTests, buildNode, timeout, refreshCache = false,
|
|
351
254
|
linux = false, linuxImage = null, gate, stackNodes = [], _runTestSuite, _runLinuxSuite = runLinuxSuite, _exec,
|
|
352
255
|
}) {
|
|
353
256
|
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
257
|
const command = nodes.map((n) => n.command).filter(Boolean).join(' && ') || null;
|
|
357
258
|
// A fresh `runs: []` per return — two rollups must never alias one array.
|
|
358
259
|
const nothingRan = (reason) => ({
|
|
@@ -406,19 +307,8 @@ async function runStackTestNodes({ projectPath, testsNode, stackNodes, e2eNode,
|
|
|
406
307
|
return rollUpTestRuns(runs, { command: testsNode.command, source: testsNode.source, environment: testsNode.environment });
|
|
407
308
|
}
|
|
408
309
|
|
|
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
|
-
*/
|
|
310
|
+
// Pre-decisions run before the container exists, so a filtered node is never written
|
|
311
|
+
// into its script; results go through the same `judgeRun()` as the host path.
|
|
422
312
|
async function runNodesInLinux({
|
|
423
313
|
projectPath, nodes, filter, timeout, linuxImage, gate, _runLinuxSuite, _exec,
|
|
424
314
|
}) {
|
|
@@ -430,8 +320,7 @@ async function runNodesInLinux({
|
|
|
430
320
|
else toRun.push(node);
|
|
431
321
|
}
|
|
432
322
|
|
|
433
|
-
// One container, one clock: the
|
|
434
|
-
// have been given locally, since they share the run.
|
|
323
|
+
// One container, one clock: the sum of each node's local timeout.
|
|
435
324
|
const containerTimeout = toRun.reduce(
|
|
436
325
|
(sum, node) => sum + resolveTestTimeout(projectPath, node, timeout), 0
|
|
437
326
|
) || undefined;
|
|
@@ -450,16 +339,13 @@ async function runNodesInLinux({
|
|
|
450
339
|
if (decided.has(node.id)) return decided.get(node.id);
|
|
451
340
|
const raw = byId.get(node.id);
|
|
452
341
|
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
342
|
return { ...emptyRunEntry(node), status: 'fail', reason: 'o container não reportou este nó' };
|
|
456
343
|
}
|
|
457
344
|
if (raw.skipped) {
|
|
458
345
|
return { ...emptyRunEntry(node), reason: raw.reason };
|
|
459
346
|
}
|
|
460
347
|
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.
|
|
348
|
+
// No exit code is NOT a pass: the container died before this node reported.
|
|
463
349
|
? { passed: false, output: raw.output || '', reason: raw.reason, exitCode: null }
|
|
464
350
|
: { passed: raw.exitCode === 0, output: raw.output || '', exitCode: raw.exitCode };
|
|
465
351
|
return judgeRun({ node, result, durationMs: raw.durationMs ?? 0 });
|
|
@@ -468,30 +354,17 @@ async function runNodesInLinux({
|
|
|
468
354
|
return { runs, environment: suite.environment };
|
|
469
355
|
}
|
|
470
356
|
|
|
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
357
|
async function runOneTestNode({
|
|
479
358
|
projectPath, node, filter, timeout, refreshCache = false, gate, env = null, _runTestSuite,
|
|
480
359
|
}) {
|
|
481
360
|
const decided = preDecideRun({ node, filter, projectPath, refreshCache });
|
|
482
361
|
if (decided) return decided;
|
|
483
362
|
|
|
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
363
|
const evalChannel = node.runner === EVAL_RUNNER
|
|
489
364
|
? evalNodeEnv(node, projectPath, { refresh: refreshCache })
|
|
490
365
|
: 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.
|
|
366
|
+
// The summary path is fixed per node id and pid: a file left by an earlier run
|
|
367
|
+
// would be read as this run's and hide an empty run behind the old count.
|
|
495
368
|
if (evalChannel) discardFile(evalChannel.summaryPath);
|
|
496
369
|
|
|
497
370
|
gate.enter('tests');
|
|
@@ -508,26 +381,12 @@ async function runOneTestNode({
|
|
|
508
381
|
return judged;
|
|
509
382
|
}
|
|
510
383
|
|
|
511
|
-
/** Remove a scratch file the CLI owns; a file that is not there (or will not go) changes nothing. */
|
|
512
384
|
function discardFile(path) {
|
|
513
|
-
try { rmSync(path, { force: true }); } catch { /*
|
|
385
|
+
try { rmSync(path, { force: true }); } catch { /* the next run removes it first */ }
|
|
514
386
|
}
|
|
515
387
|
|
|
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
|
-
*/
|
|
388
|
+
// The scenario count comes ONLY from the summary (the dotnet extractor counts xunit
|
|
389
|
+
// cases, another unit); no readable summary is null, UNKNOWN, never 0.
|
|
531
390
|
function judgeEvalRun(entry, node, summaryPath) {
|
|
532
391
|
const summary = readEvalSummary(summaryPath);
|
|
533
392
|
const warnings = [...(entry.warnings || [])];
|
|
@@ -539,11 +398,8 @@ function judgeEvalRun(entry, node, summaryPath) {
|
|
|
539
398
|
const verdict = compareTestCount(count, node.expectedCount);
|
|
540
399
|
if (verdict.warning) warnings.push(verdict.warning);
|
|
541
400
|
|
|
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.
|
|
401
|
+
// Named failures go into `output` (feedback prefers it to `reason`), prepended
|
|
402
|
+
// rather than replacing it because a runner crash also lives in that tail.
|
|
547
403
|
const named = evalFailureLines(summary);
|
|
548
404
|
const withNames = named.length > 0
|
|
549
405
|
? { ...entry, output: [...named, ...(entry.output ? [entry.output] : [])].join('\n') }
|
|
@@ -555,14 +411,13 @@ function judgeEvalRun(entry, node, summaryPath) {
|
|
|
555
411
|
}
|
|
556
412
|
|
|
557
413
|
if (entry.status === 'pass' && verdict.verdict === 'drop') {
|
|
558
|
-
return { ...withNames, status: 'fail', count, warnings, reason: verdict.reason };
|
|
414
|
+
return { ...withNames, status: 'fail', count, warnings, reason: verdict.reason, countDrop: true };
|
|
559
415
|
}
|
|
560
416
|
if (verdict.verdict === 'drop') warnings.push(verdict.reason);
|
|
561
417
|
|
|
562
418
|
return { ...withNames, count, warnings };
|
|
563
419
|
}
|
|
564
420
|
|
|
565
|
-
/** The `runs[]` entry every path starts from — every field present, nothing implied. */
|
|
566
421
|
function emptyRunEntry(node) {
|
|
567
422
|
return {
|
|
568
423
|
id: node.id,
|
|
@@ -570,8 +425,7 @@ function emptyRunEntry(node) {
|
|
|
570
425
|
cwd: node.cwd,
|
|
571
426
|
runner: node.runner,
|
|
572
427
|
expectedCount: node.expectedCount,
|
|
573
|
-
//
|
|
574
|
-
// optional chaining. **null is UNKNOWN, never 0** (D7).
|
|
428
|
+
// null is UNKNOWN, never 0.
|
|
575
429
|
count: null,
|
|
576
430
|
status: 'skip',
|
|
577
431
|
durationMs: 0,
|
|
@@ -581,36 +435,22 @@ function emptyRunEntry(node) {
|
|
|
581
435
|
};
|
|
582
436
|
}
|
|
583
437
|
|
|
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
|
-
*/
|
|
438
|
+
// Verdicts decided before execution, shared by the host loop and `--linux` so both
|
|
439
|
+
// refuse for the same reason with the same words; null means the node must run.
|
|
593
440
|
function preDecideRun({ node, filter, projectPath = '.', refreshCache = false }) {
|
|
594
441
|
const entry = emptyRunEntry(node);
|
|
595
442
|
|
|
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
443
|
if (node.status) {
|
|
600
444
|
return { ...entry, status: node.status, reason: node.reason };
|
|
601
445
|
}
|
|
602
446
|
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
// invalid node costs no clock and still reddens the gate (D4).
|
|
447
|
+
// After the pre-decided verdict on purpose: an invalid node costs no clock and
|
|
448
|
+
// still reddens the gate even when filtered out.
|
|
606
449
|
if (!nodeMatchesFilter(node, filter)) {
|
|
607
450
|
return { ...entry, reason: `filtered out (--filter ${filter.raw})` };
|
|
608
451
|
}
|
|
609
452
|
|
|
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.
|
|
453
|
+
// A cache miss is a fail, never a skip: a skip would be green over nothing.
|
|
614
454
|
if (node.runner === EVAL_RUNNER) {
|
|
615
455
|
const pre = evalPreflight(projectPath, node, { refresh: refreshCache });
|
|
616
456
|
if (pre.status) return { ...entry, status: pre.status, reason: pre.reason };
|
|
@@ -619,14 +459,7 @@ function preDecideRun({ node, filter, projectPath = '.', refreshCache = false })
|
|
|
619
459
|
return null;
|
|
620
460
|
}
|
|
621
461
|
|
|
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
|
-
*/
|
|
462
|
+
// The single count rule for host and container runs; a second copy makes them disagree.
|
|
630
463
|
function judgeRun({ node, result, durationMs }) {
|
|
631
464
|
const entry = emptyRunEntry(node);
|
|
632
465
|
|
|
@@ -634,8 +467,7 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
634
467
|
return { ...entry, durationMs, reason: result.reason || 'no test command detected' };
|
|
635
468
|
}
|
|
636
469
|
|
|
637
|
-
// A
|
|
638
|
-
// a passing run cut mid-sentence. Lead with what actually happened.
|
|
470
|
+
// A truncated log tail reads like a passing run cut mid-sentence: lead with the timeout.
|
|
639
471
|
if (result.timedOut) {
|
|
640
472
|
const tail = tailLines(result.output, OUTPUT_TAIL_LINES);
|
|
641
473
|
return {
|
|
@@ -648,29 +480,19 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
648
480
|
};
|
|
649
481
|
}
|
|
650
482
|
|
|
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.
|
|
483
|
+
// Parsed from the FULL output, before truncation to a tail.
|
|
655
484
|
const parsed = parseTestCount(result.output, node.runner);
|
|
656
485
|
const count = parsed ? parsed.total : null;
|
|
657
486
|
|
|
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.
|
|
487
|
+
// An eval node's unit is the SCENARIO, compared only by `judgeEvalRun`; the
|
|
488
|
+
// extractor's xunit-case count against that baseline would fail green runs.
|
|
663
489
|
const verdict = compareTestCount(count, node.runner === EVAL_RUNNER ? null : node.expectedCount);
|
|
664
490
|
const warnings = verdict.warning ? [verdict.warning] : [];
|
|
665
491
|
|
|
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.
|
|
492
|
+
// Only a drop fails a node; a rise never does, or failing on growth teaches deleting tests.
|
|
669
493
|
if (verdict.verdict === 'drop' && result.passed) {
|
|
670
|
-
return { ...entry, status: 'fail', durationMs, count, warnings, reason: verdict.reason, output: '' };
|
|
494
|
+
return { ...entry, status: 'fail', durationMs, count, warnings, reason: verdict.reason, output: '', countDrop: true };
|
|
671
495
|
}
|
|
672
|
-
// Already red: the failing test is the better lead, so the drop travels
|
|
673
|
-
// beside it instead of overwriting the reason.
|
|
674
496
|
if (verdict.verdict === 'drop') warnings.push(verdict.reason);
|
|
675
497
|
|
|
676
498
|
return {
|
|
@@ -679,23 +501,18 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
679
501
|
durationMs,
|
|
680
502
|
count,
|
|
681
503
|
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.
|
|
504
|
+
// `test-build-failed` / `stale-binary`: refused before any test ran.
|
|
686
505
|
reason: result.reason ?? null,
|
|
687
506
|
output: result.passed ? '' : tailLines(result.output, OUTPUT_TAIL_LINES),
|
|
688
507
|
};
|
|
689
508
|
}
|
|
690
509
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
* report — one green run among skips is a pass.
|
|
698
|
-
*/
|
|
510
|
+
function acceptRecordedDrops(testsNode) {
|
|
511
|
+
const runs = testsNode.runs.map(({ countDrop, ...run }) => (countDrop ? { ...run, status: 'pass', reason: null } : run));
|
|
512
|
+
return rollUpTestRuns(runs, { command: testsNode.command, source: testsNode.source, environment: testsNode.environment });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Any red run reddens the rollup; one green run among skips is a pass.
|
|
699
516
|
function rollUpTestRuns(runs, { command, source, environment = null }) {
|
|
700
517
|
const failed = runs.filter((r) => r.status === 'fail');
|
|
701
518
|
const status = failed.length > 0
|
|
@@ -709,13 +526,8 @@ function rollUpTestRuns(runs, { command, source, environment = null }) {
|
|
|
709
526
|
reason: null,
|
|
710
527
|
output: failed.map((r) => r.output).filter(Boolean).join('\n'),
|
|
711
528
|
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.
|
|
529
|
+
// null is the local host; a container run is named so feature.json says what was proven.
|
|
716
530
|
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
531
|
warnings: runs.flatMap((r) => (r.warnings || []).map((w) => `${r.id}: ${w}`)),
|
|
720
532
|
runs,
|
|
721
533
|
};
|
|
@@ -729,17 +541,8 @@ function rollUpTestRuns(runs, { command, source, environment = null }) {
|
|
|
729
541
|
return node;
|
|
730
542
|
}
|
|
731
543
|
|
|
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
544
|
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.
|
|
545
|
+
// Warnings print whatever the verdict: a stale baseline lives on a GREEN node.
|
|
743
546
|
for (const run of testsNode.runs || []) {
|
|
744
547
|
for (const warning of run.warnings || []) {
|
|
745
548
|
feedback.push(`[tests/${run.id}] warning: ${warning}`);
|
|
@@ -754,31 +557,7 @@ function appendTestsFeedback(feedback, testsNode) {
|
|
|
754
557
|
}
|
|
755
558
|
}
|
|
756
559
|
|
|
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
|
-
*/
|
|
560
|
+
// The missing credential is judged per node by `evalPreflight`, not here.
|
|
782
561
|
function refreshCacheProblem(projectPath, testPlan, filter, { skipTests }) {
|
|
783
562
|
if (skipTests) {
|
|
784
563
|
return '--refresh-cache com --skip-tests: as duas flags se anulam — a regravação da cache É a suíte de eval';
|
|
@@ -798,30 +577,8 @@ function refreshCacheProblem(projectPath, testPlan, filter, { skipTests }) {
|
|
|
798
577
|
return null;
|
|
799
578
|
}
|
|
800
579
|
|
|
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
|
-
*/
|
|
580
|
+
// Never creates config.json nor a `project.tests[]` entry: fallback nodes have no
|
|
581
|
+
// declaration to update, and inventing one is not this command's job.
|
|
825
582
|
function baselineTargetProblem(projectPath, testPlan, { skipTests }) {
|
|
826
583
|
if (skipTests) {
|
|
827
584
|
return '--record-baseline com --skip-tests não mede nada — uma baseline vem de uma execução, não de uma omissão';
|
|
@@ -837,27 +594,16 @@ function baselineTargetProblem(projectPath, testPlan, { skipTests }) {
|
|
|
837
594
|
return null;
|
|
838
595
|
}
|
|
839
596
|
|
|
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
|
-
*/
|
|
597
|
+
// Only a run with EVERY node green records: a red run would freeze the regression,
|
|
598
|
+
// a partial one would claim counts for nodes nobody measured. A count drop on a run that
|
|
599
|
+
// exited 0 is the one red it accepts: lowering the number is how deleting tests passes.
|
|
854
600
|
function recordBaselineFrom(projectPath, testsNode) {
|
|
855
601
|
const runs = testsNode.runs || [];
|
|
856
602
|
const refuse = (reason) => ({ recorded: false, reason, updates: [] });
|
|
857
603
|
|
|
858
604
|
if (runs.length === 0) return refuse('nenhum nó de teste executou');
|
|
859
605
|
|
|
860
|
-
const notGreen = runs.filter((r) => r.status !== 'pass');
|
|
606
|
+
const notGreen = runs.filter((r) => r.status !== 'pass' && !r.countDrop);
|
|
861
607
|
if (notGreen.length > 0) {
|
|
862
608
|
const named = notGreen.map((r) => `${r.id} (${r.status}${r.reason ? `: ${r.reason}` : ''})`).join('; ');
|
|
863
609
|
return refuse(
|
|
@@ -869,23 +615,17 @@ function recordBaselineFrom(projectPath, testsNode) {
|
|
|
869
615
|
return recordTestBaselines(projectPath, runs.map((r) => ({ id: r.id, count: r.count })));
|
|
870
616
|
}
|
|
871
617
|
|
|
872
|
-
// ============================================================================
|
|
873
|
-
// Validators node
|
|
874
|
-
// ============================================================================
|
|
875
|
-
|
|
876
618
|
async function runValidatorsNode({ projectPath, featureName, _runValidation }) {
|
|
877
619
|
const start = Date.now();
|
|
878
620
|
|
|
879
|
-
// runValidation() prints
|
|
880
|
-
// JSON — silence console during the call and ALWAYS restore it, even if
|
|
881
|
-
// runValidation throws.
|
|
621
|
+
// runValidation() prints progress to stdout, which would corrupt verify's JSON.
|
|
882
622
|
const originalLog = console.log;
|
|
883
623
|
const originalWarn = console.warn;
|
|
884
624
|
let validationResult;
|
|
885
625
|
try {
|
|
886
626
|
console.log = () => {};
|
|
887
627
|
console.warn = () => {};
|
|
888
|
-
validationResult = await _runValidation(projectPath, featureName
|
|
628
|
+
validationResult = await _runValidation(projectPath, featureName);
|
|
889
629
|
} finally {
|
|
890
630
|
console.log = originalLog;
|
|
891
631
|
console.warn = originalWarn;
|
|
@@ -917,8 +657,7 @@ async function runValidatorsNode({ projectPath, featureName, _runValidation }) {
|
|
|
917
657
|
}
|
|
918
658
|
|
|
919
659
|
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.
|
|
660
|
+
// A validator that could not check is invisible in a green node otherwise.
|
|
922
661
|
if (validatorsNode.status !== 'skip') {
|
|
923
662
|
for (const s of validatorsNode.skipped || []) {
|
|
924
663
|
if (s.unavailable) feedback.push(`[validators/${s.validator}] não rodou: ${s.reason}`);
|
|
@@ -935,30 +674,22 @@ function appendValidatorsFeedback(feedback, validatorsNode) {
|
|
|
935
674
|
}
|
|
936
675
|
}
|
|
937
676
|
|
|
938
|
-
|
|
939
|
-
// E2E node
|
|
940
|
-
// ============================================================================
|
|
677
|
+
const skippedE2e = (reason) => ({ node: { status: 'skip', command: null, durationMs: 0, reason, output: '' }, stackEnv: null });
|
|
941
678
|
|
|
942
|
-
async function runE2eNode({
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
command: null,
|
|
948
|
-
durationMs: 0,
|
|
949
|
-
reason: 'build failed — the stack could not come up anyway',
|
|
950
|
-
output: '',
|
|
951
|
-
},
|
|
952
|
-
stackEnv: null,
|
|
953
|
-
};
|
|
954
|
-
}
|
|
679
|
+
async function runE2eNode({
|
|
680
|
+
projectPath, featureName, skipE2e, freshE2e, buildNode, testsNode, hasStackNodes, gate, _runE2eCheck, _e2eApplicability,
|
|
681
|
+
}) {
|
|
682
|
+
if (buildNode.status === 'fail') return skippedE2e('build failed — the stack could not come up anyway');
|
|
683
|
+
if (testsNode?.status === 'fail') return skippedE2e('tests failed — the e2e does not run over a red suite');
|
|
955
684
|
|
|
956
685
|
const config = readConfig(projectPath);
|
|
686
|
+
// An `e2e: true` test node is the backend's proof against the stack: it always brings the stack up.
|
|
687
|
+
if (!skipE2e && !hasStackNodes) {
|
|
688
|
+
const applicability = _e2eApplicability(projectPath, featureName, config);
|
|
689
|
+
if (!applicability.applicable) return skippedE2e(applicability.reason);
|
|
690
|
+
}
|
|
957
691
|
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.
|
|
692
|
+
// Without the port block, two worktrees verifying at once collide on host ports.
|
|
962
693
|
const env = buildComposeEnv({ cwd: projectPath, config, featureName, gitCommonDir });
|
|
963
694
|
|
|
964
695
|
const start = Date.now();
|
|
@@ -971,9 +702,7 @@ async function runE2eNode({ projectPath, featureName, skipE2e, freshE2e, buildNo
|
|
|
971
702
|
forceFresh: freshE2e,
|
|
972
703
|
beforeHeavy: () => gate.enter('e2e'),
|
|
973
704
|
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.
|
|
705
|
+
// Cold-starting a .NET container per verify is too slow; `e2e down` and `finish` tear it down.
|
|
977
706
|
keepUp: true,
|
|
978
707
|
});
|
|
979
708
|
const durationMs = Date.now() - start;
|
|
@@ -985,11 +714,7 @@ async function runE2eNode({ projectPath, featureName, skipE2e, freshE2e, buildNo
|
|
|
985
714
|
durationMs,
|
|
986
715
|
reason: result.reason ?? null,
|
|
987
716
|
output: result.output || '',
|
|
988
|
-
// Specs Playwright counted in this run; null = no specs ran or unreadable summary.
|
|
989
717
|
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
718
|
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
994
719
|
},
|
|
995
720
|
stackEnv: { COMPOSE_PROJECT_NAME: composeProjectName(featureName), ...env },
|
|
@@ -1007,13 +732,8 @@ function appendE2eFeedback(feedback, e2eNode) {
|
|
|
1007
732
|
}
|
|
1008
733
|
}
|
|
1009
734
|
|
|
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). */
|
|
735
|
+
// Never the stack-profile default: it falls back to dotnet and would fire
|
|
736
|
+
// `dotnet build` on a greenfield JS project.
|
|
1017
737
|
function readStack(projectPath) {
|
|
1018
738
|
try {
|
|
1019
739
|
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
@@ -1025,10 +745,6 @@ function readStack(projectPath) {
|
|
|
1025
745
|
}
|
|
1026
746
|
}
|
|
1027
747
|
|
|
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
748
|
function readProjectPaths(projectPath) {
|
|
1033
749
|
try {
|
|
1034
750
|
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
@@ -1041,7 +757,6 @@ function readProjectPaths(projectPath) {
|
|
|
1041
757
|
}
|
|
1042
758
|
}
|
|
1043
759
|
|
|
1044
|
-
/** Read the whole config.json — the e2e node needs `e2e` and `project` together. */
|
|
1045
760
|
function readConfig(projectPath) {
|
|
1046
761
|
try {
|
|
1047
762
|
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
@@ -1052,14 +767,8 @@ function readConfig(projectPath) {
|
|
|
1052
767
|
}
|
|
1053
768
|
}
|
|
1054
769
|
|
|
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
|
-
*/
|
|
770
|
+
// The e2e freshness stamp lives in the common dir: shared by every worktree and
|
|
771
|
+
// never dirties `git status`. Fail-open: no git costs a rebuild, never a crash.
|
|
1063
772
|
function resolveGitCommonDir(projectPath) {
|
|
1064
773
|
try {
|
|
1065
774
|
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
|