@comfanion/usethis_search 4.4.0 → 4.5.1

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.
@@ -9,7 +9,7 @@ import { tool } from "@opencode-ai/plugin"
9
9
  import path from "path"
10
10
  import fs from "fs/promises"
11
11
 
12
- import { CodebaseIndexer } from "../vectorizer/index.ts"
12
+ import { CodebaseIndexer, getIndexer, releaseIndexer, destroyIndexer } from "../vectorizer/index.ts"
13
13
 
14
14
  const INDEX_EXTENSIONS: Record<string, string[]> = {
15
15
  code: [".js", ".ts", ".jsx", ".tsx", ".go", ".py", ".rs", ".java", ".kt", ".swift", ".c", ".cpp", ".h", ".cs", ".rb", ".php"],
@@ -98,14 +98,17 @@ Available indexes:
98
98
  output += `### Active Indexes\n\n`
99
99
  for (const idx of indexes) {
100
100
  try {
101
- const indexer = await new CodebaseIndexer(projectRoot, idx).init()
102
- const stats = await indexer.getStats()
103
- await indexer.unloadModel()
104
- const desc = INDEX_DESCRIPTIONS[idx] || "Custom index"
105
- const features = stats.features
106
- ? ` | chunking: ${stats.features.chunking}, hybrid: ${stats.features.hybrid ? "on" : "off"}`
107
- : ""
108
- output += `- **${idx}** - ${desc} (files: ${stats.fileCount}, chunks: ${stats.chunkCount}${features})\n`
101
+ const indexer = await getIndexer(projectRoot, idx)
102
+ try {
103
+ const stats = await indexer.getStats()
104
+ const desc = INDEX_DESCRIPTIONS[idx] || "Custom index"
105
+ const features = stats.features
106
+ ? ` | chunking: ${stats.features.chunking}, hybrid: ${stats.features.hybrid ? "on" : "off"}`
107
+ : ""
108
+ output += `- **${idx}** - ${desc} (files: ${stats.fileCount}, chunks: ${stats.chunkCount}${features})\n`
109
+ } finally {
110
+ releaseIndexer(projectRoot, idx)
111
+ }
109
112
  } catch {
110
113
  output += `- ${idx}\n`
111
114
  }
@@ -119,41 +122,44 @@ Available indexes:
119
122
  if (args.action === "status") {
120
123
  const hashesFile = path.join(vectorsDir, indexName, "hashes.json")
121
124
  try {
122
- const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
123
- const stats = await indexer.getStats()
124
- await indexer.unloadModel()
125
-
126
- const sampleFiles = Object.keys(JSON.parse(await fs.readFile(hashesFile, "utf8"))).slice(0, 5)
127
- const desc = INDEX_DESCRIPTIONS[indexName] || "Custom index"
128
-
129
- let output = `## Index Status: "${indexName}"\n\n`
130
- output += `**Description:** ${desc}\n`
131
- output += `**Files indexed:** ${stats.fileCount}\n`
132
- output += `**Total chunks:** ${stats.chunkCount}\n`
133
- output += `**Model:** ${stats.model}\n`
134
-
135
- if (stats.features) {
136
- output += `\n**Features:**\n`
137
- output += `- Chunking strategy: ${stats.features.chunking}\n`
138
- output += `- Hybrid search: ${stats.features.hybrid ? "enabled" : "disabled"}\n`
139
- output += `- Metrics: ${stats.features.metrics ? "enabled" : "disabled"}\n`
140
- output += `- Query cache: ${stats.features.cache ? "enabled" : "disabled"}\n`
141
- }
142
-
143
- // Show metrics summary if available
125
+ const indexer = await getIndexer(projectRoot, indexName)
144
126
  try {
145
- const metrics = await indexer.getMetrics()
146
- if (metrics.total_queries > 0) {
147
- output += `\n**Search Metrics:**\n`
148
- output += `- Total queries: ${metrics.total_queries}\n`
149
- output += `- Avg results/query: ${metrics.avg_results_per_query.toFixed(1)}\n`
150
- output += `- Zero results rate: ${(metrics.zero_results_rate * 100).toFixed(1)}%\n`
151
- output += `- Avg relevance: ${metrics.avg_relevance.toFixed(3)}\n`
127
+ const stats = await indexer.getStats()
128
+
129
+ const sampleFiles = Object.keys(JSON.parse(await fs.readFile(hashesFile, "utf8"))).slice(0, 5)
130
+ const desc = INDEX_DESCRIPTIONS[indexName] || "Custom index"
131
+
132
+ let output = `## Index Status: "${indexName}"\n\n`
133
+ output += `**Description:** ${desc}\n`
134
+ output += `**Files indexed:** ${stats.fileCount}\n`
135
+ output += `**Total chunks:** ${stats.chunkCount}\n`
136
+ output += `**Model:** ${stats.model}\n`
137
+
138
+ if (stats.features) {
139
+ output += `\n**Features:**\n`
140
+ output += `- Chunking strategy: ${stats.features.chunking}\n`
141
+ output += `- Hybrid search: ${stats.features.hybrid ? "enabled" : "disabled"}\n`
142
+ output += `- Metrics: ${stats.features.metrics ? "enabled" : "disabled"}\n`
143
+ output += `- Query cache: ${stats.features.cache ? "enabled" : "disabled"}\n`
152
144
  }
153
- } catch {}
154
145
 
155
- output += `\n**Sample indexed files:**\n${sampleFiles.map((f) => `- ${f}`).join("\n")}${stats.fileCount > 5 ? `\n- ... and ${stats.fileCount - 5} more` : ""}`
156
- return output
146
+ // Show metrics summary if available
147
+ try {
148
+ const metrics = await indexer.getMetrics()
149
+ if (metrics.total_queries > 0) {
150
+ output += `\n**Search Metrics:**\n`
151
+ output += `- Total queries: ${metrics.total_queries}\n`
152
+ output += `- Avg results/query: ${metrics.avg_results_per_query.toFixed(1)}\n`
153
+ output += `- Zero results rate: ${(metrics.zero_results_rate * 100).toFixed(1)}%\n`
154
+ output += `- Avg relevance: ${metrics.avg_relevance.toFixed(3)}\n`
155
+ }
156
+ } catch {}
157
+
158
+ output += `\n**Sample indexed files:**\n${sampleFiles.map((f) => `- ${f}`).join("\n")}${stats.fileCount > 5 ? `\n- ... and ${stats.fileCount - 5} more` : ""}`
159
+ return output
160
+ } finally {
161
+ releaseIndexer(projectRoot, indexName)
162
+ }
157
163
  } catch {
158
164
  return `## Index Status: "${indexName}"\n\nIndex "${indexName}" not created yet. Create it with: codeindex({ action: "reindex", index: "${indexName}" })`
159
165
  }
@@ -161,6 +167,8 @@ Available indexes:
161
167
 
162
168
  if (args.action === "reindex") {
163
169
  try {
170
+ // Destroy any pooled instance to get fresh state for reindex
171
+ await destroyIndexer(projectRoot, indexName)
164
172
  const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
165
173
 
166
174
  const baseDir = args.dir ? path.resolve(projectRoot, args.dir) : projectRoot
@@ -195,8 +203,8 @@ Available indexes:
195
203
  } catch {}
196
204
  }
197
205
 
198
- await indexer.unloadModel()
199
206
  const stats = await indexer.getStats()
207
+ await indexer.unloadModel()
200
208
 
201
209
  let output = `## Re-indexing Complete\n\n`
202
210
  output += `**Index:** ${indexName}\n`
@@ -258,41 +266,43 @@ Available indexes:
258
266
  return `## Gold Dataset Test\n\nNo test queries found in gold dataset.`
259
267
  }
260
268
 
261
- const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
262
- let passed = 0
263
- let failed = 0
264
- let output = `## Gold Dataset Test Results\n\n`
265
-
266
- for (const t of tests) {
267
- const results = await indexer.search(t.query, 10, false)
268
- const foundFiles = results.map((r: any) => r.file)
269
- const foundExpected = t.expected_files.filter(f => foundFiles.includes(f))
270
- const topScore = results.length > 0 && results[0]._distance != null
271
- ? 1 - results[0]._distance
272
- : 0
273
-
274
- const pass = foundExpected.length >= Math.ceil(t.expected_files.length * 0.5) && topScore >= t.min_relevance
275
-
276
- if (pass) {
277
- passed++
278
- output += `**PASS** Query: "${t.query}"\n`
279
- } else {
280
- failed++
281
- output += `**FAIL** Query: "${t.query}"\n`
282
- }
269
+ const indexer = await getIndexer(projectRoot, indexName)
270
+ try {
271
+ let passed = 0
272
+ let failed = 0
273
+ let output = `## Gold Dataset Test Results\n\n`
274
+
275
+ for (const t of tests) {
276
+ const results = await indexer.search(t.query, 10, false)
277
+ const foundFiles = results.map((r: any) => r.file)
278
+ const foundExpected = t.expected_files.filter(f => foundFiles.includes(f))
279
+ const topScore = results.length > 0 && results[0]._distance != null
280
+ ? 1 - results[0]._distance
281
+ : 0
282
+
283
+ const pass = foundExpected.length >= Math.ceil(t.expected_files.length * 0.5) && topScore >= t.min_relevance
284
+
285
+ if (pass) {
286
+ passed++
287
+ output += `**PASS** Query: "${t.query}"\n`
288
+ } else {
289
+ failed++
290
+ output += `**FAIL** Query: "${t.query}"\n`
291
+ }
283
292
 
284
- output += ` Found: ${foundFiles.slice(0, 3).map((f: string) => `${f} (${(1 - (results.find((r: any) => r.file === f)?._distance ?? 1)).toFixed(2)})`).join(", ")}\n`
285
- if (foundExpected.length < t.expected_files.length) {
286
- const missing = t.expected_files.filter(f => !foundFiles.includes(f))
287
- output += ` Missing: ${missing.join(", ")}\n`
293
+ output += ` Found: ${foundFiles.slice(0, 3).map((f: string) => `${f} (${(1 - (results.find((r: any) => r.file === f)?._distance ?? 1)).toFixed(2)})`).join(", ")}\n`
294
+ if (foundExpected.length < t.expected_files.length) {
295
+ const missing = t.expected_files.filter(f => !foundFiles.includes(f))
296
+ output += ` Missing: ${missing.join(", ")}\n`
297
+ }
298
+ output += `\n`
288
299
  }
289
- output += `\n`
290
- }
291
300
 
292
- await indexer.unloadModel()
293
-
294
- output += `---\n**Summary:** ${passed}/${tests.length} tests passed (${Math.round(passed / tests.length * 100)}%)\n`
295
- return output
301
+ output += `---\n**Summary:** ${passed}/${tests.length} tests passed (${Math.round(passed / tests.length * 100)}%)\n`
302
+ return output
303
+ } finally {
304
+ releaseIndexer(projectRoot, indexName)
305
+ }
296
306
  } catch (error: any) {
297
307
  return `Gold dataset test failed: ${error.message || String(error)}`
298
308
  }
@@ -301,139 +311,137 @@ Available indexes:
301
311
  // NFR-031: Graph validation
302
312
  if (args.action === "validate-graph") {
303
313
  try {
304
- const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
305
-
306
- // Access internal graphDB and db
307
- const graphDB = (indexer as any).graphDB
308
- const db = (indexer as any).db
309
-
310
- if (!graphDB) {
311
- await indexer.unloadModel()
312
- return `## Graph Validation: "${indexName}"\n\nNo graph database found. Run reindex first.`
313
- }
314
-
315
- // 1. Get all triples from graph
316
- let allTriples: any[] = []
314
+ const indexer = await getIndexer(projectRoot, indexName)
317
315
  try {
318
- allTriples = await graphDB.getAllTriples()
319
- } catch (e: any) {
320
- await indexer.unloadModel()
321
- return `## Graph Validation: "${indexName}"\n\n**Error:** Failed to read graph database: ${e.message || String(e)}\n\nThe graph database may be corrupted. Run: codeindex({ action: "reindex", index: "${indexName}" })`
322
- }
316
+ // Access internal graphDB and db
317
+ const graphDB = (indexer as any).graphDB
318
+ const db = (indexer as any).db
323
319
 
324
- // 2. Get all chunk IDs from vector DB
325
- const knownChunkIds = new Set<string>()
326
- const tables = await db.tableNames()
327
- if (tables.includes("chunks")) {
328
- const table = await db.openTable("chunks")
320
+ if (!graphDB) {
321
+ return `## Graph Validation: "${indexName}"\n\nNo graph database found. Run reindex first.`
322
+ }
323
+
324
+ // 1. Get all triples from graph
325
+ let allTriples: any[] = []
329
326
  try {
330
- const rows = await table.filter("").limit(100000).execute()
331
- for (const row of rows) {
332
- if (row.chunk_id) knownChunkIds.add(row.chunk_id)
333
- }
327
+ allTriples = await graphDB.getAllTriples()
334
328
  } catch (e: any) {
335
- await indexer.unloadModel()
336
- return `## Graph Validation: "${indexName}"\n\n**Error:** Failed to read vector database: ${e.message || String(e)}\n\nThe vector database may be corrupted. Run: codeindex({ action: "reindex", index: "${indexName}" })`
329
+ return `## Graph Validation: "${indexName}"\n\n**Error:** Failed to read graph database: ${e.message || String(e)}\n\nThe graph database may be corrupted. Run: codeindex({ action: "reindex", index: "${indexName}" })`
337
330
  }
338
- }
339
331
 
340
- // 3. Validate: find orphaned triples (subject or object points to non-existent chunk)
341
- const orphanedSubjects: Array<{ triple: string; missingId: string }> = []
342
- const orphanedObjects: Array<{ triple: string; missingId: string }> = []
343
- const predicateCounts: Record<string, number> = {}
344
- const sourceCounts: Record<string, number> = {}
345
- const fileCounts: Record<string, number> = {}
346
-
347
- for (const t of allTriples) {
348
- // Count predicates/sources
349
- predicateCounts[t.predicate] = (predicateCounts[t.predicate] || 0) + 1
350
- sourceCounts[t.source] = (sourceCounts[t.source] || 0) + 1
351
- fileCounts[t.file] = (fileCounts[t.file] || 0) + 1
352
-
353
- // Check subject (skip meta: prefixed subjects)
354
- if (!t.subject.startsWith("meta:") && t.subject.startsWith("chunk_") && !knownChunkIds.has(t.subject)) {
355
- orphanedSubjects.push({
356
- triple: `${t.subject} --[${t.predicate}]--> ${t.object}`,
357
- missingId: t.subject,
358
- })
332
+ // 2. Get all chunk IDs from vector DB
333
+ const knownChunkIds = new Set<string>()
334
+ const tables = await db.tableNames()
335
+ if (tables.includes("chunks")) {
336
+ const table = await db.openTable("chunks")
337
+ try {
338
+ const rows = await table.filter("").limit(100000).execute()
339
+ for (const row of rows) {
340
+ if (row.chunk_id) knownChunkIds.add(row.chunk_id)
341
+ }
342
+ } catch (e: any) {
343
+ return `## Graph Validation: "${indexName}"\n\n**Error:** Failed to read vector database: ${e.message || String(e)}\n\nThe vector database may be corrupted. Run: codeindex({ action: "reindex", index: "${indexName}" })`
344
+ }
359
345
  }
360
346
 
361
- // Check object (skip non-chunk objects like file paths, hashes)
362
- if (t.object.startsWith("chunk_") && !knownChunkIds.has(t.object)) {
363
- orphanedObjects.push({
364
- triple: `${t.subject} --[${t.predicate}]--> ${t.object}`,
365
- missingId: t.object,
366
- })
367
- }
368
- }
347
+ // 3. Validate: find orphaned triples (subject or object points to non-existent chunk)
348
+ const orphanedSubjects: Array<{ triple: string; missingId: string }> = []
349
+ const orphanedObjects: Array<{ triple: string; missingId: string }> = []
350
+ const predicateCounts: Record<string, number> = {}
351
+ const sourceCounts: Record<string, number> = {}
352
+ const fileCounts: Record<string, number> = {}
353
+
354
+ for (const t of allTriples) {
355
+ // Count predicates/sources
356
+ predicateCounts[t.predicate] = (predicateCounts[t.predicate] || 0) + 1
357
+ sourceCounts[t.source] = (sourceCounts[t.source] || 0) + 1
358
+ fileCounts[t.file] = (fileCounts[t.file] || 0) + 1
359
+
360
+ // Check subject (skip meta: prefixed subjects)
361
+ if (!t.subject.startsWith("meta:") && t.subject.startsWith("chunk_") && !knownChunkIds.has(t.subject)) {
362
+ orphanedSubjects.push({
363
+ triple: `${t.subject} --[${t.predicate}]--> ${t.object}`,
364
+ missingId: t.subject,
365
+ })
366
+ }
369
367
 
370
- // 4. Get file metadata stats
371
- let fileMeta: Array<{ filePath: string; hash: string; timestamp: number }> = []
372
- try {
373
- fileMeta = await graphDB.getAllFileMeta()
374
- } catch (e: any) {
375
- // Non-fatal - continue validation without metadata
376
- console.warn(`Warning: Failed to get file metadata: ${e.message || String(e)}`)
377
- }
368
+ // Check object (skip non-chunk objects like file paths, hashes)
369
+ if (t.object.startsWith("chunk_") && !knownChunkIds.has(t.object)) {
370
+ orphanedObjects.push({
371
+ triple: `${t.subject} --[${t.predicate}]--> ${t.object}`,
372
+ missingId: t.object,
373
+ })
374
+ }
375
+ }
378
376
 
379
- await indexer.unloadModel()
377
+ // 4. Get file metadata stats
378
+ let fileMeta: Array<{ filePath: string; hash: string; timestamp: number }> = []
379
+ try {
380
+ fileMeta = await graphDB.getAllFileMeta()
381
+ } catch (e: any) {
382
+ // Non-fatal - continue validation without metadata
383
+ console.warn(`Warning: Failed to get file metadata: ${e.message || String(e)}`)
384
+ }
380
385
 
381
- // 5. Build report
382
- const totalOrphaned = orphanedSubjects.length + orphanedObjects.length
383
- const isHealthy = totalOrphaned === 0
386
+ // 5. Build report
387
+ const totalOrphaned = orphanedSubjects.length + orphanedObjects.length
388
+ const isHealthy = totalOrphaned === 0
384
389
 
385
- let output = `## Graph Validation: "${indexName}"\n\n`
386
- output += `**Status:** ${isHealthy ? "HEALTHY" : "ISSUES FOUND"}\n\n`
390
+ let output = `## Graph Validation: "${indexName}"\n\n`
391
+ output += `**Status:** ${isHealthy ? "HEALTHY" : "ISSUES FOUND"}\n\n`
387
392
 
388
- output += `### Statistics\n`
389
- output += `- **Total triples:** ${allTriples.length}\n`
390
- output += `- **Known chunk IDs:** ${knownChunkIds.size}\n`
391
- output += `- **Files with graph metadata:** ${fileMeta.length}\n`
392
- output += `- **Unique files in graph:** ${Object.keys(fileCounts).length}\n\n`
393
+ output += `### Statistics\n`
394
+ output += `- **Total triples:** ${allTriples.length}\n`
395
+ output += `- **Known chunk IDs:** ${knownChunkIds.size}\n`
396
+ output += `- **Files with graph metadata:** ${fileMeta.length}\n`
397
+ output += `- **Unique files in graph:** ${Object.keys(fileCounts).length}\n\n`
393
398
 
394
- output += `### Edge Types\n`
395
- for (const [pred, count] of Object.entries(predicateCounts).sort((a, b) => b[1] - a[1])) {
396
- output += `- **${pred}:** ${count}\n`
397
- }
398
- output += `\n`
399
+ output += `### Edge Types\n`
400
+ for (const [pred, count] of Object.entries(predicateCounts).sort((a, b) => b[1] - a[1])) {
401
+ output += `- **${pred}:** ${count}\n`
402
+ }
403
+ output += `\n`
399
404
 
400
- output += `### Edge Sources\n`
401
- for (const [source, count] of Object.entries(sourceCounts).sort((a, b) => b[1] - a[1])) {
402
- output += `- **${source}:** ${count}\n`
403
- }
404
- output += `\n`
405
+ output += `### Edge Sources\n`
406
+ for (const [source, count] of Object.entries(sourceCounts).sort((a, b) => b[1] - a[1])) {
407
+ output += `- **${source}:** ${count}\n`
408
+ }
409
+ output += `\n`
405
410
 
406
- if (totalOrphaned > 0) {
407
- output += `### Orphaned References (${totalOrphaned})\n\n`
411
+ if (totalOrphaned > 0) {
412
+ output += `### Orphaned References (${totalOrphaned})\n\n`
408
413
 
409
- if (orphanedSubjects.length > 0) {
410
- output += `**Broken subjects** (${orphanedSubjects.length}):\n`
411
- for (const o of orphanedSubjects.slice(0, 10)) {
412
- output += `- \`${o.missingId}\` in: ${o.triple}\n`
413
- }
414
- if (orphanedSubjects.length > 10) {
415
- output += `- ... and ${orphanedSubjects.length - 10} more\n`
414
+ if (orphanedSubjects.length > 0) {
415
+ output += `**Broken subjects** (${orphanedSubjects.length}):\n`
416
+ for (const o of orphanedSubjects.slice(0, 10)) {
417
+ output += `- \`${o.missingId}\` in: ${o.triple}\n`
418
+ }
419
+ if (orphanedSubjects.length > 10) {
420
+ output += `- ... and ${orphanedSubjects.length - 10} more\n`
421
+ }
422
+ output += `\n`
416
423
  }
417
- output += `\n`
418
- }
419
424
 
420
- if (orphanedObjects.length > 0) {
421
- output += `**Broken objects** (${orphanedObjects.length}):\n`
422
- for (const o of orphanedObjects.slice(0, 10)) {
423
- output += `- \`${o.missingId}\` in: ${o.triple}\n`
424
- }
425
- if (orphanedObjects.length > 10) {
426
- output += `- ... and ${orphanedObjects.length - 10} more\n`
425
+ if (orphanedObjects.length > 0) {
426
+ output += `**Broken objects** (${orphanedObjects.length}):\n`
427
+ for (const o of orphanedObjects.slice(0, 10)) {
428
+ output += `- \`${o.missingId}\` in: ${o.triple}\n`
429
+ }
430
+ if (orphanedObjects.length > 10) {
431
+ output += `- ... and ${orphanedObjects.length - 10} more\n`
432
+ }
433
+ output += `\n`
427
434
  }
428
- output += `\n`
435
+
436
+ output += `**Recommendation:** Run \`codeindex({ action: "reindex", index: "${indexName}" })\` to rebuild the graph.\n`
437
+ } else {
438
+ output += `### Integrity\nAll chunk references are valid. No orphaned triples found.\n`
429
439
  }
430
440
 
431
- output += `**Recommendation:** Run \`codeindex({ action: "reindex", index: "${indexName}" })\` to rebuild the graph.\n`
432
- } else {
433
- output += `### Integrity\nAll chunk references are valid. No orphaned triples found.\n`
441
+ return output
442
+ } finally {
443
+ releaseIndexer(projectRoot, indexName)
434
444
  }
435
-
436
- return output
437
445
  } catch (error: any) {
438
446
  return `Graph validation failed: ${error.message || String(error)}`
439
447
  }