tlc-claude-code 1.2.27 → 1.2.29

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 (179) hide show
  1. package/README.md +9 -4
  2. package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
  3. package/dashboard/dist/components/ActivityFeed.js +42 -0
  4. package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
  5. package/dashboard/dist/components/ActivityFeed.test.js +162 -0
  6. package/dashboard/dist/components/BranchSelector.d.ts +16 -0
  7. package/dashboard/dist/components/BranchSelector.js +49 -0
  8. package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
  9. package/dashboard/dist/components/BranchSelector.test.js +166 -0
  10. package/dashboard/dist/components/CommandPalette.d.ts +17 -0
  11. package/dashboard/dist/components/CommandPalette.js +118 -0
  12. package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
  13. package/dashboard/dist/components/CommandPalette.test.js +181 -0
  14. package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
  15. package/dashboard/dist/components/ConnectionStatus.js +27 -0
  16. package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
  17. package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
  18. package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
  19. package/dashboard/dist/components/DeviceFrame.js +52 -0
  20. package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
  21. package/dashboard/dist/components/DeviceFrame.test.js +118 -0
  22. package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
  23. package/dashboard/dist/components/EnvironmentBadge.js +16 -0
  24. package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
  25. package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
  26. package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
  27. package/dashboard/dist/components/FocusIndicator.js +47 -0
  28. package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
  29. package/dashboard/dist/components/FocusIndicator.test.js +117 -0
  30. package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
  31. package/dashboard/dist/components/KeyboardHelp.js +61 -0
  32. package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
  33. package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
  34. package/dashboard/dist/components/LogSearch.d.ts +13 -0
  35. package/dashboard/dist/components/LogSearch.js +43 -0
  36. package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
  37. package/dashboard/dist/components/LogSearch.test.js +100 -0
  38. package/dashboard/dist/components/LogStream.d.ts +21 -0
  39. package/dashboard/dist/components/LogStream.js +123 -0
  40. package/dashboard/dist/components/LogStream.test.d.ts +1 -0
  41. package/dashboard/dist/components/LogStream.test.js +159 -0
  42. package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
  43. package/dashboard/dist/components/PreviewPanel.js +73 -0
  44. package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
  45. package/dashboard/dist/components/PreviewPanel.test.js +124 -0
  46. package/dashboard/dist/components/ProjectCard.d.ts +18 -0
  47. package/dashboard/dist/components/ProjectCard.js +19 -0
  48. package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
  49. package/dashboard/dist/components/ProjectCard.test.js +53 -0
  50. package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
  51. package/dashboard/dist/components/ProjectDetail.js +65 -0
  52. package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
  53. package/dashboard/dist/components/ProjectDetail.test.js +196 -0
  54. package/dashboard/dist/components/ProjectList.d.ts +11 -0
  55. package/dashboard/dist/components/ProjectList.js +62 -0
  56. package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
  57. package/dashboard/dist/components/ProjectList.test.js +93 -0
  58. package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
  59. package/dashboard/dist/components/SettingsPanel.js +154 -0
  60. package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
  61. package/dashboard/dist/components/SettingsPanel.test.js +196 -0
  62. package/dashboard/dist/components/StatusBar.d.ts +16 -0
  63. package/dashboard/dist/components/StatusBar.js +47 -0
  64. package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
  65. package/dashboard/dist/components/StatusBar.test.js +123 -0
  66. package/dashboard/dist/components/TaskBoard.d.ts +22 -0
  67. package/dashboard/dist/components/TaskBoard.js +102 -0
  68. package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
  69. package/dashboard/dist/components/TaskBoard.test.js +113 -0
  70. package/dashboard/dist/components/TaskCard.d.ts +17 -0
  71. package/dashboard/dist/components/TaskCard.js +29 -0
  72. package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
  73. package/dashboard/dist/components/TaskCard.test.js +109 -0
  74. package/dashboard/dist/components/TaskDetail.d.ts +36 -0
  75. package/dashboard/dist/components/TaskDetail.js +41 -0
  76. package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
  77. package/dashboard/dist/components/TaskDetail.test.js +164 -0
  78. package/dashboard/dist/components/TaskFilter.d.ts +12 -0
  79. package/dashboard/dist/components/TaskFilter.js +138 -0
  80. package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
  81. package/dashboard/dist/components/TaskFilter.test.js +109 -0
  82. package/dashboard/dist/components/TeamPanel.d.ts +15 -0
  83. package/dashboard/dist/components/TeamPanel.js +24 -0
  84. package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
  85. package/dashboard/dist/components/TeamPanel.test.js +109 -0
  86. package/dashboard/dist/components/TeamPresence.d.ts +14 -0
  87. package/dashboard/dist/components/TeamPresence.js +31 -0
  88. package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
  89. package/dashboard/dist/components/TeamPresence.test.js +144 -0
  90. package/dashboard/dist/components/layout/Header.d.ts +9 -0
  91. package/dashboard/dist/components/layout/Header.js +11 -0
  92. package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
  93. package/dashboard/dist/components/layout/Header.test.js +35 -0
  94. package/dashboard/dist/components/layout/Shell.d.ts +10 -0
  95. package/dashboard/dist/components/layout/Shell.js +5 -0
  96. package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
  97. package/dashboard/dist/components/layout/Shell.test.js +34 -0
  98. package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
  99. package/dashboard/dist/components/layout/Sidebar.js +8 -0
  100. package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
  101. package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
  102. package/dashboard/dist/components/ui/Badge.d.ts +9 -0
  103. package/dashboard/dist/components/ui/Badge.js +13 -0
  104. package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
  105. package/dashboard/dist/components/ui/Badge.test.js +69 -0
  106. package/dashboard/dist/components/ui/Button.d.ts +12 -0
  107. package/dashboard/dist/components/ui/Button.js +14 -0
  108. package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
  109. package/dashboard/dist/components/ui/Button.test.js +81 -0
  110. package/dashboard/dist/components/ui/Card.d.ts +21 -0
  111. package/dashboard/dist/components/ui/Card.js +20 -0
  112. package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
  113. package/dashboard/dist/components/ui/Card.test.js +82 -0
  114. package/dashboard/dist/components/ui/Input.d.ts +13 -0
  115. package/dashboard/dist/components/ui/Input.js +8 -0
  116. package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
  117. package/dashboard/dist/components/ui/Input.test.js +68 -0
  118. package/dashboard/dist/styles/tokens.d.ts +150 -0
  119. package/dashboard/dist/styles/tokens.js +184 -0
  120. package/dashboard/dist/styles/tokens.test.d.ts +1 -0
  121. package/dashboard/dist/styles/tokens.test.js +95 -0
  122. package/dashboard/dist/test/setup.d.ts +1 -0
  123. package/dashboard/dist/test/setup.js +1 -0
  124. package/dashboard/package.json +3 -0
  125. package/package.json +15 -4
  126. package/scripts/capture-screenshots.js +170 -0
  127. package/scripts/docs-update.js +253 -0
  128. package/scripts/generate-screenshots.js +321 -0
  129. package/scripts/project-docs.js +377 -0
  130. package/scripts/vps-setup.sh +477 -0
  131. package/server/lib/adapters/base-adapter.js +114 -0
  132. package/server/lib/adapters/base-adapter.test.js +90 -0
  133. package/server/lib/adapters/claude-adapter.js +141 -0
  134. package/server/lib/adapters/claude-adapter.test.js +180 -0
  135. package/server/lib/adapters/deepseek-adapter.js +153 -0
  136. package/server/lib/adapters/deepseek-adapter.test.js +193 -0
  137. package/server/lib/adapters/openai-adapter.js +190 -0
  138. package/server/lib/adapters/openai-adapter.test.js +231 -0
  139. package/server/lib/budget-tracker.js +169 -0
  140. package/server/lib/budget-tracker.test.js +165 -0
  141. package/server/lib/claude-injector.js +85 -0
  142. package/server/lib/claude-injector.test.js +161 -0
  143. package/server/lib/consensus-engine.js +135 -0
  144. package/server/lib/consensus-engine.test.js +152 -0
  145. package/server/lib/context-builder.js +112 -0
  146. package/server/lib/context-builder.test.js +120 -0
  147. package/server/lib/file-collector.js +322 -0
  148. package/server/lib/file-collector.test.js +307 -0
  149. package/server/lib/memory-classifier.js +175 -0
  150. package/server/lib/memory-classifier.test.js +169 -0
  151. package/server/lib/memory-committer.js +138 -0
  152. package/server/lib/memory-committer.test.js +136 -0
  153. package/server/lib/memory-hooks.js +127 -0
  154. package/server/lib/memory-hooks.test.js +136 -0
  155. package/server/lib/memory-init.js +104 -0
  156. package/server/lib/memory-init.test.js +119 -0
  157. package/server/lib/memory-observer.js +149 -0
  158. package/server/lib/memory-observer.test.js +158 -0
  159. package/server/lib/memory-reader.js +243 -0
  160. package/server/lib/memory-reader.test.js +216 -0
  161. package/server/lib/memory-storage.js +120 -0
  162. package/server/lib/memory-storage.test.js +136 -0
  163. package/server/lib/memory-writer.js +176 -0
  164. package/server/lib/memory-writer.test.js +231 -0
  165. package/server/lib/overdrive-command.js +30 -6
  166. package/server/lib/overdrive-command.test.js +8 -1
  167. package/server/lib/pattern-detector.js +216 -0
  168. package/server/lib/pattern-detector.test.js +241 -0
  169. package/server/lib/relevance-scorer.js +175 -0
  170. package/server/lib/relevance-scorer.test.js +107 -0
  171. package/server/lib/review-command.js +238 -0
  172. package/server/lib/review-command.test.js +245 -0
  173. package/server/lib/review-orchestrator.js +273 -0
  174. package/server/lib/review-orchestrator.test.js +300 -0
  175. package/server/lib/review-reporter.js +288 -0
  176. package/server/lib/review-reporter.test.js +240 -0
  177. package/server/lib/session-summary.js +90 -0
  178. package/server/lib/session-summary.test.js +156 -0
  179. package/templates/docs-sync.yml +91 -0
