@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.
- package/README.md +69 -3
- package/dist/src/cli.js +68 -13
- package/dist/src/graph/repository.d.ts +4 -1
- package/dist/src/graph/repository.js +106 -6
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +2 -0
- package/dist/src/mcp/server.js +2 -2
- package/dist/src/scanner/discover.js +0 -2
- package/dist/src/scanner/packagePlugin.d.ts +3 -0
- package/dist/src/scanner/packagePlugin.js +168 -0
- package/dist/src/scanner/plugins.d.ts +86 -0
- package/dist/src/scanner/plugins.js +503 -0
- package/dist/src/storage/connection.d.ts +6 -0
- package/dist/src/storage/connection.js +133 -0
- package/dist/src/storage/context-queries.d.ts +20 -0
- package/dist/src/storage/context-queries.js +137 -0
- package/dist/src/storage/database.d.ts +12 -71
- package/dist/src/storage/database.js +12 -562
- package/dist/src/storage/graph-writes.d.ts +17 -0
- package/dist/src/storage/graph-writes.js +126 -0
- package/dist/src/storage/plugin-contributions.d.ts +15 -0
- package/dist/src/storage/plugin-contributions.js +28 -0
- package/dist/src/storage/plugin-graph.d.ts +6 -0
- package/dist/src/storage/plugin-graph.js +81 -0
- package/dist/src/storage/queries.d.ts +25 -0
- package/dist/src/storage/queries.js +129 -0
- package/dist/src/storage/row-decoders.d.ts +8 -0
- package/dist/src/storage/row-decoders.js +37 -0
- package/dist/src/storage/schema.d.ts +2 -2
- package/dist/src/storage/schema.js +13 -2
- package/dist/src/storage/traversal-queries.d.ts +15 -0
- package/dist/src/storage/traversal-queries.js +87 -0
- package/dist/src/types.d.ts +10 -4
- package/dist/src/view/index.d.ts +1 -1
- package/dist/src/view/index.js +2 -2
- package/dist/src/view/model.d.ts +2 -0
- package/dist/src/view/render.d.ts +5 -2
- package/dist/src/view/render.js +81 -13
- package/dist/src/view/templates/icons/call.svg +13 -0
- package/dist/src/view/templates/icons/export.svg +1 -0
- package/dist/src/view/templates/icons/file.svg +9 -0
- package/dist/src/view/templates/icons/import.svg +1 -0
- package/dist/src/view/templates/icons/package.svg +1 -0
- package/dist/src/view/templates/icons/symbol.svg +7 -0
- package/dist/src/view/templates/icons/user.svg +15 -0
- package/dist/src/view/templates/interactive.css +17 -11
- package/dist/src/view/templates/interactive.html +236 -52
- 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
|
-
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
|
|
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 =
|
|
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
|
|
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
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
|
|
141
|
+
export interface CallGraphMatch extends GraphNodeTextRow {
|
|
136
142
|
upstream: CallGraphRelation[];
|
|
137
143
|
downstream: CallGraphRelation[];
|
|
138
144
|
}
|