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,309 @@
1
+ 'use strict';
2
+
3
+ // @cap-feature(feature:F-047) Migration tool: fragmented @cap-* tags -> unified anchor block.
4
+ // @cap-history(sessions:2, edits:7, since:2026-04-20, learned:2026-05-07) Frequently modified — 2 sessions, 7 edits
5
+ //
6
+ // v1 strategy is ADDITIVE: a unified anchor block is inserted near the top of each file
7
+ // that carries fragmented AC-level tags, but the legacy tags themselves are NOT removed.
8
+ // Both formats coexist during the deprecation window (AC-2). A future `--remove-legacy`
9
+ // mode can delete the fragmented tags once the ecosystem has fully switched.
10
+ //
11
+ // @cap-decision Additive migration keeps the blast radius tiny: no line deletes, no regex-
12
+ // based source rewrites of tag annotations, no risk of destroying surrounding code. The
13
+ // tradeoff is dual-tag files until the cleanup pass runs. Documented as an explicit choice.
14
+
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+ const anchor = require('./cap-anchor.cjs');
18
+ const scanner = require('./cap-tag-scanner.cjs');
19
+ // @cap-feature(feature:F-085) Scope filter — same module as the scanner uses, ensures both
20
+ // tools share gitignore + path-pattern + plugin-mirror exclusions.
21
+ const scopeModule = require('./cap-scope-filter.cjs');
22
+
23
+ // @cap-todo(ac:F-047/AC-3) Per-file summary of what the migration would / did change.
24
+ /**
25
+ * @typedef {Object} FileMigrationResult
26
+ * @property {string} file - Relative path
27
+ * @property {boolean} changed - True when an anchor would be inserted
28
+ * @property {string|null} newContent - Proposed content (dry-run) or written content
29
+ * @property {string|null} anchorBlock - The anchor line that would be inserted (or null)
30
+ * @property {string[]} consolidatedFeatures - Feature IDs consolidated into the anchor
31
+ * @property {string[]} consolidatedAcs - AC IDs consolidated (e.g. 'F-001/AC-1')
32
+ * @property {string} reason - 'inserted', 'already-has-anchor', 'no-feature-tags'
33
+ */
34
+
35
+ /**
36
+ * Decide the comment delimiter style appropriate for this file extension.
37
+ * Mirrors cap-anchor.emitAnchorBlock() styles: block (slash-star), line (hash), html.
38
+ *
39
+ * @param {string} filePath
40
+ * @returns {'block'|'line'|'html'}
41
+ */
42
+ function commentStyleForFile(filePath) {
43
+ const ext = path.extname(filePath).toLowerCase();
44
+ if (ext === '.html' || ext === '.htm' || ext === '.xml' || ext === '.vue' || ext === '.md') {
45
+ return 'html';
46
+ }
47
+ if (
48
+ ext === '.py' || ext === '.rb' || ext === '.sh' || ext === '.bash' || ext === '.zsh' ||
49
+ ext === '.yml' || ext === '.yaml' || ext === '.toml'
50
+ ) {
51
+ return 'line';
52
+ }
53
+ // Default: block comment works in JS/TS/Go/Rust/C/C++/Java/SQL/CSS.
54
+ return 'block';
55
+ }
56
+
57
+ /**
58
+ * Build the anchor structure for a single feature group (the file may tag multiple features —
59
+ * in which case we emit one anchor per feature). Returns null when the group has no AC coverage
60
+ * AND no primary role (nothing worth consolidating).
61
+ *
62
+ * @param {string} featureId
63
+ * @param {CapTag[]} tags - Tags for this feature on this file (feature + todo subset)
64
+ * @returns {{feature:string, acs:string[], role:('primary'|'secondary'|null)}|null}
65
+ */
66
+ function consolidateGroup(featureId, tags) {
67
+ const acs = new Set();
68
+ let role = null;
69
+ for (const t of tags) {
70
+ if (t.type === 'feature') {
71
+ if (t.metadata && t.metadata.primary === true) role = 'primary';
72
+ } else if (t.type === 'todo' && t.metadata && typeof t.metadata.ac === 'string') {
73
+ // ac format: 'F-XXX/AC-N' or 'AC-N' (resolved against feature). Anchor both ends
74
+ // so a stray 'notAC-12-ish' cannot accidentally match.
75
+ const m = t.metadata.ac.match(/^(?:F-\d{3,}\/)?(AC-\d+)$/);
76
+ if (m) acs.add(m[1]);
77
+ }
78
+ }
79
+ if (acs.size === 0 && role === null) return null;
80
+ return { feature: featureId, acs: [...acs].sort(compareAcIds), role };
81
+ }
82
+
83
+ function compareAcIds(a, b) {
84
+ const na = parseInt(a.slice(3), 10);
85
+ const nb = parseInt(b.slice(3), 10);
86
+ return na - nb;
87
+ }
88
+
89
+ /**
90
+ * Find the line index where an anchor block should be inserted: after the shebang (if any)
91
+ * and after any 'use strict' directive, but before the first code line.
92
+ *
93
+ * @param {string[]} lines
94
+ * @returns {number} Zero-based index of the insertion point
95
+ */
96
+ function findInsertionIndex(lines) {
97
+ let i = 0;
98
+ if (lines.length === 0) return 0;
99
+ if (lines[0].startsWith('#!')) i++;
100
+ // Skip blank + 'use strict' line
101
+ while (i < lines.length) {
102
+ const t = lines[i].trim();
103
+ if (t === '' || /^['"]use strict['"];?$/.test(t)) { i++; continue; }
104
+ break;
105
+ }
106
+ return i;
107
+ }
108
+
109
+ /**
110
+ * Compute the migration for a single file WITHOUT writing. The caller decides whether to
111
+ * persist `newContent`. Scanner tags are required for the file; callers pass them in so this
112
+ * module stays decoupled from disk except for the actual read/write operations in helpers.
113
+ *
114
+ * @param {string} filePath - Absolute path
115
+ * @param {string} projectRoot - Absolute project root (for relative-path reporting)
116
+ * @param {CapTag[]} tags - Scanner output for this file (legacy tags only)
117
+ * @returns {FileMigrationResult}
118
+ */
119
+ function planFileMigration(filePath, projectRoot, tags) {
120
+ const rel = path.relative(projectRoot, filePath);
121
+ /** @type {FileMigrationResult} */
122
+ const result = {
123
+ file: rel,
124
+ changed: false,
125
+ newContent: null,
126
+ anchorBlock: null,
127
+ consolidatedFeatures: [],
128
+ consolidatedAcs: [],
129
+ reason: 'no-feature-tags',
130
+ };
131
+
132
+ let content;
133
+ try {
134
+ content = fs.readFileSync(filePath, 'utf8');
135
+ } catch (_e) {
136
+ result.reason = 'read-error';
137
+ return result;
138
+ }
139
+
140
+ // Short-circuit: if the file already contains a unified anchor, leave it alone.
141
+ const anchorTags = anchor.scanAnchorsInContent(content, rel);
142
+ if (anchorTags.length > 0) {
143
+ result.reason = 'already-has-anchor';
144
+ return result;
145
+ }
146
+
147
+ // Group tags by feature. Non-feature-bound tags (@cap-risk, @cap-decision with no `feature`)
148
+ // are intentionally ignored for consolidation.
149
+ const byFeature = new Map();
150
+ for (const t of tags) {
151
+ const fid = t.metadata && t.metadata.feature;
152
+ if (fid) {
153
+ if (!byFeature.has(fid)) byFeature.set(fid, []);
154
+ byFeature.get(fid).push(t);
155
+ continue;
156
+ }
157
+ // @cap-todo with ac:F-XXX/AC-N (no feature key) — infer feature from ac prefix.
158
+ if (t.type === 'todo' && t.metadata && typeof t.metadata.ac === 'string') {
159
+ const m = t.metadata.ac.match(/^(F-\d{3,})\//);
160
+ if (m) {
161
+ const inferred = m[1];
162
+ if (!byFeature.has(inferred)) byFeature.set(inferred, []);
163
+ byFeature.get(inferred).push(t);
164
+ }
165
+ }
166
+ }
167
+
168
+ if (byFeature.size === 0) return result;
169
+
170
+ // Build anchor lines per feature group. Stable order: features sorted alphanumerically.
171
+ const featureIds = [...byFeature.keys()].sort();
172
+ const style = commentStyleForFile(filePath);
173
+ const anchorLines = [];
174
+ for (const fid of featureIds) {
175
+ const consolidated = consolidateGroup(fid, byFeature.get(fid));
176
+ if (!consolidated) continue;
177
+ anchorLines.push(anchor.emitAnchorBlock(consolidated, style));
178
+ result.consolidatedFeatures.push(fid);
179
+ for (const ac of consolidated.acs) {
180
+ result.consolidatedAcs.push(`${fid}/${ac}`);
181
+ }
182
+ }
183
+
184
+ if (anchorLines.length === 0) return result;
185
+
186
+ const lines = content.split('\n');
187
+ const insertAt = findInsertionIndex(lines);
188
+ const newLines = [...lines];
189
+ // Insert anchor lines + one trailing blank separator if the next line isn't already blank
190
+ const needsSpacer = newLines[insertAt] !== undefined && newLines[insertAt].trim() !== '';
191
+ const toInsert = needsSpacer ? [...anchorLines, ''] : [...anchorLines];
192
+ newLines.splice(insertAt, 0, ...toInsert);
193
+
194
+ result.changed = true;
195
+ result.anchorBlock = anchorLines.join('\n');
196
+ result.newContent = newLines.join('\n');
197
+ result.reason = 'inserted';
198
+ return result;
199
+ }
200
+
201
+ /**
202
+ * Compute migrations for every file under the project. Does NOT write.
203
+ *
204
+ * @param {string} projectRoot
205
+ * @param {{ extensions?: string[], exclude?: string[] }} [options]
206
+ * @returns {FileMigrationResult[]}
207
+ */
208
+ function planProjectMigration(projectRoot, options = {}) {
209
+ // @cap-todo(ac:F-085/AC-1) The migrator builds (or accepts) the same scope filter as the
210
+ // scanner so both tools agree on which files are in scope. Passing it explicitly into
211
+ // scanDirectory short-circuits the scanner's default-build path.
212
+ const scope = options.scope || scopeModule.buildScopeFilter(projectRoot, {
213
+ dirExcludes: options.exclude,
214
+ pathExcludes: options.pathExcludes,
215
+ excludes: options.excludes,
216
+ includes: options.includes,
217
+ respectGitignore: options.respectGitignore,
218
+ });
219
+ // We want legacy tags only — force unifiedAnchors:false so the scan baseline is clean.
220
+ const allTags = scanner.scanDirectory(projectRoot, { ...options, scope, unifiedAnchors: false });
221
+
222
+ // Group tags by file
223
+ const byFile = new Map();
224
+ for (const t of allTags) {
225
+ const abs = path.resolve(projectRoot, t.file);
226
+ if (!byFile.has(abs)) byFile.set(abs, []);
227
+ byFile.get(abs).push(t);
228
+ }
229
+
230
+ const results = [];
231
+ for (const [abs, tags] of byFile.entries()) {
232
+ results.push(planFileMigration(abs, projectRoot, tags));
233
+ }
234
+ return results.sort((a, b) => a.file.localeCompare(b.file));
235
+ }
236
+
237
+ /**
238
+ * Write planned migrations to disk. Only files flagged changed=true are touched.
239
+ *
240
+ * @param {FileMigrationResult[]} results
241
+ * @param {string} projectRoot
242
+ * @param {{ allowLargeDiff?: boolean }} [options]
243
+ * @returns {{ written: string[], skipped: string[] }}
244
+ */
245
+ function applyMigrations(results, projectRoot, options = {}) {
246
+ // @cap-todo(ac:F-085/AC-7) Large-diff guard: bare --apply against >500 candidate files
247
+ // is almost always a scope-filter bug, not user intent. We throw with an actionable
248
+ // error so the caller can re-run with allowLargeDiff:true once the scope is verified.
249
+ const changed = results.filter((r) => r.changed && r.newContent != null);
250
+ if (changed.length > scopeModule.LARGE_DIFF_THRESHOLD && !options.allowLargeDiff) {
251
+ const err = new Error(
252
+ `cap-migrate-tags: refusing to apply migration to ${changed.length} files ` +
253
+ `(threshold ${scopeModule.LARGE_DIFF_THRESHOLD}). This usually indicates a ` +
254
+ `scope-filter problem — verify the dry-run report is what you intended, then ` +
255
+ `re-run with allowLargeDiff:true to override.`
256
+ );
257
+ err.code = 'CAP_MIGRATE_LARGE_DIFF';
258
+ err.changedCount = changed.length;
259
+ err.threshold = scopeModule.LARGE_DIFF_THRESHOLD;
260
+ throw err;
261
+ }
262
+ const written = [];
263
+ const skipped = [];
264
+ for (const r of results) {
265
+ if (!r.changed || r.newContent == null) {
266
+ skipped.push(r.file);
267
+ continue;
268
+ }
269
+ const abs = path.resolve(projectRoot, r.file);
270
+ fs.writeFileSync(abs, r.newContent, 'utf8');
271
+ written.push(r.file);
272
+ }
273
+ return { written, skipped };
274
+ }
275
+
276
+ /**
277
+ * Produce a human-readable dry-run report.
278
+ *
279
+ * @param {FileMigrationResult[]} results
280
+ * @returns {string}
281
+ */
282
+ function formatMigrationReport(results) {
283
+ const changed = results.filter((r) => r.changed);
284
+ const alreadyHas = results.filter((r) => r.reason === 'already-has-anchor');
285
+ const noTags = results.filter((r) => r.reason === 'no-feature-tags');
286
+
287
+ const lines = [];
288
+ lines.push(`Migration plan — ${changed.length} file(s) would be updated:`);
289
+ lines.push('');
290
+ for (const r of changed) {
291
+ lines.push(` ${r.file}`);
292
+ lines.push(` anchor: ${r.anchorBlock}`);
293
+ lines.push(` consolidates: ${r.consolidatedAcs.join(', ') || '(feature-only, no ACs)'}`);
294
+ lines.push('');
295
+ }
296
+ lines.push(`${alreadyHas.length} file(s) already use unified anchors; skipped.`);
297
+ lines.push(`${noTags.length} file(s) had no feature-bound tags; skipped.`);
298
+ return lines.join('\n').trimEnd();
299
+ }
300
+
301
+ module.exports = {
302
+ commentStyleForFile,
303
+ consolidateGroup,
304
+ findInsertionIndex,
305
+ planFileMigration,
306
+ planProjectMigration,
307
+ applyMigrations,
308
+ formatMigrationReport,
309
+ };