@codragraph/cli 2.1.5 → 2.2.0-rc.6
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 +18 -13
- package/dist/cli/analyze.d.ts +9 -4
- package/dist/cli/analyze.js +37 -13
- package/dist/cli/graphpack.d.ts +48 -0
- package/dist/cli/graphpack.js +217 -0
- package/dist/cli/index.js +81 -3
- package/dist/cli/status.d.ts +1 -1
- package/dist/cli/status.js +8 -0
- package/dist/cli/tool.d.ts +11 -2
- package/dist/cli/tool.js +138 -8
- package/dist/core/adaptive-profile.d.ts +52 -0
- package/dist/core/adaptive-profile.js +180 -0
- package/dist/core/cgdb/cgdb-adapter.d.ts +34 -5
- package/dist/core/cgdb/cgdb-adapter.js +418 -5
- package/dist/core/cgdb/pool-adapter.js +1 -1
- package/dist/core/graphpack/index.d.ts +14 -0
- package/dist/core/graphpack/index.js +474 -0
- package/dist/core/graphpack/types.d.ts +129 -0
- package/dist/core/graphpack/types.js +4 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +3 -1
- package/dist/core/ingestion/pipeline-phases/structure.js +19 -3
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/run-analyze.d.ts +27 -2
- package/dist/core/run-analyze.js +598 -27
- package/dist/core/search/bm25-index.d.ts +19 -0
- package/dist/core/search/bm25-index.js +68 -29
- package/dist/core/semantic/relationships.d.ts +36 -0
- package/dist/core/semantic/relationships.js +261 -0
- package/dist/mcp/local/local-backend.js +48 -3
- package/dist/mcp/resources.js +125 -0
- package/dist/mcp/tools.js +105 -0
- package/dist/server/api.js +112 -0
- package/dist/storage/repo-manager.d.ts +29 -0
- package/dist/web/assets/agent-CQNZQ-hg.js +1139 -0
- package/dist/web/assets/architectureDiagram-UL44E2DR-B5_goS_i.js +36 -0
- package/dist/web/assets/blockDiagram-7IZFK4PR-D7ZAlDyv.js +132 -0
- package/dist/web/assets/{c4Diagram-DFAF54RM-C4Hl3J2U.js → c4Diagram-Y2BXMSZH-Djcgm_54.js} +1 -1
- package/dist/web/assets/{chunk-7RZVMHOQ-BitYcNVR.js → chunk-3SSMPTDK-Cv2Zy2FO.js} +1 -1
- package/dist/web/assets/{chunk-TBF5ZNIQ-DL5stGM1.js → chunk-6764PJDD-Cppb-jH-.js} +1 -1
- package/dist/web/assets/{chunk-KSICW3F5-BYzvDLNI.js → chunk-AZZRMDJM-BHlLC7p3.js} +1 -1
- package/dist/web/assets/{chunk-AEOMTBSW-BgTIXPsY.js → chunk-JQRUD6KW-3F8Zg-1N.js} +1 -1
- package/dist/web/assets/chunk-KRXBNO2N-C0mbN9a7.js +1 -0
- package/dist/web/assets/chunk-LCXTWHL2-BoiuJpIF.js +231 -0
- package/dist/web/assets/{chunk-O5ABG6QK-dHwHzA6n.js → chunk-LII3EMHJ-Dqq0Qguw.js} +1 -1
- package/dist/web/assets/chunk-RG4AUYOV-Bl5F_gDs.js +206 -0
- package/dist/web/assets/{chunk-TU3PZOEN-RLyvLcv-.js → chunk-T5OCTHI4-B2tIcggA.js} +1 -1
- package/dist/web/assets/chunk-W44A43WB-BHe37iN7.js +13 -0
- package/dist/web/assets/{chunk-RWUO3TPN-BgRTY0_k.js → chunk-ZXARS5L4-wcrIaQvY.js} +1 -1
- package/dist/web/assets/classDiagram-KGZ6W3CR-IbI6v_24.js +1 -0
- package/dist/web/assets/classDiagram-v2-72OJOZXJ-IbI6v_24.js +1 -0
- package/dist/web/assets/{cose-bilkent-PNC4W37J-DVhePRYg.js → cose-bilkent-UX7MHV2Q-BWr7v0Wr.js} +1 -1
- package/dist/web/assets/dagre-ND4H6XIP-De5LIh1B.js +4 -0
- package/dist/web/assets/diagram-3NCE3AQN-Dd22FSHy.js +43 -0
- package/dist/web/assets/diagram-GF46GFSD-Cev3THY8.js +24 -0
- package/dist/web/assets/diagram-HNR7UZ2L-D8Z8RQGs.js +3 -0
- package/dist/web/assets/diagram-QXG6HAR7-B8VOJOiE.js +24 -0
- package/dist/web/assets/diagram-WEQXMOUZ-va1bLoMD.js +10 -0
- package/dist/web/assets/{erDiagram-GCSMX5X6-C3dhDFA8.js → erDiagram-L5TCEMPS-B3_9uAoP.js} +5 -5
- package/dist/web/assets/{flowDiagram-OTCZ4VVT-CWSFWmhr.js → flowDiagram-H6V6AXG4-98m6maI1.js} +9 -9
- package/dist/web/assets/ganttDiagram-JCBTUEKG-vE2nzETb.js +292 -0
- package/dist/web/assets/gitGraphDiagram-S2ZK5IYY-DKc8uUg_.js +106 -0
- package/dist/web/assets/index-BAhe1HSk.css +1 -0
- package/dist/web/assets/index-VTKdaklA.js +1415 -0
- package/dist/web/assets/infoDiagram-3YFTVSEB-DYP-Srzx.js +2 -0
- package/dist/web/assets/{ishikawaDiagram-YMYX4NHK-DUoJvNP2.js → ishikawaDiagram-BNXS4ZKH-QZnkpmmb.js} +3 -3
- package/dist/web/assets/{journeyDiagram-SO5T7YLQ-RMFPNNqz.js → journeyDiagram-M6C3CM3L-B5ojIuqu.js} +1 -1
- package/dist/web/assets/{kanban-definition-LJHFXRCJ-BzpDs1K9.js → kanban-definition-75IXJCU3-BJA8liRR.js} +4 -4
- package/dist/web/assets/{katex-GD7MH7QM-DBQvrix-.js → katex-K3KEBU37-DUqZiCRL.js} +1 -1
- package/dist/web/assets/mindmap-definition-2TDM6QVE-BQj5yylD.js +96 -0
- package/dist/web/assets/pieDiagram-CU6KROY3-4eSrPiQz.js +30 -0
- package/dist/web/assets/quadrantDiagram-VICAPDV7-PzxN8j55.js +7 -0
- package/dist/web/assets/{requirementDiagram-M5DCFWZL-DLHOVTSv.js → requirementDiagram-JXO7QTGE-CtplTc5y.js} +2 -2
- package/dist/web/assets/sankeyDiagram-URQDO5SZ-CoSgvkxv.js +40 -0
- package/dist/web/assets/sequenceDiagram-VS2MUI6T-D7ygyXvJ.js +162 -0
- package/dist/web/assets/stateDiagram-7D4R322I-v01gvwji.js +1 -0
- package/dist/web/assets/stateDiagram-v2-36443NZ5-DFD2b8_x.js +1 -0
- package/dist/web/assets/{timeline-definition-5SPVSISX-TRSDRgPw.js → timeline-definition-O6YCAMPW-CTI3M65J.js} +4 -4
- package/dist/web/assets/{vennDiagram-IE5QUKF5-DNy7HRBM.js → vennDiagram-MWXL3ELB-RnB0XMP7.js} +6 -6
- package/dist/web/assets/wardley-L42UT6IY-5TKZOOLJ-C-ZcgEBb.js +173 -0
- package/dist/web/assets/wardleyDiagram-CUQ6CDDI-EwRi4kwo.js +78 -0
- package/dist/web/assets/{xychartDiagram-ZHJ5623Y-Dr9r7a35.js → xychartDiagram-N2JHSOCM-DA38II6y.js} +4 -4
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/vendor/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
- package/dist/web/assets/agent-D5lb0zXz.js +0 -1089
- package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +0 -36
- package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +0 -132
- package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +0 -231
- package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +0 -1
- package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +0 -13
- package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +0 -1
- package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +0 -206
- package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +0 -1
- package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +0 -1
- package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +0 -4
- package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +0 -43
- package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +0 -24
- package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +0 -10
- package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +0 -24
- package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +0 -292
- package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +0 -106
- package/dist/web/assets/index-BgeqpYgd.js +0 -1415
- package/dist/web/assets/index-CT0GtFLZ.css +0 -1
- package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +0 -2
- package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +0 -96
- package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +0 -30
- package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +0 -7
- package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +0 -10
- package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +0 -157
- package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +0 -1
- package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +0 -1
- package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +0 -161
- package/dist/web/assets/wardleyDiagram-XU3VSMPF-BP-r1xzR.js +0 -20
|
@@ -5,6 +5,7 @@ import { once } from 'events';
|
|
|
5
5
|
import { finished } from 'stream/promises';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import cgdb from '@ladybugdb/core';
|
|
8
|
+
import { createKnowledgeGraph } from '../graph/graph.js';
|
|
8
9
|
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, STALE_HASH_SENTINEL, } from './schema.js';
|
|
9
10
|
import { streamAllCSVsToDisk } from './csv-generator.js';
|
|
10
11
|
/**
|
|
@@ -432,6 +433,419 @@ export const loadGraphToCgdb = async (graph, repoPath, storagePath, onProgress,
|
|
|
432
433
|
catch { }
|
|
433
434
|
return { success: true, insertedRels, skippedRels, warnings };
|
|
434
435
|
};
|
|
436
|
+
const FILE_SCOPED_NODE_TABLES = NODE_TABLES.filter((table) => table !== 'Community' && table !== 'Process' && table !== 'FeatureCluster');
|
|
437
|
+
const GLOBAL_NODE_TABLES = new Set(['Community', 'Process', 'FeatureCluster']);
|
|
438
|
+
const PRESERVABLE_OUTGOING_REL_TYPES = new Set([
|
|
439
|
+
'MEMBER_OF',
|
|
440
|
+
'STEP_IN_PROCESS',
|
|
441
|
+
'ENTRY_POINT_OF',
|
|
442
|
+
'FEATURE_MEMBER_OF',
|
|
443
|
+
]);
|
|
444
|
+
const rowString = (row, named, positional) => String(row[named] ?? row[positional] ?? '');
|
|
445
|
+
const rowNumber = (row, named, positional, fallback) => {
|
|
446
|
+
const value = Number(row[named] ?? row[positional] ?? fallback);
|
|
447
|
+
return Number.isFinite(value) ? value : fallback;
|
|
448
|
+
};
|
|
449
|
+
const nodeLabelForId = (nodeId) => {
|
|
450
|
+
if (nodeId.startsWith('comm_'))
|
|
451
|
+
return 'Community';
|
|
452
|
+
if (nodeId.startsWith('proc_'))
|
|
453
|
+
return 'Process';
|
|
454
|
+
return nodeId.split(':')[0] || 'CodeElement';
|
|
455
|
+
};
|
|
456
|
+
const fileScopedNodeSignature = (label, name, filePath) => {
|
|
457
|
+
if (!name || !filePath)
|
|
458
|
+
return null;
|
|
459
|
+
return `${label}\0${name}\0${filePath}`;
|
|
460
|
+
};
|
|
461
|
+
const normalizePathAliases = (aliases) => {
|
|
462
|
+
if (!aliases)
|
|
463
|
+
return new Map();
|
|
464
|
+
if (aliases instanceof Map) {
|
|
465
|
+
return new Map([...aliases].map(([from, to]) => [from.replace(/\\/g, '/'), to.replace(/\\/g, '/')]));
|
|
466
|
+
}
|
|
467
|
+
return new Map(Object.entries(aliases).map(([from, to]) => [
|
|
468
|
+
from.replace(/\\/g, '/'),
|
|
469
|
+
String(to).replace(/\\/g, '/'),
|
|
470
|
+
]));
|
|
471
|
+
};
|
|
472
|
+
const remapFilePath = (filePath, aliases) => {
|
|
473
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
474
|
+
return aliases.get(normalized) ?? normalized;
|
|
475
|
+
};
|
|
476
|
+
const collectNodeIdsForFiles = async (filePaths) => {
|
|
477
|
+
const nodeIds = new Set();
|
|
478
|
+
if (filePaths.length === 0)
|
|
479
|
+
return nodeIds;
|
|
480
|
+
for (const table of FILE_SCOPED_NODE_TABLES) {
|
|
481
|
+
for (const filePath of filePaths) {
|
|
482
|
+
try {
|
|
483
|
+
const rows = await executePrepared(`MATCH (n:${escapeTableName(table)}) WHERE n.filePath = $filePath RETURN n.id AS id`, { filePath });
|
|
484
|
+
for (const row of rows) {
|
|
485
|
+
const id = rowString(row, 'id', 0);
|
|
486
|
+
if (id)
|
|
487
|
+
nodeIds.add(id);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
/* table may be absent in old indexes */
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return nodeIds;
|
|
496
|
+
};
|
|
497
|
+
const buildPatchNodeLookup = (graph, filePaths) => {
|
|
498
|
+
const ids = new Set();
|
|
499
|
+
const signatureBuckets = new Map();
|
|
500
|
+
for (const node of graph.iterNodes()) {
|
|
501
|
+
const filePath = node.properties?.filePath;
|
|
502
|
+
if (typeof filePath === 'string' && filePaths.has(filePath)) {
|
|
503
|
+
ids.add(node.id);
|
|
504
|
+
const signature = fileScopedNodeSignature(node.label, typeof node.properties?.name === 'string' ? node.properties.name : undefined, filePath);
|
|
505
|
+
if (signature) {
|
|
506
|
+
let bucket = signatureBuckets.get(signature);
|
|
507
|
+
if (bucket === undefined) {
|
|
508
|
+
bucket = new Set();
|
|
509
|
+
signatureBuckets.set(signature, bucket);
|
|
510
|
+
}
|
|
511
|
+
bucket.add(node.id);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const signatureToId = new Map();
|
|
516
|
+
for (const [signature, nodeIds] of signatureBuckets) {
|
|
517
|
+
if (nodeIds.size === 1) {
|
|
518
|
+
signatureToId.set(signature, [...nodeIds][0]);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { ids, signatureToId };
|
|
522
|
+
};
|
|
523
|
+
const prunePatchGraphRelationships = (graph, patchNodeIds) => {
|
|
524
|
+
for (const rel of [...graph.iterRelationships()]) {
|
|
525
|
+
if (!patchNodeIds.has(rel.sourceId) && !patchNodeIds.has(rel.targetId)) {
|
|
526
|
+
graph.removeRelationship(rel.id);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
const collectPreservedRelationships = async (oldNodeIds) => {
|
|
531
|
+
const preserved = [];
|
|
532
|
+
for (const nodeId of oldNodeIds) {
|
|
533
|
+
try {
|
|
534
|
+
const incoming = await executePrepared(`MATCH (a)-[r:${REL_TABLE_NAME}]->(b) WHERE b.id = $nodeId RETURN a.id AS sourceId, b.id AS targetId, r.type AS type, r.confidence AS confidence, r.reason AS reason, r.step AS step, a.name AS sourceName, a.filePath AS sourceFilePath, b.name AS targetName, b.filePath AS targetFilePath`, { nodeId });
|
|
535
|
+
for (const row of incoming) {
|
|
536
|
+
const sourceId = rowString(row, 'sourceId', 0);
|
|
537
|
+
const targetId = rowString(row, 'targetId', 1);
|
|
538
|
+
if (!sourceId || !targetId || oldNodeIds.has(sourceId))
|
|
539
|
+
continue;
|
|
540
|
+
preserved.push({
|
|
541
|
+
sourceId,
|
|
542
|
+
targetId,
|
|
543
|
+
type: rowString(row, 'type', 2),
|
|
544
|
+
confidence: rowNumber(row, 'confidence', 3, 1),
|
|
545
|
+
reason: rowString(row, 'reason', 4),
|
|
546
|
+
step: rowNumber(row, 'step', 5, 0),
|
|
547
|
+
sourceName: rowString(row, 'sourceName', 6),
|
|
548
|
+
sourceFilePath: rowString(row, 'sourceFilePath', 7),
|
|
549
|
+
targetName: rowString(row, 'targetName', 8),
|
|
550
|
+
targetFilePath: rowString(row, 'targetFilePath', 9),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
/* best-effort preservation */
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const outgoing = await executePrepared(`MATCH (a)-[r:${REL_TABLE_NAME}]->(b) WHERE a.id = $nodeId RETURN a.id AS sourceId, b.id AS targetId, r.type AS type, r.confidence AS confidence, r.reason AS reason, r.step AS step, a.name AS sourceName, a.filePath AS sourceFilePath, b.name AS targetName, b.filePath AS targetFilePath`, { nodeId });
|
|
559
|
+
for (const row of outgoing) {
|
|
560
|
+
const sourceId = rowString(row, 'sourceId', 0);
|
|
561
|
+
const targetId = rowString(row, 'targetId', 1);
|
|
562
|
+
const type = rowString(row, 'type', 2);
|
|
563
|
+
if (!sourceId ||
|
|
564
|
+
!targetId ||
|
|
565
|
+
oldNodeIds.has(targetId) ||
|
|
566
|
+
!PRESERVABLE_OUTGOING_REL_TYPES.has(type)) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
preserved.push({
|
|
570
|
+
sourceId,
|
|
571
|
+
targetId,
|
|
572
|
+
type,
|
|
573
|
+
confidence: rowNumber(row, 'confidence', 3, 1),
|
|
574
|
+
reason: rowString(row, 'reason', 4),
|
|
575
|
+
step: rowNumber(row, 'step', 5, 0),
|
|
576
|
+
sourceName: rowString(row, 'sourceName', 6),
|
|
577
|
+
sourceFilePath: rowString(row, 'sourceFilePath', 7),
|
|
578
|
+
targetName: rowString(row, 'targetName', 8),
|
|
579
|
+
targetFilePath: rowString(row, 'targetFilePath', 9),
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
/* best-effort preservation */
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return preserved;
|
|
588
|
+
};
|
|
589
|
+
const deleteEmbeddingsForNodeIds = async (nodeIds) => {
|
|
590
|
+
await executeWithReusedStatement(`MATCH (e:${EMBEDDING_TABLE_NAME} {nodeId: $nodeId}) DELETE e`, [...nodeIds].map((nodeId) => ({ nodeId })));
|
|
591
|
+
};
|
|
592
|
+
const deleteFileScopedNodes = async (filePaths) => {
|
|
593
|
+
for (const table of FILE_SCOPED_NODE_TABLES) {
|
|
594
|
+
for (const filePath of filePaths) {
|
|
595
|
+
try {
|
|
596
|
+
await executePrepared(`MATCH (n:${escapeTableName(table)}) WHERE n.filePath = $filePath DETACH DELETE n`, { filePath });
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
if (!isMissingColumnOrTableError(err instanceof Error ? err.message : String(err))) {
|
|
600
|
+
throw err;
|
|
601
|
+
}
|
|
602
|
+
/* table may be absent in old indexes */
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
const restorePreservedRelationships = async (relationships, patchLookup, pathAliases) => {
|
|
608
|
+
let restored = 0;
|
|
609
|
+
const seen = new Set();
|
|
610
|
+
for (const rel of relationships) {
|
|
611
|
+
const sourceSignature = fileScopedNodeSignature(nodeLabelForId(rel.sourceId), rel.sourceName, remapFilePath(rel.sourceFilePath, pathAliases));
|
|
612
|
+
const targetSignature = fileScopedNodeSignature(nodeLabelForId(rel.targetId), rel.targetName, remapFilePath(rel.targetFilePath, pathAliases));
|
|
613
|
+
const resolvedSourceId = (patchLookup.ids.has(rel.sourceId) ? rel.sourceId : undefined) ??
|
|
614
|
+
(sourceSignature ? patchLookup.signatureToId.get(sourceSignature) : undefined) ??
|
|
615
|
+
rel.sourceId;
|
|
616
|
+
const resolvedTargetId = (patchLookup.ids.has(rel.targetId) ? rel.targetId : undefined) ??
|
|
617
|
+
(targetSignature ? patchLookup.signatureToId.get(targetSignature) : undefined) ??
|
|
618
|
+
rel.targetId;
|
|
619
|
+
const patchSideSurvives = patchLookup.ids.has(resolvedSourceId) || patchLookup.ids.has(resolvedTargetId);
|
|
620
|
+
if (!patchSideSurvives)
|
|
621
|
+
continue;
|
|
622
|
+
const key = `${resolvedSourceId}\0${rel.type}\0${resolvedTargetId}\0${rel.step}`;
|
|
623
|
+
if (seen.has(key))
|
|
624
|
+
continue;
|
|
625
|
+
seen.add(key);
|
|
626
|
+
const sourceLabel = escapeTableName(nodeLabelForId(resolvedSourceId));
|
|
627
|
+
const targetLabel = escapeTableName(nodeLabelForId(resolvedTargetId));
|
|
628
|
+
try {
|
|
629
|
+
const existingRows = await executePrepared(`MATCH (a:${sourceLabel})-[r:${REL_TABLE_NAME}]->(b:${targetLabel}) WHERE a.id = $sourceId AND b.id = $targetId AND r.type = $type AND r.step = $step RETURN count(r) AS cnt`, { ...rel, sourceId: resolvedSourceId, targetId: resolvedTargetId });
|
|
630
|
+
const existing = rowNumber(existingRows?.[0] ?? {}, 'cnt', 0, 0);
|
|
631
|
+
if (existing > 0)
|
|
632
|
+
continue;
|
|
633
|
+
await executePrepared(`MATCH (a:${sourceLabel} {id: $sourceId}), (b:${targetLabel} {id: $targetId}) CREATE (a)-[:${REL_TABLE_NAME} {type: $type, confidence: $confidence, reason: $reason, step: $step}]->(b)`, { ...rel, sourceId: resolvedSourceId, targetId: resolvedTargetId });
|
|
634
|
+
restored++;
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
/* source or target disappeared; skip */
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return restored;
|
|
641
|
+
};
|
|
642
|
+
export const applyFileGraphPatchToCgdb = async (graph, repoPath, storagePath, filePaths, onProgress, options) => {
|
|
643
|
+
if (!conn) {
|
|
644
|
+
throw new Error('LadybugDB not initialized. Call initCgdb first.');
|
|
645
|
+
}
|
|
646
|
+
const normalizedFiles = [...new Set(filePaths.map((p) => p.replace(/\\/g, '/')))];
|
|
647
|
+
const pathAliases = normalizePathAliases(options?.pathAliases);
|
|
648
|
+
const patchFileSet = new Set(normalizedFiles);
|
|
649
|
+
const oldNodeIds = await collectNodeIdsForFiles(normalizedFiles);
|
|
650
|
+
const preservedRels = await collectPreservedRelationships(oldNodeIds);
|
|
651
|
+
await loadFTSExtension();
|
|
652
|
+
await deleteEmbeddingsForNodeIds(oldNodeIds);
|
|
653
|
+
await deleteFileScopedNodes(normalizedFiles);
|
|
654
|
+
const patchLookup = buildPatchNodeLookup(graph, patchFileSet);
|
|
655
|
+
prunePatchGraphRelationships(graph, patchLookup.ids);
|
|
656
|
+
const loadResult = graph.nodeCount > 0
|
|
657
|
+
? await loadGraphToCgdb(graph, repoPath, storagePath, onProgress, options)
|
|
658
|
+
: { insertedRels: 0 };
|
|
659
|
+
const restoredRels = await restorePreservedRelationships(preservedRels, patchLookup, pathAliases);
|
|
660
|
+
const prunedFolders = await pruneEmptyFoldersFromCgdb();
|
|
661
|
+
return {
|
|
662
|
+
replacedFiles: normalizedFiles.length,
|
|
663
|
+
deletedNodeIds: oldNodeIds.size,
|
|
664
|
+
insertedRels: loadResult.insertedRels,
|
|
665
|
+
restoredRels,
|
|
666
|
+
prunedFolders,
|
|
667
|
+
};
|
|
668
|
+
};
|
|
669
|
+
const deleteEmbeddingsForAllNodes = async () => {
|
|
670
|
+
try {
|
|
671
|
+
await executeQuery(`MATCH (e:${EMBEDDING_TABLE_NAME}) DELETE e`);
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
/* table may be absent in old indexes */
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
const deleteAllFileScopedNodes = async () => {
|
|
678
|
+
let deleted = 0;
|
|
679
|
+
for (const table of FILE_SCOPED_NODE_TABLES) {
|
|
680
|
+
try {
|
|
681
|
+
const rows = await executeQuery(`MATCH (n:${escapeTableName(table)}) RETURN count(n) AS cnt`);
|
|
682
|
+
deleted += Number(rows?.[0]?.cnt ?? rows?.[0]?.[0] ?? 0);
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
if (!isMissingColumnOrTableError(err instanceof Error ? err.message : String(err))) {
|
|
686
|
+
throw err;
|
|
687
|
+
}
|
|
688
|
+
/* table may be absent in old indexes */
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
await executeQuery(`MATCH (n:${escapeTableName(table)}) DETACH DELETE n`);
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
if (!isMissingColumnOrTableError(err instanceof Error ? err.message : String(err))) {
|
|
695
|
+
throw err;
|
|
696
|
+
}
|
|
697
|
+
/* table may be absent in old indexes */
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return deleted;
|
|
701
|
+
};
|
|
702
|
+
export const replaceFileScopedGraphInCgdb = async (graph, repoPath, storagePath, onProgress, options) => {
|
|
703
|
+
if (!conn) {
|
|
704
|
+
throw new Error('LadybugDB not initialized. Call initCgdb first.');
|
|
705
|
+
}
|
|
706
|
+
await loadFTSExtension();
|
|
707
|
+
await deleteEmbeddingsForAllNodes();
|
|
708
|
+
const deletedNodes = await deleteAllFileScopedNodes();
|
|
709
|
+
const loadResult = graph.nodeCount > 0
|
|
710
|
+
? await loadGraphToCgdb(graph, repoPath, storagePath, onProgress, options)
|
|
711
|
+
: { insertedRels: 0 };
|
|
712
|
+
return { deletedNodes, insertedRels: loadResult.insertedRels };
|
|
713
|
+
};
|
|
714
|
+
export const replaceGlobalGraphLayersInCgdb = async (graph, repoPath, storagePath, onProgress, options) => {
|
|
715
|
+
if (!conn) {
|
|
716
|
+
throw new Error('LadybugDB not initialized. Call initCgdb first.');
|
|
717
|
+
}
|
|
718
|
+
await loadFTSExtension();
|
|
719
|
+
let deletedGlobalNodes = 0;
|
|
720
|
+
for (const table of GLOBAL_NODE_TABLES) {
|
|
721
|
+
try {
|
|
722
|
+
const rows = await executeQuery(`MATCH (n:${escapeTableName(table)}) RETURN count(n) AS cnt`);
|
|
723
|
+
deletedGlobalNodes += Number(rows?.[0]?.cnt ?? rows?.[0]?.[0] ?? 0);
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
/* table may be absent in old indexes */
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
await executeQuery(`MATCH (n:${escapeTableName(table)}) DETACH DELETE n`);
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
/* table may be absent in old indexes */
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const loadResult = graph.nodeCount > 0
|
|
736
|
+
? await loadGraphToCgdb(graph, repoPath, storagePath, onProgress, options)
|
|
737
|
+
: { insertedRels: 0 };
|
|
738
|
+
return { deletedGlobalNodes, insertedRels: loadResult.insertedRels };
|
|
739
|
+
};
|
|
740
|
+
const unwrapNodeResult = (raw) => {
|
|
741
|
+
if (!raw || typeof raw !== 'object')
|
|
742
|
+
return null;
|
|
743
|
+
const row = raw;
|
|
744
|
+
const candidate = row.n ?? row[0];
|
|
745
|
+
return candidate && typeof candidate === 'object' && !Array.isArray(candidate)
|
|
746
|
+
? candidate
|
|
747
|
+
: null;
|
|
748
|
+
};
|
|
749
|
+
const nodePropertiesFromCgdbRow = (row) => {
|
|
750
|
+
const properties = {
|
|
751
|
+
name: String(row.name ?? ''),
|
|
752
|
+
filePath: String(row.filePath ?? ''),
|
|
753
|
+
};
|
|
754
|
+
for (const [key, value] of Object.entries(row)) {
|
|
755
|
+
if (key === 'id' || key === '_id' || key === '_label')
|
|
756
|
+
continue;
|
|
757
|
+
properties[key] = value;
|
|
758
|
+
}
|
|
759
|
+
return properties;
|
|
760
|
+
};
|
|
761
|
+
export const loadKnowledgeGraphFromCgdb = async (options = {}) => {
|
|
762
|
+
if (!conn) {
|
|
763
|
+
throw new Error('LadybugDB not initialized. Call initCgdb first.');
|
|
764
|
+
}
|
|
765
|
+
const includeGlobal = options.includeGlobal ?? false;
|
|
766
|
+
const graph = createKnowledgeGraph();
|
|
767
|
+
for (const table of NODE_TABLES) {
|
|
768
|
+
if (!includeGlobal && GLOBAL_NODE_TABLES.has(table))
|
|
769
|
+
continue;
|
|
770
|
+
try {
|
|
771
|
+
const rows = await executeQuery(`MATCH (n:${escapeTableName(table)}) RETURN n`);
|
|
772
|
+
for (const raw of rows) {
|
|
773
|
+
const node = unwrapNodeResult(raw);
|
|
774
|
+
const id = node ? String(node.id ?? '') : '';
|
|
775
|
+
if (!node || !id)
|
|
776
|
+
continue;
|
|
777
|
+
graph.addNode({
|
|
778
|
+
id,
|
|
779
|
+
label: table,
|
|
780
|
+
properties: nodePropertiesFromCgdbRow(node),
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
/* table may be absent in old indexes */
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
const rows = await executeQuery(`MATCH (a)-[r:${REL_TABLE_NAME}]->(b) RETURN a.id AS sourceId, b.id AS targetId, r.type AS type, r.confidence AS confidence, r.reason AS reason, r.step AS step`);
|
|
790
|
+
let relIndex = 0;
|
|
791
|
+
for (const row of rows) {
|
|
792
|
+
const sourceId = rowString(row, 'sourceId', 0);
|
|
793
|
+
const targetId = rowString(row, 'targetId', 1);
|
|
794
|
+
if (!sourceId || !targetId)
|
|
795
|
+
continue;
|
|
796
|
+
if (!includeGlobal) {
|
|
797
|
+
const sourceLabel = nodeLabelForId(sourceId);
|
|
798
|
+
const targetLabel = nodeLabelForId(targetId);
|
|
799
|
+
if (GLOBAL_NODE_TABLES.has(sourceLabel) || GLOBAL_NODE_TABLES.has(targetLabel))
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const type = rowString(row, 'type', 2);
|
|
803
|
+
graph.addRelationship({
|
|
804
|
+
id: `cgdb:${relIndex++}:${sourceId}->${type}->${targetId}`,
|
|
805
|
+
sourceId,
|
|
806
|
+
targetId,
|
|
807
|
+
type: type,
|
|
808
|
+
confidence: rowNumber(row, 'confidence', 3, 1),
|
|
809
|
+
reason: rowString(row, 'reason', 4),
|
|
810
|
+
step: rowNumber(row, 'step', 5, 0),
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
/* relationships may be absent in old indexes */
|
|
816
|
+
}
|
|
817
|
+
return graph;
|
|
818
|
+
};
|
|
819
|
+
const pruneEmptyFoldersFromCgdb = async () => {
|
|
820
|
+
let folderRows = [];
|
|
821
|
+
let fileRows = [];
|
|
822
|
+
try {
|
|
823
|
+
folderRows = await executeQuery('MATCH (f:Folder) RETURN f.id AS id, f.filePath AS filePath');
|
|
824
|
+
fileRows = await executeQuery('MATCH (f:File) RETURN f.filePath AS filePath');
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
return 0;
|
|
828
|
+
}
|
|
829
|
+
const filePaths = fileRows.map((row) => rowString(row, 'filePath', 0)).filter(Boolean);
|
|
830
|
+
let pruned = 0;
|
|
831
|
+
for (const row of folderRows) {
|
|
832
|
+
const id = rowString(row, 'id', 0);
|
|
833
|
+
const folderPath = rowString(row, 'filePath', 1);
|
|
834
|
+
if (!id || !folderPath)
|
|
835
|
+
continue;
|
|
836
|
+
const hasDescendantFile = filePaths.some((filePath) => filePath.startsWith(`${folderPath}/`));
|
|
837
|
+
if (hasDescendantFile)
|
|
838
|
+
continue;
|
|
839
|
+
try {
|
|
840
|
+
await executePrepared('MATCH (f:Folder {id: $id}) DETACH DELETE f', { id });
|
|
841
|
+
pruned++;
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
/* best-effort cleanup */
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return pruned;
|
|
848
|
+
};
|
|
435
849
|
// LadybugDB default ESCAPE is '\' (backslash), but our CSV uses RFC 4180 escaping ("" for literal quotes).
|
|
436
850
|
// Source code content is full of backslashes which confuse the auto-detection.
|
|
437
851
|
// We MUST explicitly set ESCAPE='"' to use RFC 4180 escaping, and disable auto_detect to prevent
|
|
@@ -1254,12 +1668,10 @@ export const createFTSIndex = async (tableName, indexName, properties, stemmer =
|
|
|
1254
1668
|
}
|
|
1255
1669
|
};
|
|
1256
1670
|
/**
|
|
1257
|
-
*
|
|
1671
|
+
* Create an FTS index if needed, caching the fact in-process.
|
|
1258
1672
|
*
|
|
1259
|
-
* Used by
|
|
1260
|
-
*
|
|
1261
|
-
* the cost is moved to the first `query`/`context` call in a session,
|
|
1262
|
-
* where it's amortised across many lookups.
|
|
1673
|
+
* Used by analyze to warm persisted keyword indexes while the DB is writable,
|
|
1674
|
+
* and by direct core search as a defensive fallback for older indexes.
|
|
1263
1675
|
*
|
|
1264
1676
|
* Safe to call repeatedly — the in-process Set guarantees only the first
|
|
1265
1677
|
* call hits LadybugDB. `closeCgdb` clears the cache so re-init starts fresh.
|
|
@@ -1327,6 +1739,7 @@ export const dropFTSIndex = async (tableName, indexName) => {
|
|
|
1327
1739
|
if (!conn) {
|
|
1328
1740
|
throw new Error('LadybugDB not initialized. Call initCgdb first.');
|
|
1329
1741
|
}
|
|
1742
|
+
await loadFTSExtension();
|
|
1330
1743
|
try {
|
|
1331
1744
|
await conn.query(`CALL DROP_FTS_INDEX('${tableName}', '${indexName}')`);
|
|
1332
1745
|
}
|
|
@@ -586,7 +586,7 @@ function isBenignLabelScanError(error) {
|
|
|
586
586
|
const message = error.message.toLowerCase();
|
|
587
587
|
return (message.includes('cannot find property') ||
|
|
588
588
|
message.includes('does not have property') ||
|
|
589
|
-
message.includes('property') && message.includes('not found'));
|
|
589
|
+
(message.includes('property') && message.includes('not found')));
|
|
590
590
|
}
|
|
591
591
|
export const executeQuery = async (repoId, cypher) => {
|
|
592
592
|
const entry = pool.get(repoId);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type GraphpackBootstrapResult, type GraphpackPublishOptions, type GraphpackPublishResult, type GraphpackPullOptions, type GraphpackPullResult, type GraphpackStatus, type GraphpackLock } from './types.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export declare const defaultLockPath: (repoPath: string) => string;
|
|
4
|
+
export declare const readGraphpackLock: (lockPath: string) => Promise<GraphpackLock | null>;
|
|
5
|
+
export declare const writeGraphpackLock: (lockPath: string, lock: GraphpackLock) => Promise<void>;
|
|
6
|
+
export declare const publishGraphpack: (opts: GraphpackPublishOptions) => Promise<GraphpackPublishResult>;
|
|
7
|
+
export declare const getGraphpackStatus: (opts: {
|
|
8
|
+
repoPath: string;
|
|
9
|
+
storagePath: string;
|
|
10
|
+
lockPath?: string;
|
|
11
|
+
strict?: boolean;
|
|
12
|
+
}) => Promise<GraphpackStatus>;
|
|
13
|
+
export declare const pullGraphpack: (opts: GraphpackPullOptions) => Promise<GraphpackPullResult>;
|
|
14
|
+
export declare const bootstrapGraphpack: (opts: GraphpackPullOptions) => Promise<GraphpackBootstrapResult>;
|