@psnext/lscg 0.1.2 → 0.1.4

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.
Files changed (48) hide show
  1. package/README.md +69 -3
  2. package/dist/src/cli.js +68 -13
  3. package/dist/src/graph/repository.d.ts +4 -1
  4. package/dist/src/graph/repository.js +106 -6
  5. package/dist/src/index.d.ts +3 -0
  6. package/dist/src/index.js +2 -0
  7. package/dist/src/mcp/server.js +2 -2
  8. package/dist/src/scanner/discover.js +0 -2
  9. package/dist/src/scanner/packagePlugin.d.ts +3 -0
  10. package/dist/src/scanner/packagePlugin.js +168 -0
  11. package/dist/src/scanner/plugins.d.ts +86 -0
  12. package/dist/src/scanner/plugins.js +503 -0
  13. package/dist/src/storage/connection.d.ts +6 -0
  14. package/dist/src/storage/connection.js +133 -0
  15. package/dist/src/storage/context-queries.d.ts +20 -0
  16. package/dist/src/storage/context-queries.js +137 -0
  17. package/dist/src/storage/database.d.ts +12 -71
  18. package/dist/src/storage/database.js +12 -562
  19. package/dist/src/storage/graph-writes.d.ts +17 -0
  20. package/dist/src/storage/graph-writes.js +126 -0
  21. package/dist/src/storage/plugin-contributions.d.ts +15 -0
  22. package/dist/src/storage/plugin-contributions.js +28 -0
  23. package/dist/src/storage/plugin-graph.d.ts +6 -0
  24. package/dist/src/storage/plugin-graph.js +81 -0
  25. package/dist/src/storage/queries.d.ts +25 -0
  26. package/dist/src/storage/queries.js +129 -0
  27. package/dist/src/storage/row-decoders.d.ts +8 -0
  28. package/dist/src/storage/row-decoders.js +37 -0
  29. package/dist/src/storage/schema.d.ts +2 -2
  30. package/dist/src/storage/schema.js +13 -2
  31. package/dist/src/storage/traversal-queries.d.ts +15 -0
  32. package/dist/src/storage/traversal-queries.js +87 -0
  33. package/dist/src/types.d.ts +10 -4
  34. package/dist/src/view/index.d.ts +1 -1
  35. package/dist/src/view/index.js +2 -2
  36. package/dist/src/view/model.d.ts +2 -0
  37. package/dist/src/view/render.d.ts +5 -2
  38. package/dist/src/view/render.js +81 -13
  39. package/dist/src/view/templates/icons/call.svg +13 -0
  40. package/dist/src/view/templates/icons/export.svg +1 -0
  41. package/dist/src/view/templates/icons/file.svg +9 -0
  42. package/dist/src/view/templates/icons/import.svg +1 -0
  43. package/dist/src/view/templates/icons/package.svg +1 -0
  44. package/dist/src/view/templates/icons/symbol.svg +7 -0
  45. package/dist/src/view/templates/icons/user.svg +15 -0
  46. package/dist/src/view/templates/interactive.css +17 -11
  47. package/dist/src/view/templates/interactive.html +236 -52
  48. package/package.json +1 -1
