@polymorphism-tech/morph-spec 9.0.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +11 -63
  4. package/bin/validate.js +1 -39
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -1
  9. package/framework/MORPH.md +3 -3
  10. package/framework/agents.json +19 -11
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +22 -31
  14. package/framework/evals/review-checklist.md +12 -2
  15. package/framework/hooks/README.md +8 -44
  16. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  17. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  18. package/framework/hooks/claude-code/core/chain.js +4 -65
  19. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  20. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  21. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  22. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  23. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  24. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  25. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  26. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  27. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  28. package/framework/hooks/claude-code/core/state-sync.js +12 -68
  29. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  30. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  31. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  32. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  33. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  34. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  35. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  36. package/framework/hooks/claude-code/statusline.sh +11 -11
  37. package/framework/hooks/shared/activity-logger.js +9 -89
  38. package/framework/hooks/shared/agent-reap.js +2 -17
  39. package/framework/hooks/shared/build-check.js +6 -75
  40. package/framework/hooks/shared/claim-registry.js +9 -119
  41. package/framework/hooks/shared/command-tokens.js +6 -51
  42. package/framework/hooks/shared/comment-rules.js +5 -20
  43. package/framework/hooks/shared/comment-scanner.js +6 -25
  44. package/framework/hooks/shared/comment-stripper.js +11 -85
  45. package/framework/hooks/shared/e2e-check.js +0 -0
  46. package/framework/hooks/shared/errata.js +2 -15
  47. package/framework/hooks/shared/feature-locator.js +11 -95
  48. package/framework/hooks/shared/frontend-locations.js +1 -3
  49. package/framework/hooks/shared/gate-question.js +1 -7
  50. package/framework/hooks/shared/git-exclude.js +3 -20
  51. package/framework/hooks/shared/git-paths.js +8 -104
  52. package/framework/hooks/shared/graph-freshness.js +22 -166
  53. package/framework/hooks/shared/graph-refresh.js +21 -149
  54. package/framework/hooks/shared/heavy-command.js +4 -12
  55. package/framework/hooks/shared/hook-response.js +8 -83
  56. package/framework/hooks/shared/morph-cli.js +8 -52
  57. package/framework/hooks/shared/payload-utils.js +10 -132
  58. package/framework/hooks/shared/phase-utils.js +1 -42
  59. package/framework/hooks/shared/redact-secrets.js +4 -15
  60. package/framework/hooks/shared/reparse-scan.js +6 -66
  61. package/framework/hooks/shared/slice-progress.js +4 -23
  62. package/framework/hooks/shared/source-files.js +4 -17
  63. package/framework/hooks/shared/state-reader.js +13 -183
  64. package/framework/hooks/shared/state-writer.js +8 -52
  65. package/framework/hooks/shared/stdin-reader.js +1 -11
  66. package/framework/hooks/shared/telemetry-logger.js +7 -67
  67. package/framework/hooks/shared/transcript-usage.js +7 -29
  68. package/framework/hooks/shared/worktree-root.js +7 -29
  69. package/framework/rules/csharp-standards.md +1 -1
  70. package/framework/rules/frontend-standards.md +1 -0
  71. package/framework/rules/hangfire-standards.md +1 -1
  72. package/framework/rules/neon-standards.md +1 -1
  73. package/framework/rules/nextjs-standards.md +1 -1
  74. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  75. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  76. package/framework/skills/core/morph-apply/SKILL.md +4 -4
  77. package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
  78. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  79. package/framework/skills/core/morph-media/SKILL.md +1 -1
  80. package/framework/skills/core/morph-plan/SKILL.md +9 -7
  81. package/framework/skills/core/morph-review/SKILL.md +9 -6
  82. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  83. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  84. package/framework/standards/STANDARDS.json +36 -14
  85. package/framework/standards/STANDARDS.meta.json +16 -2
  86. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  87. package/framework/standards/ai-agents/agent-session.md +1 -1
  88. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  89. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  90. package/framework/standards/ai-agents/guardrails.md +1 -1
  91. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  92. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  93. package/framework/standards/ai-agents/production.md +1 -1
  94. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  95. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  96. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  97. package/framework/standards/ai-agents/setup.md +2 -2
  98. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  99. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  100. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  101. package/framework/standards/ai-agents/workflows.md +1 -1
  102. package/framework/standards/architecture/code-comments.md +1 -1
  103. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  104. package/framework/standards/backend/api/validation.md +1 -1
  105. package/framework/standards/backend/dotnet/testing.md +10 -4
  106. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  107. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  108. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  109. package/framework/standards/data/nosql/cache/redis.md +1 -1
  110. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  111. package/framework/standards/frontend/admin-app/forms.md +1 -1
  112. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  113. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  114. package/framework/standards/frontend/nextjs/testing.md +10 -6
  115. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  116. package/framework/templates/.idea/morph-templates.xml +92 -0
  117. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  118. package/framework/templates/CLAUDE.md.core.template +3 -3
  119. package/framework/templates/IDE-SNIPPETS.md +262 -262
  120. package/framework/templates/REGISTRY.json +9 -9
  121. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  122. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  123. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  124. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  125. package/framework/templates/docs/plan.md +3 -1
  126. package/framework/templates/docs/ui-mockups.md +4 -0
  127. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  128. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  129. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  130. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  131. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  132. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  133. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  134. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  135. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  136. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  137. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  138. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  139. package/framework/templates/state.template.json +17 -17
  140. package/framework/templates/ui/design-system.css +226 -226
  141. package/package.json +1 -1
  142. package/src/commands/context/context-refresh.js +0 -16
  143. package/src/commands/dashboard/dashboard.js +2 -8
  144. package/src/commands/e2e/e2e.js +17 -104
  145. package/src/commands/graph/affected.js +0 -14
  146. package/src/commands/graph/build.js +11 -50
  147. package/src/commands/graph/explain.js +0 -16
  148. package/src/commands/graph/status.js +1 -12
  149. package/src/commands/mcp/mcp-setup.js +4 -62
  150. package/src/commands/observability/cost.js +5 -57
  151. package/src/commands/observability/telemetry.js +1 -14
  152. package/src/commands/project/branch.js +4 -45
  153. package/src/commands/project/claim.js +4 -48
  154. package/src/commands/project/doctor.js +48 -302
  155. package/src/commands/project/env.js +1 -27
  156. package/src/commands/project/fleet.js +3 -32
  157. package/src/commands/project/init.js +4 -68
  158. package/src/commands/project/install-plugin-cmd.js +0 -1
  159. package/src/commands/project/standards.js +1 -62
  160. package/src/commands/project/status.js +0 -53
  161. package/src/commands/project/update.js +31 -239
  162. package/src/commands/project/worktree.js +108 -882
  163. package/src/commands/scaffold/scaffold.js +0 -25
  164. package/src/commands/state/advance.js +14 -85
  165. package/src/commands/state/approve.js +15 -97
  166. package/src/commands/state/archive.js +12 -64
  167. package/src/commands/state/auto-approval.js +2 -5
  168. package/src/commands/state/create.js +22 -109
  169. package/src/commands/state/delete.js +11 -63
  170. package/src/commands/state/finish.js +98 -475
  171. package/src/commands/state/gate-check.js +22 -132
  172. package/src/commands/state/gate-decision.js +3 -39
  173. package/src/commands/state/handoff.js +3 -30
  174. package/src/commands/state/list.js +14 -84
  175. package/src/commands/state/report-check.js +8 -58
  176. package/src/commands/state/retype.js +4 -26
  177. package/src/commands/validation/index.js +0 -3
  178. package/src/commands/validation/mutate.js +11 -11
  179. package/src/commands/validation/plan-check.js +4 -2
  180. package/src/commands/validation/tests-audit.js +7 -0
  181. package/src/commands/validation/validate-feature.js +10 -97
  182. package/src/commands/validation/verify.js +30 -113
  183. package/src/core/paths/output-schema.js +2 -101
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +0 -27
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +22 -304
  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 +0 -1
  191. package/src/core/work-types.js +1 -36
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  194. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  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 +1 -5
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/main.jsx +13 -13
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  211. package/src/dashboard/app/pages/Features.jsx +1 -8
  212. package/src/dashboard/app/pages/Guide.jsx +0 -1
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  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 +10 -69
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +0 -6
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +10 -66
  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 +2 -6
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/dashboard/vite.config.js +24 -24
  233. package/src/lib/ai/ai-checks.js +7 -52
  234. package/src/lib/ai/ai-project-scan.js +14 -108
  235. package/src/lib/ai/code-audit.js +7 -63
  236. package/src/lib/ai/pin-drift.js +8 -51
  237. package/src/lib/ai/registry-audit.js +11 -90
  238. package/src/lib/ci/ci-verify.js +4 -19
  239. package/src/lib/ci/github-ci.js +9 -37
  240. package/src/lib/ci/workflow-scaffold.js +14 -10
  241. package/src/lib/detectors/claude-config-detector.js +13 -74
  242. package/src/lib/detectors/index.js +4 -50
  243. package/src/lib/e2e/applicability.js +37 -0
  244. package/src/lib/e2e/compose-env.js +5 -66
  245. package/src/lib/e2e/stack-teardown.js +6 -40
  246. package/src/lib/generators/architecture-scanner.js +18 -156
  247. package/src/lib/generators/handoff-generator.js +9 -53
  248. package/src/lib/generators/scaffold-generator.js +5 -101
  249. package/src/lib/generators/settings-generator.js +4 -37
  250. package/src/lib/git/branch-classifier.js +11 -84
  251. package/src/lib/git/default-branch.js +14 -169
  252. package/src/lib/git/evidence-staleness.js +1 -0
  253. package/src/lib/git/gh.js +2 -17
  254. package/src/lib/git/pending-prs.js +6 -37
  255. package/src/lib/git/pr-files.js +3 -28
  256. package/src/lib/git/pr-index.js +3 -34
  257. package/src/lib/git/tree-fingerprint.js +3 -15
  258. package/src/lib/git/tree-state.js +6 -33
  259. package/src/lib/graph/god-nodes.js +2 -22
  260. package/src/lib/graph/graph-health.js +3 -24
  261. package/src/lib/graph/graph-loader.js +5 -56
  262. package/src/lib/graph/graphify-runtime.js +7 -37
  263. package/src/lib/graph/graphifyignore.js +8 -74
  264. package/src/lib/graph/sanitize.js +16 -136
  265. package/src/lib/heavy/cleanup.js +1 -1
  266. package/src/lib/installers/effects.js +8 -93
  267. package/src/lib/installers/mcp-installer.js +9 -169
  268. package/src/lib/installers/memory-installer.js +4 -27
  269. package/src/lib/installers/templates-reconcile.js +5 -60
  270. package/src/lib/mutation/adapters.js +6 -17
  271. package/src/lib/mutation/dotnet-introspection.js +20 -106
  272. package/src/lib/mutation/runner.js +23 -75
  273. package/src/lib/mutation/subset.js +1 -4
  274. package/src/lib/mutation/trx.js +0 -1
  275. package/src/lib/plan/artifact-vocabulary.js +5 -25
  276. package/src/lib/plan/checks.js +34 -33
  277. package/src/lib/plan/plan-parser.js +7 -8
  278. package/src/lib/projects/registry.js +7 -44
  279. package/src/lib/review-reports.js +11 -26
  280. package/src/lib/risk-detector.js +41 -173
  281. package/src/lib/specs/adr-index.js +6 -34
  282. package/src/lib/specs/frontmatter.js +2 -15
  283. package/src/lib/specs/gfm-table.js +1 -7
  284. package/src/lib/specs/revocations.js +1 -14
  285. package/src/lib/specs/screen-checklist.js +47 -124
  286. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +1 -8
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +1 -7
  291. package/src/lib/standards/sections.js +2 -13
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +21 -169
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -240
  296. package/src/lib/tasks/test-runner.js +43 -341
  297. package/src/lib/telemetry/metrics.js +7 -36
  298. package/src/lib/telemetry/telemetry-reader.js +13 -125
  299. package/src/lib/telemetry/verify-event.js +2 -30
  300. package/src/lib/tests-audit/audit.js +96 -0
  301. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  302. package/src/lib/validators/code-budget/index.js +0 -7
  303. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  304. package/src/lib/validators/contracts/index.js +0 -6
  305. package/src/lib/validators/nextjs/index.js +0 -5
  306. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  307. package/src/lib/validators/packages/index.js +0 -6
  308. package/src/lib/validators/packages/package-validator.js +6 -56
  309. package/src/lib/validators/reuse/index.js +0 -8
  310. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  311. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  312. package/src/lib/validators/traceability-check.js +5 -63
  313. package/src/lib/validators/validation-runner.js +21 -158
  314. package/src/lib/validators/verify-runner.js +85 -376
  315. package/src/lib/work-type-detector.js +5 -67
  316. package/src/lib/worktree/docker-audit.js +17 -136
  317. package/src/lib/worktree/env-allocator.js +10 -98
  318. package/src/lib/worktree/feature-truth.js +12 -115
  319. package/src/lib/worktree/finish-backup.js +32 -242
  320. package/src/lib/worktree/fleet-scanner.js +13 -95
  321. package/src/lib/worktree/ignored-triage.js +15 -91
  322. package/src/lib/worktree/vscode-settings.js +2 -3
  323. package/src/lib/worktree/worktree-audit.js +21 -213
  324. package/src/lib/worktree/worktree-include.js +3 -5
  325. package/src/scripts/global-install.js +3 -9
  326. package/src/scripts/install-plugin.js +1 -13
  327. package/src/scripts/setup-infra.js +17 -147
  328. package/src/utils/agents-installer.js +29 -276
  329. package/src/utils/banner.js +4 -25
  330. package/src/utils/claude-md-injector.js +1 -37
  331. package/src/utils/claude-settings-manager.js +1 -52
  332. package/src/utils/file-copier.js +11 -95
  333. package/src/utils/hooks-installer.js +21 -298
  334. package/src/utils/skills-installer.js +4 -101
  335. package/src/utils/version-checker.js +4 -83
  336. package/framework/commands/morph-proposal.md +0 -9
  337. package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
  338. package/framework/hooks/git/pre-commit/agents.sh +0 -25
  339. package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
  340. package/framework/hooks/git/pre-push/run-tests.sh +0 -44
  341. package/src/commands/utils/index.js +0 -3
  342. package/src/lib/detectors/design-system-detector.js +0 -188
  343. package/src/lib/generators/design-system-generator.js +0 -508
  344. package/src/lib/specs/old-format.js +0 -68
  345. 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,36 +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
