@harness-engineering/graph 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +29 -13
- package/dist/index.d.ts +29 -13
- package/dist/index.js +573 -480
- package/dist/index.mjs +573 -480
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -434,6 +434,53 @@ var VectorStore = class _VectorStore {
|
|
|
434
434
|
};
|
|
435
435
|
|
|
436
436
|
// src/query/ContextQL.ts
|
|
437
|
+
function edgeKey(e) {
|
|
438
|
+
return `${e.from}|${e.to}|${e.type}`;
|
|
439
|
+
}
|
|
440
|
+
function addEdge(state, edge) {
|
|
441
|
+
const key = edgeKey(edge);
|
|
442
|
+
if (!state.edgeSet.has(key)) {
|
|
443
|
+
state.edgeSet.add(key);
|
|
444
|
+
state.resultEdges.push(edge);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function shouldPruneNode(neighbor, pruneObservability, params) {
|
|
448
|
+
if (pruneObservability && OBSERVABILITY_TYPES.has(neighbor.type)) return true;
|
|
449
|
+
if (params.includeTypes && !params.includeTypes.includes(neighbor.type)) return true;
|
|
450
|
+
if (params.excludeTypes && params.excludeTypes.includes(neighbor.type)) return true;
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
function isEdgeExcluded(edge, params) {
|
|
454
|
+
return !!(params.includeEdges && !params.includeEdges.includes(edge.type));
|
|
455
|
+
}
|
|
456
|
+
function processNeighbor(store, edge, neighborId, nextDepth, queue, state, pruneObservability, params) {
|
|
457
|
+
if (isEdgeExcluded(edge, params)) return;
|
|
458
|
+
if (state.visited.has(neighborId)) {
|
|
459
|
+
addEdge(state, edge);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const neighbor = store.getNode(neighborId);
|
|
463
|
+
if (!neighbor) return;
|
|
464
|
+
state.visited.add(neighborId);
|
|
465
|
+
if (shouldPruneNode(neighbor, pruneObservability, params)) {
|
|
466
|
+
state.pruned++;
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
state.resultNodeMap.set(neighborId, neighbor);
|
|
470
|
+
addEdge(state, edge);
|
|
471
|
+
queue.push({ id: neighborId, depth: nextDepth });
|
|
472
|
+
}
|
|
473
|
+
function addCrossEdges(store, state) {
|
|
474
|
+
const resultNodeIds = new Set(state.resultNodeMap.keys());
|
|
475
|
+
for (const nodeId of resultNodeIds) {
|
|
476
|
+
const outEdges = store.getEdges({ from: nodeId });
|
|
477
|
+
for (const edge of outEdges) {
|
|
478
|
+
if (resultNodeIds.has(edge.to)) {
|
|
479
|
+
addEdge(state, edge);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
437
484
|
var ContextQL = class {
|
|
438
485
|
store;
|
|
439
486
|
constructor(store) {
|
|
@@ -443,89 +490,69 @@ var ContextQL = class {
|
|
|
443
490
|
const maxDepth = params.maxDepth ?? 3;
|
|
444
491
|
const bidirectional = params.bidirectional ?? false;
|
|
445
492
|
const pruneObservability = params.pruneObservability ?? true;
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const addEdge = (edge) => {
|
|
454
|
-
const key = edgeKey(edge);
|
|
455
|
-
if (!edgeSet.has(key)) {
|
|
456
|
-
edgeSet.add(key);
|
|
457
|
-
resultEdges.push(edge);
|
|
458
|
-
}
|
|
493
|
+
const state = {
|
|
494
|
+
visited: /* @__PURE__ */ new Set(),
|
|
495
|
+
resultNodeMap: /* @__PURE__ */ new Map(),
|
|
496
|
+
resultEdges: [],
|
|
497
|
+
edgeSet: /* @__PURE__ */ new Set(),
|
|
498
|
+
pruned: 0,
|
|
499
|
+
depthReached: 0
|
|
459
500
|
};
|
|
460
501
|
const queue = [];
|
|
461
|
-
|
|
502
|
+
this.seedRootNodes(params.rootNodeIds, state, queue);
|
|
503
|
+
this.runBFS(queue, maxDepth, bidirectional, pruneObservability, params, state);
|
|
504
|
+
addCrossEdges(this.store, state);
|
|
505
|
+
return {
|
|
506
|
+
nodes: Array.from(state.resultNodeMap.values()),
|
|
507
|
+
edges: state.resultEdges,
|
|
508
|
+
stats: {
|
|
509
|
+
totalTraversed: state.visited.size,
|
|
510
|
+
totalReturned: state.resultNodeMap.size,
|
|
511
|
+
pruned: state.pruned,
|
|
512
|
+
depthReached: state.depthReached
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
seedRootNodes(rootNodeIds, state, queue) {
|
|
517
|
+
for (const rootId of rootNodeIds) {
|
|
462
518
|
const node = this.store.getNode(rootId);
|
|
463
519
|
if (node) {
|
|
464
|
-
visited.add(rootId);
|
|
465
|
-
resultNodeMap.set(rootId, node);
|
|
520
|
+
state.visited.add(rootId);
|
|
521
|
+
state.resultNodeMap.set(rootId, node);
|
|
466
522
|
queue.push({ id: rootId, depth: 0 });
|
|
467
523
|
}
|
|
468
524
|
}
|
|
525
|
+
}
|
|
526
|
+
runBFS(queue, maxDepth, bidirectional, pruneObservability, params, state) {
|
|
469
527
|
let head = 0;
|
|
470
528
|
while (head < queue.length) {
|
|
471
529
|
const entry = queue[head++];
|
|
472
530
|
const { id: currentId, depth } = entry;
|
|
473
531
|
if (depth >= maxDepth) continue;
|
|
474
532
|
const nextDepth = depth + 1;
|
|
475
|
-
if (nextDepth > depthReached) depthReached = nextDepth;
|
|
476
|
-
const
|
|
477
|
-
const inEdges = bidirectional ? this.store.getEdges({ to: currentId }) : [];
|
|
478
|
-
const allEdges = [
|
|
479
|
-
...outEdges.map((e) => ({ edge: e, neighborId: e.to })),
|
|
480
|
-
...inEdges.map((e) => ({ edge: e, neighborId: e.from }))
|
|
481
|
-
];
|
|
533
|
+
if (nextDepth > state.depthReached) state.depthReached = nextDepth;
|
|
534
|
+
const allEdges = this.gatherEdges(currentId, bidirectional);
|
|
482
535
|
for (const { edge, neighborId } of allEdges) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (pruneObservability && OBSERVABILITY_TYPES.has(neighbor.type)) {
|
|
494
|
-
pruned++;
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
if (params.includeTypes && !params.includeTypes.includes(neighbor.type)) {
|
|
498
|
-
pruned++;
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
if (params.excludeTypes && params.excludeTypes.includes(neighbor.type)) {
|
|
502
|
-
pruned++;
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
resultNodeMap.set(neighborId, neighbor);
|
|
506
|
-
addEdge(edge);
|
|
507
|
-
queue.push({ id: neighborId, depth: nextDepth });
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
const resultNodeIds = new Set(resultNodeMap.keys());
|
|
511
|
-
for (const nodeId of resultNodeIds) {
|
|
512
|
-
const outEdges = this.store.getEdges({ from: nodeId });
|
|
513
|
-
for (const edge of outEdges) {
|
|
514
|
-
if (resultNodeIds.has(edge.to)) {
|
|
515
|
-
addEdge(edge);
|
|
516
|
-
}
|
|
536
|
+
processNeighbor(
|
|
537
|
+
this.store,
|
|
538
|
+
edge,
|
|
539
|
+
neighborId,
|
|
540
|
+
nextDepth,
|
|
541
|
+
queue,
|
|
542
|
+
state,
|
|
543
|
+
pruneObservability,
|
|
544
|
+
params
|
|
545
|
+
);
|
|
517
546
|
}
|
|
518
547
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
};
|
|
548
|
+
}
|
|
549
|
+
gatherEdges(nodeId, bidirectional) {
|
|
550
|
+
const outEdges = this.store.getEdges({ from: nodeId });
|
|
551
|
+
const inEdges = bidirectional ? this.store.getEdges({ to: nodeId }) : [];
|
|
552
|
+
return [
|
|
553
|
+
...outEdges.map((e) => ({ edge: e, neighborId: e.to })),
|
|
554
|
+
...inEdges.map((e) => ({ edge: e, neighborId: e.from }))
|
|
555
|
+
];
|
|
529
556
|
}
|
|
530
557
|
};
|
|
531
558
|
|
|
@@ -578,6 +605,15 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
578
605
|
// src/ingest/CodeIngestor.ts
|
|
579
606
|
var fs = __toESM(require("fs/promises"));
|
|
580
607
|
var path = __toESM(require("path"));
|
|
608
|
+
var SKIP_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "if", "for", "while", "switch"]);
|
|
609
|
+
function countBraces(line) {
|
|
610
|
+
let net = 0;
|
|
611
|
+
for (const ch of line) {
|
|
612
|
+
if (ch === "{") net++;
|
|
613
|
+
else if (ch === "}") net--;
|
|
614
|
+
}
|
|
615
|
+
return net;
|
|
616
|
+
}
|
|
581
617
|
var CodeIngestor = class {
|
|
582
618
|
constructor(store) {
|
|
583
619
|
this.store = store;
|
|
@@ -592,41 +628,9 @@ var CodeIngestor = class {
|
|
|
592
628
|
const fileContents = /* @__PURE__ */ new Map();
|
|
593
629
|
for (const filePath of files) {
|
|
594
630
|
try {
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const fileId = `file:${relativePath}`;
|
|
599
|
-
fileContents.set(relativePath, content);
|
|
600
|
-
const fileNode = {
|
|
601
|
-
id: fileId,
|
|
602
|
-
type: "file",
|
|
603
|
-
name: path.basename(filePath),
|
|
604
|
-
path: relativePath,
|
|
605
|
-
metadata: { language: this.detectLanguage(filePath) },
|
|
606
|
-
lastModified: stat2.mtime.toISOString()
|
|
607
|
-
};
|
|
608
|
-
this.store.addNode(fileNode);
|
|
609
|
-
nodesAdded++;
|
|
610
|
-
const symbols = this.extractSymbols(content, fileId, relativePath);
|
|
611
|
-
for (const { node, edge } of symbols) {
|
|
612
|
-
this.store.addNode(node);
|
|
613
|
-
this.store.addEdge(edge);
|
|
614
|
-
nodesAdded++;
|
|
615
|
-
edgesAdded++;
|
|
616
|
-
if (node.type === "function" || node.type === "method") {
|
|
617
|
-
let files2 = nameToFiles.get(node.name);
|
|
618
|
-
if (!files2) {
|
|
619
|
-
files2 = /* @__PURE__ */ new Set();
|
|
620
|
-
nameToFiles.set(node.name, files2);
|
|
621
|
-
}
|
|
622
|
-
files2.add(relativePath);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
const imports = await this.extractImports(content, fileId, relativePath, rootDir);
|
|
626
|
-
for (const edge of imports) {
|
|
627
|
-
this.store.addEdge(edge);
|
|
628
|
-
edgesAdded++;
|
|
629
|
-
}
|
|
631
|
+
const result = await this.processFile(filePath, rootDir, nameToFiles, fileContents);
|
|
632
|
+
nodesAdded += result.nodesAdded;
|
|
633
|
+
edgesAdded += result.edgesAdded;
|
|
630
634
|
} catch (err) {
|
|
631
635
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
632
636
|
}
|
|
@@ -645,6 +649,48 @@ var CodeIngestor = class {
|
|
|
645
649
|
durationMs: Date.now() - start
|
|
646
650
|
};
|
|
647
651
|
}
|
|
652
|
+
async processFile(filePath, rootDir, nameToFiles, fileContents) {
|
|
653
|
+
let nodesAdded = 0;
|
|
654
|
+
let edgesAdded = 0;
|
|
655
|
+
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
656
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
657
|
+
const stat2 = await fs.stat(filePath);
|
|
658
|
+
const fileId = `file:${relativePath}`;
|
|
659
|
+
fileContents.set(relativePath, content);
|
|
660
|
+
const fileNode = {
|
|
661
|
+
id: fileId,
|
|
662
|
+
type: "file",
|
|
663
|
+
name: path.basename(filePath),
|
|
664
|
+
path: relativePath,
|
|
665
|
+
metadata: { language: this.detectLanguage(filePath) },
|
|
666
|
+
lastModified: stat2.mtime.toISOString()
|
|
667
|
+
};
|
|
668
|
+
this.store.addNode(fileNode);
|
|
669
|
+
nodesAdded++;
|
|
670
|
+
const symbols = this.extractSymbols(content, fileId, relativePath);
|
|
671
|
+
for (const { node, edge } of symbols) {
|
|
672
|
+
this.store.addNode(node);
|
|
673
|
+
this.store.addEdge(edge);
|
|
674
|
+
nodesAdded++;
|
|
675
|
+
edgesAdded++;
|
|
676
|
+
this.trackCallable(node, relativePath, nameToFiles);
|
|
677
|
+
}
|
|
678
|
+
const imports = await this.extractImports(content, fileId, relativePath, rootDir);
|
|
679
|
+
for (const edge of imports) {
|
|
680
|
+
this.store.addEdge(edge);
|
|
681
|
+
edgesAdded++;
|
|
682
|
+
}
|
|
683
|
+
return { nodesAdded, edgesAdded };
|
|
684
|
+
}
|
|
685
|
+
trackCallable(node, relativePath, nameToFiles) {
|
|
686
|
+
if (node.type !== "function" && node.type !== "method") return;
|
|
687
|
+
let files = nameToFiles.get(node.name);
|
|
688
|
+
if (!files) {
|
|
689
|
+
files = /* @__PURE__ */ new Set();
|
|
690
|
+
nameToFiles.set(node.name, files);
|
|
691
|
+
}
|
|
692
|
+
files.add(relativePath);
|
|
693
|
+
}
|
|
648
694
|
async findSourceFiles(dir) {
|
|
649
695
|
const results = [];
|
|
650
696
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -661,149 +707,152 @@ var CodeIngestor = class {
|
|
|
661
707
|
extractSymbols(content, fileId, relativePath) {
|
|
662
708
|
const results = [];
|
|
663
709
|
const lines = content.split("\n");
|
|
664
|
-
|
|
665
|
-
let currentClassId = null;
|
|
666
|
-
let braceDepth = 0;
|
|
667
|
-
let insideClass = false;
|
|
710
|
+
const ctx = { className: null, classId: null, insideClass: false, braceDepth: 0 };
|
|
668
711
|
for (let i = 0; i < lines.length; i++) {
|
|
669
712
|
const line = lines[i];
|
|
670
|
-
|
|
671
|
-
if (
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
id,
|
|
678
|
-
type: "function",
|
|
679
|
-
name,
|
|
680
|
-
path: relativePath,
|
|
681
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
682
|
-
metadata: {
|
|
683
|
-
exported: line.includes("export"),
|
|
684
|
-
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
685
|
-
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
686
|
-
lineCount: endLine - i,
|
|
687
|
-
parameterCount: this.countParameters(line)
|
|
688
|
-
}
|
|
689
|
-
},
|
|
690
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
691
|
-
});
|
|
692
|
-
if (!insideClass) {
|
|
693
|
-
currentClassName = null;
|
|
694
|
-
currentClassId = null;
|
|
695
|
-
}
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
|
|
699
|
-
if (classMatch) {
|
|
700
|
-
const name = classMatch[1];
|
|
701
|
-
const id = `class:${relativePath}:${name}`;
|
|
702
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
703
|
-
results.push({
|
|
704
|
-
node: {
|
|
705
|
-
id,
|
|
706
|
-
type: "class",
|
|
707
|
-
name,
|
|
708
|
-
path: relativePath,
|
|
709
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
710
|
-
metadata: { exported: line.includes("export") }
|
|
711
|
-
},
|
|
712
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
713
|
-
});
|
|
714
|
-
currentClassName = name;
|
|
715
|
-
currentClassId = id;
|
|
716
|
-
insideClass = true;
|
|
717
|
-
braceDepth = 0;
|
|
718
|
-
for (const ch of line) {
|
|
719
|
-
if (ch === "{") braceDepth++;
|
|
720
|
-
if (ch === "}") braceDepth--;
|
|
721
|
-
}
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
const ifaceMatch = line.match(/(?:export\s+)?interface\s+(\w+)/);
|
|
725
|
-
if (ifaceMatch) {
|
|
726
|
-
const name = ifaceMatch[1];
|
|
727
|
-
const id = `interface:${relativePath}:${name}`;
|
|
728
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
729
|
-
results.push({
|
|
730
|
-
node: {
|
|
731
|
-
id,
|
|
732
|
-
type: "interface",
|
|
733
|
-
name,
|
|
734
|
-
path: relativePath,
|
|
735
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
736
|
-
metadata: { exported: line.includes("export") }
|
|
737
|
-
},
|
|
738
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
739
|
-
});
|
|
740
|
-
currentClassName = null;
|
|
741
|
-
currentClassId = null;
|
|
742
|
-
insideClass = false;
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
|
-
if (insideClass) {
|
|
746
|
-
for (const ch of line) {
|
|
747
|
-
if (ch === "{") braceDepth++;
|
|
748
|
-
if (ch === "}") braceDepth--;
|
|
749
|
-
}
|
|
750
|
-
if (braceDepth <= 0) {
|
|
751
|
-
currentClassName = null;
|
|
752
|
-
currentClassId = null;
|
|
753
|
-
insideClass = false;
|
|
754
|
-
continue;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
if (insideClass && currentClassName && currentClassId) {
|
|
758
|
-
const methodMatch = line.match(
|
|
759
|
-
/^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/
|
|
760
|
-
);
|
|
761
|
-
if (methodMatch) {
|
|
762
|
-
const methodName = methodMatch[1];
|
|
763
|
-
if (methodName === "constructor" || methodName === "if" || methodName === "for" || methodName === "while" || methodName === "switch")
|
|
764
|
-
continue;
|
|
765
|
-
const id = `method:${relativePath}:${currentClassName}.${methodName}`;
|
|
766
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
767
|
-
results.push({
|
|
768
|
-
node: {
|
|
769
|
-
id,
|
|
770
|
-
type: "method",
|
|
771
|
-
name: methodName,
|
|
772
|
-
path: relativePath,
|
|
773
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
774
|
-
metadata: {
|
|
775
|
-
className: currentClassName,
|
|
776
|
-
exported: false,
|
|
777
|
-
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
778
|
-
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
779
|
-
lineCount: endLine - i,
|
|
780
|
-
parameterCount: this.countParameters(line)
|
|
781
|
-
}
|
|
782
|
-
},
|
|
783
|
-
edge: { from: currentClassId, to: id, type: "contains" }
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
continue;
|
|
787
|
-
}
|
|
788
|
-
const varMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)/);
|
|
789
|
-
if (varMatch) {
|
|
790
|
-
const name = varMatch[1];
|
|
791
|
-
const id = `variable:${relativePath}:${name}`;
|
|
792
|
-
results.push({
|
|
793
|
-
node: {
|
|
794
|
-
id,
|
|
795
|
-
type: "variable",
|
|
796
|
-
name,
|
|
797
|
-
path: relativePath,
|
|
798
|
-
location: { fileId, startLine: i + 1, endLine: i + 1 },
|
|
799
|
-
metadata: { exported: line.includes("export") }
|
|
800
|
-
},
|
|
801
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
802
|
-
});
|
|
803
|
-
}
|
|
713
|
+
if (this.tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
714
|
+
if (this.tryExtractClass(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
715
|
+
if (this.tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
716
|
+
if (this.updateClassContext(line, ctx)) continue;
|
|
717
|
+
if (this.tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
718
|
+
if (ctx.insideClass) continue;
|
|
719
|
+
this.tryExtractVariable(line, i, fileId, relativePath, results);
|
|
804
720
|
}
|
|
805
721
|
return results;
|
|
806
722
|
}
|
|
723
|
+
tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results) {
|
|
724
|
+
const fnMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
|
|
725
|
+
if (!fnMatch) return false;
|
|
726
|
+
const name = fnMatch[1];
|
|
727
|
+
const id = `function:${relativePath}:${name}`;
|
|
728
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
729
|
+
results.push({
|
|
730
|
+
node: {
|
|
731
|
+
id,
|
|
732
|
+
type: "function",
|
|
733
|
+
name,
|
|
734
|
+
path: relativePath,
|
|
735
|
+
location: { fileId, startLine: i + 1, endLine },
|
|
736
|
+
metadata: {
|
|
737
|
+
exported: line.includes("export"),
|
|
738
|
+
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
739
|
+
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
740
|
+
lineCount: endLine - i,
|
|
741
|
+
parameterCount: this.countParameters(line)
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
edge: { from: fileId, to: id, type: "contains" }
|
|
745
|
+
});
|
|
746
|
+
if (!ctx.insideClass) {
|
|
747
|
+
ctx.className = null;
|
|
748
|
+
ctx.classId = null;
|
|
749
|
+
}
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
tryExtractClass(line, lines, i, fileId, relativePath, ctx, results) {
|
|
753
|
+
const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
|
|
754
|
+
if (!classMatch) return false;
|
|
755
|
+
const name = classMatch[1];
|
|
756
|
+
const id = `class:${relativePath}:${name}`;
|
|
757
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
758
|
+
results.push({
|
|
759
|
+
node: {
|
|
760
|
+
id,
|
|
761
|
+
type: "class",
|
|
762
|
+
name,
|
|
763
|
+
path: relativePath,
|
|
764
|
+
location: { fileId, startLine: i + 1, endLine },
|
|
765
|
+
metadata: { exported: line.includes("export") }
|
|
766
|
+
},
|
|
767
|
+
edge: { from: fileId, to: id, type: "contains" }
|
|
768
|
+
});
|
|
769
|
+
ctx.className = name;
|
|
770
|
+
ctx.classId = id;
|
|
771
|
+
ctx.insideClass = true;
|
|
772
|
+
ctx.braceDepth = countBraces(line);
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results) {
|
|
776
|
+
const ifaceMatch = line.match(/(?:export\s+)?interface\s+(\w+)/);
|
|
777
|
+
if (!ifaceMatch) return false;
|
|
778
|
+
const name = ifaceMatch[1];
|
|
779
|
+
const id = `interface:${relativePath}:${name}`;
|
|
780
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
781
|
+
results.push({
|
|
782
|
+
node: {
|
|
783
|
+
id,
|
|
784
|
+
type: "interface",
|
|
785
|
+
name,
|
|
786
|
+
path: relativePath,
|
|
787
|
+
location: { fileId, startLine: i + 1, endLine },
|
|
788
|
+
metadata: { exported: line.includes("export") }
|
|
789
|
+
},
|
|
790
|
+
edge: { from: fileId, to: id, type: "contains" }
|
|
791
|
+
});
|
|
792
|
+
ctx.className = null;
|
|
793
|
+
ctx.classId = null;
|
|
794
|
+
ctx.insideClass = false;
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
797
|
+
/** Update brace tracking; returns true when line is consumed (class ended or tracked). */
|
|
798
|
+
updateClassContext(line, ctx) {
|
|
799
|
+
if (!ctx.insideClass) return false;
|
|
800
|
+
ctx.braceDepth += countBraces(line);
|
|
801
|
+
if (ctx.braceDepth <= 0) {
|
|
802
|
+
ctx.className = null;
|
|
803
|
+
ctx.classId = null;
|
|
804
|
+
ctx.insideClass = false;
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results) {
|
|
810
|
+
if (!ctx.insideClass || !ctx.className || !ctx.classId) return false;
|
|
811
|
+
const methodMatch = line.match(
|
|
812
|
+
/^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/
|
|
813
|
+
);
|
|
814
|
+
if (!methodMatch) return false;
|
|
815
|
+
const methodName = methodMatch[1];
|
|
816
|
+
if (SKIP_METHOD_NAMES.has(methodName)) return false;
|
|
817
|
+
const id = `method:${relativePath}:${ctx.className}.${methodName}`;
|
|
818
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
819
|
+
results.push({
|
|
820
|
+
node: {
|
|
821
|
+
id,
|
|
822
|
+
type: "method",
|
|
823
|
+
name: methodName,
|
|
824
|
+
path: relativePath,
|
|
825
|
+
location: { fileId, startLine: i + 1, endLine },
|
|
826
|
+
metadata: {
|
|
827
|
+
className: ctx.className,
|
|
828
|
+
exported: false,
|
|
829
|
+
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
830
|
+
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
831
|
+
lineCount: endLine - i,
|
|
832
|
+
parameterCount: this.countParameters(line)
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
edge: { from: ctx.classId, to: id, type: "contains" }
|
|
836
|
+
});
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
tryExtractVariable(line, i, fileId, relativePath, results) {
|
|
840
|
+
const varMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)/);
|
|
841
|
+
if (!varMatch) return;
|
|
842
|
+
const name = varMatch[1];
|
|
843
|
+
const id = `variable:${relativePath}:${name}`;
|
|
844
|
+
results.push({
|
|
845
|
+
node: {
|
|
846
|
+
id,
|
|
847
|
+
type: "variable",
|
|
848
|
+
name,
|
|
849
|
+
path: relativePath,
|
|
850
|
+
location: { fileId, startLine: i + 1, endLine: i + 1 },
|
|
851
|
+
metadata: { exported: line.includes("export") }
|
|
852
|
+
},
|
|
853
|
+
edge: { from: fileId, to: id, type: "contains" }
|
|
854
|
+
});
|
|
855
|
+
}
|
|
807
856
|
/**
|
|
808
857
|
* Find the closing brace for a construct starting at the given line.
|
|
809
858
|
* Uses a simple brace-counting heuristic. Returns 1-indexed line number.
|
|
@@ -1423,17 +1472,33 @@ var KnowledgeIngestor = class {
|
|
|
1423
1472
|
|
|
1424
1473
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
1425
1474
|
var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1475
|
+
var SANITIZE_RULES = [
|
|
1476
|
+
// Strip XML/HTML-like instruction tags that could be interpreted as system prompts
|
|
1477
|
+
{
|
|
1478
|
+
pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
|
|
1479
|
+
replacement: ""
|
|
1480
|
+
},
|
|
1481
|
+
// Strip markdown-style system prompt markers (including trailing space)
|
|
1482
|
+
{
|
|
1483
|
+
pattern: /^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim,
|
|
1484
|
+
replacement: ""
|
|
1485
|
+
},
|
|
1486
|
+
// Strip common injection prefixes
|
|
1487
|
+
{
|
|
1488
|
+
pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
|
|
1489
|
+
replacement: "[filtered]"
|
|
1490
|
+
},
|
|
1491
|
+
// Strip "you are now" re-roling attempts (only when followed by AI/agent role words)
|
|
1492
|
+
{
|
|
1493
|
+
pattern: /you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
|
|
1494
|
+
replacement: "[filtered]"
|
|
1495
|
+
}
|
|
1496
|
+
];
|
|
1426
1497
|
function sanitizeExternalText(text, maxLength = 2e3) {
|
|
1427
|
-
let sanitized = text
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
/(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
|
|
1432
|
-
"[filtered]"
|
|
1433
|
-
).replace(
|
|
1434
|
-
/you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
|
|
1435
|
-
"[filtered]"
|
|
1436
|
-
);
|
|
1498
|
+
let sanitized = text;
|
|
1499
|
+
for (const rule of SANITIZE_RULES) {
|
|
1500
|
+
sanitized = sanitized.replace(rule.pattern, rule.replacement);
|
|
1501
|
+
}
|
|
1437
1502
|
if (sanitized.length > maxLength) {
|
|
1438
1503
|
sanitized = sanitized.slice(0, maxLength) + "\u2026";
|
|
1439
1504
|
}
|
|
@@ -1533,6 +1598,28 @@ var SyncManager = class {
|
|
|
1533
1598
|
};
|
|
1534
1599
|
|
|
1535
1600
|
// src/ingest/connectors/JiraConnector.ts
|
|
1601
|
+
function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
|
|
1602
|
+
return {
|
|
1603
|
+
nodesAdded,
|
|
1604
|
+
nodesUpdated: 0,
|
|
1605
|
+
edgesAdded,
|
|
1606
|
+
edgesUpdated: 0,
|
|
1607
|
+
errors,
|
|
1608
|
+
durationMs: Date.now() - start
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function buildJql(config) {
|
|
1612
|
+
const project2 = config.project;
|
|
1613
|
+
let jql = project2 ? `project=${project2}` : "";
|
|
1614
|
+
const filters = config.filters;
|
|
1615
|
+
if (filters?.status?.length) {
|
|
1616
|
+
jql += `${jql ? " AND " : ""}status IN (${filters.status.map((s) => `"${s}"`).join(",")})`;
|
|
1617
|
+
}
|
|
1618
|
+
if (filters?.labels?.length) {
|
|
1619
|
+
jql += `${jql ? " AND " : ""}labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`;
|
|
1620
|
+
}
|
|
1621
|
+
return jql;
|
|
1622
|
+
}
|
|
1536
1623
|
var JiraConnector = class {
|
|
1537
1624
|
name = "jira";
|
|
1538
1625
|
source = "jira";
|
|
@@ -1542,105 +1629,81 @@ var JiraConnector = class {
|
|
|
1542
1629
|
}
|
|
1543
1630
|
async ingest(store, config) {
|
|
1544
1631
|
const start = Date.now();
|
|
1545
|
-
const errors = [];
|
|
1546
1632
|
let nodesAdded = 0;
|
|
1547
1633
|
let edgesAdded = 0;
|
|
1548
1634
|
const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
|
|
1549
1635
|
const apiKey = process.env[apiKeyEnv];
|
|
1550
1636
|
if (!apiKey) {
|
|
1551
|
-
return
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
durationMs: Date.now() - start
|
|
1558
|
-
};
|
|
1637
|
+
return buildIngestResult(
|
|
1638
|
+
0,
|
|
1639
|
+
0,
|
|
1640
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
1641
|
+
start
|
|
1642
|
+
);
|
|
1559
1643
|
}
|
|
1560
1644
|
const baseUrlEnv = config.baseUrlEnv ?? "JIRA_BASE_URL";
|
|
1561
1645
|
const baseUrl = process.env[baseUrlEnv];
|
|
1562
1646
|
if (!baseUrl) {
|
|
1563
|
-
return
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
durationMs: Date.now() - start
|
|
1570
|
-
};
|
|
1571
|
-
}
|
|
1572
|
-
const project2 = config.project;
|
|
1573
|
-
let jql = project2 ? `project=${project2}` : "";
|
|
1574
|
-
const filters = config.filters;
|
|
1575
|
-
if (filters?.status?.length) {
|
|
1576
|
-
jql += `${jql ? " AND " : ""}status IN (${filters.status.map((s) => `"${s}"`).join(",")})`;
|
|
1577
|
-
}
|
|
1578
|
-
if (filters?.labels?.length) {
|
|
1579
|
-
jql += `${jql ? " AND " : ""}labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`;
|
|
1647
|
+
return buildIngestResult(
|
|
1648
|
+
0,
|
|
1649
|
+
0,
|
|
1650
|
+
[`Missing base URL: environment variable "${baseUrlEnv}" is not set`],
|
|
1651
|
+
start
|
|
1652
|
+
);
|
|
1580
1653
|
}
|
|
1654
|
+
const jql = buildJql(config);
|
|
1581
1655
|
const headers = {
|
|
1582
1656
|
Authorization: `Basic ${apiKey}`,
|
|
1583
1657
|
"Content-Type": "application/json"
|
|
1584
1658
|
};
|
|
1585
|
-
let startAt = 0;
|
|
1586
|
-
const maxResults = 50;
|
|
1587
|
-
let total = Infinity;
|
|
1588
1659
|
try {
|
|
1660
|
+
let startAt = 0;
|
|
1661
|
+
const maxResults = 50;
|
|
1662
|
+
let total = Infinity;
|
|
1589
1663
|
while (startAt < total) {
|
|
1590
1664
|
const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
|
|
1591
1665
|
const response = await this.httpClient(url, { headers });
|
|
1592
1666
|
if (!response.ok) {
|
|
1593
|
-
return
|
|
1594
|
-
nodesAdded,
|
|
1595
|
-
nodesUpdated: 0,
|
|
1596
|
-
edgesAdded,
|
|
1597
|
-
edgesUpdated: 0,
|
|
1598
|
-
errors: ["Jira API request failed"],
|
|
1599
|
-
durationMs: Date.now() - start
|
|
1600
|
-
};
|
|
1667
|
+
return buildIngestResult(nodesAdded, edgesAdded, ["Jira API request failed"], start);
|
|
1601
1668
|
}
|
|
1602
1669
|
const data = await response.json();
|
|
1603
1670
|
total = data.total;
|
|
1604
1671
|
for (const issue of data.issues) {
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
type: "issue",
|
|
1609
|
-
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
1610
|
-
metadata: {
|
|
1611
|
-
key: issue.key,
|
|
1612
|
-
status: issue.fields.status?.name,
|
|
1613
|
-
priority: issue.fields.priority?.name,
|
|
1614
|
-
assignee: issue.fields.assignee?.displayName,
|
|
1615
|
-
labels: issue.fields.labels ?? []
|
|
1616
|
-
}
|
|
1617
|
-
});
|
|
1618
|
-
nodesAdded++;
|
|
1619
|
-
const searchText = sanitizeExternalText(
|
|
1620
|
-
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
1621
|
-
);
|
|
1622
|
-
edgesAdded += linkToCode(store, searchText, nodeId, "applies_to");
|
|
1672
|
+
const counts = this.processIssue(store, issue);
|
|
1673
|
+
nodesAdded += counts.nodesAdded;
|
|
1674
|
+
edgesAdded += counts.edgesAdded;
|
|
1623
1675
|
}
|
|
1624
1676
|
startAt += maxResults;
|
|
1625
1677
|
}
|
|
1626
1678
|
} catch (err) {
|
|
1627
|
-
return
|
|
1679
|
+
return buildIngestResult(
|
|
1628
1680
|
nodesAdded,
|
|
1629
|
-
nodesUpdated: 0,
|
|
1630
1681
|
edgesAdded,
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
};
|
|
1682
|
+
[`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
|
|
1683
|
+
start
|
|
1684
|
+
);
|
|
1635
1685
|
}
|
|
1636
|
-
return
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1686
|
+
return buildIngestResult(nodesAdded, edgesAdded, [], start);
|
|
1687
|
+
}
|
|
1688
|
+
processIssue(store, issue) {
|
|
1689
|
+
const nodeId = `issue:jira:${issue.key}`;
|
|
1690
|
+
store.addNode({
|
|
1691
|
+
id: nodeId,
|
|
1692
|
+
type: "issue",
|
|
1693
|
+
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
1694
|
+
metadata: {
|
|
1695
|
+
key: issue.key,
|
|
1696
|
+
status: issue.fields.status?.name,
|
|
1697
|
+
priority: issue.fields.priority?.name,
|
|
1698
|
+
assignee: issue.fields.assignee?.displayName,
|
|
1699
|
+
labels: issue.fields.labels ?? []
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
const searchText = sanitizeExternalText(
|
|
1703
|
+
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
1704
|
+
);
|
|
1705
|
+
const edgesAdded = linkToCode(store, searchText, nodeId, "applies_to");
|
|
1706
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1644
1707
|
}
|
|
1645
1708
|
};
|
|
1646
1709
|
|
|
@@ -1673,44 +1736,10 @@ var SlackConnector = class {
|
|
|
1673
1736
|
const oldest = config.lookbackDays ? String(Math.floor((Date.now() - Number(config.lookbackDays) * 864e5) / 1e3)) : void 0;
|
|
1674
1737
|
for (const channel of channels) {
|
|
1675
1738
|
try {
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
const response = await this.httpClient(url, {
|
|
1681
|
-
headers: {
|
|
1682
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1683
|
-
"Content-Type": "application/json"
|
|
1684
|
-
}
|
|
1685
|
-
});
|
|
1686
|
-
if (!response.ok) {
|
|
1687
|
-
errors.push(`Slack API request failed for channel ${channel}`);
|
|
1688
|
-
continue;
|
|
1689
|
-
}
|
|
1690
|
-
const data = await response.json();
|
|
1691
|
-
if (!data.ok) {
|
|
1692
|
-
errors.push(`Slack API error for channel ${channel}`);
|
|
1693
|
-
continue;
|
|
1694
|
-
}
|
|
1695
|
-
for (const message of data.messages) {
|
|
1696
|
-
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
1697
|
-
const sanitizedText = sanitizeExternalText(message.text);
|
|
1698
|
-
const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
|
|
1699
|
-
store.addNode({
|
|
1700
|
-
id: nodeId,
|
|
1701
|
-
type: "conversation",
|
|
1702
|
-
name: snippet,
|
|
1703
|
-
metadata: {
|
|
1704
|
-
author: message.user,
|
|
1705
|
-
channel,
|
|
1706
|
-
timestamp: message.ts
|
|
1707
|
-
}
|
|
1708
|
-
});
|
|
1709
|
-
nodesAdded++;
|
|
1710
|
-
edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
|
|
1711
|
-
checkPaths: true
|
|
1712
|
-
});
|
|
1713
|
-
}
|
|
1739
|
+
const result = await this.processChannel(store, channel, apiKey, oldest);
|
|
1740
|
+
nodesAdded += result.nodesAdded;
|
|
1741
|
+
edgesAdded += result.edgesAdded;
|
|
1742
|
+
errors.push(...result.errors);
|
|
1714
1743
|
} catch (err) {
|
|
1715
1744
|
errors.push(
|
|
1716
1745
|
`Slack API error for channel ${channel}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -1726,6 +1755,52 @@ var SlackConnector = class {
|
|
|
1726
1755
|
durationMs: Date.now() - start
|
|
1727
1756
|
};
|
|
1728
1757
|
}
|
|
1758
|
+
async processChannel(store, channel, apiKey, oldest) {
|
|
1759
|
+
const errors = [];
|
|
1760
|
+
let nodesAdded = 0;
|
|
1761
|
+
let edgesAdded = 0;
|
|
1762
|
+
let url = `https://slack.com/api/conversations.history?channel=${encodeURIComponent(channel)}`;
|
|
1763
|
+
if (oldest) {
|
|
1764
|
+
url += `&oldest=${oldest}`;
|
|
1765
|
+
}
|
|
1766
|
+
const response = await this.httpClient(url, {
|
|
1767
|
+
headers: {
|
|
1768
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1769
|
+
"Content-Type": "application/json"
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
if (!response.ok) {
|
|
1773
|
+
return {
|
|
1774
|
+
nodesAdded: 0,
|
|
1775
|
+
edgesAdded: 0,
|
|
1776
|
+
errors: [`Slack API request failed for channel ${channel}`]
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
const data = await response.json();
|
|
1780
|
+
if (!data.ok) {
|
|
1781
|
+
return { nodesAdded: 0, edgesAdded: 0, errors: [`Slack API error for channel ${channel}`] };
|
|
1782
|
+
}
|
|
1783
|
+
for (const message of data.messages) {
|
|
1784
|
+
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
1785
|
+
const sanitizedText = sanitizeExternalText(message.text);
|
|
1786
|
+
const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
|
|
1787
|
+
store.addNode({
|
|
1788
|
+
id: nodeId,
|
|
1789
|
+
type: "conversation",
|
|
1790
|
+
name: snippet,
|
|
1791
|
+
metadata: {
|
|
1792
|
+
author: message.user,
|
|
1793
|
+
channel,
|
|
1794
|
+
timestamp: message.ts
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
nodesAdded++;
|
|
1798
|
+
edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
|
|
1799
|
+
checkPaths: true
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
return { nodesAdded, edgesAdded, errors };
|
|
1803
|
+
}
|
|
1729
1804
|
};
|
|
1730
1805
|
|
|
1731
1806
|
// src/ingest/connectors/ConfluenceConnector.ts
|
|
@@ -1757,36 +1832,10 @@ var ConfluenceConnector = class {
|
|
|
1757
1832
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
1758
1833
|
const spaceKey = config.spaceKey ?? "";
|
|
1759
1834
|
try {
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
});
|
|
1765
|
-
if (!response.ok) {
|
|
1766
|
-
errors.push(`Confluence API error: status ${response.status}`);
|
|
1767
|
-
break;
|
|
1768
|
-
}
|
|
1769
|
-
const data = await response.json();
|
|
1770
|
-
for (const page of data.results) {
|
|
1771
|
-
const nodeId = `confluence:${page.id}`;
|
|
1772
|
-
store.addNode({
|
|
1773
|
-
id: nodeId,
|
|
1774
|
-
type: "document",
|
|
1775
|
-
name: sanitizeExternalText(page.title, 500),
|
|
1776
|
-
metadata: {
|
|
1777
|
-
source: "confluence",
|
|
1778
|
-
spaceKey,
|
|
1779
|
-
pageId: page.id,
|
|
1780
|
-
status: page.status,
|
|
1781
|
-
url: page._links?.webui ?? ""
|
|
1782
|
-
}
|
|
1783
|
-
});
|
|
1784
|
-
nodesAdded++;
|
|
1785
|
-
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
1786
|
-
edgesAdded += linkToCode(store, text, nodeId, "documents");
|
|
1787
|
-
}
|
|
1788
|
-
nextUrl = data._links?.next ? `${baseUrl}${data._links.next}` : null;
|
|
1789
|
-
}
|
|
1835
|
+
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
1836
|
+
nodesAdded = result.nodesAdded;
|
|
1837
|
+
edgesAdded = result.edgesAdded;
|
|
1838
|
+
errors.push(...result.errors);
|
|
1790
1839
|
} catch (err) {
|
|
1791
1840
|
errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1792
1841
|
}
|
|
@@ -1799,6 +1848,47 @@ var ConfluenceConnector = class {
|
|
|
1799
1848
|
durationMs: Date.now() - start
|
|
1800
1849
|
};
|
|
1801
1850
|
}
|
|
1851
|
+
async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
|
|
1852
|
+
const errors = [];
|
|
1853
|
+
let nodesAdded = 0;
|
|
1854
|
+
let edgesAdded = 0;
|
|
1855
|
+
let nextUrl = `${baseUrl}/wiki/api/v2/pages?spaceKey=${encodeURIComponent(spaceKey)}&limit=25&body-format=storage`;
|
|
1856
|
+
while (nextUrl) {
|
|
1857
|
+
const response = await this.httpClient(nextUrl, {
|
|
1858
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1859
|
+
});
|
|
1860
|
+
if (!response.ok) {
|
|
1861
|
+
errors.push(`Confluence API error: status ${response.status}`);
|
|
1862
|
+
break;
|
|
1863
|
+
}
|
|
1864
|
+
const data = await response.json();
|
|
1865
|
+
for (const page of data.results) {
|
|
1866
|
+
const counts = this.processPage(store, page, spaceKey);
|
|
1867
|
+
nodesAdded += counts.nodesAdded;
|
|
1868
|
+
edgesAdded += counts.edgesAdded;
|
|
1869
|
+
}
|
|
1870
|
+
nextUrl = data._links?.next ? `${baseUrl}${data._links.next}` : null;
|
|
1871
|
+
}
|
|
1872
|
+
return { nodesAdded, edgesAdded, errors };
|
|
1873
|
+
}
|
|
1874
|
+
processPage(store, page, spaceKey) {
|
|
1875
|
+
const nodeId = `confluence:${page.id}`;
|
|
1876
|
+
store.addNode({
|
|
1877
|
+
id: nodeId,
|
|
1878
|
+
type: "document",
|
|
1879
|
+
name: sanitizeExternalText(page.title, 500),
|
|
1880
|
+
metadata: {
|
|
1881
|
+
source: "confluence",
|
|
1882
|
+
spaceKey,
|
|
1883
|
+
pageId: page.id,
|
|
1884
|
+
status: page.status,
|
|
1885
|
+
url: page._links?.webui ?? ""
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
1889
|
+
const edgesAdded = linkToCode(store, text, nodeId, "documents");
|
|
1890
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1891
|
+
}
|
|
1802
1892
|
};
|
|
1803
1893
|
|
|
1804
1894
|
// src/ingest/connectors/CIConnector.ts
|
|
@@ -2091,22 +2181,25 @@ var GraphEntropyAdapter = class {
|
|
|
2091
2181
|
* 3. Unreachable = code nodes NOT in visited set
|
|
2092
2182
|
*/
|
|
2093
2183
|
computeDeadCodeData() {
|
|
2094
|
-
const
|
|
2184
|
+
const entryPoints = this.findEntryPoints();
|
|
2185
|
+
const visited = this.bfsFromEntryPoints(entryPoints);
|
|
2186
|
+
const unreachableNodes = this.collectUnreachableNodes(visited);
|
|
2187
|
+
return { reachableNodeIds: visited, unreachableNodes, entryPoints };
|
|
2188
|
+
}
|
|
2189
|
+
findEntryPoints() {
|
|
2095
2190
|
const entryPoints = [];
|
|
2096
|
-
for (const node of allFileNodes) {
|
|
2097
|
-
if (node.name === "index.ts" || node.metadata?.entryPoint === true) {
|
|
2098
|
-
entryPoints.push(node.id);
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
2191
|
for (const nodeType of CODE_NODE_TYPES3) {
|
|
2102
|
-
if (nodeType === "file") continue;
|
|
2103
2192
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2104
2193
|
for (const node of nodes) {
|
|
2105
|
-
|
|
2194
|
+
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
2195
|
+
if (isIndexFile || node.metadata?.entryPoint === true) {
|
|
2106
2196
|
entryPoints.push(node.id);
|
|
2107
2197
|
}
|
|
2108
2198
|
}
|
|
2109
2199
|
}
|
|
2200
|
+
return entryPoints;
|
|
2201
|
+
}
|
|
2202
|
+
bfsFromEntryPoints(entryPoints) {
|
|
2110
2203
|
const visited = /* @__PURE__ */ new Set();
|
|
2111
2204
|
const queue = [...entryPoints];
|
|
2112
2205
|
let head = 0;
|
|
@@ -2114,25 +2207,22 @@ var GraphEntropyAdapter = class {
|
|
|
2114
2207
|
const nodeId = queue[head++];
|
|
2115
2208
|
if (visited.has(nodeId)) continue;
|
|
2116
2209
|
visited.add(nodeId);
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
queue.push(edge.to);
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
const containsEdges = this.store.getEdges({ from: nodeId, type: "contains" });
|
|
2130
|
-
for (const edge of containsEdges) {
|
|
2210
|
+
this.enqueueOutboundEdges(nodeId, visited, queue);
|
|
2211
|
+
}
|
|
2212
|
+
return visited;
|
|
2213
|
+
}
|
|
2214
|
+
enqueueOutboundEdges(nodeId, visited, queue) {
|
|
2215
|
+
const edgeTypes = ["imports", "calls", "contains"];
|
|
2216
|
+
for (const edgeType of edgeTypes) {
|
|
2217
|
+
const edges = this.store.getEdges({ from: nodeId, type: edgeType });
|
|
2218
|
+
for (const edge of edges) {
|
|
2131
2219
|
if (!visited.has(edge.to)) {
|
|
2132
2220
|
queue.push(edge.to);
|
|
2133
2221
|
}
|
|
2134
2222
|
}
|
|
2135
2223
|
}
|
|
2224
|
+
}
|
|
2225
|
+
collectUnreachableNodes(visited) {
|
|
2136
2226
|
const unreachableNodes = [];
|
|
2137
2227
|
for (const nodeType of CODE_NODE_TYPES3) {
|
|
2138
2228
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
@@ -2147,11 +2237,7 @@ var GraphEntropyAdapter = class {
|
|
|
2147
2237
|
}
|
|
2148
2238
|
}
|
|
2149
2239
|
}
|
|
2150
|
-
return
|
|
2151
|
-
reachableNodeIds: visited,
|
|
2152
|
-
unreachableNodes,
|
|
2153
|
-
entryPoints
|
|
2154
|
-
};
|
|
2240
|
+
return unreachableNodes;
|
|
2155
2241
|
}
|
|
2156
2242
|
/**
|
|
2157
2243
|
* Count all nodes and edges by type.
|
|
@@ -2202,33 +2288,9 @@ var GraphComplexityAdapter = class {
|
|
|
2202
2288
|
const hotspots = [];
|
|
2203
2289
|
for (const fnNode of functionNodes) {
|
|
2204
2290
|
const complexity = fnNode.metadata?.cyclomaticComplexity ?? 1;
|
|
2205
|
-
const
|
|
2206
|
-
let fileId;
|
|
2207
|
-
for (const edge of containsEdges) {
|
|
2208
|
-
const sourceNode = this.store.getNode(edge.from);
|
|
2209
|
-
if (sourceNode?.type === "file") {
|
|
2210
|
-
fileId = sourceNode.id;
|
|
2211
|
-
break;
|
|
2212
|
-
}
|
|
2213
|
-
if (sourceNode?.type === "class") {
|
|
2214
|
-
const classContainsEdges = this.store.getEdges({ to: sourceNode.id, type: "contains" });
|
|
2215
|
-
for (const classEdge of classContainsEdges) {
|
|
2216
|
-
const parentNode = this.store.getNode(classEdge.from);
|
|
2217
|
-
if (parentNode?.type === "file") {
|
|
2218
|
-
fileId = parentNode.id;
|
|
2219
|
-
break;
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
if (fileId) break;
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2291
|
+
const fileId = this.findContainingFileId(fnNode.id);
|
|
2225
2292
|
if (!fileId) continue;
|
|
2226
|
-
|
|
2227
|
-
if (changeFrequency === void 0) {
|
|
2228
|
-
const referencesEdges = this.store.getEdges({ to: fileId, type: "references" });
|
|
2229
|
-
changeFrequency = referencesEdges.length;
|
|
2230
|
-
fileChangeFrequency.set(fileId, changeFrequency);
|
|
2231
|
-
}
|
|
2293
|
+
const changeFrequency = this.getChangeFrequency(fileId, fileChangeFrequency);
|
|
2232
2294
|
const hotspotScore = changeFrequency * complexity;
|
|
2233
2295
|
const filePath = fnNode.path ?? fileId.replace(/^file:/, "");
|
|
2234
2296
|
hotspots.push({
|
|
@@ -2246,6 +2308,39 @@ var GraphComplexityAdapter = class {
|
|
|
2246
2308
|
);
|
|
2247
2309
|
return { hotspots, percentile95Score };
|
|
2248
2310
|
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Walk the 'contains' edges to find the file node that contains a given function/method.
|
|
2313
|
+
* For methods, walks through the intermediate class node.
|
|
2314
|
+
*/
|
|
2315
|
+
findContainingFileId(nodeId) {
|
|
2316
|
+
const containsEdges = this.store.getEdges({ to: nodeId, type: "contains" });
|
|
2317
|
+
for (const edge of containsEdges) {
|
|
2318
|
+
const sourceNode = this.store.getNode(edge.from);
|
|
2319
|
+
if (sourceNode?.type === "file") return sourceNode.id;
|
|
2320
|
+
if (sourceNode?.type === "class") {
|
|
2321
|
+
const fileId = this.findParentFileOfClass(sourceNode.id);
|
|
2322
|
+
if (fileId) return fileId;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
return void 0;
|
|
2326
|
+
}
|
|
2327
|
+
findParentFileOfClass(classNodeId) {
|
|
2328
|
+
const classContainsEdges = this.store.getEdges({ to: classNodeId, type: "contains" });
|
|
2329
|
+
for (const classEdge of classContainsEdges) {
|
|
2330
|
+
const parentNode = this.store.getNode(classEdge.from);
|
|
2331
|
+
if (parentNode?.type === "file") return parentNode.id;
|
|
2332
|
+
}
|
|
2333
|
+
return void 0;
|
|
2334
|
+
}
|
|
2335
|
+
getChangeFrequency(fileId, cache) {
|
|
2336
|
+
let freq = cache.get(fileId);
|
|
2337
|
+
if (freq === void 0) {
|
|
2338
|
+
const referencesEdges = this.store.getEdges({ to: fileId, type: "references" });
|
|
2339
|
+
freq = referencesEdges.length;
|
|
2340
|
+
cache.set(fileId, freq);
|
|
2341
|
+
}
|
|
2342
|
+
return freq;
|
|
2343
|
+
}
|
|
2249
2344
|
computePercentile(descendingScores, percentile) {
|
|
2250
2345
|
if (descendingScores.length === 0) return 0;
|
|
2251
2346
|
const ascending = [...descendingScores].sort((a, b) => a - b);
|
|
@@ -2893,6 +2988,23 @@ var STOP_WORDS2 = /* @__PURE__ */ new Set([
|
|
|
2893
2988
|
var PASCAL_OR_CAMEL_RE = /\b([A-Z][a-z]+[A-Za-z]*[a-z][A-Za-z]*|[a-z]+[A-Z][A-Za-z]*)\b/g;
|
|
2894
2989
|
var FILE_PATH_RE = /(?:\.\/|[a-zA-Z0-9_-]+\/)[a-zA-Z0-9_\-./]+\.[a-zA-Z]{1,10}/g;
|
|
2895
2990
|
var QUOTED_RE = /["']([^"']+)["']/g;
|
|
2991
|
+
function isSkippableWord(cleaned, allConsumed) {
|
|
2992
|
+
if (allConsumed.has(cleaned)) return true;
|
|
2993
|
+
const lower = cleaned.toLowerCase();
|
|
2994
|
+
if (STOP_WORDS2.has(lower)) return true;
|
|
2995
|
+
if (INTENT_KEYWORDS.has(lower)) return true;
|
|
2996
|
+
if (cleaned === cleaned.toUpperCase() && /^[A-Z]+$/.test(cleaned)) return true;
|
|
2997
|
+
return false;
|
|
2998
|
+
}
|
|
2999
|
+
function buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed) {
|
|
3000
|
+
const quotedWords = /* @__PURE__ */ new Set();
|
|
3001
|
+
for (const q of quotedConsumed) {
|
|
3002
|
+
for (const w of q.split(/\s+/)) {
|
|
3003
|
+
if (w.length > 0) quotedWords.add(w);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
return /* @__PURE__ */ new Set([...quotedConsumed, ...quotedWords, ...casingConsumed, ...pathConsumed]);
|
|
3007
|
+
}
|
|
2896
3008
|
var EntityExtractor = class {
|
|
2897
3009
|
/**
|
|
2898
3010
|
* Extract candidate entity mentions from a natural language query.
|
|
@@ -2933,27 +3045,12 @@ var EntityExtractor = class {
|
|
|
2933
3045
|
add(path6);
|
|
2934
3046
|
pathConsumed.add(path6);
|
|
2935
3047
|
}
|
|
2936
|
-
const
|
|
2937
|
-
for (const q of quotedConsumed) {
|
|
2938
|
-
for (const w of q.split(/\s+/)) {
|
|
2939
|
-
if (w.length > 0) quotedWords.add(w);
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
const allConsumed = /* @__PURE__ */ new Set([
|
|
2943
|
-
...quotedConsumed,
|
|
2944
|
-
...quotedWords,
|
|
2945
|
-
...casingConsumed,
|
|
2946
|
-
...pathConsumed
|
|
2947
|
-
]);
|
|
3048
|
+
const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
|
|
2948
3049
|
const words = trimmed.split(/\s+/);
|
|
2949
3050
|
for (const raw of words) {
|
|
2950
3051
|
const cleaned = raw.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "");
|
|
2951
3052
|
if (cleaned.length === 0) continue;
|
|
2952
|
-
|
|
2953
|
-
if (allConsumed.has(cleaned)) continue;
|
|
2954
|
-
if (STOP_WORDS2.has(lower)) continue;
|
|
2955
|
-
if (INTENT_KEYWORDS.has(lower)) continue;
|
|
2956
|
-
if (cleaned === cleaned.toUpperCase() && /^[A-Z]+$/.test(cleaned)) continue;
|
|
3053
|
+
if (isSkippableWord(cleaned, allConsumed)) continue;
|
|
2957
3054
|
add(cleaned);
|
|
2958
3055
|
}
|
|
2959
3056
|
return result;
|
|
@@ -3276,14 +3373,20 @@ var Assembler = class {
|
|
|
3276
3373
|
const fusion = this.getFusionLayer();
|
|
3277
3374
|
const topResults = fusion.search(intent, 10);
|
|
3278
3375
|
if (topResults.length === 0) {
|
|
3279
|
-
return {
|
|
3280
|
-
nodes: [],
|
|
3281
|
-
edges: [],
|
|
3282
|
-
tokenEstimate: 0,
|
|
3283
|
-
intent,
|
|
3284
|
-
truncated: false
|
|
3285
|
-
};
|
|
3376
|
+
return { nodes: [], edges: [], tokenEstimate: 0, intent, truncated: false };
|
|
3286
3377
|
}
|
|
3378
|
+
const { nodeMap, collectedEdges, nodeScores } = this.expandSearchResults(topResults);
|
|
3379
|
+
const sortedNodes = Array.from(nodeMap.values()).sort((a, b) => {
|
|
3380
|
+
return (nodeScores.get(b.id) ?? 0) - (nodeScores.get(a.id) ?? 0);
|
|
3381
|
+
});
|
|
3382
|
+
const { keptNodes, tokenEstimate, truncated } = this.truncateToFit(sortedNodes, tokenBudget);
|
|
3383
|
+
const keptNodeIds = new Set(keptNodes.map((n) => n.id));
|
|
3384
|
+
const keptEdges = collectedEdges.filter(
|
|
3385
|
+
(e) => keptNodeIds.has(e.from) && keptNodeIds.has(e.to)
|
|
3386
|
+
);
|
|
3387
|
+
return { nodes: keptNodes, edges: keptEdges, tokenEstimate, intent, truncated };
|
|
3388
|
+
}
|
|
3389
|
+
expandSearchResults(topResults) {
|
|
3287
3390
|
const contextQL = new ContextQL(this.store);
|
|
3288
3391
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
3289
3392
|
const edgeSet = /* @__PURE__ */ new Set();
|
|
@@ -3311,9 +3414,9 @@ var Assembler = class {
|
|
|
3311
3414
|
}
|
|
3312
3415
|
}
|
|
3313
3416
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3417
|
+
return { nodeMap, collectedEdges, nodeScores };
|
|
3418
|
+
}
|
|
3419
|
+
truncateToFit(sortedNodes, tokenBudget) {
|
|
3317
3420
|
let tokenEstimate = 0;
|
|
3318
3421
|
const keptNodes = [];
|
|
3319
3422
|
let truncated = false;
|
|
@@ -3326,17 +3429,7 @@ var Assembler = class {
|
|
|
3326
3429
|
tokenEstimate += nodeTokens;
|
|
3327
3430
|
keptNodes.push(node);
|
|
3328
3431
|
}
|
|
3329
|
-
|
|
3330
|
-
const keptEdges = collectedEdges.filter(
|
|
3331
|
-
(e) => keptNodeIds.has(e.from) && keptNodeIds.has(e.to)
|
|
3332
|
-
);
|
|
3333
|
-
return {
|
|
3334
|
-
nodes: keptNodes,
|
|
3335
|
-
edges: keptEdges,
|
|
3336
|
-
tokenEstimate,
|
|
3337
|
-
intent,
|
|
3338
|
-
truncated
|
|
3339
|
-
};
|
|
3432
|
+
return { keptNodes, tokenEstimate, truncated };
|
|
3340
3433
|
}
|
|
3341
3434
|
/**
|
|
3342
3435
|
* Compute a token budget allocation across node types.
|
|
@@ -3509,8 +3602,8 @@ var GraphConstraintAdapter = class {
|
|
|
3509
3602
|
const { edges } = this.computeDependencyGraph();
|
|
3510
3603
|
const violations = [];
|
|
3511
3604
|
for (const edge of edges) {
|
|
3512
|
-
const fromRelative = (0, import_node_path2.relative)(rootDir, edge.from);
|
|
3513
|
-
const toRelative = (0, import_node_path2.relative)(rootDir, edge.to);
|
|
3605
|
+
const fromRelative = (0, import_node_path2.relative)(rootDir, edge.from).replaceAll("\\", "/");
|
|
3606
|
+
const toRelative = (0, import_node_path2.relative)(rootDir, edge.to).replaceAll("\\", "/");
|
|
3514
3607
|
const fromLayer = this.resolveLayer(fromRelative, layers);
|
|
3515
3608
|
const toLayer = this.resolveLayer(toRelative, layers);
|
|
3516
3609
|
if (!fromLayer || !toLayer) continue;
|