cap-pro 1.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 (275) hide show
  1. package/.claude-plugin/README.md +26 -0
  2. package/.claude-plugin/marketplace.json +24 -0
  3. package/.claude-plugin/plugin.json +24 -0
  4. package/LICENSE +21 -0
  5. package/README.ja-JP.md +834 -0
  6. package/README.ko-KR.md +823 -0
  7. package/README.md +806 -0
  8. package/README.pt-BR.md +452 -0
  9. package/README.zh-CN.md +800 -0
  10. package/agents/cap-architect.md +269 -0
  11. package/agents/cap-brainstormer.md +207 -0
  12. package/agents/cap-curator.md +276 -0
  13. package/agents/cap-debugger.md +365 -0
  14. package/agents/cap-designer.md +246 -0
  15. package/agents/cap-historian.md +464 -0
  16. package/agents/cap-migrator.md +291 -0
  17. package/agents/cap-prototyper.md +197 -0
  18. package/agents/cap-validator.md +308 -0
  19. package/bin/install.js +5433 -0
  20. package/cap/bin/cap-tools.cjs +853 -0
  21. package/cap/bin/lib/arc-scanner.cjs +344 -0
  22. package/cap/bin/lib/cap-affinity-engine.cjs +862 -0
  23. package/cap/bin/lib/cap-anchor.cjs +228 -0
  24. package/cap/bin/lib/cap-annotation-writer.cjs +340 -0
  25. package/cap/bin/lib/cap-checkpoint.cjs +434 -0
  26. package/cap/bin/lib/cap-cluster-detect.cjs +945 -0
  27. package/cap/bin/lib/cap-cluster-display.cjs +52 -0
  28. package/cap/bin/lib/cap-cluster-format.cjs +245 -0
  29. package/cap/bin/lib/cap-cluster-helpers.cjs +295 -0
  30. package/cap/bin/lib/cap-cluster-io.cjs +212 -0
  31. package/cap/bin/lib/cap-completeness.cjs +540 -0
  32. package/cap/bin/lib/cap-deps.cjs +583 -0
  33. package/cap/bin/lib/cap-design-families.cjs +332 -0
  34. package/cap/bin/lib/cap-design.cjs +966 -0
  35. package/cap/bin/lib/cap-divergence-detector.cjs +400 -0
  36. package/cap/bin/lib/cap-doctor.cjs +752 -0
  37. package/cap/bin/lib/cap-feature-map-internals.cjs +19 -0
  38. package/cap/bin/lib/cap-feature-map-migrate.cjs +335 -0
  39. package/cap/bin/lib/cap-feature-map-monorepo.cjs +885 -0
  40. package/cap/bin/lib/cap-feature-map-shard.cjs +315 -0
  41. package/cap/bin/lib/cap-feature-map.cjs +1943 -0
  42. package/cap/bin/lib/cap-fitness-score.cjs +1075 -0
  43. package/cap/bin/lib/cap-impact-analysis.cjs +652 -0
  44. package/cap/bin/lib/cap-learn-review.cjs +1072 -0
  45. package/cap/bin/lib/cap-learning-signals.cjs +627 -0
  46. package/cap/bin/lib/cap-loader.cjs +227 -0
  47. package/cap/bin/lib/cap-logger.cjs +57 -0
  48. package/cap/bin/lib/cap-memory-bridge.cjs +764 -0
  49. package/cap/bin/lib/cap-memory-confidence.cjs +452 -0
  50. package/cap/bin/lib/cap-memory-dir.cjs +987 -0
  51. package/cap/bin/lib/cap-memory-engine.cjs +698 -0
  52. package/cap/bin/lib/cap-memory-extends.cjs +398 -0
  53. package/cap/bin/lib/cap-memory-graph.cjs +790 -0
  54. package/cap/bin/lib/cap-memory-migrate.cjs +2015 -0
  55. package/cap/bin/lib/cap-memory-pin.cjs +183 -0
  56. package/cap/bin/lib/cap-memory-platform.cjs +490 -0
  57. package/cap/bin/lib/cap-memory-prune.cjs +707 -0
  58. package/cap/bin/lib/cap-memory-schema.cjs +812 -0
  59. package/cap/bin/lib/cap-migrate-tags.cjs +309 -0
  60. package/cap/bin/lib/cap-migrate.cjs +540 -0
  61. package/cap/bin/lib/cap-pattern-apply.cjs +1203 -0
  62. package/cap/bin/lib/cap-pattern-pipeline.cjs +1034 -0
  63. package/cap/bin/lib/cap-plugin-manifest.cjs +80 -0
  64. package/cap/bin/lib/cap-realtime-affinity.cjs +399 -0
  65. package/cap/bin/lib/cap-reconcile.cjs +570 -0
  66. package/cap/bin/lib/cap-research-gate.cjs +218 -0
  67. package/cap/bin/lib/cap-scope-filter.cjs +402 -0
  68. package/cap/bin/lib/cap-semantic-pipeline.cjs +1038 -0
  69. package/cap/bin/lib/cap-session-extract.cjs +987 -0
  70. package/cap/bin/lib/cap-session.cjs +445 -0
  71. package/cap/bin/lib/cap-snapshot-linkage.cjs +963 -0
  72. package/cap/bin/lib/cap-stack-docs.cjs +646 -0
  73. package/cap/bin/lib/cap-tag-observer.cjs +371 -0
  74. package/cap/bin/lib/cap-tag-scanner.cjs +1766 -0
  75. package/cap/bin/lib/cap-telemetry.cjs +466 -0
  76. package/cap/bin/lib/cap-test-audit.cjs +1438 -0
  77. package/cap/bin/lib/cap-thread-migrator.cjs +307 -0
  78. package/cap/bin/lib/cap-thread-synthesis.cjs +545 -0
  79. package/cap/bin/lib/cap-thread-tracker.cjs +519 -0
  80. package/cap/bin/lib/cap-trace.cjs +399 -0
  81. package/cap/bin/lib/cap-trust-mode.cjs +336 -0
  82. package/cap/bin/lib/cap-ui-design-editor.cjs +642 -0
  83. package/cap/bin/lib/cap-ui-mind-map.cjs +712 -0
  84. package/cap/bin/lib/cap-ui-thread-nav.cjs +693 -0
  85. package/cap/bin/lib/cap-ui.cjs +1245 -0
  86. package/cap/bin/lib/cap-upgrade.cjs +1028 -0
  87. package/cap/bin/lib/cli/arg-helpers.cjs +49 -0
  88. package/cap/bin/lib/cli/frontmatter-router.cjs +31 -0
  89. package/cap/bin/lib/cli/init-router.cjs +68 -0
  90. package/cap/bin/lib/cli/phase-router.cjs +102 -0
  91. package/cap/bin/lib/cli/state-router.cjs +61 -0
  92. package/cap/bin/lib/cli/template-router.cjs +37 -0
  93. package/cap/bin/lib/cli/uat-router.cjs +29 -0
  94. package/cap/bin/lib/cli/validation-router.cjs +26 -0
  95. package/cap/bin/lib/cli/verification-router.cjs +31 -0
  96. package/cap/bin/lib/cli/workstream-router.cjs +39 -0
  97. package/cap/bin/lib/commands.cjs +961 -0
  98. package/cap/bin/lib/config.cjs +467 -0
  99. package/cap/bin/lib/convention-reader.cjs +258 -0
  100. package/cap/bin/lib/core.cjs +1241 -0
  101. package/cap/bin/lib/feature-aggregator.cjs +423 -0
  102. package/cap/bin/lib/frontmatter.cjs +337 -0
  103. package/cap/bin/lib/init.cjs +1443 -0
  104. package/cap/bin/lib/manifest-generator.cjs +383 -0
  105. package/cap/bin/lib/milestone.cjs +253 -0
  106. package/cap/bin/lib/model-profiles.cjs +69 -0
  107. package/cap/bin/lib/monorepo-context.cjs +226 -0
  108. package/cap/bin/lib/monorepo-migrator.cjs +509 -0
  109. package/cap/bin/lib/phase.cjs +889 -0
  110. package/cap/bin/lib/profile-output.cjs +989 -0
  111. package/cap/bin/lib/profile-pipeline.cjs +540 -0
  112. package/cap/bin/lib/roadmap.cjs +330 -0
  113. package/cap/bin/lib/security.cjs +394 -0
  114. package/cap/bin/lib/session-manager.cjs +292 -0
  115. package/cap/bin/lib/skeleton-generator.cjs +179 -0
  116. package/cap/bin/lib/state.cjs +1032 -0
  117. package/cap/bin/lib/template.cjs +231 -0
  118. package/cap/bin/lib/test-detector.cjs +62 -0
  119. package/cap/bin/lib/uat.cjs +283 -0
  120. package/cap/bin/lib/verify.cjs +889 -0
  121. package/cap/bin/lib/workspace-detector.cjs +371 -0
  122. package/cap/bin/lib/workstream.cjs +492 -0
  123. package/cap/commands/gsd/workstreams.md +63 -0
  124. package/cap/references/arc-standard.md +315 -0
  125. package/cap/references/cap-agent-architecture.md +101 -0
  126. package/cap/references/cap-gitignore-template +9 -0
  127. package/cap/references/cap-zero-deps.md +158 -0
  128. package/cap/references/checkpoints.md +778 -0
  129. package/cap/references/continuation-format.md +249 -0
  130. package/cap/references/contract-test-templates.md +312 -0
  131. package/cap/references/feature-map-template.md +25 -0
  132. package/cap/references/git-integration.md +295 -0
  133. package/cap/references/git-planning-commit.md +38 -0
  134. package/cap/references/model-profiles.md +174 -0
  135. package/cap/references/phase-numbering.md +126 -0
  136. package/cap/references/planning-config.md +202 -0
  137. package/cap/references/property-test-templates.md +316 -0
  138. package/cap/references/security-test-templates.md +347 -0
  139. package/cap/references/session-template.json +8 -0
  140. package/cap/references/tdd.md +263 -0
  141. package/cap/references/user-profiling.md +681 -0
  142. package/cap/references/verification-patterns.md +612 -0
  143. package/cap/templates/UAT.md +265 -0
  144. package/cap/templates/claude-md.md +175 -0
  145. package/cap/templates/codebase/architecture.md +255 -0
  146. package/cap/templates/codebase/concerns.md +310 -0
  147. package/cap/templates/codebase/conventions.md +307 -0
  148. package/cap/templates/codebase/integrations.md +280 -0
  149. package/cap/templates/codebase/stack.md +186 -0
  150. package/cap/templates/codebase/structure.md +285 -0
  151. package/cap/templates/codebase/testing.md +480 -0
  152. package/cap/templates/config.json +44 -0
  153. package/cap/templates/context.md +352 -0
  154. package/cap/templates/continue-here.md +78 -0
  155. package/cap/templates/copilot-instructions.md +7 -0
  156. package/cap/templates/debug-subagent-prompt.md +91 -0
  157. package/cap/templates/discussion-log.md +63 -0
  158. package/cap/templates/milestone-archive.md +123 -0
  159. package/cap/templates/milestone.md +115 -0
  160. package/cap/templates/phase-prompt.md +610 -0
  161. package/cap/templates/planner-subagent-prompt.md +117 -0
  162. package/cap/templates/project.md +186 -0
  163. package/cap/templates/requirements.md +231 -0
  164. package/cap/templates/research-project/ARCHITECTURE.md +204 -0
  165. package/cap/templates/research-project/FEATURES.md +147 -0
  166. package/cap/templates/research-project/PITFALLS.md +200 -0
  167. package/cap/templates/research-project/STACK.md +120 -0
  168. package/cap/templates/research-project/SUMMARY.md +170 -0
  169. package/cap/templates/research.md +552 -0
  170. package/cap/templates/roadmap.md +202 -0
  171. package/cap/templates/state.md +176 -0
  172. package/cap/templates/summary.md +364 -0
  173. package/cap/templates/user-preferences.md +498 -0
  174. package/cap/templates/verification-report.md +322 -0
  175. package/cap/workflows/add-phase.md +112 -0
  176. package/cap/workflows/add-tests.md +351 -0
  177. package/cap/workflows/add-todo.md +158 -0
  178. package/cap/workflows/audit-milestone.md +340 -0
  179. package/cap/workflows/audit-uat.md +109 -0
  180. package/cap/workflows/autonomous.md +891 -0
  181. package/cap/workflows/check-todos.md +177 -0
  182. package/cap/workflows/cleanup.md +152 -0
  183. package/cap/workflows/complete-milestone.md +767 -0
  184. package/cap/workflows/diagnose-issues.md +231 -0
  185. package/cap/workflows/discovery-phase.md +289 -0
  186. package/cap/workflows/discuss-phase-assumptions.md +653 -0
  187. package/cap/workflows/discuss-phase.md +1049 -0
  188. package/cap/workflows/do.md +104 -0
  189. package/cap/workflows/execute-phase.md +846 -0
  190. package/cap/workflows/execute-plan.md +514 -0
  191. package/cap/workflows/fast.md +105 -0
  192. package/cap/workflows/forensics.md +265 -0
  193. package/cap/workflows/health.md +181 -0
  194. package/cap/workflows/help.md +660 -0
  195. package/cap/workflows/insert-phase.md +130 -0
  196. package/cap/workflows/list-phase-assumptions.md +178 -0
  197. package/cap/workflows/list-workspaces.md +56 -0
  198. package/cap/workflows/manager.md +362 -0
  199. package/cap/workflows/map-codebase.md +377 -0
  200. package/cap/workflows/milestone-summary.md +223 -0
  201. package/cap/workflows/new-milestone.md +486 -0
  202. package/cap/workflows/new-project.md +1250 -0
  203. package/cap/workflows/new-workspace.md +237 -0
  204. package/cap/workflows/next.md +97 -0
  205. package/cap/workflows/node-repair.md +92 -0
  206. package/cap/workflows/note.md +156 -0
  207. package/cap/workflows/pause-work.md +176 -0
  208. package/cap/workflows/plan-milestone-gaps.md +273 -0
  209. package/cap/workflows/plan-phase.md +857 -0
  210. package/cap/workflows/plant-seed.md +169 -0
  211. package/cap/workflows/pr-branch.md +129 -0
  212. package/cap/workflows/profile-user.md +449 -0
  213. package/cap/workflows/progress.md +507 -0
  214. package/cap/workflows/quick.md +757 -0
  215. package/cap/workflows/remove-phase.md +155 -0
  216. package/cap/workflows/remove-workspace.md +90 -0
  217. package/cap/workflows/research-phase.md +82 -0
  218. package/cap/workflows/resume-project.md +326 -0
  219. package/cap/workflows/review.md +228 -0
  220. package/cap/workflows/session-report.md +146 -0
  221. package/cap/workflows/settings.md +283 -0
  222. package/cap/workflows/ship.md +228 -0
  223. package/cap/workflows/stats.md +60 -0
  224. package/cap/workflows/transition.md +671 -0
  225. package/cap/workflows/ui-phase.md +298 -0
  226. package/cap/workflows/ui-review.md +161 -0
  227. package/cap/workflows/update.md +323 -0
  228. package/cap/workflows/validate-phase.md +170 -0
  229. package/cap/workflows/verify-phase.md +254 -0
  230. package/cap/workflows/verify-work.md +637 -0
  231. package/commands/cap/annotate.md +165 -0
  232. package/commands/cap/brainstorm.md +393 -0
  233. package/commands/cap/checkpoint.md +106 -0
  234. package/commands/cap/completeness.md +94 -0
  235. package/commands/cap/continue.md +72 -0
  236. package/commands/cap/debug.md +588 -0
  237. package/commands/cap/deps.md +169 -0
  238. package/commands/cap/design.md +479 -0
  239. package/commands/cap/init.md +354 -0
  240. package/commands/cap/iterate.md +249 -0
  241. package/commands/cap/learn.md +459 -0
  242. package/commands/cap/memory.md +275 -0
  243. package/commands/cap/migrate-feature-map.md +91 -0
  244. package/commands/cap/migrate-memory.md +108 -0
  245. package/commands/cap/migrate-tags.md +91 -0
  246. package/commands/cap/migrate.md +131 -0
  247. package/commands/cap/prototype.md +510 -0
  248. package/commands/cap/reconcile.md +121 -0
  249. package/commands/cap/review.md +360 -0
  250. package/commands/cap/save.md +72 -0
  251. package/commands/cap/scan.md +404 -0
  252. package/commands/cap/start.md +356 -0
  253. package/commands/cap/status.md +118 -0
  254. package/commands/cap/test-audit.md +262 -0
  255. package/commands/cap/test.md +394 -0
  256. package/commands/cap/trace.md +133 -0
  257. package/commands/cap/ui.md +167 -0
  258. package/hooks/dist/cap-check-update.js +115 -0
  259. package/hooks/dist/cap-context-monitor.js +185 -0
  260. package/hooks/dist/cap-learn-review-hook.js +114 -0
  261. package/hooks/dist/cap-learning-hook.js +192 -0
  262. package/hooks/dist/cap-memory.js +299 -0
  263. package/hooks/dist/cap-prompt-guard.js +97 -0
  264. package/hooks/dist/cap-statusline.js +157 -0
  265. package/hooks/dist/cap-tag-observer.js +115 -0
  266. package/hooks/dist/cap-version-check.js +112 -0
  267. package/hooks/dist/cap-workflow-guard.js +175 -0
  268. package/hooks/hooks.json +55 -0
  269. package/package.json +58 -0
  270. package/scripts/base64-scan.sh +262 -0
  271. package/scripts/build-hooks.js +93 -0
  272. package/scripts/cap-removal-checklist.md +202 -0
  273. package/scripts/prompt-injection-scan.sh +199 -0
  274. package/scripts/run-tests.cjs +181 -0
  275. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,19 @@