@@ -0,0 +1,307 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import {
6
+ collectFiles,
7
+ collectFromDirectory,
8
+ loadIgnorePatterns,
9
+ parseIgnoreFile,
10
+ shouldIgnore,
11
+ matchesPattern,
12
+ isBinaryFile,
13
+ matchesExtension,
14
+ readFileContent,
15
+ DEFAULT_IGNORES,
16
+ } from './file-collector.js';
17
+
18
+ describe('File Collector', () => {
19
+ let testDir;
20
+
21
+ beforeEach(() => {
22
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-collect-test-'));
23
+ });
24
+
25
+ afterEach(() => {
26
+ fs.rmSync(testDir, { recursive: true, force: true });
27
+ });
28
+
29
+ describe('parseIgnoreFile', () => {
30
+ it('parses patterns from content', () => {
31
+ const content = `
32
+ node_modules
33
+ dist
34
+ *.log
35
+ # This is a comment
36
+ build/
37
+ `;
38
+ const patterns = parseIgnoreFile(content);
39
+ expect(patterns).toEqual(['node_modules', 'dist', '*.log', 'build/']);
40
+ });
41
+
42
+ it('ignores comments and empty lines', () => {
43
+ const content = `
44
+ # Comment
45
+ pattern1
46
+
47
+ # Another comment
48
+ pattern2
49
+ `;
50
+ const patterns = parseIgnoreFile(content);
51
+ expect(patterns).toEqual(['pattern1', 'pattern2']);
52
+ });
53
+ });
54
+
55
+ describe('matchesPattern', () => {
56
+ it('matches exact paths', () => {
57
+ expect(matchesPattern('node_modules', 'node_modules')).toBe(true);
58
+ expect(matchesPattern('src/index.js', 'src/index.js')).toBe(true);
59
+ });
60
+
61
+ it('matches directory patterns', () => {
62
+ expect(matchesPattern('node_modules/package', 'node_modules/')).toBe(true);
63
+ expect(matchesPattern('node_modules', 'node_modules/')).toBe(true); // Directory itself matches
64
+ expect(matchesPattern('src/node_modules/pkg', 'node_modules/')).toBe(true);
65
+ });
66
+
67
+ it('matches glob patterns', () => {
68
+ expect(matchesPattern('test.log', '*.log')).toBe(true);
69
+ expect(matchesPattern('debug.log', '*.log')).toBe(true);
70
+ expect(matchesPattern('test.txt', '*.log')).toBe(false);
71
+ });
72
+
73
+ it('matches nested paths', () => {
74
+ expect(matchesPattern('src/node_modules/pkg', 'node_modules')).toBe(true);
75
+ expect(matchesPattern('deep/nested/node_modules/pkg', 'node_modules')).toBe(true);
76
+ });
77
+
78
+ it('matches basename', () => {
79
+ expect(matchesPattern('path/to/package.json', 'package.json')).toBe(true);
80
+ });
81
+ });
82
+
83
+ describe('shouldIgnore', () => {
84
+ it('returns true for matching patterns', () => {
85
+ const patterns = ['node_modules', '*.log', 'dist/'];
86
+ expect(shouldIgnore('node_modules/package', patterns)).toBe(true);
87
+ expect(shouldIgnore('app.log', patterns)).toBe(true);
88
+ expect(shouldIgnore('dist/bundle.js', patterns)).toBe(true);
89
+ });
90
+
91
+ it('returns false for non-matching paths', () => {
92
+ const patterns = ['node_modules', '*.log'];
93
+ expect(shouldIgnore('src/index.js', patterns)).toBe(false);
94
+ expect(shouldIgnore('README.md', patterns)).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe('isBinaryFile', () => {
99
+ it('identifies binary files by extension', () => {
100
+ expect(isBinaryFile('image.png')).toBe(true);
101
+ expect(isBinaryFile('photo.jpg')).toBe(true);
102
+ expect(isBinaryFile('archive.zip')).toBe(true);
103
+ expect(isBinaryFile('file.pdf')).toBe(true);
104
+ expect(isBinaryFile('module.pyc')).toBe(true);
105
+ });
106
+
107
+ it('identifies text files', () => {
108
+ expect(isBinaryFile('script.js')).toBe(false);
109
+ expect(isBinaryFile('style.css')).toBe(false);
110
+ expect(isBinaryFile('doc.md')).toBe(false);
111
+ expect(isBinaryFile('config.json')).toBe(false);
112
+ });
113
+
114
+ it('is case insensitive', () => {
115
+ expect(isBinaryFile('image.PNG')).toBe(true);
116
+ expect(isBinaryFile('image.Png')).toBe(true);
117
+ });
118
+ });
119
+
120
+ describe('matchesExtension', () => {
121
+ it('matches extensions with dot', () => {
122
+ expect(matchesExtension('file.js', ['.js'])).toBe(true);
123
+ expect(matchesExtension('file.ts', ['.js', '.ts'])).toBe(true);
124
+ });
125
+
126
+ it('matches extensions without dot', () => {
127
+ expect(matchesExtension('file.js', ['js'])).toBe(true);
128
+ expect(matchesExtension('file.ts', ['js', 'ts'])).toBe(true);
129
+ });
130
+
131
+ it('returns true for empty extensions array', () => {
132
+ expect(matchesExtension('file.js', [])).toBe(true);
133
+ expect(matchesExtension('file.ts', null)).toBe(true);
134
+ });
135
+
136
+ it('returns false for non-matching', () => {
137
+ expect(matchesExtension('file.js', ['.ts'])).toBe(false);
138
+ });
139
+ });
140
+
141
+ describe('collectFromDirectory', () => {
142
+ it('collects all files from directory', () => {
143
+ fs.writeFileSync(path.join(testDir, 'file1.js'), 'code');
144
+ fs.writeFileSync(path.join(testDir, 'file2.js'), 'code');
145
+
146
+ const files = collectFromDirectory(testDir);
147
+ expect(files).toHaveLength(2);
148
+ });
149
+
150
+ it('collects files recursively', () => {
151
+ fs.mkdirSync(path.join(testDir, 'sub'));
152
+ fs.writeFileSync(path.join(testDir, 'file1.js'), 'code');
153
+ fs.writeFileSync(path.join(testDir, 'sub', 'file2.js'), 'code');
154
+
155
+ const files = collectFromDirectory(testDir);
156
+ expect(files).toHaveLength(2);
157
+ });
158
+
159
+ it('skips node_modules by default', () => {
160
+ fs.mkdirSync(path.join(testDir, 'node_modules', 'pkg'), { recursive: true });
161
+ fs.writeFileSync(path.join(testDir, 'file1.js'), 'code');
162
+ fs.writeFileSync(path.join(testDir, 'node_modules', 'pkg', 'index.js'), 'code');
163
+
164
+ const files = collectFromDirectory(testDir);
165
+ expect(files).toHaveLength(1);
166
+ });
167
+
168
+ it('respects extension filter', () => {
169
+ fs.writeFileSync(path.join(testDir, 'file.js'), 'code');
170
+ fs.writeFileSync(path.join(testDir, 'file.ts'), 'code');
171
+ fs.writeFileSync(path.join(testDir, 'file.css'), 'code');
172
+
173
+ const files = collectFromDirectory(testDir, { extensions: ['.js', '.ts'] });
174
+ expect(files).toHaveLength(2);
175
+ });
176
+
177
+ it('skips binary files', () => {
178
+ fs.writeFileSync(path.join(testDir, 'file.js'), 'code');
179
+ fs.writeFileSync(path.join(testDir, 'image.png'), 'binary');
180
+
181
+ const files = collectFromDirectory(testDir);
182
+ expect(files).toHaveLength(1);
183
+ });
184
+
185
+ it('skips hidden files by default', () => {
186
+ fs.writeFileSync(path.join(testDir, 'file.js'), 'code');
187
+ fs.writeFileSync(path.join(testDir, '.hidden.js'), 'code');
188
+
189
+ const files = collectFromDirectory(testDir);
190
+ expect(files).toHaveLength(1);
191
+ });
192
+
193
+ it('includes hidden files when option set', () => {
194
+ fs.writeFileSync(path.join(testDir, 'file.js'), 'code');
195
+ fs.writeFileSync(path.join(testDir, '.hidden.js'), 'code');
196
+
197
+ const files = collectFromDirectory(testDir, { includeHidden: true });
198
+ expect(files).toHaveLength(2);
199
+ });
200
+
201
+ it('respects maxDepth option', () => {
202
+ fs.mkdirSync(path.join(testDir, 'a', 'b', 'c'), { recursive: true });
203
+ fs.writeFileSync(path.join(testDir, 'root.js'), 'code');
204
+ fs.writeFileSync(path.join(testDir, 'a', 'level1.js'), 'code');
205
+ fs.writeFileSync(path.join(testDir, 'a', 'b', 'level2.js'), 'code');
206
+ fs.writeFileSync(path.join(testDir, 'a', 'b', 'c', 'level3.js'), 'code');
207
+
208
+ const files = collectFromDirectory(testDir, { maxDepth: 2 });
209
+ expect(files).toHaveLength(3); // root, level1, level2
210
+ });
211
+ });
212
+
213
+ describe('loadIgnorePatterns', () => {
214
+ it('loads patterns from .tlcignore', () => {
215
+ fs.writeFileSync(path.join(testDir, '.tlcignore'), 'custom_ignore\n*.tmp');
216
+
217
+ const patterns = loadIgnorePatterns(testDir);
218
+ expect(patterns).toContain('custom_ignore');
219
+ expect(patterns).toContain('*.tmp');
220
+ });
221
+
222
+ it('falls back to .gitignore if no .tlcignore', () => {
223
+ fs.writeFileSync(path.join(testDir, '.gitignore'), 'build\ncoverage');
224
+
225
+ const patterns = loadIgnorePatterns(testDir);
226
+ expect(patterns).toContain('build');
227
+ expect(patterns).toContain('coverage');
228
+ });
229
+
230
+ it('prefers .tlcignore over .gitignore', () => {
231
+ fs.writeFileSync(path.join(testDir, '.tlcignore'), 'tlc_pattern');
232
+ fs.writeFileSync(path.join(testDir, '.gitignore'), 'git_pattern');
233
+
234
+ const patterns = loadIgnorePatterns(testDir);
235
+ expect(patterns).toContain('tlc_pattern');
236
+ expect(patterns).not.toContain('git_pattern');
237
+ });
238
+
239
+ it('returns empty array if no ignore files', () => {
240
+ const patterns = loadIgnorePatterns(testDir);
241
+ expect(patterns).toEqual([]);
242
+ });
243
+ });
244
+
245
+ describe('collectFiles', () => {
246
+ it('collects single file', () => {
247
+ const filePath = path.join(testDir, 'test.js');
248
+ fs.writeFileSync(filePath, 'code');
249
+
250
+ const result = collectFiles(filePath);
251
+ expect(result.files).toHaveLength(1);
252
+ expect(result.stats.total).toBe(1);
253
+ });
254
+
255
+ it('returns error for non-existent path', () => {
256
+ const result = collectFiles('/nonexistent/path');
257
+ expect(result.files).toHaveLength(0);
258
+ expect(result.stats.error).toContain('not found');
259
+ });
260
+
261
+ it('skips binary files', () => {
262
+ const filePath = path.join(testDir, 'image.png');
263
+ fs.writeFileSync(filePath, 'binary');
264
+
265
+ const result = collectFiles(filePath);
266
+ expect(result.files).toHaveLength(0);
267
+ expect(result.stats.skipped).toBe(1);
268
+ });
269
+
270
+ it('collects directory with patterns', () => {
271
+ fs.writeFileSync(path.join(testDir, '.tlcignore'), 'ignored/');
272
+ fs.mkdirSync(path.join(testDir, 'src'));
273
+ fs.mkdirSync(path.join(testDir, 'ignored'));
274
+ fs.writeFileSync(path.join(testDir, 'src', 'index.js'), 'code');
275
+ fs.writeFileSync(path.join(testDir, 'ignored', 'skip.js'), 'code');
276
+
277
+ const result = collectFiles(testDir);
278
+ expect(result.files).toHaveLength(1);
279
+ });
280
+ });
281
+
282
+ describe('readFileContent', () => {
283
+ it('reads file content', () => {
284
+ const filePath = path.join(testDir, 'test.js');
285
+ fs.writeFileSync(filePath, 'const x = 1;');
286
+
287
+ const { content, error } = readFileContent(filePath);
288
+ expect(content).toBe('const x = 1;');
289
+ expect(error).toBeNull();
290
+ });
291
+
292
+ it('returns error for non-existent file', () => {
293
+ const { content, error } = readFileContent('/nonexistent');
294
+ expect(content).toBeNull();
295
+ expect(error).toBeTruthy();
296
+ });
297
+ });
298
+
299
+ describe('DEFAULT_IGNORES', () => {
300
+ it('includes common patterns', () => {
301
+ expect(DEFAULT_IGNORES).toContain('node_modules');
302
+ expect(DEFAULT_IGNORES).toContain('.git');
303
+ expect(DEFAULT_IGNORES).toContain('dist');
304
+ expect(DEFAULT_IGNORES).toContain('coverage');
305
+ });
306
+ });
307
+ });
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Memory Classifier - Classify memory items as team or personal
3
+ */
4
+
5
+ const CLASSIFICATION = {
6
+ TEAM: 'team',
7
+ PERSONAL: 'personal',
8
+ };
9
+
10
+ /**
11
+ * Keywords that indicate team-level memory
12
+ */
13
+ const TEAM_KEYWORDS = [
14
+ 'database',
15
+ 'api',
16
+ 'infrastructure',
17
+ 'deployment',
18
+ 'architecture',
19
+ 'server',
20
+ 'backend',
21
+ 'frontend',
22
+ 'service',
23
+ 'microservice',
24
+ 'queue',
25
+ 'cache',
26
+ 'redis',
27
+ 'postgres',
28
+ 'mysql',
29
+ 'mongodb',
30
+ 'docker',
31
+ 'kubernetes',
32
+ 'aws',
33
+ 'gcp',
34
+ 'azure',
35
+ 'ci/cd',
36
+ 'pipeline',
37
+ 'authentication',
38
+ 'authorization',
39
+ 'security',
40
+ 'scaling',
41
+ 'performance',
42
+ 'migration',
43
+ ];
44
+
45
+ /**
46
+ * Keywords that indicate personal preference memory
47
+ */
48
+ const PERSONAL_KEYWORDS = [
49
+ 'prefer',
50
+ 'style',
51
+ 'formatting',
52
+ 'indentation',
53
+ 'tabs',
54
+ 'spaces',
55
+ 'semicolons',
56
+ 'quotes',
57
+ 'naming',
58
+ 'convention',
59
+ 'readable',
60
+ 'cleaner',
61
+ 'shorter',
62
+ 'longer',
63
+ 'comment',
64
+ 'documentation',
65
+ 'verbose',
66
+ 'concise',
67
+ ];
68
+
69
+ /**
70
+ * Check if text contains any of the keywords
71
+ * @param {string} text - Text to search
72
+ * @param {string[]} keywords - Keywords to look for
73
+ * @returns {boolean}
74
+ */
75
+ function containsKeyword(text, keywords) {
76
+ if (!text) return false;
77
+ const lower = text.toLowerCase();
78
+ return keywords.some(kw => lower.includes(kw.toLowerCase()));
79
+ }
80
+
81
+ /**
82
+ * Get all text content from a memory item
83
+ * @param {Object} item - Memory item
84
+ * @returns {string}
85
+ */
86
+ function getItemText(item) {
87
+ if (!item) return '';
88
+
89
+ const parts = [
90
+ item.raw,
91
+ item.choice,
92
+ item.preference,
93
+ item.antiPreference,
94
+ item.content,
95
+ item.subject,
96
+ item.issue,
97
+ item.reasoning,
98
+ item.context,
99
+ ].filter(Boolean);
100
+
101
+ return parts.join(' ');
102
+ }
103
+
104
+ /**
105
+ * Classify a memory item as team or personal
106
+ * @param {Object} item - Memory item from pattern detector
107
+ * @returns {string} 'team' or 'personal'
108
+ */
109
+ function classifyMemory(item) {
110
+ if (!item) return CLASSIFICATION.PERSONAL;
111
+
112
+ const text = getItemText(item);
113
+ const type = item.type;
114
+
115
+ // Gotchas are almost always team-level
116
+ if (type === 'gotcha') {
117
+ return CLASSIFICATION.TEAM;
118
+ }
119
+
120
+ // Check for "we" language (team indicator)
121
+ if (item.raw && /\bwe\b/i.test(item.raw)) {
122
+ return CLASSIFICATION.TEAM;
123
+ }
124
+
125
+ // Check for "I" language (personal indicator)
126
+ if (item.raw && /\bI\b/.test(item.raw)) {
127
+ return CLASSIFICATION.PERSONAL;
128
+ }
129
+
130
+ // Decisions are usually team-level unless about personal style
131
+ if (type === 'decision') {
132
+ if (containsKeyword(text, PERSONAL_KEYWORDS)) {
133
+ return CLASSIFICATION.PERSONAL;
134
+ }
135
+ return CLASSIFICATION.TEAM;
136
+ }
137
+
138
+ // Preferences are usually personal unless about infrastructure
139
+ if (type === 'preference') {
140
+ if (containsKeyword(text, TEAM_KEYWORDS)) {
141
+ return CLASSIFICATION.TEAM;
142
+ }
143
+ return CLASSIFICATION.PERSONAL;
144
+ }
145
+
146
+ // Reasoning depends on content
147
+ if (type === 'reasoning') {
148
+ if (containsKeyword(text, TEAM_KEYWORDS)) {
149
+ return CLASSIFICATION.TEAM;
150
+ }
151
+ if (containsKeyword(text, PERSONAL_KEYWORDS)) {
152
+ return CLASSIFICATION.PERSONAL;
153
+ }
154
+ // Check for "I" in reasoning
155
+ if (/\bI\b/.test(text)) {
156
+ return CLASSIFICATION.PERSONAL;
157
+ }
158
+ return CLASSIFICATION.TEAM;
159
+ }
160
+
161
+ // Check keywords as fallback
162
+ if (containsKeyword(text, TEAM_KEYWORDS)) {
163
+ return CLASSIFICATION.TEAM;
164
+ }
165
+
166
+ // Default to personal when ambiguous
167
+ return CLASSIFICATION.PERSONAL;
168
+ }
169
+
170
+ module.exports = {
171
+ classifyMemory,
172
+ CLASSIFICATION,
173
+ TEAM_KEYWORDS,
174
+ PERSONAL_KEYWORDS,
175
+ };
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { classifyMemory, CLASSIFICATION } from './memory-classifier.js';
3
+
4
+ describe('memory-classifier', () => {
5
+ describe('classifyMemory', () => {
6
+ describe('team classification', () => {
7
+ it('classifies architectural decision as team', () => {
8
+ const item = {
9
+ type: 'decision',
10
+ choice: 'PostgreSQL',
11
+ context: 'database selection',
12
+ };
13
+
14
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.TEAM);
15
+ });
16
+
17
+ it('classifies technology choice as team', () => {
18
+ const item = {
19
+ type: 'decision',
20
+ choice: 'React',
21
+ reasoning: 'for frontend',
22
+ };
23
+
24
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.TEAM);
25
+ });
26
+
27
+ it('classifies project gotcha as team', () => {
28
+ const item = {
29
+ type: 'gotcha',
30
+ subject: 'auth service',
31
+ issue: 'needs warm up time',
32
+ };
33
+
34
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.TEAM);
35
+ });
36
+
37
+ it('classifies "we decided" language as team', () => {
38
+ const item = {
39
+ type: 'decision',
40
+ raw: 'we decided to use REST',
41
+ };
42
+
43
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.TEAM);
44
+ });
45
+
46
+ it('classifies API/infrastructure decisions as team', () => {
47
+ const item = {
48
+ type: 'decision',
49
+ choice: 'REST API',
50
+ };
51
+
52
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.TEAM);
53
+ });
54
+ });
55
+
56
+ describe('personal classification', () => {
57
+ it('classifies style preference as personal', () => {
58
+ const item = {
59
+ type: 'preference',
60
+ preference: 'functional programming',
61
+ category: 'codeStyle',
62
+ };
63
+
64
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.PERSONAL);
65
+ });
66
+
67
+ it('classifies code correction as personal', () => {
68
+ const item = {
69
+ type: 'preference',
70
+ preference: 'named exports',
71
+ antiPreference: 'default exports',
72
+ };
73
+
74
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.PERSONAL);
75
+ });
76
+
77
+ it('classifies "I prefer" language as personal', () => {
78
+ const item = {
79
+ type: 'preference',
80
+ raw: 'I prefer small functions',
81
+ };
82
+
83
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.PERSONAL);
84
+ });
85
+
86
+ it('classifies formatting preference as personal', () => {
87
+ const item = {
88
+ type: 'preference',
89
+ preference: 'tabs over spaces',
90
+ };
91
+
92
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.PERSONAL);
93
+ });
94
+
95
+ it('classifies comment style as personal', () => {
96
+ const item = {
97
+ type: 'preference',
98
+ preference: 'minimal comments',
99
+ };
100
+
101
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.PERSONAL);
102
+ });
103
+ });
104
+
105
+ describe('edge cases', () => {
106
+ it('defaults to personal when ambiguous', () => {
107
+ const item = {
108
+ type: 'unknown',
109
+ content: 'something vague',
110
+ };
111
+
112
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.PERSONAL);
113
+ });
114
+
115
+ it('handles empty item', () => {
116
+ expect(classifyMemory({})).toBe(CLASSIFICATION.PERSONAL);
117
+ });
118
+
119
+ it('handles undefined', () => {
120
+ expect(classifyMemory(undefined)).toBe(CLASSIFICATION.PERSONAL);
121
+ });
122
+
123
+ it('classifies reasoning as team when about architecture', () => {
124
+ const item = {
125
+ type: 'reasoning',
126
+ content: 'because the database needs to scale',
127
+ };
128
+
129
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.TEAM);
130
+ });
131
+
132
+ it('classifies reasoning as personal when about style', () => {
133
+ const item = {
134
+ type: 'reasoning',
135
+ content: 'because I find it more readable',
136
+ };
137
+
138
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.PERSONAL);
139
+ });
140
+ });
141
+
142
+ describe('keyword detection', () => {
143
+ it('detects team keywords in content', () => {
144
+ const teamKeywords = ['database', 'API', 'infrastructure', 'deployment', 'architecture'];
145
+
146
+ for (const keyword of teamKeywords) {
147
+ const item = { type: 'decision', choice: `something with ${keyword}` };
148
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.TEAM);
149
+ }
150
+ });
151
+
152
+ it('detects personal keywords in content', () => {
153
+ const personalKeywords = ['prefer', 'style', 'formatting', 'indentation'];
154
+
155
+ for (const keyword of personalKeywords) {
156
+ const item = { type: 'preference', preference: `my ${keyword} choice` };
157
+ expect(classifyMemory(item)).toBe(CLASSIFICATION.PERSONAL);
158
+ }
159
+ });
160
+ });
161
+ });
162
+
163
+ describe('CLASSIFICATION', () => {
164
+ it('exports classification constants', () => {
165
+ expect(CLASSIFICATION.TEAM).toBe('team');
166
+ expect(CLASSIFICATION.PERSONAL).toBe('personal');
167
+ });
168
+ });
169
+ });