@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
@@ -0,0 +1,126 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { hashParts } from '../graph/extract.js';
3
+ export function upsertRepository(db, repository) {
4
+ db.prepare(`
5
+ INSERT INTO repositories(id, root, name, updated_at)
6
+ VALUES (?, ?, ?, datetime('now'))
7
+ ON CONFLICT(id) DO UPDATE SET
8
+ root = excluded.root,
9
+ name = excluded.name,
10
+ updated_at = datetime('now')
11
+ `).run(repository.id, repository.root, repository.name);
12
+ }
13
+ export function replaceFileGraph(db, { repository, file, nodes, edges, attribution }) {
14
+ upsertRepository(db, repository);
15
+ db.exec('BEGIN IMMEDIATE');
16
+ try {
17
+ if (attribution) {
18
+ upsertContributorNodes(db, repository, attribution.contributorEmails);
19
+ }
20
+ db.prepare('DELETE FROM edges WHERE file_id = ?').run(file.id);
21
+ db.prepare('DELETE FROM nodes WHERE file_id = ?').run(file.id);
22
+ db.prepare(`
23
+ INSERT INTO files(id, repository_id, path, language, hash, size, mtime_ms, scanned_at)
24
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
25
+ ON CONFLICT(repository_id, path) DO UPDATE SET
26
+ id = excluded.id,
27
+ language = excluded.language,
28
+ hash = excluded.hash,
29
+ size = excluded.size,
30
+ mtime_ms = excluded.mtime_ms,
31
+ scanned_at = datetime('now')
32
+ `).run(file.id, repository.id, file.path, file.language, file.hash, file.size, file.mtimeMs);
33
+ const insertNode = db.prepare(`
34
+ INSERT INTO nodes(
35
+ id, repository_id, file_id, kind, type, name, start_byte, end_byte,
36
+ start_point, end_point, source_hash, parser, parser_version, metadata_json, last_modified_user_id
37
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
38
+ `);
39
+ const attributionByNodeId = new Map(attribution?.nodeAttributions.map((entry) => [entry.nodeId, entry]) ?? []);
40
+ for (const node of nodes) {
41
+ const nodeAttribution = attributionByNodeId.get(node.id);
42
+ 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));
43
+ }
44
+ const insertEdge = db.prepare(`
45
+ INSERT INTO edges(
46
+ id, repository_id, file_id, source_id, target_id, kind, confidence, metadata_json
47
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
48
+ `);
49
+ for (const edge of edges) {
50
+ insertEdge.run(edge.id, repository.id, file.id, edge.sourceId, edge.targetId, edge.kind, edge.confidence ?? 1, JSON.stringify(edge.metadata ?? {}));
51
+ }
52
+ if (attribution) {
53
+ for (const entry of attribution.nodeAttributions) {
54
+ for (const email of entry.contributorEmails) {
55
+ const contributorId = resolveUserId(repository, email);
56
+ if (!contributorId)
57
+ continue;
58
+ 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 }));
59
+ }
60
+ }
61
+ }
62
+ db.exec('COMMIT');
63
+ }
64
+ catch (error) {
65
+ db.exec('ROLLBACK');
66
+ throw error;
67
+ }
68
+ }
69
+ export function reconcileDeletedFiles(db, repositoryId, discoveredPaths) {
70
+ const existing = db.prepare('SELECT path FROM files WHERE repository_id = ?').all(repositoryId);
71
+ const keep = new Set(discoveredPaths);
72
+ db.exec('BEGIN IMMEDIATE');
73
+ try {
74
+ for (const row of existing) {
75
+ if (!keep.has(row.path))
76
+ db.prepare('DELETE FROM files WHERE repository_id = ? AND path = ?').run(repositoryId, row.path);
77
+ }
78
+ db.exec('COMMIT');
79
+ }
80
+ catch (error) {
81
+ db.exec('ROLLBACK');
82
+ throw error;
83
+ }
84
+ }
85
+ export function selectFileInventory(db, repositoryId) {
86
+ return db.prepare('SELECT path, size, mtime_ms AS mtimeMs FROM files WHERE repository_id = ? ORDER BY path').all(repositoryId);
87
+ }
88
+ function upsertContributorNodes(db, repository, contributorEmails) {
89
+ const insertNode = db.prepare(`
90
+ INSERT INTO nodes(
91
+ id, repository_id, file_id, kind, type, name, start_byte, end_byte,
92
+ start_point, end_point, source_hash, parser, parser_version, metadata_json, last_modified_user_id
93
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
94
+ ON CONFLICT(id) DO UPDATE SET
95
+ repository_id = excluded.repository_id,
96
+ file_id = excluded.file_id,
97
+ kind = excluded.kind,
98
+ type = excluded.type,
99
+ name = excluded.name,
100
+ start_byte = excluded.start_byte,
101
+ end_byte = excluded.end_byte,
102
+ start_point = excluded.start_point,
103
+ end_point = excluded.end_point,
104
+ source_hash = excluded.source_hash,
105
+ parser = excluded.parser,
106
+ parser_version = excluded.parser_version,
107
+ metadata_json = excluded.metadata_json,
108
+ last_modified_user_id = excluded.last_modified_user_id
109
+ `);
110
+ for (const contributor of contributorEmails) {
111
+ const normalizedEmail = normalizeEmail(contributor.email);
112
+ if (!normalizedEmail)
113
+ continue;
114
+ 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);
115
+ }
116
+ }
117
+ function resolveUserId(repository, email) {
118
+ const normalizedEmail = normalizeEmail(email);
119
+ if (!normalizedEmail)
120
+ return null;
121
+ return hashParts([repository.id, 'user', normalizedEmail]);
122
+ }
123
+ function normalizeEmail(value) {
124
+ return value?.trim().replace(/^<|>$/g, '').toLowerCase() ?? '';
125
+ }
126
+ //# sourceMappingURL=graph-writes.js.map
@@ -0,0 +1,15 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import type { GraphEdge, GraphNode, RepositoryRecord } from '../types.js';
3
+ export interface StoredPluginContribution {
4
+ pluginName: string;
5
+ generation: number;
6
+ nodes: GraphNode[];
7
+ edges: GraphEdge[];
8
+ }
9
+ export declare function loadPluginContributions(db: DatabaseSync, repository: RepositoryRecord, pluginNames: readonly string[]): Map<string, StoredPluginContribution>;
10
+ export declare function persistPluginContributions(db: DatabaseSync, repository: RepositoryRecord, contributions: ReadonlyMap<string, {
11
+ nodes: GraphNode[];
12
+ edges: GraphEdge[];
13
+ }>, generation: number): void;
14
+ export declare function nextPluginGeneration(db: DatabaseSync, repository: RepositoryRecord): number;
15
+ //# sourceMappingURL=plugin-contributions.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ export function loadPluginContributions(db, repository, pluginNames) {
3
+ if (pluginNames.length === 0)
4
+ return new Map();
5
+ const placeholders = pluginNames.map(() => '?').join(', ');
6
+ const rows = db.prepare(`SELECT plugin_name AS pluginName, generation, nodes_json AS nodesJson, edges_json AS edgesJson FROM plugin_contributions WHERE repository_id = ? AND plugin_name IN (${placeholders})`).all(repository.id, ...pluginNames);
7
+ return new Map(rows.map((row) => [row.pluginName, { pluginName: row.pluginName, generation: row.generation, nodes: JSON.parse(row.nodesJson), edges: JSON.parse(row.edgesJson) }]));
8
+ }
9
+ export function persistPluginContributions(db, repository, contributions, generation) {
10
+ if (contributions.size === 0)
11
+ return;
12
+ db.exec('BEGIN IMMEDIATE');
13
+ try {
14
+ const upsert = db.prepare(`INSERT INTO plugin_contributions(repository_id, plugin_name, generation, nodes_json, edges_json) VALUES (?, ?, ?, ?, ?) ON CONFLICT(repository_id, plugin_name) DO UPDATE SET generation = excluded.generation, nodes_json = excluded.nodes_json, edges_json = excluded.edges_json, updated_at = datetime('now')`);
15
+ for (const [pluginName, contribution] of contributions)
16
+ upsert.run(repository.id, pluginName, generation, JSON.stringify(contribution.nodes), JSON.stringify(contribution.edges));
17
+ db.exec('COMMIT');
18
+ }
19
+ catch (error) {
20
+ db.exec('ROLLBACK');
21
+ throw error;
22
+ }
23
+ }
24
+ export function nextPluginGeneration(db, repository) {
25
+ const row = db.prepare('SELECT COALESCE(MAX(generation), 0) AS generation FROM plugin_contributions WHERE repository_id = ?').get(repository.id);
26
+ return row.generation + 1;
27
+ }
28
+ //# sourceMappingURL=plugin-contributions.js.map
@@ -0,0 +1,6 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import type { GraphEdge, GraphNode, RepositoryRecord } from '../types.js';
3
+ export declare function reconcilePluginEdges(db: DatabaseSync, repository: RepositoryRecord, edges: GraphEdge[], fileIdByPath: Map<string, string>, activePlugins: readonly string[]): number;
4
+ export declare function reconcileRepositoryPluginNodes(db: DatabaseSync, repository: RepositoryRecord, activePlugins: readonly string[]): void;
5
+ export declare function insertRepositoryPluginNodes(db: DatabaseSync, repository: RepositoryRecord, nodes: GraphNode[]): void;
6
+ //# sourceMappingURL=plugin-graph.d.ts.map
@@ -0,0 +1,81 @@
1
+ import path from 'node:path';
2
+ import { DatabaseSync } from 'node:sqlite';
3
+ export function reconcilePluginEdges(db, repository, edges, fileIdByPath, activePlugins) {
4
+ if (activePlugins.length === 0)
5
+ return 0;
6
+ let inserted = 0;
7
+ db.exec('BEGIN IMMEDIATE');
8
+ try {
9
+ for (const plugin of activePlugins) {
10
+ db.prepare(`
11
+ DELETE FROM edges
12
+ WHERE repository_id = ? AND json_extract(metadata_json, '$.plugin') = ?
13
+ `).run(repository.id, plugin);
14
+ }
15
+ const insert = db.prepare(`
16
+ INSERT OR REPLACE INTO edges(id, repository_id, file_id, source_id, target_id, kind, confidence, metadata_json)
17
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
18
+ `);
19
+ for (const edge of edges) {
20
+ const plugin = typeof edge.metadata.plugin === 'string' ? edge.metadata.plugin : undefined;
21
+ if (!plugin || !activePlugins.includes(plugin))
22
+ continue;
23
+ const filePath = typeof edge.metadata.filePath === 'string' ? edge.metadata.filePath : undefined;
24
+ const normalizedPath = filePath ? normalizePluginFilePath(filePath) : undefined;
25
+ const fileId = normalizedPath ? fileIdByPath.get(normalizedPath) : null;
26
+ if (filePath && !fileId)
27
+ continue;
28
+ const endpoints = db.prepare('SELECT count(*) AS count FROM nodes WHERE repository_id = ? AND id IN (?, ?)').get(repository.id, edge.sourceId, edge.targetId);
29
+ const requiredEndpoints = edge.sourceId === edge.targetId ? 1 : 2;
30
+ if (endpoints.count !== requiredEndpoints)
31
+ continue;
32
+ insert.run(edge.id, repository.id, fileId, edge.sourceId, edge.targetId, edge.kind, edge.confidence ?? 1, JSON.stringify(edge.metadata ?? {}));
33
+ inserted += 1;
34
+ }
35
+ db.exec('COMMIT');
36
+ return inserted;
37
+ }
38
+ catch (error) {
39
+ db.exec('ROLLBACK');
40
+ throw error;
41
+ }
42
+ }
43
+ export function reconcileRepositoryPluginNodes(db, repository, activePlugins) {
44
+ if (activePlugins.length === 0)
45
+ return;
46
+ const placeholders = activePlugins.map(() => '?').join(', ');
47
+ db.prepare(`
48
+ DELETE FROM nodes
49
+ WHERE repository_id = ? AND file_id IS NULL
50
+ AND json_extract(metadata_json, '$.plugin') IN (${placeholders})
51
+ `).run(repository.id, ...activePlugins);
52
+ }
53
+ export function insertRepositoryPluginNodes(db, repository, nodes) {
54
+ const repositoryNodes = nodes.filter((node) => typeof node.metadata.filePath !== 'string');
55
+ if (repositoryNodes.length === 0)
56
+ return;
57
+ db.exec('BEGIN IMMEDIATE');
58
+ try {
59
+ const insert = db.prepare(`
60
+ INSERT OR REPLACE INTO nodes(
61
+ id, repository_id, file_id, kind, type, name, start_byte, end_byte,
62
+ start_point, end_point, source_hash, parser, parser_version, metadata_json, last_modified_user_id
63
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
64
+ `);
65
+ for (const node of repositoryNodes) {
66
+ insert.run(node.id, repository.id, node.kind, node.type, node.name, node.startByte, node.endByte, JSON.stringify(node.startPoint), JSON.stringify(node.endPoint), node.sourceHash, node.parser, node.parserVersion, JSON.stringify(node.metadata ?? {}), node.lastModifiedUserId);
67
+ }
68
+ db.exec('COMMIT');
69
+ }
70
+ catch (error) {
71
+ db.exec('ROLLBACK');
72
+ throw error;
73
+ }
74
+ }
75
+ function normalizePluginFilePath(value) {
76
+ const normalized = path.posix.normalize(value.replaceAll('\\', '/'));
77
+ if (path.posix.isAbsolute(normalized) || normalized === '..' || normalized.startsWith('../'))
78
+ return undefined;
79
+ return normalized.replace(/^\.\//, '');
80
+ }
81
+ //# sourceMappingURL=plugin-graph.js.map
@@ -0,0 +1,25 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import type { GraphEdgeRow, GraphNodeRow, GraphNodeTextRow, StatusResult } from '../types.js';
3
+ export declare function getStatus(db: DatabaseSync, repositoryId: string): StatusResult;
4
+ export declare function selectNodes(db: DatabaseSync, repositoryId: string, { kind, limit }?: {
5
+ kind?: string | undefined;
6
+ limit?: number | undefined;
7
+ }): GraphNodeRow[];
8
+ export declare function selectNodeTextRows(db: DatabaseSync, repositoryId: string, { kind, term, limit }?: {
9
+ kind?: string | undefined;
10
+ term?: string | undefined;
11
+ limit?: number | undefined;
12
+ }): GraphNodeTextRow[];
13
+ export declare function selectAllNodeTextRows(db: DatabaseSync, repositoryId: string): GraphNodeTextRow[];
14
+ export declare function selectAllNodes(db: DatabaseSync, repositoryId: string, { kind }?: {
15
+ kind?: string | undefined;
16
+ }): GraphNodeRow[];
17
+ export declare function selectEdges(db: DatabaseSync, repositoryId: string, { kind, limit }?: {
18
+ kind?: string | undefined;
19
+ limit?: number | undefined;
20
+ }): GraphEdgeRow[];
21
+ export declare function selectAllEdges(db: DatabaseSync, repositoryId: string, { kind }?: {
22
+ kind?: string | undefined;
23
+ }): GraphEdgeRow[];
24
+ export declare function clampLimit(limit: number | undefined, max?: number): number;
25
+ //# sourceMappingURL=queries.d.ts.map
@@ -0,0 +1,129 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { parsePoint } from './row-decoders.js';
3
+ export function getStatus(db, repositoryId) {
4
+ const repository = db.prepare('SELECT * FROM repositories WHERE id = ?').get(repositoryId);
5
+ const files = db.prepare('SELECT count(*) AS count FROM files WHERE repository_id = ?').get(repositoryId).count;
6
+ const nodes = db.prepare('SELECT count(*) AS count FROM nodes WHERE repository_id = ?').get(repositoryId).count;
7
+ const edges = db.prepare('SELECT count(*) AS count FROM edges WHERE repository_id = ?').get(repositoryId).count;
8
+ return { repository: repository ?? null, files, nodes, edges };
9
+ }
10
+ export function selectNodes(db, repositoryId, { kind, limit = 50 } = {}) {
11
+ const safeLimit = clampLimit(limit);
12
+ if (kind) {
13
+ return db.prepare(`
14
+ 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
15
+ FROM nodes
16
+ WHERE repository_id = ? AND kind = ?
17
+ ORDER BY name IS NULL, name, id
18
+ LIMIT ?
19
+ `).all(repositoryId, kind, safeLimit);
20
+ }
21
+ return db.prepare(`
22
+ 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
23
+ FROM nodes
24
+ WHERE repository_id = ?
25
+ ORDER BY kind, name IS NULL, name, id
26
+ LIMIT ?
27
+ `).all(repositoryId, safeLimit);
28
+ }
29
+ export function selectNodeTextRows(db, repositoryId, { kind, term, limit = 50 } = {}) {
30
+ const predicates = ['n.repository_id = ?'];
31
+ const params = [repositoryId];
32
+ if (kind) {
33
+ predicates.push('n.kind = ?');
34
+ params.push(kind);
35
+ }
36
+ if (term) {
37
+ predicates.push('lower(COALESCE(n.name, \'\')) LIKE ?');
38
+ params.push(`%${term.toLocaleLowerCase()}%`);
39
+ }
40
+ const rows = db.prepare(`
41
+ SELECT n.id, n.kind, n.type, n.name, n.file_id AS fileId,
42
+ n.start_byte AS startByte, n.end_byte AS endByte,
43
+ n.start_point AS startPoint, n.end_point AS endPoint,
44
+ n.metadata_json AS metadataJson, n.last_modified_user_id AS lastModifiedUserId,
45
+ f.path AS path
46
+ FROM nodes n LEFT JOIN files f ON f.id = n.file_id
47
+ WHERE ${predicates.join(' AND ')}
48
+ ORDER BY n.kind, n.name IS NULL, n.name, n.id
49
+ LIMIT ?
50
+ `).all(...params, clampLimit(limit));
51
+ return rows.map((row) => ({
52
+ ...row,
53
+ startPoint: parsePoint(row.startPoint),
54
+ endPoint: parsePoint(row.endPoint)
55
+ }));
56
+ }
57
+ export function selectAllNodeTextRows(db, repositoryId) {
58
+ const rows = db.prepare(`
59
+ SELECT n.id, n.kind, n.type, n.name, n.file_id AS fileId,
60
+ n.start_byte AS startByte, n.end_byte AS endByte,
61
+ n.start_point AS startPoint, n.end_point AS endPoint,
62
+ n.metadata_json AS metadataJson, n.last_modified_user_id AS lastModifiedUserId,
63
+ f.path AS path
64
+ FROM nodes n LEFT JOIN files f ON f.id = n.file_id
65
+ WHERE n.repository_id = ?
66
+ ORDER BY n.kind, n.name IS NULL, n.name, n.id
67
+ `).all(repositoryId);
68
+ return rows.map((row) => ({
69
+ ...row,
70
+ startPoint: parsePoint(row.startPoint),
71
+ endPoint: parsePoint(row.endPoint)
72
+ }));
73
+ }
74
+ export function selectAllNodes(db, repositoryId, { kind } = {}) {
75
+ if (kind) {
76
+ return db.prepare(`
77
+ 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
78
+ FROM nodes
79
+ WHERE repository_id = ? AND kind = ?
80
+ ORDER BY name IS NULL, name, id
81
+ `).all(repositoryId, kind);
82
+ }
83
+ return db.prepare(`
84
+ 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
85
+ FROM nodes
86
+ WHERE repository_id = ?
87
+ ORDER BY kind, name IS NULL, name, id
88
+ `).all(repositoryId);
89
+ }
90
+ export function selectEdges(db, repositoryId, { kind, limit = 50 } = {}) {
91
+ const safeLimit = clampLimit(limit);
92
+ if (kind) {
93
+ return db.prepare(`
94
+ SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
95
+ FROM edges
96
+ WHERE repository_id = ? AND kind = ?
97
+ ORDER BY kind, id
98
+ LIMIT ?
99
+ `).all(repositoryId, kind, safeLimit);
100
+ }
101
+ return db.prepare(`
102
+ SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
103
+ FROM edges
104
+ WHERE repository_id = ?
105
+ ORDER BY kind, id
106
+ LIMIT ?
107
+ `).all(repositoryId, safeLimit);
108
+ }
109
+ export function selectAllEdges(db, repositoryId, { kind } = {}) {
110
+ if (kind) {
111
+ return db.prepare(`
112
+ SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
113
+ FROM edges
114
+ WHERE repository_id = ? AND kind = ?
115
+ ORDER BY kind, id
116
+ `).all(repositoryId, kind);
117
+ }
118
+ return db.prepare(`
119
+ SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
120
+ FROM edges
121
+ WHERE repository_id = ?
122
+ ORDER BY kind, id
123
+ `).all(repositoryId);
124
+ }
125
+ export function clampLimit(limit, max = 500) {
126
+ const value = Number(limit) || 50;
127
+ return Math.max(1, Math.min(value, max));
128
+ }
129
+ //# sourceMappingURL=queries.js.map
@@ -0,0 +1,8 @@
1
+ import type { ContextNodeRow } from './context-queries.js';
2
+ export declare function contextReferenceFromRow(row: Record<string, unknown>): ContextNodeRow;
3
+ export declare function parsePoint(value: unknown): {
4
+ row: number;
5
+ column: number;
6
+ };
7
+ export declare function parseMetadata(value: unknown): Record<string, unknown>;
8
+ //# sourceMappingURL=row-decoders.d.ts.map
@@ -0,0 +1,37 @@
1
+ export function contextReferenceFromRow(row) {
2
+ return {
3
+ id: String(row.id),
4
+ path: row.path == null ? null : String(row.path),
5
+ kind: row.kind,
6
+ type: String(row.type),
7
+ name: row.name == null ? null : String(row.name),
8
+ span: { start: parsePoint(row.startPoint), end: parsePoint(row.endPoint) },
9
+ sourceHash: row.sourceHash == null ? null : String(row.sourceHash),
10
+ parser: row.parser == null ? null : String(row.parser),
11
+ parserVersion: row.parserVersion == null ? null : String(row.parserVersion),
12
+ metadata: parseMetadata(row.metadataJson),
13
+ fileId: row.fileId == null ? null : String(row.fileId)
14
+ };
15
+ }
16
+ export function parsePoint(value) {
17
+ if (typeof value === 'string') {
18
+ try {
19
+ const point = JSON.parse(value);
20
+ return { row: Number(point.row ?? 0), column: Number(point.column ?? 0) };
21
+ }
22
+ catch { /* fall through */ }
23
+ }
24
+ return { row: 0, column: 0 };
25
+ }
26
+ export function parseMetadata(value) {
27
+ if (typeof value !== 'string')
28
+ return {};
29
+ try {
30
+ const parsed = JSON.parse(value);
31
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
32
+ }
33
+ catch {
34
+ return {};
35
+ }
36
+ }
37
+ //# sourceMappingURL=row-decoders.js.map
@@ -1,4 +1,4 @@
1
- export declare const SCHEMA_VERSION = 2;
2
- export declare const CREATE_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS schema_migrations (\n version INTEGER PRIMARY KEY,\n applied_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS repositories (\n id TEXT PRIMARY KEY,\n root TEXT NOT NULL,\n name TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS files (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n language TEXT NOT NULL,\n hash TEXT NOT NULL,\n size INTEGER NOT NULL,\n mtime_ms INTEGER NOT NULL,\n scanned_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(repository_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n name TEXT,\n start_byte INTEGER NOT NULL,\n end_byte INTEGER NOT NULL,\n start_point TEXT NOT NULL,\n end_point TEXT NOT NULL,\n source_hash TEXT NOT NULL,\n parser TEXT NOT NULL,\n parser_version TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n last_modified_user_id TEXT\n);\n\nCREATE TABLE IF NOT EXISTS edges (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n metadata_json TEXT NOT NULL DEFAULT '{}'\n);\n\nCREATE TABLE IF NOT EXISTS overlays (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n subject_id TEXT NOT NULL,\n subject_kind TEXT NOT NULL,\n operation TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_files_repo_path ON files(repository_id, path);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_kind ON nodes(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_name ON nodes(repository_id, name);\nCREATE INDEX IF NOT EXISTS idx_edges_repo_kind ON edges(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);\nCREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);\n";
1
+ export declare const SCHEMA_VERSION = 4;
2
+ export declare const CREATE_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS schema_migrations (\n version INTEGER PRIMARY KEY,\n applied_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS repositories (\n id TEXT PRIMARY KEY,\n root TEXT NOT NULL,\n name TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS files (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n language TEXT NOT NULL,\n hash TEXT NOT NULL,\n size INTEGER NOT NULL,\n mtime_ms INTEGER NOT NULL,\n scanned_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(repository_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n name TEXT,\n start_byte INTEGER NOT NULL,\n end_byte INTEGER NOT NULL,\n start_point TEXT NOT NULL,\n end_point TEXT NOT NULL,\n source_hash TEXT NOT NULL,\n parser TEXT NOT NULL,\n parser_version TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n last_modified_user_id TEXT\n);\n\nCREATE TABLE IF NOT EXISTS edges (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n metadata_json TEXT NOT NULL DEFAULT '{}'\n);\n\nCREATE TABLE IF NOT EXISTS overlays (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n subject_id TEXT NOT NULL,\n subject_kind TEXT NOT NULL,\n operation TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_files_repo_path ON files(repository_id, path);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_kind ON nodes(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_name ON nodes(repository_id, name);\nCREATE INDEX IF NOT EXISTS idx_edges_repo_kind ON edges(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);\nCREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);\n\nCREATE TABLE IF NOT EXISTS plugin_contributions (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n plugin_name TEXT NOT NULL,\n generation INTEGER NOT NULL,\n nodes_json TEXT NOT NULL,\n edges_json TEXT NOT NULL,\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, plugin_name)\n);\nCREATE INDEX IF NOT EXISTS idx_plugin_contributions_repo ON plugin_contributions(repository_id);\n";
3
3
  export declare const CREATE_NODES_TABLE_SQL = "\nCREATE TABLE nodes_new (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n name TEXT,\n start_byte INTEGER NOT NULL,\n end_byte INTEGER NOT NULL,\n start_point TEXT NOT NULL,\n end_point TEXT NOT NULL,\n source_hash TEXT NOT NULL,\n parser TEXT NOT NULL,\n parser_version TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n last_modified_user_id TEXT\n);\n";
4
4
  //# sourceMappingURL=schema.d.ts.map
@@ -1,4 +1,4 @@
1
- export const SCHEMA_VERSION = 2;
1
+ export const SCHEMA_VERSION = 4;
2
2
  export const CREATE_SCHEMA_SQL = `
3
3
  CREATE TABLE IF NOT EXISTS schema_migrations (
4
4
  version INTEGER PRIMARY KEY,
@@ -46,7 +46,7 @@ CREATE TABLE IF NOT EXISTS nodes (
46
46
  CREATE TABLE IF NOT EXISTS edges (
47
47
  id TEXT PRIMARY KEY,
48
48
  repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
49
- file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
49
+ file_id TEXT REFERENCES files(id) ON DELETE CASCADE,
50
50
  source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
51
51
  target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
52
52
  kind TEXT NOT NULL,
@@ -70,6 +70,17 @@ CREATE INDEX IF NOT EXISTS idx_nodes_repo_name ON nodes(repository_id, name);
70
70
  CREATE INDEX IF NOT EXISTS idx_edges_repo_kind ON edges(repository_id, kind);
71
71
  CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
72
72
  CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
73
+
74
+ CREATE TABLE IF NOT EXISTS plugin_contributions (
75
+ repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
76
+ plugin_name TEXT NOT NULL,
77
+ generation INTEGER NOT NULL,
78
+ nodes_json TEXT NOT NULL,
79
+ edges_json TEXT NOT NULL,
80
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
81
+ PRIMARY KEY(repository_id, plugin_name)
82
+ );
83
+ CREATE INDEX IF NOT EXISTS idx_plugin_contributions_repo ON plugin_contributions(repository_id);
73
84
  `;
74
85
  export const CREATE_NODES_TABLE_SQL = `
75
86
  CREATE TABLE nodes_new (
@@ -0,0 +1,15 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import type { CallGraphMatch, NeighborRow } from '../types.js';
3
+ export declare function selectCallGraph(db: DatabaseSync, repositoryId: string, term: string, { kind, depth, limit }?: {
4
+ kind?: string | undefined;
5
+ depth?: number | undefined;
6
+ limit?: number | undefined;
7
+ }): CallGraphMatch[];
8
+ export declare function selectNeighbors(db: DatabaseSync, repositoryId: string, nodeId: string, { depth, limit }?: {
9
+ depth?: number | undefined;
10
+ limit?: number | undefined;
11
+ }): NeighborRow[];
12
+ export declare function runSelect(db: DatabaseSync, sql: string, { limit }?: {
13
+ limit?: number | undefined;
14
+ }): Array<Record<string, unknown>>;
15
+ //# sourceMappingURL=traversal-queries.d.ts.map
@@ -0,0 +1,87 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { selectAllEdges, selectAllNodeTextRows, clampLimit } from './queries.js';
3
+ export function selectCallGraph(db, repositoryId, term, { kind, depth = 5, limit = 100 } = {}) {
4
+ const safeDepth = Math.max(1, Math.min(Number(depth) || 1, 5));
5
+ const safeLimit = clampLimit(limit, 500);
6
+ const nodes = selectAllNodeTextRows(db, repositoryId);
7
+ const edges = selectAllEdges(db, repositoryId);
8
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
9
+ const needle = term.toLocaleLowerCase();
10
+ const matches = nodes.filter((node) => (!kind || node.kind === kind) && node.name?.toLocaleLowerCase().includes(needle));
11
+ const walk = (startId, direction) => {
12
+ const relations = [];
13
+ const seen = new Set([startId]);
14
+ let frontier = [startId];
15
+ for (let currentDepth = 1; currentDepth <= safeDepth && frontier.length > 0; currentDepth += 1) {
16
+ const next = [];
17
+ for (const nodeId of frontier) {
18
+ for (const edge of edges) {
19
+ const adjacentId = direction === 'upstream'
20
+ ? edge.targetId === nodeId ? edge.sourceId : null
21
+ : edge.sourceId === nodeId ? edge.targetId : null;
22
+ if (!adjacentId || seen.has(adjacentId))
23
+ continue;
24
+ const adjacent = nodeById.get(adjacentId);
25
+ if (!adjacent)
26
+ continue;
27
+ seen.add(adjacentId);
28
+ next.push(adjacentId);
29
+ relations.push({
30
+ id: adjacent.id,
31
+ kind: adjacent.kind,
32
+ type: adjacent.type,
33
+ name: adjacent.name,
34
+ path: adjacent.path,
35
+ startPoint: adjacent.startPoint,
36
+ parentId: nodeId,
37
+ viaEdgeId: edge.id,
38
+ edgeKind: edge.kind,
39
+ depth: currentDepth
40
+ });
41
+ if (relations.length >= safeLimit)
42
+ return relations;
43
+ }
44
+ }
45
+ frontier = next;
46
+ }
47
+ return relations;
48
+ };
49
+ return matches.slice(0, safeLimit).map((node) => ({
50
+ ...node,
51
+ upstream: walk(node.id, 'upstream'),
52
+ downstream: walk(node.id, 'downstream')
53
+ }));
54
+ }
55
+ export function selectNeighbors(db, repositoryId, nodeId, { depth = 1, limit = 100 } = {}) {
56
+ const safeDepth = Math.max(1, Math.min(Number(depth) || 1, 5));
57
+ const safeLimit = clampLimit(limit, 500);
58
+ return db.prepare(`
59
+ WITH RECURSIVE walk(node_id, via_edge_id, depth) AS (
60
+ SELECT ?, NULL, 0
61
+ UNION ALL
62
+ SELECT
63
+ CASE WHEN e.source_id = walk.node_id THEN e.target_id ELSE e.source_id END,
64
+ e.id,
65
+ walk.depth + 1
66
+ FROM walk
67
+ JOIN edges e ON e.repository_id = ? AND (e.source_id = walk.node_id OR e.target_id = walk.node_id)
68
+ WHERE walk.depth < ?
69
+ )
70
+ SELECT DISTINCT
71
+ n.id, n.kind, n.type, n.name, w.via_edge_id AS viaEdgeId, w.depth
72
+ FROM walk w
73
+ JOIN nodes n ON n.id = w.node_id
74
+ WHERE n.repository_id = ?
75
+ ORDER BY w.depth, n.kind, n.name
76
+ LIMIT ?
77
+ `).all(nodeId, repositoryId, safeDepth, repositoryId, safeLimit);
78
+ }
79
+ export function runSelect(db, sql, { limit = 200 } = {}) {
80
+ const trimmed = sql.trim();
81
+ if (!/^(select|with|pragma)\b/i.test(trimmed)) {
82
+ throw new Error('Only read-only SELECT, WITH, and PRAGMA queries are allowed');
83
+ }
84
+ const rows = db.prepare(trimmed).all();
85
+ return rows.slice(0, clampLimit(limit, 1000));
86
+ }
87
+ //# sourceMappingURL=traversal-queries.js.map
@@ -1,8 +1,8 @@
1
1
  import type Parser from 'tree-sitter';
2
2
  export type GraphScope = 'repo' | 'home' | 'both';
3
3
  export type StorageScope = Exclude<GraphScope, 'both'>;
4
- export type GraphNodeKind = 'file' | 'symbol' | 'import' | 'export' | 'call' | 'user';
5
- export type GraphEdgeKind = 'contains' | 'defines' | 'imports' | 'exports' | 'calls' | 'attributed_to';
4
+ export type GraphNodeKind = 'file' | 'symbol' | 'import' | 'export' | 'call' | 'user' | 'package';
5
+ export type GraphEdgeKind = 'contains' | 'defines' | 'imports' | 'exports' | 'calls' | 'attributed_to' | 'provides';
6
6
  export interface Point {
7
7
  row: number;
8
8
  column: number;
@@ -84,6 +84,9 @@ export interface ScanSummary {
84
84
  nodesWritten: number;
85
85
  edgesWritten: number;
86
86
  skipped: string[];
87
+ pluginDiagnostics?: string[];
88
+ failedPlugins?: string[];
89
+ stalePlugins?: string[];
87
90
  }
88
91
  export interface RepositoryInfo {
89
92
  id: string;
@@ -111,7 +114,7 @@ export interface GraphEdgeRow {
111
114
  kind: GraphEdgeKind;
112
115
  sourceId: string;
113
116
  targetId: string;
114
- fileId: string;
117
+ fileId: string | null;
115
118
  confidence: number;
116
119
  metadataJson: string;
117
120
  }
@@ -128,11 +131,14 @@ export interface CallGraphRelation {
128
131
  kind: GraphNodeKind;
129
132
  type: string;
130
133
  name: string | null;
134
+ path: string | null;
135
+ startPoint: Point;
136
+ parentId: string;
131
137
  viaEdgeId: string;
132
138
  edgeKind: GraphEdgeKind;
133
139
  depth: number;
134
140
  }
135
- export interface CallGraphMatch extends GraphNodeRow {
141
+ export interface CallGraphMatch extends GraphNodeTextRow {
136
142
  upstream: CallGraphRelation[];
137
143
  downstream: CallGraphRelation[];
138
144
  }