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,698 @@
1
+ // @cap-feature(feature:F-027) Memory Accumulation Engine — detect decisions, pitfalls, patterns, hotspots from session data
2
+ // @cap-decision Pure logic module with no I/O — takes parsed session data as input, outputs structured memory entries. Enables dry-run and unit testing.
3
+ // @cap-decision Relevance-based aging with pinned escape hatch — annotations expire after N sessions without edits, but pinned:true exempts from aging.
4
+ // @cap-constraint Zero external dependencies — uses only Node.js built-ins.
5
+
6
+ 'use strict';
7
+
8
+ // @cap-history(sessions:2, edits:2, since:2026-04-21, learned:2026-05-08) Frequently modified — 2 sessions, 2 edits
9
+ // @cap-history(sessions:2, edits:27, since:2026-04-03, learned:2026-04-03) Frequently modified — 2 sessions, 27 edits
10
+ const fs = require('node:fs');
11
+ const path = require('node:path');
12
+ const confidence = require('./cap-memory-confidence.cjs');
13
+
14
+ // --- Constants ---
15
+
16
+ /** Default number of sessions without edits before annotation is marked stale. */
17
+ const DEFAULT_STALE_THRESHOLD = 5;
18
+
19
+ /** Minimum sessions with edits for a file to qualify as a hotspot. */
20
+ const MIN_HOTSPOT_SESSIONS = 2;
21
+
22
+ /** Minimum successful applications for a pattern to be recorded. */
23
+ const MIN_PATTERN_CONFIRMATIONS = 2;
24
+
25
+ /** Regex patterns for detecting decision-related content in assistant messages.
26
+ * Tightened: require verb+noun combinations, not just isolated keywords. */
27
+ const DECISION_PATTERNS = [
28
+ /(?:(?:I|we) (?:decided|chose|picked|selected|went with)\b)/i,
29
+ /(?:decision(?:\s+(?:was|is|to))\b)/i,
30
+ /(?:trade-?off(?:\s+(?:between|is|was))\b)/i,
31
+ /(?:root cause(?:\s+(?:is|was|:))\b)/i,
32
+ /(?:the fix(?:\s+(?:is|was|needs|requires))\b)/i,
33
+ ];
34
+
35
+ /** Regex patterns for detecting pitfall/failure content.
36
+ * Tightened: require action context, not just isolated words. */
37
+ const PITFALL_PATTERNS = [
38
+ /(?:(?:don't|do not|never|avoid)\s+\w{3,})/i,
39
+ /(?:(?:watch out|careful|gotcha|pitfall|trap)\s+(?:for|with|when|:))/i,
40
+ /(?:hours?\s+(?:of\s+)?debugging)/i,
41
+ /(?:regression\s+(?:in|from|caused|when))/i,
42
+ /(?:(?:this|the)\s+(?:bug|crash|failure)\s+(?:is|was|happens|occurs|caused))/i,
43
+ ];
44
+
45
+ /** Regex patterns for detecting successful patterns.
46
+ * Tightened: require specific recommendation language. */
47
+ const PATTERN_PATTERNS = [
48
+ /(?:this (?:approach|pattern|method) (?:works?|solved|is better))/i,
49
+ /(?:(?:proven|reliable)\s+(?:approach|pattern|method|strategy))/i,
50
+ ];
51
+
52
+ /** Feature ID regex */
53
+ const FEATURE_RE = /F-\d{3}/g;
54
+
55
+ // --- Types ---
56
+
57
+ /**
58
+ * @typedef {'decision'|'pitfall'|'pattern'|'hotspot'} MemoryCategory
59
+ */
60
+
61
+ /**
62
+ * @typedef {Object} MemoryEntry
63
+ * @property {MemoryCategory} category
64
+ * @property {string|null} file - Target file path (null for cross-cutting entries)
65
+ * @property {string} content - Human-readable description
66
+ * @property {Object} metadata
67
+ * @property {string} metadata.source - Source session date
68
+ * @property {string|null} metadata.branch - Git branch
69
+ * @property {string[]} metadata.relatedFiles - Other files involved
70
+ * @property {string[]} metadata.features - Feature IDs referenced
71
+ * @property {boolean} metadata.pinned - Whether exempt from aging
72
+ * @property {number} [metadata.sessions] - Number of sessions (hotspots)
73
+ * @property {number} [metadata.edits] - Total edit count (hotspots)
74
+ * @property {string} [metadata.since] - Earliest session date (hotspots)
75
+ * @property {number} [metadata.confirmations] - Times confirmed (patterns)
76
+ */
77
+
78
+ /**
79
+ * @typedef {Object} ExistingAnnotation
80
+ * @property {MemoryCategory} category
81
+ * @property {string} file
82
+ * @property {string} content
83
+ * @property {boolean} pinned
84
+ * @property {number} lastEditSession - Session index when file was last edited (0 = current)
85
+ */
86
+
87
+ /**
88
+ * @typedef {Object} AccumulationResult
89
+ * @property {MemoryEntry[]} newEntries - New memory entries to write
90
+ * @property {ExistingAnnotation[]} staleEntries - Existing annotations to remove
91
+ * @property {MemoryEntry[]} updatedEntries - Existing annotations to update (e.g., increment counts)
92
+ * @property {Object} stats - Accumulation statistics
93
+ */
94
+
95
+ // --- Session Analysis ---
96
+
97
+ /**
98
+ * Extract text content from a message (mirrors F-025 helper).
99
+ * @param {Object} msg
100
+ * @returns {string}
101
+ */
102
+ function extractText(msg) {
103
+ const content = msg.message?.content;
104
+ if (!content) return '';
105
+ if (typeof content === 'string') return content;
106
+ if (Array.isArray(content)) {
107
+ return content.filter(c => c.type === 'text').map(c => c.text || '').join('\n');
108
+ }
109
+ return '';
110
+ }
111
+
112
+ /**
113
+ * Extract tool uses from a message.
114
+ * @param {Object} msg
115
+ * @returns {Array<{tool: string, input: Object}>}
116
+ */
117
+ function extractTools(msg) {
118
+ const content = msg.message?.content;
119
+ if (!Array.isArray(content)) return [];
120
+ return content.filter(c => c.type === 'tool_use').map(c => ({ tool: c.name, input: c.input }));
121
+ }
122
+
123
+ /**
124
+ * Strip system tags from text.
125
+ * @param {string} text
126
+ * @returns {string}
127
+ */
128
+ function stripTags(text) {
129
+ return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
130
+ }
131
+
132
+ /** Patterns that indicate conversational noise rather than real knowledge. */
133
+ const NOISE_PATTERNS = [
134
+ // Conversational openers/fillers
135
+ /^(Let me |Let's |I'll |I will |Now |OK|Sure|Got it|Done|Great|Perfect|Absolutely|Here's|Here is|Alright)/i,
136
+ // Imperative prompts from agent
137
+ /^(Please |Provide |Check |Run |Show |Read |Write |Create |Update |Delete |Review |Look |Schauen|Lass)/i,
138
+ // Markdown formatting: tables, headers, bold-prefixed lines, quotes, HR, code blocks
139
+ /^\||^```|^#+\s|^\*\*[A-Z]|^>\s|^---|^- \*\*/,
140
+ // Code/command references
141
+ /^`\//,
142
+ // Numbered lists, bullet points
143
+ /^\d+[\.\)]\s|^- [a-z]/,
144
+ // GSD/CAP command references
145
+ /^`?(\/gsd:|\/cap:|cap:|gsd:)/,
146
+ // Status/log messages
147
+ /^(Last scan|Shell cwd|Exit code|Session|Commit|What was generated)/i,
148
+ // German filler / conversational
149
+ /^(Aendere|Weiter mit|Dann |Jetzt |Genau|Besser|Gut |Ich baue|Soll ich|Oder )/i,
150
+ // Agent workflow noise
151
+ /^(Starting|Spawning|Loading|Checking|Analyzing|Processing|Running|Scanning)/i,
152
+ // Contains only markdown bold + colon (structured output, not prose)
153
+ /^\*\*.*:\*\*$/,
154
+ // Lines starting with bold (structured output headers, not decisions)
155
+ /^\*\*\d+-\d+/,
156
+ // Bullet lists that start with "- Discovers", "- Analyzes", etc. (agent workflow descriptions)
157
+ /^- (Discovers|Analyzes|Creates|Produces|Reads|Writes|Returns|Generates|Validates|Checks)/i,
158
+ // Progress reports (e.g., "Von 235 → 146 Decisions")
159
+ /(?:Von|From)\s+\d+\s*[→\-]\s*\d+/i,
160
+ // Lines containing @cap-* or @gsd-* tags (meta-discussion about the system itself)
161
+ /@(?:cap|gsd)-(?:feature|todo|decision|pitfall|history|pattern|risk|constraint|context|ref|api)\b/,
162
+ // Code identifiers / function call references (not prose)
163
+ /^[a-zA-Z_]\w*\.\w+\(|^[a-zA-Z_]\w*\(\)/,
164
+ // Ergebnis/Result summary lines
165
+ /^(Ergebnis|Result|Output|Nächste Schritte|Next steps):/i,
166
+ // Lines that are mostly special characters (ASCII art, box drawing)
167
+ /[─│┌┐└┘┬┴├┤╔╗╚╝]{3,}/,
168
+ // AC table fragments
169
+ /^\|\s*AC-\d/,
170
+ // Lines starting with file paths
171
+ /^`?(?:cap\/|hooks\/|bin\/|commands\/|tests\/|scripts\/|src\/)/,
172
+ // Sentences with trailing markdown code block markers
173
+ /```\s*$/,
174
+ // Meta-discussion about regex patterns or test data
175
+ /(?:Pattern|Regex|regex)\s+`/,
176
+ // Sentences referencing test fixtures or test sentences
177
+ /(?:test sentence|test data|Testfall|Test-Satz)/i,
178
+ // Lines ending with orphaned numbering (e.g., "...visible errors\n4.")
179
+ /^\d+\.$/,
180
+ ];
181
+
182
+ /**
183
+ * Check if a sentence is conversational noise rather than real knowledge.
184
+ * @param {string} text
185
+ * @returns {boolean}
186
+ */
187
+ function isNoise(text) {
188
+ return NOISE_PATTERNS.some(p => p.test(text));
189
+ }
190
+
191
+ /**
192
+ * Normalize a file path for cross-session matching.
193
+ * Strips worktree paths and resolves to monorepo-relative path.
194
+ * @param {string} fp - Absolute file path
195
+ * @param {string|null} projectRoot - Project root to make paths relative
196
+ * @returns {string} Normalized path
197
+ */
198
+ function normalizeFilePath(fp, projectRoot) {
199
+ if (!fp) return fp;
200
+ let normalized = fp;
201
+ // Strip worktree prefix: .claude/worktrees/<name>/ → ""
202
+ normalized = normalized.replace(/\.claude\/worktrees\/[^/]+\//, '');
203
+ // Strip /private/var/folders temp paths for worktrees
204
+ const worktreeMatch = normalized.match(/\/private\/var\/.*?\/([^/]+)\/(.*)/);
205
+ if (worktreeMatch) {
206
+ // Try to find the project name in the path
207
+ normalized = worktreeMatch[2] || normalized;
208
+ }
209
+ // Make relative to project root if provided
210
+ if (projectRoot && normalized.startsWith(projectRoot)) {
211
+ normalized = normalized.substring(projectRoot.length).replace(/^\//, '');
212
+ }
213
+ return normalized;
214
+ }
215
+
216
+ /**
217
+ * Analyze a single parsed session for memory-worthy content.
218
+ * @param {Object} parsed - { meta, messages } from parseSession
219
+ * @param {Object} [options]
220
+ * @param {boolean} [options.isDebugSession] - Whether this session was a debug session
221
+ * @param {string} [options.projectRoot] - Project root for path normalization
222
+ * @returns {{decisions: string[], pitfalls: string[], patterns: string[], editedFiles: Object<string, number>, features: Set<string>}}
223
+ */
224
+ function analyzeSession(parsed, options = {}) {
225
+ const { meta, messages } = parsed;
226
+ const decisions = [];
227
+ const pitfalls = [];
228
+ const patterns = [];
229
+ const editedFiles = {}; // path -> edit count
230
+ const features = new Set();
231
+
232
+ for (const msg of messages) {
233
+ if (msg.type !== 'assistant') continue;
234
+
235
+ // File edits: collect from ALL messages including subagents
236
+ for (const tool of extractTools(msg)) {
237
+ if (tool.tool === 'Write' || tool.tool === 'Edit' || tool.tool === 'MultiEdit') {
238
+ const rawFp = tool.input?.file_path || tool.input?.filePath || null;
239
+ if (rawFp) {
240
+ // Skip planning/memory/config artifacts — not real source code hotspots
241
+ if (/\.(planning|cap)\/|memory\/|\.claude\/|SESSION\.json|MEMORY\.md|STATE\.md/.test(rawFp)) continue;
242
+ const fp = normalizeFilePath(rawFp, options.projectRoot || null);
243
+ editedFiles[fp] = (editedFiles[fp] || 0) + 1;
244
+ }
245
+ }
246
+ }
247
+
248
+ // Text analysis: include subagent messages (they contain decisions/pitfalls too)
249
+ const text = stripTags(extractText(msg));
250
+ if (!text) continue;
251
+
252
+ // Collect feature references
253
+ const featureMatches = text.match(FEATURE_RE);
254
+ if (featureMatches) featureMatches.forEach(f => features.add(f));
255
+
256
+ // Extract sentences — skip markdown formatting artifacts
257
+ const sentences = text.split(/(?<=[.!?\n])\s+/);
258
+ for (const sentence of sentences) {
259
+ if (sentence.length < 40 || sentence.length > 300) continue;
260
+ const clean = sentence.trim();
261
+ if (isNoise(clean)) continue;
262
+
263
+ if (DECISION_PATTERNS.some(p => p.test(clean))) {
264
+ decisions.push(clean);
265
+ }
266
+ if (PITFALL_PATTERNS.some(p => p.test(clean))) {
267
+ pitfalls.push(clean);
268
+ }
269
+ if (PATTERN_PATTERNS.some(p => p.test(clean))) {
270
+ patterns.push(clean);
271
+ }
272
+ }
273
+ }
274
+
275
+ return {
276
+ decisions: [...new Set(decisions)],
277
+ pitfalls: [...new Set(pitfalls)],
278
+ patterns: [...new Set(patterns)],
279
+ editedFiles,
280
+ features,
281
+ date: meta?.timestamp || null,
282
+ branch: meta?.branch || null,
283
+ };
284
+ }
285
+
286
+ // --- Cross-Session Accumulation ---
287
+
288
+ // @cap-todo(ref:F-027:AC-2) Detect four memory categories: decisions, pitfalls, patterns, hotspots
289
+
290
+ /**
291
+ * Accumulate memory from multiple analyzed sessions.
292
+ * @param {Array<ReturnType<typeof analyzeSession>>} sessionAnalyses - Results from analyzeSession for each session
293
+ * @param {Object} [options]
294
+ * @param {number} [options.staleThreshold] - Sessions without edits before stale (default: 5)
295
+ * @param {number} [options.minHotspotSessions] - Min sessions for hotspot (default: 2)
296
+ * @param {number} [options.minPatternConfirmations] - Min confirmations for pattern (default: 2)
297
+ * @param {ExistingAnnotation[]} [options.existingAnnotations] - Current annotations in code
298
+ * @returns {AccumulationResult}
299
+ */
300
+ function accumulate(sessionAnalyses, options = {}) {
301
+ const staleThreshold = options.staleThreshold || DEFAULT_STALE_THRESHOLD;
302
+ const minHotspot = options.minHotspotSessions || MIN_HOTSPOT_SESSIONS;
303
+ const minPattern = options.minPatternConfirmations || MIN_PATTERN_CONFIRMATIONS;
304
+ const existing = options.existingAnnotations || [];
305
+
306
+ const newEntries = [];
307
+ const updatedEntries = [];
308
+ const staleEntries = [];
309
+
310
+ // --- Hotspots (AC-3): files edited across multiple sessions ---
311
+ const fileSessionMap = {}; // path -> { sessions: Set, totalEdits: number, earliestDate: string }
312
+ for (const analysis of sessionAnalyses) {
313
+ for (const [fp, editCount] of Object.entries(analysis.editedFiles)) {
314
+ if (!fileSessionMap[fp]) {
315
+ fileSessionMap[fp] = { sessions: new Set(), totalEdits: 0, earliestDate: analysis.date };
316
+ }
317
+ fileSessionMap[fp].sessions.add(analysis.date || 'unknown');
318
+ fileSessionMap[fp].totalEdits += editCount;
319
+ if (analysis.date && (!fileSessionMap[fp].earliestDate || analysis.date < fileSessionMap[fp].earliestDate)) {
320
+ fileSessionMap[fp].earliestDate = analysis.date;
321
+ }
322
+ }
323
+ }
324
+
325
+ for (const [fp, data] of Object.entries(fileSessionMap)) {
326
+ if (data.sessions.size >= minHotspot) {
327
+ newEntries.push({
328
+ category: 'hotspot',
329
+ file: fp,
330
+ content: `Frequently modified — ${data.sessions.size} sessions, ${data.totalEdits} edits`,
331
+ metadata: {
332
+ source: [...data.sessions].sort().pop(),
333
+ branch: null,
334
+ relatedFiles: [],
335
+ features: [],
336
+ pinned: false,
337
+ sessions: data.sessions.size,
338
+ edits: data.totalEdits,
339
+ since: data.earliestDate ? data.earliestDate.substring(0, 10) : null,
340
+ },
341
+ });
342
+ }
343
+ }
344
+
345
+ // --- Decisions (AC-2): collect unique decisions across sessions ---
346
+ const seenDecisions = new Set();
347
+ for (const analysis of sessionAnalyses) {
348
+ for (const decision of analysis.decisions) {
349
+ const key = decision.substring(0, 80).toLowerCase();
350
+ if (seenDecisions.has(key)) continue;
351
+ seenDecisions.add(key);
352
+
353
+ // Find which files were edited in the same session
354
+ const relatedFiles = Object.keys(analysis.editedFiles).slice(0, 5);
355
+
356
+ newEntries.push({
357
+ category: 'decision',
358
+ file: relatedFiles[0] || null, // primary file, or cross-cutting
359
+ content: decision,
360
+ metadata: {
361
+ source: analysis.date,
362
+ branch: analysis.branch,
363
+ relatedFiles,
364
+ features: [...analysis.features],
365
+ pinned: false,
366
+ },
367
+ });
368
+ }
369
+ }
370
+
371
+ // --- Pitfalls (AC-4): only from debug sessions or explicit failure context ---
372
+ const seenPitfalls = new Set();
373
+ for (const analysis of sessionAnalyses) {
374
+ for (const pitfall of analysis.pitfalls) {
375
+ const key = pitfall.substring(0, 80).toLowerCase();
376
+ if (seenPitfalls.has(key)) continue;
377
+ seenPitfalls.add(key);
378
+
379
+ const relatedFiles = Object.keys(analysis.editedFiles).slice(0, 5);
380
+
381
+ newEntries.push({
382
+ category: 'pitfall',
383
+ file: relatedFiles[0] || null,
384
+ content: pitfall,
385
+ metadata: {
386
+ source: analysis.date,
387
+ branch: analysis.branch,
388
+ relatedFiles,
389
+ features: [...analysis.features],
390
+ pinned: false,
391
+ },
392
+ });
393
+ }
394
+ }
395
+
396
+ // --- Patterns (AC-5): only when confirmed across multiple sessions ---
397
+ const patternCounts = {}; // normalized key -> { content, count, sessions }
398
+ for (const analysis of sessionAnalyses) {
399
+ for (const pattern of analysis.patterns) {
400
+ const key = pattern.substring(0, 80).toLowerCase();
401
+ if (!patternCounts[key]) {
402
+ patternCounts[key] = { content: pattern, count: 0, sessions: [], relatedFiles: new Set(), features: new Set() };
403
+ }
404
+ patternCounts[key].count++;
405
+ if (analysis.date) patternCounts[key].sessions.push(analysis.date);
406
+ Object.keys(analysis.editedFiles).forEach(f => patternCounts[key].relatedFiles.add(f));
407
+ analysis.features.forEach(f => patternCounts[key].features.add(f));
408
+ }
409
+ }
410
+
411
+ for (const [, data] of Object.entries(patternCounts)) {
412
+ if (data.count >= minPattern) {
413
+ newEntries.push({
414
+ category: 'pattern',
415
+ file: [...data.relatedFiles][0] || null,
416
+ content: data.content,
417
+ metadata: {
418
+ source: data.sessions.sort().pop(),
419
+ branch: null,
420
+ relatedFiles: [...data.relatedFiles].slice(0, 5),
421
+ features: [...data.features],
422
+ pinned: false,
423
+ confirmations: data.count,
424
+ },
425
+ });
426
+ }
427
+ }
428
+
429
+ // --- Aging (AC-6, AC-7): check existing annotations for staleness ---
430
+ const currentlyEditedFiles = new Set();
431
+ for (const analysis of sessionAnalyses) {
432
+ for (const fp of Object.keys(analysis.editedFiles)) {
433
+ currentlyEditedFiles.add(fp);
434
+ }
435
+ }
436
+
437
+ for (const annotation of existing) {
438
+ // AC-7: pinned annotations never go stale
439
+ if (annotation.pinned) continue;
440
+
441
+ if (annotation.lastEditSession >= staleThreshold && !currentlyEditedFiles.has(annotation.file)) {
442
+ staleEntries.push(annotation);
443
+ }
444
+ }
445
+
446
+ // --- Stats ---
447
+ const stats = {
448
+ sessionsAnalyzed: sessionAnalyses.length,
449
+ hotspots: newEntries.filter(e => e.category === 'hotspot').length,
450
+ decisions: newEntries.filter(e => e.category === 'decision').length,
451
+ pitfalls: newEntries.filter(e => e.category === 'pitfall').length,
452
+ patterns: newEntries.filter(e => e.category === 'pattern').length,
453
+ stale: staleEntries.length,
454
+ total: newEntries.length,
455
+ };
456
+
457
+ return { newEntries, staleEntries, updatedEntries, stats };
458
+ }
459
+
460
+ // --- Code-Based Memory (primary source — code is the single source of truth) ---
461
+
462
+ /**
463
+ * Extract memory entries from code tags (via tag scanner).
464
+ * Code tags are high-signal, zero-noise — they are explicit developer annotations.
465
+ *
466
+ * Learning signals (F-055): when `options.existingEntries` is supplied and
467
+ * `options.learningSignals !== false`, each new entry is compared against the
468
+ * existing set. Matches are merged as re-observations (confidence bump, evidence +1);
469
+ * contradictions damp the existing entry's confidence without merging.
470
+ *
471
+ * @param {Array<{type: string, file: string, line: number, metadata: Object, description: string, subtype: string|null}>} tags - Tags from cap-tag-scanner
472
+ * @param {Object} [options]
473
+ * @param {Array} [options.existingEntries] - Entries previously read from .cap/memory/*.md (for learning signals)
474
+ * @param {boolean} [options.learningSignals] - Default true; opt-out for callers that want raw extraction
475
+ * @returns {MemoryEntry[]}
476
+ */
477
+ function accumulateFromCode(tags, options = {}) {
478
+ const entries = [];
479
+ const seen = new Set();
480
+
481
+ for (const tag of tags) {
482
+ // @cap-decision tags → decision entries
483
+ if (tag.type === 'decision') {
484
+ if (!tag.description || tag.description.length < 10) continue;
485
+ const key = tag.description.substring(0, 80).toLowerCase();
486
+ if (seen.has('d:' + key)) continue;
487
+ seen.add('d:' + key);
488
+
489
+ // @cap-todo(ac:F-055/AC-2) New entries start with confidence:0.5, evidence_count:1 (injected via initFields).
490
+ // @cap-feature(feature:F-091, primary:true) Source-aware initial confidence — explicit
491
+ // @cap-decision tags start at 0.8 so they survive the F-090 filter (threshold 0.6)
492
+ // on first emission instead of needing re-observation.
493
+ entries.push({
494
+ category: 'decision',
495
+ file: tag.file,
496
+ content: tag.description,
497
+ metadata: {
498
+ source: 'code',
499
+ branch: null,
500
+ relatedFiles: [tag.file],
501
+ features: tag.metadata?.feature ? [tag.metadata.feature] : [],
502
+ pinned: false,
503
+ line: tag.line,
504
+ ...confidence.initFields({
505
+ initialConfidence: confidence.initialConfidenceForSource('cap-decision'),
506
+ }),
507
+ },
508
+ });
509
+ }
510
+
511
+ // Extract pitfalls from @cap-todo with risk: subtype
512
+ if (tag.type === 'todo' && tag.subtype === 'risk') {
513
+ const desc = tag.description.replace(/^risk:\s*/i, '');
514
+ if (!desc || desc.length < 10) continue;
515
+ const key = desc.substring(0, 80).toLowerCase();
516
+ if (seen.has('p:' + key)) continue;
517
+ seen.add('p:' + key);
518
+
519
+ // @cap-feature(feature:F-091) Explicit @cap-todo risk: subtype starts at 0.7.
520
+ entries.push({
521
+ category: 'pitfall',
522
+ file: tag.file,
523
+ content: desc,
524
+ metadata: {
525
+ source: 'code',
526
+ branch: null,
527
+ relatedFiles: [tag.file],
528
+ features: tag.metadata?.feature ? [tag.metadata.feature] : [],
529
+ pinned: false,
530
+ line: tag.line,
531
+ ...confidence.initFields({
532
+ initialConfidence: confidence.initialConfidenceForSource('cap-todo-risk'),
533
+ }),
534
+ },
535
+ });
536
+ }
537
+
538
+ // Extract pitfalls from standalone @cap-risk tags
539
+ if (tag.type === 'risk') {
540
+ if (!tag.description || tag.description.length < 10) continue;
541
+ const key = tag.description.substring(0, 80).toLowerCase();
542
+ if (seen.has('p:' + key)) continue;
543
+ seen.add('p:' + key);
544
+
545
+ // @cap-feature(feature:F-091) Standalone @cap-risk starts at 0.7.
546
+ entries.push({
547
+ category: 'pitfall',
548
+ file: tag.file,
549
+ content: tag.description,
550
+ metadata: {
551
+ source: 'code',
552
+ branch: null,
553
+ relatedFiles: [tag.file],
554
+ features: tag.metadata?.feature ? [tag.metadata.feature] : [],
555
+ pinned: false,
556
+ line: tag.line,
557
+ ...confidence.initFields({
558
+ initialConfidence: confidence.initialConfidenceForSource('cap-risk'),
559
+ }),
560
+ },
561
+ });
562
+ }
563
+ }
564
+
565
+ // Learning signals: opt-in via existingEntries presence; can be disabled explicitly.
566
+ const signalsEnabled = options.learningSignals !== false && Array.isArray(options.existingEntries);
567
+ if (!signalsEnabled) return entries;
568
+
569
+ // Work on a mutable copy so contradictions can rewrite existing entries in-place for the caller's view.
570
+ const existing = options.existingEntries.map((e) => ({
571
+ ...e,
572
+ metadata: { ...(e.metadata || {}) },
573
+ }));
574
+
575
+ const result = [];
576
+ // Track existing-entry indices that have been damped so multiple new entries
577
+ // contradicting the same existing one don't emit duplicate damped records.
578
+ // The final damp state already lives in existing[i] (applyLearningSignals
579
+ // reads the mutated existing array on each iteration), so we emit each
580
+ // touched index exactly once at the end.
581
+ const dampedIndices = new Set();
582
+ for (const newEntry of entries) {
583
+ const { mergedEntry, action } = confidence.applyLearningSignals(newEntry, existing);
584
+
585
+ if (action === 'reobserved') {
586
+ // @cap-todo(ac:F-055/AC-4) Re-observation: bump evidence + confidence on the existing entry.
587
+ result.push(mergedEntry);
588
+ } else if (action === 'contradicted') {
589
+ // @cap-todo(ac:F-055/AC-5) Contradiction: damp the existing entry's confidence; keep the new entry as a separate observation.
590
+ const updateRec = mergedEntry._contradictedExistingUpdate;
591
+ if (updateRec && existing[updateRec.index]) {
592
+ existing[updateRec.index] = { ...existing[updateRec.index], metadata: updateRec.updatedMetadata };
593
+ dampedIndices.add(updateRec.index);
594
+ }
595
+ const clean = { ...mergedEntry };
596
+ delete clean._contradictedExistingUpdate;
597
+ result.push(clean);
598
+ } else {
599
+ result.push(mergedEntry);
600
+ }
601
+ }
602
+
603
+ for (const idx of dampedIndices) result.push(existing[idx]);
604
+
605
+ return result;
606
+ }
607
+
608
+ // --- Convenience: Full Pipeline Input ---
609
+
610
+ /**
611
+ * Parse raw JSONL session files and accumulate memory.
612
+ * Sessions provide HOTSPOTS ONLY (edit frequency). Decisions/pitfalls come from code tags.
613
+ * @param {Array<{path: string, isDebugSession?: boolean}>} sessionFiles - Session file descriptors
614
+ * @param {Object} [options] - Options passed to accumulate()
615
+ * @param {string} [options.projectRoot] - Project root for file path normalization (monorepo-aware)
616
+ * @returns {AccumulationResult}
617
+ */
618
+ function accumulateFromFiles(sessionFiles, options = {}) {
619
+ const analyses = [];
620
+
621
+ for (const sf of sessionFiles) {
622
+ try {
623
+ const lines = fs.readFileSync(sf.path, 'utf8').trim().split('\n');
624
+ const messages = [];
625
+ let meta = null;
626
+
627
+ for (const line of lines) {
628
+ try {
629
+ const obj = JSON.parse(line);
630
+ if (obj.sessionId && !meta) {
631
+ meta = { id: obj.sessionId, timestamp: obj.timestamp || null, branch: obj.gitBranch || null };
632
+ }
633
+ if (!meta?.timestamp && obj.timestamp) meta.timestamp = obj.timestamp;
634
+ if (obj.type === 'user' || obj.type === 'assistant') messages.push(obj);
635
+ } catch { /* skip malformed */ }
636
+ }
637
+
638
+ const analysis = analyzeSession(
639
+ { meta: meta || { id: 'unknown', timestamp: null, branch: null }, messages },
640
+ { isDebugSession: sf.isDebugSession || false, projectRoot: options.projectRoot || null }
641
+ );
642
+ // Sessions contribute only hotspots — clear noisy text-based extractions
643
+ analysis.decisions = [];
644
+ analysis.pitfalls = [];
645
+ analysis.patterns = [];
646
+ analyses.push(analysis);
647
+ } catch { /* skip unreadable files */ }
648
+ }
649
+
650
+ return accumulate(analyses, options);
651
+ }
652
+
653
+ /**
654
+ * Format a memory entry as an annotation string (without comment prefix).
655
+ * @param {MemoryEntry} entry
656
+ * @returns {string}
657
+ */
658
+ function formatAnnotation(entry) {
659
+ const tag = entry.category === 'hotspot' ? 'cap-history'
660
+ : entry.category === 'decision' ? 'cap-decision'
661
+ : entry.category === 'pitfall' ? 'cap-pitfall'
662
+ : 'cap-pattern';
663
+
664
+ const meta = [];
665
+ if (entry.metadata.sessions) meta.push(`sessions:${entry.metadata.sessions}`);
666
+ if (entry.metadata.edits) meta.push(`edits:${entry.metadata.edits}`);
667
+ if (entry.metadata.since) meta.push(`since:${entry.metadata.since}`);
668
+ if (entry.metadata.confirmations) meta.push(`confirmed:${entry.metadata.confirmations}`);
669
+ if (entry.metadata.pinned) meta.push('pinned:true');
670
+ if (entry.metadata.source) meta.push(`learned:${entry.metadata.source.substring(0, 10)}`);
671
+
672
+ const metaStr = meta.length > 0 ? `(${meta.join(', ')})` : '';
673
+ return `@${tag}${metaStr} ${entry.content}`;
674
+ }
675
+
676
+ module.exports = {
677
+ // Core
678
+ analyzeSession,
679
+ accumulate,
680
+ accumulateFromCode,
681
+ accumulateFromFiles,
682
+ formatAnnotation,
683
+
684
+ // Helpers (for testing)
685
+ extractText,
686
+ extractTools,
687
+ stripTags,
688
+ isNoise,
689
+ normalizeFilePath,
690
+
691
+ // Constants (configurable via options, exposed for transparency)
692
+ DEFAULT_STALE_THRESHOLD,
693
+ MIN_HOTSPOT_SESSIONS,
694
+ MIN_PATTERN_CONFIRMATIONS,
695
+ DECISION_PATTERNS,
696
+ PITFALL_PATTERNS,
697
+ PATTERN_PATTERNS,
698
+ };