lattice-graph 0.1.0
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/LICENSE +21 -0
- package/README.md +391 -0
- package/package.json +56 -0
- package/src/commands/build.ts +208 -0
- package/src/commands/init.ts +111 -0
- package/src/commands/lint.ts +245 -0
- package/src/commands/populate.ts +224 -0
- package/src/commands/update.ts +175 -0
- package/src/config.ts +93 -0
- package/src/extract/extractor.ts +13 -0
- package/src/extract/parser.ts +117 -0
- package/src/extract/python/calls.ts +121 -0
- package/src/extract/python/extractor.ts +171 -0
- package/src/extract/python/frameworks.ts +142 -0
- package/src/extract/python/imports.ts +115 -0
- package/src/extract/python/symbols.ts +121 -0
- package/src/extract/tags.ts +77 -0
- package/src/extract/typescript/calls.ts +110 -0
- package/src/extract/typescript/extractor.ts +130 -0
- package/src/extract/typescript/imports.ts +71 -0
- package/src/extract/typescript/symbols.ts +252 -0
- package/src/graph/database.ts +95 -0
- package/src/graph/queries.ts +336 -0
- package/src/graph/writer.ts +147 -0
- package/src/main.ts +525 -0
- package/src/output/json.ts +79 -0
- package/src/output/text.ts +265 -0
- package/src/types/config.ts +32 -0
- package/src/types/graph.ts +87 -0
- package/src/types/lint.ts +21 -0
- package/src/types/result.ts +58 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { Node } from "../types/graph.ts";
|
|
3
|
+
|
|
4
|
+
/** Raw node row from SQLite with snake_case column names. */
|
|
5
|
+
type NodeRow = {
|
|
6
|
+
id: string;
|
|
7
|
+
kind: string;
|
|
8
|
+
name: string;
|
|
9
|
+
file: string;
|
|
10
|
+
line_start: number;
|
|
11
|
+
line_end: number;
|
|
12
|
+
language: string;
|
|
13
|
+
signature: string | null;
|
|
14
|
+
is_test: number;
|
|
15
|
+
metadata: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Converts a raw SQLite row to a typed Node. */
|
|
19
|
+
function rowToNode(row: NodeRow): Node {
|
|
20
|
+
return {
|
|
21
|
+
id: row.id,
|
|
22
|
+
kind: row.kind as Node["kind"],
|
|
23
|
+
name: row.name,
|
|
24
|
+
file: row.file,
|
|
25
|
+
lineStart: row.line_start,
|
|
26
|
+
lineEnd: row.line_end,
|
|
27
|
+
language: row.language,
|
|
28
|
+
signature: row.signature ?? undefined,
|
|
29
|
+
isTest: row.is_test === 1,
|
|
30
|
+
metadata: row.metadata ? (JSON.parse(row.metadata) as Record<string, string>) : undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Flow entry point with its tag value and associated node. */
|
|
35
|
+
type FlowEntry = {
|
|
36
|
+
readonly value: string;
|
|
37
|
+
readonly node: Node;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Boundary tag with its value and associated node. */
|
|
41
|
+
type BoundaryEntry = {
|
|
42
|
+
readonly value: string;
|
|
43
|
+
readonly node: Node;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** An event connection between an emitter and a handler. */
|
|
47
|
+
type EventConnection = {
|
|
48
|
+
readonly eventName: string;
|
|
49
|
+
readonly emitterName: string;
|
|
50
|
+
readonly emitterFile: string;
|
|
51
|
+
readonly handlerName: string;
|
|
52
|
+
readonly handlerFile: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolves a symbol by full ID or short name.
|
|
57
|
+
* Full ID match takes priority. Falls back to name match.
|
|
58
|
+
*
|
|
59
|
+
* @param db - An open Database handle
|
|
60
|
+
* @param symbol - Full node ID or short symbol name
|
|
61
|
+
* @returns Matching nodes, empty if none found
|
|
62
|
+
*/
|
|
63
|
+
function resolveSymbol(db: Database, symbol: string): readonly Node[] {
|
|
64
|
+
// Try exact ID match first
|
|
65
|
+
const exact = db.query("SELECT * FROM nodes WHERE id = ?").get(symbol) as NodeRow | null;
|
|
66
|
+
if (exact) return [rowToNode(exact)];
|
|
67
|
+
|
|
68
|
+
// Fall back to name match
|
|
69
|
+
const byName = db.query("SELECT * FROM nodes WHERE name = ?").all(symbol) as NodeRow[];
|
|
70
|
+
return byName.map(rowToNode);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns all nodes that are members of a flow via recursive call graph traversal.
|
|
75
|
+
* Includes the tagged entry point(s) and all nodes reachable through calls and event edges.
|
|
76
|
+
*
|
|
77
|
+
* @param db - An open Database handle
|
|
78
|
+
* @param flowName - The flow tag value to query
|
|
79
|
+
* @returns All nodes in the flow
|
|
80
|
+
*/
|
|
81
|
+
function getFlowMembers(db: Database, flowName: string): readonly Node[] {
|
|
82
|
+
const rows = db
|
|
83
|
+
.query(
|
|
84
|
+
`WITH RECURSIVE flow_members AS (
|
|
85
|
+
SELECT node_id FROM tags WHERE kind = 'flow' AND value = ?
|
|
86
|
+
UNION
|
|
87
|
+
SELECT e.target_id FROM edges e
|
|
88
|
+
JOIN flow_members fm ON e.source_id = fm.node_id
|
|
89
|
+
WHERE e.kind IN ('calls', 'event')
|
|
90
|
+
)
|
|
91
|
+
SELECT n.* FROM nodes n WHERE n.id IN (SELECT node_id FROM flow_members)`,
|
|
92
|
+
)
|
|
93
|
+
.all(flowName) as NodeRow[];
|
|
94
|
+
return rows.map(rowToNode);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns direct callers of a node (reverse call/event edges).
|
|
99
|
+
*
|
|
100
|
+
* @param db - An open Database handle
|
|
101
|
+
* @param nodeId - The full node ID
|
|
102
|
+
* @returns Nodes that directly call or trigger this node
|
|
103
|
+
*/
|
|
104
|
+
function getCallers(db: Database, nodeId: string): readonly Node[] {
|
|
105
|
+
const rows = db
|
|
106
|
+
.query(
|
|
107
|
+
`SELECT n.* FROM nodes n
|
|
108
|
+
JOIN edges e ON n.id = e.source_id
|
|
109
|
+
WHERE e.target_id = ? AND e.kind IN ('calls', 'event')`,
|
|
110
|
+
)
|
|
111
|
+
.all(nodeId) as NodeRow[];
|
|
112
|
+
return rows.map(rowToNode);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns direct callees of a node (forward call/event edges).
|
|
117
|
+
*
|
|
118
|
+
* @param db - An open Database handle
|
|
119
|
+
* @param nodeId - The full node ID
|
|
120
|
+
* @returns Nodes that this node directly calls or triggers
|
|
121
|
+
*/
|
|
122
|
+
function getCallees(db: Database, nodeId: string): readonly Node[] {
|
|
123
|
+
const rows = db
|
|
124
|
+
.query(
|
|
125
|
+
`SELECT n.* FROM nodes n
|
|
126
|
+
JOIN edges e ON n.id = e.target_id
|
|
127
|
+
WHERE e.source_id = ? AND e.kind IN ('calls', 'event')`,
|
|
128
|
+
)
|
|
129
|
+
.all(nodeId) as NodeRow[];
|
|
130
|
+
return rows.map(rowToNode);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns all transitive callers of a node (upstream traversal).
|
|
135
|
+
* Used for impact analysis — "what is affected if I change this?"
|
|
136
|
+
*
|
|
137
|
+
* @param db - An open Database handle
|
|
138
|
+
* @param nodeId - The full node ID to analyze
|
|
139
|
+
* @returns All nodes that transitively depend on this node
|
|
140
|
+
*/
|
|
141
|
+
function getImpact(db: Database, nodeId: string): readonly Node[] {
|
|
142
|
+
const rows = db
|
|
143
|
+
.query(
|
|
144
|
+
`WITH RECURSIVE upstream AS (
|
|
145
|
+
SELECT source_id FROM edges WHERE target_id = ? AND kind IN ('calls', 'event')
|
|
146
|
+
UNION
|
|
147
|
+
SELECT e.source_id FROM edges e
|
|
148
|
+
JOIN upstream u ON e.target_id = u.source_id
|
|
149
|
+
WHERE e.kind IN ('calls', 'event')
|
|
150
|
+
)
|
|
151
|
+
SELECT n.* FROM nodes n WHERE n.id IN (SELECT source_id FROM upstream)`,
|
|
152
|
+
)
|
|
153
|
+
.all(nodeId) as NodeRow[];
|
|
154
|
+
return rows.map(rowToNode);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Returns which flows a node participates in (derived from graph traversal).
|
|
159
|
+
* Checks if any flow entry point can reach this node through the call graph.
|
|
160
|
+
*
|
|
161
|
+
* @param db - An open Database handle
|
|
162
|
+
* @param nodeId - The full node ID
|
|
163
|
+
* @returns Flow names this node belongs to
|
|
164
|
+
*/
|
|
165
|
+
function getFlowsForNode(db: Database, nodeId: string): readonly string[] {
|
|
166
|
+
// First check if the node itself has a flow tag
|
|
167
|
+
const directTags = db
|
|
168
|
+
.query("SELECT value FROM tags WHERE node_id = ? AND kind = 'flow'")
|
|
169
|
+
.all(nodeId) as { value: string }[];
|
|
170
|
+
|
|
171
|
+
// Then check if any flow reaches this node
|
|
172
|
+
const derivedFlows = db
|
|
173
|
+
.query(
|
|
174
|
+
`SELECT DISTINCT t.value FROM tags t
|
|
175
|
+
WHERE t.kind = 'flow'
|
|
176
|
+
AND EXISTS (
|
|
177
|
+
WITH RECURSIVE flow_members AS (
|
|
178
|
+
SELECT t.node_id AS node_id
|
|
179
|
+
UNION
|
|
180
|
+
SELECT e.target_id FROM edges e
|
|
181
|
+
JOIN flow_members fm ON e.source_id = fm.node_id
|
|
182
|
+
WHERE e.kind IN ('calls', 'event')
|
|
183
|
+
)
|
|
184
|
+
SELECT 1 FROM flow_members WHERE node_id = ?
|
|
185
|
+
)`,
|
|
186
|
+
)
|
|
187
|
+
.all(nodeId) as { value: string }[];
|
|
188
|
+
|
|
189
|
+
const all = new Set([...directTags.map((t) => t.value), ...derivedFlows.map((f) => f.value)]);
|
|
190
|
+
return [...all];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Returns all flow entry points in the graph.
|
|
195
|
+
*
|
|
196
|
+
* @param db - An open Database handle
|
|
197
|
+
* @returns Flow entries with tag value and associated node
|
|
198
|
+
*/
|
|
199
|
+
function getAllFlows(db: Database): readonly FlowEntry[] {
|
|
200
|
+
const rows = db
|
|
201
|
+
.query(
|
|
202
|
+
`SELECT t.value, n.* FROM tags t
|
|
203
|
+
JOIN nodes n ON t.node_id = n.id
|
|
204
|
+
WHERE t.kind = 'flow'
|
|
205
|
+
ORDER BY t.value, n.file`,
|
|
206
|
+
)
|
|
207
|
+
.all() as (NodeRow & { value: string })[];
|
|
208
|
+
return rows.map((row) => ({ value: row.value, node: rowToNode(row) }));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Returns all boundary-tagged nodes in the graph.
|
|
213
|
+
*
|
|
214
|
+
* @param db - An open Database handle
|
|
215
|
+
* @returns Boundary entries with tag value and associated node
|
|
216
|
+
*/
|
|
217
|
+
function getAllBoundaries(db: Database): readonly BoundaryEntry[] {
|
|
218
|
+
const rows = db
|
|
219
|
+
.query(
|
|
220
|
+
`SELECT t.value, n.* FROM tags t
|
|
221
|
+
JOIN nodes n ON t.node_id = n.id
|
|
222
|
+
WHERE t.kind = 'boundary'
|
|
223
|
+
ORDER BY t.value, n.file`,
|
|
224
|
+
)
|
|
225
|
+
.all() as (NodeRow & { value: string })[];
|
|
226
|
+
return rows.map((row) => ({ value: row.value, node: rowToNode(row) }));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Returns all event connections (emits → handles).
|
|
231
|
+
*
|
|
232
|
+
* @param db - An open Database handle
|
|
233
|
+
* @returns Event connections with emitter and handler info
|
|
234
|
+
*/
|
|
235
|
+
function getAllEvents(db: Database): readonly EventConnection[] {
|
|
236
|
+
const rows = db
|
|
237
|
+
.query(
|
|
238
|
+
`SELECT e.value AS event_name,
|
|
239
|
+
emitter.name AS emitter_name, emitter.file AS emitter_file,
|
|
240
|
+
handler.name AS handler_name, handler.file AS handler_file
|
|
241
|
+
FROM tags e
|
|
242
|
+
JOIN tags h ON e.value = h.value AND h.kind = 'handles'
|
|
243
|
+
JOIN nodes emitter ON e.node_id = emitter.id
|
|
244
|
+
JOIN nodes handler ON h.node_id = handler.id
|
|
245
|
+
WHERE e.kind = 'emits'
|
|
246
|
+
ORDER BY e.value`,
|
|
247
|
+
)
|
|
248
|
+
.all() as {
|
|
249
|
+
event_name: string;
|
|
250
|
+
emitter_name: string;
|
|
251
|
+
emitter_file: string;
|
|
252
|
+
handler_name: string;
|
|
253
|
+
handler_file: string;
|
|
254
|
+
}[];
|
|
255
|
+
return rows.map((row) => ({
|
|
256
|
+
eventName: row.event_name,
|
|
257
|
+
emitterName: row.emitter_name,
|
|
258
|
+
emitterFile: row.emitter_file,
|
|
259
|
+
handlerName: row.handler_name,
|
|
260
|
+
handlerFile: row.handler_file,
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Finds all paths from a source node to a target node using DFS with backtracking.
|
|
266
|
+
* Traverses calls and event edges. Cycle-aware via visited set.
|
|
267
|
+
*
|
|
268
|
+
* @param db - An open Database handle
|
|
269
|
+
* @param sourceId - Starting node ID
|
|
270
|
+
* @param targetId - Target node ID
|
|
271
|
+
* @returns All distinct paths as arrays of node IDs
|
|
272
|
+
*/
|
|
273
|
+
function findAllPaths(
|
|
274
|
+
db: Database,
|
|
275
|
+
sourceId: string,
|
|
276
|
+
targetId: string,
|
|
277
|
+
): readonly (readonly string[])[] {
|
|
278
|
+
// Build adjacency list in memory for efficient traversal
|
|
279
|
+
const edges = db
|
|
280
|
+
.query("SELECT source_id, target_id FROM edges WHERE kind IN ('calls', 'event')")
|
|
281
|
+
.all() as { source_id: string; target_id: string }[];
|
|
282
|
+
|
|
283
|
+
const adjacency = new Map<string, string[]>();
|
|
284
|
+
for (const edge of edges) {
|
|
285
|
+
const existing = adjacency.get(edge.source_id);
|
|
286
|
+
if (existing) {
|
|
287
|
+
existing.push(edge.target_id);
|
|
288
|
+
} else {
|
|
289
|
+
adjacency.set(edge.source_id, [edge.target_id]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const results: string[][] = [];
|
|
294
|
+
const visited = new Set<string>();
|
|
295
|
+
|
|
296
|
+
function dfs(current: string, path: string[]): void {
|
|
297
|
+
if (current === targetId) {
|
|
298
|
+
results.push([...path]);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const neighbors = adjacency.get(current);
|
|
303
|
+
if (!neighbors) return;
|
|
304
|
+
|
|
305
|
+
for (const neighbor of neighbors) {
|
|
306
|
+
if (!visited.has(neighbor)) {
|
|
307
|
+
visited.add(neighbor);
|
|
308
|
+
path.push(neighbor);
|
|
309
|
+
dfs(neighbor, path);
|
|
310
|
+
path.pop();
|
|
311
|
+
visited.delete(neighbor);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
visited.add(sourceId);
|
|
317
|
+
dfs(sourceId, [sourceId]);
|
|
318
|
+
|
|
319
|
+
return results;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export {
|
|
323
|
+
type BoundaryEntry,
|
|
324
|
+
type EventConnection,
|
|
325
|
+
type FlowEntry,
|
|
326
|
+
findAllPaths,
|
|
327
|
+
getAllBoundaries,
|
|
328
|
+
getAllEvents,
|
|
329
|
+
getAllFlows,
|
|
330
|
+
getCallees,
|
|
331
|
+
getCallers,
|
|
332
|
+
getFlowMembers,
|
|
333
|
+
getFlowsForNode,
|
|
334
|
+
getImpact,
|
|
335
|
+
resolveSymbol,
|
|
336
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { Edge, Node, Tag, UnresolvedReference } from "../types/graph.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Inserts nodes into the graph database.
|
|
6
|
+
* Uses INSERT OR REPLACE to handle re-indexing.
|
|
7
|
+
*
|
|
8
|
+
* @param db - An open Database handle
|
|
9
|
+
* @param nodes - Nodes to insert
|
|
10
|
+
*/
|
|
11
|
+
function insertNodes(db: Database, nodes: readonly Node[]): void {
|
|
12
|
+
const stmt = db.prepare(
|
|
13
|
+
"INSERT OR REPLACE INTO nodes (id, kind, name, file, line_start, line_end, language, signature, is_test, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
14
|
+
);
|
|
15
|
+
const tx = db.transaction(() => {
|
|
16
|
+
for (const node of nodes) {
|
|
17
|
+
stmt.run(
|
|
18
|
+
node.id,
|
|
19
|
+
node.kind,
|
|
20
|
+
node.name,
|
|
21
|
+
node.file,
|
|
22
|
+
node.lineStart,
|
|
23
|
+
node.lineEnd,
|
|
24
|
+
node.language,
|
|
25
|
+
node.signature ?? null,
|
|
26
|
+
node.isTest ? 1 : 0,
|
|
27
|
+
node.metadata ? JSON.stringify(node.metadata) : null,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
tx();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Inserts edges into the graph database.
|
|
36
|
+
* Uses INSERT OR IGNORE to skip duplicates.
|
|
37
|
+
*
|
|
38
|
+
* @param db - An open Database handle
|
|
39
|
+
* @param edges - Edges to insert
|
|
40
|
+
*/
|
|
41
|
+
function insertEdges(db: Database, edges: readonly Edge[]): void {
|
|
42
|
+
const stmt = db.prepare(
|
|
43
|
+
"INSERT OR IGNORE INTO edges (source_id, target_id, kind, certainty) VALUES (?, ?, ?, ?)",
|
|
44
|
+
);
|
|
45
|
+
const tx = db.transaction(() => {
|
|
46
|
+
for (const edge of edges) {
|
|
47
|
+
stmt.run(edge.sourceId, edge.targetId, edge.kind, edge.certainty);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
tx();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Inserts tags into the graph database.
|
|
55
|
+
* Uses INSERT OR IGNORE to skip duplicates.
|
|
56
|
+
*
|
|
57
|
+
* @param db - An open Database handle
|
|
58
|
+
* @param tags - Tags to insert
|
|
59
|
+
*/
|
|
60
|
+
function insertTags(db: Database, tags: readonly Tag[]): void {
|
|
61
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO tags (node_id, kind, value) VALUES (?, ?, ?)");
|
|
62
|
+
const tx = db.transaction(() => {
|
|
63
|
+
for (const tag of tags) {
|
|
64
|
+
stmt.run(tag.nodeId, tag.kind, tag.value);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
tx();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Inserts unresolved references into the database.
|
|
72
|
+
* Uses INSERT OR IGNORE to skip duplicates.
|
|
73
|
+
*
|
|
74
|
+
* @param db - An open Database handle
|
|
75
|
+
* @param refs - Unresolved references to insert
|
|
76
|
+
*/
|
|
77
|
+
function insertUnresolved(db: Database, refs: readonly UnresolvedReference[]): void {
|
|
78
|
+
const stmt = db.prepare(
|
|
79
|
+
"INSERT OR IGNORE INTO unresolved (file, line, expression, reason) VALUES (?, ?, ?, ?)",
|
|
80
|
+
);
|
|
81
|
+
const tx = db.transaction(() => {
|
|
82
|
+
for (const ref of refs) {
|
|
83
|
+
stmt.run(ref.file, ref.line, ref.expression, ref.reason);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
tx();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Deletes all nodes, edges, tags, and unresolved references for a given file.
|
|
91
|
+
* Edges where the file's nodes are either source or target are removed.
|
|
92
|
+
*
|
|
93
|
+
* @param db - An open Database handle
|
|
94
|
+
* @param file - Relative file path to delete data for
|
|
95
|
+
*/
|
|
96
|
+
function deleteFileData(db: Database, file: string): void {
|
|
97
|
+
const tx = db.transaction(() => {
|
|
98
|
+
// Get node IDs for this file
|
|
99
|
+
const nodeIds = db.query("SELECT id FROM nodes WHERE file = ?").all(file) as { id: string }[];
|
|
100
|
+
const ids = nodeIds.map((n) => n.id);
|
|
101
|
+
|
|
102
|
+
if (ids.length > 0) {
|
|
103
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
104
|
+
// Delete tags for these nodes
|
|
105
|
+
db.run(`DELETE FROM tags WHERE node_id IN (${placeholders})`, ids);
|
|
106
|
+
// Delete edges where these nodes are source or target
|
|
107
|
+
db.run(`DELETE FROM edges WHERE source_id IN (${placeholders})`, ids);
|
|
108
|
+
db.run(`DELETE FROM edges WHERE target_id IN (${placeholders})`, ids);
|
|
109
|
+
// Delete the nodes themselves
|
|
110
|
+
db.run(`DELETE FROM nodes WHERE id IN (${placeholders})`, ids);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Delete unresolved references for this file
|
|
114
|
+
db.run("DELETE FROM unresolved WHERE file = ?", [file]);
|
|
115
|
+
});
|
|
116
|
+
tx();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates synthetic event edges from @lattice:emits to @lattice:handles.
|
|
121
|
+
* Deletes all existing event edges first, then recreates from current tags.
|
|
122
|
+
* This ensures event edges stay consistent after any tag changes.
|
|
123
|
+
*
|
|
124
|
+
* @param db - An open Database handle
|
|
125
|
+
*/
|
|
126
|
+
function synthesizeEventEdges(db: Database): void {
|
|
127
|
+
const tx = db.transaction(() => {
|
|
128
|
+
db.run("DELETE FROM edges WHERE kind = 'event'");
|
|
129
|
+
db.run(`
|
|
130
|
+
INSERT OR IGNORE INTO edges (source_id, target_id, kind, certainty)
|
|
131
|
+
SELECT e.node_id, h.node_id, 'event', 'certain'
|
|
132
|
+
FROM tags e
|
|
133
|
+
JOIN tags h ON e.value = h.value
|
|
134
|
+
WHERE e.kind = 'emits' AND h.kind = 'handles'
|
|
135
|
+
`);
|
|
136
|
+
});
|
|
137
|
+
tx();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export {
|
|
141
|
+
deleteFileData,
|
|
142
|
+
insertEdges,
|
|
143
|
+
insertNodes,
|
|
144
|
+
insertTags,
|
|
145
|
+
insertUnresolved,
|
|
146
|
+
synthesizeEventEdges,
|
|
147
|
+
};
|