@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
@@ -21,14 +21,8 @@ function gh(cwd, args, timeout = 15000) {
21
21
  return execFileSync(bin, [...prefix, ...args], { cwd, stdio: 'pipe', timeout }).toString();
22
22
  }
23
23
 
24
- /**
25
- * The workflow that can stand in for `verify {feature}`: it must call `morph-spec verify` AND
26
- * publish its JSON, because only that artifact becomes a stamp. A green run of any other
27
- * workflow (lint, deploy) proves nothing about the suite.
28
- *
29
- * @param {string} root
30
- * @returns {string|null} workflow file name, as `gh run list --workflow` takes it
31
- */
24
+ // Must call `morph-spec verify` AND publish its JSON: only that artifact becomes a stamp, and a green
25
+ // lint or deploy run proves nothing about the suite.
32
26
  export function findCiWorkflow(root) {
33
27
  const dir = join(root, CI_WORKFLOW_DIR);
34
28
  if (!existsSync(dir)) return null;
@@ -69,15 +63,7 @@ function ghAuthText(root) {
69
63
  return `${res.stdout ?? ''}${res.stderr ?? ''}`;
70
64
  }
71
65
 
72
- /**
73
- * Whether CI can answer for this feature, and why not. Never throws: every "no" is a reason the
74
- * caller prints before falling back to the local verify.
75
- *
76
- * @param {string} root - the feature's tree
77
- * @param {string} featureName
78
- * @param {object|null} config - `.morph/config/config.json`
79
- * @returns {{ ok: true, branch: string, https: boolean, scopes: string|null } | { ok: false, reason: string }}
80
- */
66
+ /** Never throws: every "no" is a reason the caller prints before falling back to the local verify. */
81
67
  export function ciAvailability(root, featureName, config) {
82
68
  if (config?.project?.ci === false) return { ok: false, reason: 'project.ci = false no config.json' };
83
69
  const url = originUrl(root);
@@ -93,21 +79,14 @@ export function ciAvailability(root, featureName, config) {
93
79
  return { ok: true, branch, https: /^https?:/i.test(url), scopes };
94
80
  }
95
81
 
96
- /**
97
- * Creating a workflow file over HTTPS needs the token's `workflow` scope; without it GitHub
98
- * rejects the whole push. Unknown scopes (fine-grained token, SSH remote) are not a refusal.
99
- */
82
+ // Creating a workflow file over HTTPS needs the token's `workflow` scope, or GitHub rejects the whole push.
83
+ // Unknown scopes (fine-grained token, SSH remote) are not a refusal.
100
84
  export function workflowPushProblem({ https, scopes }) {
101
85
  if (!https || scopes === null) return null;
102
86
  return /\bworkflow\b/.test(scopes) ? null : 'o token do gh não tem o escopo "workflow" (gh auth refresh -s workflow)';
103
87
  }
104
88
 
105
- /**
106
- * Paths CI would not see: uncommitted or untracked files outside `.morph/`, plus the config,
107
- * which names the test commands.
108
- *
109
- * @returns {string[]|null} null when git did not answer
110
- */
89
+ /** Uncommitted or untracked files outside `.morph/`, plus the config; null when git did not answer. */
111
90
  export function uncommittedCode(root) {
112
91
  let out;
113
92
  try {
@@ -138,10 +117,8 @@ export function listRuns(root, branch, workflow) {
138
117
  }
139
118
  }
140
119
 
141
- /**
142
- * The newest run over the same code as HEAD. `isCurrent` is a tree comparison, not SHA equality:
143
- * the review agents and the archive commit only `.morph/`, and the workflow does not re-run for them.
144
- */
120
+ // `isCurrent` compares trees, not SHAs: the review agents and the archive commit only `.morph/`,
121
+ // and the workflow does not re-run for them.
145
122
  export function pickRun(runs, isCurrent) {
146
123
  return (runs || []).find((run) => !(run.status === 'completed' && NO_VERDICT.has(run.conclusion))
147
124
  && run.headSha && isCurrent(run.headSha)) ?? null;
@@ -160,12 +137,7 @@ export function downloadResult(root, runId) {
160
137
  }
161
138
  }
162
139
 
163
- /**
164
- * Polls until the run over the current tree completes. Never decides: the caller turns
165
- * `timeout`/`no-run`/`unknown` into "no CI proof", never into red.
166
- *
167
- * @returns {Promise<{ state: 'completed'|'timeout'|'no-run'|'unknown', run?: object }>}
168
- */
140
+ /** Never decides: the caller turns `timeout`/`no-run`/`unknown` into "no CI proof", never into red. */
169
141
  export async function waitForRun(root, { branch, workflow, isCurrent, timeoutMs, appearMs, pollMs = 15000,
170
142
  sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now, onPending = () => {}, list = listRuns }) {
171
143
  const start = now();
@@ -2,6 +2,7 @@ import { existsSync, readdirSync } from 'fs';
2
2
  import { join, posix, relative, resolve } from 'path';
3
3
  import { PACKAGE_LOCATIONS } from '../detectors/index.js';
4
4
  import { CI_ARTIFACT, CI_RESULT_FILE } from './github-ci.js';
5
+ import { discoverDotnetSolution } from '../tasks/test-runner.js';
5
6
 
6
7
  const LOCKFILES = [
7
8
  ['package-lock.json', 'npm ci'],
@@ -50,22 +51,24 @@ function dotnetSetup(root, config) {
50
51
  ];
51
52
  }
52
53
 
54
+ // The verify build node runs `dotnet build --no-restore` (right on a warm tree): a fresh checkout restores first,
55
+ // against the same target the build node resolves.
56
+ function dotnetRestore(root, config) {
57
+ if (!usesDotnet(root, config)) return [];
58
+ const backend = config?.project?.backendPath;
59
+ if (typeof backend === 'string') return step('dotnet restore', backend, 'dotnet restore');
60
+ const { target } = discoverDotnetSolution(root);
61
+ return step('dotnet restore', '.', target && target !== '.' ? `dotnet restore "${target}"` : 'dotnet restore');
62
+ }
63
+
53
64
  const step = (name, dir, run) => [
54
65
  ` - name: ${name}`,
55
66
  ...(dir === '.' ? [] : [` working-directory: ${posix.normalize(dir)}`]),
56
67
  ` run: ${run}`,
57
68
  ];
58
69
 
59
- /**
60
- * The workflow `verify {f} --ci` relies on. Same judge as the local run: it calls the project's
61
- * pinned morph-spec, so the suite is whatever `project.tests` says, not a second definition here.
62
- * `.morph/` is out of the trigger (bookkeeping commits would cancel the run over the code) except
63
- * the config, which names the commands.
64
- *
65
- * @param {string} root
66
- * @param {object|null} config
67
- * @returns {string}
68
- */
70
+ // Calls the project's pinned morph-spec, so the suite is whatever `project.tests` says. `.morph/` is out of
71
+ // the trigger (bookkeeping commits would cancel the run over the code) except the config.
69
72
  export function renderCiWorkflow(root, config) {
70
73
  const packages = packageDirs(root, config);
71
74
  const lines = [
@@ -102,6 +105,7 @@ export function renderCiWorkflow(root, config) {
102
105
  lines.push(...step(`install (${pkg.dir})`, pkg.dir, pkg.install));
103
106
  if (pkg.playwright) lines.push(...step(`playwright browsers (${pkg.dir})`, pkg.dir, 'npx playwright install --with-deps chromium'));
104
107
  }
108
+ lines.push(...dotnetRestore(root, config));
105
109
  lines.push(
106
110
  ' - name: morph-spec verify',
107
111
  ' shell: bash',
@@ -1,24 +1,8 @@
1
- /**
2
- * @fileoverview Claude Config Detector
3
- *
4
- * Scans .claude/ settings and plugins for MCP servers, hooks, skills, and commands.
5
- * Used by mcp-setup and session-start hooks.
6
- *
7
- * @module detectors/claude-config-detector
8
- */
9
-
10
1
  import { existsSync, readFileSync, readdirSync } from 'fs';
11
2
  import { join } from 'path';
12
3
  import { homedir } from 'os';
13
4
 
14
- /**
15
- * Plugin ids (`name@marketplace`) that are switched ON. User settings first, then
16
- * the project's; a later file overrides an earlier one, and `false` turns one off.
17
- *
18
- * @param {string} targetPath - Root path of the project
19
- * @param {string} [home] - Home directory holding `.claude/settings.json`
20
- * @returns {string[]}
21
- */
5
+ // User settings first, then the project's: a later file overrides, and `false` turns a plugin off.
22
6
  export function readEnabledPlugins(targetPath, home = homedir()) {
23
7
  const merged = new Map();
24
8
  const files = [
@@ -32,17 +16,11 @@ export function readEnabledPlugins(targetPath, home = homedir()) {
32
16
  const map = JSON.parse(readFileSync(file, 'utf8').replace(/^/, '')).enabledPlugins;
33
17
  if (!map || typeof map !== 'object') continue;
34
18
  for (const [id, on] of Object.entries(map)) merged.set(id, on === true);
35
- } catch { /* invalid JSON */ }
19
+ } catch {}
36
20
  }
37
21
  return [...merged].filter(([, on]) => on).map(([id]) => id);
38
22
  }
39
23
 
40
- /**
41
- * Detect Claude Code configuration for a project.
42
- *
43
- * @param {string} targetPath - Root path of the project
44
- * @returns {{ plugins: string[], enabledPlugins: string[], mcpServers: Array<{name: string, config: Object}>, customSkills: string[], customCommands: string[], hooks: string[] }}
45
- */
46
24
  export function detectClaudeConfig(targetPath) {
47
25
  const result = {
48
26
  plugins: [],
@@ -58,7 +36,6 @@ export function detectClaudeConfig(targetPath) {
58
36
  const mcpJsonNames = new Set();
59
37
 
60
38
  try {
61
- // ── MCP Servers from .mcp.json (primary — Claude Code reads this) ───────
62
39
  const mcpJsonPath = join(targetPath, '.mcp.json');
63
40
  if (existsSync(mcpJsonPath)) {
64
41
  try {
@@ -68,10 +45,9 @@ export function detectClaudeConfig(targetPath) {
68
45
  result.mcpServers.push({ name, config });
69
46
  mcpJsonNames.add(name);
70
47
  }
71
- } catch { /* invalid JSON */ }
48
+ } catch {}
72
49
  }
73
50
 
74
- // ── MCP Servers + hooks from settings (legacy + hooks/permissions) ───────
75
51
  const settingsPaths = [
76
52
  join(targetPath, '.claude', 'settings.json'),
77
53
  join(targetPath, '.claude', 'settings.local.json'),
@@ -81,19 +57,18 @@ export function detectClaudeConfig(targetPath) {
81
57
  if (existsSync(sp)) {
82
58
  try {
83
59
  const settings = JSON.parse(readFileSync(sp, 'utf8'));
84
- // Legacy MCP entries — skip if already in .mcp.json, but allow later settings files to override earlier ones
60
+ // Legacy entries: .mcp.json wins; among settings files, the later one wins.
85
61
  const mcps = settings.mcpServers || {};
86
62
  for (const [name, config] of Object.entries(mcps)) {
87
- if (mcpJsonNames.has(name)) continue; // .mcp.json takes priority
63
+ if (mcpJsonNames.has(name)) continue;
88
64
  const idx = result.mcpServers.findIndex(m => m.name === name);
89
65
  if (idx >= 0) {
90
- result.mcpServers[idx] = { name, config }; // later file wins
66
+ result.mcpServers[idx] = { name, config };
91
67
  } else {
92
68
  result.mcpServers.push({ name, config });
93
69
  }
94
70
  }
95
71
 
96
- // Collect hooks
97
72
  if (settings.hooks) {
98
73
  for (const [event, eventHooks] of Object.entries(settings.hooks)) {
99
74
  for (const group of (Array.isArray(eventHooks) ? eventHooks : [])) {
@@ -103,60 +78,43 @@ export function detectClaudeConfig(targetPath) {
103
78
  }
104
79
  }
105
80
  }
106
- } catch { /* invalid JSON */ }
81
+ } catch {}
107
82
  }
108
83
  }
109
84
 
110
- // ── Plugins from global config ──────────────────────────────────────────
111
85
  const pluginsPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
112
86
  if (existsSync(pluginsPath)) {
113
87
  try {
114
88
  const pluginsData = JSON.parse(readFileSync(pluginsPath, 'utf8'));
115
89
  const plugins = pluginsData.plugins || {};
116
90
  result.plugins = Object.keys(plugins);
117
- } catch { /* invalid JSON */ }
91
+ } catch {}
118
92
  }
119
93
 
120
- // ── Custom skills ───────────────────────────────────────────────────────
121
94
  const skillsPath = join(targetPath, '.claude', 'skills');
122
95
  if (existsSync(skillsPath)) {
123
96
  try {
124
97
  result.customSkills = readdirSync(skillsPath).filter(f => !f.startsWith('.'));
125
- } catch { /* ignore */ }
98
+ } catch {}
126
99
  }
127
100
 
128
- // ── Custom commands ─────────────────────────────────────────────────────
129
101
  const commandsPath = join(targetPath, '.claude', 'commands');
130
102
  if (existsSync(commandsPath)) {
131
103
  try {
132
104
  result.customCommands = readdirSync(commandsPath).filter(f => f.endsWith('.md'));
133
- } catch { /* ignore */ }
105
+ } catch {}
134
106
  }
135
107
  } catch {
136
- // Fail-safe
137
108
  }
138
109
 
139
110
  return result;
140
111
  }
141
112
 
142
- /**
143
- * Cross-reference configured MCPs with the MCP registry's phase matrix.
144
- *
145
- * `phaseMatrix` and `currentPhase` default rather than being required: the
146
- * `init` caller passed only `mcpServers`, so `phaseMatrix[currentPhase]` threw
147
- * a TypeError that `init`'s outer catch swallowed as
148
- * "Claude config detection skipped" — taking `saveIntegrations` and
149
- * `generateSettings` down with it, since both run after this line.
150
- *
151
- * @param {Array<{name: string}>} mcpServers - Detected MCP server list
152
- * @param {Object} [phaseMatrix] - From framework/mcp/registry.json → phaseMatrix
153
- * @param {string} [currentPhase] - Phase to look up; omit for every phase
154
- * @returns {{ available: string[], recommended: string[], missing: string[] }}
155
- */
113
+ // `phaseMatrix`/`currentPhase` default: `init` passes only `mcpServers`, and a throw here was
114
+ // swallowed by its outer catch, skipping the steps after it.
156
115
  export function mapMcpsToPhases(mcpServers, phaseMatrix = {}, currentPhase = null) {
157
116
  const configuredNames = new Set((mcpServers || []).map(m => m.name.toLowerCase()));
158
- // No phase asked for → the union across every phase, deduped. That is what a
159
- // caller showing "which of my MCPs this framework uses" wants.
117
+ // No phase asked for → the union across every phase, deduped.
160
118
  const recommended = currentPhase
161
119
  ? (phaseMatrix[currentPhase] || [])
162
120
  : [...new Set(Object.values(phaseMatrix).flat())];
@@ -168,19 +126,6 @@ export function mapMcpsToPhases(mcpServers, phaseMatrix = {}, currentPhase = nul
168
126
  };
169
127
  }
170
128
 
171
- /**
172
- * The MCP-by-phase table `init` prints: every phase that recommends at least
173
- * one of the MCPs this machine actually has configured.
174
- *
175
- * Separate from `mapMcpsToPhases` because the caller wants a per-phase
176
- * breakdown, not one phase's slice — reaching for the wrong one is what
177
- * produced `Object.entries()` over `{available, recommended, missing}` and a
178
- * `.map(m => m.name)` over plain strings.
179
- *
180
- * @param {Array<{name: string}>} mcpServers
181
- * @param {Object} phaseMatrix
182
- * @returns {Array<{phase: string, available: string[]}>} phases with ≥1 match
183
- */
184
129
  export function mcpAvailabilityByPhase(mcpServers, phaseMatrix = {}) {
185
130
  return Object.keys(phaseMatrix)
186
131
  .map(phase => ({
@@ -190,12 +135,6 @@ export function mcpAvailabilityByPhase(mcpServers, phaseMatrix = {}) {
190
135
  .filter(({ available }) => available.length > 0);
191
136
  }
192
137
 
193
- /**
194
- * Format a human-readable one-liner summary of detected config.
195
- *
196
- * @param {{ plugins: string[], mcpServers: Array<{name: string}> }} config
197
- * @returns {string}
198
- */
199
138
  export function formatConfigSummary(config) {
200
139
  const parts = [];
201
140
  if (config.mcpServers.length > 0) {
@@ -1,12 +1,3 @@
1
- /**
2
- * @fileoverview Project Detector
3
- *
4
- * Scans a project directory for stack, architecture, UI library, and integrations.
5
- * Used by `morph-spec init`, `mcp setup` and the test-node fallback (test-nodes.js).
6
- *
7
- * @module detectors/index
8
- */
9
-
10
1
  import { existsSync, readFileSync, readdirSync } from 'fs';
11
2
  import { join } from 'path';
12
3
 
@@ -14,14 +5,6 @@ import { PACKAGE_LOCATIONS } from '../../../framework/hooks/shared/frontend-loca
14
5
 
15
6
  export { PACKAGE_LOCATIONS };
16
7
 
17
- /**
18
- * Detect project structure from filesystem evidence.
19
- *
20
- * @param {string} targetPath - Root path of the project
21
- * @param {Object} [opts] - Options
22
- * @param {boolean} [opts.generateStandards=false] - Whether to generate inferred markdown
23
- * @returns {{ structure: { stack: string, architecture: string|null, uiLibrary: string|null, integrations: string[], hasVercel: boolean }, inferred: { markdown: string } }}
24
- */
25
8
  export function detectProject(targetPath, opts = {}) {
26
9
  const structure = {
27
10
  stack: 'unknown',
@@ -32,8 +15,6 @@ export function detectProject(targetPath, opts = {}) {
32
15
  };
33
16
 
34
17
  try {
35
- // ── Package.json analysis ──────────────────────────────────────────────
36
- // Collect deps from root and common monorepo subfolders
37
18
  const allDeps = {};
38
19
  let rootPkg = null;
39
20
  let scripts = [];
@@ -45,17 +26,15 @@ export function detectProject(targetPath, opts = {}) {
45
26
  if (loc === '.') rootPkg = pkg;
46
27
  Object.assign(allDeps, pkg.dependencies, pkg.devDependencies);
47
28
  scripts.push(...Object.keys(pkg.scripts || {}));
48
- // Track frontend path for config
49
29
  if (loc !== '.' && (pkg.dependencies?.next || pkg.dependencies?.react || pkg.dependencies?.vue)) {
50
30
  structure.frontendPath = loc;
51
31
  }
52
- } catch { /* invalid JSON */ }
32
+ } catch {}
53
33
  }
54
34
  const depNames = Object.keys(allDeps);
55
35
 
56
36
  if (depNames.length > 0) {
57
37
 
58
- // Stack detection from deps
59
38
  if (depNames.includes('next')) {
60
39
  structure.stack = 'nextjs';
61
40
  } else if (depNames.includes('react')) {
@@ -68,12 +47,10 @@ export function detectProject(targetPath, opts = {}) {
68
47
  structure.stack = 'nodejs';
69
48
  }
70
49
 
71
- // UI library detection
72
50
  if (existsSync(join(targetPath, 'components.json')) || existsSync(join(targetPath, structure.frontendPath || '.', 'components.json')) || depNames.includes('@shadcn/ui')) {
73
51
  structure.uiLibrary = 'shadcn';
74
52
  }
75
53
 
76
- // Integrations
77
54
  if (depNames.includes('@neondatabase/serverless') || depNames.includes('postgres') || depNames.includes('pg')) {
78
55
  structure.integrations.push('neon');
79
56
  }
@@ -84,13 +61,11 @@ export function detectProject(targetPath, opts = {}) {
84
61
  structure.integrations.push('stripe');
85
62
  }
86
63
 
87
- // Vercel detection
88
64
  if (depNames.includes('vercel') || scripts.includes('vercel')) {
89
65
  structure.hasVercel = true;
90
66
  }
91
67
  }
92
68
 
93
- // ── .csproj detection ──────────────────────────────────────────────────
94
69
  let hasDotnet = false;
95
70
  const csprojLocations = ['.', 'src', 'src/Api', 'src/Web', 'src/backend'];
96
71
 
@@ -105,7 +80,7 @@ export function detectProject(targetPath, opts = {}) {
105
80
  if (content.includes('Npgsql')) {
106
81
  if (!structure.integrations.includes('neon')) structure.integrations.push('neon');
107
82
  }
108
- } catch { /* ignore */ }
83
+ } catch {}
109
84
  }
110
85
 
111
86
  for (const loc of csprojLocations) {
@@ -113,41 +88,34 @@ export function detectProject(targetPath, opts = {}) {
113
88
  const dir = join(targetPath, loc);
114
89
  if (!existsSync(dir)) continue;
115
90
  const entries = readdirSyncSafe(dir);
116
- // Direct .csproj files
117
91
  for (const e of entries.filter(f => f.endsWith('.csproj'))) {
118
92
  checkCsproj(join(dir, e));
119
93
  }
120
- // One level deeper (e.g., src/backend/MyProject/*.csproj)
121
94
  for (const sub of entries) {
122
95
  const subDir = join(dir, sub);
123
96
  for (const e of readdirSyncSafe(subDir).filter(f => f.endsWith('.csproj'))) {
124
97
  checkCsproj(join(subDir, e));
125
98
  }
126
99
  }
127
- } catch { /* ignore */ }
100
+ } catch {}
128
101
  }
129
102
 
130
- // Determine stack: monorepo (nextjs+dotnet), dotnet, or keep frontend stack
131
103
  if (hasDotnet) {
132
104
  if (structure.stack === 'nextjs' || structure.stack === 'react' || structure.stack === 'vue') {
133
- // Monorepo: frontend + .NET backend
134
105
  structure.stack = `${structure.stack}+dotnet`;
135
106
  } else if (structure.stack === 'unknown') {
136
107
  structure.stack = 'dotnet';
137
108
  }
138
109
  }
139
110
 
140
- // ── vercel.json detection ──────────────────────────────────────────────
141
111
  if (existsSync(join(targetPath, 'vercel.json'))) {
142
112
  structure.hasVercel = true;
143
113
  }
144
114
 
145
- // ── tsconfig.json detection (fallback for stack) ────────────────────────
146
115
  if (structure.stack === 'unknown' && existsSync(join(targetPath, 'tsconfig.json'))) {
147
116
  structure.stack = 'nodejs';
148
117
  }
149
118
 
150
- // ── Architecture detection ─────────────────────────────────────────────
151
119
  try {
152
120
  const configPath = join(targetPath, '.morph/config/config.json');
153
121
  if (existsSync(configPath)) {
@@ -156,16 +124,14 @@ export function detectProject(targetPath, opts = {}) {
156
124
  structure.architecture = config.architecture.style;
157
125
  }
158
126
  }
159
- } catch { /* ignore */ }
127
+ } catch {}
160
128
 
161
- // ── Docker detection ───────────────────────────────────────────────────
162
129
  if (existsSync(join(targetPath, 'docker-compose.yml')) ||
163
130
  existsSync(join(targetPath, 'docker-compose.yaml')) ||
164
131
  existsSync(join(targetPath, 'Dockerfile'))) {
165
132
  if (!structure.integrations.includes('docker')) structure.integrations.push('docker');
166
133
  }
167
134
 
168
- // ── Azure detection ────────────────────────────────────────────────────
169
135
  const azureSignals = [
170
136
  '.azure',
171
137
  'azure-pipelines.yml',
@@ -176,7 +142,6 @@ export function detectProject(targetPath, opts = {}) {
176
142
  const p = join(targetPath, signal);
177
143
  if (!existsSync(p)) return false;
178
144
  if (signal === 'infra') {
179
- // Check for .bicep files inside infra/
180
145
  return readdirSyncSafe(p).some(f => f.endsWith('.bicep'));
181
146
  }
182
147
  return true;
@@ -185,7 +150,6 @@ export function detectProject(targetPath, opts = {}) {
185
150
  structure.integrations.push('azure');
186
151
  }
187
152
  } catch {
188
- // Fail-safe — return defaults
189
153
  }
190
154
 
191
155
  return {
@@ -196,11 +160,6 @@ export function detectProject(targetPath, opts = {}) {
196
160
  };
197
161
  }
198
162
 
199
- /**
200
- * Safe readdir that returns [] on error.
201
- * @param {string} dir
202
- * @returns {string[]}
203
- */
204
163
  function readdirSyncSafe(dir) {
205
164
  try {
206
165
  return readdirSync(dir);
@@ -209,11 +168,6 @@ function readdirSyncSafe(dir) {
209
168
  }
210
169
  }
211
170
 
212
- /**
213
- * Generate a markdown summary from detected structure.
214
- * @param {Object} structure
215
- * @returns {string}
216
- */
217
171
  function generateInferredMarkdown(structure) {
218
172
  const lines = [`## Detected Stack: ${structure.stack}`];
219
173
  if (structure.architecture) lines.push(`Architecture: ${structure.architecture}`);
@@ -0,0 +1,37 @@
1
+ import { basename } from 'path';
2
+ import { readPlanChecks } from '../plan/checks.js';
3
+ import { changedPathsAgainst, detectDefaultBranch } from '../git/default-branch.js';
4
+ import { resolveComposeFile } from '../../../framework/hooks/shared/e2e-check.js';
5
+ import { E2E_SPEC_LOCATIONS } from '../../../framework/hooks/shared/frontend-locations.js';
6
+
7
+ export const E2E_NOT_APPLICABLE = 'not applicable: no `prova: e2e` scenario and the diff touches no UI, e2e spec or container file';
8
+
9
+ const UI_FILE = /\.(tsx|jsx|vue|svelte|astro|css|scss|sass|less|html)$/i;
10
+ const RUNTIME_FILE = /(^|\/)(dockerfile[^/]*|\.dockerignore|(docker-)?compose[^/]*\.ya?ml|playwright\.config\.[cm]?[jt]s)$/i;
11
+ const E2E_DIR = /(^|\/)e2e\//i;
12
+ const KNOWN_PACKAGE_DIRS = E2E_SPEC_LOCATIONS.filter((dir) => dir !== '.');
13
+
14
+ function packageDirsOf(config) {
15
+ const declared = [config?.project?.frontendPath, ...(config?.project?.tests ?? []).filter((n) => n?.runner !== 'dotnet').map((n) => n?.cwd)];
16
+ const extra = declared.filter((d) => typeof d === 'string').map((d) => d.replace(/\\/g, '/').replace(/^\.\/|\/$/g, '')).filter((d) => d && d !== '.');
17
+ return [...KNOWN_PACKAGE_DIRS, ...extra];
18
+ }
19
+
20
+ const touchesRuntime = (file, composeFile, packageDirs) => UI_FILE.test(file)
21
+ || RUNTIME_FILE.test(file)
22
+ || E2E_DIR.test(file)
23
+ || (composeFile && basename(file) === basename(composeFile))
24
+ || packageDirs.some((dir) => file === dir || file.startsWith(`${dir}/`));
25
+
26
+ /** Unknown (no git, no base, empty diff) is applicable: the e2e only stands down on evidence. */
27
+ export function e2eApplicability(projectPath, featureName, config) {
28
+ const { checks } = readPlanChecks(projectPath, featureName);
29
+ if (checks?.checks?.some((c) => c.proof === 'e2e')) return { applicable: true, reason: 'prova: e2e' };
30
+ const base = detectDefaultBranch(projectPath);
31
+ const changed = base ? changedPathsAgainst(base, projectPath).filter((f) => !f.startsWith('.morph/')) : [];
32
+ if (changed.length === 0) return { applicable: true, reason: 'diff unknown' };
33
+ const composeFile = resolveComposeFile(projectPath, config);
34
+ const packageDirs = packageDirsOf(config);
35
+ const hit = changed.find((f) => touchesRuntime(f, composeFile, packageDirs));
36
+ return hit ? { applicable: true, reason: hit } : { applicable: false, reason: E2E_NOT_APPLICABLE };
37
+ }
@@ -1,22 +1,5 @@
1
- /**
2
- * Compose environment for the e2e stack — the port block a feature owns, plus
3
- * the credentials its auth setup needs.
4
- *
5
- * This lives in ONE place because it has two callers that must never disagree:
6
- * `morph-spec e2e up` (manual) and the `e2e` node inside `morph-spec verify`
7
- * (the one that actually runs in the loop). When only the manual path built the
8
- * port block, the isolation was real in the command nobody runs and absent in
9
- * the one everybody runs — two worktrees would get distinct container names via
10
- * COMPOSE_PROJECT_NAME and then collide on host port 3000 anyway, surfacing as
11
- * "port is already allocated" and pausing a gate over an infrastructure clash
12
- * rather than a defect.
13
- *
14
- * It sits in src/lib rather than framework/hooks/shared because it depends on
15
- * env-allocator.js: hooks are installed standalone into target projects, where
16
- * `src/` does not exist, so the dependency may only point this way.
17
- *
18
- * @module lib/e2e/compose-env
19
- */
1
+ // One place for `e2e up` and the `e2e` node of `verify`, which must never disagree on ports. Lives in
2
+ // src/lib, not hooks/shared, because it depends on env-allocator.js and hooks run without `src/`.
20
3
 
21
4
  import { join } from 'path';
22
5
 
@@ -25,43 +8,11 @@ import chalk from 'chalk';
25
8
  import { loadEnvFile } from '../../../framework/hooks/shared/e2e-check.js';
26
9
  import { allocateEnvBlock, portLayout, resolveFleetConfig, resolvePortAliases } from '../worktree/env-allocator.js';
27
10
 
28
- /**
29
- * Build the environment a `docker compose` invocation should carry.
30
- *
31
- * Port variables come from `portLayout()` in env-allocator.js — the SAME
32
- * function that renders `.morph/worktree.env` — so a compose file and the
33
- * worktree manifest describe the same world by construction (`PORT`,
34
- * `API_PORT`/`ASPNETCORE_URLS`, `POSTGRES_PORT`, `COMPOSE_PROJECT_NAME`).
35
- *
36
- * `PLAYWRIGHT_BASE_URL` is derived here rather than left to the spec config —
37
- * the suite has to be told where the app actually landed, otherwise a feature
38
- * on port 4020 gets tested against a hardcoded localhost:3000 and fails with
39
- * ECONNREFUSED that looks exactly like a broken app.
40
- *
41
- * The credentials env file loads AFTER the layout but cannot outrank it: a
42
- * `.env.e2e` copied from a sibling worktree carries THAT worktree's block, and
43
- * letting it win points this stack at the neighbour's postgres while every
44
- * other surface still reports the allocated port. The protected keys are the
45
- * ones actually assigned above — never a second literal list, which is exactly
46
- * how an offset lands in one writer and not the other. A key that gets ignored
47
- * is announced on stderr (never stdout: `morph-spec env` writes shell-eval
48
- * output there), because swapping a silent override for a silent discard just
49
- * moves the debugging session.
50
- *
51
- * Fail-open throughout: with no git dir (or a corrupt registry) the caller gets
52
- * default ports, which only matters when two stacks run at once. With no layout
53
- * applied nothing is filtered — there the env file is the only port source.
54
- *
55
- * @param {object} params
56
- * @param {string} params.cwd - project root
57
- * @param {object|null} [params.config] - .morph/config/config.json contents
58
- * @param {string} params.featureName
59
- * @param {string|null} [params.gitCommonDir]
60
- * @returns {Record<string,string>}
61
- */
11
+ // Ports come from `portLayout()`, the same function that renders `.morph/worktree.env`. The credentials
12
+ // file loads after the layout but cannot outrank it: a `.env.e2e` copied from a sibling worktree carries
13
+ // that worktree's block. Ignored keys are announced on stderr; stdout carries `morph-spec env` output.
62
14
  export function buildComposeEnv({ cwd, config, featureName, gitCommonDir }) {
63
15
  const env = {};
64
- /** @type {Set<string>|null} - null while no layout has been applied */
65
16
  let layoutKeys = null;
66
17
 
67
18
  if (gitCommonDir) {
@@ -97,18 +48,6 @@ export function buildComposeEnv({ cwd, config, featureName, gitCommonDir }) {
97
48
  return env;
98
49
  }
99
50
 
100
- /**
101
- * Tell the caller — `e2e up`, the `e2e` node of `verify`, `stack-teardown` and
102
- * `morph-spec env` all reach this through `buildComposeEnv` — which keys of the
103
- * credentials file were dropped and what won instead.
104
- *
105
- * stderr, always: `morph-spec env` prints `export K=v` on stdout for a
106
- * shell to eval, and `verify` prints JSON there.
107
- *
108
- * @param {string} envFile
109
- * @param {string[]} ignored
110
- * @param {Record<string,string>} env - the layout that won
111
- */
112
51
  function reportIgnoredLayoutKeys(envFile, ignored, env) {
113
52
  const shown = ignored.map((key) => `${key}=${env[key]}`).join(', ');
114
53
  console.warn(chalk.yellow(