ai-mind-map 1.14.0 → 1.15.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/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/knowledge-graph/graph.d.ts +26 -0
- package/dist/knowledge-graph/graph.d.ts.map +1 -1
- package/dist/knowledge-graph/graph.js +210 -131
- package/dist/knowledge-graph/graph.js.map +1 -1
- package/dist/tools/project-map-tool.d.ts +22 -0
- package/dist/tools/project-map-tool.d.ts.map +1 -0
- package/dist/tools/project-map-tool.js +877 -0
- package/dist/tools/project-map-tool.js.map +1 -0
- package/package.json +1 -1
|
@@ -50,8 +50,9 @@ CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type);
|
|
|
50
50
|
CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
|
|
51
51
|
CREATE INDEX IF NOT EXISTS idx_nodes_language ON nodes(language);
|
|
52
52
|
CREATE INDEX IF NOT EXISTS idx_nodes_hash ON nodes(hash);
|
|
53
|
-
|
|
54
|
-
CREATE INDEX IF NOT EXISTS
|
|
53
|
+
-- Composite indexes for common edge queries (type is always in WHERE clause)
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source_type ON edges(sourceId, type);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target_type ON edges(targetId, type);
|
|
55
56
|
CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);
|
|
56
57
|
|
|
57
58
|
-- FTS5 virtual table for full-text search across names, signatures, and doc comments
|
|
@@ -123,7 +124,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS content_fts USING fts5(
|
|
|
123
124
|
tokenize='unicode61'
|
|
124
125
|
);
|
|
125
126
|
`;
|
|
126
|
-
const SCHEMA_VERSION = '
|
|
127
|
+
const SCHEMA_VERSION = '6';
|
|
127
128
|
// ============================================================
|
|
128
129
|
// KnowledgeGraph Class
|
|
129
130
|
// ============================================================
|
|
@@ -135,6 +136,23 @@ const SCHEMA_VERSION = '5';
|
|
|
135
136
|
*/
|
|
136
137
|
export class KnowledgeGraph {
|
|
137
138
|
db;
|
|
139
|
+
// Cached prepared statements for hot-path queries (parse once, reuse forever)
|
|
140
|
+
_stmtCache = new Map();
|
|
141
|
+
/** Get or create a cached prepared statement */
|
|
142
|
+
stmt(sql) {
|
|
143
|
+
let s = this._stmtCache.get(sql);
|
|
144
|
+
if (!s) {
|
|
145
|
+
s = this.db.prepare(sql);
|
|
146
|
+
this._stmtCache.set(sql, s);
|
|
147
|
+
}
|
|
148
|
+
return s;
|
|
149
|
+
}
|
|
150
|
+
// In-memory adjacency cache for ultra-fast graph traversal
|
|
151
|
+
adjOut = new Map(); // nodeId → type → targetIds
|
|
152
|
+
adjIn = new Map(); // nodeId → type → sourceIds
|
|
153
|
+
adjDirty = true; // Rebuilt on first query after index
|
|
154
|
+
// Stats cache with 5-second TTL
|
|
155
|
+
_statsCache = null;
|
|
138
156
|
/**
|
|
139
157
|
* Create or open a knowledge graph database.
|
|
140
158
|
*
|
|
@@ -232,7 +250,7 @@ export class KnowledgeGraph {
|
|
|
232
250
|
* Used by changelog engine to diff old vs new nodes during re-indexing.
|
|
233
251
|
*/
|
|
234
252
|
getNodesForFile(filePath) {
|
|
235
|
-
const rows = this.
|
|
253
|
+
const rows = this.stmt('SELECT * FROM nodes WHERE filePath = ?').all(filePath);
|
|
236
254
|
return rows.map((r) => this.rowToNode(r));
|
|
237
255
|
}
|
|
238
256
|
// ============================================================
|
|
@@ -360,21 +378,21 @@ export class KnowledgeGraph {
|
|
|
360
378
|
* Get a node by its ID.
|
|
361
379
|
*/
|
|
362
380
|
getNode(id) {
|
|
363
|
-
const row = this.
|
|
381
|
+
const row = this.stmt('SELECT * FROM nodes WHERE id = ?').get(id);
|
|
364
382
|
return row ? this.rowToNode(row) : null;
|
|
365
383
|
}
|
|
366
384
|
/**
|
|
367
385
|
* Get nodes by name (may return multiple matches across files).
|
|
368
386
|
*/
|
|
369
387
|
getNodesByName(name) {
|
|
370
|
-
const rows = this.
|
|
388
|
+
const rows = this.stmt('SELECT * FROM nodes WHERE name = ?').all(name);
|
|
371
389
|
return rows.map((r) => this.rowToNode(r));
|
|
372
390
|
}
|
|
373
391
|
/**
|
|
374
392
|
* Get all nodes of a specific type.
|
|
375
393
|
*/
|
|
376
394
|
getNodesByType(type) {
|
|
377
|
-
const rows = this.
|
|
395
|
+
const rows = this.stmt('SELECT * FROM nodes WHERE type = ?').all(type);
|
|
378
396
|
return rows.map((r) => this.rowToNode(r));
|
|
379
397
|
}
|
|
380
398
|
/**
|
|
@@ -460,7 +478,7 @@ export class KnowledgeGraph {
|
|
|
460
478
|
* Get all edges originating from a node.
|
|
461
479
|
*/
|
|
462
480
|
getOutEdges(nodeId) {
|
|
463
|
-
const rows = this.
|
|
481
|
+
const rows = this.stmt('SELECT * FROM edges WHERE sourceId = ?').all(nodeId);
|
|
464
482
|
return rows.map(r => ({
|
|
465
483
|
sourceId: r.sourceId,
|
|
466
484
|
targetId: r.targetId,
|
|
@@ -472,7 +490,7 @@ export class KnowledgeGraph {
|
|
|
472
490
|
* Get all edges pointing to a node.
|
|
473
491
|
*/
|
|
474
492
|
getInEdges(nodeId) {
|
|
475
|
-
const rows = this.
|
|
493
|
+
const rows = this.stmt('SELECT * FROM edges WHERE targetId = ?').all(nodeId);
|
|
476
494
|
return rows.map(r => ({
|
|
477
495
|
sourceId: r.sourceId,
|
|
478
496
|
targetId: r.targetId,
|
|
@@ -484,7 +502,7 @@ export class KnowledgeGraph {
|
|
|
484
502
|
* Get edges of a specific type originating from a node.
|
|
485
503
|
*/
|
|
486
504
|
getOutEdgesByType(nodeId, type) {
|
|
487
|
-
const rows = this.
|
|
505
|
+
const rows = this.stmt('SELECT * FROM edges WHERE sourceId = ? AND type = ?').all(nodeId, type);
|
|
488
506
|
return rows.map(r => ({
|
|
489
507
|
sourceId: r.sourceId,
|
|
490
508
|
targetId: r.targetId,
|
|
@@ -496,7 +514,7 @@ export class KnowledgeGraph {
|
|
|
496
514
|
* Get edges of a specific type pointing to a node.
|
|
497
515
|
*/
|
|
498
516
|
getInEdgesByType(nodeId, type) {
|
|
499
|
-
const rows = this.
|
|
517
|
+
const rows = this.stmt('SELECT * FROM edges WHERE targetId = ? AND type = ?').all(nodeId, type);
|
|
500
518
|
return rows.map(r => ({
|
|
501
519
|
sourceId: r.sourceId,
|
|
502
520
|
targetId: r.targetId,
|
|
@@ -511,7 +529,7 @@ export class KnowledgeGraph {
|
|
|
511
529
|
* Find all nodes that call a given node (callers / "who calls this?").
|
|
512
530
|
*/
|
|
513
531
|
findCallers(nodeId) {
|
|
514
|
-
const rows = this.
|
|
532
|
+
const rows = this.stmt(`
|
|
515
533
|
SELECT n.* FROM nodes n
|
|
516
534
|
JOIN edges e ON n.id = e.sourceId
|
|
517
535
|
WHERE e.targetId = ? AND e.type = 'calls'
|
|
@@ -522,7 +540,7 @@ export class KnowledgeGraph {
|
|
|
522
540
|
* Find all nodes that a given node calls (callees / "what does this call?").
|
|
523
541
|
*/
|
|
524
542
|
findCallees(nodeId) {
|
|
525
|
-
const rows = this.
|
|
543
|
+
const rows = this.stmt(`
|
|
526
544
|
SELECT n.* FROM nodes n
|
|
527
545
|
JOIN edges e ON n.id = e.targetId
|
|
528
546
|
WHERE e.sourceId = ? AND e.type = 'calls'
|
|
@@ -536,44 +554,26 @@ export class KnowledgeGraph {
|
|
|
536
554
|
* @param maxDepth - Maximum traversal depth (default 10)
|
|
537
555
|
*/
|
|
538
556
|
findAncestors(nodeId, maxDepth = 10) {
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
SELECT e.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
// contains: sourceId=parent, targetId=child → find parent via targetId=current
|
|
562
|
-
const containsEdges = this.db.prepare(`
|
|
563
|
-
SELECT e.sourceId FROM edges e
|
|
564
|
-
WHERE e.targetId = ? AND e.type = 'contains'
|
|
565
|
-
`).all(current.id);
|
|
566
|
-
for (const { sourceId } of containsEdges) {
|
|
567
|
-
if (!visited.has(sourceId)) {
|
|
568
|
-
const node = this.getNode(sourceId);
|
|
569
|
-
if (node) {
|
|
570
|
-
result.push(node);
|
|
571
|
-
queue.push({ id: sourceId, depth: current.depth + 1 });
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
return result;
|
|
557
|
+
// Single recursive CTE replaces N+1 JavaScript BFS loop
|
|
558
|
+
const rows = this.db.prepare(`
|
|
559
|
+
WITH RECURSIVE anc(id, depth) AS (
|
|
560
|
+
SELECT @nodeId, 0
|
|
561
|
+
UNION
|
|
562
|
+
SELECT e.targetId, anc.depth + 1
|
|
563
|
+
FROM edges e
|
|
564
|
+
JOIN anc ON e.sourceId = anc.id
|
|
565
|
+
WHERE anc.depth < @maxDepth AND e.type IN ('inherits', 'implements')
|
|
566
|
+
UNION
|
|
567
|
+
SELECT e.sourceId, anc.depth + 1
|
|
568
|
+
FROM edges e
|
|
569
|
+
JOIN anc ON e.targetId = anc.id
|
|
570
|
+
WHERE anc.depth < @maxDepth AND e.type = 'contains'
|
|
571
|
+
)
|
|
572
|
+
SELECT DISTINCT n.* FROM nodes n
|
|
573
|
+
JOIN anc ON n.id = anc.id
|
|
574
|
+
WHERE n.id != @nodeId
|
|
575
|
+
`).all({ nodeId, maxDepth });
|
|
576
|
+
return rows.map((r) => this.rowToNode(r));
|
|
577
577
|
}
|
|
578
578
|
/**
|
|
579
579
|
* Find descendants (child classes, implementors, contained members) — traverse downward.
|
|
@@ -582,44 +582,26 @@ export class KnowledgeGraph {
|
|
|
582
582
|
* @param maxDepth - Maximum traversal depth (default 10)
|
|
583
583
|
*/
|
|
584
584
|
findDescendants(nodeId, maxDepth = 10) {
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
SELECT e.
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
// inherits/implements: sourceId=child, targetId=parent → find children via targetId=current
|
|
608
|
-
const inheritEdges = this.db.prepare(`
|
|
609
|
-
SELECT e.sourceId FROM edges e
|
|
610
|
-
WHERE e.targetId = ? AND e.type IN ('inherits', 'implements')
|
|
611
|
-
`).all(current.id);
|
|
612
|
-
for (const { sourceId } of inheritEdges) {
|
|
613
|
-
if (!visited.has(sourceId)) {
|
|
614
|
-
const node = this.getNode(sourceId);
|
|
615
|
-
if (node) {
|
|
616
|
-
result.push(node);
|
|
617
|
-
queue.push({ id: sourceId, depth: current.depth + 1 });
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
return result;
|
|
585
|
+
// Single recursive CTE replaces N+1 JavaScript BFS loop
|
|
586
|
+
const rows = this.db.prepare(`
|
|
587
|
+
WITH RECURSIVE desc_tree(id, depth) AS (
|
|
588
|
+
SELECT @nodeId, 0
|
|
589
|
+
UNION
|
|
590
|
+
SELECT e.targetId, desc_tree.depth + 1
|
|
591
|
+
FROM edges e
|
|
592
|
+
JOIN desc_tree ON e.sourceId = desc_tree.id
|
|
593
|
+
WHERE desc_tree.depth < @maxDepth AND e.type = 'contains'
|
|
594
|
+
UNION
|
|
595
|
+
SELECT e.sourceId, desc_tree.depth + 1
|
|
596
|
+
FROM edges e
|
|
597
|
+
JOIN desc_tree ON e.targetId = desc_tree.id
|
|
598
|
+
WHERE desc_tree.depth < @maxDepth AND e.type IN ('inherits', 'implements')
|
|
599
|
+
)
|
|
600
|
+
SELECT DISTINCT n.* FROM nodes n
|
|
601
|
+
JOIN desc_tree ON n.id = desc_tree.id
|
|
602
|
+
WHERE n.id != @nodeId
|
|
603
|
+
`).all({ nodeId, maxDepth });
|
|
604
|
+
return rows.map((r) => this.rowToNode(r));
|
|
623
605
|
}
|
|
624
606
|
/**
|
|
625
607
|
* Blast radius analysis: find all nodes transitively affected if a node changes.
|
|
@@ -631,32 +613,23 @@ export class KnowledgeGraph {
|
|
|
631
613
|
* @returns All nodes that depend on the changed node
|
|
632
614
|
*/
|
|
633
615
|
blastRadius(nodeId, maxDepth = 5) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const node = this.getNode(sourceId);
|
|
652
|
-
if (node) {
|
|
653
|
-
result.push(node);
|
|
654
|
-
queue.push({ id: sourceId, depth: current.depth + 1 });
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
return result;
|
|
616
|
+
// Single recursive CTE replaces N+1 JavaScript BFS loop
|
|
617
|
+
// Traces reverse dependencies: who depends on this node, transitively
|
|
618
|
+
const rows = this.db.prepare(`
|
|
619
|
+
WITH RECURSIVE blast(id, depth) AS (
|
|
620
|
+
SELECT @nodeId, 0
|
|
621
|
+
UNION
|
|
622
|
+
SELECT e.sourceId, blast.depth + 1
|
|
623
|
+
FROM edges e
|
|
624
|
+
JOIN blast ON e.targetId = blast.id
|
|
625
|
+
WHERE blast.depth < @maxDepth
|
|
626
|
+
AND e.type IN ('calls','imports','inherits','implements','uses','depends_on')
|
|
627
|
+
)
|
|
628
|
+
SELECT DISTINCT n.* FROM nodes n
|
|
629
|
+
JOIN blast ON n.id = blast.id
|
|
630
|
+
WHERE n.id != @nodeId
|
|
631
|
+
`).all({ nodeId, maxDepth });
|
|
632
|
+
return rows.map((r) => this.rowToNode(r));
|
|
660
633
|
}
|
|
661
634
|
// ============================================================
|
|
662
635
|
// Full-Text Search
|
|
@@ -828,14 +801,14 @@ export class KnowledgeGraph {
|
|
|
828
801
|
* Get the content hash for a file node (used for change detection).
|
|
829
802
|
*/
|
|
830
803
|
getFileHash(filePath) {
|
|
831
|
-
const row = this.
|
|
804
|
+
const row = this.stmt("SELECT hash FROM nodes WHERE filePath = ? AND type = 'file' LIMIT 1").get(filePath);
|
|
832
805
|
return row?.hash ?? null;
|
|
833
806
|
}
|
|
834
807
|
/**
|
|
835
808
|
* Get all indexed file paths.
|
|
836
809
|
*/
|
|
837
810
|
getIndexedFiles() {
|
|
838
|
-
const rows = this.
|
|
811
|
+
const rows = this.stmt("SELECT DISTINCT filePath FROM nodes WHERE type = 'file' ORDER BY filePath").all();
|
|
839
812
|
return rows.map(r => r.filePath);
|
|
840
813
|
}
|
|
841
814
|
/**
|
|
@@ -895,25 +868,32 @@ export class KnowledgeGraph {
|
|
|
895
868
|
* Get graph statistics.
|
|
896
869
|
*/
|
|
897
870
|
getStats() {
|
|
898
|
-
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
871
|
+
// Return cached stats if <5 seconds old (avoids 6 COUNT queries per tool response)
|
|
872
|
+
const now = Date.now();
|
|
873
|
+
if (this._statsCache && now - this._statsCache.time < 5000) {
|
|
874
|
+
return this._statsCache.data;
|
|
875
|
+
}
|
|
876
|
+
const totalNodes = this.stmt('SELECT COUNT(*) as count FROM nodes').get().count;
|
|
877
|
+
const totalEdges = this.stmt('SELECT COUNT(*) as count FROM edges').get().count;
|
|
878
|
+
const totalFiles = this.stmt("SELECT COUNT(*) as count FROM nodes WHERE type = 'file'").get().count;
|
|
879
|
+
const nodesByTypeRows = this.stmt('SELECT type, COUNT(*) as count FROM nodes GROUP BY type').all();
|
|
902
880
|
const nodesByType = {};
|
|
903
881
|
for (const { type, count } of nodesByTypeRows) {
|
|
904
882
|
nodesByType[type] = count;
|
|
905
883
|
}
|
|
906
|
-
const edgesByTypeRows = this.
|
|
884
|
+
const edgesByTypeRows = this.stmt('SELECT type, COUNT(*) as count FROM edges GROUP BY type').all();
|
|
907
885
|
const edgesByType = {};
|
|
908
886
|
for (const { type, count } of edgesByTypeRows) {
|
|
909
887
|
edgesByType[type] = count;
|
|
910
888
|
}
|
|
911
|
-
const langRows = this.
|
|
889
|
+
const langRows = this.stmt("SELECT language, COUNT(*) as count FROM nodes WHERE type = 'file' GROUP BY language").all();
|
|
912
890
|
const languageBreakdown = {};
|
|
913
891
|
for (const { language, count } of langRows) {
|
|
914
892
|
languageBreakdown[language] = count;
|
|
915
893
|
}
|
|
916
|
-
|
|
894
|
+
const result = { totalNodes, totalEdges, totalFiles, nodesByType, edgesByType, languageBreakdown };
|
|
895
|
+
this._statsCache = { data: result, time: now };
|
|
896
|
+
return result;
|
|
917
897
|
}
|
|
918
898
|
// ============================================================
|
|
919
899
|
// Bulk Operations
|
|
@@ -930,6 +910,8 @@ export class KnowledgeGraph {
|
|
|
930
910
|
this.upsertNodes(nodes);
|
|
931
911
|
this.upsertEdges(edges);
|
|
932
912
|
})();
|
|
913
|
+
this.adjDirty = true;
|
|
914
|
+
this._statsCache = null;
|
|
933
915
|
}
|
|
934
916
|
/**
|
|
935
917
|
* Replace data for MANY files in a single transaction.
|
|
@@ -1013,6 +995,8 @@ export class KnowledgeGraph {
|
|
|
1013
995
|
}
|
|
1014
996
|
}
|
|
1015
997
|
})();
|
|
998
|
+
this.adjDirty = true;
|
|
999
|
+
this._statsCache = null;
|
|
1016
1000
|
}
|
|
1017
1001
|
/**
|
|
1018
1002
|
* Optimized insert for full reindex (no existing data, skip conflict checks).
|
|
@@ -1075,13 +1059,14 @@ export class KnowledgeGraph {
|
|
|
1075
1059
|
}
|
|
1076
1060
|
}
|
|
1077
1061
|
})();
|
|
1062
|
+
this.adjDirty = true;
|
|
1063
|
+
this._statsCache = null;
|
|
1078
1064
|
}
|
|
1079
1065
|
/**
|
|
1080
1066
|
* Get all node IDs in the graph (used for PageRank).
|
|
1081
1067
|
*/
|
|
1082
1068
|
getAllNodeIds() {
|
|
1083
|
-
|
|
1084
|
-
return rows.map(r => r.id);
|
|
1069
|
+
return this.stmt('SELECT id FROM nodes').pluck().all();
|
|
1085
1070
|
}
|
|
1086
1071
|
/**
|
|
1087
1072
|
* Get all edges in the graph (used for PageRank adjacency matrix).
|
|
@@ -1122,6 +1107,8 @@ export class KnowledgeGraph {
|
|
|
1122
1107
|
this.db.prepare('DELETE FROM edges').run();
|
|
1123
1108
|
this.db.prepare('DELETE FROM nodes').run();
|
|
1124
1109
|
})();
|
|
1110
|
+
this.adjDirty = true;
|
|
1111
|
+
this._statsCache = null;
|
|
1125
1112
|
}
|
|
1126
1113
|
/**
|
|
1127
1114
|
* Clear only nodes and edges belonging to files under a specific project root.
|
|
@@ -1150,6 +1137,8 @@ export class KnowledgeGraph {
|
|
|
1150
1137
|
this.db.prepare('DELETE FROM file_index WHERE file_path LIKE ? OR file_path LIKE ?').run(`${prefix}%`, `${altPrefix}%`);
|
|
1151
1138
|
return del.changes;
|
|
1152
1139
|
})();
|
|
1140
|
+
this.adjDirty = true;
|
|
1141
|
+
this._statsCache = null;
|
|
1153
1142
|
return result;
|
|
1154
1143
|
}
|
|
1155
1144
|
// ============================================================
|
|
@@ -1248,7 +1237,7 @@ export class KnowledgeGraph {
|
|
|
1248
1237
|
* Get stored file index entry for staleness comparison.
|
|
1249
1238
|
*/
|
|
1250
1239
|
getFileIndexEntry(filePath) {
|
|
1251
|
-
return this.
|
|
1240
|
+
return this.stmt('SELECT mtime_ms, size_bytes, content_hash, indexed_at FROM file_index WHERE file_path = ?').get(filePath) ?? null;
|
|
1252
1241
|
}
|
|
1253
1242
|
/**
|
|
1254
1243
|
* Upsert a file index entry after indexing.
|
|
@@ -1275,7 +1264,7 @@ export class KnowledgeGraph {
|
|
|
1275
1264
|
* Count tracked files in file_index.
|
|
1276
1265
|
*/
|
|
1277
1266
|
getFileIndexCount() {
|
|
1278
|
-
return this.
|
|
1267
|
+
return this.stmt('SELECT COUNT(*) as c FROM file_index').get()?.c ?? 0;
|
|
1279
1268
|
}
|
|
1280
1269
|
/**
|
|
1281
1270
|
* Remove orphaned edges (edges pointing to non-existent nodes).
|
|
@@ -1328,9 +1317,12 @@ export class KnowledgeGraph {
|
|
|
1328
1317
|
this.db.exec('DROP INDEX IF EXISTS idx_nodes_name');
|
|
1329
1318
|
this.db.exec('DROP INDEX IF EXISTS idx_nodes_language');
|
|
1330
1319
|
this.db.exec('DROP INDEX IF EXISTS idx_nodes_hash');
|
|
1320
|
+
this.db.exec('DROP INDEX IF EXISTS idx_edges_source_type');
|
|
1321
|
+
this.db.exec('DROP INDEX IF EXISTS idx_edges_target_type');
|
|
1322
|
+
this.db.exec('DROP INDEX IF EXISTS idx_edges_type');
|
|
1323
|
+
// Also drop old single-column indexes if they exist from previous versions
|
|
1331
1324
|
this.db.exec('DROP INDEX IF EXISTS idx_edges_sourceId');
|
|
1332
1325
|
this.db.exec('DROP INDEX IF EXISTS idx_edges_targetId');
|
|
1333
|
-
this.db.exec('DROP INDEX IF EXISTS idx_edges_type');
|
|
1334
1326
|
}
|
|
1335
1327
|
/** Recreate all performance indexes */
|
|
1336
1328
|
recreateIndexes() {
|
|
@@ -1339,8 +1331,8 @@ export class KnowledgeGraph {
|
|
|
1339
1331
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name)');
|
|
1340
1332
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_language ON nodes(language)');
|
|
1341
1333
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_hash ON nodes(hash)');
|
|
1342
|
-
this.db.exec('CREATE INDEX IF NOT EXISTS
|
|
1343
|
-
this.db.exec('CREATE INDEX IF NOT EXISTS
|
|
1334
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_edges_source_type ON edges(sourceId, type)');
|
|
1335
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_edges_target_type ON edges(targetId, type)');
|
|
1344
1336
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type)');
|
|
1345
1337
|
}
|
|
1346
1338
|
/** Enter bulk insert mode - relaxes safety for maximum speed */
|
|
@@ -1355,6 +1347,7 @@ export class KnowledgeGraph {
|
|
|
1355
1347
|
}
|
|
1356
1348
|
/** Exit bulk insert mode - restores safety and rebuilds indexes/FTS */
|
|
1357
1349
|
exitBulkMode() {
|
|
1350
|
+
this.invalidateAdjCache();
|
|
1358
1351
|
this.recreateIndexes();
|
|
1359
1352
|
this.recreateFtsTriggers();
|
|
1360
1353
|
this.rebuildFts();
|
|
@@ -1365,6 +1358,92 @@ export class KnowledgeGraph {
|
|
|
1365
1358
|
this.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
1366
1359
|
this.db.pragma('wal_autocheckpoint = 1000');
|
|
1367
1360
|
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Build in-memory adjacency cache for ultra-fast graph traversal.
|
|
1363
|
+
* Called automatically on first traversal query after index.
|
|
1364
|
+
* ~2MB RAM for 13K edges — negligible.
|
|
1365
|
+
*/
|
|
1366
|
+
buildAdjacencyCache() {
|
|
1367
|
+
this.adjOut.clear();
|
|
1368
|
+
this.adjIn.clear();
|
|
1369
|
+
const rows = this.stmt('SELECT sourceId, targetId, type FROM edges').all();
|
|
1370
|
+
for (const r of rows) {
|
|
1371
|
+
// Forward: sourceId → type → [targetIds]
|
|
1372
|
+
if (!this.adjOut.has(r.sourceId))
|
|
1373
|
+
this.adjOut.set(r.sourceId, new Map());
|
|
1374
|
+
const fwd = this.adjOut.get(r.sourceId);
|
|
1375
|
+
if (!fwd.has(r.type))
|
|
1376
|
+
fwd.set(r.type, []);
|
|
1377
|
+
fwd.get(r.type).push(r.targetId);
|
|
1378
|
+
// Reverse: targetId → type → [sourceIds]
|
|
1379
|
+
if (!this.adjIn.has(r.targetId))
|
|
1380
|
+
this.adjIn.set(r.targetId, new Map());
|
|
1381
|
+
const rev = this.adjIn.get(r.targetId);
|
|
1382
|
+
if (!rev.has(r.type))
|
|
1383
|
+
rev.set(r.type, []);
|
|
1384
|
+
rev.get(r.type).push(r.sourceId);
|
|
1385
|
+
}
|
|
1386
|
+
this.adjDirty = false;
|
|
1387
|
+
}
|
|
1388
|
+
/** Invalidate adjacency cache (call after any edge mutation) */
|
|
1389
|
+
invalidateAdjCache() {
|
|
1390
|
+
this.adjDirty = true;
|
|
1391
|
+
this._statsCache = null;
|
|
1392
|
+
this._stmtCache.clear(); // Also clear stmt cache on schema changes
|
|
1393
|
+
}
|
|
1394
|
+
/** Get callers from adjacency cache (in-memory, sub-microsecond) */
|
|
1395
|
+
getCallersFromCache(nodeId) {
|
|
1396
|
+
if (this.adjDirty)
|
|
1397
|
+
this.buildAdjacencyCache();
|
|
1398
|
+
return this.adjIn.get(nodeId)?.get('calls') ?? [];
|
|
1399
|
+
}
|
|
1400
|
+
/** Get callees from adjacency cache (in-memory, sub-microsecond) */
|
|
1401
|
+
getCalleesFromCache(nodeId) {
|
|
1402
|
+
if (this.adjDirty)
|
|
1403
|
+
this.buildAdjacencyCache();
|
|
1404
|
+
return this.adjOut.get(nodeId)?.get('calls') ?? [];
|
|
1405
|
+
}
|
|
1406
|
+
/** Get reverse dependencies from adjacency cache for blast radius */
|
|
1407
|
+
getReverseDepsFromCache(nodeId, types) {
|
|
1408
|
+
if (this.adjDirty)
|
|
1409
|
+
this.buildAdjacencyCache();
|
|
1410
|
+
const byType = this.adjIn.get(nodeId);
|
|
1411
|
+
if (!byType)
|
|
1412
|
+
return [];
|
|
1413
|
+
const result = [];
|
|
1414
|
+
for (const t of types) {
|
|
1415
|
+
const ids = byType.get(t);
|
|
1416
|
+
if (ids)
|
|
1417
|
+
result.push(...ids);
|
|
1418
|
+
}
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Shortest path between two nodes via dependency edges.
|
|
1423
|
+
* Returns the path as an array of node IDs, or empty if no path exists.
|
|
1424
|
+
*/
|
|
1425
|
+
shortestPath(startId, endId, maxDepth = 10) {
|
|
1426
|
+
try {
|
|
1427
|
+
const rows = this.db.prepare(`
|
|
1428
|
+
WITH RECURSIVE path(id, depth, route) AS (
|
|
1429
|
+
SELECT ?1, 0, ?1
|
|
1430
|
+
UNION ALL
|
|
1431
|
+
SELECT e.targetId, path.depth + 1, path.route || '>' || e.targetId
|
|
1432
|
+
FROM edges e
|
|
1433
|
+
JOIN path ON e.sourceId = path.id
|
|
1434
|
+
WHERE path.depth < ?3
|
|
1435
|
+
AND e.type IN ('calls','imports','inherits','implements','uses','depends_on')
|
|
1436
|
+
AND instr(path.route, e.targetId) = 0
|
|
1437
|
+
)
|
|
1438
|
+
SELECT route FROM path WHERE id = ?2
|
|
1439
|
+
ORDER BY depth LIMIT 1
|
|
1440
|
+
`).get(startId, endId, maxDepth);
|
|
1441
|
+
return rows ? rows.route.split('>') : [];
|
|
1442
|
+
}
|
|
1443
|
+
catch {
|
|
1444
|
+
return [];
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1368
1447
|
/**
|
|
1369
1448
|
* Close the database connection.
|
|
1370
1449
|
*/
|