tlc-claude-code 1.8.5 → 2.1.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.
- package/.claude/commands/tlc/bootstrap.md +77 -0
- package/.claude/commands/tlc/build.md +20 -6
- package/.claude/commands/tlc/deploy.md +194 -2
- package/.claude/commands/tlc/e2e-verify.md +214 -0
- package/.claude/commands/tlc/guard.md +191 -0
- package/.claude/commands/tlc/help.md +32 -0
- package/.claude/commands/tlc/init.md +73 -37
- package/.claude/commands/tlc/llm.md +19 -4
- package/.claude/commands/tlc/preflight.md +134 -0
- package/.claude/commands/tlc/recall.md +87 -0
- package/.claude/commands/tlc/remember.md +71 -0
- package/.claude/commands/tlc/review.md +17 -4
- package/.claude/commands/tlc/watchci.md +159 -0
- package/.claude/hooks/tlc-block-tools.sh +41 -0
- package/.claude/hooks/tlc-capture-exchange.sh +50 -0
- package/.claude/hooks/tlc-post-build.sh +38 -0
- package/.claude/hooks/tlc-post-push.sh +22 -0
- package/.claude/hooks/tlc-prompt-guard.sh +69 -0
- package/.claude/hooks/tlc-session-init.sh +123 -0
- package/CLAUDE.md +96 -201
- package/bin/install.js +171 -2
- package/bin/postinstall.js +45 -26
- package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
- package/dashboard-web/dist/index.html +2 -2
- package/docker-compose.dev.yml +18 -12
- package/package.json +3 -1
- package/server/index.js +240 -1
- package/server/lib/bug-writer.js +204 -0
- package/server/lib/bug-writer.test.js +279 -0
- package/server/lib/capture-bridge.js +242 -0
- package/server/lib/capture-bridge.test.js +363 -0
- package/server/lib/capture-guard.js +140 -0
- package/server/lib/capture-guard.test.js +182 -0
- package/server/lib/claude-cascade.js +247 -0
- package/server/lib/claude-cascade.test.js +245 -0
- package/server/lib/command-runner.js +159 -0
- package/server/lib/command-runner.test.js +92 -0
- package/server/lib/context-injection.js +121 -0
- package/server/lib/context-injection.test.js +340 -0
- package/server/lib/conversation-chunker.js +320 -0
- package/server/lib/conversation-chunker.test.js +573 -0
- package/server/lib/deploy/runners/dependency-runner.js +106 -0
- package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
- package/server/lib/deploy/runners/secrets-runner.js +174 -0
- package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
- package/server/lib/deploy/security-gates.js +11 -24
- package/server/lib/deploy/security-gates.test.js +9 -2
- package/server/lib/deploy-engine.js +182 -0
- package/server/lib/deploy-engine.test.js +147 -0
- package/server/lib/docker-api.js +137 -0
- package/server/lib/docker-api.test.js +202 -0
- package/server/lib/docker-client.js +297 -0
- package/server/lib/docker-client.test.js +308 -0
- package/server/lib/embedding-client.js +160 -0
- package/server/lib/embedding-client.test.js +243 -0
- package/server/lib/global-config.js +198 -0
- package/server/lib/global-config.test.js +288 -0
- package/server/lib/inherited-search.js +184 -0
- package/server/lib/inherited-search.test.js +343 -0
- package/server/lib/input-sanitizer.js +86 -0
- package/server/lib/input-sanitizer.test.js +117 -0
- package/server/lib/launchd-agent.js +225 -0
- package/server/lib/launchd-agent.test.js +185 -0
- package/server/lib/memory-api.js +182 -0
- package/server/lib/memory-api.test.js +320 -0
- package/server/lib/memory-bridge-e2e.test.js +160 -0
- package/server/lib/memory-committer.js +18 -4
- package/server/lib/memory-committer.test.js +21 -0
- package/server/lib/memory-hooks-capture.test.js +415 -0
- package/server/lib/memory-hooks-integration.test.js +98 -0
- package/server/lib/memory-hooks.js +139 -0
- package/server/lib/memory-inheritance.js +179 -0
- package/server/lib/memory-inheritance.test.js +360 -0
- package/server/lib/memory-store-adapter.js +105 -0
- package/server/lib/memory-store-adapter.test.js +141 -0
- package/server/lib/memory-wiring-e2e.test.js +93 -0
- package/server/lib/nginx-config.js +114 -0
- package/server/lib/nginx-config.test.js +82 -0
- package/server/lib/ollama-health.js +91 -0
- package/server/lib/ollama-health.test.js +74 -0
- package/server/lib/plan-writer.js +196 -0
- package/server/lib/plan-writer.test.js +298 -0
- package/server/lib/port-guard.js +44 -0
- package/server/lib/port-guard.test.js +65 -0
- package/server/lib/project-scanner.js +302 -0
- package/server/lib/project-scanner.test.js +541 -0
- package/server/lib/project-status.js +302 -0
- package/server/lib/project-status.test.js +470 -0
- package/server/lib/projects-registry.js +237 -0
- package/server/lib/projects-registry.test.js +275 -0
- package/server/lib/recall-command.js +207 -0
- package/server/lib/recall-command.test.js +306 -0
- package/server/lib/remember-command.js +98 -0
- package/server/lib/remember-command.test.js +288 -0
- package/server/lib/rich-capture.js +221 -0
- package/server/lib/rich-capture.test.js +312 -0
- package/server/lib/roadmap-api.js +200 -0
- package/server/lib/roadmap-api.test.js +318 -0
- package/server/lib/security/crypto-utils.test.js +2 -2
- package/server/lib/semantic-recall.js +242 -0
- package/server/lib/semantic-recall.test.js +463 -0
- package/server/lib/setup-generator.js +315 -0
- package/server/lib/setup-generator.test.js +303 -0
- package/server/lib/ssh-client.js +184 -0
- package/server/lib/ssh-client.test.js +127 -0
- package/server/lib/test-inventory.js +112 -0
- package/server/lib/test-inventory.test.js +360 -0
- package/server/lib/vector-indexer.js +246 -0
- package/server/lib/vector-indexer.test.js +459 -0
- package/server/lib/vector-store.js +260 -0
- package/server/lib/vector-store.test.js +706 -0
- package/server/lib/vps-api.js +184 -0
- package/server/lib/vps-api.test.js +208 -0
- package/server/lib/vps-bootstrap.js +124 -0
- package/server/lib/vps-bootstrap.test.js +79 -0
- package/server/lib/vps-monitor.js +126 -0
- package/server/lib/vps-monitor.test.js +98 -0
- package/server/lib/workspace-api.js +992 -0
- package/server/lib/workspace-api.test.js +1217 -0
- package/server/lib/workspace-bootstrap.js +164 -0
- package/server/lib/workspace-bootstrap.test.js +503 -0
- package/server/lib/workspace-context.js +129 -0
- package/server/lib/workspace-context.test.js +214 -0
- package/server/lib/workspace-detector.js +162 -0
- package/server/lib/workspace-detector.test.js +193 -0
- package/server/lib/workspace-init.js +307 -0
- package/server/lib/workspace-init.test.js +244 -0
- package/server/lib/workspace-snapshot.js +236 -0
- package/server/lib/workspace-snapshot.test.js +444 -0
- package/server/lib/workspace-watcher.js +162 -0
- package/server/lib/workspace-watcher.test.js +257 -0
- package/server/package-lock.json +1306 -17
- package/server/package.json +7 -0
- package/dashboard-web/dist/assets/index-B1I_joSL.js +0 -393
- package/dashboard-web/dist/assets/index-B1I_joSL.js.map +0 -1
- package/dashboard-web/dist/assets/index-Trhg1C1Y.css +0 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
const { GlobalConfig } = await import('./global-config.js');
|
|
7
|
+
|
|
8
|
+
describe('GlobalConfig', () => {
|
|
9
|
+
let tempDir;
|
|
10
|
+
let originalEnv;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'global-config-test-'));
|
|
14
|
+
originalEnv = process.env.TLC_CONFIG_DIR;
|
|
15
|
+
process.env.TLC_CONFIG_DIR = tempDir;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
if (originalEnv !== undefined) {
|
|
20
|
+
process.env.TLC_CONFIG_DIR = originalEnv;
|
|
21
|
+
} else {
|
|
22
|
+
delete process.env.TLC_CONFIG_DIR;
|
|
23
|
+
}
|
|
24
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('initialization', () => {
|
|
28
|
+
it('creates config directory if not exists', () => {
|
|
29
|
+
const configDir = path.join(tempDir, 'subdir');
|
|
30
|
+
process.env.TLC_CONFIG_DIR = configDir;
|
|
31
|
+
|
|
32
|
+
const config = new GlobalConfig();
|
|
33
|
+
config.load();
|
|
34
|
+
|
|
35
|
+
expect(fs.existsSync(configDir)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('creates config file with defaults on first access', () => {
|
|
39
|
+
const config = new GlobalConfig();
|
|
40
|
+
const data = config.load();
|
|
41
|
+
|
|
42
|
+
expect(data).toBeDefined();
|
|
43
|
+
expect(data.version).toBe(1);
|
|
44
|
+
expect(data.roots).toEqual([]);
|
|
45
|
+
expect(data.scanDepth).toBe(5);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('respects TLC_CONFIG_DIR environment variable', () => {
|
|
49
|
+
const customDir = path.join(tempDir, 'custom');
|
|
50
|
+
process.env.TLC_CONFIG_DIR = customDir;
|
|
51
|
+
|
|
52
|
+
const config = new GlobalConfig();
|
|
53
|
+
config.load();
|
|
54
|
+
|
|
55
|
+
const configPath = path.join(customDir, 'config.json');
|
|
56
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('config schema has version field', () => {
|
|
60
|
+
const config = new GlobalConfig();
|
|
61
|
+
const data = config.load();
|
|
62
|
+
|
|
63
|
+
expect(data.version).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('getRoots', () => {
|
|
68
|
+
it('returns empty roots when not configured', () => {
|
|
69
|
+
const config = new GlobalConfig();
|
|
70
|
+
|
|
71
|
+
const roots = config.getRoots();
|
|
72
|
+
|
|
73
|
+
expect(roots).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns configured roots', () => {
|
|
77
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const config = new GlobalConfig();
|
|
81
|
+
config.addRoot(rootPath);
|
|
82
|
+
|
|
83
|
+
const roots = config.getRoots();
|
|
84
|
+
|
|
85
|
+
expect(roots).toContain(rootPath);
|
|
86
|
+
} finally {
|
|
87
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('addRoot', () => {
|
|
93
|
+
it('adds root path and persists to disk', () => {
|
|
94
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const config = new GlobalConfig();
|
|
98
|
+
config.addRoot(rootPath);
|
|
99
|
+
|
|
100
|
+
// Re-read from disk
|
|
101
|
+
const config2 = new GlobalConfig();
|
|
102
|
+
const roots = config2.getRoots();
|
|
103
|
+
|
|
104
|
+
expect(roots).toContain(rootPath);
|
|
105
|
+
} finally {
|
|
106
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('rejects non-existent directory path', () => {
|
|
111
|
+
const config = new GlobalConfig();
|
|
112
|
+
|
|
113
|
+
expect(() => config.addRoot('/tmp/does-not-exist-xyz-123')).toThrow(/does not exist/i);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rejects file path (must be directory)', () => {
|
|
117
|
+
const filePath = path.join(tempDir, 'somefile.txt');
|
|
118
|
+
fs.writeFileSync(filePath, 'hello');
|
|
119
|
+
|
|
120
|
+
const config = new GlobalConfig();
|
|
121
|
+
|
|
122
|
+
expect(() => config.addRoot(filePath)).toThrow(/not a directory/i);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('duplicate root paths rejected', () => {
|
|
126
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const config = new GlobalConfig();
|
|
130
|
+
config.addRoot(rootPath);
|
|
131
|
+
|
|
132
|
+
expect(() => config.addRoot(rootPath)).toThrow(/already configured/i);
|
|
133
|
+
} finally {
|
|
134
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('multiple roots supported', () => {
|
|
139
|
+
const root1 = fs.mkdtempSync(path.join(os.tmpdir(), 'root1-'));
|
|
140
|
+
const root2 = fs.mkdtempSync(path.join(os.tmpdir(), 'root2-'));
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const config = new GlobalConfig();
|
|
144
|
+
config.addRoot(root1);
|
|
145
|
+
config.addRoot(root2);
|
|
146
|
+
|
|
147
|
+
const roots = config.getRoots();
|
|
148
|
+
|
|
149
|
+
expect(roots).toHaveLength(2);
|
|
150
|
+
expect(roots).toContain(root1);
|
|
151
|
+
expect(roots).toContain(root2);
|
|
152
|
+
} finally {
|
|
153
|
+
fs.rmSync(root1, { recursive: true, force: true });
|
|
154
|
+
fs.rmSync(root2, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('removeRoot', () => {
|
|
160
|
+
it('removes root path', () => {
|
|
161
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const config = new GlobalConfig();
|
|
165
|
+
config.addRoot(rootPath);
|
|
166
|
+
config.removeRoot(rootPath);
|
|
167
|
+
|
|
168
|
+
const roots = config.getRoots();
|
|
169
|
+
|
|
170
|
+
expect(roots).not.toContain(rootPath);
|
|
171
|
+
} finally {
|
|
172
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('removing non-existent root does not throw', () => {
|
|
177
|
+
const config = new GlobalConfig();
|
|
178
|
+
|
|
179
|
+
expect(() => config.removeRoot('/some/path')).not.toThrow();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('isConfigured', () => {
|
|
184
|
+
it('returns false with no roots', () => {
|
|
185
|
+
const config = new GlobalConfig();
|
|
186
|
+
|
|
187
|
+
expect(config.isConfigured()).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('returns true with roots', () => {
|
|
191
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const config = new GlobalConfig();
|
|
195
|
+
config.addRoot(rootPath);
|
|
196
|
+
|
|
197
|
+
expect(config.isConfigured()).toBe(true);
|
|
198
|
+
} finally {
|
|
199
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('scanDepth', () => {
|
|
205
|
+
it('defaults to 5', () => {
|
|
206
|
+
const config = new GlobalConfig();
|
|
207
|
+
const data = config.load();
|
|
208
|
+
|
|
209
|
+
expect(data.scanDepth).toBe(5);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('can be updated', () => {
|
|
213
|
+
const config = new GlobalConfig();
|
|
214
|
+
config.setScanDepth(3);
|
|
215
|
+
|
|
216
|
+
const config2 = new GlobalConfig();
|
|
217
|
+
const data = config2.load();
|
|
218
|
+
|
|
219
|
+
expect(data.scanDepth).toBe(3);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('lastScan tracking', () => {
|
|
224
|
+
it('stores lastScan timestamp per root', () => {
|
|
225
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const config = new GlobalConfig();
|
|
229
|
+
config.addRoot(rootPath);
|
|
230
|
+
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
config.setLastScan(rootPath, now);
|
|
233
|
+
|
|
234
|
+
const lastScan = config.getLastScan(rootPath);
|
|
235
|
+
|
|
236
|
+
expect(lastScan).toBe(now);
|
|
237
|
+
} finally {
|
|
238
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('returns null for root that has not been scanned', () => {
|
|
243
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const config = new GlobalConfig();
|
|
247
|
+
config.addRoot(rootPath);
|
|
248
|
+
|
|
249
|
+
const lastScan = config.getLastScan(rootPath);
|
|
250
|
+
|
|
251
|
+
expect(lastScan).toBeNull();
|
|
252
|
+
} finally {
|
|
253
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('error handling', () => {
|
|
259
|
+
it('handles corrupted JSON gracefully (resets to defaults)', () => {
|
|
260
|
+
const configPath = path.join(tempDir, 'config.json');
|
|
261
|
+
fs.writeFileSync(configPath, '{invalid json!!!');
|
|
262
|
+
|
|
263
|
+
const config = new GlobalConfig();
|
|
264
|
+
const data = config.load();
|
|
265
|
+
|
|
266
|
+
expect(data.version).toBe(1);
|
|
267
|
+
expect(data.roots).toEqual([]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('atomic write prevents partial file corruption', () => {
|
|
271
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const config = new GlobalConfig();
|
|
275
|
+
config.addRoot(rootPath);
|
|
276
|
+
|
|
277
|
+
// Verify file is valid JSON after write
|
|
278
|
+
const configPath = path.join(tempDir, 'config.json');
|
|
279
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
280
|
+
const parsed = JSON.parse(raw);
|
|
281
|
+
|
|
282
|
+
expect(parsed.roots).toContain(rootPath);
|
|
283
|
+
} finally {
|
|
284
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inherited Search — wraps semantic-recall with inheritance-aware search
|
|
3
|
+
* that walks from project scope up through workspace scope, adjusting
|
|
4
|
+
* scores and deduplicating results.
|
|
5
|
+
*
|
|
6
|
+
* Workspace results receive a 0.8x score multiplier to prefer local
|
|
7
|
+
* project memories while still surfacing relevant workspace knowledge.
|
|
8
|
+
*
|
|
9
|
+
* Auto-widening: when project scope returns fewer than 3 results,
|
|
10
|
+
* the search automatically widens to include workspace results.
|
|
11
|
+
*
|
|
12
|
+
* @module inherited-search
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Minimum results before auto-widening from project to workspace */
|
|
16
|
+
const AUTO_WIDEN_THRESHOLD = 3;
|
|
17
|
+
|
|
18
|
+
/** Score multiplier for workspace-level results */
|
|
19
|
+
const WORKSPACE_SCORE_MULTIPLIER = 0.8;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Deduplicate results by id, keeping the entry with the highest score.
|
|
23
|
+
*
|
|
24
|
+
* @param {Array} results - Scored results that may contain duplicates
|
|
25
|
+
* @returns {Array} Deduplicated results
|
|
26
|
+
*/
|
|
27
|
+
function deduplicateById(results) {
|
|
28
|
+
const bestById = new Map();
|
|
29
|
+
|
|
30
|
+
for (const result of results) {
|
|
31
|
+
const existing = bestById.get(result.id);
|
|
32
|
+
if (!existing || result.score > existing.score) {
|
|
33
|
+
bestById.set(result.id, result);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [...bestById.values()];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply a score multiplier to an array of results, returning new objects.
|
|
42
|
+
*
|
|
43
|
+
* @param {Array} results - Search results
|
|
44
|
+
* @param {number} multiplier - Score multiplier to apply
|
|
45
|
+
* @returns {Array} Results with adjusted scores
|
|
46
|
+
*/
|
|
47
|
+
function applyScoreMultiplier(results, multiplier) {
|
|
48
|
+
return results.map((r) => ({ ...r, score: r.score * multiplier }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create an inherited search instance that wraps semantic-recall with
|
|
53
|
+
* inheritance-aware scope walking.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} deps - Dependencies
|
|
56
|
+
* @param {object} deps.semanticRecall - Semantic recall instance (from Phase 71)
|
|
57
|
+
* @param {object} deps.workspaceDetector - Workspace detector instance (from Task 1)
|
|
58
|
+
* @param {object} deps.vectorIndexer - Vector indexer instance (from Phase 71)
|
|
59
|
+
* @returns {object} Object with search and indexAll methods
|
|
60
|
+
*/
|
|
61
|
+
export function createInheritedSearch({ semanticRecall, workspaceDetector, vectorIndexer }) {
|
|
62
|
+
/**
|
|
63
|
+
* Fetch workspace-scope results using the workspace root as the context
|
|
64
|
+
* workspace, applying the 0.8x score multiplier.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} query - Search query
|
|
67
|
+
* @param {object} context - Original context
|
|
68
|
+
* @param {object} options - Search options (without scope)
|
|
69
|
+
* @param {string} workspaceRoot - Workspace root path
|
|
70
|
+
* @returns {Promise<Array>} Workspace results with adjusted scores
|
|
71
|
+
*/
|
|
72
|
+
async function fetchWorkspaceResults(query, context, options, workspaceRoot) {
|
|
73
|
+
const wsContext = { ...context, workspace: workspaceRoot };
|
|
74
|
+
const wsOptions = { ...options, scope: 'workspace' };
|
|
75
|
+
const wsResults = await semanticRecall.recall(query, wsContext, wsOptions);
|
|
76
|
+
return applyScoreMultiplier(wsResults, WORKSPACE_SCORE_MULTIPLIER);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Search with inheritance-aware scope walking.
|
|
81
|
+
*
|
|
82
|
+
* Scope behavior:
|
|
83
|
+
* - 'project': search project only; auto-widen to workspace if < 3 results
|
|
84
|
+
* - 'workspace': search workspace only (delegates directly to semanticRecall)
|
|
85
|
+
* - 'inherited': always search both project and workspace, merge results
|
|
86
|
+
* - 'global': delegates directly to semanticRecall with global scope
|
|
87
|
+
*
|
|
88
|
+
* @param {string} query - Search query text
|
|
89
|
+
* @param {object} context - Current context
|
|
90
|
+
* @param {object} [options] - Search options
|
|
91
|
+
* @param {string} [options.scope='project'] - Search scope
|
|
92
|
+
* @returns {Promise<Array>} Scored and ranked results
|
|
93
|
+
*/
|
|
94
|
+
async function search(query, context, options = {}) {
|
|
95
|
+
const { scope = 'project', ...restOptions } = options;
|
|
96
|
+
|
|
97
|
+
// For global or workspace scope, pass through directly
|
|
98
|
+
if (scope === 'global' || scope === 'workspace') {
|
|
99
|
+
return semanticRecall.recall(query, context, { ...restOptions, scope });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Detect workspace info for the current project
|
|
103
|
+
const wsInfo = workspaceDetector.detectWorkspace(context.workspace);
|
|
104
|
+
|
|
105
|
+
// For inherited scope: always search both project and workspace
|
|
106
|
+
if (scope === 'inherited') {
|
|
107
|
+
const projectResults = await semanticRecall.recall(
|
|
108
|
+
query,
|
|
109
|
+
context,
|
|
110
|
+
{ ...restOptions, scope: 'project' },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// If not in a workspace, return project results only
|
|
114
|
+
if (!wsInfo.isInWorkspace || !wsInfo.workspaceRoot) {
|
|
115
|
+
return projectResults;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const wsResults = await fetchWorkspaceResults(
|
|
119
|
+
query,
|
|
120
|
+
context,
|
|
121
|
+
restOptions,
|
|
122
|
+
wsInfo.workspaceRoot,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Merge, deduplicate, and sort
|
|
126
|
+
const merged = deduplicateById([...projectResults, ...wsResults]);
|
|
127
|
+
merged.sort((a, b) => b.score - a.score);
|
|
128
|
+
return merged;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// scope === 'project': search project first, auto-widen if needed
|
|
132
|
+
const projectResults = await semanticRecall.recall(
|
|
133
|
+
query,
|
|
134
|
+
context,
|
|
135
|
+
{ ...restOptions, scope: 'project' },
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// If enough project results or not in a workspace, return as-is
|
|
139
|
+
if (
|
|
140
|
+
projectResults.length >= AUTO_WIDEN_THRESHOLD
|
|
141
|
+
|| !wsInfo.isInWorkspace
|
|
142
|
+
|| !wsInfo.workspaceRoot
|
|
143
|
+
) {
|
|
144
|
+
return projectResults;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Auto-widen: fetch workspace results and merge
|
|
148
|
+
const wsResults = await fetchWorkspaceResults(
|
|
149
|
+
query,
|
|
150
|
+
context,
|
|
151
|
+
restOptions,
|
|
152
|
+
wsInfo.workspaceRoot,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const merged = deduplicateById([...projectResults, ...wsResults]);
|
|
156
|
+
merged.sort((a, b) => b.score - a.score);
|
|
157
|
+
return merged;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Index all memory for a project, including workspace memory if applicable.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
164
|
+
* @returns {Promise<object>} Combined indexing results
|
|
165
|
+
*/
|
|
166
|
+
async function indexAll(projectRoot) {
|
|
167
|
+
const projectResult = await vectorIndexer.indexAll(projectRoot);
|
|
168
|
+
|
|
169
|
+
const wsInfo = workspaceDetector.detectWorkspace(projectRoot);
|
|
170
|
+
|
|
171
|
+
if (wsInfo.isInWorkspace && wsInfo.workspaceRoot) {
|
|
172
|
+
const wsResult = await vectorIndexer.indexAll(wsInfo.workspaceRoot);
|
|
173
|
+
return {
|
|
174
|
+
indexed: projectResult.indexed + wsResult.indexed,
|
|
175
|
+
skipped: projectResult.skipped + wsResult.skipped,
|
|
176
|
+
errors: projectResult.errors + wsResult.errors,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return projectResult;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { search, indexAll };
|
|
184
|
+
}
|