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,519 @@
1
+ // @cap-feature(feature:F-031) Conversation Thread Tracking — persist brainstorm sessions as named threads with branching and topic detection
2
+ // @cap-decision Pure logic module with explicit I/O functions — same pattern as cap-memory-engine.cjs. No side effects in analysis functions.
3
+ // @cap-decision Thread storage uses individual JSON files per thread plus a central index — enables git-friendly diffs and parallel team access.
4
+ // @cap-constraint Zero external dependencies — uses only Node.js built-ins (fs, path, crypto).
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+ const crypto = require('node:crypto');
11
+
12
+ // --- Constants ---
13
+
14
+ /** Directory for thread storage relative to project root. */
15
+ const THREADS_DIR = path.join('.cap', 'memory', 'threads');
16
+
17
+ /** Thread index file relative to project root. */
18
+ const THREAD_INDEX_FILE = path.join('.cap', 'memory', 'thread-index.json');
19
+
20
+ /** Minimum keyword overlap ratio (0-1) to consider threads as revisiting the same topic. */
21
+ const REVISIT_KEYWORD_THRESHOLD = 0.25;
22
+
23
+ /** Minimum number of shared keywords required for a revisit match. */
24
+ const REVISIT_MIN_SHARED_KEYWORDS = 2;
25
+
26
+ /** Stop words excluded from keyword extraction. */
27
+ const STOP_WORDS = new Set([
28
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
29
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
30
+ 'should', 'may', 'might', 'shall', 'can', 'need', 'must', 'ought',
31
+ 'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either',
32
+ 'neither', 'each', 'every', 'all', 'any', 'few', 'more', 'most',
33
+ 'other', 'some', 'such', 'no', 'only', 'own', 'same', 'than',
34
+ 'too', 'very', 'just', 'because', 'as', 'until', 'while', 'of',
35
+ 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'through',
36
+ 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up',
37
+ 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again',
38
+ 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why',
39
+ 'how', 'what', 'which', 'who', 'whom', 'this', 'that', 'these',
40
+ 'those', 'i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'you',
41
+ 'your', 'yours', 'he', 'him', 'his', 'she', 'her', 'hers', 'it',
42
+ 'its', 'they', 'them', 'their', 'theirs', 'also', 'into', 'if',
43
+ ]);
44
+
45
+ // --- Types ---
46
+
47
+ /**
48
+ * @typedef {Object} Thread
49
+ * @property {string} id - Unique thread ID (e.g., "thr-a1b2c3d4")
50
+ * @property {string} name - Human-readable thread name (derived from problem statement)
51
+ * @property {string} timestamp - ISO timestamp when thread was created
52
+ * @property {string|null} parentThreadId - Parent thread ID if branched, null otherwise
53
+ * @property {string|null} divergencePoint - Description of where this thread diverged from parent
54
+ * @property {string} problemStatement - The problem or topic being explored
55
+ * @property {string} solutionShape - High-level solution direction discovered
56
+ * @property {string[]} boundaryDecisions - Key boundary/scope decisions made during brainstorm
57
+ * @property {string[]} featureIds - Feature Map entries (F-IDs) that resulted from this thread
58
+ * @property {string[]} keywords - Extracted problem-space keywords for topic matching
59
+ */
60
+
61
+ /**
62
+ * @typedef {Object} ThreadIndexEntry
63
+ * @property {string} id - Thread ID
64
+ * @property {string} name - Thread name
65
+ * @property {string} timestamp - ISO timestamp
66
+ * @property {string[]} featureIds - Associated feature IDs
67
+ * @property {string|null} parentThreadId - Parent thread ID if branched
68
+ * @property {string[]} keywords - Problem-space keywords
69
+ */
70
+
71
+ /**
72
+ * @typedef {Object} ThreadIndex
73
+ * @property {string} version - Index schema version
74
+ * @property {ThreadIndexEntry[]} threads - All thread index entries
75
+ */
76
+
77
+ /**
78
+ * @typedef {Object} RevisitMatch
79
+ * @property {string} threadId - Matching thread ID
80
+ * @property {string} threadName - Matching thread name
81
+ * @property {number} keywordOverlap - Number of shared keywords
82
+ * @property {string[]} sharedKeywords - The overlapping keywords
83
+ * @property {string[]} sharedFeatureIds - Overlapping feature IDs
84
+ * @property {number} score - Combined relevance score (0-1)
85
+ */
86
+
87
+ // --- Thread ID Generation ---
88
+
89
+ // @cap-decision Thread IDs use crypto.randomBytes for uniqueness — deterministic IDs (content hash) would collide when two threads start from the same problem statement.
90
+
91
+ /**
92
+ * Generate a unique thread ID.
93
+ * @returns {string} Thread ID in format "thr-{8 hex chars}"
94
+ */
95
+ function generateThreadId() {
96
+ const bytes = crypto.randomBytes(4);
97
+ return 'thr-' + bytes.toString('hex');
98
+ }
99
+
100
+ // --- Keyword Extraction ---
101
+
102
+ // @cap-todo(ac:F-031/AC-3) Keyword extraction for topic revisit detection
103
+
104
+ /**
105
+ * Extract problem-space keywords from text.
106
+ * Filters stop words, short words, and normalizes to lowercase.
107
+ * @param {string} text - Input text (problem statement, solution shape, etc.)
108
+ * @returns {string[]} Deduplicated sorted keywords
109
+ */
110
+ function extractKeywords(text) {
111
+ if (!text || typeof text !== 'string') return [];
112
+
113
+ const words = text
114
+ .toLowerCase()
115
+ .replace(/[^a-z0-9\s-]/g, ' ')
116
+ .split(/\s+/)
117
+ .filter(w => w.length >= 3 && !STOP_WORDS.has(w));
118
+
119
+ return [...new Set(words)].sort();
120
+ }
121
+
122
+ // --- Thread Creation ---
123
+
124
+ // @cap-todo(ac:F-031/AC-1) Persist each brainstorm session as a named thread with unique ID, timestamp, and parent reference
125
+ // @cap-todo(ac:F-031/AC-2) Capture full discovery context: problem statement, solution shape, boundary decisions, feature IDs
126
+
127
+ /**
128
+ * Create a new thread object from brainstorm session data.
129
+ * @param {Object} params
130
+ * @param {string} params.problemStatement - The problem or topic being explored
131
+ * @param {string} [params.solutionShape] - High-level solution direction
132
+ * @param {string[]} [params.boundaryDecisions] - Key boundary/scope decisions
133
+ * @param {string[]} [params.featureIds] - Resulting Feature Map entry IDs
134
+ * @param {string|null} [params.parentThreadId] - Parent thread ID if branching
135
+ * @param {string|null} [params.divergencePoint] - Where this diverges from parent
136
+ * @param {string} [params.name] - Optional human-readable name (auto-derived if omitted)
137
+ * @returns {Thread}
138
+ */
139
+ function createThread(params) {
140
+ const {
141
+ problemStatement,
142
+ solutionShape = '',
143
+ boundaryDecisions = [],
144
+ featureIds = [],
145
+ parentThreadId = null,
146
+ divergencePoint = null,
147
+ name = null,
148
+ } = params;
149
+
150
+ const id = generateThreadId();
151
+ const timestamp = new Date().toISOString();
152
+
153
+ // Auto-derive name from problem statement: first 60 chars, trimmed at word boundary
154
+ const derivedName = name || deriveName(problemStatement);
155
+
156
+ // Extract keywords from all textual content
157
+ const allText = [problemStatement, solutionShape, ...boundaryDecisions].join(' ');
158
+ const keywords = extractKeywords(allText);
159
+
160
+ return {
161
+ id,
162
+ name: derivedName,
163
+ timestamp,
164
+ parentThreadId,
165
+ divergencePoint,
166
+ problemStatement,
167
+ solutionShape,
168
+ boundaryDecisions,
169
+ featureIds,
170
+ keywords,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Derive a human-readable name from a problem statement.
176
+ * @param {string} problemStatement
177
+ * @returns {string}
178
+ */
179
+ function deriveName(problemStatement) {
180
+ if (!problemStatement) return 'Untitled Thread';
181
+ const trimmed = problemStatement.substring(0, 60).trim();
182
+ // Trim at last word boundary if we truncated
183
+ if (problemStatement.length > 60) {
184
+ const lastSpace = trimmed.lastIndexOf(' ');
185
+ if (lastSpace > 20) return trimmed.substring(0, lastSpace) + '...';
186
+ return trimmed + '...';
187
+ }
188
+ return trimmed;
189
+ }
190
+
191
+ // --- Thread Branching ---
192
+
193
+ // @cap-todo(ac:F-031/AC-4) Support thread branching with parent thread ID and divergence point
194
+
195
+ /**
196
+ * Create a branched thread from an existing parent thread.
197
+ * @param {Thread} parentThread - The thread to branch from
198
+ * @param {Object} params - Same as createThread params (minus parentThreadId/divergencePoint)
199
+ * @param {string} params.problemStatement - The divergent problem statement
200
+ * @param {string} params.divergencePoint - Description of where/why the branch diverged
201
+ * @param {string} [params.solutionShape]
202
+ * @param {string[]} [params.boundaryDecisions]
203
+ * @param {string[]} [params.featureIds]
204
+ * @param {string} [params.name]
205
+ * @returns {Thread}
206
+ */
207
+ function branchThread(parentThread, params) {
208
+ return createThread({
209
+ ...params,
210
+ parentThreadId: parentThread.id,
211
+ divergencePoint: params.divergencePoint || null,
212
+ });
213
+ }
214
+
215
+ // --- Topic Revisit Detection ---
216
+
217
+ // @cap-todo(ac:F-031/AC-3) Detect when a brainstorm session revisits a topic covered by an existing thread
218
+
219
+ /**
220
+ * Compute keyword overlap between two keyword sets.
221
+ * @param {string[]} keywordsA
222
+ * @param {string[]} keywordsB
223
+ * @returns {{ shared: string[], overlapRatio: number }}
224
+ */
225
+ function computeKeywordOverlap(keywordsA, keywordsB) {
226
+ const setB = new Set(keywordsB);
227
+ const shared = keywordsA.filter(k => setB.has(k));
228
+ const unionSize = new Set([...keywordsA, ...keywordsB]).size;
229
+ const overlapRatio = unionSize > 0 ? shared.length / unionSize : 0;
230
+ return { shared, overlapRatio };
231
+ }
232
+
233
+ /**
234
+ * Detect threads that revisit a given topic.
235
+ * Compares problem-space keywords and feature IDs against existing thread index.
236
+ * @param {ThreadIndex} index - Current thread index
237
+ * @param {Object} params
238
+ * @param {string} params.problemStatement - New session's problem statement
239
+ * @param {string[]} [params.featureIds] - Feature IDs referenced in new session
240
+ * @param {string} [params.solutionShape] - Solution direction text
241
+ * @param {number} [params.keywordThreshold] - Override keyword overlap threshold
242
+ * @param {number} [params.minSharedKeywords] - Override minimum shared keywords
243
+ * @returns {RevisitMatch[]} Matching threads sorted by relevance score descending
244
+ */
245
+ function detectRevisits(index, params) {
246
+ const {
247
+ problemStatement,
248
+ featureIds = [],
249
+ solutionShape = '',
250
+ keywordThreshold = REVISIT_KEYWORD_THRESHOLD,
251
+ minSharedKeywords = REVISIT_MIN_SHARED_KEYWORDS,
252
+ } = params;
253
+
254
+ if (!index || !index.threads || index.threads.length === 0) return [];
255
+
256
+ const newKeywords = extractKeywords([problemStatement, solutionShape].join(' '));
257
+ if (newKeywords.length === 0 && featureIds.length === 0) return [];
258
+
259
+ const newFeatureSet = new Set(featureIds);
260
+ const matches = [];
261
+
262
+ for (const entry of index.threads) {
263
+ const { shared, overlapRatio } = computeKeywordOverlap(newKeywords, entry.keywords || []);
264
+ const sharedFeatureIds = (entry.featureIds || []).filter(f => newFeatureSet.has(f));
265
+
266
+ // Score: weighted combination of keyword overlap and feature ID overlap
267
+ const keywordScore = overlapRatio;
268
+ const featureScore = sharedFeatureIds.length > 0 ? 0.5 : 0;
269
+ const score = keywordScore * 0.6 + featureScore * 0.4;
270
+
271
+ const meetsKeywordThreshold = overlapRatio >= keywordThreshold && shared.length >= minSharedKeywords;
272
+ const hasFeatureOverlap = sharedFeatureIds.length > 0;
273
+
274
+ if (meetsKeywordThreshold || hasFeatureOverlap) {
275
+ matches.push({
276
+ threadId: entry.id,
277
+ threadName: entry.name,
278
+ keywordOverlap: shared.length,
279
+ sharedKeywords: shared,
280
+ sharedFeatureIds,
281
+ score,
282
+ });
283
+ }
284
+ }
285
+
286
+ // Sort by score descending
287
+ matches.sort((a, b) => b.score - a.score);
288
+ return matches;
289
+ }
290
+
291
+ // --- Thread Index Management ---
292
+
293
+ // @cap-todo(ac:F-031/AC-5) Store thread metadata in .cap/memory/thread-index.json
294
+
295
+ /**
296
+ * Create an empty thread index.
297
+ * @returns {ThreadIndex}
298
+ */
299
+ function createEmptyIndex() {
300
+ return {
301
+ version: '1.0.0',
302
+ threads: [],
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Add a thread to the index.
308
+ * @param {ThreadIndex} index - Current index (mutated in place)
309
+ * @param {Thread} thread - Thread to add
310
+ * @returns {ThreadIndex} The updated index
311
+ */
312
+ function addToIndex(index, thread) {
313
+ // Remove existing entry with same ID (idempotent upsert)
314
+ index.threads = index.threads.filter(t => t.id !== thread.id);
315
+
316
+ index.threads.push({
317
+ id: thread.id,
318
+ name: thread.name,
319
+ timestamp: thread.timestamp,
320
+ featureIds: thread.featureIds,
321
+ parentThreadId: thread.parentThreadId,
322
+ keywords: thread.keywords,
323
+ });
324
+
325
+ return index;
326
+ }
327
+
328
+ /**
329
+ * Remove a thread from the index by ID.
330
+ * @param {ThreadIndex} index
331
+ * @param {string} threadId
332
+ * @returns {ThreadIndex}
333
+ */
334
+ function removeFromIndex(index, threadId) {
335
+ index.threads = index.threads.filter(t => t.id !== threadId);
336
+ return index;
337
+ }
338
+
339
+ // --- File I/O ---
340
+
341
+ // @cap-todo(ac:F-031/AC-6) Thread data shall be git-committable (not gitignored)
342
+ // @cap-decision Threads stored as individual JSON files — each thread is a single atomic file, enabling clean git diffs and minimal merge conflicts.
343
+
344
+ /**
345
+ * Load the thread index from disk.
346
+ * @param {string} projectRoot - Absolute path to project root
347
+ * @returns {ThreadIndex}
348
+ */
349
+ function loadIndex(projectRoot) {
350
+ const indexPath = path.join(projectRoot, THREAD_INDEX_FILE);
351
+ try {
352
+ if (!fs.existsSync(indexPath)) return createEmptyIndex();
353
+ const content = fs.readFileSync(indexPath, 'utf8');
354
+ const parsed = JSON.parse(content);
355
+ // Merge with defaults for forward compatibility
356
+ return { ...createEmptyIndex(), ...parsed };
357
+ } catch (_e) {
358
+ return createEmptyIndex();
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Save the thread index to disk.
364
+ * @param {string} projectRoot - Absolute path to project root
365
+ * @param {ThreadIndex} index
366
+ */
367
+ function saveIndex(projectRoot, index) {
368
+ const indexPath = path.join(projectRoot, THREAD_INDEX_FILE);
369
+ const dir = path.dirname(indexPath);
370
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
371
+ // @cap-decision Sorted keys and 2-space indent for git-friendly diffs
372
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n', 'utf8');
373
+ }
374
+
375
+ /**
376
+ * Load a single thread from disk.
377
+ * @param {string} projectRoot - Absolute path to project root
378
+ * @param {string} threadId - Thread ID
379
+ * @returns {Thread|null} Thread object or null if not found
380
+ */
381
+ function loadThread(projectRoot, threadId) {
382
+ const threadPath = path.join(projectRoot, THREADS_DIR, `${threadId}.json`);
383
+ try {
384
+ if (!fs.existsSync(threadPath)) return null;
385
+ const content = fs.readFileSync(threadPath, 'utf8');
386
+ return JSON.parse(content);
387
+ } catch (_e) {
388
+ return null;
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Save a single thread to disk.
394
+ * @param {string} projectRoot - Absolute path to project root
395
+ * @param {Thread} thread
396
+ */
397
+ function saveThread(projectRoot, thread) {
398
+ const threadsDir = path.join(projectRoot, THREADS_DIR);
399
+ if (!fs.existsSync(threadsDir)) fs.mkdirSync(threadsDir, { recursive: true });
400
+ const threadPath = path.join(threadsDir, `${thread.id}.json`);
401
+ fs.writeFileSync(threadPath, JSON.stringify(thread, null, 2) + '\n', 'utf8');
402
+ }
403
+
404
+ /**
405
+ * Delete a thread from disk and remove from index.
406
+ * @param {string} projectRoot - Absolute path to project root
407
+ * @param {string} threadId - Thread ID to delete
408
+ * @returns {boolean} True if thread was deleted
409
+ */
410
+ function deleteThread(projectRoot, threadId) {
411
+ const threadPath = path.join(projectRoot, THREADS_DIR, `${threadId}.json`);
412
+ const existed = fs.existsSync(threadPath);
413
+ if (existed) fs.unlinkSync(threadPath);
414
+
415
+ const index = loadIndex(projectRoot);
416
+ removeFromIndex(index, threadId);
417
+ saveIndex(projectRoot, index);
418
+
419
+ return existed;
420
+ }
421
+
422
+ // --- High-Level Convenience Functions ---
423
+
424
+ /**
425
+ * Persist a brainstorm thread: save thread file + update index.
426
+ * Single call for the common case.
427
+ * @param {string} projectRoot - Absolute path to project root
428
+ * @param {Thread} thread - Thread to persist
429
+ * @returns {{ thread: Thread, index: ThreadIndex }}
430
+ */
431
+ function persistThread(projectRoot, thread) {
432
+ saveThread(projectRoot, thread);
433
+ const index = loadIndex(projectRoot);
434
+ addToIndex(index, thread);
435
+ saveIndex(projectRoot, index);
436
+ return { thread, index };
437
+ }
438
+
439
+ // @cap-todo(ac:F-031/AC-7) cap-brainstormer shall check thread index at session start and surface relevant prior threads
440
+
441
+ /**
442
+ * Check for relevant prior threads before starting a new brainstorm session.
443
+ * Returns matching threads and their full data for the brainstormer agent to surface.
444
+ * @param {string} projectRoot - Absolute path to project root
445
+ * @param {Object} params
446
+ * @param {string} params.problemStatement - The new session's problem statement
447
+ * @param {string[]} [params.featureIds] - Feature IDs referenced
448
+ * @param {string} [params.solutionShape] - Solution direction text
449
+ * @returns {{ matches: RevisitMatch[], threads: Thread[] }} Matches with full thread data
450
+ */
451
+ function checkPriorThreads(projectRoot, params) {
452
+ const index = loadIndex(projectRoot);
453
+ const matches = detectRevisits(index, params);
454
+
455
+ // Load full thread data for each match
456
+ const threads = [];
457
+ for (const match of matches) {
458
+ const thread = loadThread(projectRoot, match.threadId);
459
+ if (thread) threads.push(thread);
460
+ }
461
+
462
+ return { matches, threads };
463
+ }
464
+
465
+ /**
466
+ * List all threads, optionally filtered by feature ID.
467
+ * @param {string} projectRoot - Absolute path to project root
468
+ * @param {Object} [options]
469
+ * @param {string} [options.featureId] - Filter by feature ID
470
+ * @returns {ThreadIndexEntry[]}
471
+ */
472
+ function listThreads(projectRoot, options = {}) {
473
+ const index = loadIndex(projectRoot);
474
+ let threads = index.threads;
475
+
476
+ if (options.featureId) {
477
+ threads = threads.filter(t => t.featureIds && t.featureIds.includes(options.featureId));
478
+ }
479
+
480
+ // Sort by timestamp descending (most recent first)
481
+ return [...threads].sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
482
+ }
483
+
484
+ module.exports = {
485
+ // Core
486
+ createThread,
487
+ branchThread,
488
+ detectRevisits,
489
+ extractKeywords,
490
+ computeKeywordOverlap,
491
+ deriveName,
492
+
493
+ // Index management
494
+ createEmptyIndex,
495
+ addToIndex,
496
+ removeFromIndex,
497
+
498
+ // File I/O
499
+ loadIndex,
500
+ saveIndex,
501
+ loadThread,
502
+ saveThread,
503
+ deleteThread,
504
+ persistThread,
505
+
506
+ // High-level
507
+ checkPriorThreads,
508
+ listThreads,
509
+
510
+ // Constants (exposed for testing and configuration)
511
+ THREADS_DIR,
512
+ THREAD_INDEX_FILE,
513
+ REVISIT_KEYWORD_THRESHOLD,
514
+ REVISIT_MIN_SHARED_KEYWORDS,
515
+ STOP_WORDS,
516
+
517
+ // Internal (for testing)
518
+ generateThreadId,
519
+ };