- * A node id is what `--filter` matches and what keys `runs[]`. Lowercase and
56
- * dash-only so it survives a shell argument and a JSON key without quoting.
57
- */
58
12
  export const TEST_NODE_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
59
13
 
60
- /** Id given to the single node the fallback synthesizes — it has no name of its own. */
61
14
  const FALLBACK_NODE_ID = 'default';
62
15
 
63
16
  /**
64
- * Parse the `--filter` argument into the token list `nodeMatchesFilter` reads.
65
- *
66
- * Lives here, beside TEST_NODE_ID_PATTERN, because the thing being matched is
67
- * a node id: the spelling rules for the id and the spelling rules for what
68
- * selects it must never drift into two modules.
69
- *
70
- * An argument that yields NO tokens (`--filter ""`, `--filter " , "`) returns
71
- * an empty token list rather than null. That distinction is the point: null
72
- * means "the user did not filter", `[]` means "the user filtered and selected
73
- * nothing" — and selecting nothing is a usage error, not a full run.
74
- *
75
- * @param {string|null|undefined} raw
76
- * @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}
77
20
  */
78
21
  export function parseTestFilter(raw) {
79
22
  if (raw === undefined || raw === null || raw === false) return null;
@@ -84,64 +27,36 @@ export function parseTestFilter(raw) {
84
27
  };
85
28
  }
