@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,67 +1,13 @@
1
- /**
2
- * templates-reconcile.js — reconcile a pre-existing `.morph/framework/templates/`
3
- * copy against the project's prior recorded morph-spec version.
4
- *
5
- * Shared by BOTH install paths that can encounter this directory:
6
- * - `morph-spec update` (src/commands/project/update.js) — always knows the
7
- * project's prior version (`checkProjectOutdated().current`, read from
8
- * `.morph/.morphversion` BEFORE this run overwrites it).
9
- * - `morph-spec init`/`setup-infra` (src/scripts/setup-infra.js) — the SAME
10
- * source (`getProjectMorphVersion()`, the function `checkProjectOutdated`
11
- * itself calls), read at the same point in the run (before its own write
12
- * of `.morph/.morphversion`), when it's available — e.g. a repair run
13
- * over an already-initialized project. Falls back to a plain warning when
14
- * it isn't (a project with no recorded version has nothing here for this
15
- * function to reconcile against safely).
16
- */
17
-
18
1
  import { existsSync } from 'fs';
19
2
  import { compareVersions } from '../../utils/version-checker.js';
20
3
  import { realEffects } from './effects.js';
21
4
 
22
- /**
23
- * Last CLI version whose `update`/`setup-infra` still wrote
24
- * `.morph/framework/templates/` unconditionally, before Task 2 of the R3
25
- * perf plan removed that copy. Used as the decision boundary in
26
- * `reconcileLegacyTemplatesCopy` below — see its doc comment for why a
27
- * version check is used instead of diffing file content.
28
- */
5
+ // Last CLI version whose install still copied `.morph/framework/templates/` unconditionally.
29
6
  export const LAST_VERSION_THAT_COPIED_TEMPLATES = '8.17.0';
30
7
 
31
- /**
32
- * Reconcile a pre-existing `.morph/framework/templates/` copy now that
33
- * neither install path unconditionally writes it anymore (Task 2, R3 perf
34
- * plan — resolution falls back to the npm package's own framework/templates/,
35
- * see template-registry.js / architecture-scanner.js's layered resolver).
36
- *
37
- * Decided from the project's PRIOR recorded CLI version (before THIS run
38
- * writes a new one), not by diffing content against the package. A content
39
- * diff can't tell "stale copy from an older CLI version" apart from
40
- * "deliberate user override" — and staleness is the dominant case for
41
- * anyone who actually runs `update`/`setup-infra` again, since template
42
- * content does change between releases. The version boundary is unambiguous
43
- * instead:
44
- *
45
- * - No local copy → nothing to do.
46
- * - Prior version is unrecorded, OR <= LAST_VERSION_THAT_COPIED_TEMPLATES
47
- * → the copy could ONLY have been put there by the OLD installer:
48
- * `.morph/framework/**` is Edit-denied to the agent (MORPH_DENY in
49
- * hooks-installer.js — "installer-managed, immutable at runtime"),
50
- * the directory is gitignored, and every install/update before this fix
51
- * unconditionally wiped + recopied it. There is no path by which a
52
- * project on an old version could have a genuine override sitting
53
- * here. Safe to delete unconditionally — it is dead weight, not data.
54
- * - Prior version > LAST_VERSION_THAT_COPIED_TEMPLATES → this install path
55
- * has never written this directory for this project. Anything found
56
- * here was placed deliberately (by the user or other tooling) — this is
57
- * the forward-looking override use case the layered resolver exists
58
- * for. Preserve untouched, unconditionally, with no diffing.
59
- *
60
- * @param {string} templatesDest - .morph/framework/templates (may not exist)
61
- * @param {string|null} priorProjectVersion - project's recorded morph-spec
62
- * version BEFORE this run (e.g. `checkProjectOutdated().current`)
63
- * @returns {'absent'|'removed-legacy'|'preserved-override'}
64
- */
8
+ // Decided by the PRIOR recorded version, not by diffing: a diff can't tell a stale copy from an override.
9
+ // Unrecorded or <= the boundary → only the old installer could have written it, delete.
10
+ // Newer → placed deliberately, preserve.
65
11
  export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion, fx = realEffects()) {
66
12
  if (!existsSync(templatesDest)) return 'absent';
67
13
 
@@ -74,8 +20,7 @@ export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion,
74
20
  fx.remove(templatesDest);
75
21
  return 'removed-legacy';
76
22
  } catch {
77
- // Couldn't remove — don't risk a half-broken state; leave it and let
78
- // the layered resolver's override-first check pick it up as before.
23
+ // Couldn't remove: leave it; the resolver's override-first check picks it up.
79
24
  return 'preserved-override';
80
25
  }
81
26
  }
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { dotnetBuildCommands, withFilter, withTrx, withoutBuild } from './commands.js';
5
- import { methodOf, parseTrx } from './trx.js';
5
+ import { parseTrx } from './trx.js';
6
6
  import { stripAnsi } from '../tasks/test-runner.js';
