@polymorphism-tech/morph-spec 9.0.0 → 9.1.0

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