86
29
 
87
- /**
88
- * Does this node answer to the filter? Exact id, or the token as a substring
89
- * of the id (`--filter dot` selects `dotnet`), case-insensitively.
90
- *
91
- * @param {TestNode} node
92
- * @param {{ tokens: string[] }|null} filter - null (no filter) matches everything
93
- * @returns {boolean}
94
- */
30
+ /** Exact id or substring of the id, case-insensitive; a null filter matches everything. */
95
31
  export function nodeMatchesFilter(node, filter) {
96
32
  if (!filter) return true;
97
33
  const id = String(node?.id ?? '').toLowerCase();
98
34
  return filter.tokens.some((token) => id === token || id.includes(token));
99
35
  }
100
36
 
101
- /** 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. */
102
38
  const NO_SUITE_REASON = 'No testCommand configured and no known project file detected';
103
39
 
104
40
  /**
105
- * One declared (or synthesized) test suite.
106
- *
107
41
  * @typedef {Object} TestNode
108
- * @property {string} id - Unique within the list; what `--filter` matches.
109
- * @property {string|null} command - Literal shell command; null only on a node already decided.
110
- * @property {string} cwd - Relative to the project root; '.' by default.
111
- * @property {string} runner - One of TEST_RUNNERS; inferred from `command` when not declared.
112
- * @property {number|null} expectedCount - Recorded baseline. **null is UNKNOWN, never 0**:
113
- * a 0 here would fabricate a count drop out of "no baseline recorded yet".
114
- * @property {number|null} timeoutMs - Per-node ceiling. **null is UNKNOWN, never 0** — it
115
- * means "derive the ceiling", not "no time allowed".
116
- * @property {string[]} build - Test projects to compile first (dotnet only); `[]` is
117
- * "nothing declared", which is a real answer, not an unknown.
118
- * @property {boolean} e2e - Needs the stack up; feature scope only.
119
- * @property {string|null} linuxImage - Container image for `verify --linux`; null defers to the run's.
120
- * @property {number} projectCount - How many test projects `command` chains — the input to
121
- * the derived timeout. Always 1 for a declared node.
122
- * @property {'fail'|'skip'|null} status - Pre-decided verdict. **Non-null means DO NOT
123
- * EXECUTE**: the node is invalid config, or an explicit declaration of "no tests".
124
- * null is the normal case: no verdict yet, the run decides.
125
- * @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
126
54
  */
