@polymorphism-tech/morph-spec 8.47.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 (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  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 +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,41 +1,3 @@
1
- /**
2
- * Test Nodes — the PLURAL resolver for "which test suites does this project have?".
3
- *
4
- * WHY THIS MODULE EXISTS
5
- *
6
- * `test-runner.js` answers a singular question: ONE command, resolved by a
7
- * four-step fallback. That shape is what defect F30 exploited — the resolver
8
- * found a `.sln`, answered "dotnet test", and 3.082 vitest tests in the same
9
- * repository never ran while the gate reported green. A project with two
10
- * runners had no way to say so, and a verifier that cannot be told about a
11
- * suite cannot fail because of it.
12
- *
13
- * So the unit of verification becomes a LIST of nodes. `project.tests[]` in
14
- * `.morph/config/config.json` declares them; when it is absent, the four-step
15
- * fallback still runs and is synthesized into exactly one node. Declared beats
16
- * guessed (D3): a declaration — `tests[]` OR `testCommand` — still wins
17
- * completely, and deeper auto-detection would only trade one silent false
18
- * negative for another there. But with NOTHING declared, the guess stops
19
- * being allowed to be a single answer: the old first-match fallback still
20
- * picks the primary node exactly as before, and any vitest/jest suite found
21
- * under a `package.json` (issue #87) is added BESIDE it, because that single
22
- * guess is exactly what hid 3.082 vitest tests behind a green gate (F30) —
23
- * the fallback found `.sln`, answered "dotnet test", and never looked twice.
24
- *
25
- * WHY VALIDATION IS FAIL-CLOSED HERE (D4)
26
- *
27
- * Every other config reader in this repo is fail-open — `JSON.parse` inside a
28
- * `try/catch`, optional chaining, absent means "carry on". That is right for a
29
- * hook: a guardrail that kills the session is worse than one that stays quiet.
30
- * It is exactly WRONG for the verifier. A typo in `tests[]` treated fail-open
31
- * falls through to the legacy resolver and reproduces F30 — green over a suite
32
- * that never ran. So an invalid node is not ignored: it becomes a node with a
33
- * pre-decided `status: 'fail'` and a reason naming the problem. It shows up in
34
- * `runs[]` and it reddens the gate.
35
- *
36
- * @module test-nodes
37
- */
38
-
39
1
  import { existsSync, readFileSync } from 'fs';
40
2
  import { join, resolve, relative, sep } from 'path';
41
3
 
@@ -44,71 +6,17 @@ import { EVAL_RUNNER, evalBlockProblem } from './eval-node.js';
44
6
  import { atomicWriteJson, CONFIG_FILE_NAME } from '../../core/state/state-manager.js';
45
7
  import { PACKAGE_LOCATIONS } from '../detectors/index.js';
46
8
 
47
- /**
48
- * The count extractors that exist (one per runner, see parseTestCount).
49
- * `none` is the honest answer for a command whose summary nobody can read —
50
- * it yields an UNKNOWN count, never a fabricated zero.
51
- */
9
+ /** `none` yields an UNKNOWN count, never a fabricated zero. */
52
10
  export const TEST_RUNNERS = ['dotnet', 'vitest', 'jest', 'node-test', 'playwright', 'none', EVAL_RUNNER];
53
11
 
54
- /**
55
- * The task-group vocabulary a node can declare via `groups`, and that
56
- * `tasks.json` entries declare via `group` (framework/schemas/tasks.schema.json
57
- * `~:28`) — ONE enum, checked to agree by canary, never two that drift.
58
- * Single home for B3's task-scope selection (`test-scope.js`) to import from.
59
- */
60
- export const TASK_GROUPS = ['backend', 'frontend', 'tests', 'infra', 'docs'];
61
-
62
- /**
63
- * Which group(s) a node serves when it does NOT declare `groups` itself,
64
- * keyed by runner. `none` and `evals` are deliberately ABSENT — a command
65
- * nobody could read (`none`) and a declared eval suite (`evals`) have no
66
- * honest default group, so `effectiveGroups()` answers null for them rather
67
- * than guessing one.
68
- */
69
- export const RUNNER_DEFAULT_GROUPS = {
70
- dotnet: ['backend'],
71
- 'node-test': ['backend'],
72
- vitest: ['frontend'],
73
- jest: ['frontend'],
74
- playwright: ['frontend'],
75
- };
76
-
77
- /**
78
- * Which task group(s) this node answers to: what it DECLARED, or else its
79
- * runner's default. **null is "no mapping"**, never an invented guess — B3's
80
- * task-scope selection reads null as "this node is unscoped, always runs".
81
- *
82
- * @param {import('./test-nodes.js').TestNode} node
83
- * @returns {string[]|null}
84
- */
85
- export function effectiveGroups(node) {
86
- return node?.groups ?? RUNNER_DEFAULT_GROUPS[node?.runner] ?? null;
87
- }
88
-
89
- /**
90
- * A node id is what `--filter` matches and what keys `runs[]`. Lowercase and
91
- * dash-only so it survives a shell argument and a JSON key without quoting.
92
- */
93
12
  export const TEST_NODE_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
94
13
 
95
- /** Id given to the single node the fallback synthesizes — it has no name of its own. */
96
14
  const FALLBACK_NODE_ID = 'default';
97
15
 
98
16
  /**
99
- * Parse the `--filter` argument into the token list `nodeMatchesFilter` reads.
100
- *
101
- * Lives here, beside TEST_NODE_ID_PATTERN, because the thing being matched is
102
- * a node id: the spelling rules for the id and the spelling rules for what
103
- * selects it must never drift into two modules.
104
- *
105
- * An argument that yields NO tokens (`--filter ""`, `--filter " , "`) returns
106
- * an empty token list rather than null. That distinction is the point: null
107
- * means "the user did not filter", `[]` means "the user filtered and selected
108
- * nothing" — and selecting nothing is a usage error, not a full run.
109
- *
110
- * @param {string|null|undefined} raw
111
- * @returns {{ raw: string, tokens: string[] }|null} null when no filter was given
17
+ * null means "no filter given"; `{tokens: []}` means "filtered and selected nothing",
18
+ * which is a usage error, not a full run.
19
+ * @returns {{ raw: string, tokens: string[] }|null}
112
20
  */
113
21
  export function parseTestFilter(raw) {
114
22
  if (raw === undefined || raw === null || raw === false) return null;
@@ -119,64 +27,36 @@ export function parseTestFilter(raw) {
119
27
  };
120
28
  }
121
29
 
122
- /**
123
- * Does this node answer to the filter? Exact id, or the token as a substring
124
- * of the id (`--filter dot` selects `dotnet`), case-insensitively.
125
- *
126
- * @param {TestNode} node
127
- * @param {{ tokens: string[] }|null} filter - null (no filter) matches everything
128
- * @returns {boolean}
129
- */
30
+ /** Exact id or substring of the id, case-insensitive; a null filter matches everything. */
130
31
  export function nodeMatchesFilter(node, filter) {
131
32
  if (!filter) return true;
132
33
  const id = String(node?.id ?? '').toLowerCase();
133
34
  return filter.tokens.some((token) => id === token || id.includes(token));
134
35
  }
135
36
 
136
- /** Reason kept WORD FOR WORD from the singular resolver: readers already print it. */
37
+ /** Kept word for word from the singular resolver: readers already print it. */
137
38
  const NO_SUITE_REASON = 'No testCommand configured and no known project file detected';
138
39
 
139
40
  /**
140
- * One declared (or synthesized) test suite.
141
- *
142
41
  * @typedef {Object} TestNode
143
- * @property {string} id - Unique within the list; what `--filter` matches.
144
- * @property {string|null} command - Literal shell command; null only on a node already decided.
145
- * @property {string} cwd - Relative to the project root; '.' by default.
146
- * @property {string} runner - One of TEST_RUNNERS; inferred from `command` when not declared.
147
- * @property {number|null} expectedCount - Recorded baseline. **null is UNKNOWN, never 0**:
148
- * a 0 here would fabricate a count drop out of "no baseline recorded yet".
149
- * @property {number|null} timeoutMs - Per-node ceiling. **null is UNKNOWN, never 0** — it
150
- * means "derive the ceiling", not "no time allowed".
151
- * @property {string[]} build - Test projects to compile first (dotnet only); `[]` is
152
- * "nothing declared", which is a real answer, not an unknown.
153
- * @property {boolean} e2e - Needs the stack up; feature scope only.
154
- * @property {string|null} linuxImage - Container image for `verify --linux`; null defers to the run's.
155
- * @property {number} projectCount - How many test projects `command` chains — the input to
156
- * the derived timeout. Always 1 for a declared node.
157
- * @property {'fail'|'skip'|null} status - Pre-decided verdict. **Non-null means DO NOT
158
- * EXECUTE**: the node is invalid config, or an explicit declaration of "no tests".
159
- * null is the normal case: no verdict yet, the run decides.
160
- * @property {string|null} reason - Why, when `status` is set.
42
+ * @property {string} id
43
+ * @property {string|null} command - null only on a node already decided.
44
+ * @property {string} cwd - Relative to the project root.
45
+ * @property {string} runner - One of TEST_RUNNERS.
46
+ * @property {number|null} expectedCount - null is UNKNOWN, never 0.
47
+ * @property {number|null} timeoutMs - null means "derive the ceiling", never 0.
48
+ * @property {string[]} build - Test projects to compile first (dotnet only).
49
+ * @property {boolean} e2e
50
+ * @property {string|null} linuxImage - null defers to the run's image.
51
+ * @property {number} projectCount - Input to the derived timeout; 1 for a declared node.
52
+ * @property {'fail'|'skip'|null} status - Non-null means DO NOT EXECUTE.
53
+ * @property {string|null} reason
161
54
  */
162
55
 
163
- /**
164
- * Infer which summary extractor can read a command's output.
165
- *
166
- * Deliberately conservative: `npm test` can front ANY runner, so it infers
167
- * `none` (unknown count) rather than guessing. An extractor applied to the
168
- * wrong output produces `null` anyway — but a WRONG guess that happens to
169
- * match would invent a number, and numbers here fail gates.
170
- *
171
- * @param {string|null|undefined} command
172
- * @returns {string} one of TEST_RUNNERS
173
- */
56
+ /** `npm test` can front any runner, so it infers `none`: a wrong guess would invent a number that fails gates. */
174
57
  export function inferRunner(command) {
175
- // `evals` is never inferred, on purpose: it is a DECLARATION, not a shape a
176
- // command line can betray. An eval command is literally `dotnet test`, so any
177
- // sniffing here would classify every dotnet suite as an eval — and, worse, the
178
- // reverse mistake (an eval declared and read as plain dotnet) would silently
179
- // drop the cache pre-flight.
58
+ // `evals` is never inferred: an eval command is literally `dotnet test`, and
59
+ // reading a declared eval as plain dotnet would drop the cache pre-flight.
180
60
  if (typeof command !== 'string' || !command.trim()) return 'none';
181
61
  if (/\bdotnet\s+test\b/.test(command)) return 'dotnet';
182
62
  if (/\bvitest\b/.test(command)) return 'vitest';
@@ -186,10 +66,6 @@ export function inferRunner(command) {
186
66
  return 'none';
187
67
  }
188
68
 
189
- /**
190
- * Build a fully-shaped node. Every field is present on every node so that no
191
- * reader has to distinguish "absent" from "unknown" by optional chaining.
192
- */
193
69
  function makeNode(partial) {
194
70
  return {
195
71
  id: partial.id,
@@ -200,26 +76,15 @@ function makeNode(partial) {
200
76
  timeoutMs: partial.timeoutMs ?? null,
201
77
  build: partial.build ?? [],
202
78
  e2e: partial.e2e ?? false,
203
- // Per-node image for `verify --linux`. **null is UNKNOWN, never ''** — it
204
- // means "use the run's image", not "use no image".
205
79
  linuxImage: partial.linuxImage ?? null,
206
80
  projectCount: partial.projectCount ?? 1,
207
- // The eval block travels on the node so `verify-runner` never has to go back
208
- // to config.json to learn where the cache lives. **null is "this is not an
209
- // eval node"** — distinct from an eval node with an empty block, which
210
- // `validateTestNodes` already refused.
81
+ // null is "not an eval node", distinct from an empty block (already refused).
211
82
  evals: partial.evals ?? null,
212
- // Declared task-group(s) this node serves (§B3 task-scope selection).
213
- // **null is "not declared"**, distinct from an empty array (which
214
- // `firstProblem` refuses outright) — effectiveGroups() falls back to the
215
- // runner's default only when this is null.
216
- groups: partial.groups ?? null,
217
83
  status: partial.status ?? null,
218
84
  reason: partial.reason ?? null,
219
85
  };
220
86
  }
221
87
 
222
- /** A node that will not run, and says why in the vocabulary of §2.2 of the spec. */
223
88
  function invalidNode(id, problem, raw = {}) {
224
89
  return makeNode({
225
90
  id,
@@ -238,15 +103,6 @@ function isNonNegativeInteger(value) {
238
103
  return Number.isInteger(value) && value >= 0;
239
104
  }
240
105
 
241
- /**
242
- * First problem with one raw `tests[]` entry, or null when it is well formed.
243
- * First problem only: a list of complaints about a single node buries the one
244
- * that has to be fixed first.
245
- *
246
- * @param {*} raw
247
- * @param {string|null} projectPath - When given, `cwd` is also checked against the disk.
248
- * @returns {string|null}
249
- */
250
106
  function firstProblem(raw, projectPath) {
251
107
  if (!isPlainObject(raw)) return 'must be an object';
252
108
 
@@ -271,10 +127,7 @@ function firstProblem(raw, projectPath) {
271
127
  if (raw.build !== undefined && (!Array.isArray(raw.build) || raw.build.some((b) => typeof b !== 'string'))) {
272
128
  return 'build must be an array of strings';
273
129
  }
274
- // `build[]` is compiled by the dotnet prebuild and by nothing else. Declared
275
- // on a vitest node it would be silently ignored — and a declaration the
276
- // verifier ignores is the same fail-open shape as the typo in `tests[]`:
277
- // the author believes something is being compiled that never is.
130
+ // A declaration the verifier would silently ignore is refused, not dropped.
278
131
  if (Array.isArray(raw.build) && raw.build.length > 0
279
132
  && (raw.runner ?? inferRunner(raw.command)) !== 'dotnet') {
280
133
  return 'build applies only to runner "dotnet"';
@@ -285,10 +138,6 @@ function firstProblem(raw, projectPath) {
285
138
  if (raw.linuxImage !== undefined && (typeof raw.linuxImage !== 'string' || !raw.linuxImage.trim())) {
286
139
  return 'linuxImage must be a non-empty string';
287
140
  }
288
- // `runner: "evals"` REQUIRES its block; the block on any other runner is a
289
- // declaration the verifier would silently ignore — the same fail-open shape as
290
- // a typo in `tests[]`, where the author believes something is being checked
291
- // that never is.
292
141
  if ((raw.runner ?? inferRunner(raw.command)) === EVAL_RUNNER) {
293
142
  const problem = evalBlockProblem(raw, projectPath);
294
143
  if (problem) return problem;
@@ -296,41 +145,14 @@ function firstProblem(raw, projectPath) {
296
145
  return `evals applies only to runner "${EVAL_RUNNER}"`;
297
146
  }
298
147
 
299
- if (raw.groups !== undefined) {
300
- if (!Array.isArray(raw.groups)) {
301
- return 'groups must be a non-empty array';
302
- }
303
- if (raw.groups.length === 0) {
304
- return 'groups must be a non-empty array';
305
- }
306
- const invalid = raw.groups.filter((g) => !TASK_GROUPS.includes(g));
307
- if (invalid.length > 0) {
308
- return `groups must contain only ${TASK_GROUPS.join(' | ')} (invalid: ${invalid.join(', ')})`;
309
- }
310
- const seenGroups = new Set();
311
- const duplicates = new Set();
312
- for (const g of raw.groups) {
313
- if (seenGroups.has(g)) duplicates.add(g);
314
- seenGroups.add(g);
315
- }
316
- if (duplicates.size > 0) {
317
- return `groups must not contain duplicates (repeated: ${[...duplicates].join(', ')})`;
318
- }
319
- }
320
-
321
- // The cwd checks come last because they are the expensive ones, and because
322
- // a node that is malformed in shape has a better error to report first.
323
148
  const cwd = typeof raw.cwd === 'string' ? raw.cwd.trim() : '.';
324
149
  const root = resolve(projectPath ?? '.');
325
150
  const abs = resolve(root, cwd);
326
151
  if (!isInside(abs, root)) {
327
- // A cwd that leaves the root addresses ANOTHER tree — the same damage the
328
- // worktree exclusion in collectDotnetTestProjects exists to prevent.
329
152
  return `cwd "${cwd}" resolves outside the project root`;
330
153
  }
331
154
  if (projectPath && !existsSync(abs)) {
332
- // P5: named failure, never a skip. A wrong cwd is a plan error, and a skip
333
- // would hide it behind a green gate.
155
+ // Named failure, never a skip: a skip would hide a wrong cwd behind a green gate.
334
156
  return `cwd "${cwd}" does not exist`;
335
157
  }
336
158
 
@@ -338,16 +160,9 @@ function firstProblem(raw, projectPath) {
338
160
  }
339
161
 
340
162
  /**
341
- * Validate `config.project.tests[]` IN USE — this is called by
342
- * resolveTestNodes(), not merely declared in a schema. A schema that nothing
343
- * calls is decoration; this repo already paid for fifteen guardrails that
344
- * could not detect anything (v8.9/v8.11).
345
- *
346
- * Fail-closed by design (D4): a bad entry becomes a node with
347
- * `status: 'fail'`, never a node that disappears.
348
- *
349
- * @param {object|null} config - Parsed `.morph/config/config.json`
350
- * @param {string|null} [projectPath] - Project root; enables the on-disk `cwd` check
163
+ * Fail-CLOSED, unlike the repo's other config readers: a typo that falls through
164
+ * to the fallback resolver gives a green gate over a suite that never ran.
165
+ * A bad entry becomes a `status: 'fail'` node, never a node that disappears.
351
166
  * @returns {TestNode[]} One node per declared entry, in declaration order
352
167
  */
353
168
  export function validateTestNodes(config, projectPath = null) {
@@ -357,9 +172,7 @@ export function validateTestNodes(config, projectPath = null) {
357
172
  return [invalidNode('project.tests', 'must be an array of test nodes')];
358
173
  }
359
174
 
360
- // Count ids BEFORE judging: two nodes sharing an id make `--filter` and
361
- // `runs[]` ambiguous, and picking a winner would silence one declared suite.
362
- // Both sides fail — the ambiguity is the defect, not either entry.
175
+ // Duplicate ids fail on both sides: picking a winner would silence a declared suite.
363
176
  const seen = new Map();
364
177
  for (const entry of raw) {
365
178
  if (isPlainObject(entry) && typeof entry.id === 'string') {
@@ -390,13 +203,11 @@ export function validateTestNodes(config, projectPath = null) {
390
203
  e2e: entry.e2e ?? false,
391
204
  linuxImage: entry.linuxImage ?? null,
392
205
  evals: entry.evals ?? null,
393
- groups: entry.groups ?? null,
394
206
  projectCount: 1,
395
207
  });
396
208
  });
397
209
  }
398
210
 
399
- /** Turn a relative path into an id-safe slug matching TEST_NODE_ID_PATTERN. */
400
211
  function slugifyForId(text) {
401
212
  return text
402
213
  .toLowerCase()
@@ -405,20 +216,8 @@ function slugifyForId(text) {
405
216
  }
406
217
 
407
218
  /**
408
- * Guess which frontend test suites this project has, from `package.json`
409
- * alone — a GUESS (issue #87), never a declaration. Only called by the
410
- * fallback of `resolveTestNodes()`, and only when nothing was declared: a
411
- * `package.json` that names `vitest`/`jest` in `dependencies` or
412
- * `devDependencies` becomes one candidate suite, exactly the shape F30
413
- * exploited when a repo had .NET AND a frontend and the first-match fallback
414
- * only ever answered "dotnet test".
415
- *
416
- * Fail-open by construction (it IS a guess): an unreadable or malformed
417
- * `package.json` is skipped silently, never a `fail` node — that fail-closed
418
- * treatment is reserved for a DECLARATION (`tests[]`), which this is not.
419
- *
420
- * @param {string} projectPath - Project root
421
- * @param {object|null} config - Parsed `.morph/config/config.json`, or null
219
+ * A GUESS from `package.json` (vitest/jest in deps), never a declaration — so it
220
+ * is fail-open: an unreadable `package.json` is skipped, never a `fail` node.
422
221
  * @returns {Array<{ id: string, cwd: string, runner: 'vitest'|'jest', command: string }>}
423
222
  */
424
223
  export function discoverFrontendPackages(projectPath, config) {
@@ -428,9 +227,6 @@ export function discoverFrontendPackages(projectPath, config) {
428
227
  const frontendPath = config?.project?.frontendPath;
429
228
  if (typeof frontendPath === 'string' && frontendPath.trim()) {
430
229
  const abs = resolve(root, frontendPath.trim());
431
- // Outside the root is IGNORED, not refused — this is a guess reusing
432
- // another guess (detectProject's own frontendPath), and neither one gets
433
- // to address another tree.
434
230
  if (isInside(abs, root)) candidates.push(frontendPath.trim());
435
231
  }
436
232
 
@@ -450,7 +246,7 @@ export function discoverFrontendPackages(projectPath, config) {
450
246
  try {
451
247
  pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
452
248
  } catch {
453
- continue; // unreadable/invalid — skipped silently, fail-open (it's a guess)
249
+ continue;
454
250
  }
455
251
 
456
252
  const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
@@ -482,40 +278,19 @@ export function discoverFrontendPackages(projectPath, config) {
482
278
  }
483
279
 
484
280
  /**
485
- * Resolve every test suite of a project, in the order the verifier must run them.
486
- *
487
- * Precedence — the whole point is that the last three steps still work with NO
488
- * config at all, because this repository (the framework source) has none and
489
- * every project installed before this feature has none either:
490
- *
491
- * 1. `project.tests[]` declared → one node per entry, source 'config-tests'
492
- * 2. `project.testCommand` declared → ONE synthesized node, source 'config'
493
- * 3. the four-step fallback of → ONE synthesized node, source
494
- * resolveTestPlan() (root .sln, 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'
495
- * *Tests.csproj scan, npm test)
496
- *
497
- * Steps 2 and 3 are `resolveTestPlan()` itself — CALLED, never re-implemented.
498
- * That matters twice over: it is the only place that knows the fallback order,
499
- * and it is the only place that excludes linked worktrees from the
500
- * `*Tests.csproj` scan (a worktree is another feature's checkout; scanning it
501
- * doubles the clock and reports on code outside the tree being judged).
502
- *
503
- * The list is never empty: "nothing to run" is expressed as ONE node already
504
- * decided as `skip`, so the reason travels to the report instead of vanishing.
505
- *
506
- * @param {string} [projectPath] - Project root directory (defaults to '.')
281
+ * Precedence: `project.tests[]` → `project.testCommand` → `resolveTestPlan()`'s
282
+ * fallback (called, never re-implemented: it owns the order and the worktree
283
+ * exclusion). Never empty: "nothing to run" is one node decided as `skip`.
507
284
  * @returns {{ nodes: TestNode[], source: string }}
508
285
  */
509
286
  export function resolveTestNodes(projectPath = '.') {
510
287
  const config = readMorphConfig(projectPath);
511
288
 
512
- // `tests` present in ANY shape is a declaration of intent. Even a malformed
513
- // one: falling back from a typo is how F30 comes back.
289
+ // `tests` in ANY shape is a declaration: falling back from a typo hides a suite.
514
290
  if (config?.project?.tests !== undefined) {
515
291
  const nodes = validateTestNodes(config, projectPath);
516
292
  if (Array.isArray(config.project.tests) && config.project.tests.length === 0) {
517
- // `[]` is an explicit "this project declares no test suites" — distinct
518
- // from an absent key, which means "guess for me".
293
+ // `[]` declares "no suites"; an absent key means "guess for me".
519
294
  return {
520
295
  nodes: [makeNode({ id: 'none', status: 'skip', reason: 'no test nodes declared' })],
521
296
  source: 'config-tests',
@@ -532,16 +307,11 @@ export function resolveTestNodes(projectPath = '.') {
532
307
  : makeNode({ id: FALLBACK_NODE_ID, status: 'skip', reason: NO_SUITE_REASON }),
533
308
  ];
534
309
 
535
- // The union (#87 PR B / F30): add discovered frontend suites BESIDE the
536
- // fallback node — but only when nothing was declared. `plan.source ===
537
- // 'config'` means `project.testCommand` IS a declaration (it lives inside
538
- // resolveTestPlan's own step 1), and a declaration wins completely: nothing
539
- // is added beside it, same as the `project.tests[]` branch above.
310
+ // Discovered frontend suites go BESIDE the fallback node, never beside a
311
+ // declaration (`plan.source === 'config'` is `project.testCommand`).
540
312
  if (plan.source !== 'config') {
541
313
  const discovered = discoverFrontendPackages(projectPath, config)
542
- // The root package is already run by `npm test` when that is the
543
- // fallback's own answer — adding it again would run the same suite
544
- // twice under two different commands.
314
+ // The root package is already run by the fallback's `npm test`.
545
315
  .filter((pkg) => !(pkg.cwd === '.' && plan.source === 'npm'))
546
316
  .map((pkg) => makeNode({ id: pkg.id, command: pkg.command, cwd: pkg.cwd, runner: pkg.runner }));
547
317
  nodes.push(...discovered);
@@ -550,50 +320,15 @@ export function resolveTestNodes(projectPath = '.') {
550
320
  return { nodes, source: plan.source };
551
321
  }
552
322
 
553
- /**
554
- * Path of the config the baseline lives in. One spelling, so the writer and
555
- * `readMorphConfig()` can never disagree about which file is meant — and that
556
- * is now literally true: both derive from `CONFIG_FILE_NAME`
557
- * (`core/state/state-manager.js`), the same constant `state-manager.js` itself
558
- * reads the project identity through. It used to be a promise made by a JSDoc
559
- * over two hand-typed copies of the same string.
560
- */
561
323
  export function morphConfigPath(projectPath = '.') {
562
324
  return join(projectPath, CONFIG_FILE_NAME);
563
325
  }
564
326
 
565
327
  /**
566
- * Write measured `expectedCount` values back into `project.tests[]`.
567
- *
568
- * WHY THIS IS A FUNCTION AND NOT AN EDIT (D8)
569
- *
570
- * `MORPH.md` says a baseline is a COMMAND, never a number — and this feature
571
- * stores a number. The rule survives because nobody is allowed to type it: the
572
- * only writer is `verify --record-baseline`, and it writes what a real, green
573
- * run measured. A hand-typed number describes what someone believed; the
574
- * divergence between belief and the suite is exactly the failure the rule was
575
- * written after.
576
- *
577
- * The write is a POINTED mutation: read → set one field per declared node →
578
- * atomic `tmp.{pid}` + rename via `atomicWriteJson` (the same door
579
- * `state-manager.js` uses, not a second copy of it). Every other key of
580
- * `config.json` survives, because this command owns one field and nothing else.
581
- *
582
- * It refuses rather than improvises:
583
- * - a measurement whose `count` is not an integer (**null is UNKNOWN, never
584
- * 0** — a written 0 would fabricate a drop against every later run);
585
- * - a measured id that is not declared (appending a node would be inventing
586
- * the declaration the command came to update);
587
- * - a declared node with no measurement (a baseline recorded for part of a
588
- * fleet says nothing true about the rest of it);
589
- * - a measurement carrying a task-scope `selection` (#87): narrowed, scoped
590
- * out or run whole INSIDE a task-scoped verify, it measured a recorte, and
591
- * `expectedCount` is the whole suite's number. `verify` already refuses
592
- * `--record-baseline` with a task id before the build; this is the second
593
- * barrier, at the writer, so no caller can route around it.
594
- *
595
- * @param {string} projectPath - Project root
596
- * @param {Array<{id: string, count: number|null, selection?: object|null}>} measurements - One per node that ran
328
+ * The only writer of `expectedCount`: a baseline is measured by a green run, never typed.
329
+ * Refuses a non-integer count (null is UNKNOWN, never 0), an undeclared id, or a
330
+ * declared node with no measurement. Every other config key survives.
331
+ * @param {Array<{id: string, count: number|null}>} measurements
597
332
  * @returns {{ recorded: boolean, reason: string|null, updates: Array<{id: string, from: number|null, to: number}>, path: string }}
598
333
  */
599
334
  export function recordTestBaselines(projectPath, measurements) {
@@ -614,11 +349,6 @@ export function recordTestBaselines(projectPath, measurements) {
614
349
  return refuse('config.json não declara project.tests[] — não há entrada onde gravar a baseline');
615
350
  }
616
351
 
617
- const recortes = measurements.filter((m) => m.selection != null).map((m) => m.id);
618
- if (recortes.length > 0) {
619
- return refuse(`medição de recorte de task em: ${recortes.join(', ')} — baseline só de execução inteira, sem {task}`);
620
- }
621
-
622
352
  const unreadable = measurements.filter((m) => !Number.isInteger(m.count)).map((m) => m.id);
623
353
  if (unreadable.length > 0) {
624
354
  return refuse(`contagem desconhecida em: ${unreadable.join(', ')} — desconhecido nunca vira 0`);