@harness-engineering/graph 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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 visited = /* @__PURE__ */ new Set();
370
- const resultNodeMap = /* @__PURE__ */ new Map();
371
- const resultEdges = [];
372
- const edgeSet = /* @__PURE__ */ new Set();
373
- let pruned = 0;
374
- let depthReached = 0;
375
- const edgeKey = (e) => `${e.from}|${e.to}|${e.type}`;
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
- for (const rootId of params.rootNodeIds) {
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 outEdges = this.store.getEdges({ from: currentId });
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
- if (params.includeEdges && !params.includeEdges.includes(edge.type)) {
407
- continue;
408
- }
409
- if (visited.has(neighborId)) {
410
- addEdge(edge);
411
- continue;
412
- }
413
- const neighbor = this.store.getNode(neighborId);
414
- if (!neighbor) continue;
415
- visited.add(neighborId);
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
- return {
443
- nodes: Array.from(resultNodeMap.values()),
444
- edges: resultEdges,
445
- stats: {
446
- totalTraversed: visited.size,
447
- totalReturned: resultNodeMap.size,
448
- pruned,
449
- depthReached
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 relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
519
- const content = await fs.readFile(filePath, "utf-8");
520
- const stat2 = await fs.stat(filePath);
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
- let currentClassName = null;
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
- const fnMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
594
- if (fnMatch) {
595
- const name = fnMatch[1];
596
- const id = `function:${relativePath}:${name}`;
597
- const endLine = this.findClosingBrace(lines, i);
598
- results.push({
599
- node: {
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.replace(
1351
- /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
1352
- ""
1353
- ).replace(/^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim, "").replace(
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
- nodesAdded: 0,
1476
- nodesUpdated: 0,
1477
- edgesAdded: 0,
1478
- edgesUpdated: 0,
1479
- errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
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
- nodesAdded: 0,
1488
- nodesUpdated: 0,
1489
- edgesAdded: 0,
1490
- edgesUpdated: 0,
1491
- errors: [`Missing base URL: environment variable "${baseUrlEnv}" is not set`],
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 nodeId = `issue:jira:${issue.key}`;
1529
- store.addNode({
1530
- id: nodeId,
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
- edgesUpdated: 0,
1555
- errors: [`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
1556
- durationMs: Date.now() - start
1557
- };
1605
+ [`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
1606
+ start
1607
+ );
1558
1608
  }
1559
- return {
1560
- nodesAdded,
1561
- nodesUpdated: 0,
1562
- edgesAdded,
1563
- edgesUpdated: 0,
1564
- errors,
1565
- durationMs: Date.now() - start
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
- let url = `https://slack.com/api/conversations.history?channel=${encodeURIComponent(channel)}`;
1600
- if (oldest) {
1601
- url += `&oldest=${oldest}`;
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
- let nextUrl = `${baseUrl}/wiki/api/v2/pages?spaceKey=${encodeURIComponent(spaceKey)}&limit=25&body-format=storage`;
1684
- while (nextUrl) {
1685
- const response = await this.httpClient(nextUrl, {
1686
- headers: { Authorization: `Bearer ${apiKey}` }
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 allFileNodes = this.store.findNodes({ type: "file" });
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
- if (node.metadata?.entryPoint === true) {
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
- const importEdges = this.store.getEdges({ from: nodeId, type: "imports" });
2041
- for (const edge of importEdges) {
2042
- if (!visited.has(edge.to)) {
2043
- queue.push(edge.to);
2044
- }
2045
- }
2046
- const callEdges = this.store.getEdges({ from: nodeId, type: "calls" });
2047
- for (const edge of callEdges) {
2048
- if (!visited.has(edge.to)) {
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 containsEdges = this.store.getEdges({ to: fnNode.id, type: "contains" });
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
- let changeFrequency = fileChangeFrequency.get(fileId);
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 quotedWords = /* @__PURE__ */ new Set();
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
- const lower = cleaned.toLowerCase();
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
- const sortedNodes = Array.from(nodeMap.values()).sort((a, b) => {
3238
- return (nodeScores.get(b.id) ?? 0) - (nodeScores.get(a.id) ?? 0);
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
- const keptNodeIds = new Set(keptNodes.map((n) => n.id));
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;