1
+ // @cap-feature(feature:F-083) Shared internals for cap-feature-map.cjs and cap-feature-map-monorepo.cjs.
2
+ // Hosts constants and primitives that BOTH main modules need without forcing one to lazy-require
3
+ // the other just to read a string literal.
4
+ // @cap-decision(F-083/followup) F-083-FIX-A: shared constants moved to cap-feature-map-internals.cjs.
5
+ // Both cap-feature-map.cjs and cap-feature-map-monorepo.cjs previously declared a string-equal
6
+ // `FEATURE_MAP_FILE = 'FEATURE-MAP.md'`. The duplicates are value-equal today, but if either site
7
+ // changed the value (e.g. to support `.feature-map.json`) the modules would silently disagree.
8
+ // Move to a single source of truth so future drift is impossible by construction.
9
+ // @cap-constraint Zero external dependencies — Node.js built-ins only (none required here).
10
+
11
+ 'use strict';
12
+
13
+ // @cap-feature(feature:F-083) Canonical Feature Map filename — single source of truth.
14
+ // Both cap-feature-map.cjs and cap-feature-map-monorepo.cjs require this constant from here.
15
+ const FEATURE_MAP_FILE = 'FEATURE-MAP.md';
16
+
17
+ module.exports = {
18
+ FEATURE_MAP_FILE,
19
+ };
@@ -0,0 +1,335 @@
1
+ // @cap-context F-089 sharded Feature Map — migration tool. Transforms a monolithic FEATURE-MAP.md
2
+ // into the sharded layout: index file (FEATURE-MAP.md) + per-feature files (features/<ID>.md).
3
+ // @cap-decision(F-089/AC-6) Byte-lossless extraction of feature blocks. We slice each block from its
4
+ // `### F-...` header to just before the next `### F-...` (or to the next non-feature `## ` header
5
+ // for the final feature). The slice is written verbatim — no parse → serialize round-trip — so all
6
+ // prose, group markers, separator lines, and header-format variations survive intact. F-088 lossy
7
+ // round-trip class is sidestepped entirely on the migration path.
8
+ // @cap-decision(F-089/AC-6 idempotency) Already-sharded projects (features/ dir present with at least
9
+ // one F-*.md) return a no-op status. Re-running the migrator is safe.
10
+ // @cap-decision(F-089/AC-6 backup) On --apply, we write a backup copy `FEATURE-MAP.md.backup-pre-F-089`
11
+ // alongside the original before overwriting. Also surfaced in the dry-run report so the user knows
12
+ // the recovery path before committing.
13
+
14
+ 'use strict';
15
+
16
+ // @cap-feature(feature:F-089) Sharded Feature Map — Migration
17
+
18
+ const fs = require('node:fs');
19
+ const path = require('node:path');
20
+
21
+ const shard = require('./cap-feature-map-shard.cjs');
22
+
23
+ const BACKUP_SUFFIX = '.backup-pre-F-089';
24
+
25
+ /**
26
+ * @typedef {Object} FeatureBlock
27
+ * @property {string} id
28
+ * @property {string} state
29
+ * @property {string} title
30
+ * @property {string} rawBlock The original content slice, byte-identical to source
31
+ * @property {number} startLine 1-based line number of the `### F-` header in the source
32
+ * @property {number} endLine 1-based line number of the last line of the block
33
+ */
34
+
35
+ /**
36
+ * Extract feature blocks from monolithic FEATURE-MAP.md content. Byte-lossless — each block is a
37
+ * raw substring slice of the input. Header parsing for {id, state, title} accepts the same shapes
38
+ * the existing parser does (bracket form + em-dash separator), but we DO NOT re-emit the header.
39
+ *
40
+ * @param {string} content
41
+ * @returns {{ blocks: FeatureBlock[], preFeaturesIntro: string, postFeaturesTail: string }}
42
+ */
43
+ function extractFeatureBlocks(content) {
44
+ const lines = String(content).split('\n');
45
+ // @cap-decision(F-089/regex-sync) Header regex is intentionally permissive — must accept everything
46
+ // the cap-feature-map.cjs parser accepts (mixed-case IDs are a F-089 expansion; legacy uppercase-only
47
+ // forms also accepted). Keep in sync with FEATURE_ID_PATTERN in cap-feature-map-shard.cjs.
48
+ const headerRE = /^###\s+(F-(?:\d{3,}|[A-Z](?:[A-Z0-9_]*[A-Z0-9])?(?:[-_][A-Z0-9_]*[A-Z0-9])*|[A-Z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)+))(?::\s+|\s+[—–-]\s+)(.+?)\s*$/;
49
+ // Top-level section break (## Foo) — terminates the final feature block.
50
+ const topSectionRE = /^##\s+/;
51
+
52
+ /** @type {FeatureBlock[]} */
53
+ const blocks = [];
54
+ /** @type {{ id: string, title: string, state: string, startIdx: number }|null} */
55
+ let current = null;
56
+ /** @type {number|null} */
57
+ let firstHeaderIdx = null;
58
+ /** @type {number|null} */
59
+ let postTailStart = null;
60
+
61
+ for (let i = 0; i < lines.length; i++) {
62
+ const line = lines[i];
63
+ const headerMatch = line.match(headerRE);
64
+ const isTopSection = topSectionRE.test(line) && !line.startsWith('### ');
65
+
66
+ if (headerMatch) {
67
+ if (firstHeaderIdx === null) firstHeaderIdx = i;
68
+ // Close out previous feature block if any.
69
+ if (current) {
70
+ const blockLines = lines.slice(current.startIdx, i);
71
+ // Trim only trailing blank lines (preserve leading prose / decorations within block).
72
+ while (blockLines.length > 0 && blockLines[blockLines.length - 1].trim() === '') {
73
+ blockLines.pop();
74
+ }
75
+ blocks.push({
76
+ id: current.id,
77
+ state: current.state,
78
+ title: current.title,
79
+ rawBlock: blockLines.join('\n') + '\n',
80
+ startLine: current.startIdx + 1,
81
+ endLine: current.startIdx + blockLines.length,
82
+ });
83
+ }
84
+ // Start new feature block.
85
+ let title = headerMatch[2];
86
+ let state = 'planned';
87
+ const stateMatch = title.match(/^(.+?)\s+\[(\w+)\]\s*$/);
88
+ if (stateMatch) {
89
+ title = stateMatch[1];
90
+ state = stateMatch[2];
91
+ }
92
+ current = {
93
+ id: headerMatch[1],
94
+ title: title.trim(),
95
+ state,
96
+ startIdx: i,
97
+ };
98
+ continue;
99
+ }
100
+
101
+ if (isTopSection && current) {
102
+ // Top-level section header (e.g. `## Legend`) — terminates the final feature block.
103
+ const blockLines = lines.slice(current.startIdx, i);
104
+ while (blockLines.length > 0 && blockLines[blockLines.length - 1].trim() === '') {
105
+ blockLines.pop();
106
+ }
107
+ blocks.push({
108
+ id: current.id,
109
+ state: current.state,
110
+ title: current.title,
111
+ rawBlock: blockLines.join('\n') + '\n',
112
+ startLine: current.startIdx + 1,
113
+ endLine: current.startIdx + blockLines.length,
114
+ });
115
+ current = null;
116
+ postTailStart = i;
117
+ // Continue scanning — but no further blocks expected.
118
+ }
119
+ }
120
+
121
+ // EOF: close any still-open feature block.
122
+ if (current) {
123
+ const blockLines = lines.slice(current.startIdx);
124
+ while (blockLines.length > 0 && blockLines[blockLines.length - 1].trim() === '') {
125
+ blockLines.pop();
126
+ }
127
+ blocks.push({
128
+ id: current.id,
129
+ state: current.state,
130
+ title: current.title,
131
+ rawBlock: blockLines.join('\n') + '\n',
132
+ startLine: current.startIdx + 1,
133
+ endLine: current.startIdx + blockLines.length,
134
+ });
135
+ current = null;
136
+ }
137
+
138
+ const preFeaturesIntro =
139
+ firstHeaderIdx !== null && firstHeaderIdx > 0
140
+ ? lines.slice(0, firstHeaderIdx).join('\n')
141
+ : '';
142
+ const postFeaturesTail =
143
+ postTailStart !== null
144
+ ? lines.slice(postTailStart).join('\n')
145
+ : '';
146
+
147
+ return { blocks, preFeaturesIntro, postFeaturesTail };
148
+ }
149
+
150
+ /**
151
+ * @typedef {Object} MigrationPlan
152
+ * @property {'sharded' | 'monolithic' | 'missing'} sourceMode
153
+ * @property {string} featureMapPath Absolute path to the monolithic source file
154
+ * @property {string} featuresDir Absolute path to the per-feature directory
155
+ * @property {string} backupPath Absolute path of the backup that will be written
156
+ * @property {Array<{id: string, filePath: string, lineCount: number, state: string, title: string}>} writes
157
+ * @property {Array<{id: string, reason: string}>} skips
158
+ * @property {Array<string>} warnings
159
+ */
160
+
161
+ /**
162
+ * Plan a migration without writing anything. Idempotent — returns sourceMode:'sharded' for already
163
+ * migrated projects.
164
+ *
165
+ * @param {string} projectRoot
166
+ * @param {string|null|undefined} [appPath]
167
+ * @returns {MigrationPlan}
168
+ */
169
+ function planMigration(projectRoot, appPath) {
170
+ const baseDir = appPath ? path.join(projectRoot, appPath) : projectRoot;
171
+ const featureMapPath = path.join(baseDir, shard.FEATURE_MAP_FILE);
172
+ const featuresDir = shard.featuresDirPath(projectRoot, appPath);
173
+ const backupPath = featureMapPath + BACKUP_SUFFIX;
174
+
175
+ /** @type {MigrationPlan} */
176
+ const plan = {
177
+ sourceMode: 'missing',
178
+ featureMapPath,
179
+ featuresDir,
180
+ backupPath,
181
+ writes: [],
182
+ skips: [],
183
+ warnings: [],
184
+ };
185
+
186
+ if (!fs.existsSync(featureMapPath)) {
187
+ plan.warnings.push('FEATURE-MAP.md not found — nothing to migrate.');
188
+ return plan;
189
+ }
190
+
191
+ if (shard.isShardedMap(projectRoot, appPath)) {
192
+ plan.sourceMode = 'sharded';
193
+ plan.warnings.push('Already in sharded mode (features/ exists with F-*.md files). No migration needed.');
194
+ return plan;
195
+ }
196
+
197
+ plan.sourceMode = 'monolithic';
198
+ const content = fs.readFileSync(featureMapPath, 'utf8');
199
+ const { blocks } = extractFeatureBlocks(content);
200
+
201
+ if (blocks.length === 0) {
202
+ plan.warnings.push('No feature blocks found in FEATURE-MAP.md — index will be empty.');
203
+ }
204
+
205
+ // Detect duplicate IDs — refuse migration to keep the user-visible failure mode loud.
206
+ const seen = new Map();
207
+ for (const b of blocks) {
208
+ if (!shard.validateFeatureId(b.id)) {
209
+ plan.skips.push({ id: b.id, reason: 'Invalid feature ID — would produce unsafe filename.' });
210
+ continue;
211
+ }
212
+ if (seen.has(b.id)) {
213
+ plan.skips.push({
214
+ id: b.id,
215
+ reason: `Duplicate ID — first occurrence at line ${seen.get(b.id)}, this one at line ${b.startLine}. Resolve in source before migrating.`,
216
+ });
217
+ continue;
218
+ }
219
+ seen.set(b.id, b.startLine);
220
+ plan.writes.push({
221
+ id: b.id,
222
+ filePath: shard.featureFilePath(projectRoot, b.id, appPath),
223
+ lineCount: b.rawBlock.split('\n').length,
224
+ state: b.state,
225
+ title: b.title,
226
+ });
227
+ }
228
+
229
+ return plan;
230
+ }
231
+
232
+ /**
233
+ * Apply a migration. Writes per-feature files, backup of original FEATURE-MAP.md, and the new
234
+ * index FEATURE-MAP.md. Atomic-ish: feature files first, then backup, then index. If the index
235
+ * write fails, the per-feature files and backup are present so the user can recover manually.
236
+ *
237
+ * @param {string} projectRoot
238
+ * @param {string|null|undefined} [appPath]
239
+ * @param {{ force?: boolean }} [options]
240
+ * @returns {{ ok: boolean, plan: MigrationPlan, applied: { featuresWritten: number, indexWritten: boolean, backupWritten: boolean } }}
241
+ */
242
+ function applyMigration(projectRoot, appPath, options) {
243
+ const force = Boolean(options && options.force);
244
+ const plan = planMigration(projectRoot, appPath);
245
+ const applied = { featuresWritten: 0, indexWritten: false, backupWritten: false };
246
+
247
+ if (plan.sourceMode !== 'monolithic') {
248
+ return { ok: false, plan, applied };
249
+ }
250
+ if (plan.skips.length > 0 && !force) {
251
+ plan.warnings.push('Refused to apply: ' + plan.skips.length + ' feature(s) had errors. Pass {force:true} to skip them and proceed (NOT recommended for duplicate IDs).');
252
+ return { ok: false, plan, applied };
253
+ }
254
+
255
+ // Re-extract blocks (planMigration discarded them); we need them here for content writes.
256
+ const content = fs.readFileSync(plan.featureMapPath, 'utf8');
257
+ const { blocks } = extractFeatureBlocks(content);
258
+
259
+ // Step 1: ensure features/ dir exists.
260
+ fs.mkdirSync(plan.featuresDir, { recursive: true });
261
+
262
+ // Step 2: write per-feature files. Skip duplicates and invalid IDs (already in plan.skips).
263
+ const planned = new Set(plan.writes.map(w => w.id));
264
+ for (const b of blocks) {
265
+ if (!planned.has(b.id)) continue;
266
+ const target = shard.featureFilePath(projectRoot, b.id, appPath);
267
+ // Atomic write: tmp + rename.
268
+ const tmp = target + '.tmp';
269
+ fs.writeFileSync(tmp, b.rawBlock, 'utf8');
270
+ fs.renameSync(tmp, target);
271
+ applied.featuresWritten++;
272
+ }
273
+
274
+ // Step 3: backup the source.
275
+ fs.writeFileSync(plan.backupPath, content, 'utf8');
276
+ applied.backupWritten = true;
277
+
278
+ // Step 4: write the new index FEATURE-MAP.md from the planned writes (already validated).
279
+ const indexEntries = plan.writes.map(w => ({ id: w.id, state: w.state, title: w.title }));
280
+ const indexContent = shard.serializeIndex(indexEntries);
281
+ // Atomic write.
282
+ const indexTmp = plan.featureMapPath + '.tmp';
283
+ fs.writeFileSync(indexTmp, indexContent, 'utf8');
284
+ fs.renameSync(indexTmp, plan.featureMapPath);
285
+ applied.indexWritten = true;
286
+
287
+ return { ok: true, plan, applied };
288
+ }
289
+
290
+ /**
291
+ * Render a MigrationPlan as a human-friendly text report (used by the CLI / command).
292
+ * @param {MigrationPlan} plan
293
+ * @returns {string}
294
+ */
295
+ function formatPlan(plan) {
296
+ const lines = [];
297
+ lines.push('Migration plan: monolithic → sharded FEATURE-MAP');
298
+ lines.push('');
299
+ lines.push('Source mode: ' + plan.sourceMode);
300
+ lines.push('Source file: ' + plan.featureMapPath);
301
+ lines.push('Target dir : ' + plan.featuresDir);
302
+ lines.push('Backup file: ' + plan.backupPath);
303
+ lines.push('');
304
+ if (plan.warnings.length > 0) {
305
+ lines.push('Warnings:');
306
+ for (const w of plan.warnings) lines.push(' - ' + w);
307
+ lines.push('');
308
+ }
309
+ if (plan.writes.length > 0) {
310
+ lines.push('Will write ' + plan.writes.length + ' per-feature file(s):');
311
+ for (const w of plan.writes) {
312
+ lines.push(' - ' + w.id + ' [' + w.state + '] ' + w.title + ' (' + w.lineCount + ' lines)');
313
+ }
314
+ lines.push('');
315
+ }
316
+ if (plan.skips.length > 0) {
317
+ lines.push('Skipped ' + plan.skips.length + ' feature(s):');
318
+ for (const s of plan.skips) {
319
+ lines.push(' - ' + s.id + ': ' + s.reason);
320
+ }
321
+ lines.push('');
322
+ }
323
+ if (plan.sourceMode === 'monolithic' && plan.writes.length > 0) {
324
+ lines.push('Apply with: applyMigration(projectRoot, appPath) // dry-run by default');
325
+ }
326
+ return lines.join('\n');
327
+ }
328
+
329
+ module.exports = {
330
+ BACKUP_SUFFIX,
331
+ extractFeatureBlocks,
332
+ planMigration,
333
+ applyMigration,
334
+ formatPlan,
335
+ };