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,315 @@
1
+ // @cap-context F-089 sharded Feature Map — pure helpers for ID validation, filename derivation,
2
+ // index-line parse/serialize, and surgical index-entry patching. Zero I/O except where explicitly
3
+ // marked (existsSync / readdirSync probes for sharded-mode detection).
4
+ // @cap-decision(F-089/strategy) Helpers live in a separate module so the surface stays auditable and
5
+ // the core cap-feature-map.cjs can lazy-require it (mirrors F-083 monorepo split pattern).
6
+ // @cap-pattern(F-089/test-first) Module is pure-functions where possible — every export has a unit test
7
+ // in tests/cap-feature-map-shard.test.cjs.
8
+
9
+ 'use strict';
10
+
11
+ // @cap-feature(feature:F-089, primary:true) Sharded Feature Map — Index + Per-Feature Files
12
+
13
+ const fs = require('node:fs');
14
+ const path = require('node:path');
15
+
16
+ const FEATURES_DIR = 'features';
17
+ const FEATURE_MAP_FILE = 'FEATURE-MAP.md';
18
+ const MAX_ID_LENGTH = 64;
19
+
20
+ // @cap-decision(F-089/AC-3) Three-branch union accepts:
21
+ // 1. F-NNN legacy numeric, 3+ digits — F-001, F-1234
22
+ // 2. F-LONGFORM uppercase legacy (single or compound) — F-DEPLOY, F-HUB-AUTH, F-FOO_BAR (F-081 heritage)
23
+ // 3. F-Deskriptiv mixed-case with REQUIRED hyphen separator — F-Hub-Spotlight-Carousel, F-App2-Feature3
24
+ // Each branch enforces a distinct shape so collisions like `F-deploy` (lowercase single segment)
25
+ // are rejected — that case must be either numeric, all-uppercase, or have an explicit segment.
26
+ // Rejects digit-leading suffixed forms — `F-076-suffix` matches NEITHER branch:
27
+ // - branch 1 is digits-only
28
+ // - branches 2/3 require letter-first
29
+ // This preserves the F-076 schema invariant proven by cap-memory-schema tests.
30
+ // @cap-risk(reason:regex-asymmetry) The cap-feature-map header regex (`featureHeaderRE`) and
31
+ // surgical-patch regex (`_surgicalSetAcStatus`'s next-header) historically used the narrower
32
+ // F-081 pattern. F-089 widens both — keep them in sync with this constant or the parser/patcher
33
+ // will silently skip mixed-case IDs.
34
+ const FEATURE_ID_PATTERN = /^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]+)+)$/;
35
+
36
+ /**
37
+ * @typedef {Object} IndexEntry
38
+ * @property {string} id Feature ID (e.g. "F-001" or "F-Hub-Spotlight-Carousel")
39
+ * @property {string} state Feature lifecycle state
40
+ * @property {string} title Feature title
41
+ */
42
+
43
+ /**
44
+ * Validate a feature ID. Accepts legacy numeric, F-LONGFORM, and deskriptiv mixed-case forms.
45
+ * Defense-in-depth: even if regex passes, FS-traversal characters must be absent.
46
+ * @param {*} id
47
+ * @returns {boolean}
48
+ */
49
+ function validateFeatureId(id) {
50
+ if (typeof id !== 'string') return false;
51
+ if (id.length === 0 || id.length > MAX_ID_LENGTH) return false;
52
+ if (!FEATURE_ID_PATTERN.test(id)) return false;
53
+ // Defense-in-depth: regex already rejects these but a future loosening must not regress.
54
+ if (id.includes('..') || id.includes('/') || id.includes('\\')) return false;
55
+ return true;
56
+ }
57
+
58
+ /**
59
+ * Derive the per-feature filename (basename only) from a validated ID.
60
+ * @param {string} id
61
+ * @returns {string} e.g. "F-001.md"
62
+ */
63
+ function featureFilename(id) {
64
+ if (!validateFeatureId(id)) {
65
+ throw new Error('cap: featureFilename — invalid feature ID: ' + JSON.stringify(id));
66
+ }
67
+ return id + '.md';
68
+ }
69
+
70
+ /**
71
+ * Resolve the absolute path to the features/ directory for a given project root + optional appPath.
72
+ * @param {string} projectRoot
73
+ * @param {string|null|undefined} [appPath]
74
+ * @returns {string}
75
+ */
76
+ function featuresDirPath(projectRoot, appPath) {
77
+ const baseDir = appPath ? path.join(projectRoot, appPath) : projectRoot;
78
+ return path.join(baseDir, FEATURES_DIR);
79
+ }
80
+
81
+ /**
82
+ * Resolve the absolute path to a per-feature file.
83
+ * @param {string} projectRoot
84
+ * @param {string} id
85
+ * @param {string|null|undefined} [appPath]
86
+ * @returns {string}
87
+ */
88
+ function featureFilePath(projectRoot, id, appPath) {
89
+ return path.join(featuresDirPath(projectRoot, appPath), featureFilename(id));
90
+ }
91
+
92
+ /**
93
+ * Detect whether a project is in sharded mode (features/ dir exists with at least one F-*.md file).
94
+ * Used by readFeatureMap for AC-7 backwards-compat fallback.
95
+ * @param {string} projectRoot
96
+ * @param {string|null|undefined} [appPath]
97
+ * @returns {boolean}
98
+ */
99
+ function isShardedMap(projectRoot, appPath) {
100
+ const dir = featuresDirPath(projectRoot, appPath);
101
+ if (!fs.existsSync(dir)) return false;
102
+ let stat;
103
+ try {
104
+ stat = fs.statSync(dir);
105
+ } catch (_e) {
106
+ return false;
107
+ }
108
+ if (!stat.isDirectory()) return false;
109
+ let entries;
110
+ try {
111
+ entries = fs.readdirSync(dir);
112
+ } catch (_e) {
113
+ return false;
114
+ }
115
+ return entries.some(e => /^F-.+\.md$/.test(e));
116
+ }
117
+
118
+ // @cap-decision(F-089/AC-1) Index line format: `- <ID> | <state> | <title>`.
119
+ // Pipe-delimited with single-space padding. Title cannot contain `|` or newlines (validated).
120
+ // Markdown bullet (`-`) makes the line render as a list item if the index is opened in a viewer.
121
+ const INDEX_LINE_RE = /^-\s+(F-\S+)\s*\|\s*(\w+)\s*\|\s*(.+?)\s*$/;
122
+
123
+ /**
124
+ * Parse a single index line. Returns null on malformed input or invalid feature ID.
125
+ * @param {string} line
126
+ * @returns {IndexEntry|null}
127
+ */
128
+ function parseIndexLine(line) {
129
+ const m = INDEX_LINE_RE.exec(line);
130
+ if (!m) return null;
131
+ const id = m[1];
132
+ if (!validateFeatureId(id)) return null;
133
+ return { id, state: m[2], title: m[3].trim() };
134
+ }
135
+
136
+ /**
137
+ * Serialize an IndexEntry to a single line.
138
+ * @param {IndexEntry} entry
139
+ * @returns {string}
140
+ */
141
+ function serializeIndexEntry(entry) {
142
+ if (!entry || typeof entry !== 'object') {
143
+ throw new Error('cap: serializeIndexEntry — entry must be an object');
144
+ }
145
+ if (!validateFeatureId(entry.id)) {
146
+ throw new Error('cap: serializeIndexEntry — invalid feature ID: ' + JSON.stringify(entry.id));
147
+ }
148
+ if (typeof entry.state !== 'string' || entry.state.length === 0 || /\s/.test(entry.state)) {
149
+ throw new Error('cap: serializeIndexEntry — invalid state: ' + JSON.stringify(entry.state));
150
+ }
151
+ if (typeof entry.title !== 'string' || entry.title.includes('|') || entry.title.includes('\n')) {
152
+ throw new Error('cap: serializeIndexEntry — title cannot contain "|" or newlines: ' + JSON.stringify(entry.title));
153
+ }
154
+ return `- ${entry.id} | ${entry.state} | ${entry.title}`;
155
+ }
156
+
157
+ /**
158
+ * Parse an index file (FEATURE-MAP.md in sharded mode) into IndexEntry[].
159
+ * Walks the `## Features` section and collects every recognized index line.
160
+ * Lines outside the Features section are ignored (Legend, footer, prose).
161
+ * @param {string} content
162
+ * @returns {IndexEntry[]}
163
+ */
164
+ function parseIndex(content) {
165
+ const entries = [];
166
+ const lines = String(content).split('\n');
167
+ let inFeaturesSection = false;
168
+ for (const line of lines) {
169
+ if (/^##\s+Features\s*$/i.test(line)) { inFeaturesSection = true; continue; }
170
+ if (/^##\s/.test(line) && inFeaturesSection) { inFeaturesSection = false; continue; }
171
+ if (!inFeaturesSection) continue;
172
+ const entry = parseIndexLine(line);
173
+ if (entry) entries.push(entry);
174
+ }
175
+ return entries;
176
+ }
177
+
178
+ // @cap-decision(F-089/AC-9) Surgical patch — analog F-088 pattern. We rewrite the matched index
179
+ // line in-place via regex substitution so the rest of the file (header, prose, Legend, footer)
180
+ // stays byte-identical. No re-serialization of the whole index.
181
+ function _escapeRegex(s) {
182
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
183
+ }
184
+
185
+ /**
186
+ * Surgically update one index entry. Returns hit:false if the ID is not present (caller appends).
187
+ * Validates the new title before writing — pipe/newline rejected as a hit:false miss to force
188
+ * the caller into the explicit append path or surface the error.
189
+ *
190
+ * @param {string} content
191
+ * @param {string} id
192
+ * @param {{ state?: string, title?: string }} fields
193
+ * @returns {{ content: string, hit: boolean }}
194
+ */
195
+ function _updateIndexEntry(content, id, fields) {
196
+ if (!validateFeatureId(id)) return { content, hit: false };
197
+ const escapedId = _escapeRegex(id);
198
+ // Match: `- F-NNN | state | title` (allowing leading whitespace tolerance for hand-edited files).
199
+ const re = new RegExp(
200
+ '^(\\s*-\\s+' + escapedId + '\\s*\\|\\s*)(\\w+)(\\s*\\|\\s*)([^\\n]*?)(\\s*)$',
201
+ 'm'
202
+ );
203
+ const m = re.exec(content);
204
+ if (!m) return { content, hit: false };
205
+ const newState = fields && typeof fields.state === 'string' ? fields.state : m[2];
206
+ const newTitle = fields && typeof fields.title === 'string' ? fields.title : m[4];
207
+ if (/\s/.test(newState) || newState.length === 0) return { content, hit: false };
208
+ if (newTitle.includes('|') || newTitle.includes('\n')) return { content, hit: false };
209
+ const replaced = content.replace(re, (_full, prefix, _state, sep, _title, trailing) => {
210
+ return prefix + newState + sep + newTitle + trailing;
211
+ });
212
+ return { content: replaced, hit: true };
213
+ }
214
+
215
+ /**
216
+ * Append a new entry into the `## Features` section of the index, keeping the section anchor
217
+ * intact. If the section doesn't exist (template malformed), returns hit:false.
218
+ * The append point is just before the next `##`-level header (or end-of-content if none).
219
+ *
220
+ * @param {string} content
221
+ * @param {IndexEntry} entry
222
+ * @returns {{ content: string, hit: boolean }}
223
+ */
224
+ function _appendIndexEntry(content, entry) {
225
+ const line = serializeIndexEntry(entry); // throws on invalid input — appropriate at this boundary
226
+ const lines = String(content).split('\n');
227
+ // Find the start of the `## Features` section.
228
+ let featuresStart = -1;
229
+ for (let i = 0; i < lines.length; i++) {
230
+ if (/^##\s+Features\s*$/i.test(lines[i])) { featuresStart = i; break; }
231
+ }
232
+ if (featuresStart === -1) return { content, hit: false };
233
+ // Find the end of the Features section: the next `## ...` header, or end-of-file.
234
+ let featuresEnd = lines.length;
235
+ for (let i = featuresStart + 1; i < lines.length; i++) {
236
+ if (/^##\s/.test(lines[i])) { featuresEnd = i; break; }
237
+ }
238
+ // Insert position: just before featuresEnd, but trim trailing blanks within the section first
239
+ // so we don't accumulate blank lines on repeated appends.
240
+ let insertAt = featuresEnd;
241
+ while (insertAt > featuresStart + 1 && lines[insertAt - 1].trim() === '') insertAt--;
242
+ // Build new lines: keep through insertAt-1, splice in the entry line, then the rest.
243
+ // We do NOT add a trailing blank — `after` (the slice from insertAt onward) typically
244
+ // starts with the section's existing blank-line padding before the next `##` header,
245
+ // so we'd otherwise accumulate blank lines on every append.
246
+ const before = lines.slice(0, insertAt);
247
+ const after = lines.slice(insertAt);
248
+ return { content: before.concat([line], after).join('\n'), hit: true };
249
+ }
250
+
251
+ /**
252
+ * Serialize a complete index file from scratch (header + Features section + Legend + footer).
253
+ * Used by the migrator to build the initial index after sharding. Surgical patches handle
254
+ * subsequent updates so this serializer is only called on full rebuilds.
255
+ *
256
+ * @param {IndexEntry[]} entries
257
+ * @param {{ now?: () => Date }} [options]
258
+ * @returns {string}
259
+ */
260
+ function serializeIndex(entries, options) {
261
+ const now = options && typeof options.now === 'function' ? options.now() : new Date();
262
+ const lines = [
263
+ '# Feature Map',
264
+ '',
265
+ '> Single source of truth — sharded layout (F-089). Each feature has its own file in `features/<ID>.md`.',
266
+ '> The index below lists every feature with id, state, and title; load the per-feature file for full details.',
267
+ '',
268
+ '## Features',
269
+ '',
270
+ ];
271
+ if (Array.isArray(entries) && entries.length > 0) {
272
+ for (const e of entries) lines.push(serializeIndexEntry(e));
273
+ lines.push('');
274
+ } else {
275
+ lines.push('<!-- No features yet. Run /cap:brainstorm or add features. -->');
276
+ lines.push('');
277
+ }
278
+ lines.push('## Legend');
279
+ lines.push('');
280
+ lines.push('| State | Meaning |');
281
+ lines.push('|-------|---------|');
282
+ lines.push('| planned | Feature identified, not yet implemented |');
283
+ lines.push('| prototyped | Initial implementation exists |');
284
+ lines.push('| tested | Tests written and passing |');
285
+ lines.push('| shipped | Deployed / merged to main |');
286
+ lines.push('');
287
+ lines.push('---');
288
+ lines.push(`*Last updated: ${now.toISOString()}*`);
289
+ lines.push('');
290
+ return lines.join('\n');
291
+ }
292
+
293
+ module.exports = {
294
+ // Constants
295
+ FEATURES_DIR,
296
+ FEATURE_MAP_FILE,
297
+ MAX_ID_LENGTH,
298
+ FEATURE_ID_PATTERN,
299
+ // Validation + path helpers
300
+ validateFeatureId,
301
+ featureFilename,
302
+ featuresDirPath,
303
+ featureFilePath,
304
+ isShardedMap,
305
+ // Index parse/serialize
306
+ parseIndexLine,
307
+ serializeIndexEntry,
308
+ parseIndex,
309
+ serializeIndex,
310
+ // Surgical updates
311
+ _updateIndexEntry,
312
+ _appendIndexEntry,
313
+ // Internal (exposed for tests)
314
+ _escapeRegex,
315
+ };