@psnext/lscg 0.1.1 → 0.1.3

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 +125 -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 +11 -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 +272 -0
  47. package/dist/src/view/templates/interactive.html +642 -0
  48. package/package.json +2 -1
@@ -0,0 +1,133 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { CREATE_NODES_TABLE_SQL, CREATE_SCHEMA_SQL, SCHEMA_VERSION } from './schema.js';
5
+ export function openGraphDatabase(databasePath) {
6
+ mkdirSync(path.dirname(databasePath), { recursive: true });
7
+ const db = new DatabaseSync(databasePath);
8
+ db.exec('PRAGMA foreign_keys = ON;');
9
+ db.exec('PRAGMA journal_mode = WAL;');
10
+ db.exec(CREATE_SCHEMA_SQL);
11
+ ensureSchemaVersion(db);
12
+ return db;
13
+ }
14
+ export function openReadOnlyGraphDatabase(databasePath) {
15
+ const db = new DatabaseSync(databasePath, { readOnly: true });
16
+ db.exec('PRAGMA foreign_keys = ON;');
17
+ return db;
18
+ }
19
+ export function closeDatabase(db) {
20
+ db.close();
21
+ }
22
+ export function attachDatabase(db, alias, databasePath) {
23
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(alias)) {
24
+ throw new Error(`Unsafe SQLite attachment alias: ${alias}`);
25
+ }
26
+ const escapedPath = databasePath.replaceAll("'", "''");
27
+ db.exec(`ATTACH DATABASE '${escapedPath}' AS ${alias}`);
28
+ }
29
+ function ensureSchemaVersion(db) {
30
+ const currentVersion = getCurrentSchemaVersion(db);
31
+ if (currentVersion === 0) {
32
+ db.prepare('INSERT OR IGNORE INTO schema_migrations(version) VALUES (?)').run(SCHEMA_VERSION);
33
+ return;
34
+ }
35
+ if (currentVersion >= SCHEMA_VERSION)
36
+ return;
37
+ if (currentVersion === 1) {
38
+ migrateSchemaV1ToV2(db);
39
+ db.prepare('INSERT OR IGNORE INTO schema_migrations(version) VALUES (?)').run(2);
40
+ }
41
+ if (currentVersion <= 2) {
42
+ migrateSchemaV2ToV3(db);
43
+ db.prepare('INSERT OR IGNORE INTO schema_migrations(version) VALUES (?)').run(3);
44
+ }
45
+ if (currentVersion <= 3) {
46
+ migrateSchemaV3ToV4(db);
47
+ db.prepare('INSERT OR IGNORE INTO schema_migrations(version) VALUES (?)').run(SCHEMA_VERSION);
48
+ return;
49
+ }
50
+ throw new Error(`Unsupported schema version: ${currentVersion}`);
51
+ }
52
+ function migrateSchemaV3ToV4(db) {
53
+ db.exec(`
54
+ CREATE TABLE IF NOT EXISTS plugin_contributions (
55
+ repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
56
+ plugin_name TEXT NOT NULL,
57
+ generation INTEGER NOT NULL,
58
+ nodes_json TEXT NOT NULL,
59
+ edges_json TEXT NOT NULL,
60
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
61
+ PRIMARY KEY(repository_id, plugin_name)
62
+ );
63
+ CREATE INDEX IF NOT EXISTS idx_plugin_contributions_repo ON plugin_contributions(repository_id);
64
+ `);
65
+ }
66
+ function getCurrentSchemaVersion(db) {
67
+ const row = db.prepare('SELECT COALESCE(MAX(version), 0) AS version FROM schema_migrations').get();
68
+ return Number(row?.version ?? 0);
69
+ }
70
+ function migrateSchemaV1ToV2(db) {
71
+ db.exec('PRAGMA foreign_keys = OFF;');
72
+ db.exec('BEGIN IMMEDIATE');
73
+ try {
74
+ db.exec(`DROP TABLE IF EXISTS nodes_new;`);
75
+ db.exec(CREATE_NODES_TABLE_SQL);
76
+ db.exec(`
77
+ INSERT INTO nodes_new(
78
+ id, repository_id, file_id, kind, type, name, start_byte, end_byte,
79
+ start_point, end_point, source_hash, parser, parser_version, metadata_json, last_modified_user_id
80
+ )
81
+ SELECT
82
+ id, repository_id, file_id, kind, type, name, start_byte, end_byte,
83
+ start_point, end_point, source_hash, parser, parser_version, metadata_json, NULL
84
+ FROM nodes;
85
+ `);
86
+ db.exec('DROP TABLE nodes;');
87
+ db.exec('ALTER TABLE nodes_new RENAME TO nodes;');
88
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_repo_kind ON nodes(repository_id, kind);');
89
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_repo_name ON nodes(repository_id, name);');
90
+ db.exec('COMMIT');
91
+ }
92
+ catch (error) {
93
+ db.exec('ROLLBACK');
94
+ throw error;
95
+ }
96
+ finally {
97
+ db.exec('PRAGMA foreign_keys = ON;');
98
+ }
99
+ }
100
+ function migrateSchemaV2ToV3(db) {
101
+ db.exec('PRAGMA foreign_keys = OFF;');
102
+ db.exec('BEGIN IMMEDIATE');
103
+ try {
104
+ db.exec(`
105
+ CREATE TABLE edges_new (
106
+ id TEXT PRIMARY KEY,
107
+ repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
108
+ file_id TEXT REFERENCES files(id) ON DELETE CASCADE,
109
+ source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
110
+ target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
111
+ kind TEXT NOT NULL,
112
+ confidence REAL NOT NULL DEFAULT 1.0,
113
+ metadata_json TEXT NOT NULL DEFAULT '{}'
114
+ );
115
+ INSERT INTO edges_new(id, repository_id, file_id, source_id, target_id, kind, confidence, metadata_json)
116
+ SELECT id, repository_id, file_id, source_id, target_id, kind, confidence, metadata_json FROM edges;
117
+ DROP TABLE edges;
118
+ ALTER TABLE edges_new RENAME TO edges;
119
+ CREATE INDEX IF NOT EXISTS idx_edges_repo_kind ON edges(repository_id, kind);
120
+ CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
121
+ CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
122
+ `);
123
+ db.exec('COMMIT');
124
+ }
125
+ catch (error) {
126
+ db.exec('ROLLBACK');
127
+ throw error;
128
+ }
129
+ finally {
130
+ db.exec('PRAGMA foreign_keys = ON;');
131
+ }
132
+ }
133
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1,20 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import type { ContextCandidate, ContextSourceReference } from '../types.js';
3
+ export interface ContextNodeRow extends ContextSourceReference {
4
+ fileId: string | null;
5
+ }
6
+ export declare function selectContextCandidates(db: DatabaseSync, repositoryId: string, term: string, { kind, file, limit }?: {
7
+ kind?: string | undefined;
8
+ file?: string | undefined;
9
+ limit?: number | undefined;
10
+ }): ContextCandidate[];
11
+ export declare function selectContextRelationships(db: DatabaseSync, repositoryId: string, anchorId: string, limit?: number, maxDepth?: number): Array<{
12
+ edgeId: string;
13
+ edgeKind: 'imports' | 'exports' | 'calls';
14
+ source: ContextNodeRow;
15
+ target: ContextNodeRow;
16
+ confidence: number;
17
+ metadata: Record<string, unknown>;
18
+ depth: number;
19
+ }>;
20
+ //# sourceMappingURL=context-queries.d.ts.map
@@ -0,0 +1,137 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { parseMetadata, parsePoint, contextReferenceFromRow } from './row-decoders.js';
3
+ import { clampLimit } from './queries.js';
4
+ export function selectContextCandidates(db, repositoryId, term, { kind, file, limit = 20 } = {}) {
5
+ const safeLimit = clampLimit(limit, 100);
6
+ const normalized = term.trim().toLocaleLowerCase();
7
+ if (!normalized)
8
+ return [];
9
+ const rows = [];
10
+ const seen = new Set();
11
+ const stages = [
12
+ { match: 'qualified', condition: "(lower(f.path || ':' || COALESCE(n.name, '')) = ? OR lower(f.path || '::' || COALESCE(n.name, '')) = ?)", value: normalized },
13
+ { match: 'exact', condition: 'lower(n.name) = ?', value: normalized },
14
+ { match: 'prefix', condition: 'lower(n.name) LIKE ?', value: `${normalized}%` },
15
+ { match: 'substring', condition: 'lower(n.name) LIKE ?', value: `%${normalized}%` }
16
+ ];
17
+ for (let stage = 0; stage < stages.length; stage += 1) {
18
+ const entry = stages[stage];
19
+ if (!entry)
20
+ continue;
21
+ const predicates = [`n.repository_id = ?`, entry.condition];
22
+ const params = [repositoryId, entry.value, entry.value];
23
+ if (!kind)
24
+ predicates.push("n.kind IN ('symbol', 'file')");
25
+ if (entry.match !== 'qualified')
26
+ params.pop();
27
+ if (kind) {
28
+ predicates.push('n.kind = ?');
29
+ params.push(kind);
30
+ }
31
+ if (file) {
32
+ predicates.push('f.path = ?');
33
+ params.push(file);
34
+ }
35
+ const selected = db.prepare(`
36
+ SELECT n.id, n.kind, n.type, n.name, n.file_id AS fileId,
37
+ n.start_point AS startPoint, n.end_point AS endPoint, n.source_hash AS sourceHash,
38
+ n.parser, n.parser_version AS parserVersion, n.metadata_json AS metadataJson,
39
+ f.path AS path
40
+ FROM nodes n LEFT JOIN files f ON f.id = n.file_id
41
+ WHERE ${predicates.join(' AND ')}
42
+ ORDER BY lower(COALESCE(f.path, '')), n.kind, lower(COALESCE(n.name, '')), n.id
43
+ LIMIT ?
44
+ `).all(...params, safeLimit);
45
+ for (const row of selected) {
46
+ const id = String(row.id);
47
+ if (seen.has(id))
48
+ continue;
49
+ seen.add(id);
50
+ rows.push({
51
+ ...contextReferenceFromRow(row),
52
+ rank: stage,
53
+ match: entry.match,
54
+ stage
55
+ });
56
+ }
57
+ // A unique exact/qualified match is authoritative. Prefix and substring are
58
+ // only consulted when no higher-priority stage produced a result.
59
+ if (rows.length > 0)
60
+ break;
61
+ }
62
+ return rows.map(({ stage: _stage, ...candidate }) => candidate);
63
+ }
64
+ export function selectContextRelationships(db, repositoryId, anchorId, limit = 50, maxDepth = 1) {
65
+ const safeLimit = clampLimit(limit, 500);
66
+ const safeDepth = Math.max(1, Math.min(Number(maxDepth) || 1, 5));
67
+ const rows = [];
68
+ const seenNodes = new Set([anchorId]);
69
+ const seenEdges = new Set();
70
+ let frontier = [anchorId];
71
+ const kinds = ['calls', 'imports', 'exports'];
72
+ for (let depth = 1; depth <= safeDepth && frontier.length > 0; depth += 1) {
73
+ const next = new Set();
74
+ for (const kind of kinds) {
75
+ const remaining = safeLimit - rows.filter((row) => row.edgeKind === kind).length;
76
+ if (remaining <= 0)
77
+ continue;
78
+ const placeholders = frontier.map(() => '?').join(', ');
79
+ const selected = db.prepare(`
80
+ SELECT e.id AS edgeId, e.kind AS edgeKind, e.confidence, e.metadata_json AS edgeMetadata,
81
+ s.id AS sourceId, s.kind AS sourceKind, s.type AS sourceType, s.name AS sourceName,
82
+ s.start_point AS sourceStartPoint, s.end_point AS sourceEndPoint, s.source_hash AS sourceHash,
83
+ s.parser AS sourceParser, s.parser_version AS sourceParserVersion, s.metadata_json AS sourceMetadata,
84
+ sf.path AS sourcePath,
85
+ t.id AS targetId, t.kind AS targetKind, t.type AS targetType, t.name AS targetName,
86
+ t.start_point AS targetStartPoint, t.end_point AS targetEndPoint, t.source_hash AS targetHash,
87
+ t.parser AS targetParser, t.parser_version AS targetParserVersion, t.metadata_json AS targetMetadata,
88
+ tf.path AS targetPath
89
+ FROM edges e
90
+ JOIN nodes s ON s.id = e.source_id AND s.repository_id = e.repository_id
91
+ JOIN nodes t ON t.id = e.target_id AND t.repository_id = e.repository_id
92
+ LEFT JOIN files sf ON sf.id = s.file_id
93
+ LEFT JOIN files tf ON tf.id = t.file_id
94
+ WHERE e.repository_id = ? AND e.kind = ?
95
+ AND (e.source_id IN (${placeholders}) OR e.target_id IN (${placeholders}))
96
+ ORDER BY COALESCE(sf.path, tf.path), e.id
97
+ LIMIT ?
98
+ `).all(repositoryId, kind, ...frontier, ...frontier, remaining);
99
+ for (const row of selected) {
100
+ const edgeId = String(row.edgeId);
101
+ if (seenEdges.has(edgeId))
102
+ continue;
103
+ seenEdges.add(edgeId);
104
+ rows.push({ ...row, depth });
105
+ const sourceId = String(row.sourceId);
106
+ const targetId = String(row.targetId);
107
+ if (seenNodes.has(sourceId))
108
+ next.add(targetId);
109
+ if (seenNodes.has(targetId))
110
+ next.add(sourceId);
111
+ }
112
+ }
113
+ for (const nodeId of next)
114
+ seenNodes.add(nodeId);
115
+ frontier = [...next].filter((nodeId) => nodeId !== anchorId);
116
+ }
117
+ return rows.map((row) => ({
118
+ edgeId: String(row.edgeId),
119
+ edgeKind: row.edgeKind,
120
+ source: contextReferenceFromRow({
121
+ id: row.sourceId, kind: row.sourceKind, type: row.sourceType, name: row.sourceName,
122
+ startPoint: row.sourceStartPoint, endPoint: row.sourceEndPoint, sourceHash: row.sourceHash,
123
+ parser: row.sourceParser, parserVersion: row.sourceParserVersion, metadataJson: row.sourceMetadata,
124
+ path: row.sourcePath
125
+ }),
126
+ target: contextReferenceFromRow({
127
+ id: row.targetId, kind: row.targetKind, type: row.targetType, name: row.targetName,
128
+ startPoint: row.targetStartPoint, endPoint: row.targetEndPoint, sourceHash: row.targetHash,
129
+ parser: row.targetParser, parserVersion: row.targetParserVersion, metadataJson: row.targetMetadata,
130
+ path: row.targetPath
131
+ }),
132
+ confidence: Number(row.confidence ?? 0),
133
+ metadata: parseMetadata(row.edgeMetadata),
134
+ depth: row.depth
135
+ }));
136
+ }
137
+ //# sourceMappingURL=context-queries.js.map
@@ -1,72 +1,13 @@
1
- import { DatabaseSync } from 'node:sqlite';
2
- import type { FileAttribution, FileRecord, GraphEdge, GraphEdgeRow, GraphNode, GraphNodeRow, GraphNodeTextRow, NeighborRow, CallGraphMatch, RepositoryRecord, StatusResult, ContextCandidate, ContextSourceReference } from '../types.js';
3
- export declare function openGraphDatabase(databasePath: string): DatabaseSync;
4
- export declare function openReadOnlyGraphDatabase(databasePath: string): DatabaseSync;
5
- export declare function closeDatabase(db: DatabaseSync): void;
6
- export declare function upsertRepository(db: DatabaseSync, repository: RepositoryRecord): void;
7
- export declare function replaceFileGraph(db: DatabaseSync, { repository, file, nodes, edges, attribution }: {
8
- repository: RepositoryRecord;
9
- file: FileRecord;
10
- nodes: GraphNode[];
11
- edges: GraphEdge[];
12
- attribution?: FileAttribution | null;
13
- }): void;
14
- export declare function getStatus(db: DatabaseSync, repositoryId: string): StatusResult;
15
- export declare function selectNodes(db: DatabaseSync, repositoryId: string, { kind, limit }?: {
16
- kind?: string | undefined;
17
- limit?: number | undefined;
18
- }): GraphNodeRow[];
19
- export declare function selectNodeTextRows(db: DatabaseSync, repositoryId: string, { kind, term, limit }?: {
20
- kind?: string | undefined;
21
- term?: string | undefined;
22
- limit?: number | undefined;
23
- }): GraphNodeTextRow[];
24
- export declare function selectAllNodes(db: DatabaseSync, repositoryId: string, { kind }?: {
25
- kind?: string | undefined;
26
- }): GraphNodeRow[];
27
- export declare function selectEdges(db: DatabaseSync, repositoryId: string, { kind, limit }?: {
28
- kind?: string | undefined;
29
- limit?: number | undefined;
30
- }): GraphEdgeRow[];
31
- export declare function selectAllEdges(db: DatabaseSync, repositoryId: string, { kind }?: {
32
- kind?: string | undefined;
33
- }): GraphEdgeRow[];
34
- export interface ContextNodeRow extends ContextSourceReference {
35
- fileId: string | null;
36
- }
37
- /** Candidate lookup used by context. Stages are queried independently so a unique
38
- * higher-priority match never gets merged with lower-priority fuzzy matches. */
39
- export declare function selectContextCandidates(db: DatabaseSync, repositoryId: string, term: string, { kind, file, limit }?: {
40
- kind?: string | undefined;
41
- file?: string | undefined;
42
- limit?: number | undefined;
43
- }): ContextCandidate[];
44
- export declare function selectContextRelationships(db: DatabaseSync, repositoryId: string, anchorId: string, limit?: number, maxDepth?: number): Array<{
45
- edgeId: string;
46
- edgeKind: 'imports' | 'exports' | 'calls';
47
- source: ContextNodeRow;
48
- target: ContextNodeRow;
49
- confidence: number;
50
- metadata: Record<string, unknown>;
51
- depth: number;
52
- }>;
53
- export declare function reconcileDeletedFiles(db: DatabaseSync, repositoryId: string, discoveredPaths: string[]): void;
54
- export declare function selectFileInventory(db: DatabaseSync, repositoryId: string): Array<{
55
- path: string;
56
- size: number;
57
- mtimeMs: number;
58
- }>;
59
- export declare function selectCallGraph(db: DatabaseSync, repositoryId: string, term: string, { kind, depth, limit }?: {
60
- kind?: string | undefined;
61
- depth?: number | undefined;
62
- limit?: number | undefined;
63
- }): CallGraphMatch[];
64
- export declare function selectNeighbors(db: DatabaseSync, repositoryId: string, nodeId: string, { depth, limit }?: {
65
- depth?: number | undefined;
66
- limit?: number | undefined;
67
- }): NeighborRow[];
68
- export declare function runSelect(db: DatabaseSync, sql: string, { limit }?: {
69
- limit?: number | undefined;
70
- }): Array<Record<string, unknown>>;
71
- export declare function attachDatabase(db: DatabaseSync, alias: string, databasePath: string): void;
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';
72
13
  //# sourceMappingURL=database.d.ts.map