@comfanion/usethis_search 3.0.0-dev.0 → 3.0.0-dev.10

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/api.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * usethis_search API
3
+ *
4
+ * Exports internal functions for plugin-to-plugin communication.
5
+ * Used by Mind plugin for graph-based workspace management.
6
+ */
7
+
8
+ import { GraphDB } from "./vectorizer/graph-db"
9
+
10
+ // Global GraphDB instance (shared across plugins)
11
+ let graphDBInstance: GraphDB | null = null
12
+
13
+ /**
14
+ * Initialize API with GraphDB instance
15
+ */
16
+ export function initGraphAPI(db: GraphDB): void {
17
+ graphDBInstance = db
18
+ }
19
+
20
+ /**
21
+ * Get related files for a given file path
22
+ *
23
+ * @param filePath - File path to get relations for
24
+ * @param maxDepth - Maximum graph depth to traverse (default: 1)
25
+ * @returns Array of related files with relation type and weight
26
+ *
27
+ * Example:
28
+ * ```javascript
29
+ * const related = await getRelatedFiles("src/auth/login.ts", 1)
30
+ * // Returns:
31
+ * [
32
+ * { path: "src/types/User.ts", relation: "imports", weight: 0.9 },
33
+ * { path: "src/auth/BaseAuth.ts", relation: "extends", weight: 0.95 },
34
+ * { path: "src/routes/api.ts", relation: "used_by", weight: 0.8 }
35
+ * ]
36
+ * ```
37
+ */
38
+ export async function getRelatedFiles(
39
+ filePath: string,
40
+ maxDepth: number = 1
41
+ ): Promise<{path: string, relation: string, weight: number}[]> {
42
+ if (!graphDBInstance) {
43
+ console.warn("[usethis_search API] GraphDB not initialized. Returning empty array.")
44
+ return []
45
+ }
46
+
47
+ try {
48
+ const chunkId = `file:${filePath}`
49
+ const related = await graphDBInstance.getRelatedFiles(chunkId, maxDepth)
50
+
51
+ // Filter out the input file itself (it might appear in the graph)
52
+ const filtered = related.filter(r => r.path !== filePath)
53
+
54
+ return filtered
55
+ } catch (error) {
56
+ console.error(`[usethis_search API] Error getting related files for ${filePath}:`, error)
57
+ return []
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Check if graph API is available
63
+ */
64
+ export function isGraphAPIAvailable(): boolean {
65
+ return graphDBInstance !== null
66
+ }
67
+
68
+ /**
69
+ * Get all graph entries for a file (both incoming and outgoing)
70
+ */
71
+ export async function getGraphEntries(filePath: string) {
72
+ if (!graphDBInstance) {
73
+ return null
74
+ }
75
+
76
+ try {
77
+ const chunkId = `file:${filePath}`
78
+ const [outgoing, incoming] = await Promise.all([
79
+ graphDBInstance.getOutgoing(chunkId),
80
+ graphDBInstance.getIncoming(chunkId),
81
+ ])
82
+
83
+ return {
84
+ imports: outgoing.filter(t => t.predicate === "imports"),
85
+ extends: outgoing.filter(t => t.predicate === "extends"),
86
+ used_by: incoming,
87
+ }
88
+ } catch (error) {
89
+ console.error(`[usethis_search API] Error getting graph entries for ${filePath}:`, error)
90
+ return null
91
+ }
92
+ }
package/file-indexer.ts CHANGED
@@ -3,7 +3,8 @@ import path from "path"
3
3
  import fs from "fs/promises"
4
4
  import fsSync from "fs"
5
5
 
6
- import { CodebaseIndexer } from "./vectorizer/index.js"
6
+ import { CodebaseIndexer } from "./vectorizer/index.ts"
7
+ import { initGraphAPI } from "./api"
7
8
 
8
9
  /**
9
10
  * File Indexer Plugin
@@ -252,7 +253,19 @@ async function ensureIndexOnSessionStart(
252
253
  for (const [indexName, indexConfig] of Object.entries(config.indexes)) {
253
254
  if (!indexConfig.enabled) continue
254
255
  const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
256
+
255
257
  try {
258
+ // Initialize graph API for Mind plugin integration
259
+ try {
260
+ const graphDB = (indexer as any).graphDB
261
+ if (graphDB) {
262
+ initGraphAPI(graphDB)
263
+ log("Graph API initialized for Mind plugin")
264
+ }
265
+ } catch (error) {
266
+ debug("Failed to initialize graph API:", error)
267
+ }
268
+
256
269
  const indexExists = await hasIndex(projectRoot, indexName)
257
270
  const health = await indexer.checkHealth(config.exclude)
258
271
 
package/index.ts CHANGED
@@ -2,20 +2,34 @@ import type { Plugin } from "@opencode-ai/plugin"
2
2
 
3
3
  import search from "./tools/search"
4
4
  import codeindex from "./tools/codeindex"
5
- import readInterceptor from "./tools/read-interceptor"
6
5
  import FileIndexerPlugin from "./file-indexer"
7
6
 
8
- const UsethisSearchPlugin: Plugin = async (ctx) => {
9
- const fileIndexerHooks = await FileIndexerPlugin(ctx as any)
7
+ const UsethisSearchPlugin: Plugin = async ({ directory, client }) => {
8
+ // Start file indexer (background indexing + event handling)
9
+ let fileIndexerEvent: ((args: any) => Promise<void>) | null = null
10
+ try {
11
+ const hooks = await FileIndexerPlugin({ directory, client } as any)
12
+ fileIndexerEvent = hooks?.event || null
13
+ } catch {
14
+ // file indexer init failed — tools still work, just no auto-indexing
15
+ }
10
16
 
11
17
  return {
12
- ...fileIndexerHooks,
13
18
  tool: {
14
19
  search,
15
20
  codeindex,
16
- read: readInterceptor,
21
+ },
22
+
23
+ event: async (args: any) => {
24
+ if (fileIndexerEvent) {
25
+ try {
26
+ await fileIndexerEvent(args)
27
+ } catch {
28
+ // non-fatal
29
+ }
30
+ }
17
31
  },
18
32
  }
19
33
  }
20
34
 
21
- export default UsethisSearchPlugin;
35
+ export default UsethisSearchPlugin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/usethis_search",
3
- "version": "3.0.0-dev.0",
3
+ "version": "3.0.0-dev.10",
4
4
  "description": "OpenCode plugin: semantic search with graph-based context (v3: graph relations, 1-hop context, LSP + regex analyzers)",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -12,11 +12,11 @@
12
12
  },
13
13
  "files": [
14
14
  "index.ts",
15
+ "api.ts",
15
16
  "file-indexer.ts",
16
17
  "tools/search.ts",
17
18
  "tools/codeindex.ts",
18
- "tools/read-interceptor.ts",
19
- "vectorizer/index.js",
19
+ "vectorizer/index.ts",
20
20
  "vectorizer/content-cleaner.ts",
21
21
  "vectorizer/metadata-extractor.ts",
22
22
  "vectorizer/bm25-index.ts",
@@ -24,9 +24,11 @@
24
24
  "vectorizer/query-cache.ts",
25
25
  "vectorizer/search-metrics.ts",
26
26
  "vectorizer/graph-db.ts",
27
+ "vectorizer/usage-tracker.ts",
27
28
  "vectorizer/graph-builder.ts",
28
29
  "vectorizer/analyzers/regex-analyzer.ts",
29
30
  "vectorizer/analyzers/lsp-analyzer.ts",
31
+ "vectorizer/analyzers/lsp-client.ts",
30
32
  "vectorizer/chunkers/markdown-chunker.ts",
31
33
  "vectorizer/chunkers/code-chunker.ts",
32
34
  "vectorizer/chunkers/chunker-factory.ts",
@@ -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.js"
12
+ import { CodebaseIndexer } 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"],
@@ -61,6 +61,7 @@ Actions:
61
61
  - "list" → List all available indexes with stats
62
62
  - "reindex" → Re-index files using local vectorizer
63
63
  - "test" → Run gold dataset quality tests (if configured)
64
+ - "validate-graph" → Validate graph consistency (orphaned triples, broken chunk refs)
64
65
 
65
66
  Available indexes:
66
67
  - "code" - Source code files
@@ -68,7 +69,7 @@ Available indexes:
68
69
  - "config" - Configuration files`,
69
70
 
70
71
  args: {
71
- action: tool.schema.enum(["status", "list", "reindex", "test"]).describe("Action to perform"),
72
+ action: tool.schema.enum(["status", "list", "reindex", "test", "validate-graph"]).describe("Action to perform"),
72
73
  index: tool.schema.string().optional().default("code").describe("Index name: code, docs, config"),
73
74
  dir: tool.schema.string().optional().describe("Directory to index (default: project root)"),
74
75
  },
@@ -170,11 +171,27 @@ Available indexes:
170
171
 
171
172
  let indexed = 0
172
173
  let skipped = 0
173
- for (const filePath of files) {
174
+ const total = files.length
175
+
176
+ // FR-053: Progress reporting during indexing + graph building
177
+ const progressLines: string[] = []
178
+ for (let i = 0; i < files.length; i++) {
179
+ const filePath = files[i]
174
180
  try {
175
181
  const wasIndexed = await indexer.indexFile(filePath)
176
- if (wasIndexed) indexed++
177
- else skipped++
182
+ if (wasIndexed) {
183
+ indexed++
184
+ // Log progress at 10%, 25%, 50%, 75%, 100% milestones
185
+ const pct = Math.round(((i + 1) / total) * 100)
186
+ if (pct === 10 || pct === 25 || pct === 50 || pct === 75 || pct === 100) {
187
+ const msg = `Building index + graph: ${i + 1}/${total} files (${pct}%)`
188
+ if (progressLines.length === 0 || progressLines[progressLines.length - 1] !== msg) {
189
+ progressLines.push(msg)
190
+ }
191
+ }
192
+ } else {
193
+ skipped++
194
+ }
178
195
  } catch {}
179
196
  }
180
197
 
@@ -184,13 +201,21 @@ Available indexes:
184
201
  let output = `## Re-indexing Complete\n\n`
185
202
  output += `**Index:** ${indexName}\n`
186
203
  output += `**Directory:** ${args.dir || "(project root)"}\n`
187
- output += `**Files found:** ${files.length}\n`
204
+ output += `**Files found:** ${total}\n`
188
205
  output += `**Files indexed:** ${indexed}\n`
189
206
  output += `**Files unchanged:** ${skipped}\n`
190
207
  output += `**Total chunks:** ${stats.chunkCount}\n`
191
208
  if (stats.features) {
192
209
  output += `**Chunking:** ${stats.features.chunking}\n`
193
210
  }
211
+
212
+ if (progressLines.length > 0) {
213
+ output += `\n**Build Progress:**\n`
214
+ for (const line of progressLines) {
215
+ output += `- ${line}\n`
216
+ }
217
+ }
218
+
194
219
  return output
195
220
  } catch (error: any) {
196
221
  return `Re-indexing failed: ${error.message || String(error)}`
@@ -273,6 +298,147 @@ Available indexes:
273
298
  }
274
299
  }
275
300
 
276
- return `Unknown action: ${args.action}. Use: status, list, reindex, or test`
301
+ // NFR-031: Graph validation
302
+ if (args.action === "validate-graph") {
303
+ 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[] = []
317
+ 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
+ }
323
+
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")
329
+ try {
330
+ const rows = await table.search([0]).limit(100000).execute()
331
+ for (const row of rows) {
332
+ if (row.chunk_id) knownChunkIds.add(row.chunk_id)
333
+ }
334
+ } 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}" })`
337
+ }
338
+ }
339
+
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
+ })
359
+ }
360
+
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
+ }
369
+
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
+ }
378
+
379
+ await indexer.unloadModel()
380
+
381
+ // 5. Build report
382
+ const totalOrphaned = orphanedSubjects.length + orphanedObjects.length
383
+ const isHealthy = totalOrphaned === 0
384
+
385
+ let output = `## Graph Validation: "${indexName}"\n\n`
386
+ output += `**Status:** ${isHealthy ? "HEALTHY" : "ISSUES FOUND"}\n\n`
387
+
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
+
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
+
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
+
406
+ if (totalOrphaned > 0) {
407
+ output += `### Orphaned References (${totalOrphaned})\n\n`
408
+
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`
416
+ }
417
+ output += `\n`
418
+ }
419
+
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`
427
+ }
428
+ output += `\n`
429
+ }
430
+
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`
434
+ }
435
+
436
+ return output
437
+ } catch (error: any) {
438
+ return `Graph validation failed: ${error.message || String(error)}`
439
+ }
440
+ }
441
+
442
+ return `Unknown action: ${args.action}. Use: status, list, reindex, test, or validate-graph`
277
443
  },
278
444
  })
package/tools/search.ts CHANGED
@@ -10,7 +10,7 @@ import { tool } from "@opencode-ai/plugin"
10
10
  import path from "path"
11
11
  import fs from "fs/promises"
12
12
 
13
- import { CodebaseIndexer } from "../vectorizer/index.js"
13
+ import { CodebaseIndexer } from "../vectorizer/index.ts"
14
14
 
15
15
  export default tool({
16
16
  description: `Search the codebase semantically. Use this to find relevant code snippets, functions, or files based on meaning, not just text matching.