@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.
Files changed (304) hide show
  1. package/CLAUDE.md +53 -44
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +38 -55
  4. package/bin/validate.js +5 -134
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/create-umbraco-lp.md +2 -0
  12. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  13. package/framework/commands/morph-apply.md +5 -28
  14. package/framework/commands/morph-archive.md +2 -2
  15. package/framework/commands/morph-hotfix.md +8 -8
  16. package/framework/commands/morph-infra.md +17 -203
  17. package/framework/commands/morph-media.md +3 -0
  18. package/framework/commands/morph-preflight.md +7 -31
  19. package/framework/commands/morph-proposal.md +2 -50
  20. package/framework/commands/morph-status.md +3 -3
  21. package/framework/commands/morph-troubleshoot.md +2 -14
  22. package/framework/docs/CLI.md +585 -286
  23. package/framework/evals/review-checklist.md +82 -41
  24. package/framework/hooks/README.md +10 -11
  25. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  26. package/framework/hooks/claude-code/core/denylist.json +33 -3
  27. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  28. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  30. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  31. package/framework/hooks/claude-code/core/state-sync.js +17 -19
  32. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  33. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  34. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  35. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  36. package/framework/hooks/claude-code/statusline.py +55 -41
  37. package/framework/hooks/package.json +3 -0
  38. package/framework/hooks/shared/activity-logger.js +1 -1
  39. package/framework/hooks/shared/build-check.js +10 -6
  40. package/framework/hooks/shared/command-tokens.js +169 -0
  41. package/framework/hooks/shared/comment-rules.js +1 -1
  42. package/framework/hooks/shared/comment-scanner.js +10 -23
  43. package/framework/hooks/shared/comment-stripper.js +1 -1
  44. package/framework/hooks/shared/e2e-check.js +67 -31
  45. package/framework/hooks/shared/errata.js +72 -0
  46. package/framework/hooks/shared/feature-locator.js +1 -1
  47. package/framework/hooks/shared/frontend-locations.js +6 -0
  48. package/framework/hooks/shared/gate-question.js +18 -0
  49. package/framework/hooks/shared/phase-utils.js +15 -32
  50. package/framework/hooks/shared/slice-progress.js +118 -0
  51. package/framework/hooks/shared/spawn-command.js +23 -0
  52. package/framework/hooks/shared/state-reader.js +61 -87
  53. package/framework/hooks/shared/state-writer.js +3 -3
  54. package/framework/mcp/registry.json +3 -4
  55. package/framework/phases.json +23 -39
  56. package/framework/rules/ai-agents-standards.md +18 -16
  57. package/framework/rules/asaas-standards.md +3 -1
  58. package/framework/rules/csharp-standards.md +10 -8
  59. package/framework/rules/data-standards.md +5 -5
  60. package/framework/rules/dotnet-templates.md +1 -1
  61. package/framework/rules/frontend-standards.md +4 -2
  62. package/framework/rules/gsap-standards.md +10 -5
  63. package/framework/rules/hangfire-standards.md +3 -1
  64. package/framework/rules/infrastructure-standards.md +4 -2
  65. package/framework/rules/neon-auth-standards.md +3 -1
  66. package/framework/rules/neon-standards.md +6 -4
  67. package/framework/rules/nextjs-standards.md +13 -11
  68. package/framework/rules/nextjs-templates.md +1 -1
  69. package/framework/rules/resend-standards.md +3 -1
  70. package/framework/schemas/config.schema.json +14 -9
  71. package/framework/schemas/feature.schema.json +15 -52
  72. package/framework/skills/README.md +4 -5
  73. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  74. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  75. package/framework/skills/core/morph-apply/SKILL.md +150 -223
  76. package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
  77. package/framework/skills/core/morph-machine/SKILL.md +4 -4
  78. package/framework/skills/core/morph-media/SKILL.md +3 -3
  79. package/framework/skills/core/morph-plan/SKILL.md +138 -300
  80. package/framework/skills/core/morph-review/SKILL.md +112 -137
  81. package/framework/skills/core/morph-standards/SKILL.md +29 -41
  82. package/framework/skills/core/morph-uiux/SKILL.md +78 -153
  83. package/framework/standards/STANDARDS.json +90 -1219
  84. package/framework/standards/STANDARDS.meta.json +1305 -0
  85. package/framework/standards/ai-agents/evals-with-cache.md +2 -3
  86. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  87. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  88. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  89. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  90. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  91. package/framework/standards/ai-agents/setup.md +2 -2
  92. package/framework/standards/ai-agents/structured-output.md +1 -1
  93. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  94. package/framework/standards/architecture/code-comments.md +8 -8
  95. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  96. package/framework/standards/backend/api/validation.md +135 -47
  97. package/framework/standards/backend/database/postgresql/database.md +11 -7
  98. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  99. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  100. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  101. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  102. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  103. package/framework/standards/backend/dotnet/testing.md +1 -1
  104. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  105. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  106. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  107. package/framework/standards/data/nosql/cache/redis.md +120 -44
  108. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  109. package/framework/standards/frontend/design-system/animations.md +1 -1
  110. package/framework/standards/frontend/design-system/naming.md +1 -1
  111. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  112. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  113. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  114. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  115. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  116. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  117. package/framework/templates/README.md +2 -3
  118. package/framework/templates/REGISTRY.json +6 -170
  119. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  120. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  121. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  122. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  123. package/framework/templates/docs/plan.md +72 -0
  124. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  127. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  128. package/framework/templates/infrastructure/github/README.md +2 -1
  129. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  130. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  131. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  132. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  133. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  134. package/package.json +1 -1
  135. package/src/commands/e2e/e2e.js +2 -2
  136. package/src/commands/observability/cost.js +18 -60
  137. package/src/commands/observability/telemetry.js +3 -5
  138. package/src/commands/project/doctor-procs.js +1 -1
  139. package/src/commands/project/doctor.js +9 -9
  140. package/src/commands/project/fleet.js +4 -4
  141. package/src/commands/project/heavy.js +3 -3
  142. package/src/commands/project/init.js +6 -1
  143. package/src/commands/project/setup-infra-cmd.js +2 -1
  144. package/src/commands/project/stack-scan.js +58 -0
  145. package/src/commands/project/standards.js +79 -25
  146. package/src/commands/project/status.js +29 -69
  147. package/src/commands/project/update.js +33 -19
  148. package/src/commands/project/worktree.js +1 -1
  149. package/src/commands/state/advance.js +62 -46
  150. package/src/commands/state/approve.js +95 -138
  151. package/src/commands/state/archive.js +20 -84
  152. package/src/commands/state/auto-approval.js +68 -0
  153. package/src/commands/state/create.js +28 -1
  154. package/src/commands/state/delete.js +1 -1
  155. package/src/commands/state/finish.js +11 -23
  156. package/src/commands/state/gate-check.js +86 -95
  157. package/src/commands/state/gate-decision.js +6 -4
  158. package/src/commands/state/handoff.js +1 -1
  159. package/src/commands/state/list.js +9 -9
  160. package/src/commands/state/report-check.js +157 -311
  161. package/src/commands/state/retype.js +9 -3
  162. package/src/commands/validation/mutate.js +77 -14
  163. package/src/commands/validation/plan-check.js +81 -0
  164. package/src/commands/validation/validate-feature.js +60 -135
  165. package/src/commands/validation/verify.js +41 -82
  166. package/src/core/paths/output-schema.js +20 -67
  167. package/src/core/state/gate-order.js +5 -6
  168. package/src/core/state/lifecycle-audit.js +2 -2
  169. package/src/core/state/state-manager.js +17 -98
  170. package/src/core/templates/template-registry.js +2 -2
  171. package/src/core/templates/template-seed.js +22 -0
  172. package/src/core/work-types.js +10 -10
  173. package/src/dashboard/app/App.jsx +0 -2
  174. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  175. package/src/dashboard/app/components/CommandPalette.jsx +7 -8
  176. package/src/dashboard/app/components/EntityLink.jsx +1 -2
  177. package/src/dashboard/app/components/Markdown.jsx +1 -1
  178. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  179. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  180. package/src/dashboard/app/lib/phases.js +2 -4
  181. package/src/dashboard/app/pages/Activity.jsx +1 -1
  182. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  183. package/src/dashboard/app/pages/Features.jsx +9 -9
  184. package/src/dashboard/app/pages/Guide.jsx +2 -2
  185. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  186. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  187. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  188. package/src/dashboard/app/pages/Standards.jsx +1 -1
  189. package/src/dashboard/scanner/activity-scanner.js +6 -16
  190. package/src/dashboard/scanner/agents-scanner.js +1 -1
  191. package/src/dashboard/scanner/docs-scanner.js +5 -23
  192. package/src/dashboard/scanner/features-scanner.js +95 -60
  193. package/src/dashboard/scanner/index.js +0 -2
  194. package/src/dashboard/scanner/project-api.js +1 -1
  195. package/src/dashboard/scanner/rules-scanner.js +15 -10
  196. package/src/dashboard/scanner/skills-scanner.js +2 -3
  197. package/src/dashboard/scanner/standards-scanner.js +2 -1
  198. package/src/lib/ci/ci-verify.js +17 -1
  199. package/src/lib/ci/github-ci.js +1 -1
  200. package/src/lib/detectors/index.js +3 -8
  201. package/src/lib/generators/handoff-generator.js +34 -65
  202. package/src/lib/generators/settings-generator.js +11 -89
  203. package/src/lib/git/evidence-staleness.js +1 -1
  204. package/src/lib/git/tree-fingerprint.js +19 -7
  205. package/src/lib/git/tree-state.js +2 -2
  206. package/src/lib/heavy/cleanup.js +1 -5
  207. package/src/lib/installers/memory-installer.js +6 -8
  208. package/src/lib/mutation/commands.js +3 -2
  209. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  210. package/src/lib/mutation/plan.js +14 -6
  211. package/src/lib/mutation/runner.js +1 -1
  212. package/src/lib/mutation/subset.js +46 -9
  213. package/src/lib/plan/artifact-vocabulary.js +60 -0
  214. package/src/lib/plan/checks.js +234 -0
  215. package/src/lib/plan/plan-parser.js +209 -0
  216. package/src/lib/plan/plan-template.js +18 -0
  217. package/src/lib/review-reports.js +222 -0
  218. package/src/lib/risk-detector.js +134 -135
  219. package/src/lib/specs/adr-index.js +50 -69
  220. package/src/lib/specs/design-seed.js +18 -0
  221. package/src/lib/specs/frontmatter.js +44 -0
  222. package/src/lib/specs/gfm-table.js +17 -0
  223. package/src/lib/specs/old-format.js +68 -0
  224. package/src/lib/specs/revocations.js +37 -95
  225. package/src/lib/specs/screen-checklist.js +18 -17
  226. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  227. package/src/lib/stack/stack-profile.js +1 -1
  228. package/src/lib/stack/stack-scan.js +267 -0
  229. package/src/lib/standards/registry.js +43 -0
  230. package/src/lib/standards/sections.js +58 -0
  231. package/src/lib/tasks/eval-node.js +4 -12
  232. package/src/lib/tasks/test-nodes.js +1 -74
  233. package/src/lib/tasks/test-runner.js +75 -117
  234. package/src/lib/telemetry/metrics.js +8 -69
  235. package/src/lib/telemetry/telemetry-reader.js +5 -118
  236. package/src/lib/telemetry/verify-event.js +5 -7
  237. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  238. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  239. package/src/lib/validators/reuse/reuse-validator.js +24 -48
  240. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  241. package/src/lib/validators/traceability-check.js +85 -129
  242. package/src/lib/validators/validation-runner.js +8 -88
  243. package/src/lib/validators/verify-runner.js +134 -301
  244. package/src/lib/worktree/docker-audit.js +1 -2
  245. package/src/lib/worktree/fleet-scanner.js +25 -38
  246. package/src/scripts/setup-infra.js +42 -22
  247. package/src/utils/agents-installer.js +92 -13
  248. package/src/utils/claude-settings-manager.js +0 -35
  249. package/src/utils/file-copier.js +34 -6
  250. package/src/utils/hooks-installer.js +16 -9
  251. package/src/utils/skills-installer.js +1 -2
  252. package/framework/commands/commit.md +0 -166
  253. package/framework/evals/trace-schema.md +0 -67
  254. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  255. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  256. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  257. package/framework/hooks/shared/tasks-validator.js +0 -111
  258. package/framework/rubrics/backend.json +0 -41
  259. package/framework/rubrics/default.json +0 -36
  260. package/framework/rubrics/frontend.json +0 -36
  261. package/framework/rules/morph-task-tracking.md +0 -54
  262. package/framework/rules/morph-workflow.md +0 -244
  263. package/framework/schemas/tasks.schema.json +0 -79
  264. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  265. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  266. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  267. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  268. package/framework/templates/docs/proposal.md +0 -192
  269. package/framework/templates/docs/spec.md +0 -57
  270. package/framework/templates/docs/ui-components.md +0 -52
  271. package/framework/templates/docs/ui-design-system.md +0 -76
  272. package/framework/templates/docs/ui-flows.md +0 -167
  273. package/framework/templates/examples/spec-examples.md +0 -91
  274. package/framework/templates/feature/decisions.md +0 -124
  275. package/framework/templates/feature/recap.md +0 -145
  276. package/framework/templates/project/validate.js +0 -97
  277. package/framework/templates/tasks.template.json +0 -17
  278. package/src/commands/state/usecase.js +0 -143
  279. package/src/commands/tasks/dag.js +0 -181
  280. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  281. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  282. package/src/lib/agents/output-collector.js +0 -76
  283. package/src/lib/evaluator-report.js +0 -91
  284. package/src/lib/specs/acervo-format.js +0 -70
  285. package/src/lib/specs/acervo-lifecycle.js +0 -264
  286. package/src/lib/specs/done-criteria-check.js +0 -200
  287. package/src/lib/specs/usecase-check.js +0 -301
  288. package/src/lib/specs/usecase-promoter.js +0 -301
  289. package/src/lib/specs/usecase-registry.js +0 -237
  290. package/src/lib/tasks/dispatch-planner.js +0 -319
  291. package/src/lib/tasks/mermaid-renderer.js +0 -132
  292. package/src/lib/tasks/task-classifier.js +0 -30
  293. package/src/lib/tasks/task-parser.js +0 -209
  294. package/src/lib/tasks/tasks-json-parser.js +0 -296
  295. package/src/lib/tasks/test-scope.js +0 -597
  296. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  297. package/src/lib/validators/architecture/index.js +0 -7
  298. package/src/lib/validators/css/css-validator.js +0 -340
  299. package/src/lib/validators/css/index.js +0 -7
  300. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  301. package/src/lib/validators/design-system/index.js +0 -7
  302. package/src/lib/validators/score-aggregator.js +0 -244
  303. package/src/lib/validators/ui/index.js +0 -7
  304. 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 per-task loop: build + tests + validators (code, free) first; 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, isZeroTestRun, mtpSummaryTotals,
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 (or a single task within it).
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, taskId, options = {}) {
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, taskId, options, gate);
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, taskId, options, gate) {
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, taskId });
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: { ...testPlan, nodes: scoped.nodes },
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
- // How the nodes were chosen, for whoever reads --json. It lives on the rollup
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
- const rubricId = resolveRubricId({ task, rubricOverride, projectPath });
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
- nodes.e2e = await runE2eNode({
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
- // 5. traceability — advisory-only, feature-scope only (see traceability-check.js
277
- // for the recap-vs-outputs disagreement it flags, and what it deliberately
278
- // cannot catch). Lives OUTSIDE `nodes` on purpose: `status` below is computed
279
- // from `nodes` alone, so this can never flip build/tests/validators/e2e green
280
- // to red — recap/outputs disagreement is a rastreabilidade signal, not a
281
- // correctness gate.
282
- const traceability = taskId
283
- ? { status: 'skip', warnings: [], reason: 'task-scoped verify does not run traceability', citedCount: 0 }
284
- : _checkOutputsTraceability(projectPath, featureName);
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
- async function runBuildNode({ projectPath, stack, paths = {}, timeout, skipBuild, gate, _runBuildCheck }) {
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 steps = getBuildSteps(stack, { cwd: projectPath, frontendPath: paths.frontendPath, backendPath: paths.backendPath });
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, taskId, refreshCache = false,
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, taskId, timeout, linuxImage, gate, _runLinuxSuite, _exec,
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, taskId, timeout, refreshCache, gate, _runTestSuite,
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 outside the task
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, taskId, timeout, linuxImage, gate, _runLinuxSuite, _exec,
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, taskId, projectPath });
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, taskId, timeout, refreshCache = false, gate, _runTestSuite,
479
+ projectPath, node, filter, timeout, refreshCache = false, gate, env = null, _runTestSuite,
478
480
  }) {
479
- const decided = preDecideRun({ node, filter, taskId, projectPath, refreshCache });
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, ...(evalChannel ? { env: evalChannel.env } : {}),
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, taskId, projectPath = '.', refreshCache = false }) {
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. The only
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, { taskId, refresh: refreshCache });
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, taskId?: string|null }} opts
822
+ * @param {{ skipTests: boolean }} opts
913
823
  * @returns {string|null}
