@harness-engineering/graph 0.3.2 → 0.3.3
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 +2 -2
package/dist/index.mjs
CHANGED
|
@@ -357,6 +357,53 @@ var VectorStore = class _VectorStore {
|
|
|
357
357
|
};
|
|
358
358
|
|
|
359
359
|
// src/query/ContextQL.ts
|
|
360
|
+
function edgeKey(e) {
|
|
361
|
+
return `${e.from}|${e.to}|${e.type}`;
|
|
362
|
+
}
|
|
363
|
+
function addEdge(state, edge) {
|
|
364
|
+
const key = edgeKey(edge);
|
|
365
|
+
if (!state.edgeSet.has(key)) {
|
|
366
|
+
state.edgeSet.add(key);
|
|
367
|
+
state.resultEdges.push(edge);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function shouldPruneNode(neighbor, pruneObservability, params) {
|
|
371
|
+
if (pruneObservability && OBSERVABILITY_TYPES.has(neighbor.type)) return true;
|
|
372
|
+
if (params.includeTypes && !params.includeTypes.includes(neighbor.type)) return true;
|
|
373
|
+
if (params.excludeTypes && params.excludeTypes.includes(neighbor.type)) return true;
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
function isEdgeExcluded(edge, params) {
|
|
377
|
+
return !!(params.includeEdges && !params.includeEdges.includes(edge.type));
|
|
378
|
+
}
|
|
379
|
+
function processNeighbor(store, edge, neighborId, nextDepth, queue, state, pruneObservability, params) {
|
|
380
|
+
if (isEdgeExcluded(edge, params)) return;
|
|
381
|
+
if (state.visited.has(neighborId)) {
|
|
382
|
+
addEdge(state, edge);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const neighbor = store.getNode(neighborId);
|
|
386
|
+
if (!neighbor) return;
|
|
387
|
+
state.visited.add(neighborId);
|
|
388
|
+
if (shouldPruneNode(neighbor, pruneObservability, params)) {
|
|
389
|
+
state.pruned++;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
state.resultNodeMap.set(neighborId, neighbor);
|
|
393
|
+
addEdge(state, edge);
|
|
394
|
+
queue.push({ id: neighborId, depth: nextDepth });
|
|
395
|
+
}
|
|
396
|
+
function addCrossEdges(store, state) {
|
|
397
|
+
const resultNodeIds = new Set(state.resultNodeMap.keys());
|
|
398
|
+
for (const nodeId of resultNodeIds) {
|
|
399
|
+
const outEdges = store.getEdges({ from: nodeId });
|
|
400
|
+
for (const edge of outEdges) {
|
|
401
|
+
if (resultNodeIds.has(edge.to)) {
|
|
402
|
+
addEdge(state, edge);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
360
407
|
var ContextQL = class {
|
|
361
408
|
store;
|
|
362
409
|
constructor(store) {
|
|
@@ -366,89 +413,69 @@ var ContextQL = class {
|
|
|
366
413
|
const maxDepth = params.maxDepth ?? 3;
|
|
367
414
|
const bidirectional = params.bidirectional ?? false;
|
|
368
415
|
const pruneObservability = params.pruneObservability ?? true;
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const addEdge = (edge) => {
|
|
377
|
-
const key = edgeKey(edge);
|
|
378
|
-
if (!edgeSet.has(key)) {
|
|
379
|
-
edgeSet.add(key);
|
|
380
|
-
resultEdges.push(edge);
|
|
381
|
-
}
|
|
416
|
+
const state = {
|
|
417
|
+
visited: /* @__PURE__ */ new Set(),
|
|
418
|
+
resultNodeMap: /* @__PURE__ */ new Map(),
|
|
419
|
+
resultEdges: [],
|
|
420
|
+
edgeSet: /* @__PURE__ */ new Set(),
|
|
421
|
+
pruned: 0,
|
|
422
|
+
depthReached: 0
|
|
382
423
|
};
|
|
383
424
|
const queue = [];
|
|
384
|
-
|
|
425
|
+
this.seedRootNodes(params.rootNodeIds, state, queue);
|
|
426
|
+
this.runBFS(queue, maxDepth, bidirectional, pruneObservability, params, state);
|
|
427
|
+
addCrossEdges(this.store, state);
|
|
428
|
+
return {
|
|
429
|
+
nodes: Array.from(state.resultNodeMap.values()),
|
|
430
|
+
edges: state.resultEdges,
|
|
431
|
+
stats: {
|
|
432
|
+
totalTraversed: state.visited.size,
|
|
433
|
+
totalReturned: state.resultNodeMap.size,
|
|
434
|
+
pruned: state.pruned,
|
|
435
|
+
depthReached: state.depthReached
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
seedRootNodes(rootNodeIds, state, queue) {
|
|
440
|
+
for (const rootId of rootNodeIds) {
|
|
385
441
|
const node = this.store.getNode(rootId);
|
|
386
442
|
if (node) {
|
|
387
|
-
visited.add(rootId);
|
|
388
|
-
resultNodeMap.set(rootId, node);
|
|
443
|
+
state.visited.add(rootId);
|
|
444
|
+
state.resultNodeMap.set(rootId, node);
|
|
389
445
|
queue.push({ id: rootId, depth: 0 });
|
|
390
446
|
}
|
|
391
447
|
}
|
|
448
|
+
}
|
|
449
|
+
runBFS(queue, maxDepth, bidirectional, pruneObservability, params, state) {
|
|
392
450
|
let head = 0;
|
|
393
451
|
while (head < queue.length) {
|
|
394
452
|
const entry = queue[head++];
|
|
395
453
|
const { id: currentId, depth } = entry;
|
|
396
454
|
if (depth >= maxDepth) continue;
|
|
397
455
|
const nextDepth = depth + 1;
|
|
398
|
-
if (nextDepth > depthReached) depthReached = nextDepth;
|
|
399
|
-
const
|
|
400
|
-
const inEdges = bidirectional ? this.store.getEdges({ to: currentId }) : [];
|
|
401
|
-
const allEdges = [
|
|
402
|
-
...outEdges.map((e) => ({ edge: e, neighborId: e.to })),
|
|
403
|
-
...inEdges.map((e) => ({ edge: e, neighborId: e.from }))
|
|
404
|
-
];
|
|
456
|
+
if (nextDepth > state.depthReached) state.depthReached = nextDepth;
|
|
457
|
+
const allEdges = this.gatherEdges(currentId, bidirectional);
|
|
405
458
|
for (const { edge, neighborId } of allEdges) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (pruneObservability && OBSERVABILITY_TYPES.has(neighbor.type)) {
|
|
417
|
-
pruned++;
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
if (params.includeTypes && !params.includeTypes.includes(neighbor.type)) {
|
|
421
|
-
pruned++;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
if (params.excludeTypes && params.excludeTypes.includes(neighbor.type)) {
|
|
425
|
-
pruned++;
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
resultNodeMap.set(neighborId, neighbor);
|
|
429
|
-
addEdge(edge);
|
|
430
|
-
queue.push({ id: neighborId, depth: nextDepth });
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
const resultNodeIds = new Set(resultNodeMap.keys());
|
|
434
|
-
for (const nodeId of resultNodeIds) {
|
|
435
|
-
const outEdges = this.store.getEdges({ from: nodeId });
|
|
436
|
-
for (const edge of outEdges) {
|
|
437
|
-
if (resultNodeIds.has(edge.to)) {
|
|
438
|
-
addEdge(edge);
|
|
439
|
-
}
|
|
459
|
+
processNeighbor(
|
|
460
|
+
this.store,
|
|
461
|
+
edge,
|
|
462
|
+
neighborId,
|
|
463
|
+
nextDepth,
|
|
464
|
+
queue,
|
|
465
|
+
state,
|
|
466
|
+
pruneObservability,
|
|
467
|
+
params
|
|
468
|
+
);
|
|
440
469
|
}
|
|
441
470
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
};
|
|
471
|
+
}
|
|
472
|
+
gatherEdges(nodeId, bidirectional) {
|
|
473
|
+
const outEdges = this.store.getEdges({ from: nodeId });
|
|
474
|
+
const inEdges = bidirectional ? this.store.getEdges({ to: nodeId }) : [];
|
|
475
|
+
return [
|
|
476
|
+
...outEdges.map((e) => ({ edge: e, neighborId: e.to })),
|
|
477
|
+
...inEdges.map((e) => ({ edge: e, neighborId: e.from }))
|
|
478
|
+
];
|
|
452
479
|
}
|
|
453
480
|
};
|
|
454
481
|
|
|
@@ -501,6 +528,15 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
501
528
|
// src/ingest/CodeIngestor.ts
|
|
502
529
|
import * as fs from "fs/promises";
|
|
503
530
|
import * as path from "path";
|
|
531
|
+
var SKIP_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "if", "for", "while", "switch"]);
|
|
532
|
+
function countBraces(line) {
|
|
533
|
+
let net = 0;
|
|
534
|
+
for (const ch of line) {
|
|
535
|
+
if (ch === "{") net++;
|
|
536
|
+
else if (ch === "}") net--;
|
|
537
|
+
}
|
|
538
|
+
return net;
|
|
539
|
+
}
|
|
504
540
|
var CodeIngestor = class {
|
|
505
541
|
constructor(store) {
|
|
506
542
|
this.store = store;
|
|
@@ -515,41 +551,9 @@ var CodeIngestor = class {
|
|
|
515
551
|
const fileContents = /* @__PURE__ */ new Map();
|
|
516
552
|
for (const filePath of files) {
|
|
517
553
|
try {
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const fileId = `file:${relativePath}`;
|
|
522
|
-
fileContents.set(relativePath, content);
|
|
523
|
-
const fileNode = {
|
|
524
|
-
id: fileId,
|
|
525
|
-
type: "file",
|
|
526
|
-
name: path.basename(filePath),
|
|
527
|
-
path: relativePath,
|
|
528
|
-
metadata: { language: this.detectLanguage(filePath) },
|
|
529
|
-
lastModified: stat2.mtime.toISOString()
|
|
530
|
-
};
|
|
531
|
-
this.store.addNode(fileNode);
|
|
532
|
-
nodesAdded++;
|
|
533
|
-
const symbols = this.extractSymbols(content, fileId, relativePath);
|
|
534
|
-
for (const { node, edge } of symbols) {
|
|
535
|
-
this.store.addNode(node);
|
|
536
|
-
this.store.addEdge(edge);
|
|
537
|
-
nodesAdded++;
|
|
538
|
-
edgesAdded++;
|
|
539
|
-
if (node.type === "function" || node.type === "method") {
|
|
540
|
-
let files2 = nameToFiles.get(node.name);
|
|
541
|
-
if (!files2) {
|
|
542
|
-
files2 = /* @__PURE__ */ new Set();
|
|
543
|
-
nameToFiles.set(node.name, files2);
|
|
544
|
-
}
|
|
545
|
-
files2.add(relativePath);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
const imports = await this.extractImports(content, fileId, relativePath, rootDir);
|
|
549
|
-
for (const edge of imports) {
|
|
550
|
-
this.store.addEdge(edge);
|
|
551
|
-
edgesAdded++;
|
|
552
|
-
}
|
|
554
|
+
const result = await this.processFile(filePath, rootDir, nameToFiles, fileContents);
|
|
555
|
+
nodesAdded += result.nodesAdded;
|
|
556
|
+
edgesAdded += result.edgesAdded;
|
|
553
557
|
} catch (err) {
|
|
554
558
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
555
559
|
}
|
|
@@ -568,6 +572,48 @@ var CodeIngestor = class {
|
|
|
568
572
|
durationMs: Date.now() - start
|
|
569
573
|
};
|
|
570
574
|
}
|
|
575
|
+
async processFile(filePath, rootDir, nameToFiles, fileContents) {
|
|
576
|
+
let nodesAdded = 0;
|
|
577
|
+
let edgesAdded = 0;
|
|
578
|
+
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
579
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
580
|
+
const stat2 = await fs.stat(filePath);
|
|
581
|
+
const fileId = `file:${relativePath}`;
|
|
582
|
+
fileContents.set(relativePath, content);
|
|
583
|
+
const fileNode = {
|
|
584
|
+
id: fileId,
|
|
585
|
+
type: "file",
|
|
586
|
+
name: path.basename(filePath),
|
|
587
|
+
path: relativePath,
|
|
588
|
+
metadata: { language: this.detectLanguage(filePath) },
|
|
589
|
+
lastModified: stat2.mtime.toISOString()
|
|
590
|
+
};
|
|
591
|
+
this.store.addNode(fileNode);
|
|
592
|
+
nodesAdded++;
|
|
593
|
+
const symbols = this.extractSymbols(content, fileId, relativePath);
|
|
594
|
+
for (const { node, edge } of symbols) {
|
|
595
|
+
this.store.addNode(node);
|
|
596
|
+
this.store.addEdge(edge);
|
|
597
|
+
nodesAdded++;
|
|
598
|
+
edgesAdded++;
|
|
599
|
+
this.trackCallable(node, relativePath, nameToFiles);
|
|
600
|
+
}
|
|
601
|
+
const imports = await this.extractImports(content, fileId, relativePath, rootDir);
|
|
602
|
+
for (const edge of imports) {
|
|
603
|
+
this.store.addEdge(edge);
|
|
604
|
+
edgesAdded++;
|
|
605
|
+
}
|
|
606
|
+
return { nodesAdded, edgesAdded };
|
|
607
|
+
}
|
|
608
|
+
trackCallable(node, relativePath, nameToFiles) {
|
|
609
|
+
if (node.type !== "function" && node.type !== "method") return;
|
|
610
|
+
let files = nameToFiles.get(node.name);
|
|
611
|
+
if (!files) {
|
|
612
|
+
files = /* @__PURE__ */ new Set();
|
|
613
|
+
nameToFiles.set(node.name, files);
|
|
614
|
+
}
|
|
615
|
+
files.add(relativePath);
|
|
616
|
+
}
|
|
571
617
|
async findSourceFiles(dir) {
|
|
572
618
|
const results = [];
|
|
573
619
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -584,149 +630,152 @@ var CodeIngestor = class {
|
|
|
584
630
|
extractSymbols(content, fileId, relativePath) {
|
|
585
631
|
const results = [];
|
|
586
632
|
const lines = content.split("\n");
|
|
587
|
-
|
|
588
|
-
let currentClassId = null;
|
|
589
|
-
let braceDepth = 0;
|
|
590
|
-
let insideClass = false;
|
|
633
|
+
const ctx = { className: null, classId: null, insideClass: false, braceDepth: 0 };
|
|
591
634
|
for (let i = 0; i < lines.length; i++) {
|
|
592
635
|
const line = lines[i];
|
|
593
|
-
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
id,
|
|
601
|
-
type: "function",
|
|
602
|
-
name,
|
|
603
|
-
path: relativePath,
|
|
604
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
605
|
-
metadata: {
|
|
606
|
-
exported: line.includes("export"),
|
|
607
|
-
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
608
|
-
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
609
|
-
lineCount: endLine - i,
|
|
610
|
-
parameterCount: this.countParameters(line)
|
|
611
|
-
}
|
|
612
|
-
},
|
|
613
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
614
|
-
});
|
|
615
|
-
if (!insideClass) {
|
|
616
|
-
currentClassName = null;
|
|
617
|
-
currentClassId = null;
|
|
618
|
-
}
|
|
619
|
-
continue;
|
|
620
|
-
}
|
|
621
|
-
const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
|
|
622
|
-
if (classMatch) {
|
|
623
|
-
const name = classMatch[1];
|
|
624
|
-
const id = `class:${relativePath}:${name}`;
|
|
625
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
626
|
-
results.push({
|
|
627
|
-
node: {
|
|
628
|
-
id,
|
|
629
|
-
type: "class",
|
|
630
|
-
name,
|
|
631
|
-
path: relativePath,
|
|
632
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
633
|
-
metadata: { exported: line.includes("export") }
|
|
634
|
-
},
|
|
635
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
636
|
-
});
|
|
637
|
-
currentClassName = name;
|
|
638
|
-
currentClassId = id;
|
|
639
|
-
insideClass = true;
|
|
640
|
-
braceDepth = 0;
|
|
641
|
-
for (const ch of line) {
|
|
642
|
-
if (ch === "{") braceDepth++;
|
|
643
|
-
if (ch === "}") braceDepth--;
|
|
644
|
-
}
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
const ifaceMatch = line.match(/(?:export\s+)?interface\s+(\w+)/);
|
|
648
|
-
if (ifaceMatch) {
|
|
649
|
-
const name = ifaceMatch[1];
|
|
650
|
-
const id = `interface:${relativePath}:${name}`;
|
|
651
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
652
|
-
results.push({
|
|
653
|
-
node: {
|
|
654
|
-
id,
|
|
655
|
-
type: "interface",
|
|
656
|
-
name,
|
|
657
|
-
path: relativePath,
|
|
658
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
659
|
-
metadata: { exported: line.includes("export") }
|
|
660
|
-
},
|
|
661
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
662
|
-
});
|
|
663
|
-
currentClassName = null;
|
|
664
|
-
currentClassId = null;
|
|
665
|
-
insideClass = false;
|
|
666
|
-
continue;
|
|
667
|
-
}
|
|
668
|
-
if (insideClass) {
|
|
669
|
-
for (const ch of line) {
|
|
670
|
-
if (ch === "{") braceDepth++;
|
|
671
|
-
if (ch === "}") braceDepth--;
|
|
672
|
-
}
|
|
673
|
-
if (braceDepth <= 0) {
|
|
674
|
-
currentClassName = null;
|
|
675
|
-
currentClassId = null;
|
|
676
|
-
insideClass = false;
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (insideClass && currentClassName && currentClassId) {
|
|
681
|
-
const methodMatch = line.match(
|
|
682
|
-
/^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/
|
|
683
|
-
);
|
|
684
|
-
if (methodMatch) {
|
|
685
|
-
const methodName = methodMatch[1];
|
|
686
|
-
if (methodName === "constructor" || methodName === "if" || methodName === "for" || methodName === "while" || methodName === "switch")
|
|
687
|
-
continue;
|
|
688
|
-
const id = `method:${relativePath}:${currentClassName}.${methodName}`;
|
|
689
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
690
|
-
results.push({
|
|
691
|
-
node: {
|
|
692
|
-
id,
|
|
693
|
-
type: "method",
|
|
694
|
-
name: methodName,
|
|
695
|
-
path: relativePath,
|
|
696
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
697
|
-
metadata: {
|
|
698
|
-
className: currentClassName,
|
|
699
|
-
exported: false,
|
|
700
|
-
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
701
|
-
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
702
|
-
lineCount: endLine - i,
|
|
703
|
-
parameterCount: this.countParameters(line)
|
|
704
|
-
}
|
|
705
|
-
},
|
|
706
|
-
edge: { from: currentClassId, to: id, type: "contains" }
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
continue;
|
|
710
|
-
}
|
|
711
|
-
const varMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)/);
|
|
712
|
-
if (varMatch) {
|
|
713
|
-
const name = varMatch[1];
|
|
714
|
-
const id = `variable:${relativePath}:${name}`;
|
|
715
|
-
results.push({
|
|
716
|
-
node: {
|
|
717
|
-
id,
|
|
718
|
-
type: "variable",
|
|
719
|
-
name,
|
|
720
|
-
path: relativePath,
|
|
721
|
-
location: { fileId, startLine: i + 1, endLine: i + 1 },
|
|
722
|
-
metadata: { exported: line.includes("export") }
|
|
723
|
-
},
|
|
724
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
725
|
-
});
|
|
726
|
-
}
|
|
636
|
+
if (this.tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
637
|
+
if (this.tryExtractClass(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
638
|
+
if (this.tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
639
|
+
if (this.updateClassContext(line, ctx)) continue;
|
|
640
|
+
if (this.tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
641
|
+
if (ctx.insideClass) continue;
|
|
642
|
+
this.tryExtractVariable(line, i, fileId, relativePath, results);
|
|
727
643
|
}
|
|
728
644
|
return results;
|
|
729
645
|
}
|
|
646
|
+
tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results) {
|
|
647
|
+
const fnMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
|
|
648
|
+
if (!fnMatch) return false;
|
|
649
|
+
const name = fnMatch[1];
|
|
650
|
+
const id = `function:${relativePath}:${name}`;
|
|
651
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
652
|
+
results.push({
|
|
653
|
+
node: {
|
|
654
|
+
id,
|
|
655
|
+
type: "function",
|
|
656
|
+
name,
|
|
657
|
+
path: relativePath,
|
|
658
|
+
location: { fileId, startLine: i + 1, endLine },
|
|
659
|
+
metadata: {
|
|
660
|
+
exported: line.includes("export"),
|
|
661
|
+
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
662
|
+
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
663
|
+
lineCount: endLine - i,
|
|
664
|
+
parameterCount: this.countParameters(line)
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
edge: { from: fileId, to: id, type: "contains" }
|
|
668
|
+
});
|
|
669
|
+
if (!ctx.insideClass) {
|
|
670
|
+
ctx.className = null;
|
|
671
|
+
ctx.classId = null;
|
|
672
|
+
}
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
tryExtractClass(line, lines, i, fileId, relativePath, ctx, results) {
|
|
676
|
+
const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
|
|
677
|
+
if (!classMatch) return false;
|
|
678
|
+
const name = classMatch[1];
|
|
679
|
+
const id = `class:${relativePath}:${name}`;
|
|
680
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
681
|
+
results.push({
|
|
682
|
+
node: {
|
|
683
|
+
id,
|
|
684
|
+
type: "class",
|
|
685
|
+
name,
|
|
686
|
+
path: relativePath,
|
|
687
|
+
location: { fileId, startLine: i + 1, endLine },
|
|
688
|
+
metadata: { exported: line.includes("export") }
|
|
689
|
+
},
|
|
690
|
+
edge: { from: fileId, to: id, type: "contains" }
|
|
691
|
+
});
|
|
692
|
+
ctx.className = name;
|
|
693
|
+
ctx.classId = id;
|
|
694
|
+
ctx.insideClass = true;
|
|
695
|
+
ctx.braceDepth = countBraces(line);
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results) {
|
|
699
|
+
const ifaceMatch = line.match(/(?:export\s+)?interface\s+(\w+)/);
|
|
700
|
+
if (!ifaceMatch) return false;
|
|
701
|
+
const name = ifaceMatch[1];
|
|
702
|
+
const id = `interface:${relativePath}:${name}`;
|
|
703
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
704
|
+
results.push({
|
|
705
|
+
node: {
|
|
706
|
+
id,
|
|
707
|
+
type: "interface",
|
|
708
|
+
name,
|
|
709
|
+
path: relativePath,
|
|
710
|
+
location: { fileId, startLine: i + 1, endLine },
|
|
711
|
+
metadata: { exported: line.includes("export") }
|
|
712
|
+
},
|
|
713
|
+
edge: { from: fileId, to: id, type: "contains" }
|
|
714
|
+
});
|
|
715
|
+
ctx.className = null;
|
|
716
|
+
ctx.classId = null;
|
|
717
|
+
ctx.insideClass = false;
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
/** Update brace tracking; returns true when line is consumed (class ended or tracked). */
|
|
721
|
+
updateClassContext(line, ctx) {
|
|
722
|
+
if (!ctx.insideClass) return false;
|
|
723
|
+
ctx.braceDepth += countBraces(line);
|
|
724
|
+
if (ctx.braceDepth <= 0) {
|
|
725
|
+
ctx.className = null;
|
|
726
|
+
ctx.classId = null;
|
|
727
|
+
ctx.insideClass = false;
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results) {
|
|
733
|
+
if (!ctx.insideClass || !ctx.className || !ctx.classId) return false;
|
|
734
|
+
const methodMatch = line.match(
|
|
735
|
+
/^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/
|
|
736
|
+
);
|
|
737
|
+
if (!methodMatch) return false;
|
|
738
|
+
const methodName = methodMatch[1];
|
|
739
|
+
if (SKIP_METHOD_NAMES.has(methodName)) return false;
|
|
740
|
+
const id = `method:${relativePath}:${ctx.className}.${methodName}`;
|
|
741
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
742
|
+
results.push({
|
|
743
|
+
node: {
|
|
744
|
+
id,
|
|
745
|
+
type: "method",
|
|
746
|
+
name: methodName,
|
|
747
|
+
path: relativePath,
|
|
748
|
+
location: { fileId, startLine: i + 1, endLine },
|
|
749
|
+
metadata: {
|
|
750
|
+
className: ctx.className,
|
|
751
|
+
exported: false,
|
|
752
|
+
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
753
|
+
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
754
|
+
lineCount: endLine - i,
|
|
755
|
+
parameterCount: this.countParameters(line)
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
edge: { from: ctx.classId, to: id, type: "contains" }
|
|
759
|
+
});
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
tryExtractVariable(line, i, fileId, relativePath, results) {
|
|
763
|
+
const varMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)/);
|
|
764
|
+
if (!varMatch) return;
|
|
765
|
+
const name = varMatch[1];
|
|
766
|
+
const id = `variable:${relativePath}:${name}`;
|
|
767
|
+
results.push({
|
|
768
|
+
node: {
|
|
769
|
+
id,
|
|
770
|
+
type: "variable",
|
|
771
|
+
name,
|
|
772
|
+
path: relativePath,
|
|
773
|
+
location: { fileId, startLine: i + 1, endLine: i + 1 },
|
|
774
|
+
metadata: { exported: line.includes("export") }
|
|
775
|
+
},
|
|
776
|
+
edge: { from: fileId, to: id, type: "contains" }
|
|
777
|
+
});
|
|
778
|
+
}
|
|
730
779
|
/**
|
|
731
780
|
* Find the closing brace for a construct starting at the given line.
|
|
732
781
|
* Uses a simple brace-counting heuristic. Returns 1-indexed line number.
|
|
@@ -1346,17 +1395,33 @@ var KnowledgeIngestor = class {
|
|
|
1346
1395
|
|
|
1347
1396
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
1348
1397
|
var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1398
|
+
var SANITIZE_RULES = [
|
|
1399
|
+
// Strip XML/HTML-like instruction tags that could be interpreted as system prompts
|
|
1400
|
+
{
|
|
1401
|
+
pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
|
|
1402
|
+
replacement: ""
|
|
1403
|
+
},
|
|
1404
|
+
// Strip markdown-style system prompt markers (including trailing space)
|
|
1405
|
+
{
|
|
1406
|
+
pattern: /^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim,
|
|
1407
|
+
replacement: ""
|
|
1408
|
+
},
|
|
1409
|
+
// Strip common injection prefixes
|
|
1410
|
+
{
|
|
1411
|
+
pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
|
|
1412
|
+
replacement: "[filtered]"
|
|
1413
|
+
},
|
|
1414
|
+
// Strip "you are now" re-roling attempts (only when followed by AI/agent role words)
|
|
1415
|
+
{
|
|
1416
|
+
pattern: /you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
|
|
1417
|
+
replacement: "[filtered]"
|
|
1418
|
+
}
|
|
1419
|
+
];
|
|
1349
1420
|
function sanitizeExternalText(text, maxLength = 2e3) {
|
|
1350
|
-
let sanitized = text
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
/(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
|
|
1355
|
-
"[filtered]"
|
|
1356
|
-
).replace(
|
|
1357
|
-
/you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
|
|
1358
|
-
"[filtered]"
|
|
1359
|
-
);
|
|
1421
|
+
let sanitized = text;
|
|
1422
|
+
for (const rule of SANITIZE_RULES) {
|
|
1423
|
+
sanitized = sanitized.replace(rule.pattern, rule.replacement);
|
|
1424
|
+
}
|
|
1360
1425
|
if (sanitized.length > maxLength) {
|
|
1361
1426
|
sanitized = sanitized.slice(0, maxLength) + "\u2026";
|
|
1362
1427
|
}
|
|
@@ -1456,6 +1521,28 @@ var SyncManager = class {
|
|
|
1456
1521
|
};
|
|
1457
1522
|
|
|
1458
1523
|
// src/ingest/connectors/JiraConnector.ts
|
|
1524
|
+
function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
|
|
1525
|
+
return {
|
|
1526
|
+
nodesAdded,
|
|
1527
|
+
nodesUpdated: 0,
|
|
1528
|
+
edgesAdded,
|
|
1529
|
+
edgesUpdated: 0,
|
|
1530
|
+
errors,
|
|
1531
|
+
durationMs: Date.now() - start
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
function buildJql(config) {
|
|
1535
|
+
const project2 = config.project;
|
|
1536
|
+
let jql = project2 ? `project=${project2}` : "";
|
|
1537
|
+
const filters = config.filters;
|
|
1538
|
+
if (filters?.status?.length) {
|
|
1539
|
+
jql += `${jql ? " AND " : ""}status IN (${filters.status.map((s) => `"${s}"`).join(",")})`;
|
|
1540
|
+
}
|
|
1541
|
+
if (filters?.labels?.length) {
|
|
1542
|
+
jql += `${jql ? " AND " : ""}labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`;
|
|
1543
|
+
}
|
|
1544
|
+
return jql;
|
|
1545
|
+
}
|
|
1459
1546
|
var JiraConnector = class {
|
|
1460
1547
|
name = "jira";
|
|
1461
1548
|
source = "jira";
|
|
@@ -1465,105 +1552,81 @@ var JiraConnector = class {
|
|
|
1465
1552
|
}
|
|
1466
1553
|
async ingest(store, config) {
|
|
1467
1554
|
const start = Date.now();
|
|
1468
|
-
const errors = [];
|
|
1469
1555
|
let nodesAdded = 0;
|
|
1470
1556
|
let edgesAdded = 0;
|
|
1471
1557
|
const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
|
|
1472
1558
|
const apiKey = process.env[apiKeyEnv];
|
|
1473
1559
|
if (!apiKey) {
|
|
1474
|
-
return
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
durationMs: Date.now() - start
|
|
1481
|
-
};
|
|
1560
|
+
return buildIngestResult(
|
|
1561
|
+
0,
|
|
1562
|
+
0,
|
|
1563
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
1564
|
+
start
|
|
1565
|
+
);
|
|
1482
1566
|
}
|
|
1483
1567
|
const baseUrlEnv = config.baseUrlEnv ?? "JIRA_BASE_URL";
|
|
1484
1568
|
const baseUrl = process.env[baseUrlEnv];
|
|
1485
1569
|
if (!baseUrl) {
|
|
1486
|
-
return
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
durationMs: Date.now() - start
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
const project2 = config.project;
|
|
1496
|
-
let jql = project2 ? `project=${project2}` : "";
|
|
1497
|
-
const filters = config.filters;
|
|
1498
|
-
if (filters?.status?.length) {
|
|
1499
|
-
jql += `${jql ? " AND " : ""}status IN (${filters.status.map((s) => `"${s}"`).join(",")})`;
|
|
1500
|
-
}
|
|
1501
|
-
if (filters?.labels?.length) {
|
|
1502
|
-
jql += `${jql ? " AND " : ""}labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`;
|
|
1570
|
+
return buildIngestResult(
|
|
1571
|
+
0,
|
|
1572
|
+
0,
|
|
1573
|
+
[`Missing base URL: environment variable "${baseUrlEnv}" is not set`],
|
|
1574
|
+
start
|
|
1575
|
+
);
|
|
1503
1576
|
}
|
|
1577
|
+
const jql = buildJql(config);
|
|
1504
1578
|
const headers = {
|
|
1505
1579
|
Authorization: `Basic ${apiKey}`,
|
|
1506
1580
|
"Content-Type": "application/json"
|
|
1507
1581
|
};
|
|
1508
|
-
let startAt = 0;
|
|
1509
|
-
const maxResults = 50;
|
|
1510
|
-
let total = Infinity;
|
|
1511
1582
|
try {
|
|
1583
|
+
let startAt = 0;
|
|
1584
|
+
const maxResults = 50;
|
|
1585
|
+
let total = Infinity;
|
|
1512
1586
|
while (startAt < total) {
|
|
1513
1587
|
const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
|
|
1514
1588
|
const response = await this.httpClient(url, { headers });
|
|
1515
1589
|
if (!response.ok) {
|
|
1516
|
-
return
|
|
1517
|
-
nodesAdded,
|
|
1518
|
-
nodesUpdated: 0,
|
|
1519
|
-
edgesAdded,
|
|
1520
|
-
edgesUpdated: 0,
|
|
1521
|
-
errors: ["Jira API request failed"],
|
|
1522
|
-
durationMs: Date.now() - start
|
|
1523
|
-
};
|
|
1590
|
+
return buildIngestResult(nodesAdded, edgesAdded, ["Jira API request failed"], start);
|
|
1524
1591
|
}
|
|
1525
1592
|
const data = await response.json();
|
|
1526
1593
|
total = data.total;
|
|
1527
1594
|
for (const issue of data.issues) {
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
type: "issue",
|
|
1532
|
-
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
1533
|
-
metadata: {
|
|
1534
|
-
key: issue.key,
|
|
1535
|
-
status: issue.fields.status?.name,
|
|
1536
|
-
priority: issue.fields.priority?.name,
|
|
1537
|
-
assignee: issue.fields.assignee?.displayName,
|
|
1538
|
-
labels: issue.fields.labels ?? []
|
|
1539
|
-
}
|
|
1540
|
-
});
|
|
1541
|
-
nodesAdded++;
|
|
1542
|
-
const searchText = sanitizeExternalText(
|
|
1543
|
-
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
1544
|
-
);
|
|
1545
|
-
edgesAdded += linkToCode(store, searchText, nodeId, "applies_to");
|
|
1595
|
+
const counts = this.processIssue(store, issue);
|
|
1596
|
+
nodesAdded += counts.nodesAdded;
|
|
1597
|
+
edgesAdded += counts.edgesAdded;
|
|
1546
1598
|
}
|
|
1547
1599
|
startAt += maxResults;
|
|
1548
1600
|
}
|
|
1549
1601
|
} catch (err) {
|
|
1550
|
-
return
|
|
1602
|
+
return buildIngestResult(
|
|
1551
1603
|
nodesAdded,
|
|
1552
|
-
nodesUpdated: 0,
|
|
1553
1604
|
edgesAdded,
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
};
|
|
1605
|
+
[`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
|
|
1606
|
+
start
|
|
1607
|
+
);
|
|
1558
1608
|
}
|
|
1559
|
-
return
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1609
|
+
return buildIngestResult(nodesAdded, edgesAdded, [], start);
|
|
1610
|
+
}
|
|
1611
|
+
processIssue(store, issue) {
|
|
1612
|
+
const nodeId = `issue:jira:${issue.key}`;
|
|
1613
|
+
store.addNode({
|
|
1614
|
+
id: nodeId,
|
|
1615
|
+
type: "issue",
|
|
1616
|
+
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
1617
|
+
metadata: {
|
|
1618
|
+
key: issue.key,
|
|
1619
|
+
status: issue.fields.status?.name,
|
|
1620
|
+
priority: issue.fields.priority?.name,
|
|
1621
|
+
assignee: issue.fields.assignee?.displayName,
|
|
1622
|
+
labels: issue.fields.labels ?? []
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
const searchText = sanitizeExternalText(
|
|
1626
|
+
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
1627
|
+
);
|
|
1628
|
+
const edgesAdded = linkToCode(store, searchText, nodeId, "applies_to");
|
|
1629
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1567
1630
|
}
|
|
1568
1631
|
};
|
|
1569
1632
|
|
|
@@ -1596,44 +1659,10 @@ var SlackConnector = class {
|
|
|
1596
1659
|
const oldest = config.lookbackDays ? String(Math.floor((Date.now() - Number(config.lookbackDays) * 864e5) / 1e3)) : void 0;
|
|
1597
1660
|
for (const channel of channels) {
|
|
1598
1661
|
try {
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
const response = await this.httpClient(url, {
|
|
1604
|
-
headers: {
|
|
1605
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1606
|
-
"Content-Type": "application/json"
|
|
1607
|
-
}
|
|
1608
|
-
});
|
|
1609
|
-
if (!response.ok) {
|
|
1610
|
-
errors.push(`Slack API request failed for channel ${channel}`);
|
|
1611
|
-
continue;
|
|
1612
|
-
}
|
|
1613
|
-
const data = await response.json();
|
|
1614
|
-
if (!data.ok) {
|
|
1615
|
-
errors.push(`Slack API error for channel ${channel}`);
|
|
1616
|
-
continue;
|
|
1617
|
-
}
|
|
1618
|
-
for (const message of data.messages) {
|
|
1619
|
-
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
1620
|
-
const sanitizedText = sanitizeExternalText(message.text);
|
|
1621
|
-
const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
|
|
1622
|
-
store.addNode({
|
|
1623
|
-
id: nodeId,
|
|
1624
|
-
type: "conversation",
|
|
1625
|
-
name: snippet,
|
|
1626
|
-
metadata: {
|
|
1627
|
-
author: message.user,
|
|
1628
|
-
channel,
|
|
1629
|
-
timestamp: message.ts
|
|
1630
|
-
}
|
|
1631
|
-
});
|
|
1632
|
-
nodesAdded++;
|
|
1633
|
-
edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
|
|
1634
|
-
checkPaths: true
|
|
1635
|
-
});
|
|
1636
|
-
}
|
|
1662
|
+
const result = await this.processChannel(store, channel, apiKey, oldest);
|
|
1663
|
+
nodesAdded += result.nodesAdded;
|
|
1664
|
+
edgesAdded += result.edgesAdded;
|
|
1665
|
+
errors.push(...result.errors);
|
|
1637
1666
|
} catch (err) {
|
|
1638
1667
|
errors.push(
|
|
1639
1668
|
`Slack API error for channel ${channel}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -1649,6 +1678,52 @@ var SlackConnector = class {
|
|
|
1649
1678
|
durationMs: Date.now() - start
|
|
1650
1679
|
};
|
|
1651
1680
|
}
|
|
1681
|
+
async processChannel(store, channel, apiKey, oldest) {
|
|
1682
|
+
const errors = [];
|
|
1683
|
+
let nodesAdded = 0;
|
|
1684
|
+
let edgesAdded = 0;
|
|
1685
|
+
let url = `https://slack.com/api/conversations.history?channel=${encodeURIComponent(channel)}`;
|
|
1686
|
+
if (oldest) {
|
|
1687
|
+
url += `&oldest=${oldest}`;
|
|
1688
|
+
}
|
|
1689
|
+
const response = await this.httpClient(url, {
|
|
1690
|
+
headers: {
|
|
1691
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1692
|
+
"Content-Type": "application/json"
|
|
1693
|
+
}
|
|
1694
|
+
});
|
|
1695
|
+
if (!response.ok) {
|
|
1696
|
+
return {
|
|
1697
|
+
nodesAdded: 0,
|
|
1698
|
+
edgesAdded: 0,
|
|
1699
|
+
errors: [`Slack API request failed for channel ${channel}`]
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
const data = await response.json();
|
|
1703
|
+
if (!data.ok) {
|
|
1704
|
+
return { nodesAdded: 0, edgesAdded: 0, errors: [`Slack API error for channel ${channel}`] };
|
|
1705
|
+
}
|
|
1706
|
+
for (const message of data.messages) {
|
|
1707
|
+
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
1708
|
+
const sanitizedText = sanitizeExternalText(message.text);
|
|
1709
|
+
const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
|
|
1710
|
+
store.addNode({
|
|
1711
|
+
id: nodeId,
|
|
1712
|
+
type: "conversation",
|
|
1713
|
+
name: snippet,
|
|
1714
|
+
metadata: {
|
|
1715
|
+
author: message.user,
|
|
1716
|
+
channel,
|
|
1717
|
+
timestamp: message.ts
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
nodesAdded++;
|
|
1721
|
+
edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
|
|
1722
|
+
checkPaths: true
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
return { nodesAdded, edgesAdded, errors };
|
|
1726
|
+
}
|
|
1652
1727
|
};
|
|
1653
1728
|
|
|
1654
1729
|
// src/ingest/connectors/ConfluenceConnector.ts
|
|
@@ -1680,36 +1755,10 @@ var ConfluenceConnector = class {
|
|
|
1680
1755
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
1681
1756
|
const spaceKey = config.spaceKey ?? "";
|
|
1682
1757
|
try {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
});
|
|
1688
|
-
if (!response.ok) {
|
|
1689
|
-
errors.push(`Confluence API error: status ${response.status}`);
|
|
1690
|
-
break;
|
|
1691
|
-
}
|
|
1692
|
-
const data = await response.json();
|
|
1693
|
-
for (const page of data.results) {
|
|
1694
|
-
const nodeId = `confluence:${page.id}`;
|
|
1695
|
-
store.addNode({
|
|
1696
|
-
id: nodeId,
|
|
1697
|
-
type: "document",
|
|
1698
|
-
name: sanitizeExternalText(page.title, 500),
|
|
1699
|
-
metadata: {
|
|
1700
|
-
source: "confluence",
|
|
1701
|
-
spaceKey,
|
|
1702
|
-
pageId: page.id,
|
|
1703
|
-
status: page.status,
|
|
1704
|
-
url: page._links?.webui ?? ""
|
|
1705
|
-
}
|
|
1706
|
-
});
|
|
1707
|
-
nodesAdded++;
|
|
1708
|
-
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
1709
|
-
edgesAdded += linkToCode(store, text, nodeId, "documents");
|
|
1710
|
-
}
|
|
1711
|
-
nextUrl = data._links?.next ? `${baseUrl}${data._links.next}` : null;
|
|
1712
|
-
}
|
|
1758
|
+
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
1759
|
+
nodesAdded = result.nodesAdded;
|
|
1760
|
+
edgesAdded = result.edgesAdded;
|
|
1761
|
+
errors.push(...result.errors);
|
|
1713
1762
|
} catch (err) {
|
|
1714
1763
|
errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1715
1764
|
}
|
|
@@ -1722,6 +1771,47 @@ var ConfluenceConnector = class {
|
|
|
1722
1771
|
durationMs: Date.now() - start
|
|
1723
1772
|
};
|
|
1724
1773
|
}
|
|
1774
|
+
async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
|
|
1775
|
+
const errors = [];
|
|
1776
|
+
let nodesAdded = 0;
|
|
1777
|
+
let edgesAdded = 0;
|
|
1778
|
+
let nextUrl = `${baseUrl}/wiki/api/v2/pages?spaceKey=${encodeURIComponent(spaceKey)}&limit=25&body-format=storage`;
|
|
1779
|
+
while (nextUrl) {
|
|
1780
|
+
const response = await this.httpClient(nextUrl, {
|
|
1781
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1782
|
+
});
|
|
1783
|
+
if (!response.ok) {
|
|
1784
|
+
errors.push(`Confluence API error: status ${response.status}`);
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
const data = await response.json();
|
|
1788
|
+
for (const page of data.results) {
|
|
1789
|
+
const counts = this.processPage(store, page, spaceKey);
|
|
1790
|
+
nodesAdded += counts.nodesAdded;
|
|
1791
|
+
edgesAdded += counts.edgesAdded;
|
|
1792
|
+
}
|
|
1793
|
+
nextUrl = data._links?.next ? `${baseUrl}${data._links.next}` : null;
|
|
1794
|
+
}
|
|
1795
|
+
return { nodesAdded, edgesAdded, errors };
|
|
1796
|
+
}
|
|
1797
|
+
processPage(store, page, spaceKey) {
|
|
1798
|
+
const nodeId = `confluence:${page.id}`;
|
|
1799
|
+
store.addNode({
|
|
1800
|
+
id: nodeId,
|
|
1801
|
+
type: "document",
|
|
1802
|
+
name: sanitizeExternalText(page.title, 500),
|
|
1803
|
+
metadata: {
|
|
1804
|
+
source: "confluence",
|
|
1805
|
+
spaceKey,
|
|
1806
|
+
pageId: page.id,
|
|
1807
|
+
status: page.status,
|
|
1808
|
+
url: page._links?.webui ?? ""
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
1812
|
+
const edgesAdded = linkToCode(store, text, nodeId, "documents");
|
|
1813
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1814
|
+
}
|
|
1725
1815
|
};
|
|
1726
1816
|
|
|
1727
1817
|
// src/ingest/connectors/CIConnector.ts
|
|
@@ -2014,22 +2104,25 @@ var GraphEntropyAdapter = class {
|
|
|
2014
2104
|
* 3. Unreachable = code nodes NOT in visited set
|
|
2015
2105
|
*/
|
|
2016
2106
|
computeDeadCodeData() {
|
|
2017
|
-
const
|
|
2107
|
+
const entryPoints = this.findEntryPoints();
|
|
2108
|
+
const visited = this.bfsFromEntryPoints(entryPoints);
|
|
2109
|
+
const unreachableNodes = this.collectUnreachableNodes(visited);
|
|
2110
|
+
return { reachableNodeIds: visited, unreachableNodes, entryPoints };
|
|
2111
|
+
}
|
|
2112
|
+
findEntryPoints() {
|
|
2018
2113
|
const entryPoints = [];
|
|
2019
|
-
for (const node of allFileNodes) {
|
|
2020
|
-
if (node.name === "index.ts" || node.metadata?.entryPoint === true) {
|
|
2021
|
-
entryPoints.push(node.id);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
2114
|
for (const nodeType of CODE_NODE_TYPES3) {
|
|
2025
|
-
if (nodeType === "file") continue;
|
|
2026
2115
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2027
2116
|
for (const node of nodes) {
|
|
2028
|
-
|
|
2117
|
+
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
2118
|
+
if (isIndexFile || node.metadata?.entryPoint === true) {
|
|
2029
2119
|
entryPoints.push(node.id);
|
|
2030
2120
|
}
|
|
2031
2121
|
}
|
|
2032
2122
|
}
|
|
2123
|
+
return entryPoints;
|
|
2124
|
+
}
|
|
2125
|
+
bfsFromEntryPoints(entryPoints) {
|
|
2033
2126
|
const visited = /* @__PURE__ */ new Set();
|
|
2034
2127
|
const queue = [...entryPoints];
|
|
2035
2128
|
let head = 0;
|
|
@@ -2037,25 +2130,22 @@ var GraphEntropyAdapter = class {
|
|
|
2037
2130
|
const nodeId = queue[head++];
|
|
2038
2131
|
if (visited.has(nodeId)) continue;
|
|
2039
2132
|
visited.add(nodeId);
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
queue.push(edge.to);
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
const containsEdges = this.store.getEdges({ from: nodeId, type: "contains" });
|
|
2053
|
-
for (const edge of containsEdges) {
|
|
2133
|
+
this.enqueueOutboundEdges(nodeId, visited, queue);
|
|
2134
|
+
}
|
|
2135
|
+
return visited;
|
|
2136
|
+
}
|
|
2137
|
+
enqueueOutboundEdges(nodeId, visited, queue) {
|
|
2138
|
+
const edgeTypes = ["imports", "calls", "contains"];
|
|
2139
|
+
for (const edgeType of edgeTypes) {
|
|
2140
|
+
const edges = this.store.getEdges({ from: nodeId, type: edgeType });
|
|
2141
|
+
for (const edge of edges) {
|
|
2054
2142
|
if (!visited.has(edge.to)) {
|
|
2055
2143
|
queue.push(edge.to);
|
|
2056
2144
|
}
|
|
2057
2145
|
}
|
|
2058
2146
|
}
|
|
2147
|
+
}
|
|
2148
|
+
collectUnreachableNodes(visited) {
|
|
2059
2149
|
const unreachableNodes = [];
|
|
2060
2150
|
for (const nodeType of CODE_NODE_TYPES3) {
|
|
2061
2151
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
@@ -2070,11 +2160,7 @@ var GraphEntropyAdapter = class {
|
|
|
2070
2160
|
}
|
|
2071
2161
|
}
|
|
2072
2162
|
}
|
|
2073
|
-
return
|
|
2074
|
-
reachableNodeIds: visited,
|
|
2075
|
-
unreachableNodes,
|
|
2076
|
-
entryPoints
|
|
2077
|
-
};
|
|
2163
|
+
return unreachableNodes;
|
|
2078
2164
|
}
|
|
2079
2165
|
/**
|
|
2080
2166
|
* Count all nodes and edges by type.
|
|
@@ -2125,33 +2211,9 @@ var GraphComplexityAdapter = class {
|
|
|
2125
2211
|
const hotspots = [];
|
|
2126
2212
|
for (const fnNode of functionNodes) {
|
|
2127
2213
|
const complexity = fnNode.metadata?.cyclomaticComplexity ?? 1;
|
|
2128
|
-
const
|
|
2129
|
-
let fileId;
|
|
2130
|
-
for (const edge of containsEdges) {
|
|
2131
|
-
const sourceNode = this.store.getNode(edge.from);
|
|
2132
|
-
if (sourceNode?.type === "file") {
|
|
2133
|
-
fileId = sourceNode.id;
|
|
2134
|
-
break;
|
|
2135
|
-
}
|
|
2136
|
-
if (sourceNode?.type === "class") {
|
|
2137
|
-
const classContainsEdges = this.store.getEdges({ to: sourceNode.id, type: "contains" });
|
|
2138
|
-
for (const classEdge of classContainsEdges) {
|
|
2139
|
-
const parentNode = this.store.getNode(classEdge.from);
|
|
2140
|
-
if (parentNode?.type === "file") {
|
|
2141
|
-
fileId = parentNode.id;
|
|
2142
|
-
break;
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
if (fileId) break;
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2214
|
+
const fileId = this.findContainingFileId(fnNode.id);
|
|
2148
2215
|
if (!fileId) continue;
|
|
2149
|
-
|
|
2150
|
-
if (changeFrequency === void 0) {
|
|
2151
|
-
const referencesEdges = this.store.getEdges({ to: fileId, type: "references" });
|
|
2152
|
-
changeFrequency = referencesEdges.length;
|
|
2153
|
-
fileChangeFrequency.set(fileId, changeFrequency);
|
|
2154
|
-
}
|
|
2216
|
+
const changeFrequency = this.getChangeFrequency(fileId, fileChangeFrequency);
|
|
2155
2217
|
const hotspotScore = changeFrequency * complexity;
|
|
2156
2218
|
const filePath = fnNode.path ?? fileId.replace(/^file:/, "");
|
|
2157
2219
|
hotspots.push({
|
|
@@ -2169,6 +2231,39 @@ var GraphComplexityAdapter = class {
|
|
|
2169
2231
|
);
|
|
2170
2232
|
return { hotspots, percentile95Score };
|
|
2171
2233
|
}
|
|
2234
|
+
/**
|
|
2235
|
+
* Walk the 'contains' edges to find the file node that contains a given function/method.
|
|
2236
|
+
* For methods, walks through the intermediate class node.
|
|
2237
|
+
*/
|
|
2238
|
+
findContainingFileId(nodeId) {
|
|
2239
|
+
const containsEdges = this.store.getEdges({ to: nodeId, type: "contains" });
|
|
2240
|
+
for (const edge of containsEdges) {
|
|
2241
|
+
const sourceNode = this.store.getNode(edge.from);
|
|
2242
|
+
if (sourceNode?.type === "file") return sourceNode.id;
|
|
2243
|
+
if (sourceNode?.type === "class") {
|
|
2244
|
+
const fileId = this.findParentFileOfClass(sourceNode.id);
|
|
2245
|
+
if (fileId) return fileId;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
return void 0;
|
|
2249
|
+
}
|
|
2250
|
+
findParentFileOfClass(classNodeId) {
|
|
2251
|
+
const classContainsEdges = this.store.getEdges({ to: classNodeId, type: "contains" });
|
|
2252
|
+
for (const classEdge of classContainsEdges) {
|
|
2253
|
+
const parentNode = this.store.getNode(classEdge.from);
|
|
2254
|
+
if (parentNode?.type === "file") return parentNode.id;
|
|
2255
|
+
}
|
|
2256
|
+
return void 0;
|
|
2257
|
+
}
|
|
2258
|
+
getChangeFrequency(fileId, cache) {
|
|
2259
|
+
let freq = cache.get(fileId);
|
|
2260
|
+
if (freq === void 0) {
|
|
2261
|
+
const referencesEdges = this.store.getEdges({ to: fileId, type: "references" });
|
|
2262
|
+
freq = referencesEdges.length;
|
|
2263
|
+
cache.set(fileId, freq);
|
|
2264
|
+
}
|
|
2265
|
+
return freq;
|
|
2266
|
+
}
|
|
2172
2267
|
computePercentile(descendingScores, percentile) {
|
|
2173
2268
|
if (descendingScores.length === 0) return 0;
|
|
2174
2269
|
const ascending = [...descendingScores].sort((a, b) => a - b);
|
|
@@ -2816,6 +2911,23 @@ var STOP_WORDS2 = /* @__PURE__ */ new Set([
|
|
|
2816
2911
|
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;
|
|
2817
2912
|
var FILE_PATH_RE = /(?:\.\/|[a-zA-Z0-9_-]+\/)[a-zA-Z0-9_\-./]+\.[a-zA-Z]{1,10}/g;
|
|
2818
2913
|
var QUOTED_RE = /["']([^"']+)["']/g;
|
|
2914
|
+
function isSkippableWord(cleaned, allConsumed) {
|
|
2915
|
+
if (allConsumed.has(cleaned)) return true;
|
|
2916
|
+
const lower = cleaned.toLowerCase();
|
|
2917
|
+
if (STOP_WORDS2.has(lower)) return true;
|
|
2918
|
+
if (INTENT_KEYWORDS.has(lower)) return true;
|
|
2919
|
+
if (cleaned === cleaned.toUpperCase() && /^[A-Z]+$/.test(cleaned)) return true;
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
function buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed) {
|
|
2923
|
+
const quotedWords = /* @__PURE__ */ new Set();
|
|
2924
|
+
for (const q of quotedConsumed) {
|
|
2925
|
+
for (const w of q.split(/\s+/)) {
|
|
2926
|
+
if (w.length > 0) quotedWords.add(w);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
return /* @__PURE__ */ new Set([...quotedConsumed, ...quotedWords, ...casingConsumed, ...pathConsumed]);
|
|
2930
|
+
}
|
|
2819
2931
|
var EntityExtractor = class {
|
|
2820
2932
|
/**
|
|
2821
2933
|
* Extract candidate entity mentions from a natural language query.
|
|
@@ -2856,27 +2968,12 @@ var EntityExtractor = class {
|
|
|
2856
2968
|
add(path6);
|
|
2857
2969
|
pathConsumed.add(path6);
|
|
2858
2970
|
}
|
|
2859
|
-
const
|
|
2860
|
-
for (const q of quotedConsumed) {
|
|
2861
|
-
for (const w of q.split(/\s+/)) {
|
|
2862
|
-
if (w.length > 0) quotedWords.add(w);
|
|
2863
|
-
}
|
|
2864
|
-
}
|
|
2865
|
-
const allConsumed = /* @__PURE__ */ new Set([
|
|
2866
|
-
...quotedConsumed,
|
|
2867
|
-
...quotedWords,
|
|
2868
|
-
...casingConsumed,
|
|
2869
|
-
...pathConsumed
|
|
2870
|
-
]);
|
|
2971
|
+
const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
|
|
2871
2972
|
const words = trimmed.split(/\s+/);
|
|
2872
2973
|
for (const raw of words) {
|
|
2873
2974
|
const cleaned = raw.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "");
|
|
2874
2975
|
if (cleaned.length === 0) continue;
|
|
2875
|
-
|
|
2876
|
-
if (allConsumed.has(cleaned)) continue;
|
|
2877
|
-
if (STOP_WORDS2.has(lower)) continue;
|
|
2878
|
-
if (INTENT_KEYWORDS.has(lower)) continue;
|
|
2879
|
-
if (cleaned === cleaned.toUpperCase() && /^[A-Z]+$/.test(cleaned)) continue;
|
|
2976
|
+
if (isSkippableWord(cleaned, allConsumed)) continue;
|
|
2880
2977
|
add(cleaned);
|
|
2881
2978
|
}
|
|
2882
2979
|
return result;
|
|
@@ -3199,14 +3296,20 @@ var Assembler = class {
|
|
|
3199
3296
|
const fusion = this.getFusionLayer();
|
|
3200
3297
|
const topResults = fusion.search(intent, 10);
|
|
3201
3298
|
if (topResults.length === 0) {
|
|
3202
|
-
return {
|
|
3203
|
-
nodes: [],
|
|
3204
|
-
edges: [],
|
|
3205
|
-
tokenEstimate: 0,
|
|
3206
|
-
intent,
|
|
3207
|
-
truncated: false
|
|
3208
|
-
};
|
|
3299
|
+
return { nodes: [], edges: [], tokenEstimate: 0, intent, truncated: false };
|
|
3209
3300
|
}
|
|
3301
|
+
const { nodeMap, collectedEdges, nodeScores } = this.expandSearchResults(topResults);
|
|
3302
|
+
const sortedNodes = Array.from(nodeMap.values()).sort((a, b) => {
|
|
3303
|
+
return (nodeScores.get(b.id) ?? 0) - (nodeScores.get(a.id) ?? 0);
|
|
3304
|
+
});
|
|
3305
|
+
const { keptNodes, tokenEstimate, truncated } = this.truncateToFit(sortedNodes, tokenBudget);
|
|
3306
|
+
const keptNodeIds = new Set(keptNodes.map((n) => n.id));
|
|
3307
|
+
const keptEdges = collectedEdges.filter(
|
|
3308
|
+
(e) => keptNodeIds.has(e.from) && keptNodeIds.has(e.to)
|
|
3309
|
+
);
|
|
3310
|
+
return { nodes: keptNodes, edges: keptEdges, tokenEstimate, intent, truncated };
|
|
3311
|
+
}
|
|
3312
|
+
expandSearchResults(topResults) {
|
|
3210
3313
|
const contextQL = new ContextQL(this.store);
|
|
3211
3314
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
3212
3315
|
const edgeSet = /* @__PURE__ */ new Set();
|
|
@@ -3234,9 +3337,9 @@ var Assembler = class {
|
|
|
3234
3337
|
}
|
|
3235
3338
|
}
|
|
3236
3339
|
}
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3340
|
+
return { nodeMap, collectedEdges, nodeScores };
|
|
3341
|
+
}
|
|
3342
|
+
truncateToFit(sortedNodes, tokenBudget) {
|
|
3240
3343
|
let tokenEstimate = 0;
|
|
3241
3344
|
const keptNodes = [];
|
|
3242
3345
|
let truncated = false;
|
|
@@ -3249,17 +3352,7 @@ var Assembler = class {
|
|
|
3249
3352
|
tokenEstimate += nodeTokens;
|
|
3250
3353
|
keptNodes.push(node);
|
|
3251
3354
|
}
|
|
3252
|
-
|
|
3253
|
-
const keptEdges = collectedEdges.filter(
|
|
3254
|
-
(e) => keptNodeIds.has(e.from) && keptNodeIds.has(e.to)
|
|
3255
|
-
);
|
|
3256
|
-
return {
|
|
3257
|
-
nodes: keptNodes,
|
|
3258
|
-
edges: keptEdges,
|
|
3259
|
-
tokenEstimate,
|
|
3260
|
-
intent,
|
|
3261
|
-
truncated
|
|
3262
|
-
};
|
|
3355
|
+
return { keptNodes, tokenEstimate, truncated };
|
|
3263
3356
|
}
|
|
3264
3357
|
/**
|
|
3265
3358
|
* Compute a token budget allocation across node types.
|
|
@@ -3432,8 +3525,8 @@ var GraphConstraintAdapter = class {
|
|
|
3432
3525
|
const { edges } = this.computeDependencyGraph();
|
|
3433
3526
|
const violations = [];
|
|
3434
3527
|
for (const edge of edges) {
|
|
3435
|
-
const fromRelative = relative2(rootDir, edge.from);
|
|
3436
|
-
const toRelative = relative2(rootDir, edge.to);
|
|
3528
|
+
const fromRelative = relative2(rootDir, edge.from).replaceAll("\\", "/");
|
|
3529
|
+
const toRelative = relative2(rootDir, edge.to).replaceAll("\\", "/");
|
|
3437
3530
|
const fromLayer = this.resolveLayer(fromRelative, layers);
|
|
3438
3531
|
const toLayer = this.resolveLayer(toRelative, layers);
|
|
3439
3532
|
if (!fromLayer || !toLayer) continue;
|