@polymorphism-tech/morph-spec 8.46.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CLAUDE.md +53 -44
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +38 -55
  4. package/bin/validate.js +5 -134
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/create-umbraco-lp.md +2 -0
  12. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  13. package/framework/commands/morph-apply.md +5 -28
  14. package/framework/commands/morph-archive.md +2 -2
  15. package/framework/commands/morph-hotfix.md +8 -8
  16. package/framework/commands/morph-infra.md +17 -203
  17. package/framework/commands/morph-media.md +3 -0
  18. package/framework/commands/morph-preflight.md +7 -31
  19. package/framework/commands/morph-proposal.md +2 -50
  20. package/framework/commands/morph-status.md +3 -3
  21. package/framework/commands/morph-troubleshoot.md +2 -14
  22. package/framework/docs/CLI.md +585 -286
  23. package/framework/evals/review-checklist.md +82 -41
  24. package/framework/hooks/README.md +10 -11
  25. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  26. package/framework/hooks/claude-code/core/denylist.json +33 -3
  27. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  28. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  30. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  31. package/framework/hooks/claude-code/core/state-sync.js +17 -19
  32. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  33. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  34. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  35. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  36. package/framework/hooks/claude-code/statusline.py +55 -41
  37. package/framework/hooks/package.json +3 -0
  38. package/framework/hooks/shared/activity-logger.js +1 -1
  39. package/framework/hooks/shared/build-check.js +10 -6
  40. package/framework/hooks/shared/command-tokens.js +169 -0
  41. package/framework/hooks/shared/comment-rules.js +1 -1
  42. package/framework/hooks/shared/comment-scanner.js +10 -23
  43. package/framework/hooks/shared/comment-stripper.js +1 -1
  44. package/framework/hooks/shared/e2e-check.js +67 -31
  45. package/framework/hooks/shared/errata.js +72 -0
  46. package/framework/hooks/shared/feature-locator.js +1 -1
  47. package/framework/hooks/shared/frontend-locations.js +6 -0
  48. package/framework/hooks/shared/gate-question.js +18 -0
  49. package/framework/hooks/shared/phase-utils.js +15 -32
  50. package/framework/hooks/shared/slice-progress.js +118 -0
  51. package/framework/hooks/shared/spawn-command.js +23 -0
  52. package/framework/hooks/shared/state-reader.js +61 -87
  53. package/framework/hooks/shared/state-writer.js +3 -3
  54. package/framework/mcp/registry.json +3 -4
  55. package/framework/phases.json +23 -39
  56. package/framework/rules/ai-agents-standards.md +18 -16
  57. package/framework/rules/asaas-standards.md +3 -1
  58. package/framework/rules/csharp-standards.md +10 -8
  59. package/framework/rules/data-standards.md +5 -5
  60. package/framework/rules/dotnet-templates.md +1 -1
  61. package/framework/rules/frontend-standards.md +4 -2
  62. package/framework/rules/gsap-standards.md +10 -5
  63. package/framework/rules/hangfire-standards.md +3 -1
  64. package/framework/rules/infrastructure-standards.md +4 -2
  65. package/framework/rules/neon-auth-standards.md +3 -1
  66. package/framework/rules/neon-standards.md +6 -4
  67. package/framework/rules/nextjs-standards.md +13 -11
  68. package/framework/rules/nextjs-templates.md +1 -1
  69. package/framework/rules/resend-standards.md +3 -1
  70. package/framework/schemas/config.schema.json +14 -9
  71. package/framework/schemas/feature.schema.json +15 -52
  72. package/framework/skills/README.md +4 -5
  73. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  74. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  75. package/framework/skills/core/morph-apply/SKILL.md +150 -223
  76. package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
  77. package/framework/skills/core/morph-machine/SKILL.md +4 -4
  78. package/framework/skills/core/morph-media/SKILL.md +3 -3
  79. package/framework/skills/core/morph-plan/SKILL.md +138 -300
  80. package/framework/skills/core/morph-review/SKILL.md +112 -137
  81. package/framework/skills/core/morph-standards/SKILL.md +29 -41
  82. package/framework/skills/core/morph-uiux/SKILL.md +78 -153
  83. package/framework/standards/STANDARDS.json +90 -1219
  84. package/framework/standards/STANDARDS.meta.json +1305 -0
  85. package/framework/standards/ai-agents/evals-with-cache.md +2 -3
  86. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  87. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  88. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  89. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  90. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  91. package/framework/standards/ai-agents/setup.md +2 -2
  92. package/framework/standards/ai-agents/structured-output.md +1 -1
  93. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  94. package/framework/standards/architecture/code-comments.md +8 -8
  95. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  96. package/framework/standards/backend/api/validation.md +135 -47
  97. package/framework/standards/backend/database/postgresql/database.md +11 -7
  98. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  99. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  100. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  101. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  102. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  103. package/framework/standards/backend/dotnet/testing.md +1 -1
  104. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  105. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  106. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  107. package/framework/standards/data/nosql/cache/redis.md +120 -44
  108. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  109. package/framework/standards/frontend/design-system/animations.md +1 -1
  110. package/framework/standards/frontend/design-system/naming.md +1 -1
  111. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  112. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  113. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  114. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  115. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  116. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  117. package/framework/templates/README.md +2 -3
  118. package/framework/templates/REGISTRY.json +6 -170
  119. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  120. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  121. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  122. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  123. package/framework/templates/docs/plan.md +72 -0
  124. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  127. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  128. package/framework/templates/infrastructure/github/README.md +2 -1
  129. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  130. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  131. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  132. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  133. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  134. package/package.json +1 -1
  135. package/src/commands/e2e/e2e.js +2 -2
  136. package/src/commands/observability/cost.js +18 -60
  137. package/src/commands/observability/telemetry.js +3 -5
  138. package/src/commands/project/doctor-procs.js +1 -1
  139. package/src/commands/project/doctor.js +9 -9
  140. package/src/commands/project/fleet.js +4 -4
  141. package/src/commands/project/heavy.js +3 -3
  142. package/src/commands/project/init.js +6 -1
  143. package/src/commands/project/setup-infra-cmd.js +2 -1
  144. package/src/commands/project/stack-scan.js +58 -0
  145. package/src/commands/project/standards.js +79 -25
  146. package/src/commands/project/status.js +29 -69
  147. package/src/commands/project/update.js +33 -19
  148. package/src/commands/project/worktree.js +1 -1
  149. package/src/commands/state/advance.js +62 -46
  150. package/src/commands/state/approve.js +95 -138
  151. package/src/commands/state/archive.js +20 -84
  152. package/src/commands/state/auto-approval.js +68 -0
  153. package/src/commands/state/create.js +28 -1
  154. package/src/commands/state/delete.js +1 -1
  155. package/src/commands/state/finish.js +11 -23
  156. package/src/commands/state/gate-check.js +86 -95
  157. package/src/commands/state/gate-decision.js +6 -4
  158. package/src/commands/state/handoff.js +1 -1
  159. package/src/commands/state/list.js +9 -9
  160. package/src/commands/state/report-check.js +157 -311
  161. package/src/commands/state/retype.js +9 -3
  162. package/src/commands/validation/mutate.js +77 -14
  163. package/src/commands/validation/plan-check.js +81 -0
  164. package/src/commands/validation/validate-feature.js +60 -135
  165. package/src/commands/validation/verify.js +41 -82
  166. package/src/core/paths/output-schema.js +20 -67
  167. package/src/core/state/gate-order.js +5 -6
  168. package/src/core/state/lifecycle-audit.js +2 -2
  169. package/src/core/state/state-manager.js +17 -98
  170. package/src/core/templates/template-registry.js +2 -2
  171. package/src/core/templates/template-seed.js +22 -0
  172. package/src/core/work-types.js +10 -10
  173. package/src/dashboard/app/App.jsx +0 -2
  174. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  175. package/src/dashboard/app/components/CommandPalette.jsx +7 -8
  176. package/src/dashboard/app/components/EntityLink.jsx +1 -2
  177. package/src/dashboard/app/components/Markdown.jsx +1 -1
  178. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  179. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  180. package/src/dashboard/app/lib/phases.js +2 -4
  181. package/src/dashboard/app/pages/Activity.jsx +1 -1
  182. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  183. package/src/dashboard/app/pages/Features.jsx +9 -9
  184. package/src/dashboard/app/pages/Guide.jsx +2 -2
  185. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  186. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  187. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  188. package/src/dashboard/app/pages/Standards.jsx +1 -1
  189. package/src/dashboard/scanner/activity-scanner.js +6 -16
  190. package/src/dashboard/scanner/agents-scanner.js +1 -1
  191. package/src/dashboard/scanner/docs-scanner.js +5 -23
  192. package/src/dashboard/scanner/features-scanner.js +95 -60
  193. package/src/dashboard/scanner/index.js +0 -2
  194. package/src/dashboard/scanner/project-api.js +1 -1
  195. package/src/dashboard/scanner/rules-scanner.js +15 -10
  196. package/src/dashboard/scanner/skills-scanner.js +2 -3
  197. package/src/dashboard/scanner/standards-scanner.js +2 -1
  198. package/src/lib/ci/ci-verify.js +17 -1
  199. package/src/lib/ci/github-ci.js +1 -1
  200. package/src/lib/detectors/index.js +3 -8
  201. package/src/lib/generators/handoff-generator.js +34 -65
  202. package/src/lib/generators/settings-generator.js +11 -89
  203. package/src/lib/git/evidence-staleness.js +1 -1
  204. package/src/lib/git/tree-fingerprint.js +19 -7
  205. package/src/lib/git/tree-state.js +2 -2
  206. package/src/lib/heavy/cleanup.js +1 -5
  207. package/src/lib/installers/memory-installer.js +6 -8
  208. package/src/lib/mutation/commands.js +3 -2
  209. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  210. package/src/lib/mutation/plan.js +14 -6
  211. package/src/lib/mutation/runner.js +1 -1
  212. package/src/lib/mutation/subset.js +46 -9
  213. package/src/lib/plan/artifact-vocabulary.js +60 -0
  214. package/src/lib/plan/checks.js +234 -0
  215. package/src/lib/plan/plan-parser.js +209 -0
  216. package/src/lib/plan/plan-template.js +18 -0
  217. package/src/lib/review-reports.js +222 -0
  218. package/src/lib/risk-detector.js +134 -135
  219. package/src/lib/specs/adr-index.js +50 -69
  220. package/src/lib/specs/design-seed.js +18 -0
  221. package/src/lib/specs/frontmatter.js +44 -0
  222. package/src/lib/specs/gfm-table.js +17 -0
  223. package/src/lib/specs/old-format.js +68 -0
  224. package/src/lib/specs/revocations.js +37 -95
  225. package/src/lib/specs/screen-checklist.js +18 -17
  226. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  227. package/src/lib/stack/stack-profile.js +1 -1
  228. package/src/lib/stack/stack-scan.js +267 -0
  229. package/src/lib/standards/registry.js +43 -0
  230. package/src/lib/standards/sections.js +58 -0
  231. package/src/lib/tasks/eval-node.js +4 -12
  232. package/src/lib/tasks/test-nodes.js +1 -74
  233. package/src/lib/tasks/test-runner.js +75 -117
  234. package/src/lib/telemetry/metrics.js +8 -69
  235. package/src/lib/telemetry/telemetry-reader.js +5 -118
  236. package/src/lib/telemetry/verify-event.js +5 -7
  237. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  238. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  239. package/src/lib/validators/reuse/reuse-validator.js +24 -48
  240. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  241. package/src/lib/validators/traceability-check.js +85 -129
  242. package/src/lib/validators/validation-runner.js +8 -88
  243. package/src/lib/validators/verify-runner.js +134 -301
  244. package/src/lib/worktree/docker-audit.js +1 -2
  245. package/src/lib/worktree/fleet-scanner.js +25 -38
  246. package/src/scripts/setup-infra.js +42 -22
  247. package/src/utils/agents-installer.js +92 -13
  248. package/src/utils/claude-settings-manager.js +0 -35
  249. package/src/utils/file-copier.js +34 -6
  250. package/src/utils/hooks-installer.js +16 -9
  251. package/src/utils/skills-installer.js +1 -2
  252. package/framework/commands/commit.md +0 -166
  253. package/framework/evals/trace-schema.md +0 -67
  254. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  255. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  256. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  257. package/framework/hooks/shared/tasks-validator.js +0 -111
  258. package/framework/rubrics/backend.json +0 -41
  259. package/framework/rubrics/default.json +0 -36
  260. package/framework/rubrics/frontend.json +0 -36
  261. package/framework/rules/morph-task-tracking.md +0 -54
  262. package/framework/rules/morph-workflow.md +0 -244
  263. package/framework/schemas/tasks.schema.json +0 -79
  264. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  265. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  266. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  267. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  268. package/framework/templates/docs/proposal.md +0 -192
  269. package/framework/templates/docs/spec.md +0 -57
  270. package/framework/templates/docs/ui-components.md +0 -52
  271. package/framework/templates/docs/ui-design-system.md +0 -76
  272. package/framework/templates/docs/ui-flows.md +0 -167
  273. package/framework/templates/examples/spec-examples.md +0 -91
  274. package/framework/templates/feature/decisions.md +0 -124
  275. package/framework/templates/feature/recap.md +0 -145
  276. package/framework/templates/project/validate.js +0 -97
  277. package/framework/templates/tasks.template.json +0 -17
  278. package/src/commands/state/usecase.js +0 -143
  279. package/src/commands/tasks/dag.js +0 -181
  280. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  281. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  282. package/src/lib/agents/output-collector.js +0 -76
  283. package/src/lib/evaluator-report.js +0 -91
  284. package/src/lib/specs/acervo-format.js +0 -70
  285. package/src/lib/specs/acervo-lifecycle.js +0 -264
  286. package/src/lib/specs/done-criteria-check.js +0 -200
  287. package/src/lib/specs/usecase-check.js +0 -301
  288. package/src/lib/specs/usecase-promoter.js +0 -301
  289. package/src/lib/specs/usecase-registry.js +0 -237
  290. package/src/lib/tasks/dispatch-planner.js +0 -319
  291. package/src/lib/tasks/mermaid-renderer.js +0 -132
  292. package/src/lib/tasks/task-classifier.js +0 -30
  293. package/src/lib/tasks/task-parser.js +0 -209
  294. package/src/lib/tasks/tasks-json-parser.js +0 -296
  295. package/src/lib/tasks/test-scope.js +0 -597
  296. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  297. package/src/lib/validators/architecture/index.js +0 -7
  298. package/src/lib/validators/css/css-validator.js +0 -340
  299. package/src/lib/validators/css/index.js +0 -7
  300. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  301. package/src/lib/validators/design-system/index.js +0 -7
  302. package/src/lib/validators/score-aggregator.js +0 -244
  303. package/src/lib/validators/ui/index.js +0 -7
  304. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -1,40 +1,17 @@