7
7
 
8
8
  const MAX_CAPTURE = 16 * 1024 * 1024;
@@ -39,14 +39,10 @@ export function dotnetAdapter({ node, root, workDir, exec, subset, timeoutMs })
39
39
  const trxDir = join(workDir, 'trx');
40
40
  mkdirSync(trxDir, { recursive: true });
41
41
  const base = withoutBuild(node.command);
42
- const filterOf = { subset: () => subset.filter, names: (filter) => filter };
43
- const commandFor = (scope, label, filter) => withTrx(
44
- scope === 'full' ? base : withFilter(base, filterOf[scope](filter)),
45
- { dir: trxDir, name: `${label}.trx` },
46
- );
47
- const runTests = async ({ scope, label, env, filter }) => {
42
+ const commandFor = (label) => withTrx(withFilter(base, subset.filter), { dir: trxDir, name: `${label}.trx` });
43
+ const runTests = async ({ label, env }) => {
48
44
  const trxPath = join(trxDir, `${label}.trx`);
49
- const r = await exec(commandFor(scope, label, filter), { cwd, env, timeoutMs, label });
45
+ const r = await exec(commandFor(label), { cwd, env, timeoutMs, label });
50
46
  if (!existsSync(trxPath)) {
51
47
  return { outcome: 'crash', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs, note: r.timedOut ? `timeout de ${timeoutMs} ms` : `sem TRX (exit ${r.status})` };
52
48
  }
@@ -62,12 +58,6 @@ export function dotnetAdapter({ node, root, workDir, exec, subset, timeoutMs })
62
58
  buildCommands: dotnetBuildCommands(node, root),
63
59
  cwd,
64
60
  runTests,
65
- reproduce: ({ names, label, env }) => runTests({
66
- scope: 'names',
67
- label,
68
- env,
69
- filter: [...new Set(names.map(methodOf))].map((n) => `FullyQualifiedName~${n}`).join('|'),
70
- }),
71
61
  };
72
62
  }
73
63
 
