@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.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 visited = /* @__PURE__ */ new Set();
447
- const resultNodeMap = /* @__PURE__ */ new Map();
448
- const resultEdges = [];
449
- const edgeSet = /* @__PURE__ */ new Set();
450
- let pruned = 0;
451
- let depthReached = 0;
452
- const edgeKey = (e) => `${e.from}|${e.to}|${e.type}`;
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
- for (const rootId of params.rootNodeIds) {
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 outEdges = this.store.getEdges({ from: currentId });
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
- if (params.includeEdges && !params.includeEdges.includes(edge.type)) {
484
- continue;
485
- }
486
- if (visited.has(neighborId)) {
487
- addEdge(edge);
488
- continue;
489
- }
490
- const neighbor = this.store.getNode(neighborId);
491
- if (!neighbor) continue;
492
- visited.add(neighborId);
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
- return {
520
- nodes: Array.from(resultNodeMap.values()),
521
- edges: resultEdges,
522
- stats: {
523
- totalTraversed: visited.size,
524
- totalReturned: resultNodeMap.size,
525
- pruned,
526
- depthReached
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 relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
596
- const content = await fs.readFile(filePath, "utf-8");
597
- const stat2 = await fs.stat(filePath);
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
- let currentClassName = null;
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
- const fnMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
671
- if (fnMatch) {
672
- const name = fnMatch[1];
673
- const id = `function:${relativePath}:${name}`;
674
- const endLine = this.findClosingBrace(lines, i);
675
- results.push({
676
- node: {
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.replace(
1428
- /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
1429
- ""
1430
- ).replace(/^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim, "").replace(
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
- nodesAdded: 0,
1553
- nodesUpdated: 0,
1554
- edgesAdded: 0,
1555
- edgesUpdated: 0,
1556
- errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
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
- nodesAdded: 0,
1565
- nodesUpdated: 0,
1566
- edgesAdded: 0,
1567
- edgesUpdated: 0,
1568
- errors: [`Missing base URL: environment variable "${baseUrlEnv}" is not set`],
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 nodeId = `issue:jira:${issue.key}`;
1606
- store.addNode({
1607
- id: nodeId,
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
- edgesUpdated: 0,
1632
- errors: [`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
1633
- durationMs: Date.now() - start
1634
- };
1682
+ [`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
1683
+ start
1684
+ );
1635
1685
  }
1636
- return {
1637
- nodesAdded,
1638
- nodesUpdated: 0,
1639
- edgesAdded,
1640
- edgesUpdated: 0,
1641
- errors,
1642
- durationMs: Date.now() - start
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
- let url = `https://slack.com/api/conversations.history?channel=${encodeURIComponent(channel)}`;
1677
- if (oldest) {
1678
- url += `&oldest=${oldest}`;
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
- let nextUrl = `${baseUrl}/wiki/api/v2/pages?spaceKey=${encodeURIComponent(spaceKey)}&limit=25&body-format=storage`;
1761
- while (nextUrl) {
1762
- const response = await this.httpClient(nextUrl, {
1763
- headers: { Authorization: `Bearer ${apiKey}` }
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 allFileNodes = this.store.findNodes({ type: "file" });
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
- if (node.metadata?.entryPoint === true) {
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
- const importEdges = this.store.getEdges({ from: nodeId, type: "imports" });
2118
- for (const edge of importEdges) {
2119
- if (!visited.has(edge.to)) {
2120
- queue.push(edge.to);
2121
- }
2122
- }
2123
- const callEdges = this.store.getEdges({ from: nodeId, type: "calls" });
2124
- for (const edge of callEdges) {
2125
- if (!visited.has(edge.to)) {
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 containsEdges = this.store.getEdges({ to: fnNode.id, type: "contains" });
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
- let changeFrequency = fileChangeFrequency.get(fileId);
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 quotedWords = /* @__PURE__ */ new Set();
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
- const lower = cleaned.toLowerCase();
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
- const sortedNodes = Array.from(nodeMap.values()).sort((a, b) => {
3315
- return (nodeScores.get(b.id) ?? 0) - (nodeScores.get(a.id) ?? 0);
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
- const keptNodeIds = new Set(keptNodes.map((n) => n.id));
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;