1
1
  /**
2
- * .NET test-project introspection for the task-scope recorte (issue #87): which
3
- * test project owns a `.cs` file and with which framework, which project-level
4
- * settings bind every run, which test mode the SDK picks, and which types a C#
5
- * file declares. Split out of test-scope.js, which decides; this only reads.
6
- *
7
- * Every answer errs toward "run the node whole". A wrong answer that NARROWS runs
8
- * fewer tests and still comes out green — the one failure issue #87 must never buy.
9
- *
10
- * The C# scan is NOT a parser. What narrowing relies on is narrower than "the
11
- * scan is right" and is stated exactly in csharpClasses: every type-keyword
12
- * token of the RAW text (nothing blanked, no context rule) must be a declaration
13
- * the scanner NAMED. Type names are BARE and the selectors namespace-free
14
- * (`*Type*` / `FullyQualifiedName~Type`), so no scope reading can misname one.
15
- * What that does NOT cover is listed there too.
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).
16
5
  *
17
6
  * Pure: reads files, never spawns a process.
18
- *
19
- * @module dotnet-test-introspection
20
7
  */
21
-
22
8
  import { existsSync, readFileSync, readdirSync } from 'fs';
23
9
  import { dirname, join, resolve } from 'path';
24
10
 
25
- import { isInside } from './test-runner.js';
11
+ import { isInside } from '../tasks/test-runner.js';
26
12
  import { jsoncToJson } from '../../../framework/hooks/shared/comment-stripper.js';
