@comfanion/usethis_search 3.0.0-dev.25 → 3.0.0-dev.26
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/package.json +1 -3
- package/vectorizer/graph-db.ts +122 -148
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comfanion/usethis_search",
|
|
3
|
-
"version": "3.0.0-dev.
|
|
3
|
+
"version": "3.0.0-dev.26",
|
|
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",
|
|
@@ -48,8 +48,6 @@
|
|
|
48
48
|
"@opencode-ai/plugin": ">=1.1.0",
|
|
49
49
|
"@xenova/transformers": "^2.17.0",
|
|
50
50
|
"glob": "^10.3.10",
|
|
51
|
-
"level": "^8.0.1",
|
|
52
|
-
"levelgraph": "^4.0.0",
|
|
53
51
|
"vectordb": "^0.4.0"
|
|
54
52
|
},
|
|
55
53
|
"peerDependencies": {
|
package/vectorizer/graph-db.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Level } from "level"
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
3
2
|
import { filePathFromNodeId, isStructuralPredicate } from "./graph-builder"
|
|
4
3
|
|
|
5
4
|
export interface Triple {
|
|
@@ -13,98 +12,137 @@ export interface Triple {
|
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
export class GraphDB {
|
|
16
|
-
private db:
|
|
15
|
+
private db: Database | null = null
|
|
17
16
|
private initialized: boolean = false
|
|
18
17
|
|
|
18
|
+
// Prepared statements (cached for performance)
|
|
19
|
+
private _stmtInsert: any = null
|
|
20
|
+
private _stmtBySubject: any = null
|
|
21
|
+
private _stmtByObject: any = null
|
|
22
|
+
private _stmtByFile: any = null
|
|
23
|
+
private _stmtDeleteByFile: any = null
|
|
24
|
+
private _stmtBySubjectPredicate: any = null
|
|
25
|
+
private _stmtByPredicate: any = null
|
|
26
|
+
private _stmtAll: any = null
|
|
27
|
+
|
|
19
28
|
constructor(private dbPath: string) {}
|
|
20
29
|
|
|
21
30
|
async init(): Promise<this> {
|
|
22
|
-
|
|
23
|
-
this.db
|
|
31
|
+
// bun:sqlite uses a file path; append .db if not already
|
|
32
|
+
const fullPath = this.dbPath.endsWith(".db") ? this.dbPath : this.dbPath + ".db"
|
|
33
|
+
this.db = new Database(fullPath)
|
|
34
|
+
|
|
35
|
+
// WAL mode for concurrent readers
|
|
36
|
+
this.db.exec("PRAGMA journal_mode = WAL")
|
|
37
|
+
this.db.exec("PRAGMA synchronous = NORMAL") // faster writes, safe with WAL
|
|
38
|
+
this.db.exec("PRAGMA cache_size = -2000") // 2MB cache
|
|
39
|
+
|
|
40
|
+
// Create triples table
|
|
41
|
+
this.db.exec(`
|
|
42
|
+
CREATE TABLE IF NOT EXISTS triples (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
subject TEXT NOT NULL,
|
|
45
|
+
predicate TEXT NOT NULL,
|
|
46
|
+
object TEXT NOT NULL,
|
|
47
|
+
weight REAL NOT NULL DEFAULT 0,
|
|
48
|
+
source TEXT NOT NULL DEFAULT '',
|
|
49
|
+
file TEXT NOT NULL DEFAULT '',
|
|
50
|
+
line INTEGER
|
|
51
|
+
)
|
|
52
|
+
`)
|
|
53
|
+
|
|
54
|
+
// Indexes for fast lookups
|
|
55
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_subject ON triples(subject)")
|
|
56
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_object ON triples(object)")
|
|
57
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_file ON triples(file)")
|
|
58
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_predicate ON triples(predicate)")
|
|
59
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_subject_predicate ON triples(subject, predicate)")
|
|
60
|
+
|
|
61
|
+
// Prepare statements
|
|
62
|
+
this._stmtInsert = this.db.prepare(
|
|
63
|
+
"INSERT INTO triples (subject, predicate, object, weight, source, file, line) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
64
|
+
)
|
|
65
|
+
this._stmtBySubject = this.db.prepare("SELECT * FROM triples WHERE subject = ?")
|
|
66
|
+
this._stmtByObject = this.db.prepare("SELECT * FROM triples WHERE object = ?")
|
|
67
|
+
this._stmtByFile = this.db.prepare("SELECT * FROM triples WHERE file = ?")
|
|
68
|
+
this._stmtDeleteByFile = this.db.prepare("DELETE FROM triples WHERE file = ?")
|
|
69
|
+
this._stmtBySubjectPredicate = this.db.prepare("SELECT * FROM triples WHERE subject = ? AND predicate = ?")
|
|
70
|
+
this._stmtByPredicate = this.db.prepare("SELECT * FROM triples WHERE predicate = ?")
|
|
71
|
+
this._stmtAll = this.db.prepare("SELECT * FROM triples")
|
|
72
|
+
|
|
24
73
|
this.initialized = true
|
|
25
74
|
return this
|
|
26
75
|
}
|
|
27
76
|
|
|
77
|
+
private toTriple(row: any): Triple {
|
|
78
|
+
return {
|
|
79
|
+
subject: row.subject,
|
|
80
|
+
predicate: row.predicate,
|
|
81
|
+
object: row.object,
|
|
82
|
+
weight: row.weight,
|
|
83
|
+
source: row.source,
|
|
84
|
+
file: row.file,
|
|
85
|
+
line: row.line ?? undefined,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
28
89
|
async putEdges(triples: Triple[]): Promise<void> {
|
|
29
|
-
if (!this.initialized) {
|
|
90
|
+
if (!this.initialized || !this.db) {
|
|
30
91
|
throw new Error("GraphDB not initialized. Call init() first.")
|
|
31
92
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
93
|
+
|
|
94
|
+
// Batch insert in a single transaction — much faster than individual inserts
|
|
95
|
+
const insertMany = this.db.transaction((items: Triple[]) => {
|
|
96
|
+
for (const t of items) {
|
|
97
|
+
this._stmtInsert.run(t.subject, t.predicate, t.object, t.weight, t.source, t.file, t.line ?? null)
|
|
98
|
+
}
|
|
37
99
|
})
|
|
100
|
+
insertMany(triples)
|
|
38
101
|
}
|
|
39
102
|
|
|
40
103
|
async getOutgoing(chunkId: string): Promise<Triple[]> {
|
|
41
|
-
if (!this.initialized) {
|
|
104
|
+
if (!this.initialized || !this.db) {
|
|
42
105
|
throw new Error("GraphDB not initialized. Call init() first.")
|
|
43
106
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (err) reject(err)
|
|
47
|
-
else resolve(triples || [])
|
|
48
|
-
})
|
|
49
|
-
})
|
|
107
|
+
const rows = this._stmtBySubject.all(chunkId)
|
|
108
|
+
return rows.map((r: any) => this.toTriple(r))
|
|
50
109
|
}
|
|
51
110
|
|
|
52
111
|
async getIncoming(chunkId: string): Promise<Triple[]> {
|
|
53
|
-
if (!this.initialized) {
|
|
112
|
+
if (!this.initialized || !this.db) {
|
|
54
113
|
throw new Error("GraphDB not initialized. Call init() first.")
|
|
55
114
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (err) reject(err)
|
|
59
|
-
else resolve(triples || [])
|
|
60
|
-
})
|
|
61
|
-
})
|
|
115
|
+
const rows = this._stmtByObject.all(chunkId)
|
|
116
|
+
return rows.map((r: any) => this.toTriple(r))
|
|
62
117
|
}
|
|
63
118
|
|
|
64
119
|
async deleteByFile(filePath: string): Promise<void> {
|
|
65
|
-
if (!this.initialized) {
|
|
120
|
+
if (!this.initialized || !this.db) {
|
|
66
121
|
throw new Error("GraphDB not initialized. Call init() first.")
|
|
67
122
|
}
|
|
68
|
-
|
|
69
|
-
this.db.get({}, (err: Error | undefined, triples: Triple[]) => {
|
|
70
|
-
if (err) reject(err)
|
|
71
|
-
else resolve(triples || [])
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
const toDelete = allTriples.filter(t => t.file === filePath)
|
|
76
|
-
|
|
77
|
-
for (const t of toDelete) {
|
|
78
|
-
await new Promise<void>((resolve, reject) => {
|
|
79
|
-
this.db.del(t, (err: Error | undefined) => {
|
|
80
|
-
if (err) reject(err)
|
|
81
|
-
else resolve()
|
|
82
|
-
})
|
|
83
|
-
})
|
|
84
|
-
}
|
|
123
|
+
this._stmtDeleteByFile.run(filePath)
|
|
85
124
|
}
|
|
86
125
|
|
|
87
126
|
async close(): Promise<void> {
|
|
88
127
|
if (this.initialized && this.db) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
128
|
+
this.db.close()
|
|
129
|
+
this.db = null
|
|
130
|
+
this._stmtInsert = null
|
|
131
|
+
this._stmtBySubject = null
|
|
132
|
+
this._stmtByObject = null
|
|
133
|
+
this._stmtByFile = null
|
|
134
|
+
this._stmtDeleteByFile = null
|
|
135
|
+
this._stmtBySubjectPredicate = null
|
|
136
|
+
this._stmtByPredicate = null
|
|
137
|
+
this._stmtAll = null
|
|
95
138
|
this.initialized = false
|
|
96
139
|
}
|
|
97
140
|
}
|
|
98
141
|
|
|
99
142
|
// ---- FR-054: File metadata triples for incremental updates -----------------
|
|
100
143
|
|
|
101
|
-
/**
|
|
102
|
-
* Store graph build metadata for a file as a special triple.
|
|
103
|
-
* Subject: `meta:<filePath>`, Predicate: `graph_built`, Object: `<hash>`.
|
|
104
|
-
* Weight encodes the timestamp (seconds since epoch).
|
|
105
|
-
*/
|
|
106
144
|
async setFileMeta(filePath: string, hash: string, timestamp: number): Promise<void> {
|
|
107
|
-
if (!this.initialized) throw new Error("GraphDB not initialized. Call init() first.")
|
|
145
|
+
if (!this.initialized || !this.db) throw new Error("GraphDB not initialized. Call init() first.")
|
|
108
146
|
|
|
109
147
|
// Remove old meta triple for this file first
|
|
110
148
|
await this.deleteFileMeta(filePath)
|
|
@@ -113,111 +151,61 @@ export class GraphDB {
|
|
|
113
151
|
subject: `meta:${filePath}`,
|
|
114
152
|
predicate: "graph_built",
|
|
115
153
|
object: hash,
|
|
116
|
-
weight: Math.floor(timestamp / 1000),
|
|
154
|
+
weight: Math.floor(timestamp / 1000),
|
|
117
155
|
source: "meta",
|
|
118
156
|
file: filePath,
|
|
119
157
|
}
|
|
120
158
|
await this.putEdges([triple])
|
|
121
159
|
}
|
|
122
160
|
|
|
123
|
-
/**
|
|
124
|
-
* Get the stored graph build metadata for a file.
|
|
125
|
-
* Returns { hash, timestamp } or null if not found.
|
|
126
|
-
*/
|
|
127
161
|
async getFileMeta(filePath: string): Promise<{ hash: string; timestamp: number } | null> {
|
|
128
|
-
if (!this.initialized) throw new Error("GraphDB not initialized. Call init() first.")
|
|
129
|
-
|
|
130
|
-
const triples = await new Promise<Triple[]>((resolve, reject) => {
|
|
131
|
-
this.db.get(
|
|
132
|
-
{ subject: `meta:${filePath}`, predicate: "graph_built" },
|
|
133
|
-
(err: Error | undefined, result: Triple[]) => {
|
|
134
|
-
if (err) reject(err)
|
|
135
|
-
else resolve(result || [])
|
|
136
|
-
},
|
|
137
|
-
)
|
|
138
|
-
})
|
|
162
|
+
if (!this.initialized || !this.db) throw new Error("GraphDB not initialized. Call init() first.")
|
|
139
163
|
|
|
140
|
-
|
|
164
|
+
const rows = this._stmtBySubjectPredicate.all(`meta:${filePath}`, "graph_built")
|
|
165
|
+
if (rows.length === 0) return null
|
|
141
166
|
return {
|
|
142
|
-
hash:
|
|
143
|
-
timestamp:
|
|
167
|
+
hash: rows[0].object,
|
|
168
|
+
timestamp: rows[0].weight * 1000,
|
|
144
169
|
}
|
|
145
170
|
}
|
|
146
171
|
|
|
147
|
-
/**
|
|
148
|
-
* Delete file meta triple.
|
|
149
|
-
*/
|
|
150
172
|
async deleteFileMeta(filePath: string): Promise<void> {
|
|
151
|
-
if (!this.initialized) throw new Error("GraphDB not initialized. Call init() first.")
|
|
173
|
+
if (!this.initialized || !this.db) throw new Error("GraphDB not initialized. Call init() first.")
|
|
152
174
|
|
|
153
175
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (err) reject(err)
|
|
159
|
-
else resolve(result || [])
|
|
160
|
-
},
|
|
161
|
-
)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
for (const t of triples) {
|
|
165
|
-
await new Promise<void>((resolve, reject) => {
|
|
166
|
-
this.db.del(t, (err: Error | undefined) => {
|
|
167
|
-
if (err) reject(err)
|
|
168
|
-
else resolve()
|
|
169
|
-
})
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
} catch (err) {
|
|
173
|
-
// Silently ignore errors (e.g., no meta triple exists)
|
|
176
|
+
this.db!.prepare("DELETE FROM triples WHERE subject = ? AND predicate = ?")
|
|
177
|
+
.run(`meta:${filePath}`, "graph_built")
|
|
178
|
+
} catch {
|
|
179
|
+
// Silently ignore errors
|
|
174
180
|
}
|
|
175
181
|
}
|
|
176
182
|
|
|
177
|
-
/**
|
|
178
|
-
* Get all file metadata triples (for validation / stats).
|
|
179
|
-
*/
|
|
180
183
|
async getAllFileMeta(): Promise<Array<{ filePath: string; hash: string; timestamp: number }>> {
|
|
181
|
-
if (!this.initialized) throw new Error("GraphDB not initialized. Call init() first.")
|
|
182
|
-
|
|
183
|
-
const triples = await new Promise<Triple[]>((resolve, reject) => {
|
|
184
|
-
this.db.get({ predicate: "graph_built" }, (err: Error | undefined, result: Triple[]) => {
|
|
185
|
-
if (err) reject(err)
|
|
186
|
-
else resolve(result || [])
|
|
187
|
-
})
|
|
188
|
-
})
|
|
184
|
+
if (!this.initialized || !this.db) throw new Error("GraphDB not initialized. Call init() first.")
|
|
189
185
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
186
|
+
const rows = this._stmtByPredicate.all("graph_built")
|
|
187
|
+
return rows.map((r: any) => ({
|
|
188
|
+
filePath: r.subject.replace(/^meta:/, ""),
|
|
189
|
+
hash: r.object,
|
|
190
|
+
timestamp: r.weight * 1000,
|
|
194
191
|
}))
|
|
195
192
|
}
|
|
196
193
|
|
|
197
|
-
/**
|
|
198
|
-
* Get all triples in the graph (for validation/stats).
|
|
199
|
-
* Excludes meta, anchor, and structural triples by default.
|
|
200
|
-
* Pass includeStructural=true to also get structural edges.
|
|
201
|
-
*/
|
|
202
194
|
async getAllTriples(includeStructural: boolean = false): Promise<Triple[]> {
|
|
203
|
-
if (!this.initialized) throw new Error("GraphDB not initialized. Call init() first.")
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
195
|
+
if (!this.initialized || !this.db) throw new Error("GraphDB not initialized. Call init() first.")
|
|
196
|
+
|
|
197
|
+
const allRows = this._stmtAll.all()
|
|
198
|
+
return allRows
|
|
199
|
+
.map((r: any) => this.toTriple(r))
|
|
200
|
+
.filter((t: Triple) => {
|
|
201
|
+
if (t.predicate === "graph_built" || t.predicate === "belongs_to") return false
|
|
202
|
+
if (!includeStructural && isStructuralPredicate(t.predicate)) return false
|
|
203
|
+
return true
|
|
209
204
|
})
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
return allTriples.filter(t => {
|
|
213
|
-
if (t.predicate === "graph_built" || t.predicate === "belongs_to") return false
|
|
214
|
-
if (!includeStructural && isStructuralPredicate(t.predicate)) return false
|
|
215
|
-
return true
|
|
216
|
-
})
|
|
217
205
|
}
|
|
218
206
|
|
|
219
207
|
async getRelatedFiles(chunkId: string, maxDepth: number = 1): Promise<{path: string, relation: string, weight: number}[]> {
|
|
220
|
-
if (!this.initialized) {
|
|
208
|
+
if (!this.initialized || !this.db) {
|
|
221
209
|
throw new Error("GraphDB not initialized. Call init() first.")
|
|
222
210
|
}
|
|
223
211
|
|
|
@@ -225,7 +213,6 @@ export class GraphDB {
|
|
|
225
213
|
const visited = new Set<string>()
|
|
226
214
|
const self = this
|
|
227
215
|
|
|
228
|
-
// Resolve the caller's file directly from the node ID
|
|
229
216
|
const callerFile = filePathFromNodeId(chunkId)
|
|
230
217
|
|
|
231
218
|
async function traverse(currentId: string, currentDepth: number, currentRelation: string) {
|
|
@@ -236,19 +223,12 @@ export class GraphDB {
|
|
|
236
223
|
visited.add(currentId)
|
|
237
224
|
|
|
238
225
|
try {
|
|
239
|
-
const outgoing =
|
|
240
|
-
self.db.get({ subject: currentId }, (err: Error | undefined, triples: Triple[]) => {
|
|
241
|
-
if (err) reject(err)
|
|
242
|
-
else resolve(triples || [])
|
|
243
|
-
})
|
|
244
|
-
})
|
|
226
|
+
const outgoing = self._stmtBySubject.all(currentId).map((r: any) => self.toTriple(r))
|
|
245
227
|
|
|
246
228
|
for (const triple of outgoing) {
|
|
247
|
-
// Skip meta, anchor, and structural-only edges
|
|
248
229
|
if (triple.predicate === "graph_built" || triple.predicate === "belongs_to") continue
|
|
249
230
|
if (isStructuralPredicate(triple.predicate)) continue
|
|
250
231
|
|
|
251
|
-
// Resolve file for the target node directly from its ID
|
|
252
232
|
const targetFile = filePathFromNodeId(triple.object)
|
|
253
233
|
if (!targetFile) continue
|
|
254
234
|
|
|
@@ -267,12 +247,7 @@ export class GraphDB {
|
|
|
267
247
|
}
|
|
268
248
|
}
|
|
269
249
|
|
|
270
|
-
const incoming =
|
|
271
|
-
self.db.get({ object: currentId }, (err: Error | undefined, triples: Triple[]) => {
|
|
272
|
-
if (err) reject(err)
|
|
273
|
-
else resolve(triples || [])
|
|
274
|
-
})
|
|
275
|
-
})
|
|
250
|
+
const incoming = self._stmtByObject.all(currentId).map((r: any) => self.toTriple(r))
|
|
276
251
|
|
|
277
252
|
for (const triple of incoming) {
|
|
278
253
|
if (triple.predicate === "graph_built" || triple.predicate === "belongs_to") continue
|
|
@@ -298,7 +273,6 @@ export class GraphDB {
|
|
|
298
273
|
|
|
299
274
|
await traverse(chunkId, 0, "")
|
|
300
275
|
|
|
301
|
-
// Remove the caller's own file from results
|
|
302
276
|
if (callerFile) relatedFiles.delete(callerFile)
|
|
303
277
|
|
|
304
278
|
return Array.from(relatedFiles.entries())
|