127
55
 
128
- /**
129
- * Infer which summary extractor can read a command's output.
130
- *
131
- * Deliberately conservative: `npm test` can front ANY runner, so it infers
132
- * `none` (unknown count) rather than guessing. An extractor applied to the
133
- * wrong output produces `null` anyway — but a WRONG guess that happens to
134
- * match would invent a number, and numbers here fail gates.
135
- *
136
- * @param {string|null|undefined} command
137
- * @returns {string} one of TEST_RUNNERS
138
- */
56
+ /** `npm test` can front any runner, so it infers `none`: a wrong guess would invent a number that fails gates. */
139
57
  export function inferRunner(command) {
140
- // `evals` is never inferred, on purpose: it is a DECLARATION, not a shape a
141
- // command line can betray. An eval command is literally `dotnet test`, so any
142
- // sniffing here would classify every dotnet suite as an eval — and, worse, the
143
- // reverse mistake (an eval declared and read as plain dotnet) would silently
144
- // 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.
145
60
  if (typeof command !== 'string' || !command.trim()) return 'none';
146
61
  if (/\bdotnet\s+test\b/.test(command)) return 'dotnet';
147
62
  if (/\bvitest\b/.test(command)) return 'vitest';
@@ -151,10 +66,6 @@ export function inferRunner(command) {
151
66
  return 'none';
152
67
  }
153
68
 
154
- /**
155
- * Build a fully-shaped node. Every field is present on every node so that no
156
- * reader has to distinguish "absent" from "unknown" by optional chaining.
157
- */
158
69
  function makeNode(partial) {
159
70
  return {
160
71
  id: partial.id,
@@ -165,21 +76,15 @@ function makeNode(partial) {
165
76
  timeoutMs: partial.timeoutMs ?? null,
166
77
  build: partial.build ?? [],
167
78
  e2e: partial.e2e ?? false,
168
- // Per-node image for `verify --linux`. **null is UNKNOWN, never ''** — it
169
- // means "use the run's image", not "use no image".
170
79
  linuxImage: partial.linuxImage ?? null,
171
80
  projectCount: partial.projectCount ?? 1,
172
- // The eval block travels on the node so `verify-runner` never has to go back
173
- // to config.json to learn where the cache lives. **null is "this is not an
174
- // eval node"** — distinct from an eval node with an empty block, which
175
- // `validateTestNodes` already refused.
81
+ // null is "not an eval node", distinct from an empty block (already refused).
176
82
  evals: partial.evals ?? null,
177
83
  status: partial.status ?? null,
178
84
  reason: partial.reason ?? null,
179
85
  };
180
86
  }
181
87
 
182
- /** A node that will not run, and says why in the vocabulary of §2.2 of the spec. */
183
88
  function invalidNode(id, problem, raw = {}) {
184
89
  return makeNode({
185
90
  id,
@@ -198,15 +103,6 @@ function isNonNegativeInteger(value) {
198
103
  return Number.isInteger(value) && value >= 0;
199
104
  }
200
105
 
201
- /**
202
- * First problem with one raw `tests[]` entry, or null when it is well formed.
203
- * First problem only: a list of complaints about a single node buries the one
204
- * that has to be fixed first.
205
- *
206
- * @param {*} raw
207
- * @param {string|null} projectPath - When given, `cwd` is also checked against the disk.
208
- * @returns {string|null}
209
- */
210
106
  function firstProblem(raw, projectPath) {
211
107
  if (!isPlainObject(raw)) return 'must be an object';
212
108
 
@@ -231,10 +127,7 @@ function firstProblem(raw, projectPath) {
231
127
  if (raw.build !== undefined && (!Array.isArray(raw.build) || raw.build.some((b) => typeof b !== 'string'))) {
232
128
  return 'build must be an array of strings';
233
129
  }
234
- // `build[]` is compiled by the dotnet prebuild and by nothing else. Declared
235
- // on a vitest node it would be silently ignored — and a declaration the
236
- // verifier ignores is the same fail-open shape as the typo in `tests[]`:
237
- // the author believes something is being compiled that never is.
130
+ // A declaration the verifier would silently ignore is refused, not dropped.
238
131
  if (Array.isArray(raw.build) && raw.build.length > 0
239
132
  && (raw.runner ?? inferRunner(raw.command)) !== 'dotnet') {
240
133
  return 'build applies only to runner "dotnet"';
@@ -245,10 +138,6 @@ function firstProblem(raw, projectPath) {
245
138
  if (raw.linuxImage !== undefined && (typeof raw.linuxImage !== 'string' || !raw.linuxImage.trim())) {
246
139
  return 'linuxImage must be a non-empty string';
247
140
  }
248
- // `runner: "evals"` REQUIRES its block; the block on any other runner is a
249
- // declaration the verifier would silently ignore — the same fail-open shape as
250
- // a typo in `tests[]`, where the author believes something is being checked
251
- // that never is.
252
141
  if ((raw.runner ?? inferRunner(raw.command)) === EVAL_RUNNER) {
253
142
  const problem = evalBlockProblem(raw, projectPath);
254
143
  if (problem) return problem;
@@ -256,19 +145,14 @@ function firstProblem(raw, projectPath) {
256
145
  return `evals applies only to runner "${EVAL_RUNNER}"`;
257
146
  }
258
147
 
259
- // The cwd checks come last because they are the expensive ones, and because
260
- // a node that is malformed in shape has a better error to report first.
261
148
  const cwd = typeof raw.cwd === 'string' ? raw.cwd.trim() : '.';
262
149
  const root = resolve(projectPath ?? '.');
263
150
  const abs = resolve(root, cwd);
264
151
  if (!isInside(abs, root)) {
265
- // A cwd that leaves the root addresses ANOTHER tree — the same damage the
266
- // worktree exclusion in collectDotnetTestProjects exists to prevent.
267
152
  return `cwd "${cwd}" resolves outside the project root`;
268
153
  }
269
154
  if (projectPath && !existsSync(abs)) {
270
- // P5: named failure, never a skip. A wrong cwd is a plan error, and a skip
271
- // would hide it behind a green gate.
155
+ // Named failure, never a skip: a skip would hide a wrong cwd behind a green gate.
272
156
  return `cwd "${cwd}" does not exist`;
273
157
  }
274
158
 
@@ -276,16 +160,9 @@ function firstProblem(raw, projectPath) {
276
160
  }
277
161
 
278
162
  /**
279
- * Validate `config.project.tests[]` IN USE — this is called by
280
- * resolveTestNodes(), not merely declared in a schema. A schema that nothing
281
- * calls is decoration; this repo already paid for fifteen guardrails that
282
- * could not detect anything (v8.9/v8.11).
283
- *
284
- * Fail-closed by design (D4): a bad entry becomes a node with
285
- * `status: 'fail'`, never a node that disappears.
286
- *
287
- * @param {object|null} config - Parsed `.morph/config/config.json`
288
- * @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.
289
166
  * @returns {TestNode[]} One node per declared entry, in declaration order
290
167
  */
291
168
  export function validateTestNodes(config, projectPath = null) {
@@ -295,9 +172,7 @@ export function validateTestNodes(config, projectPath = null) {
295
172
  return [invalidNode('project.tests', 'must be an array of test nodes')];
296
173
  }
297
174
 
298
- // Count ids BEFORE judging: two nodes sharing an id make `--filter` and
299
- // `runs[]` ambiguous, and picking a winner would silence one declared suite.
300
- // 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.
301
176
  const seen = new Map();
302
177
  for (const entry of raw) {
303
178
  if (isPlainObject(entry) && typeof entry.id === 'string') {
@@ -333,7 +208,6 @@ export function validateTestNodes(config, projectPath = null) {
333
208
  });
334
209
  }
335
210
 
336
- /** Turn a relative path into an id-safe slug matching TEST_NODE_ID_PATTERN. */
337
211
  function slugifyForId(text) {
338
212
  return text
339
213
  .toLowerCase()
@@ -342,20 +216,8 @@ function slugifyForId(text) {
342
216
  }
343
217
 
344
218
  /**
345
- * Guess which frontend test suites this project has, from `package.json`
346
- * alone — a GUESS (issue #87), never a declaration. Only called by the
347
- * fallback of `resolveTestNodes()`, and only when nothing was declared: a
348
- * `package.json` that names `vitest`/`jest` in `dependencies` or
349
- * `devDependencies` becomes one candidate suite, exactly the shape F30
350
- * exploited when a repo had .NET AND a frontend and the first-match fallback
351
- * only ever answered "dotnet test".
352
- *
353
- * Fail-open by construction (it IS a guess): an unreadable or malformed
354
- * `package.json` is skipped silently, never a `fail` node — that fail-closed
355
- * treatment is reserved for a DECLARATION (`tests[]`), which this is not.
356
- *
357
- * @param {string} projectPath - Project root
358
- * @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.
359
221
  * @returns {Array<{ id: string, cwd: string, runner: 'vitest'|'jest', command: string }>}
360
222
  */
361
223
  export function discoverFrontendPackages(projectPath, config) {
@@ -365,9 +227,6 @@ export function discoverFrontendPackages(projectPath, config) {
365
227
  const frontendPath = config?.project?.frontendPath;
366
228
  if (typeof frontendPath === 'string' && frontendPath.trim()) {
367
229
  const abs = resolve(root, frontendPath.trim());
368
- // Outside the root is IGNORED, not refused — this is a guess reusing
369
- // another guess (detectProject's own frontendPath), and neither one gets
370
- // to address another tree.
371
230
  if (isInside(abs, root)) candidates.push(frontendPath.trim());
372
231
  }
373
232
 
@@ -387,7 +246,7 @@ export function discoverFrontendPackages(projectPath, config) {
387
246
  try {
388
247
  pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
389
248
  } catch {
390
- continue; // unreadable/invalid — skipped silently, fail-open (it's a guess)
249
+ continue;
391
250
  }
392
251
 
393
252
  const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
@@ -419,40 +278,19 @@ export function discoverFrontendPackages(projectPath, config) {
419
278
  }
420
279
 
421
280
  /**
422
- * Resolve every test suite of a project, in the order the verifier must run them.
423
- *
424
- * Precedence — the whole point is that the last three steps still work with NO
425
- * config at all, because this repository (the framework source) has none and
426
- * every project installed before this feature has none either:
427
- *
428
- * 1. `project.tests[]` declared → one node per entry, source 'config-tests'
429
- * 2. `project.testCommand` declared → ONE synthesized node, source 'config'
430
- * 3. the four-step fallback of → ONE synthesized node, source
431
- * resolveTestPlan() (root .sln, 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'
432
- * *Tests.csproj scan, npm test)
433
- *
434
- * Steps 2 and 3 are `resolveTestPlan()` itself — CALLED, never re-implemented.
435
- * That matters twice over: it is the only place that knows the fallback order,
436
- * and it is the only place that excludes linked worktrees from the
437
- * `*Tests.csproj` scan (a worktree is another feature's checkout; scanning it
438
- * doubles the clock and reports on code outside the tree being judged).
439
- *
440
- * The list is never empty: "nothing to run" is expressed as ONE node already
441
- * decided as `skip`, so the reason travels to the report instead of vanishing.
442
- *
443
- * @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`.
444
284
  * @returns {{ nodes: TestNode[], source: string }}
445
285
  */
446
286
  export function resolveTestNodes(projectPath = '.') {
447
287
  const config = readMorphConfig(projectPath);
448
288
 
449
- // `tests` present in ANY shape is a declaration of intent. Even a malformed
450
- // 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.
451
290
  if (config?.project?.tests !== undefined) {
452
291
  const nodes = validateTestNodes(config, projectPath);
453
292
  if (Array.isArray(config.project.tests) && config.project.tests.length === 0) {
454
- // `[]` is an explicit "this project declares no test suites" — distinct
455
- // from an absent key, which means "guess for me".
293
+ // `[]` declares "no suites"; an absent key means "guess for me".
456
294
  return {
457
295
  nodes: [makeNode({ id: 'none', status: 'skip', reason: 'no test nodes declared' })],
458
296
  source: 'config-tests',
@@ -469,16 +307,11 @@ export function resolveTestNodes(projectPath = '.') {
469
307
  : makeNode({ id: FALLBACK_NODE_ID, status: 'skip', reason: NO_SUITE_REASON }),
470
308
  ];
471
309
 
472
- // The union (#87 PR B / F30): add discovered frontend suites BESIDE the
473
- // fallback node — but only when nothing was declared. `plan.source ===
474
- // 'config'` means `project.testCommand` IS a declaration (it lives inside
475
- // resolveTestPlan's own step 1), and a declaration wins completely: nothing
476
- // 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`).
477
312
  if (plan.source !== 'config') {
478
313
  const discovered = discoverFrontendPackages(projectPath, config)
479
- // The root package is already run by `npm test` when that is the
480
- // fallback's own answer — adding it again would run the same suite
481
- // twice under two different commands.
314
+ // The root package is already run by the fallback's `npm test`.
482
315
  .filter((pkg) => !(pkg.cwd === '.' && plan.source === 'npm'))
483
316
  .map((pkg) => makeNode({ id: pkg.id, command: pkg.command, cwd: pkg.cwd, runner: pkg.runner }));
484
317
  nodes.push(...discovered);
@@ -487,45 +320,15 @@ export function resolveTestNodes(projectPath = '.') {
487
320
  return { nodes, source: plan.source };
488
321
  }
489
322
 
490
- /**
491
- * Path of the config the baseline lives in. One spelling, so the writer and
492
- * `readMorphConfig()` can never disagree about which file is meant — and that
493
- * is now literally true: both derive from `CONFIG_FILE_NAME`
494
- * (`core/state/state-manager.js`), the same constant `state-manager.js` itself
495
- * reads the project identity through. It used to be a promise made by a JSDoc
496
- * over two hand-typed copies of the same string.
497
- */
498
323
  export function morphConfigPath(projectPath = '.') {
499
324
  return join(projectPath, CONFIG_FILE_NAME);
500
325
  }
501
326
 
502
327
  /**
503
- * Write measured `expectedCount` values back into `project.tests[]`.
504
- *
505
- * WHY THIS IS A FUNCTION AND NOT AN EDIT (D8)
506
- *
507
- * `MORPH.md` says a baseline is a COMMAND, never a number — and this feature
508
- * stores a number. The rule survives because nobody is allowed to type it: the
509
- * only writer is `verify --record-baseline`, and it writes what a real, green
510
- * run measured. A hand-typed number describes what someone believed; the
511
- * divergence between belief and the suite is exactly the failure the rule was
512
- * written after.
513
- *
514
- * The write is a POINTED mutation: read → set one field per declared node →
515
- * atomic `tmp.{pid}` + rename via `atomicWriteJson` (the same door
516
- * `state-manager.js` uses, not a second copy of it). Every other key of
517
- * `config.json` survives, because this command owns one field and nothing else.
518
- *
519
- * It refuses rather than improvises:
520
- * - a measurement whose `count` is not an integer (**null is UNKNOWN, never
521
- * 0** — a written 0 would fabricate a drop against every later run);
522
- * - a measured id that is not declared (appending a node would be inventing
523
- * the declaration the command came to update);
524
- * - a declared node with no measurement (a baseline recorded for part of a
525
- * fleet says nothing true about the rest of it);
526
- *
527
- * @param {string} projectPath - Project root
528
- * @param {Array<{id: string, count: number|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
529
332
  * @returns {{ recorded: boolean, reason: string|null, updates: Array<{id: string, from: number|null, to: number}>, path: string }}
530
333
  */
531
334
  export function recordTestBaselines(projectPath, measurements) {