27
13
 
28
14
  const MTP_RUNNER = 'Microsoft.Testing.Platform';
29
- const FLOOR_PROPERTY = /<TestingPlatformCommandLineArguments\b[^>]*>[^<]*--minimum-expected-tests/i;
30
- /**
31
- * `--ignore-exit-code`, or the env var MTP reads for the same thing, named ANYWHERE in an
32
- * MSBuild file that binds the project — not only inside `<TestingPlatformCommandLineArguments>`:
33
- * a property indirection (`<MtpExtraArgs>--ignore-exit-code 8</MtpExtraArgs>` referenced as
34
- * `$(MtpExtraArgs)`) carries it just as well (re-review N4, probed). Errs wide.
35
- */
36
- const IGNORE_EXIT_MARKER = /--ignore-exit-code|TESTINGPLATFORM_EXITCODE_IGNORE/i;
37
- const CASE_FILTER_PROPERTY = /<VSTestTestCaseFilter\b[^>]*>/i;
38
15
  const TYPE_HEADER = /\b(?:class|struct|record|interface)\b/;
39
16
  /** A C# identifier's first character: a letter, an Nl letter (`Ⅱ`), `_`, `@` (verbatim) or `\` (a unicode escape). */
40
17
  const NAME_START = '[\\p{L}\\p{Nl}_@\\\\]';
@@ -52,7 +29,7 @@ const NAME_START = '[\\p{L}\\p{Nl}_@\\\\]';
52
29
  * the code counts is a keyword the raw text counts, by offset.
53
30
  * A constraint keyword is therefore counted too whenever a name-shaped token
54
31
  * follows it (`where T : class where U : …`, `: class // note`): it is not named,
55
- * so the file runs whole. `class,` / `class\n{` / `class?` are not counted at all.
32
+ * so the file is unscannable. `class,` / `class\n{` / `class?` are not counted at all.
56
33
  */
57
34
  const TYPE_KEYWORD = new RegExp(`\\b(?:record\\s+(?:class|struct)|class|record|struct|interface)\\b(?=\\s*(?:${NAME_START}|["']|\\/[/*]|#))`, 'gu');
58
35
  /** A type declaration the scanner can NAME: after `; { } ]`, a newline or the file start; attributes; modifiers; a NAME_START name. */
@@ -75,14 +52,13 @@ const TEST_ATTRIBUTE_NAME = '(?:Fact|Theory|Test|TestMethod|DataTestMethod|TestC
75
52
  * or the end of the input.
76
53
  *
77
54
  * A false POSITIVE is the unsafe direction here: a helper taken for a test file
78
- * narrows to its own name, and a test whose name contains it makes the run green
79
- * while the tests that USE the helper never run. The final review of branch B found
55
+ * narrows the subset to its own name, and the tests that USE the helper never run. The final review of branch B found
80
56
  * exactly that with the old prefix match (`, TestContext` / `, Factory` / `, TheoryData`
81
57
  * after any `,`), and its re-review (N1) with a test name merely FOLLOWED by `(` or `,`:
82
58
  * a C# 12 collection laid out with `[` alone on its line (`[\n Env.Dev,\n Env.Test,\n];`,
83
59
  * a row `[Env.Test, Env.Dev],`, `[\n Test("a"),\n];`). Requiring the section to close
84
60
  * rejects all three; what still matches is stated in csharpClasses. A false NEGATIVE
85
- * only runs the node whole (a custom attribute such as `[SkippableFact]`, a test
61
+ * only leaves a test file out of a selection (a custom attribute such as `[SkippableFact]`, a test
86
62
  * attribute after an argument holding `]`).
87
63
  */
88
64
  export const CS_TEST_ATTRIBUTE = new RegExp(
@@ -109,35 +85,6 @@ const readText = (path) => {
109
85
  };
110
86
 
111
87
  /** Which test framework a csproj references, by package (or MSBuild SDK) name. */
112
- function testFrameworkOf(csprojText) {
113
- const refs = [...csprojText.matchAll(/(?:Include|Sdk)\s*=\s*"([^"/]+)/gi)].map((m) => m[1].toLowerCase());
114
- if (refs.some((r) => r.startsWith('xunit.v3'))) return 'xunit.v3';
115
- if (refs.some((r) => r.startsWith('mstest'))) return 'mstest';
116
- if (refs.some((r) => r === 'nunit')) return 'nunit';
117
- if (refs.some((r) => r === 'xunit')) return 'xunit';
118
- return null;
119
- }
120
-
121
- /**
122
- * A test project: named `*Tests.csproj`/`.fsproj`/`.vbproj`, OR referencing a
123
- * test framework / `Microsoft.NET.Test.Sdk` in the project file itself.
124
- */
125
- export function isDotnetTestProject(projectAbs) {
126
- const text = readText(projectAbs) ?? '';
127
- return /Tests\.(?:cs|fs|vb)proj$/i.test(projectAbs) || testFrameworkOf(text) !== null || /Microsoft\.NET\.Test\.Sdk/i.test(text);
128
- }
129
-
130
- /**
131
- * The test project owning a `.cs` file — nearest `.csproj` walking up, as MSBuild
132
- * globs it — or null when the file is not a test file (see isDotnetTestProject).
133
- *
134
- * @returns {{ csproj: string, framework: 'xunit.v3'|'mstest'|'nunit'|'xunit'|null }|null}
135
- */
136
- export function dotnetTestProjectOf(fileAbs, rootAbs) {
137
- const csproj = nearestCsprojOf(fileAbs, rootAbs);
138
- if (csproj === null || !isDotnetTestProject(csproj)) return null;
139
- return { csproj, framework: testFrameworkOf(readText(csproj) ?? '') };
140
- }
141
88
 
142
89
  /** Nearest file matching `projectName` walking up from a file's directory, inside the root; null when none. */
