@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.
- package/api.ts +34 -17
- package/cache/manager.ts +30 -19
- package/cli.ts +8 -5
- package/file-indexer.ts +28 -11
- package/hooks/message-before.ts +5 -5
- package/hooks/tool-substitution.ts +4 -120
- package/index.ts +17 -6
- package/package.json +3 -2
- package/tools/codeindex.ts +192 -184
- package/tools/graph.ts +265 -0
- package/tools/read-interceptor.ts +7 -3
- package/tools/search.ts +268 -190
- package/tools/workspace-state.ts +1 -2
- package/tools/workspace.ts +76 -108
- package/vectorizer/analyzers/lsp-client.ts +52 -6
- package/vectorizer/chunkers/chunker-factory.ts +6 -0
- package/vectorizer/chunkers/code-chunker.ts +73 -16
- package/vectorizer/chunkers/lsp-chunker.ts +313 -191
- package/vectorizer/graph-db.ts +6 -4
- package/vectorizer/index.ts +329 -134
- package/vectorizer/usage-tracker.ts +36 -0
- package/vectorizer.yaml +2 -2
package/tools/codeindex.ts
CHANGED
|
@@ -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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
386
|
+
// 5. Build report
|
|
387
|
+
const totalOrphaned = orphanedSubjects.length + orphanedObjects.length
|
|
388
|
+
const isHealthy = totalOrphaned === 0
|
|
384
389
|
|
|
385
|
-
|
|
386
|
-
|
|
390
|
+
let output = `## Graph Validation: "${indexName}"\n\n`
|
|
391
|
+
output += `**Status:** ${isHealthy ? "HEALTHY" : "ISSUES FOUND"}\n\n`
|
|
387
392
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
407
|
-
|
|
411
|
+
if (totalOrphaned > 0) {
|
|
412
|
+
output += `### Orphaned References (${totalOrphaned})\n\n`
|
|
408
413
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
|
432
|
-
}
|
|
433
|
-
|
|
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
|
}
|