@polymorphism-tech/morph-spec 8.46.0 → 9.0.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 +53 -44
- package/README.md +3 -4
- package/bin/morph-spec.js +38 -55
- package/bin/validate.js +5 -134
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -2
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/create-umbraco-lp.md +2 -0
- package/framework/commands/migrate-lp-to-umbraco.md +2 -0
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +2 -2
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +17 -203
- package/framework/commands/morph-media.md +3 -0
- package/framework/commands/morph-preflight.md +7 -31
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/commands/morph-troubleshoot.md +2 -14
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +17 -19
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +67 -31
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +3 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/ai-agents-standards.md +18 -16
- package/framework/rules/asaas-standards.md +3 -1
- package/framework/rules/csharp-standards.md +10 -8
- package/framework/rules/data-standards.md +5 -5
- package/framework/rules/dotnet-templates.md +1 -1
- package/framework/rules/frontend-standards.md +4 -2
- package/framework/rules/gsap-standards.md +10 -5
- package/framework/rules/hangfire-standards.md +3 -1
- package/framework/rules/infrastructure-standards.md +4 -2
- package/framework/rules/neon-auth-standards.md +3 -1
- package/framework/rules/neon-standards.md +6 -4
- package/framework/rules/nextjs-standards.md +13 -11
- package/framework/rules/nextjs-templates.md +1 -1
- package/framework/rules/resend-standards.md +3 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -52
- package/framework/skills/README.md +4 -5
- package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
- package/framework/skills/core/morph-apply/SKILL.md +150 -223
- package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
- package/framework/skills/core/morph-machine/SKILL.md +4 -4
- package/framework/skills/core/morph-media/SKILL.md +3 -3
- package/framework/skills/core/morph-plan/SKILL.md +138 -300
- package/framework/skills/core/morph-review/SKILL.md +112 -137
- package/framework/skills/core/morph-standards/SKILL.md +29 -41
- package/framework/skills/core/morph-uiux/SKILL.md +78 -153
- package/framework/standards/STANDARDS.json +90 -1219
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +2 -3
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
- package/framework/standards/ai-agents/prompt-sources.md +6 -4
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/architecture/code-comments.md +8 -8
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
- package/framework/standards/backend/api/validation.md +135 -47
- package/framework/standards/backend/database/postgresql/database.md +11 -7
- package/framework/standards/backend/database/vector-search-rag.md +207 -324
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
- package/framework/standards/backend/dotnet/result-pattern.md +40 -6
- package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
- package/framework/standards/backend/dotnet/scrutor.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
- package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
- package/framework/standards/data/nosql/cache/redis.md +120 -44
- package/framework/standards/data/nosql/cosmos-db.md +114 -71
- package/framework/standards/frontend/design-system/animations.md +1 -1
- package/framework/standards/frontend/design-system/naming.md +1 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
- package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
- package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
- package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
- package/framework/templates/infrastructure/github/README.md +2 -1
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/observability/telemetry.js +3 -5
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +6 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +33 -19
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +95 -138
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +86 -95
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +41 -82
- package/src/core/paths/output-schema.js +20 -67
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/App.jsx +0 -2
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +7 -8
- package/src/dashboard/app/components/EntityLink.jsx +1 -2
- package/src/dashboard/app/components/Markdown.jsx +1 -1
- package/src/dashboard/app/components/Sidebar.jsx +1 -2
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/RulesMap.jsx +3 -3
- package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +6 -16
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +5 -23
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/index.js +0 -2
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/rules-scanner.js +15 -10
- package/src/dashboard/scanner/skills-scanner.js +2 -3
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +134 -135
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-profile.js +1 -1
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +24 -48
- package/src/lib/validators/reuse/symbol-scanner.js +2 -4
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/validation-runner.js +8 -88
- package/src/lib/validators/verify-runner.js +134 -301
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +42 -22
- package/src/utils/agents-installer.js +92 -13
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +34 -6
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/commands/commit.md +0 -166
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rubrics/backend.json +0 -41
- package/framework/rubrics/default.json +0 -36
- package/framework/rubrics/frontend.json +0 -36
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/project/validate.js +0 -97
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/dashboard/app/pages/Rubrics.jsx +0 -97
- package/src/dashboard/scanner/rubrics-scanner.js +0 -12
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -91
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/lib/validators/architecture/architecture-validator.js +0 -270
- package/src/lib/validators/architecture/index.js +0 -7
- package/src/lib/validators/css/css-validator.js +0 -340
- package/src/lib/validators/css/index.js +0 -7
- package/src/lib/validators/design-system/design-system-validator.js +0 -237
- package/src/lib/validators/design-system/index.js +0 -7
- package/src/lib/validators/score-aggregator.js +0 -244
- package/src/lib/validators/ui/index.js +0 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Verify Runner — deterministic build+tests+validators+e2e cadence.
|
|
3
3
|
*
|
|
4
4
|
* v8 adds a DETERMINISTIC verification node that runs BEFORE the LLM judge
|
|
5
|
-
* in the
|
|
5
|
+
* in the implement loop: build + tests + validators (code, free) first; the
|
|
6
6
|
* LLM judge is only invoked once verify passes. This is a BINARY gate
|
|
7
7
|
* (pass/fail per node).
|
|
8
8
|
*
|
|
@@ -15,10 +15,8 @@
|
|
|
15
15
|
* - runBuildCheck() from framework/hooks/shared/build-check.js
|
|
16
16
|
* - runTestSuite() from src/lib/tasks/test-runner.js (one node)
|
|
17
17
|
* - resolveTestNodes() from src/lib/tasks/test-nodes.js (the LIST of suites)
|
|
18
|
-
* - applyTaskScope() from src/lib/tasks/test-scope.js (the task-scoped cut, #87)
|
|
19
18
|
* - runValidation() from ./validation-runner.js
|
|
20
19
|
* - runE2eCheck() from framework/hooks/shared/e2e-check.js
|
|
21
|
-
* - loadRubric() + aggregateScores() from ./score-aggregator.js
|
|
22
20
|
*
|
|
23
21
|
* @module verify-runner
|
|
24
22
|
*/
|
|
@@ -27,16 +25,15 @@ import { execFileSync } from 'child_process';
|
|
|
27
25
|
import { existsSync, readFileSync, rmSync } from 'fs';
|
|
28
26
|
import { join, resolve } from 'path';
|
|
29
27
|
|
|
30
|
-
import { runBuildCheck, getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
|
|
31
|
-
import { runE2eCheck } from '../../../framework/hooks/shared/e2e-check.js';
|
|
28
|
+
import { runBuildCheck, getBuildSteps, isDotnetStack } from '../../../framework/hooks/shared/build-check.js';
|
|
29
|
+
import { composeProjectName, runE2eCheck } from '../../../framework/hooks/shared/e2e-check.js';
|
|
32
30
|
import { buildComposeEnv } from '../e2e/compose-env.js';
|
|
33
31
|
import {
|
|
34
|
-
runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout,
|
|
32
|
+
runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout, discoverDotnetSolution,
|
|
35
33
|
} from '../tasks/test-runner.js';
|
|
36
34
|
import {
|
|
37
35
|
resolveTestNodes, parseTestFilter, nodeMatchesFilter, recordTestBaselines, morphConfigPath,
|
|
38
36
|
} from '../tasks/test-nodes.js';
|
|
39
|
-
import { applyTaskScope } from '../tasks/test-scope.js';
|
|
40
37
|
import { runLinuxSuite, checkLinuxPreconditions } from '../tasks/linux-runner.js';
|
|
41
38
|
import {
|
|
42
39
|
EVAL_RUNNER, evalPreflight, evalNodeEnv, readEvalSummary, evalScenarioCount,
|
|
@@ -44,25 +41,18 @@ import {
|
|
|
44
41
|
evalEmptyRunProblem, evalColdCacheWarning,
|
|
45
42
|
} from '../tasks/eval-node.js';
|
|
46
43
|
import { runValidation } from './validation-runner.js';
|
|
47
|
-
import { loadRubric, aggregateScores } from './score-aggregator.js';
|
|
48
44
|
import { checkOutputsTraceability } from './traceability-check.js';
|
|
49
|
-
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
50
45
|
import { createHeavyGate } from '../heavy/gate.js';
|
|
51
46
|
|
|
52
47
|
/** Default build timeout — dotnet cold build is slow; 30s (the Stop-hook default) is too short. */
|
|
53
48
|
const DEFAULT_BUILD_TIMEOUT = 120_000;
|
|
54
49
|
|
|
50
|
+
// Validator issues surfaced in `feedback`, errors first; the full list stays on the node.
|
|
51
|
+
const VALIDATOR_FEEDBACK_LIMIT = 10;
|
|
52
|
+
|
|
55
53
|
/** Number of trailing output lines kept for feedback/reporting. */
|
|
56
54
|
const OUTPUT_TAIL_LINES = 20;
|
|
57
55
|
|
|
58
|
-
/** A narrowed run that proved ZERO tests ran (#87) — wording pinned by the plan. */
|
|
59
|
-
const NARROWED_ZERO_TESTS = 'narrowed-zero-tests: o recorte executou 0 testes';
|
|
60
|
-
const NARROWED_ZERO_TESTS_LINK = 'narrowed-zero-tests: um link do recorte executou 0 testes (bloco MTP com total: 0)';
|
|
61
|
-
|
|
62
|
-
/** A narrowed run that exited green with no count anyone could read (#87): null is UNKNOWN, and unknown is never green. */
|
|
63
|
-
const NARROWED_COUNT_UNKNOWN = 'narrowed-count-unknown: o recorte saiu verde sem contagem legível — '
|
|
64
|
-
+ 'nada prova que algum teste rodou (desconhecido nunca vira verde)';
|
|
65
|
-
|
|
66
56
|
/**
|
|
67
57
|
* The caller asked for something impossible — not a red verification, a wrong
|
|
68
58
|
* invocation. The distinction is the exit code: `verify` answers 1 for "the
|
|
@@ -81,13 +71,11 @@ export class VerifyUsageError extends Error {
|
|
|
81
71
|
}
|
|
82
72
|
|
|
83
73
|
/**
|
|
84
|
-
* Run the deterministic verify cadence for a feature
|
|
74
|
+
* Run the deterministic verify cadence for a feature.
|
|
85
75
|
*
|
|
86
76
|
* @param {string} projectPath - Project root directory
|
|
87
77
|
* @param {string} featureName - Feature name
|
|
88
|
-
* @param {string|null} [taskId] - Task ID for task-scoped verify, or null/undefined for feature scope
|
|
89
78
|
* @param {Object} [options]
|
|
90
|
-
* @param {string} [options.rubric] - Force a rubric id (highest precedence)
|
|
91
79
|
* @param {number} [options.timeout] - Timeout in ms for the build AND tests nodes
|
|
92
80
|
* (default: 120000 for build; for tests, see resolveTestTimeout())
|
|
93
81
|
* @param {boolean} [options.skipTests] - Skip the tests node
|
|
@@ -99,8 +87,6 @@ export class VerifyUsageError extends Error {
|
|
|
99
87
|
* @param {boolean} [options.linux] - Run the test nodes inside a Linux container, on a COPY
|
|
100
88
|
* of the tree (see linux-runner.js for why a bind mount would prove nothing)
|
|
101
89
|
* @param {string} [options.linuxImage] - Image override for --linux
|
|
102
|
-
* @param {boolean} [options.full] - Task scope only: switch the recorte off and run every
|
|
103
|
-
* node whole (#87). A documented no-op at feature scope, which never narrows anything.
|
|
104
90
|
* @param {Function} [options._runBuildCheck] - Injectable override for runBuildCheck (testing)
|
|
105
91
|
* @param {Function} [options._runTestSuite] - Injectable override for runTestSuite (testing)
|
|
106
92
|
* @param {Function} [options._runValidation] - Injectable override for runValidation (testing)
|
|
@@ -110,18 +96,17 @@ export class VerifyUsageError extends Error {
|
|
|
110
96
|
* @returns {Promise<Object>} Aggregated verify result — see module docs for shape
|
|
111
97
|
* @throws {HeavyBusyError} when no heavy slot (or RAM) freed within `heavy.waitTimeoutSec`
|
|
112
98
|
*/
|
|
113
|
-
export async function runVerify(projectPath, featureName,
|
|
99
|
+
export async function runVerify(projectPath, featureName, options = {}) {
|
|
114
100
|
const gate = options._heavyGate ?? createHeavyGate({ projectPath, feature: featureName });
|
|
115
101
|
try {
|
|
116
|
-
return await runVerifyGated(projectPath, featureName,
|
|
102
|
+
return await runVerifyGated(projectPath, featureName, options, gate);
|
|
117
103
|
} finally {
|
|
118
104
|
gate.release();
|
|
119
105
|
}
|
|
120
106
|
}
|
|
121
107
|
|
|
122
|
-
async function runVerifyGated(projectPath, featureName,
|
|
108
|
+
async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
123
109
|
const {
|
|
124
|
-
rubric: rubricOverride,
|
|
125
110
|
timeout = DEFAULT_BUILD_TIMEOUT,
|
|
126
111
|
skipTests = false,
|
|
127
112
|
skipBuild = false,
|
|
@@ -131,7 +116,6 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
131
116
|
refreshCache = false,
|
|
132
117
|
linux = false,
|
|
133
118
|
linuxImage = null,
|
|
134
|
-
full = false,
|
|
135
119
|
_runLinuxSuite = runLinuxSuite,
|
|
136
120
|
_exec = undefined,
|
|
137
121
|
_runBuildCheck = runBuildCheck,
|
|
@@ -157,18 +141,13 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
157
141
|
);
|
|
158
142
|
}
|
|
159
143
|
|
|
160
|
-
// The task is read ONCE: its group picks the rubric and the e2e
|
|
161
|
-
// applicability and, with its outputs, the test scope (#87). One reader of
|
|
162
|
-
// tasks.json for verify, so the three can never disagree about the task.
|
|
163
|
-
const task = taskId ? readTask(projectPath, featureName, taskId) : null;
|
|
164
|
-
|
|
165
144
|
// `--record-baseline` is judged in the same breath, and for the same reason:
|
|
166
145
|
// discovering that there is nowhere to write AFTER running the whole fleet
|
|
167
146
|
// would charge the user a full suite for a usage error. `config.json` is
|
|
168
147
|
// never created here — this repository deliberately has none, and inventing
|
|
169
148
|
// one would break `doctor --full`.
|
|
170
149
|
if (recordBaseline) {
|
|
171
|
-
const problem = baselineTargetProblem(projectPath, testPlan, { skipTests
|
|
150
|
+
const problem = baselineTargetProblem(projectPath, testPlan, { skipTests });
|
|
172
151
|
if (problem) throw new VerifyUsageError(problem);
|
|
173
152
|
}
|
|
174
153
|
|
|
@@ -207,16 +186,6 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
207
186
|
}
|
|
208
187
|
}
|
|
209
188
|
|
|
210
|
-
// #87: at task scope the task's group picks the nodes and its outputs narrow
|
|
211
|
-
// the command inside each; test-scope.js DECIDES, this runner executes. The
|
|
212
|
-
// scoping happens ONCE, here, before runTestsNode, so the host loop and the
|
|
213
|
-
// `--linux` container receive the SAME scoped nodes (the container script is
|
|
214
|
-
// written from `node.command`) and preDecideRun/judgeRun stay the single copy
|
|
215
|
-
// of each rule for both. `--filter` was judged above on the ORIGINAL ids: a
|
|
216
|
-
// typo is a typo whatever the task. Feature scope gets the nodes back
|
|
217
|
-
// unchanged, because Gate 3 always runs every suite, whole.
|
|
218
|
-
const scoped = applyTaskScope({ projectPath, nodes: testPlan.nodes, task, taskId, filter, full });
|
|
219
|
-
|
|
220
189
|
const nodes = {};
|
|
221
190
|
const feedback = [];
|
|
222
191
|
|
|
@@ -230,58 +199,56 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
230
199
|
// default down would silently outrank config.project.testTimeoutMs and the
|
|
231
200
|
// per-project scaling (see resolveTestTimeout()).
|
|
232
201
|
const explicitTimeout = Number.isFinite(options.timeout) ? options.timeout : undefined;
|
|
202
|
+
const stackNodes = [];
|
|
233
203
|
nodes.tests = await runTestsNode({
|
|
234
204
|
projectPath,
|
|
235
|
-
testPlan
|
|
205
|
+
testPlan,
|
|
236
206
|
filter,
|
|
237
207
|
skipTests,
|
|
238
208
|
buildNode: nodes.build,
|
|
239
209
|
timeout: explicitTimeout,
|
|
240
|
-
taskId,
|
|
241
210
|
refreshCache,
|
|
242
211
|
linux,
|
|
243
212
|
linuxImage,
|
|
244
213
|
gate,
|
|
214
|
+
stackNodes,
|
|
245
215
|
_runTestSuite,
|
|
246
216
|
_runLinuxSuite,
|
|
247
217
|
_exec,
|
|
248
218
|
});
|
|
249
|
-
|
|
250
|
-
// and nowhere else: nodeStatuses() (verify.js) persists status strings only,
|
|
251
|
-
// so no part of the selection ever reaches feature.json.
|
|
252
|
-
nodes.tests.scope = scoped.summary;
|
|
253
|
-
appendTestsFeedback(feedback, nodes.tests);
|
|
219
|
+
if (stackNodes.length === 0) appendTestsFeedback(feedback, nodes.tests);
|
|
254
220
|
|
|
255
221
|
// 3. validators — ALWAYS runs, even on a red build (free static feedback).
|
|
256
|
-
|
|
257
|
-
nodes.validators = await runValidatorsNode({ projectPath, featureName, rubricId, _runValidation });
|
|
222
|
+
nodes.validators = await runValidatorsNode({ projectPath, featureName, _runValidation });
|
|
258
223
|
appendValidatorsFeedback(feedback, nodes.validators);
|
|
259
224
|
|
|
260
225
|
// 4. e2e — the only node that runs the application. Like tests, it is not
|
|
261
226
|
// invoked at all on a red build: a stack that cannot compile cannot come up,
|
|
262
227
|
// and the docker output would bury the actual compile error.
|
|
263
|
-
|
|
228
|
+
const e2e = await runE2eNode({
|
|
264
229
|
projectPath,
|
|
265
230
|
featureName,
|
|
266
|
-
taskId,
|
|
267
|
-
task,
|
|
268
231
|
skipE2e,
|
|
269
232
|
freshE2e,
|
|
270
233
|
buildNode: nodes.build,
|
|
271
234
|
gate,
|
|
272
235
|
_runE2eCheck,
|
|
273
236
|
});
|
|
237
|
+
nodes.e2e = e2e.node;
|
|
274
238
|
appendE2eFeedback(feedback, nodes.e2e);
|
|
275
239
|
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
240
|
+
// `e2e: true` suites need the stack the e2e node just brought up (and left up).
|
|
241
|
+
if (stackNodes.length > 0) {
|
|
242
|
+
nodes.tests = await runStackTestNodes({
|
|
243
|
+
projectPath, testsNode: nodes.tests, stackNodes, e2eNode: nodes.e2e, stackEnv: e2e.stackEnv,
|
|
244
|
+
timeout: explicitTimeout, gate, _runTestSuite,
|
|
245
|
+
});
|
|
246
|
+
appendTestsFeedback(feedback, nodes.tests);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Fora de `nodes` de propósito: o status sai só dos nós, então a divergência
|
|
250
|
+
// relatório × fatias (aviso de rastreabilidade) nunca vira vermelho de correção.
|
|
251
|
+
const traceability = _checkOutputsTraceability(projectPath, featureName);
|
|
285
252
|
|
|
286
253
|
const status = Object.values(nodes).some((n) => n.status === 'fail') ? 'fail' : 'pass';
|
|
287
254
|
|
|
@@ -295,10 +262,7 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
295
262
|
return {
|
|
296
263
|
version: 1,
|
|
297
264
|
feature: featureName,
|
|
298
|
-
task: taskId || null,
|
|
299
|
-
scope: taskId ? 'task' : 'feature',
|
|
300
265
|
stack,
|
|
301
|
-
rubric: rubricId,
|
|
302
266
|
status,
|
|
303
267
|
nodes,
|
|
304
268
|
feedback,
|
|
@@ -311,12 +275,29 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
311
275
|
// Build node
|
|
312
276
|
// ============================================================================
|
|
313
277
|
|
|
314
|
-
|
|
278
|
+
/**
|
|
279
|
+
* A root `dotnet build` with no solution at the root fails MSB1003: build the
|
|
280
|
+
* repository's single solution instead, or say which candidates and which key decide.
|
|
281
|
+
*/
|
|
282
|
+
export function resolveDotnetBuild(projectPath, stack, paths = {}) {
|
|
283
|
+
if (!stack || !isDotnetStack(stack.toLowerCase()) || paths.backendPath) return { paths, hint: null };
|
|
284
|
+
const { target, candidates } = discoverDotnetSolution(projectPath);
|
|
285
|
+
if (target === '.') return { paths, hint: null };
|
|
286
|
+
if (target) return { paths: { ...paths, backendTarget: target }, hint: null };
|
|
287
|
+
const found = candidates.length ? `candidatos: ${candidates.join(', ')}` : 'nenhum encontrado abaixo dela';
|
|
288
|
+
return {
|
|
289
|
+
paths,
|
|
290
|
+
hint: `nenhum .sln/.slnx/.csproj na raiz (${found}): declare project.backendPath em .morph/config/config.json com a pasta da solução, relativa à raiz`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function runBuildNode({ projectPath, stack, paths: declaredPaths = {}, timeout, skipBuild, gate, _runBuildCheck }) {
|
|
315
295
|
if (skipBuild) {
|
|
316
296
|
return { status: 'skip', command: null, durationMs: 0, reason: 'requested', output: '' };
|
|
317
297
|
}
|
|
318
298
|
|
|
319
|
-
const
|
|
299
|
+
const { paths, hint } = resolveDotnetBuild(projectPath, stack, declaredPaths);
|
|
300
|
+
const steps = getBuildSteps(stack, { cwd: projectPath, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget });
|
|
320
301
|
if (steps.length === 0) {
|
|
321
302
|
const reason = stack ? `no build command for stack "${stack}"` : 'no stack configured';
|
|
322
303
|
return { status: 'skip', command: null, durationMs: 0, reason, output: '' };
|
|
@@ -333,7 +314,7 @@ async function runBuildNode({ projectPath, stack, paths = {}, timeout, skipBuild
|
|
|
333
314
|
command,
|
|
334
315
|
durationMs,
|
|
335
316
|
reason: result.status === 'skip' ? 'no build command for stack' : null,
|
|
336
|
-
output: result.output
|
|
317
|
+
output: [result.output, result.status === 'fail' ? hint : null].filter(Boolean).join('\n'),
|
|
337
318
|
};
|
|
338
319
|
}
|
|
339
320
|
|
|
@@ -366,8 +347,8 @@ function appendBuildFeedback(feedback, buildNode) {
|
|
|
366
347
|
* `source`, `environment` (null outside a container) and `runs[]`.
|
|
367
348
|
*/
|
|
368
349
|
async function runTestsNode({
|
|
369
|
-
projectPath, testPlan, filter, skipTests, buildNode, timeout,
|
|
370
|
-
linux = false, linuxImage = null, gate, _runTestSuite, _runLinuxSuite = runLinuxSuite, _exec,
|
|
350
|
+
projectPath, testPlan, filter, skipTests, buildNode, timeout, refreshCache = false,
|
|
351
|
+
linux = false, linuxImage = null, gate, stackNodes = [], _runTestSuite, _runLinuxSuite = runLinuxSuite, _exec,
|
|
371
352
|
}) {
|
|
372
353
|
const { nodes, source } = testPlan;
|
|
373
354
|
// Kept readable for whoever only prints the string — the same join the
|
|
@@ -387,28 +368,49 @@ async function runTestsNode({
|
|
|
387
368
|
|
|
388
369
|
if (linux) {
|
|
389
370
|
const { runs, environment } = await runNodesInLinux({
|
|
390
|
-
projectPath, nodes, filter,
|
|
371
|
+
projectPath, nodes, filter, timeout, linuxImage, gate, _runLinuxSuite, _exec,
|
|
391
372
|
});
|
|
392
373
|
return rollUpTestRuns(runs, { command, source, environment });
|
|
393
374
|
}
|
|
394
375
|
|
|
395
376
|
const runs = [];
|
|
396
377
|
for (const node of nodes) {
|
|
378
|
+
if (node.e2e && !preDecideRun({ node, filter, projectPath, refreshCache })) {
|
|
379
|
+
stackNodes.push(node);
|
|
380
|
+
runs.push(emptyRunEntry(node));
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
397
383
|
runs.push(await runOneTestNode({
|
|
398
|
-
projectPath, node, filter,
|
|
384
|
+
projectPath, node, filter, timeout, refreshCache, gate, _runTestSuite,
|
|
399
385
|
}));
|
|
400
386
|
}
|
|
401
387
|
|
|
402
388
|
return rollUpTestRuns(runs, { command, source });
|
|
403
389
|
}
|
|
404
390
|
|
|
391
|
+
// No stack, no run: the skip carries the e2e verdict as reason AND warning, so the
|
|
392
|
+
// suite never disappears behind a green rollup.
|
|
393
|
+
async function runStackTestNodes({ projectPath, testsNode, stackNodes, e2eNode, stackEnv, timeout, gate, _runTestSuite }) {
|
|
394
|
+
const byId = new Map(stackNodes.map((node) => [node.id, node]));
|
|
395
|
+
const runs = [];
|
|
396
|
+
for (const run of testsNode.runs) {
|
|
397
|
+
const node = byId.get(run.id);
|
|
398
|
+
if (!node) { runs.push(run); continue; }
|
|
399
|
+
if (e2eNode.status !== 'pass') {
|
|
400
|
+
const reason = `a stack do e2e não está de pé (e2e: ${e2eNode.status}${e2eNode.reason ? ` — ${e2eNode.reason}` : ''}): nó e2e: true não roda sem ela`;
|
|
401
|
+
runs.push({ ...run, reason, warnings: [reason] });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
runs.push(await runOneTestNode({ projectPath, node, filter: null, timeout, gate, env: stackEnv, _runTestSuite }));
|
|
405
|
+
}
|
|
406
|
+
return rollUpTestRuns(runs, { command: testsNode.command, source: testsNode.source, environment: testsNode.environment });
|
|
407
|
+
}
|
|
408
|
+
|
|
405
409
|
/**
|
|
406
410
|
* Run the executable nodes inside ONE Linux container and judge them with the
|
|
407
411
|
* SAME rules the local path uses.
|
|
408
412
|
*
|
|
409
|
-
* The pre-decisions (invalid config, `--filter`, a node
|
|
410
|
-
* scope, an e2e node at task scope)
|
|
411
|
-
* happen HERE, before the container exists: a node that must not run must not
|
|
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
|
|
412
414
|
* be written into the container script either, or the flag would quietly run
|
|
413
415
|
* what the user filtered out. What the container returns — an exit code and an
|
|
414
416
|
* output — is then handed to `judgeRun()`, the same function the host path
|
|
@@ -418,12 +420,12 @@ async function runTestsNode({
|
|
|
418
420
|
* bind mount: see linux-runner.js. That is the whole reason this path exists.
|
|
419
421
|
*/
|
|
420
422
|
async function runNodesInLinux({
|
|
421
|
-
projectPath, nodes, filter,
|
|
423
|
+
projectPath, nodes, filter, timeout, linuxImage, gate, _runLinuxSuite, _exec,
|
|
422
424
|
}) {
|
|
423
425
|
const decided = new Map();
|
|
424
426
|
const toRun = [];
|
|
425
427
|
for (const node of nodes) {
|
|
426
|
-
const pre = preDecideRun({ node, filter,
|
|
428
|
+
const pre = preDecideRun({ node, filter, projectPath });
|
|
427
429
|
if (pre) decided.set(node.id, pre);
|
|
428
430
|
else toRun.push(node);
|
|
429
431
|
}
|
|
@@ -474,9 +476,9 @@ async function runNodesInLinux({
|
|
|
474
476
|
* from the report is the defect this whole feature exists to close.
|
|
475
477
|
*/
|
|
476
478
|
async function runOneTestNode({
|
|
477
|
-
projectPath, node, filter,
|
|
479
|
+
projectPath, node, filter, timeout, refreshCache = false, gate, env = null, _runTestSuite,
|
|
478
480
|
}) {
|
|
479
|
-
const decided = preDecideRun({ node, filter,
|
|
481
|
+
const decided = preDecideRun({ node, filter, projectPath, refreshCache });
|
|
480
482
|
if (decided) return decided;
|
|
481
483
|
|
|
482
484
|
// The eval channel: the CLI DICTATES where the cache, the dataset and the
|
|
@@ -494,8 +496,9 @@ async function runOneTestNode({
|
|
|
494
496
|
|
|
495
497
|
gate.enter('tests');
|
|
496
498
|
const start = Date.now();
|
|
499
|
+
const childEnv = env || evalChannel ? { ...(env || {}), ...(evalChannel?.env || {}) } : null;
|
|
497
500
|
const result = await _runTestSuite(projectPath, {
|
|
498
|
-
timeout, node, ...(
|
|
501
|
+
timeout, node, ...(childEnv ? { env: childEnv } : {}),
|
|
499
502
|
});
|
|
500
503
|
const entry = judgeRun({ node, result, durationMs: Date.now() - start });
|
|
501
504
|
if (!evalChannel) return entry;
|
|
@@ -575,11 +578,6 @@ function emptyRunEntry(node) {
|
|
|
575
578
|
reason: null,
|
|
576
579
|
warnings: [],
|
|
577
580
|
output: '',
|
|
578
|
-
// What the task-scope recorte did to this node (#87): `command` above is
|
|
579
|
-
// what actually ran, `selection.declaredCommand` what was declared. null at
|
|
580
|
-
// feature scope, where applyTaskScope leaves the key ABSENT; absent is read
|
|
581
|
-
// as null here so no reader has to tell the two apart.
|
|
582
|
-
selection: node.selection ?? null,
|
|
583
581
|
};
|
|
584
582
|
}
|
|
585
583
|
|
|
@@ -592,7 +590,7 @@ function emptyRunEntry(node) {
|
|
|
592
590
|
* two copies of this ordering is how a filtered node ends up invisible in one
|
|
593
591
|
* of them.
|
|
594
592
|
*/
|
|
595
|
-
function preDecideRun({ node, filter,
|
|
593
|
+
function preDecideRun({ node, filter, projectPath = '.', refreshCache = false }) {
|
|
596
594
|
const entry = emptyRunEntry(node);
|
|
597
595
|
|
|
598
596
|
// The resolver already decided: invalid config (fail-closed, D4) or an
|
|
@@ -609,26 +607,12 @@ function preDecideRun({ node, filter, taskId, projectPath = '.', refreshCache =
|
|
|
609
607
|
return { ...entry, reason: `filtered out (--filter ${filter.raw})` };
|
|
610
608
|
}
|
|
611
609
|
|
|
612
|
-
// The task's group scoped this node out (#87). AFTER the filter on purpose:
|
|
613
|
-
// a node the user filtered out reports the filter, never a scope it was not
|
|
614
|
-
// judged by. BEFORE the e2e and eval checks: a node outside the task scope
|
|
615
|
-
// has nothing left to decide.
|
|
616
|
-
if (node.selection?.mode === 'out-of-scope') {
|
|
617
|
-
return { ...entry, reason: node.selection.reason };
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
if (node.e2e && taskId) {
|
|
621
|
-
return { ...entry, reason: 'e2e node runs at feature scope only' };
|
|
622
|
-
}
|
|
623
|
-
|
|
624
610
|
// The eval pre-flight lives HERE, beside the other pre-decisions, so the host
|
|
625
611
|
// loop and the `--linux` path refuse for the same reason with the same words.
|
|
626
612
|
// Its three cache verdicts are FAILURES, never skips: a skip over an unusable
|
|
627
|
-
// cache is a green gate above a verification that never happened.
|
|
628
|
-
// skip it produces is scope — an eval suite does not pay for itself inside the
|
|
629
|
-
// per-task loop, and the result Gate 3 reads is the feature-scoped one.
|
|
613
|
+
// cache is a green gate above a verification that never happened.
|
|
630
614
|
if (node.runner === EVAL_RUNNER) {
|
|
631
|
-
const pre = evalPreflight(projectPath, node, {
|
|
615
|
+
const pre = evalPreflight(projectPath, node, { refresh: refreshCache });
|
|
632
616
|
if (pre.status) return { ...entry, status: pre.status, reason: pre.reason };
|
|
633
617
|
}
|
|
634
618
|
|
|
@@ -671,12 +655,6 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
671
655
|
const parsed = parseTestCount(result.output, node.runner);
|
|
672
656
|
const count = parsed ? parsed.total : null;
|
|
673
657
|
|
|
674
|
-
// A narrowed run is PARTIAL by construction (#87): it has a verdict of its
|
|
675
|
-
// own, and the count rule below never sees it.
|
|
676
|
-
if (entry.selection?.mode === 'narrowed') {
|
|
677
|
-
return judgeNarrowedRun({ entry, node, result, durationMs, count });
|
|
678
|
-
}
|
|
679
|
-
|
|
680
658
|
// An eval node's unit is the SCENARIO (#110 part 2): its baseline is the
|
|
681
659
|
// summary's scenario count, and `judgeEvalRun` is the one that compares it.
|
|
682
660
|
// Comparing here too, with the extractor's count of xunit cases, failed a
|
|
@@ -710,68 +688,6 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
710
688
|
};
|
|
711
689
|
}
|
|
712
690
|
|
|
713
|
-
/**
|
|
714
|
-
* The verdict of a NARROWED run (#87): what `judgeRun` hands over instead of
|
|
715
|
-
* its count rule. Reached from the host loop and from `--linux` alike, because
|
|
716
|
-
* both go through `judgeRun`.
|
|
717
|
-
*
|
|
718
|
-
* Two things change, and both follow from "partial by construction":
|
|
719
|
-
*
|
|
720
|
-
* - NO `compareTestCount`. `expectedCount` is the WHOLE suite's baseline: a
|
|
721
|
-
* recorte running 12 of 3.082 is not a drop, and a superset selection
|
|
722
|
-
* running more than the baseline is not a rise. Either comparison would
|
|
723
|
-
* report something about the suite that this run never measured.
|
|
724
|
-
* - a pass needs PROOF that tests ran. A filter that matches nothing exits 0
|
|
725
|
-
* under VSTest, and "zero tests, all green" is exactly the verdict issue #87
|
|
726
|
-
* must never buy. What counts as proof is the selection's `zeroGuard`
|
|
727
|
-
* (test-scope.js):
|
|
728
|
-
* `count` only a parsed count > 0. **null is UNKNOWN, never green**;
|
|
729
|
-
* `mtp-exit-8` a parsed count > 0 proves it (the MTP summary is read in
|
|
730
|
-
* pt-BR and en-US, #115); with no readable summary (another
|
|
731
|
-
* locale), exit 0 does — MTP exits 8 on zero tests.
|
|
732
|
-
* A parsed 0 is refused under either guard: it is positive evidence. It is
|
|
733
|
-
* also what closes limit (f): with exit 8 IGNORED, MTP exits 0 over a
|
|
734
|
-
* summary that says `total: 0` (measured, mtp-ignore-exit-code-8-zero-tests.txt).
|
|
735
|
-
* In a narrowed `&&` chain the SUM would hide that link behind another's
|
|
736
|
-
* count, so under `mtp-exit-8` every MTP block is checked on its own — MTP
|
|
737
|
-
* prints one block per invocation (measured), and a `total: 0` block is red.
|
|
738
|
-
*
|
|
739
|
-
* A FAILED run is renamed `narrowed-zero-tests (…)` only when it carries the
|
|
740
|
-
* runner's zero-test signature AND no count proves tests ran. A multi-project
|
|
741
|
-
* VSTest output may print "No test matches…" for one project while another
|
|
742
|
-
* ran and failed; that failure is the lead, never a zero. Anything else,
|
|
743
|
-
* exit 9 included, keeps the executor's own raw reason: a project that imposes
|
|
744
|
-
* `--minimum-expected-tests` is narrowed only with its floor lowered to 1 by
|
|
745
|
-
* `-p:` (#114, test-scope.js), where a run with tests cannot exit 9 — so a 9
|
|
746
|
-
* here is not ours to reinterpret.
|
|
747
|
-
*
|
|
748
|
-
* Every red leads its output with the verdict, like a timeout does: the
|
|
749
|
-
* feedback line reads `output` before `reason`, and a tail alone would bury it.
|
|
750
|
-
*/
|
|
751
|
-
function judgeNarrowedRun({ entry, node, result, durationMs, count }) {
|
|
752
|
-
const tail = tailLines(result.output, OUTPUT_TAIL_LINES);
|
|
753
|
-
const base = { ...entry, durationMs, count, warnings: [] };
|
|
754
|
-
const red = (reason) => ({ ...base, status: 'fail', reason, output: tail ? `${reason}\n${tail}` : reason });
|
|
755
|
-
const green = () => ({ ...base, status: 'pass', reason: result.reason ?? null, output: '' });
|
|
756
|
-
const ranTests = Number.isInteger(count) && count > 0;
|
|
757
|
-
|
|
758
|
-
if (result.passed) {
|
|
759
|
-
if (count === 0) return red(NARROWED_ZERO_TESTS);
|
|
760
|
-
if (entry.selection.zeroGuard === 'mtp-exit-8' && mtpSummaryTotals(result.output).includes(0)) {
|
|
761
|
-
return red(NARROWED_ZERO_TESTS_LINK);
|
|
762
|
-
}
|
|
763
|
-
if (ranTests) return green();
|
|
764
|
-
if (entry.selection.zeroGuard === 'mtp-exit-8' && result.exitCode === 0) return green();
|
|
765
|
-
return red(NARROWED_COUNT_UNKNOWN);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (!ranTests && isZeroTestRun(result.output, node.runner, result.exitCode)) {
|
|
769
|
-
return red(`narrowed-zero-tests (${node.runner}, exit ${result.exitCode ?? 'desconhecido'}): o recorte executou 0 testes`);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
return { ...base, status: 'fail', reason: result.reason ?? null, output: tail };
|
|
773
|
-
}
|
|
774
|
-
|
|
775
691
|
/**
|
|
776
692
|
* Aggregate the per-node runs into the single node `verify` publishes.
|
|
777
693
|
*
|
|
@@ -901,22 +817,12 @@ function refreshCacheProblem(projectPath, testPlan, filter, { skipTests }) {
|
|
|
901
817
|
* never created — the framework repository has none on purpose (creating it
|
|
902
818
|
* breaks `doctor --full`), so the honest answer is a named usage error.
|
|
903
819
|
*
|
|
904
|
-
* A TASK id is refused first (#87): at task scope the runner narrows the nodes
|
|
905
|
-
* AND the commands, so what it measures is a recorte, while `expectedCount` is
|
|
906
|
-
* the whole fleet's number. `recordTestBaselines` refuses a measurement that
|
|
907
|
-
* carries a selection (the second barrier), but paying for the recorte to
|
|
908
|
-
* learn that is the ordering this function exists to prevent.
|
|
909
|
-
*
|
|
910
820
|
* @param {string} projectPath
|
|
911
821
|
* @param {{ nodes: Array, source: string }} testPlan
|
|
912
|
-
* @param {{ skipTests: boolean
|
|
822
|
+
* @param {{ skipTests: boolean }} opts
|
|
913
823
|
* @returns {string|null}
|
|
914
824
|
*/
|
|
915
|
-
function baselineTargetProblem(projectPath, testPlan, { skipTests
|
|
916
|
-
if (taskId) {
|
|
917
|
-
return `--record-baseline é manutenção da frota: rode sem {task} (recebeu "${taskId}") — `
|
|
918
|
-
+ 'no escopo de task o verify recorta nós e testes, e baseline de recorte não é baseline';
|
|
919
|
-
}
|
|
825
|
+
function baselineTargetProblem(projectPath, testPlan, { skipTests }) {
|
|
920
826
|
if (skipTests) {
|
|
921
827
|
return '--record-baseline com --skip-tests não mede nada — uma baseline vem de uma execução, não de uma omissão';
|
|
922
828
|
}
|
|
@@ -960,19 +866,14 @@ function recordBaselineFrom(projectPath, testsNode) {
|
|
|
960
866
|
);
|
|
961
867
|
}
|
|
962
868
|
|
|
963
|
-
|
|
964
|
-
// (#87) — the second barrier behind baselineTargetProblem's usage error.
|
|
965
|
-
return recordTestBaselines(
|
|
966
|
-
projectPath,
|
|
967
|
-
runs.map((r) => ({ id: r.id, count: r.count, selection: r.selection ?? null }))
|
|
968
|
-
);
|
|
869
|
+
return recordTestBaselines(projectPath, runs.map((r) => ({ id: r.id, count: r.count })));
|
|
969
870
|
}
|
|
970
871
|
|
|
971
872
|
// ============================================================================
|
|
972
873
|
// Validators node
|
|
973
874
|
// ============================================================================
|
|
974
875
|
|
|
975
|
-
async function runValidatorsNode({ projectPath, featureName,
|
|
876
|
+
async function runValidatorsNode({ projectPath, featureName, _runValidation }) {
|
|
976
877
|
const start = Date.now();
|
|
977
878
|
|
|
978
879
|
// runValidation() prints chalk-formatted progress to stdout. verify emits
|
|
@@ -997,46 +898,40 @@ async function runValidatorsNode({ projectPath, featureName, rubricId, _runValid
|
|
|
997
898
|
if (validationResult && validationResult._skipReason) {
|
|
998
899
|
return {
|
|
999
900
|
status: 'skip',
|
|
1000
|
-
score: null,
|
|
1001
|
-
action: null,
|
|
1002
901
|
ran: [],
|
|
1003
902
|
skipped,
|
|
1004
903
|
reason: validationResult._skipReason,
|
|
1005
|
-
|
|
904
|
+
issues: [],
|
|
1006
905
|
durationMs,
|
|
1007
906
|
};
|
|
1008
907
|
}
|
|
1009
908
|
|
|
1010
|
-
const rubric = loadRubric(projectPath, rubricId);
|
|
1011
|
-
const scoreResult = aggregateScores(validationResult, rubric);
|
|
1012
|
-
|
|
1013
909
|
return {
|
|
1014
910
|
status: validationResult.passed === false ? 'fail' : 'pass',
|
|
1015
|
-
score: scoreResult.score,
|
|
1016
|
-
action: scoreResult.action,
|
|
1017
911
|
ran: Object.keys(validationResult.results || {}),
|
|
1018
912
|
skipped,
|
|
1019
913
|
reason: null,
|
|
1020
|
-
|
|
914
|
+
issues: validationResult.issues || [],
|
|
1021
915
|
durationMs,
|
|
1022
916
|
};
|
|
1023
917
|
}
|
|
1024
918
|
|
|
1025
919
|
function appendValidatorsFeedback(feedback, validatorsNode) {
|
|
1026
920
|
// A validator that could not check (reuse without a graph) is invisible in a
|
|
1027
|
-
// green
|
|
921
|
+
// green node; when the whole node skipped, its own line already says why.
|
|
1028
922
|
if (validatorsNode.status !== 'skip') {
|
|
1029
923
|
for (const s of validatorsNode.skipped || []) {
|
|
1030
924
|
if (s.unavailable) feedback.push(`[validators/${s.validator}] não rodou: ${s.reason}`);
|
|
1031
925
|
}
|
|
1032
926
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
927
|
+
const issues = validatorsNode.issues || [];
|
|
928
|
+
const ordered = [
|
|
929
|
+
...issues.filter((i) => i.level === 'error'),
|
|
930
|
+
...issues.filter((i) => i.level !== 'error'),
|
|
931
|
+
].slice(0, VALIDATOR_FEEDBACK_LIMIT);
|
|
932
|
+
for (const issue of ordered) {
|
|
933
|
+
const location = issue.file ? ` (${issue.file}${issue.line ? ':' + issue.line : ''})` : '';
|
|
934
|
+
feedback.push(`[validators/${issue.validator ?? 'unknown'}] ${issue.message}${location}`);
|
|
1040
935
|
}
|
|
1041
936
|
}
|
|
1042
937
|
|
|
@@ -1044,53 +939,60 @@ function appendValidatorsFeedback(feedback, validatorsNode) {
|
|
|
1044
939
|
// E2E node
|
|
1045
940
|
// ============================================================================
|
|
1046
941
|
|
|
1047
|
-
async function runE2eNode({ projectPath, featureName,
|
|
942
|
+
async function runE2eNode({ projectPath, featureName, skipE2e, freshE2e, buildNode, gate, _runE2eCheck }) {
|
|
1048
943
|
if (buildNode.status === 'fail') {
|
|
1049
944
|
return {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
945
|
+
node: {
|
|
946
|
+
status: 'skip',
|
|
947
|
+
command: null,
|
|
948
|
+
durationMs: 0,
|
|
949
|
+
reason: 'build failed — the stack could not come up anyway',
|
|
950
|
+
output: '',
|
|
951
|
+
},
|
|
952
|
+
stackEnv: null,
|
|
1055
953
|
};
|
|
1056
954
|
}
|
|
1057
955
|
|
|
1058
956
|
const config = readConfig(projectPath);
|
|
1059
|
-
const taskGroup = task?.group ?? null;
|
|
1060
957
|
const gitCommonDir = resolveGitCommonDir(projectPath);
|
|
958
|
+
// The port block matters MORE here than in `e2e up`: this is the path that
|
|
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.
|
|
962
|
+
const env = buildComposeEnv({ cwd: projectPath, config, featureName, gitCommonDir });
|
|
1061
963
|
|
|
1062
964
|
const start = Date.now();
|
|
1063
965
|
const result = _runE2eCheck(projectPath, {
|
|
1064
966
|
config,
|
|
1065
967
|
featureName,
|
|
1066
|
-
taskGroup,
|
|
1067
|
-
scope: taskId ? 'task' : 'feature',
|
|
1068
968
|
gitCommonDir,
|
|
1069
|
-
|
|
1070
|
-
// runs in the loop, and two worktrees verifying at once would otherwise get
|
|
1071
|
-
// distinct container names and then collide on host port 3000 — a red gate
|
|
1072
|
-
// over an infrastructure clash instead of a defect.
|
|
1073
|
-
env: buildComposeEnv({ cwd: projectPath, config, featureName, gitCommonDir }),
|
|
969
|
+
env,
|
|
1074
970
|
skipRequested: skipE2e,
|
|
1075
971
|
forceFresh: freshE2e,
|
|
1076
972
|
beforeHeavy: () => gate.enter('e2e'),
|
|
973
|
+
countSpecs: (output) => parseTestCount(output, 'playwright')?.total ?? null,
|
|
1077
974
|
// The stack stays up between verifies on purpose: cold-starting a .NET
|
|
1078
|
-
// container per
|
|
975
|
+
// container per verify is the difference between a usable loop and one
|
|
1079
976
|
// nobody runs. `morph-spec e2e down` (and `finish`) tear it down.
|
|
1080
977
|
keepUp: true,
|
|
1081
978
|
});
|
|
1082
979
|
const durationMs = Date.now() - start;
|
|
1083
980
|
|
|
1084
981
|
return {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
982
|
+
node: {
|
|
983
|
+
status: result.status,
|
|
984
|
+
command: result.command ?? null,
|
|
985
|
+
durationMs,
|
|
986
|
+
reason: result.reason ?? null,
|
|
987
|
+
output: result.output || '',
|
|
988
|
+
// Specs Playwright counted in this run; null = no specs ran or unreadable summary.
|
|
989
|
+
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
|
+
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
994
|
+
},
|
|
995
|
+
stackEnv: { COMPOSE_PROJECT_NAME: composeProjectName(featureName), ...env },
|
|
1094
996
|
};
|
|
1095
997
|
}
|
|
1096
998
|
|
|
@@ -1105,75 +1007,6 @@ function appendE2eFeedback(feedback, e2eNode) {
|
|
|
1105
1007
|
}
|
|
1106
1008
|
}
|
|
1107
1009
|
|
|
1108
|
-
// ============================================================================
|
|
1109
|
-
// Rubric selection — flag > task group > config > default
|
|
1110
|
-
// ============================================================================
|
|
1111
|
-
|
|
1112
|
-
function resolveRubricId({ projectPath, task, rubricOverride }) {
|
|
1113
|
-
if (rubricOverride) return rubricOverride;
|
|
1114
|
-
|
|
1115
|
-
const mapped = mapGroupToRubric(task?.group);
|
|
1116
|
-
if (mapped) return mapped;
|
|
1117
|
-
|
|
1118
|
-
const configRubric = readConfigRubric(projectPath);
|
|
1119
|
-
if (configRubric) return configRubric;
|
|
1120
|
-
|
|
1121
|
-
return 'default';
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
/**
|
|
1125
|
-
* Map a task's `group` field to a rubric id.
|
|
1126
|
-
* backend|infra → 'backend', frontend → 'frontend', tests|docs|absent → null
|
|
1127
|
-
* (falls through to config/default).
|
|
1128
|
-
*/
|
|
1129
|
-
function mapGroupToRubric(group) {
|
|
1130
|
-
if (!group) return null;
|
|
1131
|
-
const g = String(group).toLowerCase();
|
|
1132
|
-
if (g === 'backend' || g === 'infra') return 'backend';
|
|
1133
|
-
if (g === 'frontend') return 'frontend';
|
|
1134
|
-
return null;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
/**
|
|
1138
|
-
* The task as verify needs it, or null when tasks.json or the task is missing.
|
|
1139
|
-
*
|
|
1140
|
-
* The ONE reader of tasks.json for verify (#87): the group picks the rubric and
|
|
1141
|
-
* the e2e applicability, group + outputs pick the test scope, and the command
|
|
1142
|
-
* uses the null to refuse an unknown task id. Two readers is how a rubric and
|
|
1143
|
-
* a scope start disagreeing about the same task.
|
|
1144
|
-
*
|
|
1145
|
-
* @param {string} projectPath
|
|
1146
|
-
* @param {string} featureName
|
|
1147
|
-
* @param {string} taskId
|
|
1148
|
-
* @returns {{ group: string|null, outputs: string[] }|null}
|
|
1149
|
-
*/
|
|
1150
|
-
export function readTask(projectPath, featureName, taskId) {
|
|
1151
|
-
try {
|
|
1152
|
-
const tasksPath = getAbsoluteOutputPath(projectPath, featureName, 'tasks');
|
|
1153
|
-
if (!existsSync(tasksPath)) return null;
|
|
1154
|
-
const tasksJson = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
1155
|
-
const task = (tasksJson.tasks || []).find((t) => t.id === taskId);
|
|
1156
|
-
if (!task) return null;
|
|
1157
|
-
return {
|
|
1158
|
-
group: task.group ?? null,
|
|
1159
|
-
outputs: Array.isArray(task.outputs) ? task.outputs : [],
|
|
1160
|
-
};
|
|
1161
|
-
} catch {
|
|
1162
|
-
return null;
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function readConfigRubric(projectPath) {
|
|
1167
|
-
try {
|
|
1168
|
-
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
1169
|
-
if (!existsSync(configPath)) return null;
|
|
1170
|
-
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1171
|
-
return config?.validation?.rubric ?? null;
|
|
1172
|
-
} catch {
|
|
1173
|
-
return null;
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
1010
|
// ============================================================================
|
|
1178
1011
|
// Shared helpers
|
|
1179
1012
|
// ============================================================================
|