143
90
  function nearestProjectFile(fileAbs, rootAbs, projectName) {
@@ -150,21 +97,10 @@ function nearestProjectFile(fileAbs, rootAbs, projectName) {
150
97
  return null;
151
98
  }
152
99
 
153
- /**
154
- * The `.csproj` that compiles a `.cs` file — nearest one walking up, as MSBuild
155
- * globs it — test project or not; null when there is none inside the root.
156
- * The one door for "which project owns this file": link ownership in test-scope
157
- * asks it about every touched `.cs`, recognised as a test file or not.
158
- */
159
- export function nearestCsprojOf(fileAbs, rootAbs) {
160
- if (!/\.cs$/i.test(fileAbs)) return null;
161
- return nearestProjectFile(fileAbs, rootAbs, /\.csproj$/i);
162
- }
163
-
164
100
  /**
165
101
  * The project — `.csproj`, `.fsproj` or `.vbproj` — nearest to ANY file walking
166
102
  * up, or null inside the root. Asked about the .NET sources the C# scan cannot
167
- * read (a bUnit `.razor`, an F# `.fs`), which nearestCsprojOf does not answer.
103
+ * read (a bUnit `.razor`, an F# `.fs`).
168
104
  */
169
105
  export function nearestProjectOf(fileAbs, rootAbs) {
170
106
  return nearestProjectFile(fileAbs, rootAbs, /\.(?:cs|fs|vb)proj$/i);
@@ -173,33 +109,12 @@ export function nearestProjectOf(fileAbs, rootAbs) {
173
109
  /** .NET sources the C# scan cannot read: Razor (`.razor`, `.cshtml`), F# (`.fs`, `.fsi`, `.fsx`), VB (`.vb`). */
174
110
  const NON_CSHARP_SOURCE = /\.(?:razor|cshtml|fs|fsi|fsx|vb)$/i;
175
111
 
176
- /** A .NET source other than C#: the recorte has no type scan, hence no selector, for it. */
112
+ /** A .NET source other than C#: no type scan, hence no class selector, for it. */
177
113
  export const isNonCSharpSource = (path) => NON_CSHARP_SOURCE.test(String(path));
178
114
 
179
115
  /** Any .NET source a project compiles — C# or one of the others. */
180
116
  export const isDotnetSource = (path) => /\.cs$/i.test(String(path)) || isNonCSharpSource(path);
181
117
 
182
- const MARKER_TAIL = '\\s*(?:[\\p{L}_][\\p{L}\\p{N}_]*\\s*(?:\\.|::)\\s*)*(?:Fact|Theory|Test|DataTestMethod)';
183
- /** Per language, the LOOSE test marker readsLikeTestSource looks for in the raw text. */
184
- const LOOSE_TEST_MARKERS = [
185
- [/\.(?:cs|razor|cshtml)$/i, new RegExp(`[[,]${MARKER_TAIL}`, 'u')], // C#, Razor: `[Fact`, `, Theory`, `[Xunit.Fact(`
186
- [/\.(?:fs|fsi|fsx)$/i, new RegExp(`(?:\\[<|;)${MARKER_TAIL}`, 'u')], // F#: `[<Fact>]`, `[<Trait(…); Fact>]`
187
- [/\.vb$/i, new RegExp(`[<,]${MARKER_TAIL}`, 'u')], // VB: `<Fact>`, `<Trait(…), Fact>`
188
- ];
189
-
190
- /**
191
- * Does a file the recogniser did NOT classify READ like a test, in its raw text
192
- * (comments and strings included)? The question test-scope asks before narrowing
193
- * PAST a touched file, so its errors cost the OPPOSITE of CS_TEST_ATTRIBUTE's: a
194
- * false positive runs a node whole (time), a false negative skips a touched test.
195
- * So it errs wide on purpose — a name PREFIX is enough (`[TestContext` counts).
196
- * false for any extension it has no marker for.
197
- */
198
- export function readsLikeTestSource(fileAbs) {
199
- const marker = LOOSE_TEST_MARKERS.find(([ext]) => ext.test(fileAbs))?.[1];
200
- return marker ? marker.test(readText(fileAbs) ?? '') : false;
201
- }
202
-
203
118
  /**
204
119
  * `.NET` test mode for a cwd: nearest `global.json` walking UP (as the SDK
205
120
  * resolves it — from the CWD, never from the `.sln` argument), read as JSONC.
@@ -224,172 +139,6 @@ export function dotnetTestMode(absCwd) {
224
139
  }
225
140
  }
226
141
 
227
- /** Read on EVERY level from the project's folder up (bindingMsbuildFiles). */
228
- const DIRECTORY_FILES = ['Directory.Build.props', 'Directory.Build.targets', 'Directory.Build.rsp', 'Directory.Packages.props'];
229
-
230
- /**
231
- * The MSBuild files that bind a project, as far as this module reads them: the csproj,
232
- * the `<csproj>.user` Microsoft.Common.targets imports beside it, and every
233
- * DIRECTORY_FILES walking up to the FILESYSTEM root — MSBuild's lookup does not stop at
234
- * the repository, and a Directory.Build.targets above it binds the project just the same
235
- * (#114 review F3). Every level is read, not only the first file MSBuild would import:
236
- * that errs wide. Not read: an `<Import Project=…>` (floorOverride refuses a csproj that
237
- * has one), a NuGet package's `build/*.targets`, the SDK's own imports.
238
- */
239
- function bindingMsbuildFiles(csprojAbs) {
240
- const files = [csprojAbs, `${csprojAbs}.user`];
241
- for (let dir = dirname(csprojAbs); ; dir = dirname(dir)) {
242
- files.push(...DIRECTORY_FILES.map((name) => join(dir, name)));
243
- if (dirname(dir) === dir) return files;
244
- }
245
- }
246
-
247
- /**
248
- * Does an MSBuild file that binds this project (bindingMsbuildFiles) match `pattern`?
249
- * It errs wide: a false positive costs a whole run, a false negative a red (or a
250
- * green) that no test earned.
251
- */
252
- function msbuildDeclares(csprojAbs, pattern) {
253
- return bindingMsbuildFiles(csprojAbs).some((p) => pattern.test(readText(p) ?? ''));
254
- }
255
-
256
- /**
257
- * The project imposes `--minimum-expected-tests` on every run: in the MSBuild
258
- * property (attributes such as `Condition=` allowed), or in a `*testconfig.json`
259
- * beside the csproj. Measured: the CLI flag cannot override it, so a green recorte
260
- * exits 9 — unless floorOverride can lower it (#114).
261
- */
262
- export function imposesMinimumExpected(csprojAbs) {
263
- return msbuildDeclares(csprojAbs, FLOOR_PROPERTY) || testconfigDeclares(csprojAbs, /minimum-?expected-?tests/i);
264
- }
265
-
266
- const ARGS_NAME = 'TestingPlatformCommandLineArguments';
267
- const ARGS_ELEMENT = /<TestingPlatformCommandLineArguments\b([^>]*)>([^<]*)<\/TestingPlatformCommandLineArguments\s*>/gi;
268
- const ARGS_OPENING = /<TestingPlatformCommandLineArguments\b/gi;
269
- /** Named anywhere: an element, a `$(…)` read, a `-p:` in a Directory.Build.rsp. */
270
- const ARGS_MENTION = /TestingPlatformCommandLineArguments/i;
271
- const IMPORT_ELEMENT = /<Import\b/i;
272
- /**
273
- * A tag — or a comment, CDATA section, declaration or processing instruction, which
274
- * enclosingElements skips. An attribute value may hold `>` (`Condition="'$(A)' > '1'"`),
275
- * so a quoted value is consumed whole.
276
- */
277
- const XML_TOKEN = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<[!?][^>]*>|<(\/?)([A-Za-z_][\w.:-]*)((?:[^>"']|"[^"]*"|'[^']*')*?)(\/?)>/g;
278
- /**
279
- * What a value cannot carry and still be re-passed as ONE double-quoted `-p:`
280
- * argument meaning the same thing: MSBuild expansion and escapes (`$ @ %`), its
281
- * property separators (`; ,`), an XML entity (`&`), and shell quoting (`" ' \` \ !`).
282
- */
283
- const UNSAFE_IN_PROPERTY = /[$@%;,&"'`\\!]/;
284
- /** Read after ASCII whitespace is collapsed: a no-break space between flag and number matches nothing (review F7). */
285
- const FLOOR_ARGUMENT = /(^| )--minimum-expected-tests \d+(?= |$)/;
286
-
287
- /**
288
- * The elements enclosing the opening tag of `name`, outermost first — or null when the
289
- * tags do not balance. Fails SAFE: a document it cannot walk is null, and the node runs
290
- * whole.
291
- */
292
- function enclosingElements(text, name) {
293
- const stack = [];
294
- let enclosing = null;
295
- for (const [, closing, tag, attributes, selfClosing] of text.matchAll(XML_TOKEN)) {
296
- if (tag === undefined) continue;
297
- if (closing) {
298
- if (stack.pop()?.tag.toLowerCase() !== tag.toLowerCase()) return null;
299
- continue;
300
- }
301
- if (tag.toLowerCase() === name.toLowerCase()) enclosing = [...stack];
302
- if (!selfClosing) stack.push({ tag, attributes });
303
- }
304
- return stack.length === 0 ? enclosing : null;
305
- }
306
-
307
- /**
308
- * The element is set on EVERY evaluation, by nothing but itself: its parent is a
309
- * `<PropertyGroup>` with no attribute, directly under `<Project>` — never a `<Target>`, a
310
- * `<Choose>/<When>`, an `<ItemDefinitionGroup>` or a conditional group (#114 review F1:
311
- * the element's OWN attributes were the only ones read, and a false `Condition` on its
312
- * group got the value injected into a run MSBuild would never give it). `<Project>` does
313
- * not mark it `TreatAsLocalProperty` (the csproj would beat the global `-p:` and a green
314
- * recorte exit 9, review F5), and the csproj imports nothing that could append to it.
315
- */
316
- function setUnconditionally(text) {
317
- if (IMPORT_ELEMENT.test(text)) return false;
318
- const enclosing = enclosingElements(text, ARGS_NAME);
319
- if (enclosing?.length !== 2) return false;
320
- const [project, group] = enclosing;
321
- return /^Project$/i.test(project.tag) && !/TreatAsLocalProperty/i.test(project.attributes)
322
- && /^PropertyGroup$/i.test(group.tag) && group.attributes.trim() === '';
323
- }
324
-
325
- /**
326
- * The value for `-p:TestingPlatformCommandLineArguments=` that lowers the project's
327
- * floor to 1 and keeps every other argument (#114) — or null, and the node runs whole.
328
- *
329
- * Measured (test/fixtures/test-output/): the CLI flag does NOT override the project's
330
- * floor — a green recorte still exits 9 —, the global MSBuild property does — green
331
- * exits 0, zero tests still exit 8 — and it replaces the value WHOLE, so the rest is
332
- * re-passed verbatim (mtp-minimum-expected-override-multiarg.txt). No rebuild: six
333
- * runs alternating with and without it kept the test DLL's mtime, ~3 s each.
334
- *
335
- * So the value has to be exactly the one MSBuild would use, read from ONE place:
336
- * - ONE `<TestingPlatformCommandLineArguments>` in the csproj itself, set on every
337
- * evaluation (setUnconditionally) — no attribute of its own, no second opening
338
- * (another element, or one in an XML comment: the count is over the raw text);
339
- * - the property named in no other file that binds the project (bindingMsbuildFiles:
340
- * the `.user` file, Directory.Build.* up to the filesystem root) — one could append
341
- * to it or replace it;
342
- * - no floor in a `*testconfig.json` beside it (`-p:` does not reach that file);
343
- * - exactly one literal `--minimum-expected-tests N`, and nothing UNSAFE_IN_PROPERTY.
344
- * Out of reach: a NuGet package's build targets appending to the property. The override
345
- * drops that argument during the recorte, and one like xunit.v3's `--explicit` changes
346
- * WHICH tests run — a limit framework/docs/CLI.md states.
347
- *
348
- * @returns {string|null}
349
- */
350
- export function floorOverride(csprojAbs) {
351
- const text = readText(csprojAbs);
352
- if (text === null) return null;
353
- const elements = [...text.matchAll(ARGS_ELEMENT)];
354
- if (elements.length !== 1 || (text.match(ARGS_OPENING) ?? []).length !== 1) return null;
355
- const [, attributes, raw] = elements[0];
356
- if (attributes.trim() !== '' || !setUnconditionally(text)) return null;
357
- if (bindingMsbuildFiles(csprojAbs).slice(1).some((p) => ARGS_MENTION.test(readText(p) ?? ''))) return null;
358
- if (testconfigDeclares(csprojAbs, /minimum-?expected-?tests/i)) return null;
359
- const value = raw.replace(/[ \t\r\n]+/g, ' ').trim();
360
- if (UNSAFE_IN_PROPERTY.test(value)) return null;
361
- if ((value.match(/--minimum-expected-tests/g) ?? []).length !== 1 || !FLOOR_ARGUMENT.test(value)) return null;
362
- return value.replace(FLOOR_ARGUMENT, '$1--minimum-expected-tests 1');
363
- }
364
-
365
- /**
366
- * The project can make MTP IGNORE an exit code on every run: `--ignore-exit-code` or
367
- * the `TESTINGPLATFORM_EXITCODE_IGNORE` variable named anywhere in an MSBuild file that
368
- * binds the project — inside `<TestingPlatformCommandLineArguments>` or in a property
369
- * it references through `$(…)` — or either one in a `*testconfig.json` beside the csproj.
370
- * Under MTP a narrowed run's proof that tests ran is the summary's count and — when
371
- * the summary is not readable (#115: pt-BR and en-US are) — exit 0 (zero tests exit
372
- * 8); with 8 ignored, zero tests exit 0 and that proof is gone — the final review of
373
- * branch B probed it end to end. Errs wide, like the floor.
374
- */
375
- export function ignoresExitCode(csprojAbs) {
376
- return msbuildDeclares(csprojAbs, IGNORE_EXIT_MARKER)
377
- || testconfigDeclares(csprojAbs, /ignore-?exit-?code|exit-?code-?ignore|TESTINGPLATFORM_EXITCODE_IGNORE/i);
378
- }
379
-
380
- /** Does a `*testconfig.json` beside the csproj (MTP reads it from the project) match `pattern`? */
381
- function testconfigDeclares(csprojAbs, pattern) {
382
- const projectDir = dirname(csprojAbs);
383
- let names = [];
384
- try { names = readdirSync(projectDir).filter((n) => /testconfig\.json$/i.test(n)); } catch { /* none */ }
385
- return names.some((n) => pattern.test(readText(join(projectDir, n)) ?? ''));
386
- }
387
-
388
- /** The project declares `<VSTestTestCaseFilter>` — an appended `--filter` would silently REPLACE it. */
389
- export function declaresCaseFilter(csprojAbs) {
390
- return msbuildDeclares(csprojAbs, CASE_FILTER_PROPERTY);
391
- }
392
-
393
142
  const lineEnd = (text, i) => { const e = text.indexOf('\n', i); return e < 0 ? text.length : e; };
394
143
 
395
144
  /** End (exclusive) of a regular string or char literal opened at `start`; -1 if the line ends first. */
@@ -431,7 +180,7 @@ function holeLeftOpen(content) {
431
180
  *
432
181
  * This is a heuristic lexer and it CAN misread (known: `holeLeftOpen` counts the
433
182
  * braces of a char literal such as `'}'`, so `$"{(c == '}' ? "a" : "/*")}"` is cut
434
- * at the inner quote and `"/*"` becomes code). Nothing in narrowing relies on the
183
+ * at the inner quote and `"/*"` becomes code). Nothing relies on the
435
184
  * blanking being right: csharpClasses compares the declarations it names against
436
185
  * the type keywords of the RAW text, and any declaration a misread hides makes the
437
186
  * file unscannable.
@@ -490,8 +239,8 @@ const typeKeywordCount = (text) => [...text.matchAll(TYPE_KEYWORD)].length;
490
239
  * The types of a C# file, read FAIL-SAFE.
491
240
  *
492
241
  * `types` lists the BARE name of EVERY type declaration (class, record, struct,
493
- * interface) at ANY depth, in source order, with no namespace. That list is what
494
- * narrowing selects on, namespace-free (`--filter-class "*T*"`, measured, or the
242
+ * interface) at ANY depth, in source order, with no namespace. A selection built
243
+ * from these names is namespace-free (`--filter-class "*T*"`, measured, or the
495
244
  * substring `FullyQualifiedName~T`): any namespace, `Outer+Inner`, `Name`1` — a
496
245
  * selection that can only run MORE tests than the file's, never fewer.
497
246
  *
@@ -517,25 +266,24 @@ const typeKeywordCount = (text) => [...text.matchAll(TYPE_KEYWORD)].length;
517
266
  * opener, a quote inside an interpolation hole, a char literal — is still in R
518
267
  * and cannot be compensated by anything (a fake the code exposes is in R too).
519
268
  * - `abstract` lists the abstract types: their tests run under the names of DERIVED
520
- * classes, possibly in files the task did not touch, so narrowing refuses them.
269
+ * classes, possibly in other files.
521
270
  * - `hasTestAttribute`: a test attribute (CS_TEST_ATTRIBUTE: a whole name inside an
522
271
  * attribute section) is present in the blanked code (a file without one is a
523
272
  * helper/fake/fixture: the tests using it are elsewhere).
524
273
  * - kept fail-safes: braces balance and blankCSharpLiterals returning null → unscannable.
525
274
  *
526
275
  * NOT guaranteed (stated, not hidden):
527
- * - narrowing itself: keyword-shaped text in a real comment or string (`// the
276
+ * - completeness: keyword-shaped text in a real comment or string (`// the
528
277
  * class under test`), a constraint keyword followed by a name-shaped token
529
278
  * (`where T : class where …`, `: class // note`), `record with`, a directive
530
- * splitting a name — all run the node whole. A cost in time, never a skipped test;
531
- * - which scope a type was read in (`names`, the top-level subset, is
532
- * informational only — nothing in narrowing depends on it);
279
+ * splitting a name — all make the file unscannable;
280
+ * - which scope a type was read in;
533
281
  * - a NON-abstract class whose tests are inherited by derived classes declared in
534
282
  * OTHER files: the touched class runs; the derived copies only if their names
535
283
  * contain it;
536
284
  * - a test attribute reached through an alias (`using F = Xunit.FactAttribute; [F]`)
537
285
  * or a custom one (`[SkippableFact]`) is not seen: the file counts as a helper and
538
- * the node runs whole (safe, slower);
286
+ * it is left out of a selection;
539
287
  * - `hasTestAttribute` is still a heuristic, and ITS false positive is the unsafe
540
288
  * side. Since the re-review (N1) the section must close; what still matches in a
541
289
  * helper — probed — is a LIST PATTERN opening a line (or after `;` `{` `}` `]`)
@@ -617,16 +365,3 @@ export function csharpClasses(csFileAbs) {
617
365
  };
618
366
  }
619
367
 
620
- /**
621
- * BARE names of the TOP-LEVEL `class` declarations in a C# file — no namespace
622
- * (the selectors are namespace-free by construction, see csharpClasses). Records
623
- * are not test classes. Nested classes are left out. `[]` also when the file
624
- * cannot be scanned safely.
625
- *
626
- * @param {string} csFileAbs
627
- * @returns {string[]} unvalidated — narrowNode refuses names outside `[A-Za-z0-9_.]`
628
- */
629
- export function classNamesOf(csFileAbs) {
630
- const { names, scannable } = csharpClasses(csFileAbs);
631
- return scannable ? names : [];
632
- }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { isInside } from '../tasks/test-runner.js';
4
- import { isDotnetSource } from '../tasks/dotnet-test-introspection.js';
4
+ import { isDotnetSource } from './dotnet-introspection.js';
5
5
  import { resolveRegions, validateManifest } from './manifest.js';
6
6
 
7
7
  const JS_RUNNERS = new Set(['vitest', 'jest', 'node-test']);
@@ -14,23 +14,31 @@ export function loadPlan({ root, planPath }) {
14
14
  });
15
15
  }
16
16
 
17
- export function pickNode(file, nodes, root) {
17
+ function ownersOf(file, nodes, root) {
18
+ if (isDotnetSource(file)) return nodes.filter((n) => n.runner === 'dotnet');
18
19
  const abs = resolve(root, file);
19
- if (isDotnetSource(file)) return nodes.find((n) => n.runner === 'dotnet') ?? null;
20
- const owners = nodes
20
+ return nodes
21
21
  .filter((n) => JS_RUNNERS.has(n.runner) && isInside(abs, resolve(root, n.cwd || '.')))
22
22
  .sort((a, b) => resolve(root, b.cwd || '.').length - resolve(root, a.cwd || '.').length);
23
- return owners[0] ?? null;
23
+ }
24
+
25
+ // An `e2e: true` suite tests the running compose stack, which mutate never
26
+ // rebuilds: it is picked only when no other suite owns the file, and the caller refuses.
27
+ export function pickNode(file, nodes, root) {
28
+ const owners = ownersOf(file, nodes, root);
29
+ return owners.find((n) => !n.e2e) ?? owners[0] ?? null;
24
30
  }
25
31
 
26
32
  export function groupByNode(regions, nodes, root) {
27
33
  const groups = new Map();
28
34
  const orphans = [];
35
+ const stackOnly = [];
29
36
  for (const region of regions) {
30
37
  const node = pickNode(region.file, nodes, root);
31
38
  if (!node) { orphans.push(region); continue; }
39
+ if (node.e2e) { stackOnly.push({ region, node }); continue; }
32
40
  if (!groups.has(node.id)) groups.set(node.id, { node, regions: [] });
33
41
  groups.get(node.id).regions.push(region);
34
42
  }
35
- return { groups: [...groups.values()], orphans };
43
+ return { groups: [...groups.values()], orphans, stackOnly };
36
44
  }
@@ -5,7 +5,7 @@ import { blame, parseCompilerErrors, patchFile, switchClassName, switchSource, S
5
5
  import { md5Of, md5OfFile, openSnapshot, restoreSnapshot, trackGenerated } from './snapshot.js';
6
6
  import { parseBuiltDlls } from './commands.js';
7
7
  import { firstLines, methodOf } from './trx.js';
8
- import { nearestProjectOf } from '../tasks/dotnet-test-introspection.js';
8
+ import { nearestProjectOf } from './dotnet-introspection.js';
9
9
 
10
10
  const posix = (p) => p.replace(/\\/g, '/');
11
11
  const MAX_SCHEMATA_ATTEMPTS = 3;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
2
  import { dirname, join, relative, resolve } from 'node:path';
3
- import { csharpClasses } from '../tasks/dotnet-test-introspection.js';
3
+ import { csharpClasses } from './dotnet-introspection.js';
4
4
  import { relatedCommand } from './commands.js';
5
5
 
6
6
  const SKIP_DIRS = new Set(['bin', 'obj', 'node_modules', '.git', 'worktrees', 'TestResults']);
@@ -24,18 +24,40 @@ function walkCs(dir, out = []) {
24
24
 
25
25
  const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
26
26
 
27
+ export function mutatedTypesOf(root, files) {
28
+ const types = new Set();
29
+ for (const file of files) {
30
+ for (const t of csharpClasses(resolve(root, file)).types) if (t.length >= MIN_TYPE_LENGTH) types.add(t);
31
+ }
32
+ return [...types];
33
+ }
34
+
35
+ const mentionOf = (types) => (types.length > 0 ? new RegExp(`\\b(?:${types.map(escapeRe).join('|')})\\b`) : null);
36
+
37
+ /** Test files (a test attribute in the code) under `testProjectDirs` naming one of `types` as a whole word. */
38
+ export function testFilesCiting({ root, testProjectDirs, types, exclude = [] }) {
39
+ const mention = mentionOf(types);
40
+ if (mention === null) return [];
41
+ const skip = new Set(exclude.map(posix));
42
+ const found = [];
43
+ for (const dir of testProjectDirs) {
44
+ for (const abs of walkCs(resolve(root, dir))) {
45
+ const rel = posix(relative(root, abs));
46
+ if (skip.has(rel)) continue;
47
+ const info = csharpClasses(abs);
48
+ if (info.scannable && info.hasTestAttribute && mention.test(readFileSync(abs, 'utf8'))) found.push(rel);
49
+ }
50
+ }
51
+ return found.sort();
52
+ }
53
+
27
54
  /**
28
55
  * The tests to run FIRST, chosen by the tool: files the feature changed, and files naming a
29
56
  * type the mutated files declare. A mutant the subset does not kill goes to the whole suite.
30
57
  */
31
58
  export function dotnetSubset({ root, testProjectDirs, mutatedFiles, changed = [] }) {
32
59
  const changedSet = new Set(changed.map(posix));
33
- const mutatedTypes = new Set();
34
- for (const file of mutatedFiles) {
35
- for (const t of csharpClasses(resolve(root, file)).types) if (t.length >= MIN_TYPE_LENGTH) mutatedTypes.add(t);
36
- }
37
- const mention = mutatedTypes.size > 0
38
- ? new RegExp(`\\b(?:${[...mutatedTypes].map(escapeRe).join('|')})\\b`) : null;
60
+ const mention = mentionOf(mutatedTypesOf(root, mutatedFiles));
39
61
 
40
62
  const byChange = new Set();
41
63
  const byMention = new Set();
@@ -70,6 +92,21 @@ export function jsSubset({ runner, command, root, mutatedFiles }) {
70
92
  return { usable: true, command: cmd, files: { related: files } };
71
93
  }
72
94
 
73
- export function testProjectDirsOf(root, csprojs) {
74
- return [...new Set(csprojs.map((c) => posix(relative(root, dirname(resolve(root, c))))))];
95
+ const SLN_PROJECT = /^Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/gim;
96
+ const SLNX_PROJECT = /<Project\b[^>]*\bPath="([^"]+\.csproj)"/gi;
97
+
98
+ function solutionProjects(solutionAbs) {
99
+ let text;
100
+ try { text = readFileSync(solutionAbs, 'utf8'); } catch { return []; }
101
+ const pattern = /\.slnx$/i.test(solutionAbs) ? SLNX_PROJECT : SLN_PROJECT;
102
+ return [...text.matchAll(pattern)].map((m) => resolve(dirname(solutionAbs), posix(m[1])));
103
+ }
104
+
105
+ /** A solution stands for every project it lists; one that lists none yields no directory. */
106
+ export function testProjectDirsOf(root, targets, cwd = root) {
107
+ const projects = targets.flatMap((t) => {
108
+ const abs = resolve(cwd, t);
109
+ return /\.slnx?$/i.test(t) ? solutionProjects(abs) : [abs];
110
+ });
111
+ return [...new Set(projects.map((p) => posix(relative(root, dirname(p)))))];
75
112
  }
@@ -0,0 +1,60 @@
1
+ /** The inline justification that lets a criterion talk about an artifact on purpose. */
2
+ export const ARTIFACT_MARKER_RE = /por\s+que\s+artefato\s*:/i;
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
+ */
8
+ const anyCase = (word) => [...word].map((c) => {
9
+ const lo = c.toLowerCase();
10
+ const up = c.toUpperCase();
11
+ return lo === up ? c : `[${lo}${up}]`;
12
+ }).join('');
13
+
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
+ */
20
+ const FORMAT_TOKEN = String.raw`\s+(?:[A-Z]{2,5}(?![\p{L}\d_])|${anyCase('formato')}(?![\p{L}]))`;
21
+
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
+ */
31
+ export const ARTIFACT_VOCABULARY = [
32
+ { term: 'grep', re: /(?<![\p{L}\d_])grep(?![\p{L}\d_])/iu },
33
+ { term: 'contém a string', re: /cont[ée]m\s+as?\s+strings?/iu },
34
+ { term: 'o binário', re: /(?<![\p{L}])o\s+bin[áa]rio(?![\p{L}])/iu },
35
+ { term: 'nenhum arquivo', re: /(?<![\p{L}])nenhum\s+arquivo(?![\p{L}])/iu },
36
+ {
37
+ term: 'arquivo/pasta/diretório em',
38
+ re: new RegExp(
39
+ String.raw`(?<![\p{L}])(?:${anyCase('arquivo')}|${anyCase('pasta')}|${anyCase('diret')}[óoÓO]${anyCase('rio')})` +
40
+ String.raw`[sS]?\s+${anyCase('em')}(?![\p{L}])(?!${FORMAT_TOKEN})`,
41
+ 'u',
42
+ ),
43
+ },
44
+ { term: 'glob **/', re: /\*\*\/|\/\*\*/ },
45
+ ];
46
+
47
+ /**
48
+ * Vocabulary terms matched in `text`, in vocabulary order, each at most once.
49
+ *
50
+ * @param {string} text
51
+ * @returns {string[]}
52
+ */
53
+ export function findArtifactTerms(text) {
54
+ const s = String(text ?? '');
55
+ const found = [];
56
+ for (const { term, re } of ARTIFACT_VOCABULARY) {
57
+ if (re.test(s)) found.push(term);
58
+ }
59
+ return found;
60
+ }