@@ -78,9 +68,8 @@ export function genericAdapter({ node, root, exec, subset, timeoutMs }) {
78
68
  hasSubset: Boolean(subset?.usable),
79
69
  buildCommands: [],
80
70
  cwd,
81
- async runTests({ scope, label, env }) {
82
- const command = scope === 'subset' ? subset.command : node.command;
83
- const r = await exec(command, { cwd, env, timeoutMs, label });
71
+ async runTests({ label, env }) {
72
+ const r = await exec(subset.command, { cwd, env, timeoutMs, label });
84
73
  if (r.timedOut) return { outcome: 'crash', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs, note: `timeout de ${timeoutMs} ms` };
85
74
  if (r.status === 0) return { outcome: 'green', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs };
86
75
  const failedTests = genericFailures(`${r.stdout}\n${r.stderr}`);
@@ -1,10 +1,3 @@
1
- /**
2
- * .NET introspection the targeted mutation (`morph-spec mutate`) needs: which
3
- * project owns a file, which test mode the SDK picks, and which types a C# file
4
- * declares — read FAIL-SAFE (see csharpClasses).
5
- *
6
- * Pure: reads files, never spawns a process.
7
- */
8
1
  import { existsSync, readFileSync, readdirSync } from 'fs';
9
2
  import { dirname, join, resolve } from 'path';
10
3
 
@@ -13,54 +6,21 @@ import { jsoncToJson } from '../../../framework/hooks/shared/comment-stripper.js
13
6
 
14
7
  const MTP_RUNNER = 'Microsoft.Testing.Platform';
15
8
  const TYPE_HEADER = /\b(?:class|struct|record|interface)\b/;
16
- /** A C# identifier's first character: a letter, an Nl letter (`Ⅱ`), `_`, `@` (verbatim) or `\` (a unicode escape). */
9
+ // `@` = verbatim identifier, `\` = unicode escape.
17
10
  const NAME_START = '[\\p{L}\\p{Nl}_@\\\\]';
18
- /**
19
- * TRIPWIRE: every type-declaration keyword wherever it sits — applied to the RAW
20
- * text (nothing blanked) and to the blanked code alike, with NO context rule:
21
- * round 5 showed that any exclusion decided by what precedes the keyword (the
22
- * `:` of a generic constraint, a `where T :` shape) can be forged by a comment in
23
- * the raw text or by string content a cut exposes in the code. `record class`/
24
- * `record struct` count once. What follows the keyword, after optional whitespace,
25
- * is a name start (NAME_START), a comment or preprocessor line standing between
26
- * the keyword and its name (`class/**\/BTests` is legal), OR a quote — a quote is
27
- * what the RAW text shows where the blanked code shows the spaces of a string the
28
- * keyword is followed by (`"; class " : " Fake"` exposed as code): every keyword
29
- * the code counts is a keyword the raw text counts, by offset.
30
- * A constraint keyword is therefore counted too whenever a name-shaped token
31
- * follows it (`where T : class where U : …`, `: class // note`): it is not named,
32
- * so the file is unscannable. `class,` / `class\n{` / `class?` are not counted at all.
33
- */
11
+ // TRIPWIRE with NO context rule: any exclusion based on what precedes the keyword can be
12
+ // forged by a comment or exposed string. The quote in the lookahead makes every keyword
13
+ // the blanked code counts also counted in the raw text, at the same offset.
34
14
  const TYPE_KEYWORD = new RegExp(`\\b(?:record\\s+(?:class|struct)|class|record|struct|interface)\\b(?=\\s*(?:${NAME_START}|["']|\\/[/*]|#))`, 'gu');
35
- /** A type declaration the scanner can NAME: after `; { } ]`, a newline or the file start; attributes; modifiers; a NAME_START name. */
36
15
  const TYPE_DECL = new RegExp(`(?:^|[;{}\\]\\n])[ \\t]*(?:\\[[^\\]\\n]*\\][ \\t]*)*(?:(?:public|internal|private|protected|sealed|static|partial|abstract|unsafe|file|new|readonly|ref)\\s+)*(record\\s+(?:class|struct)|class|record|struct|interface)\\s+(${NAME_START}[^\\s:<({;,=]*)`, 'gu');
37
16
  const IDENT = '[\\p{L}_][\\p{L}\\p{N}_]*';
38
17
  const QUALIFIER = `(?:${IDENT}\\s*(?:\\.|::)\\s*)*`;
39
- /** An attribute's optional arguments: one nested level of parentheses, no `]`. */
40
18
  const ATTRIBUTE_ARGS = '(?:\\((?:[^()\\]]|\\([^()\\]]*\\))*\\))?';
41
- /** Any other attribute of the same section: a name, optionally with arguments. */
42
19
  const OTHER_ATTRIBUTE = `${QUALIFIER}${IDENT}\\s*${ATTRIBUTE_ARGS}`;
43
20
  const TEST_ATTRIBUTE_NAME = '(?:Fact|Theory|Test|TestMethod|DataTestMethod|TestCase|TestCaseSource)(?:Attribute)?';
44
- /**
45
- * A test attribute, read on the BLANKED code of a test-project file (hasTestAttribute):
46
- * a WHOLE name — `Fact`, `Theory`, `Test`, `TestMethod`, `DataTestMethod`, `TestCase`,
47
- * `TestCaseSource`, optionally `…Attribute` and qualified (`Xunit.Fact`) — inside an
48
- * attribute section that CLOSES as one: `[` where a declaration may start (line start,
49
- * after `;` `{` `}` `]` or a `*\/`), an optional target, other attributes before it
50
- * separated by `,`, the test name with its optional arguments, any further attributes,
51
- * then the `]` — followed by a declaration start (`[`, a name, a comment, a directive)
52
- * or the end of the input.
53
- *
54
- * A false POSITIVE is the unsafe direction here: a helper taken for a test file
55
- * narrows the subset to its own name, and the tests that USE the helper never run. The final review of branch B found
56
- * exactly that with the old prefix match (`, TestContext` / `, Factory` / `, TheoryData`
57
- * after any `,`), and its re-review (N1) with a test name merely FOLLOWED by `(` or `,`:
58
- * a C# 12 collection laid out with `[` alone on its line (`[\n Env.Dev,\n Env.Test,\n];`,
59
- * a row `[Env.Test, Env.Dev],`, `[\n Test("a"),\n];`). Requiring the section to close
60
- * rejects all three; what still matches is stated in csharpClasses. A false NEGATIVE
61
- * only leaves a test file out of a selection (a custom attribute such as `[SkippableFact]`, a test
62
- * attribute after an argument holding `]`).
63
- */
21
+ // A false POSITIVE is the unsafe side (a helper taken for a test file hides the tests that
22
+ // use it), so the whole name must sit in an attribute section that CLOSES: a C# 12
23
+ // collection such as `[\n Env.Test,\n];` must not match. Residual cases: csharpClasses.
64
24
  export const CS_TEST_ATTRIBUTE = new RegExp(
65
25
  '(?:^|[;{}\\]]|\\*\\/)[ \\t]*\\['
66
26
  + `\\s*(?:${IDENT}\\s*:(?!:)\\s*)?`
@@ -71,22 +31,12 @@ export const CS_TEST_ATTRIBUTE = new RegExp(
71
31
  'mu',
72
32
  );
73
33
 
74
- /**
75
- * File text with a leading UTF-8 BOM stripped (VS and `charset=utf-8-bom` write
76
- * one) and every C# new-line character mapped 1:1 to `\n` — CR, NEL (U+0085),
77
- * LS (U+2028), PS (U+2029): JS `\s` excludes NEL, so `class<NEL>Name` was
78
- * invisible to both regexes (compiled and discovered by xunit); a CR-only file had
79
- * no line ends at all. U+001A (Ctrl-Z) is mapped 1:1 to a space: Roslyn reads it
80
- * as whitespace and JS `\s` does not, so `class<SUB>Name` hid a declaration the
81
- * same way (B3 carry-over: compiled, green while skipping the class). Length is kept (CRLF becomes `\n\n`). null when unreadable.
82
- */
34
+ // Roslyn reads CR/NEL/LS/PS as new-lines and U+001A as whitespace; JS `\s` misses NEL
35
+ // and U+001A, which hid `class<NEL>Name`. Mapped 1:1 so offsets stay aligned with the code.
83
36
  const readText = (path) => {
84
37
  try { return readFileSync(path, 'utf8').replace(/\x1A/g, ' ').replace(/^\uFEFF/, '').replace(/[\r\u0085\u2028\u2029]/g, '\n'); } catch { return null; }
85
38
  };
86
39
 
87
- /** Which test framework a csproj references, by package (or MSBuild SDK) name. */
88
-
89
- /** Nearest file matching `projectName` walking up from a file's directory, inside the root; null when none. */
90
40
  function nearestProjectFile(fileAbs, rootAbs, projectName) {
91
41
  for (let dir = dirname(fileAbs); isInside(dir, rootAbs); dir = dirname(dir)) {
92
42
  let names = [];
@@ -97,32 +47,20 @@ function nearestProjectFile(fileAbs, rootAbs, projectName) {
97
47
  return null;
98
48
  }
99
49
 
100
- /**
101
- * The project — `.csproj`, `.fsproj` or `.vbproj` — nearest to ANY file walking
102
- * up, or null inside the root. Asked about the .NET sources the C# scan cannot
103
- * read (a bUnit `.razor`, an F# `.fs`).
104
- */
105
50
  export function nearestProjectOf(fileAbs, rootAbs) {
106
51
  return nearestProjectFile(fileAbs, rootAbs, /\.(?:cs|fs|vb)proj$/i);
107
52
  }
108
53
 
109
- /** .NET sources the C# scan cannot read: Razor (`.razor`, `.cshtml`), F# (`.fs`, `.fsi`, `.fsx`), VB (`.vb`). */
110
54
  const NON_CSHARP_SOURCE = /\.(?:razor|cshtml|fs|fsi|fsx|vb)$/i;
111
55
 
112
- /** A .NET source other than C#: no type scan, hence no class selector, for it. */
56
+ /** No type scan, hence no class selector, for these. */
113
57
  export const isNonCSharpSource = (path) => NON_CSHARP_SOURCE.test(String(path));
114
58
 
115
- /** Any .NET source a project compiles — C# or one of the others. */
116
59
  export const isDotnetSource = (path) => /\.cs$/i.test(String(path)) || isNonCSharpSource(path);
117
60
 
118
61
  /**
119
- * `.NET` test mode for a cwd: nearest `global.json` walking UP (as the SDK
120
- * resolves it — from the CWD, never from the `.sln` argument), read as JSONC.
121
- *
122
- * @param {string} absCwd
123
- * @returns {'mtp'|'vstest'|null} `mtp` when `test.runner` is Microsoft.Testing.Platform;
124
- * `vstest` when readable without it (or absent: the SDK default); null when a
125
- * global.json exists but cannot be read — never a guess.
62
+ * Nearest global.json walking up from the CWD, as the SDK resolves it (never from the
63
+ * `.sln` argument). null when one exists but cannot be read — never a guess.
126
64
  */
127
65
  export function dotnetTestMode(absCwd) {
128
66
  for (let dir = resolve(absCwd); ; dir = dirname(dir)) {
@@ -141,7 +79,6 @@ export function dotnetTestMode(absCwd) {
141
79
 
142
80
  const lineEnd = (text, i) => { const e = text.indexOf('\n', i); return e < 0 ? text.length : e; };
143
81
 
144
- /** End (exclusive) of a regular string or char literal opened at `start`; -1 if the line ends first. */
145
82
  function closeOnLine(text, start, quote) {
146
83
  for (let j = start + 1; j < text.length && text[j] !== '\n'; j++) {
147
84
  if (text[j] === '\\') j++;
@@ -150,11 +87,8 @@ function closeOnLine(text, start, quote) {
150
87
  return -1;
151
88
  }
152
89
 
153
- /**
154
- * Does an interpolated literal's content end inside a hole? A `"` inside a hole
155
- * (`$"{d["k"]}"`) ends the literal as the scanner reads it, so what it took for
156
- * the literal stops with the hole still open. `{{`/`}}` outside a hole are escapes.
157
- */
90
+ // A `"` inside a hole (`$"{d["k"]}"`) ends the literal as the scanner reads it,
91
+ // leaving the hole open. `{{`/`}}` outside a hole are escapes.
158
92
  function holeLeftOpen(content) {
159
93
  let depth = 0;
160
94
  for (let k = 0; k < content.length; k++) {
@@ -167,26 +101,9 @@ function holeLeftOpen(content) {
167
101
  }
168
102
 
169
103
  /**
170
- * C# source with every comment, preprocessor line and string/char literal
171
- * blanked — newlines and length kept — or null when the file cannot be read this way:
172
- * - a literal or comment never ends;
173
- * - an interpolated `$"…"`/`$@"…"`/`@$"…"` literal is cut by a quote inside a hole
174
- * (its content ends with the hole open). Blanking the part before the cut would
175
- * leave the rest of the hole as "code", with braces that can cancel out.
176
- *
177
- * Raw strings (`"""…"""`, 3+ quotes, with or without `$`/`$$`) end at the same run
178
- * of quotes; a quote inside a raw interpolation hole is NOT checked. Verbatim `@"…"`
179
- * ends at a lone `"`. Regular and char literals cannot cross a line.
180
- *
181
- * This is a heuristic lexer and it CAN misread (known: `holeLeftOpen` counts the
182
- * braces of a char literal such as `'}'`, so `$"{(c == '}' ? "a" : "/*")}"` is cut
183
- * at the inner quote and `"/*"` becomes code). Nothing relies on the
184
- * blanking being right: csharpClasses compares the declarations it names against
185
- * the type keywords of the RAW text, and any declaration a misread hides makes the
186
- * file unscannable.
187
- *
188
- * @param {string} text
189
- * @returns {string|null}
104
+ * Comments, preprocessor lines and literals blanked, length and newlines kept; null when
105
+ * a literal never ends or an interpolation hole is cut. A heuristic lexer that CAN misread:
106
+ * csharpClasses never trusts it alone (it checks against the raw text).
190
107
  */
191
108
  export function blankCSharpLiterals(text) {
192
109
  const out = text.split('');
@@ -232,7 +149,6 @@ export function blankCSharpLiterals(text) {
232
149
  return out.join('');
233
150
  }
234
151
 
235
- /** Type-keyword tokens in a text (raw or blanked) — every one; no context rule (see TYPE_KEYWORD). */
236
152
  const typeKeywordCount = (text) => [...text.matchAll(TYPE_KEYWORD)].length;
237
153
 
238
154
  /**
@@ -285,7 +201,7 @@ const typeKeywordCount = (text) => [...text.matchAll(TYPE_KEYWORD)].length;
285
201
  * or a custom one (`[SkippableFact]`) is not seen: the file counts as a helper and
286
202
  * it is left out of a selection;
287
203
  * - `hasTestAttribute` is still a heuristic, and ITS false positive is the unsafe
288
- * side. Since the re-review (N1) the section must close; what still matches in a
204
+ * side. The section must close; what still matches in a
289
205
  * helper — probed — is a LIST PATTERN opening a line (or after `;` `{` `}` `]`)
290
206
  * whose element is named exactly `Test`/`Fact`/… (qualified or not) and whose `]`
291
207
  * is followed by a combinator: `[Env.Test] or [Env.Dev] => true`. Such a helper is
@@ -317,10 +233,8 @@ export function csharpClasses(csFileAbs) {
317
233
  // length, so the raw text at the same offsets shows the directive.
318
234
  const split = decls.find((d) => /(^|\n)[ \t]*#/.test(text.slice(d.at - d.gap, d.at)));
319
235
  if (split) return unscannable(`a preprocessor directive stands between a type keyword and the name "${split.name}"`);
320
- // THE NET (R2, tightened in round 5): the raw text is the lexer-independent
321
- // truth about how many type keywords the file carries. Compare it — and the
322
- // blanked code — against what was actually named (see the JSDoc for why
323
- // equality of counts is equality of sets).
236
+ // The raw text is the lexer-independent truth about how many type keywords the file
237
+ // carries (the JSDoc explains why equal counts mean equal sets).
324
238
  const rawKeywords = typeKeywordCount(text);
325
239
  if (rawKeywords !== decls.length) {
326
240
  return unscannable(`${rawKeywords} type keyword(s) in the file but ${decls.length} declaration(s) recognised`);
@@ -4,12 +4,13 @@ import { applyRegions, diffOf } from './manifest.js';
4
4
  import { blame, parseCompilerErrors, patchFile, switchClassName, switchSource, SWITCH_ENV } from './schemata.js';
5
5
  import { md5Of, md5OfFile, openSnapshot, restoreSnapshot, trackGenerated } from './snapshot.js';
6
6
  import { parseBuiltDlls } from './commands.js';
7
- import { firstLines, methodOf } from './trx.js';
7
+ import { firstLines } from './trx.js';
8
8
  import { nearestProjectOf } from './dotnet-introspection.js';
9
9
 
10
10
  const posix = (p) => p.replace(/\\/g, '/');
11
11
  const MAX_SCHEMATA_ATTEMPTS = 3;
12
- const MAX_REPRODUCE = 5;
12
+
13
+ export const noSubsetReason = (why) => `sem subconjunto utilizável (${why ?? 'nenhum teste da feature nem que cite os tipos mutados'}): a mutação não roda a suíte inteira — prove o cenário por leitura`;
13
14
 
14
15
  const envWithout = (env) => { const { [SWITCH_ENV]: _drop, ...rest } = env; return rest; };
15
16
  const failedNames = (result) => new Set(result.failedTests.map((t) => t.name));
@@ -123,29 +124,15 @@ export async function runMutation(input) {
123
124
  .map((name) => [name, md5OfFile(dlls.get(name))]),
124
125
  );
125
126
 
126
- const run = (scope, label, env) => timed(label, () => adapter.runTests({ scope, label, env }));
127
-
128
- const confirmKill = async (id, verdict, env) => {
129
- if (!adapter.reproduce) return verdict;
130
- const names = [...new Set(verdict.failures.map((f) => f.name))].slice(0, MAX_REPRODUCE);
131
- const again = await timed(`${id}-reproduce`, () => adapter.reproduce({ names, label: `${id}-reproduce`, env }));
132
- if (again.outcome === 'crash' || !again.executed) {
133
- return { state: 'inconclusive', reason: `a falha de ${names[0]} não pôde ser confirmada na repetição (${again.note ?? 'nenhum teste executado'})` };
134
- }
135
- const wanted = new Set(names.map(methodOf));
136
- const held = again.failedTests.filter((t) => wanted.has(methodOf(t.name)));
137
- if (held.length > 0) return { ...verdict, reason: reasonOf(held) };
138
- const whole = await timed(`${id}-reproduce-full`, () => adapter.runTests({ scope: 'full', label: `${id}-reproduce-full`, env }));
139
- if (whole.outcome === 'crash' || !whole.executed) {
140
- return { state: 'inconclusive', reason: `a falha de ${names[0]} não pôde ser confirmada na repetição (${whole.note ?? 'nenhum teste executado'})` };
141
- }
142
- const heldWhole = whole.failedTests.filter((t) => wanted.has(methodOf(t.name)));
143
- if (heldWhole.length > 0) return { ...verdict, reason: reasonOf(heldWhole) };
144
- const which = names.length === 1 ? names[0] : `${names.length} testes`;
145
- return { state: 'survived', reason: `nenhum teste falhou de forma reproduzível: ${which} falhou uma vez e passou isolado e na suíte inteira repetida (instável, não prova o mutante)` };
146
- };
127
+ const run = (label, env) => timed(label, () => adapter.runTests({ label, env }));
147
128
 
148
129
  const record = (id, patch) => Object.assign(results.get(id), patch);
130
+ const recordSubset = (id, result) => {
131
+ const verdict = judge(result, baselineSubset);
132
+ const base = { scope: 'subset', executed: result.executed, durationMs: result.durationMs };
133
+ if (verdict.state === 'killed') record(id, { ...base, verdict: 'killed', killers: verdict.failures.length, reason: verdict.reason });
134
+ else record(id, { ...base, verdict: verdict.state, reason: verdict.reason ?? '' });
135
+ };
149
136
  const applyOnDisk = (id) => {
150
137
  const region = byId.get(id);
151
138
  writeFileSync(join(root, region.file), applyRegions(originals.get(region.file), [region], (x) => x.replaceText).text);
@@ -154,10 +141,8 @@ export async function runMutation(input) {
154
141
  let pristine = new Map();
155
142
  let binary = { proof: 'unavailable', projects: [] };
156
143
  let baselineSubset = null;
157
- let baselineFull = null;
158
144
  let subsetLive = adapter.hasSubset;
159
145
  let subsetNote = null;
160
- const needFullBaseline = [];
161
146
  let restoreReport = [];
162
147
 
163
148
  try {
@@ -238,40 +223,26 @@ export async function runMutation(input) {
238
223
  if (subsetLive && (schemataIds.length > 0 || hasRebuild)) {
239
224
  const copies = schemataIds.length > 0 ? jobs : 1;
240
225
  baselineSubset = mergeBaselines(await Promise.all(Array.from({ length: copies }, (_, i) => (
241
- run('subset', i === 0 ? 'baseline-subset' : `baseline-subset-${i + 1}`, envWithout(process.env))))));
226
+ run(i === 0 ? 'baseline-subset' : `baseline-subset-${i + 1}`, envWithout(process.env))))));
242
227
  if (baselineSubset.outcome === 'crash' || baselineSubset.executed === 0) {
243
228
  subsetLive = false;
244
229
  subsetNote = baselineSubset.executed === 0 ? 'o filtro do subconjunto não selecionou teste algum' : (baselineSubset.note ?? 'a rodada-base do subconjunto não terminou');
245
230
  }
246
231
  }
247
- if (!subsetLive && schemataIds.length > 0) baselineFull = await run('full', 'baseline-full', envWithout(process.env));
248
-
249
- const scopeOf = subsetLive ? 'subset' : 'full';
250
- const scopeBaseline = () => (subsetLive ? baselineSubset : baselineFull);
251
232
  const routed = [];
252
- const scopeJobs = subsetLive ? jobs : 1;
253
- workerCount = schemataIds.length > 0 ? scopeJobs : 1;
254
- const outcomes = new Map();
255
- await pool(schemataIds, scopeJobs, async (id) => {
256
- outcomes.set(id, await run(scopeOf, `${id}-${scopeOf}`, { ...process.env, [SWITCH_ENV]: id }));
257
- });
258
- for (const id of schemataIds) {
259
- const result = outcomes.get(id);
260
- const first = judge(result, scopeBaseline());
261
- const verdict = first.state === 'killed' && scopeOf === 'full' ? await confirmKill(id, first, { ...process.env, [SWITCH_ENV]: id }) : first;
262
- const base = { scope: scopeOf, executed: result.executed, durationMs: result.durationMs };
263
- if (verdict.state === 'killed') {
264
- record(id, { ...base, verdict: 'killed', killers: verdict.failures.length, reason: verdict.reason });
265
- } else if (scopeOf === 'subset' && verdict.state === 'survived') {
266
- record(id, { ...base, verdict: 'survived', reason: '' });
267
- } else {
268
- record(id, { ...base, verdict: null, reason: verdict.reason ?? '' });
269
- routed.push({ id, needs: 'full' });
270
- }
233
+ workerCount = subsetLive && schemataIds.length > 0 ? jobs : 1;
234
+ if (subsetLive) {
235
+ const outcomes = new Map();
236
+ await pool(schemataIds, jobs, async (id) => {
237
+ outcomes.set(id, await run(`${id}-subset`, { ...process.env, [SWITCH_ENV]: id }));
238
+ });
239
+ for (const id of schemataIds) recordSubset(id, outcomes.get(id));
240
+ for (const r of results.values()) if (r.mode === 'rebuild') routed.push(r.id);
241
+ } else {
242
+ for (const r of results.values()) record(r.id, { verdict: 'inconclusive', scope: null, reason: noSubsetReason(subsetNote) });
271
243
  }
272
- for (const r of results.values()) if (r.mode === 'rebuild') routed.push({ id: r.id, needs: subsetLive ? 'subset' : 'full' });
273
244
 
274
- for (const { id, needs } of routed) {
245
+ for (const id of routed) {
275
246
  applyOnDisk(id);
276
247
  try {
277
248
  if (adapter.buildCommands.length > 0) {
@@ -282,21 +253,7 @@ export async function runMutation(input) {
282
253
  continue;
283
254
  }
284
255
  }
285
- if (needs === 'subset') {
286
- const sub = await run('subset', `${id}-subset`, envWithout(process.env));
287
- const verdict = judge(sub, baselineSubset);
288
- const base = { scope: 'subset', executed: sub.executed, durationMs: sub.durationMs };
289
- if (verdict.state === 'killed') record(id, { ...base, verdict: 'killed', killers: verdict.failures.length, reason: verdict.reason });
290
- else record(id, { ...base, verdict: verdict.state, reason: verdict.reason ?? '' });
291
- continue;
292
- }
293
- const full = await run('full', `${id}-full`, envWithout(process.env));
294
- const first = judge(full, baselineFull);
295
- const verdict = first.state === 'killed' ? await confirmKill(id, first, envWithout(process.env)) : first;
296
- const base = { scope: 'full', executed: full.executed, durationMs: full.durationMs };
297
- if (verdict.state === 'inconclusive') record(id, { ...base, verdict: 'inconclusive', reason: verdict.reason });
298
- else if (verdict.state === 'survived') record(id, { ...base, verdict: 'survived', reason: verdict.reason ?? '' });
299
- else { record(id, { ...base, verdict: 'killed', killers: verdict.failures.length, reason: verdict.reason }); if (!baselineFull) needFullBaseline.push({ id, full }); }
256
+ recordSubset(id, await run(`${id}-subset`, envWithout(process.env)));
300
257
  } finally {
301
258
  restoreSnapshot(snapshot);
302
259
  }
@@ -310,14 +267,6 @@ export async function runMutation(input) {
310
267
  binary = proveBinary({ pristine, dlls: last.dlls, restoredAt, hashes: dllHashes(last.dlls) });
311
268
  }
312
269
 
313
- if (needFullBaseline.length > 0) {
314
- baselineFull = await run('full', 'baseline-full', envWithout(process.env));
315
- for (const { id, full } of needFullBaseline) {
316
- const verdict = judge(full, baselineFull);
317
- if (verdict.state === 'killed') record(id, { killers: verdict.failures.length, reason: verdict.reason });
318
- else record(id, { verdict: 'survived', killers: 0, reason: '' });
319
- }
320
- }
321
270
  } finally {
322
271
  restoreReport = restoreSnapshot(snapshot, { rewrite: false });
323
272
  gate?.release();
@@ -338,7 +287,6 @@ export async function runMutation(input) {
338
287
  mutants,
339
288
  baseline: {
340
289
  subset: baselineSubset ? { executed: baselineSubset.executed, failed: baselineSubset.failed } : null,
341
- full: baselineFull ? { executed: baselineFull.executed, failed: baselineFull.failed } : null,
342
290
  },
343
291
  binary,
344
292
  restore: {
@@ -51,10 +51,7 @@ export function testFilesCiting({ root, testProjectDirs, types, exclude = [] })
51
51
  return found.sort();
52
52
  }
53
53
 
54
- /**
55
- * The tests to run FIRST, chosen by the tool: files the feature changed, and files naming a
56
- * type the mutated files declare. A mutant the subset does not kill goes to the whole suite.
57
- */
54
+ /** Chosen by the tool, never the verifier: test files the feature changed or that name a mutated type. */
58
55
  export function dotnetSubset({ root, testProjectDirs, mutatedFiles, changed = [] }) {
59
56
  const changedSet = new Set(changed.map(posix));
60
57
  const mention = mentionOf(mutatedTypesOf(root, mutatedFiles));
@@ -43,7 +43,6 @@ export function parseTrx(xml) {
43
43
  };
44
44
  }
45
45
 
46
- export const methodOf = (testName) => String(testName).replace(/\s*\(.*$/s, '').trim();
47
46
 
48
47
  export function firstLines(message, max = 2, width = 240) {
49
48
  const lines = String(message).split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(0, max).join(' ⏎ ');
@@ -1,33 +1,18 @@
1
1
  /** The inline justification that lets a criterion talk about an artifact on purpose. */
2
2
  export const ARTIFACT_MARKER_RE = /por\s+que\s+artefato\s*:/i;
3
3
 
4
- /**
5
- * Case-insensitive spelling of `word` inside a regex that must stay
6
- * case-SENSITIVE elsewhere — Node 18 has no inline `(?i:…)` modifier.
7
- */
4
+ // Node 18 has no inline `(?i:…)` modifier, and the rest of the regex must stay case-sensitive.
8
5
  const anyCase = (word) => [...word].map((c) => {
9
6
  const lo = c.toLowerCase();
10
7
  const up = c.toUpperCase();
11
8
  return lo === up ? c : `[${lo}${up}]`;
12
9
  }).join('');
13
10
 
14
- /**
15
- * What follows "arquivo em" in a behavioural export criterion ("exporta o
16
- * arquivo em CSV", "salva o arquivo em formato aberto"): an uppercase file
17
- * format acronym or the word `formato`. Case-sensitive on purpose — `src`,
18
- * `docs` or `lib` after "arquivo em" are places, and places are artifacts.
19
- */
11
+ // Case-sensitive on purpose: `src`, `docs` or `lib` after "arquivo em" are places, and places are artifacts.
20
12
  const FORMAT_TOKEN = String.raw`\s+(?:[A-Z]{2,5}(?![\p{L}\d_])|${anyCase('formato')}(?![\p{L}]))`;
21
13
 
22
- /**
23
- * Vocabulary of artifact-shaped phrasing. `term` is the label surfaced in
24
- * warnings and cited by the `morph-plan` skill prose — keep it stable, a
25
- * parity test imports it verbatim.
26
- *
27
- * Unicode lookarounds, never `\b`: in JS, `\b` does not treat accented
28
- * letters (é, á, ó…) as word characters, so it would false-boundary inside
29
- * "não" or "binário".
30
- */
14
+ // `term` is cited verbatim by the `morph-plan` skill (a parity test imports it): keep it stable.
15
+ // Unicode lookarounds, never `\b`: JS `\b` false-boundaries inside "não" or "binário".
31
16
  export const ARTIFACT_VOCABULARY = [
32
17
  { term: 'grep', re: /(?<![\p{L}\d_])grep(?![\p{L}\d_])/iu },
33
18
  { term: 'contém a string', re: /cont[ée]m\s+as?\s+strings?/iu },
@@ -44,12 +29,7 @@ export const ARTIFACT_VOCABULARY = [
44
29
  { term: 'glob **/', re: /\*\*\/|\/\*\*/ },
45
30
  ];
46
31
 
47
- /**
48
- * Vocabulary terms matched in `text`, in vocabulary order, each at most once.
49
- *
50
- * @param {string} text
51
- * @returns {string[]}
52
- */
32
+ /** Each term at most once, in vocabulary order. */
53
33
  export function findArtifactTerms(text) {
54
34
  const s = String(text ?? '');
55
35
  const found = [];