@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -0,0 +1,260 @@
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
+ /** Only the stacks in `config.project.stack` are judged; none configured means no rule runs. */
179
+ export function scanStack(root, config) {
180
+ const stack = typeof config?.project?.stack === 'string' ? config.project.stack : null;
181
+ const tags = getProjectTags(config);
182
+ const { files, unreadable, truncated } = walk(root);
183
+ const { read, failed } = reader(root);
184
+ const hasDotnet = files.some((f) => /\.(csproj|sln|cs)$/i.test(f));
185
+ const hasNode = files.some((f) => f.endsWith('package.json') || /\.(ts|tsx)$/i.test(f));
186
+ const greenfield = !hasDotnet && !hasNode;
187
+
188
+ const rows = [];
189
+ const deviations = [];
190
+ const notes = [];
191
+ if (!greenfield) {
192
+ for (const tag of tags) {
193
+ const audit = AUDITS[tag];
194
+ if (!audit) continue;
195
+ const result = tag === 'nextjs' ? audit(root, read) : audit(files, read);
196
+ rows.push(...result.rows);
197
+ deviations.push(...result.deviations);
198
+ }
199
+ if (hasDotnet && !tags.includes('dotnet')) notes.push('Há código .NET, mas `dotnet` não está em `project.stack` — não auditado.');
200
+ 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.');
201
+ }
202
+ const unjudged = tags.filter((t) => !AUDITS[t]);
203
+ if (unjudged.length > 0) notes.push(`Sem regras de varredura para: ${unjudged.join(', ')}.`);
204
+ if (truncated) notes.push(`Varredura parou em ${MAX_FILES} arquivos.`);
205
+ if (unreadable + failed.size > 0) notes.push(`${unreadable + failed.size} arquivo(s) ou pasta(s) ilegível(is) ignorado(s).`);
206
+
207
+ deviations.sort((a, b) => SEVERITIES.indexOf(a.severity) - SEVERITIES.indexOf(b.severity));
208
+ const audited = tags.some((t) => AUDITS[t]);
209
+ return { stack, tags, greenfield, audited, rows, deviations, notes, filesScanned: files.length };
210
+ }
211
+
212
+ const cell = (s) => String(s).replace(/\|/g, '\\|');
213
+
214
+ function whereText(where) {
215
+ if (!where || where.length === 0) return '';
216
+ const unique = [...new Set(where)];
217
+ const shown = unique.slice(0, WHERE_LIMIT).map((w) => `\`${w}\``).join(', ');
218
+ return ` — onde: ${shown}${unique.length > WHERE_LIMIT ? ` (+${unique.length - WHERE_LIMIT})` : ''} (${unique.length} arquivo(s))`;
219
+ }
220
+
221
+ export function notAuditedReason(result) {
222
+ return result.stack
223
+ ? `não auditado: nenhuma regra de varredura para \`${result.stack}\``
224
+ : 'não auditado: project.stack ausente em .morph/config/config.json';
225
+ }
226
+
227
+ export function renderStackScan(result, title) {
228
+ const out = [`# Stack Scan — ${title}`, ''];
229
+ out.push(`**Stack configurado:** ${result.stack ? `\`${result.stack}\`` : 'ausente em `project.stack` — nenhuma regra aplicada'}`);
230
+ const blocking = result.deviations.filter((d) => d.severity === 'Bloqueante' || d.severity === 'Alto');
231
+ const status = result.greenfield ? 'GREENFIELD (sem código fonte)'
232
+ : !result.audited ? notAuditedReason(result)
233
+ : result.deviations.length === 0 ? 'Em compliance' : `Desvios detectados (${result.deviations.length})`;
234
+ out.push(`**Resultado:** ${status}`, '');
235
+
236
+ if (result.rows.length > 0) {
237
+ out.push('## Stack detectado', '', '| Camada | Detectado | Esperado | Status |', '|---|---|---|---|');
238
+ for (const r of result.rows) out.push(`| ${cell(r.layer)} | ${cell(r.detected)} | ${cell(r.expected)} | ${r.ok ? '✓' : '✗'} |`);
239
+ out.push('');
240
+ }
241
+ if (result.deviations.length > 0) {
242
+ out.push('## Desvios (por severidade)', '');
243
+ for (const severity of SEVERITIES) {
244
+ const list = result.deviations.filter((d) => d.severity === severity);
245
+ if (list.length === 0) continue;
246
+ out.push(`### ${severity}`, '');
247
+ for (const d of list) out.push(`- ${d.rule}${whereText(d.where)} — migração: ${d.migration}`);
248
+ out.push('');
249
+ }
250
+ }
251
+ out.push('## Notas', '');
252
+ 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`.');
253
+ for (const n of result.notes) out.push(`- ${n}`);
254
+ out.push('', '## Recomendação', '');
255
+ if (result.greenfield) out.push('Compliance trivial: aplicar o stack padrão de `MORPH.md`.');
256
+ 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.');
257
+ 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?".');
258
+ else out.push('Nenhum desvio Bloqueante ou Alto: prossiga com o plano.');
259
+ return `${out.join('\n')}\n`;
260
+ }
@@ -1,14 +1,4 @@
1
- /**
2
- * Parse `stacks:` from YAML frontmatter content.
3
- * Returns ['*'] if no stacks field found (backwards compatible — always install).
4
- *
5
- * Supports:
6
- * stacks:\n - dotnet\n - blazor (YAML list)
7
- * stacks: [dotnet, blazor] (inline array)
8
- *
9
- * @param {string} content - Raw file content with YAML frontmatter
10
- * @returns {string[]}
11
- */
1
+ /** `['*']` without a `stacks:` field (always install); accepts a YAML list or an inline array. */
12
2
  export function parseStacks(content) {
13
3
  const normalized = content.replace(/\r\n/g, '\n');
14
4
  const fmMatch = normalized.match(/^---\n([\s\S]*?)\n---/);
@@ -16,9 +6,7 @@ export function parseStacks(content) {
16
6
 
17
7
  const fm = fmMatch[1];
18
8
 
19
- // Try inline format: stacks: [dotnet, blazor] — quotes stripped like the
20
- // list branch below; `["umbraco"]` sem isso parseava como a string literal
21
- // `"umbraco"` (com aspas) e nunca casava tag nenhuma.
9
+ // Quotes stripped like the list branch: `["umbraco"]` would otherwise never match a tag.
22
10
  const inlineMatch = fm.match(/^stacks:\s*\[([^\]]*)\]/m);
23
11
  if (inlineMatch) {
24
12
  const items = inlineMatch[1]
@@ -28,7 +16,6 @@ export function parseStacks(content) {
28
16
  return items.length > 0 ? items : ['*'];
29
17
  }
30
18
 
31
- // Try YAML list format: stacks:\n - dotnet\n - blazor
32
19
  const listMatch = fm.match(/^stacks:\s*\n((?:\s+-\s+.+\n?)*)/m);
33
20
  if (listMatch) {
34
21
  const items = listMatch[1]
@@ -41,12 +28,7 @@ export function parseStacks(content) {
41
28
  return ['*'];
42
29
  }
43
30
 
44
- /**
45
- * Extract project tags from config object.
46
- * Combines stack tags (from `project.stack` split by '-' or '+')
47
- * with integration tags (from `project.integrations` array).
48
- * Deduplicates the result.
49
- */
31
+ /** Stack tags (`project.stack` split by '-' or '+') plus `project.integrations`, deduplicated. */
50
32
  export function getProjectTags(config) {
51
33
  const stack = config?.project?.stack;
52
34
  const stackTags = (stack && typeof stack === 'string')
@@ -58,12 +40,7 @@ export function getProjectTags(config) {
58
40
  return [...new Set([...stackTags, ...integrations])];
59
41
  }
60
42
 
61
- /**
62
- * Determine if an asset should be installed for the given project tags.
63
- * - stacks includes '*' → always install
64
- * - stacks is empty → treat as '*' (backwards compatible)
65
- * - Otherwise → install if intersection with projectTags is non-empty
66
- */
43
+ /** Empty or '*' stacks always install; otherwise any intersection with the project tags. */
67
44
  export function shouldInstall(assetStacks, projectTags) {
68
45
  if (assetStacks.length === 0 || assetStacks.includes('*')) return true;
69
46
  return assetStacks.some(s => projectTags.includes(s));
@@ -0,0 +1,37 @@
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
+ /** Sidecar merged by id. Missing/unreadable sidecar = no aliases; missing STANDARDS.json throws. */
26
+ export function loadStandardsRegistry(standardsJsonPath) {
27
+ const runtime = JSON.parse(readFileSync(standardsJsonPath, 'utf8'));
28
+ const meta = readMeta(standardsJsonPath);
29
+ const standards = (Array.isArray(runtime.standards) ? runtime.standards : []).map((s) => {
30
+ const extra = meta.standards[s.id];
31
+ if (!extra) return s;
32
+ const merged = { ...s };
33
+ for (const field of META_FIELDS) if (extra[field] !== undefined) merged[field] = extra[field];
34
+ return merged;
35
+ });
36
+ return { version: runtime.version ?? null, standards, sharedAliases: meta.sharedAliases };
37
+ }
@@ -0,0 +1,47 @@
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
+ /** Headings outside code fences, repeated slugs suffixed `-1`, `-2` as GitHub does. */
9
+ export function markdownHeadings(text) {
10
+ const out = [];
11
+ const seen = new Map();
12
+ let inFence = false;
13
+ const lines = text.split('\n');
14
+ for (let i = 0; i < lines.length; i++) {
15
+ const line = lines[i];
16
+ if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
17
+ if (inFence) continue;
18
+ const m = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line.replace(/\r$/, ''));
19
+ if (!m) continue;
20
+ let slug = ghSlug(m[2]);
21
+ if (seen.has(slug)) {
22
+ const n = seen.get(slug) + 1;
23
+ seen.set(slug, n);
24
+ slug = `${slug}-${n}`;
25
+ } else {
26
+ seen.set(slug, 0);
27
+ }
28
+ out.push({ level: m[1].length, title: m[2], slug, line: i });
29
+ }
30
+ return out;
31
+ }
32
+
33
+ export function headingSlugs(text) {
34
+ return new Set(markdownHeadings(text).map((h) => h.slug));
35
+ }
36
+
37
+ /** Heading through the line before the next heading of same or higher level; slug may carry `#`. */
38
+ export function sectionBySlug(text, slug) {
39
+ const want = String(slug).replace(/^#/, '');
40
+ const all = markdownHeadings(text);
41
+ const idx = all.findIndex((h) => h.slug === want);
42
+ if (idx === -1) return null;
43
+ const start = all[idx];
44
+ const next = all.slice(idx + 1).find((h) => h.level <= start.level);
45
+ const lines = text.split('\n');
46
+ return lines.slice(start.line, next ? next.line : lines.length).join('\n').replace(/\s+$/, '') + '\n';
47
+ }