@utopia-ai/cli 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.
Files changed (49) hide show
  1. package/.claude/settings.json +1 -0
  2. package/.claude/settings.local.json +38 -0
  3. package/bin/utopia.js +20 -0
  4. package/package.json +46 -0
  5. package/python/README.md +34 -0
  6. package/python/instrumenter/instrument.py +1148 -0
  7. package/python/pyproject.toml +32 -0
  8. package/python/setup.py +27 -0
  9. package/python/utopia_runtime/__init__.py +30 -0
  10. package/python/utopia_runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  11. package/python/utopia_runtime/__pycache__/client.cpython-313.pyc +0 -0
  12. package/python/utopia_runtime/__pycache__/probe.cpython-313.pyc +0 -0
  13. package/python/utopia_runtime/client.py +31 -0
  14. package/python/utopia_runtime/probe.py +446 -0
  15. package/python/utopia_runtime.egg-info/PKG-INFO +59 -0
  16. package/python/utopia_runtime.egg-info/SOURCES.txt +10 -0
  17. package/python/utopia_runtime.egg-info/dependency_links.txt +1 -0
  18. package/python/utopia_runtime.egg-info/top_level.txt +1 -0
  19. package/scripts/publish-npm.sh +14 -0
  20. package/scripts/publish-pypi.sh +17 -0
  21. package/src/cli/commands/codex.ts +193 -0
  22. package/src/cli/commands/context.ts +188 -0
  23. package/src/cli/commands/destruct.ts +237 -0
  24. package/src/cli/commands/easter-eggs.ts +203 -0
  25. package/src/cli/commands/init.ts +505 -0
  26. package/src/cli/commands/instrument.ts +962 -0
  27. package/src/cli/commands/mcp.ts +16 -0
  28. package/src/cli/commands/serve.ts +194 -0
  29. package/src/cli/commands/status.ts +304 -0
  30. package/src/cli/commands/validate.ts +328 -0
  31. package/src/cli/index.ts +37 -0
  32. package/src/cli/utils/config.ts +54 -0
  33. package/src/graph/index.ts +687 -0
  34. package/src/instrumenter/javascript.ts +1798 -0
  35. package/src/mcp/index.ts +886 -0
  36. package/src/runtime/js/index.ts +518 -0
  37. package/src/runtime/js/package-lock.json +30 -0
  38. package/src/runtime/js/package.json +30 -0
  39. package/src/runtime/js/tsconfig.json +16 -0
  40. package/src/server/db/index.ts +26 -0
  41. package/src/server/db/schema.ts +45 -0
  42. package/src/server/index.ts +79 -0
  43. package/src/server/middleware/auth.ts +74 -0
  44. package/src/server/routes/admin.ts +36 -0
  45. package/src/server/routes/graph.ts +358 -0
  46. package/src/server/routes/probes.ts +286 -0
  47. package/src/types.ts +147 -0
  48. package/src/utopia-mode/index.ts +206 -0
  49. package/tsconfig.json +19 -0