914
824
  */
915
- function baselineTargetProblem(projectPath, testPlan, { skipTests, taskId = null }) {
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
- // The selection travels with the count so the WRITER can refuse a recorte
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, rubricId, _runValidation }) {
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
- priorityFixes: [],
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
- priorityFixes: scoreResult.priorityFixes || [],
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 score; when the whole node skipped, its own line already says why.
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
- for (const fix of validatorsNode.priorityFixes || []) {
1034
- const topIssue = fix.issues && fix.issues[0];
1035
- if (!topIssue) continue;
1036
- const location = topIssue.file
1037
- ? ` (${topIssue.file}${topIssue.line ? ':' + topIssue.line : ''})`
1038
- : '';
1039
- feedback.push(`[validators/${fix.category} ${fix.score}/10] ${topIssue.message}${location}`);
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, taskId, task = null, skipE2e, freshE2e, buildNode, gate, _runE2eCheck }) {
942
+ async function runE2eNode({ projectPath, featureName, skipE2e, freshE2e, buildNode, gate, _runE2eCheck }) {
1048
943
  if (buildNode.status === 'fail') {
1049
944
  return {
1050
- status: 'skip',
1051
- command: null,
1052
- durationMs: 0,
1053
- reason: 'build failed — the stack could not come up anyway',
1054
- output: '',
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
- // The port block matters MORE here than in `e2e up`: this is the path that
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 task is the difference between a usable loop and one
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
- status: result.status,
1086
- command: result.command ?? null,
1087
- durationMs,
1088
- reason: result.reason ?? null,
1089
- output: result.output || '',
1090
- // Non-fatal findings (e.g. a compose with fixed ports at the ROOT, where
1091
- // nothing collides yet). Surfaced so the same compose does not fail
1092
- // unannounced the day the feature moves into a worktree.
1093
- warnings: Array.isArray(result.warnings) ? result.warnings : [],
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
  // ============================================================================