@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
@@ -0,0 +1,267 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, relative } from 'path';
3
+ import { getProjectTags } from '../stack-filter.js';
4
+ import { PACKAGE_LOCATIONS } from '../detectors/index.js';
5
+ import { isTestPath } from '../graph/sanitize.js';
6
+ import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
7
+
8
+ export const SEVERITIES = ['Bloqueante', 'Alto', 'Médio', 'Baixo'];
9
+ export const MAX_FILES = 20000;
10
+
11
+ const SKIP_DIRS = new Set([
12
+ 'node_modules', '.git', 'bin', 'obj', 'dist', 'build', 'out', 'coverage', '.next', '.morph', '.claude', '.vs', '.idea',
13
+ ...WORKTREE_DIR_NAMES,
14
+ ]);
15
+ const SOURCE_EXT = /\.(cs|csproj|sln|props|ts|tsx)$/i;
16
+ const DOTNET_EXPECTED_MAJOR = 10;
17
+ const WHERE_LIMIT = 5;
18
+
19
+ function walk(root) {
20
+ const files = [];
21
+ const dirs = new Set();
22
+ let unreadable = 0;
23
+ let truncated = false;
24
+ const stack = [root];
25
+ while (stack.length > 0) {
26
+ const dir = stack.pop();
27
+ let entries;
28
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { unreadable++; continue; }
29
+ for (const e of entries) {
30
+ const full = join(dir, e.name);
31
+ if (e.isDirectory()) {
32
+ if (SKIP_DIRS.has(e.name)) continue;
33
+ dirs.add(relative(root, full).replace(/\\/g, '/'));
34
+ stack.push(full);
35
+ } else if (SOURCE_EXT.test(e.name) || e.name === 'package.json') {
36
+ if (files.length >= MAX_FILES) { truncated = true; continue; }
37
+ files.push(relative(root, full).replace(/\\/g, '/'));
38
+ }
39
+ }
40
+ }
41
+ return { files, dirs, unreadable, truncated };
42
+ }
43
+
44
+ function reader(root) {
45
+ const failed = new Set();
46
+ const read = (file) => {
47
+ try { return readFileSync(join(root, file), 'utf8'); } catch { failed.add(file); return null; }
48
+ };
49
+ return { read, failed };
50
+ }
51
+
52
+ const majorOf = (version) => {
53
+ const m = /(\d+)/.exec(String(version ?? ''));
54
+ return m ? Number(m[1]) : null;
55
+ };
56
+
57
+ function dotnetPackages(files, read) {
58
+ const packages = new Map();
59
+ for (const f of files.filter((x) => /\.(csproj|props)$/i.test(x))) {
60
+ const text = read(f);
61
+ if (!text) continue;
62
+ for (const m of text.matchAll(/<Package(?:Reference|Version)\s+Include="([^"]+)"(?:\s+Version="([^"]*)")?/g)) {
63
+ if (!packages.has(m[1]) || m[2]) packages.set(m[1], m[2] ?? packages.get(m[1]) ?? null);
64
+ }
65
+ }
66
+ return packages;
67
+ }
68
+
69
+ function grepFiles(files, read, re) {
70
+ const hits = [];
71
+ for (const f of files) {
72
+ const text = read(f);
73
+ if (text && re.test(text)) hits.push(f);
74
+ }
75
+ return hits;
76
+ }
77
+
78
+ function auditDotnet(files, read) {
79
+ const rows = [];
80
+ const deviations = [];
81
+ const add = (severity, rule, where, migration) => deviations.push({ severity, stack: 'dotnet', rule, where, migration });
82
+ const projects = files.filter((f) => f.endsWith('.csproj'));
83
+ const source = files.filter((f) => f.endsWith('.cs') && !isTestPath(f));
84
+
85
+ const frameworks = new Map();
86
+ for (const p of projects) {
87
+ const m = /<TargetFrameworks?>([^<]+)</.exec(read(p) ?? '');
88
+ if (m) for (const tf of m[1].split(';').map((s) => s.trim()).filter(Boolean)) frameworks.set(tf, [...(frameworks.get(tf) ?? []), p]);
89
+ }
90
+ const tfs = [...frameworks.keys()];
91
+ const old = tfs.filter((tf) => /^net\d/.test(tf) && majorOf(tf) < DOTNET_EXPECTED_MAJOR);
92
+ rows.push({ layer: 'Runtime', detected: tfs.join(', ') || 'nenhum TargetFramework', expected: `net${DOTNET_EXPECTED_MAJOR}.0`, ok: tfs.length > 0 && old.length === 0 });
93
+ for (const tf of old) add('Bloqueante', `Runtime diferente (${tf})`, frameworks.get(tf), `subir o TargetFramework para net${DOTNET_EXPECTED_MAJOR}.0`);
94
+
95
+ const packages = dotnetPackages(files, read);
96
+ const has = (re) => [...packages.keys()].filter((p) => re.test(p));
97
+ const ef = has(/^Microsoft\.EntityFrameworkCore/);
98
+ const npgsql = has(/Npgsql/);
99
+ const otherDb = has(/^(Microsoft\.EntityFrameworkCore\.SqlServer|Pomelo\.EntityFrameworkCore\.MySql|MySql\.|MySqlConnector|Microsoft\.Data\.SqlClient|System\.Data\.SqlClient)/);
100
+ rows.push({ layer: 'ORM', detected: ef.length > 0 ? 'EF Core' : (packages.has('Dapper') ? 'Dapper' : 'ausente'), expected: 'EF Core', ok: ef.length > 0 });
101
+ rows.push({ layer: 'Banco', detected: npgsql.length > 0 ? 'Postgres (Npgsql)' : (otherDb.join(', ') || 'ausente'), expected: 'Neon (Postgres)', ok: npgsql.length > 0 && otherDb.length === 0 });
102
+ rows.push({ layer: 'IA', detected: has(/^Microsoft\.(Agents\.AI|Extensions\.AI)/).join(', ') || 'ausente', expected: 'MAF quando houver agentes', ok: true });
103
+ rows.push({ layer: 'Jobs', detected: has(/^Hangfire/).length > 0 ? 'Hangfire' : 'ausente', expected: 'Hangfire só com razão real', ok: true });
104
+ if (packages.has('Dapper') && ef.length === 0) add('Bloqueante', 'ORM diferente (Dapper puro sem EF Core)', projects, 'migrar o acesso a dados para EF Core');
105
+ if (otherDb.length > 0) add('Bloqueante', `Banco diferente (${otherDb.join(', ')})`, projects, 'migrar para Neon (Postgres, Npgsql)');
106
+ for (const p of has(/^Microsoft\.(EntityFrameworkCore|AspNetCore|Extensions)(?!\.AI)/)) {
107
+ const major = majorOf(packages.get(p));
108
+ if (major !== null && major < DOTNET_EXPECTED_MAJOR) add('Baixo', `Pacote desatualizado mas compatível (${p} ${packages.get(p)})`, [], `subir para ${DOTNET_EXPECTED_MAJOR}.x`);
109
+ }
110
+
111
+ const newGuid = grepFiles(source, read, /Guid\.NewGuid\(\)/);
112
+ if (newGuid.length > 0) add('Alto', '`Guid.NewGuid()`', newGuid, 'trocar por `Guid.CreateVersion7()`');
113
+ const repository = grepFiles(source, read, /interface\s+(IRepository\s*<|IUnitOfWork\b)/);
114
+ if (repository.length > 0) {
115
+ const di = grepFiles(source, read, /Add(Scoped|Transient|Singleton)\s*(<\s*(IRepository|IUnitOfWork)|\(\s*typeof\s*\(\s*(IRepository|IUnitOfWork))/);
116
+ add('Alto', 'Repository genérico (`IRepository<T>`/`IUnitOfWork`)', [...repository, ...di],
117
+ 'código morto: deletar a pasta `Repository/` e as 2 linhas de DI; confirme os consumidores com `morph-spec graph affected IRepository --uses-only`');
118
+ }
119
+ const serviceLayer = source.filter((f) => {
120
+ const text = read(f);
121
+ return text && /\bclass\s+\w+(Service|Manager)\b/.test(text) && /DbContext/.test(text);
122
+ });
123
+ if (serviceLayer.length > 0) add('Alto', 'Camada de Service/Manager entre handler e dados', serviceLayer, 'o handler fala com o `ApplicationDbContext` direto');
124
+ const rawSql = grepFiles(source, read, /\b(FromSqlRaw|ExecuteSqlRaw|SqlQueryRaw|ExecuteQuery)\s*\(/);
125
+ if (rawSql.length > 0) add('Alto', 'SQL cru', rawSql, 'LINQ, ou store dedicado com justificativa em ADR');
126
+ const notSealed = source.filter((f) => {
127
+ const text = read(f) ?? '';
128
+ return [...text.matchAll(/^\s*(?:public|internal)\s+((?:\w+\s+)*)class\s+\w+/gm)]
129
+ .some((m) => !/\b(sealed|abstract|static|partial)\b/.test(m[1]));
130
+ });
131
+ if (notSealed.length > 0) add('Médio', 'Classes não-sealed', notSealed, '`sealed` por padrão');
132
+ const controllers = grepFiles(source, read, /:\s*Controller(Base)?\b/);
133
+ rows.push({ layer: 'Estrutura', detected: controllers.length > 0 ? `${controllers.length} controller(s) MVC` : 'sem controllers', expected: 'VSA (endpoint por slice)', ok: controllers.length === 0 });
134
+ return { rows, deviations };
135
+ }
136
+
137
+ const UI_LIBRARIES = ['@mui/material', 'antd', '@chakra-ui/react', 'bootstrap', 'react-bootstrap', '@mantine/core'];
138
+
139
+ function auditNextjs(root, read) {
140
+ const rows = [];
141
+ const deviations = [];
142
+ const add = (severity, rule, where, migration) => deviations.push({ severity, stack: 'nextjs', rule, where, migration });
143
+ const packages = PACKAGE_LOCATIONS
144
+ .map((loc) => ({ loc, file: loc === '.' ? 'package.json' : `${loc}/package.json` }))
145
+ .filter(({ file }) => existsSync(join(root, file)))
146
+ .map((p) => {
147
+ const text = read(p.file);
148
+ let deps = null;
149
+ try { const pkg = JSON.parse(text); deps = { ...pkg.dependencies, ...pkg.devDependencies }; } catch { deps = null; }
150
+ return { ...p, deps };
151
+ })
152
+ .filter((p) => p.deps);
153
+ const next = packages.find((p) => p.deps.next);
154
+ if (!next) {
155
+ const other = packages.find((p) => p.deps.react || p.deps.vue);
156
+ rows.push({ layer: 'Frontend', detected: other ? (other.deps.vue ? 'Vue' : 'React sem Next.js') : 'sem frontend', expected: 'Next.js App Router', ok: !other });
157
+ if (other) add('Bloqueante', 'Frontend sem Next.js', [other.file], 'migrar para Next.js App Router');
158
+ return { rows, deviations };
159
+ }
160
+ const base = next.loc === '.' ? '' : `${next.loc}/`;
161
+ const isDir = (p) => { try { return statSync(join(root, p)).isDirectory(); } catch { return false; } };
162
+ const appRouter = isDir(`${base}app`) || isDir(`${base}src/app`);
163
+ const pagesRouter = isDir(`${base}pages`) || isDir(`${base}src/pages`);
164
+ rows.push({ layer: 'Frontend', detected: `Next.js ${next.deps.next}`, expected: 'Next.js App Router', ok: true });
165
+ rows.push({ layer: 'Roteamento', detected: appRouter ? 'App Router' : (pagesRouter ? 'Pages Router' : 'não detectado'), expected: 'App Router', ok: appRouter || !pagesRouter });
166
+ if (pagesRouter && !appRouter) add('Médio', 'Pages Router em vez de App Router', [next.file], 'migrar as rotas para `app/`');
167
+ rows.push({ layer: 'Estilo', detected: next.deps.tailwindcss ? 'Tailwind' : 'ausente', expected: 'Tailwind', ok: Boolean(next.deps.tailwindcss) });
168
+ rows.push({ layer: 'Motion', detected: next.deps['framer-motion'] || next.deps.motion ? 'Framer Motion' : 'ausente', expected: 'Framer Motion', ok: true });
169
+ const shadcn = existsSync(join(root, base, 'components.json'));
170
+ const otherUi = UI_LIBRARIES.filter((lib) => next.deps[lib]);
171
+ rows.push({ layer: 'UI', detected: otherUi.join(', ') || (shadcn ? 'shadcn' : 'ausente'), expected: 'shadcn', ok: otherUi.length === 0 });
172
+ if (otherUi.length > 0) add('Médio', `Biblioteca de UI diferente (${otherUi.join(', ')})`, [next.file], 'shadcn sobre Tailwind');
173
+ return { rows, deviations };
174
+ }
175
+
176
+ const AUDITS = { dotnet: auditDotnet, nextjs: auditNextjs };
177
+
178
+ /**
179
+ * Deterministic stack audit of the tree at `root`, driven by `config.project.stack`:
180
+ * only the configured stacks are judged; no configured stack means no rule runs.
181
+ *
182
+ * @param {string} root
183
+ * @param {object|null} config the parsed `.morph/config/config.json`
184
+ */
185
+ export function scanStack(root, config) {
186
+ const stack = typeof config?.project?.stack === 'string' ? config.project.stack : null;
187
+ const tags = getProjectTags(config);
188
+ const { files, unreadable, truncated } = walk(root);
189
+ const { read, failed } = reader(root);
190
+ const hasDotnet = files.some((f) => /\.(csproj|sln|cs)$/i.test(f));
191
+ const hasNode = files.some((f) => f.endsWith('package.json') || /\.(ts|tsx)$/i.test(f));
192
+ const greenfield = !hasDotnet && !hasNode;
193
+
194
+ const rows = [];
195
+ const deviations = [];
196
+ const notes = [];
197
+ if (!greenfield) {
198
+ for (const tag of tags) {
199
+ const audit = AUDITS[tag];
200
+ if (!audit) continue;
201
+ const result = tag === 'nextjs' ? audit(root, read) : audit(files, read);
202
+ rows.push(...result.rows);
203
+ deviations.push(...result.deviations);
204
+ }
205
+ if (hasDotnet && !tags.includes('dotnet')) notes.push('Há código .NET, mas `dotnet` não está em `project.stack` — não auditado.');
206
+ if (hasNode && !tags.includes('nextjs') && files.some((f) => f.endsWith('package.json'))) notes.push('Há `package.json`, mas `nextjs` não está em `project.stack` — não auditado.');
207
+ }
208
+ const unjudged = tags.filter((t) => !AUDITS[t]);
209
+ if (unjudged.length > 0) notes.push(`Sem regras de varredura para: ${unjudged.join(', ')}.`);
210
+ if (truncated) notes.push(`Varredura parou em ${MAX_FILES} arquivos.`);
211
+ if (unreadable + failed.size > 0) notes.push(`${unreadable + failed.size} arquivo(s) ou pasta(s) ilegível(is) ignorado(s).`);
212
+
213
+ deviations.sort((a, b) => SEVERITIES.indexOf(a.severity) - SEVERITIES.indexOf(b.severity));
214
+ const audited = tags.some((t) => AUDITS[t]);
215
+ return { stack, tags, greenfield, audited, rows, deviations, notes, filesScanned: files.length };
216
+ }
217
+
218
+ const cell = (s) => String(s).replace(/\|/g, '\\|');
219
+
220
+ function whereText(where) {
221
+ if (!where || where.length === 0) return '';
222
+ const unique = [...new Set(where)];
223
+ const shown = unique.slice(0, WHERE_LIMIT).map((w) => `\`${w}\``).join(', ');
224
+ return ` — onde: ${shown}${unique.length > WHERE_LIMIT ? ` (+${unique.length - WHERE_LIMIT})` : ''} (${unique.length} arquivo(s))`;
225
+ }
226
+
227
+ export function notAuditedReason(result) {
228
+ return result.stack
229
+ ? `não auditado: nenhuma regra de varredura para \`${result.stack}\``
230
+ : 'não auditado: project.stack ausente em .morph/config/config.json';
231
+ }
232
+
233
+ /** The scan as the markdown report the morph-plan skill reads. */
234
+ export function renderStackScan(result, title) {
235
+ const out = [`# Stack Scan — ${title}`, ''];
236
+ out.push(`**Stack configurado:** ${result.stack ? `\`${result.stack}\`` : 'ausente em `project.stack` — nenhuma regra aplicada'}`);
237
+ const blocking = result.deviations.filter((d) => d.severity === 'Bloqueante' || d.severity === 'Alto');
238
+ const status = result.greenfield ? 'GREENFIELD (sem código fonte)'
239
+ : !result.audited ? notAuditedReason(result)
240
+ : result.deviations.length === 0 ? 'Em compliance' : `Desvios detectados (${result.deviations.length})`;
241
+ out.push(`**Resultado:** ${status}`, '');
242
+
243
+ if (result.rows.length > 0) {
244
+ out.push('## Stack detectado', '', '| Camada | Detectado | Esperado | Status |', '|---|---|---|---|');
245
+ for (const r of result.rows) out.push(`| ${cell(r.layer)} | ${cell(r.detected)} | ${cell(r.expected)} | ${r.ok ? '✓' : '✗'} |`);
246
+ out.push('');
247
+ }
248
+ if (result.deviations.length > 0) {
249
+ out.push('## Desvios (por severidade)', '');
250
+ for (const severity of SEVERITIES) {
251
+ const list = result.deviations.filter((d) => d.severity === severity);
252
+ if (list.length === 0) continue;
253
+ out.push(`### ${severity}`, '');
254
+ for (const d of list) out.push(`- ${d.rule}${whereText(d.where)} — migração: ${d.migration}`);
255
+ out.push('');
256
+ }
257
+ }
258
+ out.push('## Notas', '');
259
+ out.push('- Contagens por varredura de texto (grep): superestimam consumidores, porque não separam herança de uso. Para um símbolo, `morph-spec graph affected <símbolo> --uses-only`.');
260
+ for (const n of result.notes) out.push(`- ${n}`);
261
+ out.push('', '## Recomendação', '');
262
+ if (result.greenfield) out.push('Compliance trivial: aplicar o stack padrão de `MORPH.md`.');
263
+ else if (!result.audited) out.push('Nenhuma regra rodou, então nada aqui atesta compliance: declare `project.stack` e rode o stack-scan de novo.');
264
+ else if (blocking.length > 0) out.push('Há desvio Bloqueante ou Alto: entra na árvore de decisões como "a migração entra nesta feature ou vira feature separada?".');
265
+ else out.push('Nenhum desvio Bloqueante ou Alto: prossiga com o plano.');
266
+ return `${out.join('\n')}\n`;
267
+ }
@@ -0,0 +1,43 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+
4
+ export const STANDARDS_FILE = 'STANDARDS.json';
5
+ export const STANDARDS_META_FILE = 'STANDARDS.meta.json';
6
+
7
+ /** Per-standard fields that live in the sidecar, keyed by id: no runtime reader needs them to pick a standard. */
8
+ export const META_FIELDS = ['aliases', 'verifiedAgainst', 'provedBy'];
9
+
10
+ export function metaPathFor(standardsJsonPath) {
11
+ return join(dirname(standardsJsonPath), STANDARDS_META_FILE);
12
+ }
13
+
14
+ function readMeta(standardsJsonPath) {
15
+ const path = metaPathFor(standardsJsonPath);
16
+ if (!existsSync(path)) return { standards: {}, sharedAliases: {} };
17
+ try {
18
+ const meta = JSON.parse(readFileSync(path, 'utf8'));
19
+ return { standards: meta?.standards || {}, sharedAliases: meta?.sharedAliases || {} };
20
+ } catch {
21
+ return { standards: {}, sharedAliases: {} };
22
+ }
23
+ }
24
+
25
+ /**
26
+ * The registry as every reader sees it: runtime entries with their sidecar
27
+ * fields merged back by id. A missing or unreadable sidecar degrades to "no
28
+ * aliases"; a missing or unreadable STANDARDS.json throws.
29
+ * @param {string} standardsJsonPath
30
+ * @returns {{ version: string|null, standards: object[], sharedAliases: Record<string, string[]> }}
31
+ */
32
+ export function loadStandardsRegistry(standardsJsonPath) {
33
+ const runtime = JSON.parse(readFileSync(standardsJsonPath, 'utf8'));
34
+ const meta = readMeta(standardsJsonPath);
35
+ const standards = (Array.isArray(runtime.standards) ? runtime.standards : []).map((s) => {
36
+ const extra = meta.standards[s.id];
37
+ if (!extra) return s;
38
+ const merged = { ...s };
39
+ for (const field of META_FIELDS) if (extra[field] !== undefined) merged[field] = extra[field];
40
+ return merged;
41
+ });
42
+ return { version: runtime.version ?? null, standards, sharedAliases: meta.sharedAliases };
43
+ }
@@ -0,0 +1,58 @@
1
+ // The literal regex of the `github-slugger` package: anchors are read off the rendered .md.
2
+ const SLUG_STRIP = /[ -⸀-⹿\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~ ]/g;
3
+
4
+ export function ghSlug(heading) {
5
+ return heading.trim().toLowerCase().replace(SLUG_STRIP, '').replace(/ /g, '-');
6
+ }
7
+
8
+ /**
9
+ * Every heading outside code fences, with the slug GitHub gives it (`-1`, `-2`
10
+ * for repeats) and the line it sits on.
11
+ * @param {string} text
12
+ * @returns {Array<{ level: number, title: string, slug: string, line: number }>}
13
+ */
14
+ export function markdownHeadings(text) {
15
+ const out = [];
16
+ const seen = new Map();
17
+ let inFence = false;
18
+ const lines = text.split('\n');
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const line = lines[i];
21
+ if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
22
+ if (inFence) continue;
23
+ const m = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line.replace(/\r$/, ''));
24
+ if (!m) continue;
25
+ let slug = ghSlug(m[2]);
26
+ if (seen.has(slug)) {
27
+ const n = seen.get(slug) + 1;
28
+ seen.set(slug, n);
29
+ slug = `${slug}-${n}`;
30
+ } else {
31
+ seen.set(slug, 0);
32
+ }
33
+ out.push({ level: m[1].length, title: m[2], slug, line: i });
34
+ }
35
+ return out;
36
+ }
37
+
38
+ export function headingSlugs(text) {
39
+ return new Set(markdownHeadings(text).map((h) => h.slug));
40
+ }
41
+
42
+ /**
43
+ * The section a slug opens: its heading through the line before the next
44
+ * heading of the same or a higher level. `null` when no heading has the slug.
45
+ * @param {string} text
46
+ * @param {string} slug - with or without the leading `#`
47
+ * @returns {string|null}
48
+ */
49
+ export function sectionBySlug(text, slug) {
50
+ const want = String(slug).replace(/^#/, '');
51
+ const all = markdownHeadings(text);
52
+ const idx = all.findIndex((h) => h.slug === want);
53
+ if (idx === -1) return null;
54
+ const start = all[idx];
55
+ const next = all.slice(idx + 1).find((h) => h.level <= start.level);
56
+ const lines = text.split('\n');
57
+ return lines.slice(start.line, next ? next.line : lines.length).join('\n').replace(/\s+$/, '') + '\n';
58
+ }
@@ -183,26 +183,18 @@ export function readEvalStamp(cacheRoot) {
183
183
  *
184
184
  * **There is no `skip` for a bad cache.** A skip here would recreate the hole
185
185
  * this harness closed twice: a green gate over a verification that never
186
- * happened. The only skip is scope — an eval suite does not belong in the
187
- * per-task loop.
186
+ * happened.
188
187
  *
189
188
  * @param {string} projectPath
190
189
  * @param {object} node - a resolved test node with `runner === 'evals'`
191
- * @param {{taskId?: string|null, refresh?: boolean}} [options]
192
- * @returns {{status: 'skip'|'fail'|null, reason: string|null}} status null means RUN
190
+ * @param {{refresh?: boolean}} [options]
191
+ * @returns {{status: 'fail'|null, reason: string|null}} status null means RUN
193
192
  */
194
193
  export function evalPreflight(projectPath, node, options = {}) {
195
- const { taskId = null, refresh = false } = options;
194
+ const { refresh = false } = options;
196
195
 
197
196
  if (node?.runner !== EVAL_RUNNER) return { status: null, reason: null };
198
197
 
199
- // Scope first: an eval suite does not pay for itself in a per-task loop, and
200
- // the result Gate 3 reads is the feature-scoped one. Same treatment `e2e: true`
201
- // already gets.
202
- if (taskId) {
203
- return { status: 'skip', reason: 'eval node runs at feature scope only' };
204
- }
205
-
206
198
  const { cacheRoot, envFile } = resolveEvalPaths(node, projectPath);
207
199
 
208
200
  // In refresh the tree is being (re)written; demanding a populated, stamped
@@ -51,41 +51,6 @@ import { PACKAGE_LOCATIONS } from '../detectors/index.js';
51
51
  */
52
52
  export const TEST_RUNNERS = ['dotnet', 'vitest', 'jest', 'node-test', 'playwright', 'none', EVAL_RUNNER];
53
53
 
54
- /**
55
- * The task-group vocabulary a node can declare via `groups`, and that
56
- * `tasks.json` entries declare via `group` (framework/schemas/tasks.schema.json
57
- * `~:28`) — ONE enum, checked to agree by canary, never two that drift.
58
- * Single home for B3's task-scope selection (`test-scope.js`) to import from.
59
- */
60
- export const TASK_GROUPS = ['backend', 'frontend', 'tests', 'infra', 'docs'];
61
-
62
- /**
63
- * Which group(s) a node serves when it does NOT declare `groups` itself,
64
- * keyed by runner. `none` and `evals` are deliberately ABSENT — a command
65
- * nobody could read (`none`) and a declared eval suite (`evals`) have no
66
- * honest default group, so `effectiveGroups()` answers null for them rather
67
- * than guessing one.
68
- */
69
- export const RUNNER_DEFAULT_GROUPS = {
70
- dotnet: ['backend'],
71
- 'node-test': ['backend'],
72
- vitest: ['frontend'],
73
- jest: ['frontend'],
74
- playwright: ['frontend'],
75
- };
76
-
77
- /**
78
- * Which task group(s) this node answers to: what it DECLARED, or else its
79
- * runner's default. **null is "no mapping"**, never an invented guess — B3's
80
- * task-scope selection reads null as "this node is unscoped, always runs".
81
- *
82
- * @param {import('./test-nodes.js').TestNode} node
83
- * @returns {string[]|null}
84
- */
85
- export function effectiveGroups(node) {
86
- return node?.groups ?? RUNNER_DEFAULT_GROUPS[node?.runner] ?? null;
87
- }
88
-
89
54
  /**
90
55
  * A node id is what `--filter` matches and what keys `runs[]`. Lowercase and
91
56
  * dash-only so it survives a shell argument and a JSON key without quoting.
@@ -209,11 +174,6 @@ function makeNode(partial) {
209
174
  // eval node"** — distinct from an eval node with an empty block, which
210
175
  // `validateTestNodes` already refused.
211
176
  evals: partial.evals ?? null,
212
- // Declared task-group(s) this node serves (§B3 task-scope selection).
213
- // **null is "not declared"**, distinct from an empty array (which
214
- // `firstProblem` refuses outright) — effectiveGroups() falls back to the
215
- // runner's default only when this is null.
216
- groups: partial.groups ?? null,
217
177
  status: partial.status ?? null,
218
178
  reason: partial.reason ?? null,
219
179
  };
@@ -296,28 +256,6 @@ function firstProblem(raw, projectPath) {
296
256
  return `evals applies only to runner "${EVAL_RUNNER}"`;
297
257
  }
298
258
 
299
- if (raw.groups !== undefined) {
300
- if (!Array.isArray(raw.groups)) {
301
- return 'groups must be a non-empty array';
302
- }
303
- if (raw.groups.length === 0) {
304
- return 'groups must be a non-empty array';
305
- }
306
- const invalid = raw.groups.filter((g) => !TASK_GROUPS.includes(g));
307
- if (invalid.length > 0) {
308
- return `groups must contain only ${TASK_GROUPS.join(' | ')} (invalid: ${invalid.join(', ')})`;
309
- }
310
- const seenGroups = new Set();
311
- const duplicates = new Set();
312
- for (const g of raw.groups) {
313
- if (seenGroups.has(g)) duplicates.add(g);
314
- seenGroups.add(g);
315
- }
316
- if (duplicates.size > 0) {
317
- return `groups must not contain duplicates (repeated: ${[...duplicates].join(', ')})`;
318
- }
319
- }
320
-
321
259
  // The cwd checks come last because they are the expensive ones, and because
322
260
  // a node that is malformed in shape has a better error to report first.
323
261
  const cwd = typeof raw.cwd === 'string' ? raw.cwd.trim() : '.';
@@ -390,7 +328,6 @@ export function validateTestNodes(config, projectPath = null) {
390
328
  e2e: entry.e2e ?? false,
391
329
  linuxImage: entry.linuxImage ?? null,
392
330
  evals: entry.evals ?? null,
393
- groups: entry.groups ?? null,
394
331
  projectCount: 1,
395
332
  });
396
333
  });
@@ -586,14 +523,9 @@ export function morphConfigPath(projectPath = '.') {
586
523
  * the declaration the command came to update);
587
524
  * - a declared node with no measurement (a baseline recorded for part of a
588
525
  * fleet says nothing true about the rest of it);
589
- * - a measurement carrying a task-scope `selection` (#87): narrowed, scoped
590
- * out or run whole INSIDE a task-scoped verify, it measured a recorte, and
591
- * `expectedCount` is the whole suite's number. `verify` already refuses
592
- * `--record-baseline` with a task id before the build; this is the second
593
- * barrier, at the writer, so no caller can route around it.
594
526
  *
595
527
  * @param {string} projectPath - Project root
596
- * @param {Array<{id: string, count: number|null, selection?: object|null}>} measurements - One per node that ran
528
+ * @param {Array<{id: string, count: number|null}>} measurements - One per node that ran
597
529
  * @returns {{ recorded: boolean, reason: string|null, updates: Array<{id: string, from: number|null, to: number}>, path: string }}
598
530
  */
599
531
  export function recordTestBaselines(projectPath, measurements) {
@@ -614,11 +546,6 @@ export function recordTestBaselines(projectPath, measurements) {
614
546
  return refuse('config.json não declara project.tests[] — não há entrada onde gravar a baseline');
615
547
  }
616
548
 
617
- const recortes = measurements.filter((m) => m.selection != null).map((m) => m.id);
618
- if (recortes.length > 0) {
619
- return refuse(`medição de recorte de task em: ${recortes.join(', ')} — baseline só de execução inteira, sem {task}`);
620
- }
621
-
622
549
  const unreadable = measurements.filter((m) => !Number.isInteger(m.count)).map((m) => m.id);
623
550
  if (unreadable.length > 0) {
624
551
  return refuse(`contagem desconhecida em: ${unreadable.join(', ')} — desconhecido nunca vira 0`);