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.
- package/README.md +9 -4
- package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
- package/dashboard/dist/components/ActivityFeed.js +42 -0
- package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
- package/dashboard/dist/components/ActivityFeed.test.js +162 -0
- package/dashboard/dist/components/BranchSelector.d.ts +16 -0
- package/dashboard/dist/components/BranchSelector.js +49 -0
- package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
- package/dashboard/dist/components/BranchSelector.test.js +166 -0
- package/dashboard/dist/components/CommandPalette.d.ts +17 -0
- package/dashboard/dist/components/CommandPalette.js +118 -0
- package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
- package/dashboard/dist/components/CommandPalette.test.js +181 -0
- package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
- package/dashboard/dist/components/ConnectionStatus.js +27 -0
- package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
- package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
- package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
- package/dashboard/dist/components/DeviceFrame.js +52 -0
- package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
- package/dashboard/dist/components/DeviceFrame.test.js +118 -0
- package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
- package/dashboard/dist/components/EnvironmentBadge.js +16 -0
- package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
- package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
- package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
- package/dashboard/dist/components/FocusIndicator.js +47 -0
- package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/FocusIndicator.test.js +117 -0
- package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
- package/dashboard/dist/components/KeyboardHelp.js +61 -0
- package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
- package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
- package/dashboard/dist/components/LogSearch.d.ts +13 -0
- package/dashboard/dist/components/LogSearch.js +43 -0
- package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
- package/dashboard/dist/components/LogSearch.test.js +100 -0
- package/dashboard/dist/components/LogStream.d.ts +21 -0
- package/dashboard/dist/components/LogStream.js +123 -0
- package/dashboard/dist/components/LogStream.test.d.ts +1 -0
- package/dashboard/dist/components/LogStream.test.js +159 -0
- package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
- package/dashboard/dist/components/PreviewPanel.js +73 -0
- package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
- package/dashboard/dist/components/PreviewPanel.test.js +124 -0
- package/dashboard/dist/components/ProjectCard.d.ts +18 -0
- package/dashboard/dist/components/ProjectCard.js +19 -0
- package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectCard.test.js +53 -0
- package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
- package/dashboard/dist/components/ProjectDetail.js +65 -0
- package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectDetail.test.js +196 -0
- package/dashboard/dist/components/ProjectList.d.ts +11 -0
- package/dashboard/dist/components/ProjectList.js +62 -0
- package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectList.test.js +93 -0
- package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
- package/dashboard/dist/components/SettingsPanel.js +154 -0
- package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
- package/dashboard/dist/components/SettingsPanel.test.js +196 -0
- package/dashboard/dist/components/StatusBar.d.ts +16 -0
- package/dashboard/dist/components/StatusBar.js +47 -0
- package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
- package/dashboard/dist/components/StatusBar.test.js +123 -0
- package/dashboard/dist/components/TaskBoard.d.ts +22 -0
- package/dashboard/dist/components/TaskBoard.js +102 -0
- package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskBoard.test.js +113 -0
- package/dashboard/dist/components/TaskCard.d.ts +17 -0
- package/dashboard/dist/components/TaskCard.js +29 -0
- package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskCard.test.js +109 -0
- package/dashboard/dist/components/TaskDetail.d.ts +36 -0
- package/dashboard/dist/components/TaskDetail.js +41 -0
- package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
- package/dashboard/dist/components/TaskDetail.test.js +164 -0
- package/dashboard/dist/components/TaskFilter.d.ts +12 -0
- package/dashboard/dist/components/TaskFilter.js +138 -0
- package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
- package/dashboard/dist/components/TaskFilter.test.js +109 -0
- package/dashboard/dist/components/TeamPanel.d.ts +15 -0
- package/dashboard/dist/components/TeamPanel.js +24 -0
- package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPanel.test.js +109 -0
- package/dashboard/dist/components/TeamPresence.d.ts +14 -0
- package/dashboard/dist/components/TeamPresence.js +31 -0
- package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPresence.test.js +144 -0
- package/dashboard/dist/components/layout/Header.d.ts +9 -0
- package/dashboard/dist/components/layout/Header.js +11 -0
- package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Header.test.js +35 -0
- package/dashboard/dist/components/layout/Shell.d.ts +10 -0
- package/dashboard/dist/components/layout/Shell.js +5 -0
- package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Shell.test.js +34 -0
- package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
- package/dashboard/dist/components/layout/Sidebar.js +8 -0
- package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
- package/dashboard/dist/components/ui/Badge.d.ts +9 -0
- package/dashboard/dist/components/ui/Badge.js +13 -0
- package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Badge.test.js +69 -0
- package/dashboard/dist/components/ui/Button.d.ts +12 -0
- package/dashboard/dist/components/ui/Button.js +14 -0
- package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Button.test.js +81 -0
- package/dashboard/dist/components/ui/Card.d.ts +21 -0
- package/dashboard/dist/components/ui/Card.js +20 -0
- package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Card.test.js +82 -0
- package/dashboard/dist/components/ui/Input.d.ts +13 -0
- package/dashboard/dist/components/ui/Input.js +8 -0
- package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Input.test.js +68 -0
- package/dashboard/dist/styles/tokens.d.ts +150 -0
- package/dashboard/dist/styles/tokens.js +184 -0
- package/dashboard/dist/styles/tokens.test.d.ts +1 -0
- package/dashboard/dist/styles/tokens.test.js +95 -0
- package/dashboard/dist/test/setup.d.ts +1 -0
- package/dashboard/dist/test/setup.js +1 -0
- package/dashboard/package.json +3 -0
- package/package.json +15 -4
- package/scripts/capture-screenshots.js +170 -0
- package/scripts/docs-update.js +253 -0
- package/scripts/generate-screenshots.js +321 -0
- package/scripts/project-docs.js +377 -0
- package/scripts/vps-setup.sh +477 -0
- package/server/lib/adapters/base-adapter.js +114 -0
- package/server/lib/adapters/base-adapter.test.js +90 -0
- package/server/lib/adapters/claude-adapter.js +141 -0
- package/server/lib/adapters/claude-adapter.test.js +180 -0
- package/server/lib/adapters/deepseek-adapter.js +153 -0
- package/server/lib/adapters/deepseek-adapter.test.js +193 -0
- package/server/lib/adapters/openai-adapter.js +190 -0
- package/server/lib/adapters/openai-adapter.test.js +231 -0
- package/server/lib/budget-tracker.js +169 -0
- package/server/lib/budget-tracker.test.js +165 -0
- package/server/lib/claude-injector.js +85 -0
- package/server/lib/claude-injector.test.js +161 -0
- package/server/lib/consensus-engine.js +135 -0
- package/server/lib/consensus-engine.test.js +152 -0
- package/server/lib/context-builder.js +112 -0
- package/server/lib/context-builder.test.js +120 -0
- package/server/lib/file-collector.js +322 -0
- package/server/lib/file-collector.test.js +307 -0
- package/server/lib/memory-classifier.js +175 -0
- package/server/lib/memory-classifier.test.js +169 -0
- package/server/lib/memory-committer.js +138 -0
- package/server/lib/memory-committer.test.js +136 -0
- package/server/lib/memory-hooks.js +127 -0
- package/server/lib/memory-hooks.test.js +136 -0
- package/server/lib/memory-init.js +104 -0
- package/server/lib/memory-init.test.js +119 -0
- package/server/lib/memory-observer.js +149 -0
- package/server/lib/memory-observer.test.js +158 -0
- package/server/lib/memory-reader.js +243 -0
- package/server/lib/memory-reader.test.js +216 -0
- package/server/lib/memory-storage.js +120 -0
- package/server/lib/memory-storage.test.js +136 -0
- package/server/lib/memory-writer.js +176 -0
- package/server/lib/memory-writer.test.js +231 -0
- package/server/lib/overdrive-command.js +30 -6
- package/server/lib/overdrive-command.test.js +8 -1
- package/server/lib/pattern-detector.js +216 -0
- package/server/lib/pattern-detector.test.js +241 -0
- package/server/lib/relevance-scorer.js +175 -0
- package/server/lib/relevance-scorer.test.js +107 -0
- package/server/lib/review-command.js +238 -0
- package/server/lib/review-command.test.js +245 -0
- package/server/lib/review-orchestrator.js +273 -0
- package/server/lib/review-orchestrator.test.js +300 -0
- package/server/lib/review-reporter.js +288 -0
- package/server/lib/review-reporter.test.js +240 -0
- package/server/lib/session-summary.js +90 -0
- package/server/lib/session-summary.test.js +156 -0
- 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
|
+
});
|