@@ -1,563 +1,13 @@
1
- import { mkdirSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { DatabaseSync } from 'node:sqlite';
4
- import { hashParts } from '../graph/extract.js';
5
- import { CREATE_NODES_TABLE_SQL, CREATE_SCHEMA_SQL, SCHEMA_VERSION } from './schema.js';
6
- export function openGraphDatabase(databasePath) {
7
- mkdirSync(path.dirname(databasePath), { recursive: true });
8
- const db = new DatabaseSync(databasePath);
9
- db.exec('PRAGMA foreign_keys = ON;');
10
- db.exec('PRAGMA journal_mode = WAL;');
11
- db.exec(CREATE_SCHEMA_SQL);
12
- ensureSchemaVersion(db);
13
- return db;
14
- }
15
- export function openReadOnlyGraphDatabase(databasePath) {
16
- const db = new DatabaseSync(databasePath, { readOnly: true });
17
- db.exec('PRAGMA foreign_keys = ON;');
18
- return db;
19
- }
20
- export function closeDatabase(db) {
21
- db.close();
22
- }
23
- export function upsertRepository(db, repository) {
24
- db.prepare(`
25
- INSERT INTO repositories(id, root, name, updated_at)
26
- VALUES (?, ?, ?, datetime('now'))
27
- ON CONFLICT(id) DO UPDATE SET
28
- root = excluded.root,
29
- name = excluded.name,
30
- updated_at = datetime('now')
31
- `).run(repository.id, repository.root, repository.name);
32
- }
33
- export function replaceFileGraph(db, { repository, file, nodes, edges, attribution }) {
34
- upsertRepository(db, repository);
35
- db.exec('BEGIN IMMEDIATE');
36
- try {
37
- if (attribution) {
38
- upsertContributorNodes(db, repository, attribution.contributorEmails);
39
- }
40
- db.prepare('DELETE FROM edges WHERE file_id = ?').run(file.id);
41
- db.prepare('DELETE FROM nodes WHERE file_id = ?').run(file.id);
42
- db.prepare(`
43
- INSERT INTO files(id, repository_id, path, language, hash, size, mtime_ms, scanned_at)
44
- VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
45
- ON CONFLICT(repository_id, path) DO UPDATE SET
46
- id = excluded.id,
47
- language = excluded.language,
48
- hash = excluded.hash,
49
- size = excluded.size,
50
- mtime_ms = excluded.mtime_ms,
51
- scanned_at = datetime('now')
52
- `).run(file.id, repository.id, file.path, file.language, file.hash, file.size, file.mtimeMs);
53
- const insertNode = db.prepare(`
54
- INSERT INTO nodes(
55
- id, repository_id, file_id, kind, type, name, start_byte, end_byte,
56
- start_point, end_point, source_hash, parser, parser_version, metadata_json, last_modified_user_id
57
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
58
- `);
59
- const attributionByNodeId = new Map(attribution?.nodeAttributions.map((entry) => [entry.nodeId, entry]) ?? []);
60
- for (const node of nodes) {
61
- const nodeAttribution = attributionByNodeId.get(node.id);
62
- insertNode.run(node.id, repository.id, file.id, node.kind, node.type, node.name ?? null, node.startByte, node.endByte, JSON.stringify(node.startPoint), JSON.stringify(node.endPoint), node.sourceHash, node.parser, node.parserVersion, JSON.stringify(node.metadata ?? {}), node.lastModifiedUserId ?? resolveUserId(repository, nodeAttribution?.lastModifiedEmail ?? null));
63
- }
64
- const insertEdge = db.prepare(`
65
- INSERT INTO edges(
66
- id, repository_id, file_id, source_id, target_id, kind, confidence, metadata_json
67
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
68
- `);
69
- for (const edge of edges) {
70
- insertEdge.run(edge.id, repository.id, file.id, edge.sourceId, edge.targetId, edge.kind, edge.confidence ?? 1, JSON.stringify(edge.metadata ?? {}));
71
- }
72
- if (attribution) {
73
- for (const entry of attribution.nodeAttributions) {
74
- for (const email of entry.contributorEmails) {
75
- const contributorId = resolveUserId(repository, email);
76
- if (!contributorId)
77
- continue;
78
- insertEdge.run(hashParts([repository.id, file.id, entry.nodeId, contributorId, 'attributed_to']), repository.id, file.id, entry.nodeId, contributorId, 'attributed_to', 1, JSON.stringify({ email }));
79
- }
80
- }
81
- }
82
- db.exec('COMMIT');
83
- }
84
- catch (error) {
85
- db.exec('ROLLBACK');
86
- throw error;
87
- }
88
- }
89
- export function getStatus(db, repositoryId) {
90
- const repository = db.prepare('SELECT * FROM repositories WHERE id = ?').get(repositoryId);
91
- const files = db.prepare('SELECT count(*) AS count FROM files WHERE repository_id = ?').get(repositoryId).count;
92
- const nodes = db.prepare('SELECT count(*) AS count FROM nodes WHERE repository_id = ?').get(repositoryId).count;
93
- const edges = db.prepare('SELECT count(*) AS count FROM edges WHERE repository_id = ?').get(repositoryId).count;
94
- return { repository: repository ?? null, files, nodes, edges };
95
- }
96
- export function selectNodes(db, repositoryId, { kind, limit = 50 } = {}) {
97
- const safeLimit = clampLimit(limit);
98
- if (kind) {
99
- return db.prepare(`
100
- SELECT id, kind, type, name, file_id AS fileId, start_byte AS startByte, end_byte AS endByte, metadata_json AS metadataJson, last_modified_user_id AS lastModifiedUserId
101
- FROM nodes
102
- WHERE repository_id = ? AND kind = ?
103
- ORDER BY name IS NULL, name, id
104
- LIMIT ?
105
- `).all(repositoryId, kind, safeLimit);
106
- }
107
- return db.prepare(`
108
- SELECT id, kind, type, name, file_id AS fileId, start_byte AS startByte, end_byte AS endByte, metadata_json AS metadataJson, last_modified_user_id AS lastModifiedUserId
109
- FROM nodes
110
- WHERE repository_id = ?
111
- ORDER BY kind, name IS NULL, name, id
112
- LIMIT ?
113
- `).all(repositoryId, safeLimit);
114
- }
115
- export function selectNodeTextRows(db, repositoryId, { kind, term, limit = 50 } = {}) {
116
- const predicates = ['n.repository_id = ?'];
117
- const params = [repositoryId];
118
- if (kind) {
119
- predicates.push('n.kind = ?');
120
- params.push(kind);
121
- }
122
- if (term) {
123
- predicates.push('lower(COALESCE(n.name, \'\')) LIKE ?');
124
- params.push(`%${term.toLocaleLowerCase()}%`);
125
- }
126
- const rows = db.prepare(`
127
- SELECT n.id, n.kind, n.type, n.name, n.file_id AS fileId,
128
- n.start_byte AS startByte, n.end_byte AS endByte,
129
- n.start_point AS startPoint, n.end_point AS endPoint,
130
- n.metadata_json AS metadataJson, n.last_modified_user_id AS lastModifiedUserId,
131
- f.path AS path
132
- FROM nodes n LEFT JOIN files f ON f.id = n.file_id
133
- WHERE ${predicates.join(' AND ')}
134
- ORDER BY n.kind, n.name IS NULL, n.name, n.id
135
- LIMIT ?
136
- `).all(...params, clampLimit(limit));
137
- return rows.map((row) => ({
138
- ...row,
139
- startPoint: parsePoint(row.startPoint),
140
- endPoint: parsePoint(row.endPoint)
141
- }));
142
- }
143
- export function selectAllNodes(db, repositoryId, { kind } = {}) {
144
- if (kind) {
145
- return db.prepare(`
146
- SELECT id, kind, type, name, file_id AS fileId, start_byte AS startByte, end_byte AS endByte, metadata_json AS metadataJson, last_modified_user_id AS lastModifiedUserId
147
- FROM nodes
148
- WHERE repository_id = ? AND kind = ?
149
- ORDER BY name IS NULL, name, id
150
- `).all(repositoryId, kind);
151
- }
152
- return db.prepare(`
153
- SELECT id, kind, type, name, file_id AS fileId, start_byte AS startByte, end_byte AS endByte, metadata_json AS metadataJson, last_modified_user_id AS lastModifiedUserId
154
- FROM nodes
155
- WHERE repository_id = ?
156
- ORDER BY kind, name IS NULL, name, id
157
- `).all(repositoryId);
158
- }
159
- export function selectEdges(db, repositoryId, { kind, limit = 50 } = {}) {
160
- const safeLimit = clampLimit(limit);
161
- if (kind) {
162
- return db.prepare(`
163
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
164
- FROM edges
165
- WHERE repository_id = ? AND kind = ?
166
- ORDER BY kind, id
167
- LIMIT ?
168
- `).all(repositoryId, kind, safeLimit);
169
- }
170
- return db.prepare(`
171
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
172
- FROM edges
173
- WHERE repository_id = ?
174
- ORDER BY kind, id
175
- LIMIT ?
176
- `).all(repositoryId, safeLimit);
177
- }
178
- export function selectAllEdges(db, repositoryId, { kind } = {}) {
179
- if (kind) {
180
- return db.prepare(`
181
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
182
- FROM edges
183
- WHERE repository_id = ? AND kind = ?
184
- ORDER BY kind, id
185
- `).all(repositoryId, kind);
186
- }
187
- return db.prepare(`
188
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
189
- FROM edges
190
- WHERE repository_id = ?
191
- ORDER BY kind, id
192
- `).all(repositoryId);
193
- }
194
- /** Candidate lookup used by context. Stages are queried independently so a unique
195
- * higher-priority match never gets merged with lower-priority fuzzy matches. */
196
- export function selectContextCandidates(db, repositoryId, term, { kind, file, limit = 20 } = {}) {
197
- const safeLimit = clampLimit(limit, 100);
198
- const normalized = term.trim().toLocaleLowerCase();
199
- if (!normalized)
200
- return [];
201
- const rows = [];
202
- const seen = new Set();
203
- const stages = [
204
- { match: 'qualified', condition: "(lower(f.path || ':' || COALESCE(n.name, '')) = ? OR lower(f.path || '::' || COALESCE(n.name, '')) = ?)", value: normalized },
205
- { match: 'exact', condition: 'lower(n.name) = ?', value: normalized },
206
- { match: 'prefix', condition: 'lower(n.name) LIKE ?', value: `${normalized}%` },
207
- { match: 'substring', condition: 'lower(n.name) LIKE ?', value: `%${normalized}%` }
208
- ];
209
- for (let stage = 0; stage < stages.length; stage += 1) {
210
- const entry = stages[stage];
211
- if (!entry)
212
- continue;
213
- const predicates = [`n.repository_id = ?`, entry.condition];
214
- const params = [repositoryId, entry.value, entry.value];
215
- if (!kind)
216
- predicates.push("n.kind IN ('symbol', 'file')");
217
- if (entry.match !== 'qualified')
218
- params.pop();
219
- if (kind) {
220
- predicates.push('n.kind = ?');
221
- params.push(kind);
222
- }
223
- if (file) {
224
- predicates.push('f.path = ?');
225
- params.push(file);
226
- }
227
- const selected = db.prepare(`
228
- SELECT n.id, n.kind, n.type, n.name, n.file_id AS fileId,
229
- n.start_point AS startPoint, n.end_point AS endPoint, n.source_hash AS sourceHash,
230
- n.parser, n.parser_version AS parserVersion, n.metadata_json AS metadataJson,
231
- f.path AS path
232
- FROM nodes n LEFT JOIN files f ON f.id = n.file_id
233
- WHERE ${predicates.join(' AND ')}
234
- ORDER BY lower(COALESCE(f.path, '')), n.kind, lower(COALESCE(n.name, '')), n.id
235
- LIMIT ?
236
- `).all(...params, safeLimit);
237
- for (const row of selected) {
238
- const id = String(row.id);
239
- if (seen.has(id))
240
- continue;
241
- seen.add(id);
242
- rows.push({
243
- ...contextReferenceFromRow(row),
244
- rank: stage,
245
- match: entry.match,
246
- stage
247
- });
248
- }
249
- // A unique exact/qualified match is authoritative. Prefix and substring are
250
- // only consulted when no higher-priority stage produced a result.
251
- if (rows.length > 0)
252
- break;
253
- }
254
- return rows.map(({ stage: _stage, ...candidate }) => candidate);
255
- }
256
- export function selectContextRelationships(db, repositoryId, anchorId, limit = 50, maxDepth = 1) {
257
- const safeLimit = clampLimit(limit, 500);
258
- const safeDepth = Math.max(1, Math.min(Number(maxDepth) || 1, 5));
259
- const rows = [];
260
- const seenNodes = new Set([anchorId]);
261
- const seenEdges = new Set();
262
- let frontier = [anchorId];
263
- const kinds = ['calls', 'imports', 'exports'];
264
- for (let depth = 1; depth <= safeDepth && frontier.length > 0; depth += 1) {
265
- const next = new Set();
266
- for (const kind of kinds) {
267
- const remaining = safeLimit - rows.filter((row) => row.edgeKind === kind).length;
268
- if (remaining <= 0)
269
- continue;
270
- const placeholders = frontier.map(() => '?').join(', ');
271
- const selected = db.prepare(`
272
- SELECT e.id AS edgeId, e.kind AS edgeKind, e.confidence, e.metadata_json AS edgeMetadata,
273
- s.id AS sourceId, s.kind AS sourceKind, s.type AS sourceType, s.name AS sourceName,
274
- s.start_point AS sourceStartPoint, s.end_point AS sourceEndPoint, s.source_hash AS sourceHash,
275
- s.parser AS sourceParser, s.parser_version AS sourceParserVersion, s.metadata_json AS sourceMetadata,
276
- sf.path AS sourcePath,
277
- t.id AS targetId, t.kind AS targetKind, t.type AS targetType, t.name AS targetName,
278
- t.start_point AS targetStartPoint, t.end_point AS targetEndPoint, t.source_hash AS targetHash,
279
- t.parser AS targetParser, t.parser_version AS targetParserVersion, t.metadata_json AS targetMetadata,
280
- tf.path AS targetPath
281
- FROM edges e
282
- JOIN nodes s ON s.id = e.source_id AND s.repository_id = e.repository_id
283
- JOIN nodes t ON t.id = e.target_id AND t.repository_id = e.repository_id
284
- LEFT JOIN files sf ON sf.id = s.file_id
285
- LEFT JOIN files tf ON tf.id = t.file_id
286
- WHERE e.repository_id = ? AND e.kind = ?
287
- AND (e.source_id IN (${placeholders}) OR e.target_id IN (${placeholders}))
288
- ORDER BY COALESCE(sf.path, tf.path), e.id
289
- LIMIT ?
290
- `).all(repositoryId, kind, ...frontier, ...frontier, remaining);
291
- for (const row of selected) {
292
- const edgeId = String(row.edgeId);
293
- if (seenEdges.has(edgeId))
294
- continue;
295
- seenEdges.add(edgeId);
296
- rows.push({ ...row, depth });
297
- const sourceId = String(row.sourceId);
298
- const targetId = String(row.targetId);
299
- if (seenNodes.has(sourceId))
300
- next.add(targetId);
301
- if (seenNodes.has(targetId))
302
- next.add(sourceId);
303
- }
304
- }
305
- for (const nodeId of next)
306
- seenNodes.add(nodeId);
307
- frontier = [...next].filter((nodeId) => nodeId !== anchorId);
308
- }
309
- return rows.map((row) => ({
310
- edgeId: String(row.edgeId),
311
- edgeKind: row.edgeKind,
312
- source: contextReferenceFromRow({
313
- id: row.sourceId, kind: row.sourceKind, type: row.sourceType, name: row.sourceName,
314
- startPoint: row.sourceStartPoint, endPoint: row.sourceEndPoint, sourceHash: row.sourceHash,
315
- parser: row.sourceParser, parserVersion: row.sourceParserVersion, metadataJson: row.sourceMetadata,
316
- path: row.sourcePath
317
- }),
318
- target: contextReferenceFromRow({
319
- id: row.targetId, kind: row.targetKind, type: row.targetType, name: row.targetName,
320
- startPoint: row.targetStartPoint, endPoint: row.targetEndPoint, sourceHash: row.targetHash,
321
- parser: row.targetParser, parserVersion: row.targetParserVersion, metadataJson: row.targetMetadata,
322
- path: row.targetPath
323
- }),
324
- confidence: Number(row.confidence ?? 0),
325
- metadata: parseMetadata(row.edgeMetadata),
326
- depth: row.depth
327
- }));
328
- }
329
- export function reconcileDeletedFiles(db, repositoryId, discoveredPaths) {
330
- const existing = db.prepare('SELECT path FROM files WHERE repository_id = ?').all(repositoryId);
331
- const keep = new Set(discoveredPaths);
332
- db.exec('BEGIN IMMEDIATE');
333
- try {
334
- for (const row of existing) {
335
- if (!keep.has(row.path))
336
- db.prepare('DELETE FROM files WHERE repository_id = ? AND path = ?').run(repositoryId, row.path);
337
- }
338
- db.exec('COMMIT');
339
- }
340
- catch (error) {
341
- db.exec('ROLLBACK');
342
- throw error;
343
- }
344
- }
345
- export function selectFileInventory(db, repositoryId) {
346
- return db.prepare('SELECT path, size, mtime_ms AS mtimeMs FROM files WHERE repository_id = ? ORDER BY path').all(repositoryId);
347
- }
348
- function contextReferenceFromRow(row) {
349
- return {
350
- id: String(row.id),
351
- path: row.path == null ? null : String(row.path),
352
- kind: row.kind,
353
- type: String(row.type),
354
- name: row.name == null ? null : String(row.name),
355
- span: { start: parsePoint(row.startPoint), end: parsePoint(row.endPoint) },
356
- sourceHash: row.sourceHash == null ? null : String(row.sourceHash),
357
- parser: row.parser == null ? null : String(row.parser),
358
- parserVersion: row.parserVersion == null ? null : String(row.parserVersion),
359
- metadata: parseMetadata(row.metadataJson),
360
- fileId: row.fileId == null ? null : String(row.fileId)
361
- };
362
- }
363
- function parsePoint(value) {
364
- if (typeof value === 'string') {
365
- try {
366
- const point = JSON.parse(value);
367
- return { row: Number(point.row ?? 0), column: Number(point.column ?? 0) };
368
- }
369
- catch { /* fall through */ }
370
- }
371
- return { row: 0, column: 0 };
372
- }
373
- function parseMetadata(value) {
374
- if (typeof value !== 'string')
375
- return {};
376
- try {
377
- const parsed = JSON.parse(value);
378
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
379
- }
380
- catch {
381
- return {};
382
- }
383
- }
384
- export function selectCallGraph(db, repositoryId, term, { kind, depth = 5, limit = 100 } = {}) {
385
- const safeDepth = Math.max(1, Math.min(Number(depth) || 1, 5));
386
- const safeLimit = clampLimit(limit, 500);
387
- const nodes = selectAllNodes(db, repositoryId);
388
- const edges = selectAllEdges(db, repositoryId);
389
- const nodeById = new Map(nodes.map((node) => [node.id, node]));
390
- const needle = term.toLocaleLowerCase();
391
- const matches = nodes.filter((node) => (!kind || node.kind === kind) && node.name?.toLocaleLowerCase().includes(needle));
392
- const walk = (startId, direction) => {
393
- const relations = [];
394
- const seen = new Set([startId]);
395
- let frontier = [startId];
396
- for (let currentDepth = 1; currentDepth <= safeDepth && frontier.length > 0; currentDepth += 1) {
397
- const next = [];
398
- for (const nodeId of frontier) {
399
- for (const edge of edges) {
400
- const adjacentId = direction === 'upstream'
401
- ? edge.targetId === nodeId ? edge.sourceId : null
402
- : edge.sourceId === nodeId ? edge.targetId : null;
403
- if (!adjacentId || seen.has(adjacentId))
404
- continue;
405
- const adjacent = nodeById.get(adjacentId);
406
- if (!adjacent)
407
- continue;
408
- seen.add(adjacentId);
409
- next.push(adjacentId);
410
- relations.push({
411
- id: adjacent.id,
412
- kind: adjacent.kind,
413
- type: adjacent.type,
414
- name: adjacent.name,
415
- viaEdgeId: edge.id,
416
- edgeKind: edge.kind,
417
- depth: currentDepth
418
- });
419
- if (relations.length >= safeLimit)
420
- return relations;
421
- }
422
- }
423
- frontier = next;
424
- }
425
- return relations;
426
- };
427
- return matches.slice(0, safeLimit).map((node) => ({
428
- ...node,
429
- upstream: walk(node.id, 'upstream'),
430
- downstream: walk(node.id, 'downstream')
431
- }));
432
- }
433
- export function selectNeighbors(db, repositoryId, nodeId, { depth = 1, limit = 100 } = {}) {
434
- const safeDepth = Math.max(1, Math.min(Number(depth) || 1, 5));
435
- const safeLimit = clampLimit(limit, 500);
436
- return db.prepare(`
437
- WITH RECURSIVE walk(node_id, via_edge_id, depth) AS (
438
- SELECT ?, NULL, 0
439
- UNION ALL
440
- SELECT
441
- CASE WHEN e.source_id = walk.node_id THEN e.target_id ELSE e.source_id END,
442
- e.id,
443
- walk.depth + 1
444
- FROM walk
445
- JOIN edges e ON e.repository_id = ? AND (e.source_id = walk.node_id OR e.target_id = walk.node_id)
446
- WHERE walk.depth < ?
447
- )
448
- SELECT DISTINCT
449
- n.id, n.kind, n.type, n.name, w.via_edge_id AS viaEdgeId, w.depth
450
- FROM walk w
451
- JOIN nodes n ON n.id = w.node_id
452
- WHERE n.repository_id = ?
453
- ORDER BY w.depth, n.kind, n.name
454
- LIMIT ?
455
- `).all(nodeId, repositoryId, safeDepth, repositoryId, safeLimit);
456
- }
457
- export function runSelect(db, sql, { limit = 200 } = {}) {
458
- const trimmed = sql.trim();
459
- if (!/^(select|with|pragma)\b/i.test(trimmed)) {
460
- throw new Error('Only read-only SELECT, WITH, and PRAGMA queries are allowed');
461
- }
462
- const rows = db.prepare(trimmed).all();
463
- return rows.slice(0, clampLimit(limit, 1000));
464
- }
465
- export function attachDatabase(db, alias, databasePath) {
466
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(alias)) {
467
- throw new Error(`Unsafe SQLite attachment alias: ${alias}`);
468
- }
469
- const escapedPath = databasePath.replaceAll("'", "''");
470
- db.exec(`ATTACH DATABASE '${escapedPath}' AS ${alias}`);
471
- }
472
- function ensureSchemaVersion(db) {
473
- const currentVersion = getCurrentSchemaVersion(db);
474
- if (currentVersion === 0) {
475
- db.prepare('INSERT OR IGNORE INTO schema_migrations(version) VALUES (?)').run(SCHEMA_VERSION);
476
- return;
477
- }
478
- if (currentVersion >= SCHEMA_VERSION)
479
- return;
480
- if (currentVersion === 1) {
481
- migrateSchemaV1ToV2(db);
482
- db.prepare('INSERT OR IGNORE INTO schema_migrations(version) VALUES (?)').run(SCHEMA_VERSION);
483
- return;
484
- }
485
- throw new Error(`Unsupported schema version: ${currentVersion}`);
486
- }
487
- function getCurrentSchemaVersion(db) {
488
- const row = db.prepare('SELECT COALESCE(MAX(version), 0) AS version FROM schema_migrations').get();
489
- return Number(row?.version ?? 0);
490
- }
491
- function migrateSchemaV1ToV2(db) {
492
- db.exec('PRAGMA foreign_keys = OFF;');
493
- db.exec('BEGIN IMMEDIATE');
494
- try {
495
- db.exec(`DROP TABLE IF EXISTS nodes_new;`);
496
- db.exec(CREATE_NODES_TABLE_SQL);
497
- db.exec(`
498
- INSERT INTO nodes_new(
499
- id, repository_id, file_id, kind, type, name, start_byte, end_byte,
500
- start_point, end_point, source_hash, parser, parser_version, metadata_json, last_modified_user_id
501
- )
502
- SELECT
503
- id, repository_id, file_id, kind, type, name, start_byte, end_byte,
504
- start_point, end_point, source_hash, parser, parser_version, metadata_json, NULL
505
- FROM nodes;
506
- `);
507
- db.exec('DROP TABLE nodes;');
508
- db.exec('ALTER TABLE nodes_new RENAME TO nodes;');
509
- db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_repo_kind ON nodes(repository_id, kind);');
510
- db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_repo_name ON nodes(repository_id, name);');
511
- db.exec('COMMIT');
512
- }
513
- catch (error) {
514
- db.exec('ROLLBACK');
515
- throw error;
516
- }
517
- finally {
518
- db.exec('PRAGMA foreign_keys = ON;');
519
- }
520
- }
521
- function upsertContributorNodes(db, repository, contributorEmails) {
522
- const insertNode = db.prepare(`
523
- INSERT INTO nodes(
524
- id, repository_id, file_id, kind, type, name, start_byte, end_byte,
525
- start_point, end_point, source_hash, parser, parser_version, metadata_json, last_modified_user_id
526
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
527
- ON CONFLICT(id) DO UPDATE SET
528
- repository_id = excluded.repository_id,
529
- file_id = excluded.file_id,
530
- kind = excluded.kind,
531
- type = excluded.type,
532
- name = excluded.name,
533
- start_byte = excluded.start_byte,
534
- end_byte = excluded.end_byte,
535
- start_point = excluded.start_point,
536
- end_point = excluded.end_point,
537
- source_hash = excluded.source_hash,
538
- parser = excluded.parser,
539
- parser_version = excluded.parser_version,
540
- metadata_json = excluded.metadata_json,
541
- last_modified_user_id = excluded.last_modified_user_id
542
- `);
543
- for (const contributor of contributorEmails) {
544
- const normalizedEmail = normalizeEmail(contributor.email);
545
- if (!normalizedEmail)
546
- continue;
547
- insertNode.run(resolveUserId(repository, normalizedEmail), repository.id, null, 'user', 'git_user', normalizedEmail, 0, 0, JSON.stringify({ row: 0, column: 0 }), JSON.stringify({ row: 0, column: 0 }), hashParts([repository.id, 'user', normalizedEmail, 'source']), 'git', 'blame-v1', JSON.stringify({ email: normalizedEmail }), null);
548
- }
549
- }
550
- function resolveUserId(repository, email) {
551
- const normalizedEmail = normalizeEmail(email);
552
- if (!normalizedEmail)
553
- return null;
554
- return hashParts([repository.id, 'user', normalizedEmail]);
555
- }
556
- function normalizeEmail(value) {
557
- return value?.trim().replace(/^<|>$/g, '').toLowerCase() ?? '';
558
- }
559
- function clampLimit(limit, max = 500) {
560
- const value = Number(limit) || 50;
561
- return Math.max(1, Math.min(value, max));
562
- }
1
+ /**
2
+ * Compatibility facade for the storage API. Keep consumers importing this module
3
+ * while the implementation stays split into focused modules small enough for
4
+ * Tree-sitter and easier to maintain.
5
+ */
6
+ export * from './connection.js';
7
+ export * from './plugin-contributions.js';
8
+ export * from './graph-writes.js';
9
+ export * from './plugin-graph.js';
10
+ export * from './queries.js';
11
+ export * from './context-queries.js';
12
+ export * from './traversal-queries.js';
563
13
  //# sourceMappingURL=database.js.map
@@ -0,0 +1,17 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import type { FileAttribution, FileRecord, GraphEdge, GraphNode, RepositoryRecord } from '../types.js';
3
+ export declare function upsertRepository(db: DatabaseSync, repository: RepositoryRecord): void;
4
+ export declare function replaceFileGraph(db: DatabaseSync, { repository, file, nodes, edges, attribution }: {
5
+ repository: RepositoryRecord;
6
+ file: FileRecord;
7
+ nodes: GraphNode[];
8
+ edges: GraphEdge[];
9
+ attribution?: FileAttribution | null;
10
+ }): void;
11
+ export declare function reconcileDeletedFiles(db: DatabaseSync, repositoryId: string, discoveredPaths: string[]): void;
12
+ export declare function selectFileInventory(db: DatabaseSync, repositoryId: string): Array<{
13
+ path: string;
14
+ size: number;
15
+ mtimeMs: number;
16
+ }>;
17
+ //# sourceMappingURL=graph-writes.d.ts.map