@@ -0,0 +1,687 @@
1
+ import type Database from 'better-sqlite3';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Types
5
+ // ---------------------------------------------------------------------------
6
+
7
+ interface ProbeRecord {
8
+ id: string;
9
+ project_id: string;
10
+ probe_type: string;
11
+ timestamp: string;
12
+ file: string;
13
+ line: number;
14
+ function_name: string;
15
+ data: string; // JSON string
16
+ metadata: string; // JSON string
17
+ }
18
+
19
+ interface GraphNodeRecord {
20
+ id: string;
21
+ type: string;
22
+ name: string;
23
+ file: string | null;
24
+ metadata: string; // JSON string
25
+ }
26
+
27
+ interface GraphEdgeRecord {
28
+ source: string;
29
+ target: string;
30
+ type: string;
31
+ weight: number;
32
+ last_seen: string;
33
+ }
34
+
35
+ interface ImpactResult {
36
+ rootNode: GraphNodeRecord;
37
+ impactedNodes: { node: GraphNodeRecord; depth: number; path: string[] }[];
38
+ edges: GraphEdgeRecord[];
39
+ totalImpacted: number;
40
+ }
41
+
42
+ interface DependencyResult {
43
+ rootNode: GraphNodeRecord;
44
+ dependencies: { node: GraphNodeRecord; depth: number; path: string[] }[];
45
+ edges: GraphEdgeRecord[];
46
+ totalDependencies: number;
47
+ }
48
+
49
+ interface GraphStats {
50
+ totalNodes: number;
51
+ nodesByType: Record<string, number>;
52
+ totalEdges: number;
53
+ edgesByType: Record<string, number>;
54
+ mostConnected: { node: GraphNodeRecord; connectionCount: number }[];
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Helpers
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Build a deterministic node ID from type and name.
63
+ * Format: `type:name`
64
+ */
65
+ function nodeId(type: string, name: string): string {
66
+ return `${type}:${name}`;
67
+ }
68
+
69
+ /**
70
+ * Derive a stable API node name from a URL string.
71
+ * Strips query parameters, fragments, trailing slashes, and normalises the
72
+ * result to `hostname/path`.
73
+ */
74
+ function normalizeApiName(raw: string): string {
75
+ try {
76
+ const url = new URL(raw);
77
+ // hostname + pathname, strip trailing slash
78
+ const pathname = url.pathname.replace(/\/+$/, '') || '';
79
+ return `${url.hostname}${pathname}`;
80
+ } catch {
81
+ // If URL parsing fails, do a best-effort strip of query/fragment
82
+ const cleaned = raw.split('?')[0].split('#')[0].replace(/\/+$/, '');
83
+ return cleaned;
84
+ }
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // ImpactGraph
89
+ // ---------------------------------------------------------------------------
90
+
91
+ class ImpactGraph {
92
+ private db: Database.Database;
93
+
94
+ // Prepared statements — lazily initialised on first use so that the
95
+ // constructor never throws if the schema hasn't been created yet.
96
+ private stmts!: {
97
+ upsertNode: Database.Statement;
98
+ upsertEdge: Database.Statement;
99
+ getNode: Database.Statement;
100
+ getOutEdges: Database.Statement;
101
+ getInEdges: Database.Statement;
102
+ allNodes: Database.Statement;
103
+ allEdges: Database.Statement;
104
+ allProbes: Database.Statement;
105
+ nodesByType: Database.Statement;
106
+ edgesForNodeSet: (ids: string[]) => Database.Statement;
107
+ countNodes: Database.Statement;
108
+ countEdges: Database.Statement;
109
+ nodeCountsByType: Database.Statement;
110
+ edgeCountsByType: Database.Statement;
111
+ };
112
+
113
+ private prepared = false;
114
+
115
+ constructor(db: Database.Database) {
116
+ this.db = db;
117
+ }
118
+
119
+ // ------------------------------------------------------------------
120
+ // Statement preparation
121
+ // ------------------------------------------------------------------
122
+
123
+ private ensurePrepared(): void {
124
+ if (this.prepared) return;
125
+
126
+ const db = this.db;
127
+
128
+ this.stmts = {
129
+ upsertNode: db.prepare(`
130
+ INSERT INTO graph_nodes (id, type, name, file, metadata)
131
+ VALUES (@id, @type, @name, @file, @metadata)
132
+ ON CONFLICT(id) DO UPDATE SET
133
+ type = excluded.type,
134
+ name = excluded.name,
135
+ file = excluded.file,
136
+ metadata = excluded.metadata
137
+ `),
138
+
139
+ upsertEdge: db.prepare(`
140
+ INSERT INTO graph_edges (source, target, type, weight, last_seen)
141
+ VALUES (@source, @target, @type, 1, datetime('now'))
142
+ ON CONFLICT(source, target, type) DO UPDATE SET
143
+ weight = graph_edges.weight + 1,
144
+ last_seen = datetime('now')
145
+ `),
146
+
147
+ getNode: db.prepare('SELECT * FROM graph_nodes WHERE id = ?'),
148
+
149
+ getOutEdges: db.prepare('SELECT * FROM graph_edges WHERE source = ?'),
150
+
151
+ getInEdges: db.prepare('SELECT * FROM graph_edges WHERE target = ?'),
152
+
153
+ allNodes: db.prepare('SELECT * FROM graph_nodes'),
154
+
155
+ allEdges: db.prepare('SELECT * FROM graph_edges'),
156
+
157
+ allProbes: db.prepare('SELECT * FROM probes ORDER BY timestamp ASC'),
158
+
159
+ nodesByType: db.prepare('SELECT * FROM graph_nodes WHERE type = ?'),
160
+
161
+ // Dynamic — returns a fresh statement for a given set of IDs
162
+ edgesForNodeSet: (ids: string[]) => {
163
+ const ph = ids.map(() => '?').join(',');
164
+ return db.prepare(
165
+ `SELECT * FROM graph_edges WHERE source IN (${ph}) OR target IN (${ph})`,
166
+ );
167
+ },
168
+
169
+ countNodes: db.prepare('SELECT COUNT(*) as cnt FROM graph_nodes'),
170
+
171
+ countEdges: db.prepare('SELECT COUNT(*) as cnt FROM graph_edges'),
172
+
173
+ nodeCountsByType: db.prepare(
174
+ 'SELECT type, COUNT(*) as cnt FROM graph_nodes GROUP BY type',
175
+ ),
176
+
177
+ edgeCountsByType: db.prepare(
178
+ 'SELECT type, COUNT(*) as cnt FROM graph_edges GROUP BY type',
179
+ ),
180
+ };
181
+
182
+ this.prepared = true;
183
+ }
184
+
185
+ // ------------------------------------------------------------------
186
+ // Low-level graph mutations
187
+ // ------------------------------------------------------------------
188
+
189
+ private upsertNode(
190
+ type: string,
191
+ name: string,
192
+ file: string | null,
193
+ metadata: Record<string, unknown> = {},
194
+ ): GraphNodeRecord {
195
+ this.ensurePrepared();
196
+ const id = nodeId(type, name);
197
+ this.stmts.upsertNode.run({
198
+ id,
199
+ type,
200
+ name,
201
+ file,
202
+ metadata: JSON.stringify(metadata),
203
+ });
204
+ return { id, type, name, file, metadata: JSON.stringify(metadata) };
205
+ }
206
+
207
+ private upsertEdge(source: string, target: string, type: string): void {
208
+ this.ensurePrepared();
209
+ this.stmts.upsertEdge.run({ source, target, type });
210
+ }
211
+
212
+ // ------------------------------------------------------------------
213
+ // processProbe
214
+ // ------------------------------------------------------------------
215
+
216
+ processProbe(probe: ProbeRecord): void {
217
+ this.ensurePrepared();
218
+
219
+ let data: Record<string, unknown>;
220
+ try {
221
+ data = JSON.parse(probe.data);
222
+ } catch {
223
+ data = {};
224
+ }
225
+
226
+ const file = probe.file;
227
+ const functionName = probe.function_name;
228
+
229
+ switch (probe.probe_type) {
230
+ case 'error':
231
+ this.processErrorProbe(file, functionName, data);
232
+ break;
233
+ case 'database':
234
+ this.processDatabaseProbe(file, functionName, data);
235
+ break;
236
+ case 'api':
237
+ this.processApiProbe(file, functionName, data);
238
+ break;
239
+ case 'infra':
240
+ this.processInfraProbe(file, data);
241
+ break;
242
+ case 'function':
243
+ this.processFunctionProbe(file, functionName, data);
244
+ break;
245
+ default:
246
+ // Unknown probe type — silently ignore
247
+ break;
248
+ }
249
+ }
250
+
251
+ private processErrorProbe(
252
+ file: string,
253
+ functionName: string,
254
+ data: Record<string, unknown>,
255
+ ): void {
256
+ // Function node for the function that errored
257
+ const fnName = functionName || 'unknown';
258
+ const fnNodeName = `${file}:${fnName}`;
259
+ this.upsertNode('function', fnNodeName, file, {
260
+ errorType: data.errorType,
261
+ message: data.message,
262
+ });
263
+
264
+ // File node
265
+ this.upsertNode('file', file, file);
266
+
267
+ // Edge: function -> file
268
+ this.upsertEdge(nodeId('function', fnNodeName), nodeId('file', file), 'depends_on');
269
+ }
270
+
271
+ private processDatabaseProbe(
272
+ file: string,
273
+ functionName: string,
274
+ data: Record<string, unknown>,
275
+ ): void {
276
+ // Function node for the caller
277
+ const fnName = functionName || 'unknown';
278
+ const fnNodeName = `${file}:${fnName}`;
279
+ this.upsertNode('function', fnNodeName, file);
280
+
281
+ // Database node — prefer table name, fall back to connection info
282
+ let dbName: string;
283
+ if (data.table && typeof data.table === 'string') {
284
+ dbName = data.table;
285
+ } else if (
286
+ data.connectionInfo &&
287
+ typeof data.connectionInfo === 'object' &&
288
+ (data.connectionInfo as Record<string, unknown>).database
289
+ ) {
290
+ dbName = String((data.connectionInfo as Record<string, unknown>).database);
291
+ } else {
292
+ dbName = 'unknown';
293
+ }
294
+
295
+ const connInfo = data.connectionInfo as Record<string, unknown> | undefined;
296
+ this.upsertNode('database', dbName, null, {
297
+ operation: data.operation,
298
+ connectionType: connInfo?.type,
299
+ host: connInfo?.host,
300
+ });
301
+
302
+ // Edge: function -> database (queries)
303
+ this.upsertEdge(nodeId('function', fnNodeName), nodeId('database', dbName), 'queries');
304
+ }
305
+
306
+ private processApiProbe(
307
+ file: string,
308
+ functionName: string,
309
+ data: Record<string, unknown>,
310
+ ): void {
311
+ // Function node for the caller
312
+ const fnName = functionName || 'unknown';
313
+ const fnNodeName = `${file}:${fnName}`;
314
+ this.upsertNode('function', fnNodeName, file);
315
+
316
+ // API node — derive name from URL
317
+ const rawUrl = (data.url as string) || 'unknown';
318
+ const apiName = normalizeApiName(rawUrl);
319
+ this.upsertNode('api', apiName, null, {
320
+ method: data.method,
321
+ statusCode: data.statusCode,
322
+ });
323
+
324
+ // Edge: function -> api (calls)
325
+ this.upsertEdge(nodeId('function', fnNodeName), nodeId('api', apiName), 'calls');
326
+ }
327
+
328
+ private processInfraProbe(
329
+ file: string,
330
+ data: Record<string, unknown>,
331
+ ): void {
332
+ // Service node — derive service name from provider + serviceType or fallback
333
+ const provider = (data.provider as string) || 'unknown';
334
+ const serviceType = (data.serviceType as string) || 'service';
335
+ const serviceName = `${provider}:${serviceType}`;
336
+
337
+ this.upsertNode('service', serviceName, null, {
338
+ provider,
339
+ region: data.region,
340
+ instanceId: data.instanceId,
341
+ });
342
+
343
+ // File node for the file the probe was placed in
344
+ this.upsertNode('file', file, file);
345
+
346
+ // Edge: service -> file (serves)
347
+ this.upsertEdge(nodeId('service', serviceName), nodeId('file', file), 'serves');
348
+ }
349
+
350
+ private processFunctionProbe(
351
+ file: string,
352
+ functionName: string,
353
+ data: Record<string, unknown>,
354
+ ): void {
355
+ // The probed function itself
356
+ const fnName = functionName || 'unknown';
357
+ const fnNodeName = `${file}:${fnName}`;
358
+ this.upsertNode('function', fnNodeName, file, {
359
+ duration: data.duration,
360
+ });
361
+
362
+ // Process call stack: each entry is a caller of the next
363
+ const callStack = (data.callStack as string[]) || [];
364
+ if (callStack.length > 0) {
365
+ // The call stack typically goes from outermost to innermost.
366
+ // Create nodes for each stack frame and edges between consecutive
367
+ // callers. We also connect the deepest caller to our function.
368
+ let previousNodeId: string | null = null;
369
+ for (const frame of callStack) {
370
+ // Frame format is usually "file:function" or just a function name
371
+ const frameName = frame.includes(':') ? frame : `${file}:${frame}`;
372
+ const frameFile = frame.includes(':') ? frame.split(':').slice(0, -1).join(':') : file;
373
+
374
+ this.upsertNode('function', frameName, frameFile);
375
+
376
+ if (previousNodeId !== null) {
377
+ this.upsertEdge(previousNodeId, nodeId('function', frameName), 'calls');
378
+ }
379
+
380
+ previousNodeId = nodeId('function', frameName);
381
+ }
382
+
383
+ // Connect deepest caller to the probed function
384
+ if (previousNodeId !== null && previousNodeId !== nodeId('function', fnNodeName)) {
385
+ this.upsertEdge(previousNodeId, nodeId('function', fnNodeName), 'calls');
386
+ }
387
+ }
388
+ }
389
+
390
+ // ------------------------------------------------------------------
391
+ // getImpact — BFS outward (following outgoing edges)
392
+ // ------------------------------------------------------------------
393
+
394
+ getImpact(startNodeId: string, maxDepth: number = 5): ImpactResult {
395
+ this.ensurePrepared();
396
+
397
+ const rootNode = this.stmts.getNode.get(startNodeId) as GraphNodeRecord | undefined;
398
+ if (!rootNode) {
399
+ throw new Error(`Node "${startNodeId}" not found`);
400
+ }
401
+
402
+ const visited = new Set<string>([startNodeId]);
403
+ const collectedEdges: GraphEdgeRecord[] = [];
404
+ const impactedNodes: { node: GraphNodeRecord; depth: number; path: string[] }[] = [];
405
+
406
+ // Map node ID -> shortest path from root
407
+ const pathMap = new Map<string, string[]>();
408
+ pathMap.set(startNodeId, [startNodeId]);
409
+
410
+ let frontier = [startNodeId];
411
+
412
+ for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
413
+ const nextFrontier: string[] = [];
414
+
415
+ for (const currentId of frontier) {
416
+ const edges = this.stmts.getOutEdges.all(currentId) as GraphEdgeRecord[];
417
+ for (const edge of edges) {
418
+ collectedEdges.push(edge);
419
+ if (!visited.has(edge.target)) {
420
+ visited.add(edge.target);
421
+ nextFrontier.push(edge.target);
422
+
423
+ const parentPath = pathMap.get(currentId) || [currentId];
424
+ pathMap.set(edge.target, [...parentPath, edge.target]);
425
+
426
+ const targetNode = this.stmts.getNode.get(edge.target) as
427
+ | GraphNodeRecord
428
+ | undefined;
429
+ if (targetNode) {
430
+ impactedNodes.push({
431
+ node: targetNode,
432
+ depth,
433
+ path: pathMap.get(edge.target)!,
434
+ });
435
+ }
436
+ }
437
+ }
438
+ }
439
+
440
+ frontier = nextFrontier;
441
+ }
442
+
443
+ // Deduplicate edges
444
+ const edgeMap = new Map<string, GraphEdgeRecord>();
445
+ for (const edge of collectedEdges) {
446
+ const key = `${edge.source}|${edge.target}|${edge.type}`;
447
+ edgeMap.set(key, edge);
448
+ }
449
+
450
+ return {
451
+ rootNode,
452
+ impactedNodes,
453
+ edges: [...edgeMap.values()],
454
+ totalImpacted: impactedNodes.length,
455
+ };
456
+ }
457
+
458
+ // ------------------------------------------------------------------
459
+ // getDependencies — BFS inward (following incoming edges in reverse)
460
+ // ------------------------------------------------------------------
461
+
462
+ getDependencies(startNodeId: string, maxDepth: number = 5): DependencyResult {
463
+ this.ensurePrepared();
464
+
465
+ const rootNode = this.stmts.getNode.get(startNodeId) as GraphNodeRecord | undefined;
466
+ if (!rootNode) {
467
+ throw new Error(`Node "${startNodeId}" not found`);
468
+ }
469
+
470
+ const visited = new Set<string>([startNodeId]);
471
+ const collectedEdges: GraphEdgeRecord[] = [];
472
+ const dependencies: { node: GraphNodeRecord; depth: number; path: string[] }[] = [];
473
+
474
+ const pathMap = new Map<string, string[]>();
475
+ pathMap.set(startNodeId, [startNodeId]);
476
+
477
+ let frontier = [startNodeId];
478
+
479
+ for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
480
+ const nextFrontier: string[] = [];
481
+
482
+ for (const currentId of frontier) {
483
+ const edges = this.stmts.getInEdges.all(currentId) as GraphEdgeRecord[];
484
+ for (const edge of edges) {
485
+ collectedEdges.push(edge);
486
+ if (!visited.has(edge.source)) {
487
+ visited.add(edge.source);
488
+ nextFrontier.push(edge.source);
489
+
490
+ const parentPath = pathMap.get(currentId) || [currentId];
491
+ pathMap.set(edge.source, [...parentPath, edge.source]);
492
+
493
+ const sourceNode = this.stmts.getNode.get(edge.source) as
494
+ | GraphNodeRecord
495
+ | undefined;
496
+ if (sourceNode) {
497
+ dependencies.push({
498
+ node: sourceNode,
499
+ depth,
500
+ path: pathMap.get(edge.source)!,
501
+ });
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ frontier = nextFrontier;
508
+ }
509
+
510
+ // Deduplicate edges
511
+ const edgeMap = new Map<string, GraphEdgeRecord>();
512
+ for (const edge of collectedEdges) {
513
+ const key = `${edge.source}|${edge.target}|${edge.type}`;
514
+ edgeMap.set(key, edge);
515
+ }
516
+
517
+ return {
518
+ rootNode,
519
+ dependencies,
520
+ edges: [...edgeMap.values()],
521
+ totalDependencies: dependencies.length,
522
+ };
523
+ }
524
+
525
+ // ------------------------------------------------------------------
526
+ // findNode — search nodes by partial match
527
+ // ------------------------------------------------------------------
528
+
529
+ findNode(query: {
530
+ file?: string;
531
+ functionName?: string;
532
+ name?: string;
533
+ type?: string;
534
+ }): GraphNodeRecord[] {
535
+ this.ensurePrepared();
536
+
537
+ const conditions: string[] = [];
538
+ const params: string[] = [];
539
+
540
+ if (query.file) {
541
+ conditions.push('file LIKE ?');
542
+ params.push(`%${query.file}%`);
543
+ }
544
+
545
+ if (query.functionName) {
546
+ conditions.push('name LIKE ?');
547
+ params.push(`%${query.functionName}%`);
548
+ }
549
+
550
+ if (query.name) {
551
+ conditions.push('name LIKE ?');
552
+ params.push(`%${query.name}%`);
553
+ }
554
+
555
+ if (query.type) {
556
+ conditions.push('type = ?');
557
+ params.push(query.type);
558
+ }
559
+
560
+ if (conditions.length === 0) {
561
+ return this.stmts.allNodes.all() as GraphNodeRecord[];
562
+ }
563
+
564
+ const sql = `SELECT * FROM graph_nodes WHERE ${conditions.join(' AND ')}`;
565
+ return this.db.prepare(sql).all(...params) as GraphNodeRecord[];
566
+ }
567
+
568
+ // ------------------------------------------------------------------
569
+ // getFullGraph
570
+ // ------------------------------------------------------------------
571
+
572
+ getFullGraph(nodeType?: string): { nodes: GraphNodeRecord[]; edges: GraphEdgeRecord[] } {
573
+ this.ensurePrepared();
574
+
575
+ if (!nodeType) {
576
+ return {
577
+ nodes: this.stmts.allNodes.all() as GraphNodeRecord[],
578
+ edges: this.stmts.allEdges.all() as GraphEdgeRecord[],
579
+ };
580
+ }
581
+
582
+ const nodes = this.stmts.nodesByType.all(nodeType) as GraphNodeRecord[];
583
+ if (nodes.length === 0) {
584
+ return { nodes: [], edges: [] };
585
+ }
586
+
587
+ const ids = nodes.map((n) => n.id);
588
+ const edges = this.stmts.edgesForNodeSet(ids).all(...ids, ...ids) as GraphEdgeRecord[];
589
+
590
+ return { nodes, edges };
591
+ }
592
+
593
+ // ------------------------------------------------------------------
594
+ // buildFromProbes — full rebuild from the probes table
595
+ // ------------------------------------------------------------------
596
+
597
+ buildFromProbes(): void {
598
+ this.ensurePrepared();
599
+
600
+ // Clear existing graph data before rebuilding
601
+ this.db.exec('DELETE FROM graph_edges');
602
+ this.db.exec('DELETE FROM graph_nodes');
603
+
604
+ const probes = this.stmts.allProbes.all() as ProbeRecord[];
605
+
606
+ const processAll = this.db.transaction((probeList: ProbeRecord[]) => {
607
+ for (const probe of probeList) {
608
+ this.processProbe(probe);
609
+ }
610
+ });
611
+
612
+ processAll(probes);
613
+ }
614
+
615
+ // ------------------------------------------------------------------
616
+ // getStats
617
+ // ------------------------------------------------------------------
618
+
619
+ getStats(): GraphStats {
620
+ this.ensurePrepared();
621
+
622
+ const totalNodes = (this.stmts.countNodes.get() as { cnt: number }).cnt;
623
+ const totalEdges = (this.stmts.countEdges.get() as { cnt: number }).cnt;
624
+
625
+ const nodesByType: Record<string, number> = {};
626
+ const nodeTypeRows = this.stmts.nodeCountsByType.all() as { type: string; cnt: number }[];
627
+ for (const row of nodeTypeRows) {
628
+ nodesByType[row.type] = row.cnt;
629
+ }
630
+
631
+ const edgesByType: Record<string, number> = {};
632
+ const edgeTypeRows = this.stmts.edgeCountsByType.all() as { type: string; cnt: number }[];
633
+ for (const row of edgeTypeRows) {
634
+ edgesByType[row.type] = row.cnt;
635
+ }
636
+
637
+ // Most connected nodes: count outgoing + incoming edges per node, top 10
638
+ const mostConnectedRows = this.db
639
+ .prepare(
640
+ `
641
+ SELECT n.*, (
642
+ (SELECT COUNT(*) FROM graph_edges WHERE source = n.id) +
643
+ (SELECT COUNT(*) FROM graph_edges WHERE target = n.id)
644
+ ) as connection_count
645
+ FROM graph_nodes n
646
+ ORDER BY connection_count DESC
647
+ LIMIT 10
648
+ `,
649
+ )
650
+ .all() as (GraphNodeRecord & { connection_count: number })[];
651
+
652
+ const mostConnected = mostConnectedRows.map((row) => ({
653
+ node: {
654
+ id: row.id,
655
+ type: row.type,
656
+ name: row.name,
657
+ file: row.file,
658
+ metadata: row.metadata,
659
+ } as GraphNodeRecord,
660
+ connectionCount: row.connection_count,
661
+ }));
662
+
663
+ return {
664
+ totalNodes,
665
+ nodesByType,
666
+ totalEdges,
667
+ edgesByType,
668
+ mostConnected,
669
+ };
670
+ }
671
+ }
672
+
673
+ // ---------------------------------------------------------------------------
674
+ // Exports
675
+ // ---------------------------------------------------------------------------
676
+
677
+ export { ImpactGraph };
678
+ export default ImpactGraph;
679
+
680
+ export type {
681
+ ProbeRecord,
682
+ GraphNodeRecord,
683
+ GraphEdgeRecord,
684
+ ImpactResult,
685
+ DependencyResult,
686
+ GraphStats,
687
+ };