@harness-engineering/graph 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +260 -1
- package/dist/index.d.ts +260 -1
- package/dist/index.js +1728 -463
- package/dist/index.mjs +1719 -463
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -357,6 +357,53 @@ var VectorStore = class _VectorStore {
|
|
|
357
357
|
};
|
|
358
358
|
|
|
359
359
|
// src/query/ContextQL.ts
|
|
360
|
+
function edgeKey(e) {
|
|
361
|
+
return `${e.from}|${e.to}|${e.type}`;
|
|
362
|
+
}
|
|
363
|
+
function addEdge(state, edge) {
|
|
364
|
+
const key = edgeKey(edge);
|
|
365
|
+
if (!state.edgeSet.has(key)) {
|
|
366
|
+
state.edgeSet.add(key);
|
|
367
|
+
state.resultEdges.push(edge);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function shouldPruneNode(neighbor, pruneObservability, params) {
|
|
371
|
+
if (pruneObservability && OBSERVABILITY_TYPES.has(neighbor.type)) return true;
|
|
372
|
+
if (params.includeTypes && !params.includeTypes.includes(neighbor.type)) return true;
|
|
373
|
+
if (params.excludeTypes && params.excludeTypes.includes(neighbor.type)) return true;
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
function isEdgeExcluded(edge, params) {
|
|
377
|
+
return !!(params.includeEdges && !params.includeEdges.includes(edge.type));
|
|
378
|
+
}
|
|
379
|
+
function processNeighbor(store, edge, neighborId, nextDepth, queue, state, pruneObservability, params) {
|
|
380
|
+
if (isEdgeExcluded(edge, params)) return;
|
|
381
|
+
if (state.visited.has(neighborId)) {
|
|
382
|
+
addEdge(state, edge);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const neighbor = store.getNode(neighborId);
|
|
386
|
+
if (!neighbor) return;
|
|
387
|
+
state.visited.add(neighborId);
|
|
388
|
+
if (shouldPruneNode(neighbor, pruneObservability, params)) {
|
|
389
|
+
state.pruned++;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
state.resultNodeMap.set(neighborId, neighbor);
|
|
393
|
+
addEdge(state, edge);
|
|
394
|
+
queue.push({ id: neighborId, depth: nextDepth });
|
|
395
|
+
}
|
|
396
|
+
function addCrossEdges(store, state) {
|
|
397
|
+
const resultNodeIds = new Set(state.resultNodeMap.keys());
|
|
398
|
+
for (const nodeId of resultNodeIds) {
|
|
399
|
+
const outEdges = store.getEdges({ from: nodeId });
|
|
400
|
+
for (const edge of outEdges) {
|
|
401
|
+
if (resultNodeIds.has(edge.to)) {
|
|
402
|
+
addEdge(state, edge);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
360
407
|
var ContextQL = class {
|
|
361
408
|
store;
|
|
362
409
|
constructor(store) {
|
|
@@ -366,89 +413,69 @@ var ContextQL = class {
|
|
|
366
413
|
const maxDepth = params.maxDepth ?? 3;
|
|
367
414
|
const bidirectional = params.bidirectional ?? false;
|
|
368
415
|
const pruneObservability = params.pruneObservability ?? true;
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const addEdge = (edge) => {
|
|
377
|
-
const key = edgeKey(edge);
|
|
378
|
-
if (!edgeSet.has(key)) {
|
|
379
|
-
edgeSet.add(key);
|
|
380
|
-
resultEdges.push(edge);
|
|
381
|
-
}
|
|
416
|
+
const state = {
|
|
417
|
+
visited: /* @__PURE__ */ new Set(),
|
|
418
|
+
resultNodeMap: /* @__PURE__ */ new Map(),
|
|
419
|
+
resultEdges: [],
|
|
420
|
+
edgeSet: /* @__PURE__ */ new Set(),
|
|
421
|
+
pruned: 0,
|
|
422
|
+
depthReached: 0
|
|
382
423
|
};
|
|
383
424
|
const queue = [];
|
|
384
|
-
|
|
425
|
+
this.seedRootNodes(params.rootNodeIds, state, queue);
|
|
426
|
+
this.runBFS(queue, maxDepth, bidirectional, pruneObservability, params, state);
|
|
427
|
+
addCrossEdges(this.store, state);
|
|
428
|
+
return {
|
|
429
|
+
nodes: Array.from(state.resultNodeMap.values()),
|
|
430
|
+
edges: state.resultEdges,
|
|
431
|
+
stats: {
|
|
432
|
+
totalTraversed: state.visited.size,
|
|
433
|
+
totalReturned: state.resultNodeMap.size,
|
|
434
|
+
pruned: state.pruned,
|
|
435
|
+
depthReached: state.depthReached
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
seedRootNodes(rootNodeIds, state, queue) {
|
|
440
|
+
for (const rootId of rootNodeIds) {
|
|
385
441
|
const node = this.store.getNode(rootId);
|
|
386
442
|
if (node) {
|
|
387
|
-
visited.add(rootId);
|
|
388
|
-
resultNodeMap.set(rootId, node);
|
|
443
|
+
state.visited.add(rootId);
|
|
444
|
+
state.resultNodeMap.set(rootId, node);
|
|
389
445
|
queue.push({ id: rootId, depth: 0 });
|
|
390
446
|
}
|
|
391
447
|
}
|
|
448
|
+
}
|
|
449
|
+
runBFS(queue, maxDepth, bidirectional, pruneObservability, params, state) {
|
|
392
450
|
let head = 0;
|
|
393
451
|
while (head < queue.length) {
|
|
394
452
|
const entry = queue[head++];
|
|
395
453
|
const { id: currentId, depth } = entry;
|
|
396
454
|
if (depth >= maxDepth) continue;
|
|
397
455
|
const nextDepth = depth + 1;
|
|
398
|
-
if (nextDepth > depthReached) depthReached = nextDepth;
|
|
399
|
-
const
|
|
400
|
-
const inEdges = bidirectional ? this.store.getEdges({ to: currentId }) : [];
|
|
401
|
-
const allEdges = [
|
|
402
|
-
...outEdges.map((e) => ({ edge: e, neighborId: e.to })),
|
|
403
|
-
...inEdges.map((e) => ({ edge: e, neighborId: e.from }))
|
|
404
|
-
];
|
|
456
|
+
if (nextDepth > state.depthReached) state.depthReached = nextDepth;
|
|
457
|
+
const allEdges = this.gatherEdges(currentId, bidirectional);
|
|
405
458
|
for (const { edge, neighborId } of allEdges) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (pruneObservability && OBSERVABILITY_TYPES.has(neighbor.type)) {
|
|
417
|
-
pruned++;
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
if (params.includeTypes && !params.includeTypes.includes(neighbor.type)) {
|
|
421
|
-
pruned++;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
if (params.excludeTypes && params.excludeTypes.includes(neighbor.type)) {
|
|
425
|
-
pruned++;
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
resultNodeMap.set(neighborId, neighbor);
|
|
429
|
-
addEdge(edge);
|
|
430
|
-
queue.push({ id: neighborId, depth: nextDepth });
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
const resultNodeIds = new Set(resultNodeMap.keys());
|
|
434
|
-
for (const nodeId of resultNodeIds) {
|
|
435
|
-
const outEdges = this.store.getEdges({ from: nodeId });
|
|
436
|
-
for (const edge of outEdges) {
|
|
437
|
-
if (resultNodeIds.has(edge.to)) {
|
|
438
|
-
addEdge(edge);
|
|
439
|
-
}
|
|
459
|
+
processNeighbor(
|
|
460
|
+
this.store,
|
|
461
|
+
edge,
|
|
462
|
+
neighborId,
|
|
463
|
+
nextDepth,
|
|
464
|
+
queue,
|
|
465
|
+
state,
|
|
466
|
+
pruneObservability,
|
|
467
|
+
params
|
|
468
|
+
);
|
|
440
469
|
}
|
|
441
470
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
};
|
|
471
|
+
}
|
|
472
|
+
gatherEdges(nodeId, bidirectional) {
|
|
473
|
+
const outEdges = this.store.getEdges({ from: nodeId });
|
|
474
|
+
const inEdges = bidirectional ? this.store.getEdges({ to: nodeId }) : [];
|
|
475
|
+
return [
|
|
476
|
+
...outEdges.map((e) => ({ edge: e, neighborId: e.to })),
|
|
477
|
+
...inEdges.map((e) => ({ edge: e, neighborId: e.from }))
|
|
478
|
+
];
|
|
452
479
|
}
|
|
453
480
|
};
|
|
454
481
|
|
|
@@ -466,9 +493,50 @@ function project(nodes, spec) {
|
|
|
466
493
|
});
|
|
467
494
|
}
|
|
468
495
|
|
|
496
|
+
// src/query/groupImpact.ts
|
|
497
|
+
var TEST_TYPES = /* @__PURE__ */ new Set(["test_result"]);
|
|
498
|
+
var DOC_TYPES = /* @__PURE__ */ new Set(["adr", "decision", "document", "learning"]);
|
|
499
|
+
var CODE_TYPES = /* @__PURE__ */ new Set([
|
|
500
|
+
"file",
|
|
501
|
+
"module",
|
|
502
|
+
"class",
|
|
503
|
+
"interface",
|
|
504
|
+
"function",
|
|
505
|
+
"method",
|
|
506
|
+
"variable"
|
|
507
|
+
]);
|
|
508
|
+
function groupNodesByImpact(nodes, excludeId) {
|
|
509
|
+
const tests = [];
|
|
510
|
+
const docs = [];
|
|
511
|
+
const code = [];
|
|
512
|
+
const other = [];
|
|
513
|
+
for (const node of nodes) {
|
|
514
|
+
if (excludeId && node.id === excludeId) continue;
|
|
515
|
+
if (TEST_TYPES.has(node.type)) {
|
|
516
|
+
tests.push(node);
|
|
517
|
+
} else if (DOC_TYPES.has(node.type)) {
|
|
518
|
+
docs.push(node);
|
|
519
|
+
} else if (CODE_TYPES.has(node.type)) {
|
|
520
|
+
code.push(node);
|
|
521
|
+
} else {
|
|
522
|
+
other.push(node);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return { tests, docs, code, other };
|
|
526
|
+
}
|
|
527
|
+
|
|
469
528
|
// src/ingest/CodeIngestor.ts
|
|
470
529
|
import * as fs from "fs/promises";
|
|
471
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
|
+
}
|
|
472
540
|
var CodeIngestor = class {
|
|
473
541
|
constructor(store) {
|
|
474
542
|
this.store = store;
|
|
@@ -483,41 +551,9 @@ var CodeIngestor = class {
|
|
|
483
551
|
const fileContents = /* @__PURE__ */ new Map();
|
|
484
552
|
for (const filePath of files) {
|
|
485
553
|
try {
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const fileId = `file:${relativePath}`;
|
|
490
|
-
fileContents.set(relativePath, content);
|
|
491
|
-
const fileNode = {
|
|
492
|
-
id: fileId,
|
|
493
|
-
type: "file",
|
|
494
|
-
name: path.basename(filePath),
|
|
495
|
-
path: relativePath,
|
|
496
|
-
metadata: { language: this.detectLanguage(filePath) },
|
|
497
|
-
lastModified: stat2.mtime.toISOString()
|
|
498
|
-
};
|
|
499
|
-
this.store.addNode(fileNode);
|
|
500
|
-
nodesAdded++;
|
|
501
|
-
const symbols = this.extractSymbols(content, fileId, relativePath);
|
|
502
|
-
for (const { node, edge } of symbols) {
|
|
503
|
-
this.store.addNode(node);
|
|
504
|
-
this.store.addEdge(edge);
|
|
505
|
-
nodesAdded++;
|
|
506
|
-
edgesAdded++;
|
|
507
|
-
if (node.type === "function" || node.type === "method") {
|
|
508
|
-
let files2 = nameToFiles.get(node.name);
|
|
509
|
-
if (!files2) {
|
|
510
|
-
files2 = /* @__PURE__ */ new Set();
|
|
511
|
-
nameToFiles.set(node.name, files2);
|
|
512
|
-
}
|
|
513
|
-
files2.add(relativePath);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
const imports = await this.extractImports(content, fileId, relativePath, rootDir);
|
|
517
|
-
for (const edge of imports) {
|
|
518
|
-
this.store.addEdge(edge);
|
|
519
|
-
edgesAdded++;
|
|
520
|
-
}
|
|
554
|
+
const result = await this.processFile(filePath, rootDir, nameToFiles, fileContents);
|
|
555
|
+
nodesAdded += result.nodesAdded;
|
|
556
|
+
edgesAdded += result.edgesAdded;
|
|
521
557
|
} catch (err) {
|
|
522
558
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
523
559
|
}
|
|
@@ -536,6 +572,48 @@ var CodeIngestor = class {
|
|
|
536
572
|
durationMs: Date.now() - start
|
|
537
573
|
};
|
|
538
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
|
+
}
|
|
539
617
|
async findSourceFiles(dir) {
|
|
540
618
|
const results = [];
|
|
541
619
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -552,149 +630,152 @@ var CodeIngestor = class {
|
|
|
552
630
|
extractSymbols(content, fileId, relativePath) {
|
|
553
631
|
const results = [];
|
|
554
632
|
const lines = content.split("\n");
|
|
555
|
-
|
|
556
|
-
let currentClassId = null;
|
|
557
|
-
let braceDepth = 0;
|
|
558
|
-
let insideClass = false;
|
|
633
|
+
const ctx = { className: null, classId: null, insideClass: false, braceDepth: 0 };
|
|
559
634
|
for (let i = 0; i < lines.length; i++) {
|
|
560
635
|
const line = lines[i];
|
|
561
|
-
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
id,
|
|
569
|
-
type: "function",
|
|
570
|
-
name,
|
|
571
|
-
path: relativePath,
|
|
572
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
573
|
-
metadata: {
|
|
574
|
-
exported: line.includes("export"),
|
|
575
|
-
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
576
|
-
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
577
|
-
lineCount: endLine - i,
|
|
578
|
-
parameterCount: this.countParameters(line)
|
|
579
|
-
}
|
|
580
|
-
},
|
|
581
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
582
|
-
});
|
|
583
|
-
if (!insideClass) {
|
|
584
|
-
currentClassName = null;
|
|
585
|
-
currentClassId = null;
|
|
586
|
-
}
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
589
|
-
const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
|
|
590
|
-
if (classMatch) {
|
|
591
|
-
const name = classMatch[1];
|
|
592
|
-
const id = `class:${relativePath}:${name}`;
|
|
593
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
594
|
-
results.push({
|
|
595
|
-
node: {
|
|
596
|
-
id,
|
|
597
|
-
type: "class",
|
|
598
|
-
name,
|
|
599
|
-
path: relativePath,
|
|
600
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
601
|
-
metadata: { exported: line.includes("export") }
|
|
602
|
-
},
|
|
603
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
604
|
-
});
|
|
605
|
-
currentClassName = name;
|
|
606
|
-
currentClassId = id;
|
|
607
|
-
insideClass = true;
|
|
608
|
-
braceDepth = 0;
|
|
609
|
-
for (const ch of line) {
|
|
610
|
-
if (ch === "{") braceDepth++;
|
|
611
|
-
if (ch === "}") braceDepth--;
|
|
612
|
-
}
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
const ifaceMatch = line.match(/(?:export\s+)?interface\s+(\w+)/);
|
|
616
|
-
if (ifaceMatch) {
|
|
617
|
-
const name = ifaceMatch[1];
|
|
618
|
-
const id = `interface:${relativePath}:${name}`;
|
|
619
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
620
|
-
results.push({
|
|
621
|
-
node: {
|
|
622
|
-
id,
|
|
623
|
-
type: "interface",
|
|
624
|
-
name,
|
|
625
|
-
path: relativePath,
|
|
626
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
627
|
-
metadata: { exported: line.includes("export") }
|
|
628
|
-
},
|
|
629
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
630
|
-
});
|
|
631
|
-
currentClassName = null;
|
|
632
|
-
currentClassId = null;
|
|
633
|
-
insideClass = false;
|
|
634
|
-
continue;
|
|
635
|
-
}
|
|
636
|
-
if (insideClass) {
|
|
637
|
-
for (const ch of line) {
|
|
638
|
-
if (ch === "{") braceDepth++;
|
|
639
|
-
if (ch === "}") braceDepth--;
|
|
640
|
-
}
|
|
641
|
-
if (braceDepth <= 0) {
|
|
642
|
-
currentClassName = null;
|
|
643
|
-
currentClassId = null;
|
|
644
|
-
insideClass = false;
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
if (insideClass && currentClassName && currentClassId) {
|
|
649
|
-
const methodMatch = line.match(
|
|
650
|
-
/^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/
|
|
651
|
-
);
|
|
652
|
-
if (methodMatch) {
|
|
653
|
-
const methodName = methodMatch[1];
|
|
654
|
-
if (methodName === "constructor" || methodName === "if" || methodName === "for" || methodName === "while" || methodName === "switch")
|
|
655
|
-
continue;
|
|
656
|
-
const id = `method:${relativePath}:${currentClassName}.${methodName}`;
|
|
657
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
658
|
-
results.push({
|
|
659
|
-
node: {
|
|
660
|
-
id,
|
|
661
|
-
type: "method",
|
|
662
|
-
name: methodName,
|
|
663
|
-
path: relativePath,
|
|
664
|
-
location: { fileId, startLine: i + 1, endLine },
|
|
665
|
-
metadata: {
|
|
666
|
-
className: currentClassName,
|
|
667
|
-
exported: false,
|
|
668
|
-
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
669
|
-
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
670
|
-
lineCount: endLine - i,
|
|
671
|
-
parameterCount: this.countParameters(line)
|
|
672
|
-
}
|
|
673
|
-
},
|
|
674
|
-
edge: { from: currentClassId, to: id, type: "contains" }
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
const varMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)/);
|
|
680
|
-
if (varMatch) {
|
|
681
|
-
const name = varMatch[1];
|
|
682
|
-
const id = `variable:${relativePath}:${name}`;
|
|
683
|
-
results.push({
|
|
684
|
-
node: {
|
|
685
|
-
id,
|
|
686
|
-
type: "variable",
|
|
687
|
-
name,
|
|
688
|
-
path: relativePath,
|
|
689
|
-
location: { fileId, startLine: i + 1, endLine: i + 1 },
|
|
690
|
-
metadata: { exported: line.includes("export") }
|
|
691
|
-
},
|
|
692
|
-
edge: { from: fileId, to: id, type: "contains" }
|
|
693
|
-
});
|
|
694
|
-
}
|
|
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);
|
|
695
643
|
}
|
|
696
644
|
return results;
|
|
697
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
|
+
}
|
|
698
779
|
/**
|
|
699
780
|
* Find the closing brace for a construct starting at the given line.
|
|
700
781
|
* Uses a simple brace-counting heuristic. Returns 1-indexed line number.
|
|
@@ -1314,17 +1395,33 @@ var KnowledgeIngestor = class {
|
|
|
1314
1395
|
|
|
1315
1396
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
1316
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
|
+
];
|
|
1317
1420
|
function sanitizeExternalText(text, maxLength = 2e3) {
|
|
1318
|
-
let sanitized = text
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
/(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
|
|
1323
|
-
"[filtered]"
|
|
1324
|
-
).replace(
|
|
1325
|
-
/you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
|
|
1326
|
-
"[filtered]"
|
|
1327
|
-
);
|
|
1421
|
+
let sanitized = text;
|
|
1422
|
+
for (const rule of SANITIZE_RULES) {
|
|
1423
|
+
sanitized = sanitized.replace(rule.pattern, rule.replacement);
|
|
1424
|
+
}
|
|
1328
1425
|
if (sanitized.length > maxLength) {
|
|
1329
1426
|
sanitized = sanitized.slice(0, maxLength) + "\u2026";
|
|
1330
1427
|
}
|
|
@@ -1424,6 +1521,28 @@ var SyncManager = class {
|
|
|
1424
1521
|
};
|
|
1425
1522
|
|
|
1426
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
|
+
}
|
|
1427
1546
|
var JiraConnector = class {
|
|
1428
1547
|
name = "jira";
|
|
1429
1548
|
source = "jira";
|
|
@@ -1433,105 +1552,81 @@ var JiraConnector = class {
|
|
|
1433
1552
|
}
|
|
1434
1553
|
async ingest(store, config) {
|
|
1435
1554
|
const start = Date.now();
|
|
1436
|
-
const errors = [];
|
|
1437
1555
|
let nodesAdded = 0;
|
|
1438
1556
|
let edgesAdded = 0;
|
|
1439
1557
|
const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
|
|
1440
1558
|
const apiKey = process.env[apiKeyEnv];
|
|
1441
1559
|
if (!apiKey) {
|
|
1442
|
-
return
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
durationMs: Date.now() - start
|
|
1449
|
-
};
|
|
1560
|
+
return buildIngestResult(
|
|
1561
|
+
0,
|
|
1562
|
+
0,
|
|
1563
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
1564
|
+
start
|
|
1565
|
+
);
|
|
1450
1566
|
}
|
|
1451
1567
|
const baseUrlEnv = config.baseUrlEnv ?? "JIRA_BASE_URL";
|
|
1452
1568
|
const baseUrl = process.env[baseUrlEnv];
|
|
1453
1569
|
if (!baseUrl) {
|
|
1454
|
-
return
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
durationMs: Date.now() - start
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
const project2 = config.project;
|
|
1464
|
-
let jql = project2 ? `project=${project2}` : "";
|
|
1465
|
-
const filters = config.filters;
|
|
1466
|
-
if (filters?.status?.length) {
|
|
1467
|
-
jql += `${jql ? " AND " : ""}status IN (${filters.status.map((s) => `"${s}"`).join(",")})`;
|
|
1468
|
-
}
|
|
1469
|
-
if (filters?.labels?.length) {
|
|
1470
|
-
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
|
+
);
|
|
1471
1576
|
}
|
|
1577
|
+
const jql = buildJql(config);
|
|
1472
1578
|
const headers = {
|
|
1473
1579
|
Authorization: `Basic ${apiKey}`,
|
|
1474
1580
|
"Content-Type": "application/json"
|
|
1475
1581
|
};
|
|
1476
|
-
let startAt = 0;
|
|
1477
|
-
const maxResults = 50;
|
|
1478
|
-
let total = Infinity;
|
|
1479
1582
|
try {
|
|
1583
|
+
let startAt = 0;
|
|
1584
|
+
const maxResults = 50;
|
|
1585
|
+
let total = Infinity;
|
|
1480
1586
|
while (startAt < total) {
|
|
1481
1587
|
const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
|
|
1482
1588
|
const response = await this.httpClient(url, { headers });
|
|
1483
1589
|
if (!response.ok) {
|
|
1484
|
-
return
|
|
1485
|
-
nodesAdded,
|
|
1486
|
-
nodesUpdated: 0,
|
|
1487
|
-
edgesAdded,
|
|
1488
|
-
edgesUpdated: 0,
|
|
1489
|
-
errors: ["Jira API request failed"],
|
|
1490
|
-
durationMs: Date.now() - start
|
|
1491
|
-
};
|
|
1590
|
+
return buildIngestResult(nodesAdded, edgesAdded, ["Jira API request failed"], start);
|
|
1492
1591
|
}
|
|
1493
1592
|
const data = await response.json();
|
|
1494
1593
|
total = data.total;
|
|
1495
1594
|
for (const issue of data.issues) {
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
type: "issue",
|
|
1500
|
-
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
1501
|
-
metadata: {
|
|
1502
|
-
key: issue.key,
|
|
1503
|
-
status: issue.fields.status?.name,
|
|
1504
|
-
priority: issue.fields.priority?.name,
|
|
1505
|
-
assignee: issue.fields.assignee?.displayName,
|
|
1506
|
-
labels: issue.fields.labels ?? []
|
|
1507
|
-
}
|
|
1508
|
-
});
|
|
1509
|
-
nodesAdded++;
|
|
1510
|
-
const searchText = sanitizeExternalText(
|
|
1511
|
-
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
1512
|
-
);
|
|
1513
|
-
edgesAdded += linkToCode(store, searchText, nodeId, "applies_to");
|
|
1595
|
+
const counts = this.processIssue(store, issue);
|
|
1596
|
+
nodesAdded += counts.nodesAdded;
|
|
1597
|
+
edgesAdded += counts.edgesAdded;
|
|
1514
1598
|
}
|
|
1515
1599
|
startAt += maxResults;
|
|
1516
1600
|
}
|
|
1517
1601
|
} catch (err) {
|
|
1518
|
-
return
|
|
1602
|
+
return buildIngestResult(
|
|
1519
1603
|
nodesAdded,
|
|
1520
|
-
nodesUpdated: 0,
|
|
1521
1604
|
edgesAdded,
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
};
|
|
1605
|
+
[`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
|
|
1606
|
+
start
|
|
1607
|
+
);
|
|
1526
1608
|
}
|
|
1527
|
-
return
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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 };
|
|
1535
1630
|
}
|
|
1536
1631
|
};
|
|
1537
1632
|
|
|
@@ -1564,44 +1659,10 @@ var SlackConnector = class {
|
|
|
1564
1659
|
const oldest = config.lookbackDays ? String(Math.floor((Date.now() - Number(config.lookbackDays) * 864e5) / 1e3)) : void 0;
|
|
1565
1660
|
for (const channel of channels) {
|
|
1566
1661
|
try {
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
const response = await this.httpClient(url, {
|
|
1572
|
-
headers: {
|
|
1573
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1574
|
-
"Content-Type": "application/json"
|
|
1575
|
-
}
|
|
1576
|
-
});
|
|
1577
|
-
if (!response.ok) {
|
|
1578
|
-
errors.push(`Slack API request failed for channel ${channel}`);
|
|
1579
|
-
continue;
|
|
1580
|
-
}
|
|
1581
|
-
const data = await response.json();
|
|
1582
|
-
if (!data.ok) {
|
|
1583
|
-
errors.push(`Slack API error for channel ${channel}`);
|
|
1584
|
-
continue;
|
|
1585
|
-
}
|
|
1586
|
-
for (const message of data.messages) {
|
|
1587
|
-
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
1588
|
-
const sanitizedText = sanitizeExternalText(message.text);
|
|
1589
|
-
const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
|
|
1590
|
-
store.addNode({
|
|
1591
|
-
id: nodeId,
|
|
1592
|
-
type: "conversation",
|
|
1593
|
-
name: snippet,
|
|
1594
|
-
metadata: {
|
|
1595
|
-
author: message.user,
|
|
1596
|
-
channel,
|
|
1597
|
-
timestamp: message.ts
|
|
1598
|
-
}
|
|
1599
|
-
});
|
|
1600
|
-
nodesAdded++;
|
|
1601
|
-
edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
|
|
1602
|
-
checkPaths: true
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1662
|
+
const result = await this.processChannel(store, channel, apiKey, oldest);
|
|
1663
|
+
nodesAdded += result.nodesAdded;
|
|
1664
|
+
edgesAdded += result.edgesAdded;
|
|
1665
|
+
errors.push(...result.errors);
|
|
1605
1666
|
} catch (err) {
|
|
1606
1667
|
errors.push(
|
|
1607
1668
|
`Slack API error for channel ${channel}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -1617,6 +1678,52 @@ var SlackConnector = class {
|
|
|
1617
1678
|
durationMs: Date.now() - start
|
|
1618
1679
|
};
|
|
1619
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
|
+
}
|
|
1620
1727
|
};
|
|
1621
1728
|
|
|
1622
1729
|
// src/ingest/connectors/ConfluenceConnector.ts
|
|
@@ -1648,36 +1755,10 @@ var ConfluenceConnector = class {
|
|
|
1648
1755
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
1649
1756
|
const spaceKey = config.spaceKey ?? "";
|
|
1650
1757
|
try {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
});
|
|
1656
|
-
if (!response.ok) {
|
|
1657
|
-
errors.push(`Confluence API error: status ${response.status}`);
|
|
1658
|
-
break;
|
|
1659
|
-
}
|
|
1660
|
-
const data = await response.json();
|
|
1661
|
-
for (const page of data.results) {
|
|
1662
|
-
const nodeId = `confluence:${page.id}`;
|
|
1663
|
-
store.addNode({
|
|
1664
|
-
id: nodeId,
|
|
1665
|
-
type: "document",
|
|
1666
|
-
name: sanitizeExternalText(page.title, 500),
|
|
1667
|
-
metadata: {
|
|
1668
|
-
source: "confluence",
|
|
1669
|
-
spaceKey,
|
|
1670
|
-
pageId: page.id,
|
|
1671
|
-
status: page.status,
|
|
1672
|
-
url: page._links?.webui ?? ""
|
|
1673
|
-
}
|
|
1674
|
-
});
|
|
1675
|
-
nodesAdded++;
|
|
1676
|
-
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
1677
|
-
edgesAdded += linkToCode(store, text, nodeId, "documents");
|
|
1678
|
-
}
|
|
1679
|
-
nextUrl = data._links?.next ? `${baseUrl}${data._links.next}` : null;
|
|
1680
|
-
}
|
|
1758
|
+
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
1759
|
+
nodesAdded = result.nodesAdded;
|
|
1760
|
+
edgesAdded = result.edgesAdded;
|
|
1761
|
+
errors.push(...result.errors);
|
|
1681
1762
|
} catch (err) {
|
|
1682
1763
|
errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1683
1764
|
}
|
|
@@ -1690,6 +1771,47 @@ var ConfluenceConnector = class {
|
|
|
1690
1771
|
durationMs: Date.now() - start
|
|
1691
1772
|
};
|
|
1692
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
|
+
}
|
|
1693
1815
|
};
|
|
1694
1816
|
|
|
1695
1817
|
// src/ingest/connectors/CIConnector.ts
|
|
@@ -1982,22 +2104,25 @@ var GraphEntropyAdapter = class {
|
|
|
1982
2104
|
* 3. Unreachable = code nodes NOT in visited set
|
|
1983
2105
|
*/
|
|
1984
2106
|
computeDeadCodeData() {
|
|
1985
|
-
const
|
|
2107
|
+
const entryPoints = this.findEntryPoints();
|
|
2108
|
+
const visited = this.bfsFromEntryPoints(entryPoints);
|
|
2109
|
+
const unreachableNodes = this.collectUnreachableNodes(visited);
|
|
2110
|
+
return { reachableNodeIds: visited, unreachableNodes, entryPoints };
|
|
2111
|
+
}
|
|
2112
|
+
findEntryPoints() {
|
|
1986
2113
|
const entryPoints = [];
|
|
1987
|
-
for (const node of allFileNodes) {
|
|
1988
|
-
if (node.name === "index.ts" || node.metadata?.entryPoint === true) {
|
|
1989
|
-
entryPoints.push(node.id);
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
2114
|
for (const nodeType of CODE_NODE_TYPES3) {
|
|
1993
|
-
if (nodeType === "file") continue;
|
|
1994
2115
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
1995
2116
|
for (const node of nodes) {
|
|
1996
|
-
|
|
2117
|
+
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
2118
|
+
if (isIndexFile || node.metadata?.entryPoint === true) {
|
|
1997
2119
|
entryPoints.push(node.id);
|
|
1998
2120
|
}
|
|
1999
2121
|
}
|
|
2000
2122
|
}
|
|
2123
|
+
return entryPoints;
|
|
2124
|
+
}
|
|
2125
|
+
bfsFromEntryPoints(entryPoints) {
|
|
2001
2126
|
const visited = /* @__PURE__ */ new Set();
|
|
2002
2127
|
const queue = [...entryPoints];
|
|
2003
2128
|
let head = 0;
|
|
@@ -2005,25 +2130,22 @@ var GraphEntropyAdapter = class {
|
|
|
2005
2130
|
const nodeId = queue[head++];
|
|
2006
2131
|
if (visited.has(nodeId)) continue;
|
|
2007
2132
|
visited.add(nodeId);
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
queue.push(edge.to);
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
const containsEdges = this.store.getEdges({ from: nodeId, type: "contains" });
|
|
2021
|
-
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) {
|
|
2022
2142
|
if (!visited.has(edge.to)) {
|
|
2023
2143
|
queue.push(edge.to);
|
|
2024
2144
|
}
|
|
2025
2145
|
}
|
|
2026
2146
|
}
|
|
2147
|
+
}
|
|
2148
|
+
collectUnreachableNodes(visited) {
|
|
2027
2149
|
const unreachableNodes = [];
|
|
2028
2150
|
for (const nodeType of CODE_NODE_TYPES3) {
|
|
2029
2151
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
@@ -2038,11 +2160,7 @@ var GraphEntropyAdapter = class {
|
|
|
2038
2160
|
}
|
|
2039
2161
|
}
|
|
2040
2162
|
}
|
|
2041
|
-
return
|
|
2042
|
-
reachableNodeIds: visited,
|
|
2043
|
-
unreachableNodes,
|
|
2044
|
-
entryPoints
|
|
2045
|
-
};
|
|
2163
|
+
return unreachableNodes;
|
|
2046
2164
|
}
|
|
2047
2165
|
/**
|
|
2048
2166
|
* Count all nodes and edges by type.
|
|
@@ -2093,33 +2211,9 @@ var GraphComplexityAdapter = class {
|
|
|
2093
2211
|
const hotspots = [];
|
|
2094
2212
|
for (const fnNode of functionNodes) {
|
|
2095
2213
|
const complexity = fnNode.metadata?.cyclomaticComplexity ?? 1;
|
|
2096
|
-
const
|
|
2097
|
-
let fileId;
|
|
2098
|
-
for (const edge of containsEdges) {
|
|
2099
|
-
const sourceNode = this.store.getNode(edge.from);
|
|
2100
|
-
if (sourceNode?.type === "file") {
|
|
2101
|
-
fileId = sourceNode.id;
|
|
2102
|
-
break;
|
|
2103
|
-
}
|
|
2104
|
-
if (sourceNode?.type === "class") {
|
|
2105
|
-
const classContainsEdges = this.store.getEdges({ to: sourceNode.id, type: "contains" });
|
|
2106
|
-
for (const classEdge of classContainsEdges) {
|
|
2107
|
-
const parentNode = this.store.getNode(classEdge.from);
|
|
2108
|
-
if (parentNode?.type === "file") {
|
|
2109
|
-
fileId = parentNode.id;
|
|
2110
|
-
break;
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
if (fileId) break;
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2214
|
+
const fileId = this.findContainingFileId(fnNode.id);
|
|
2116
2215
|
if (!fileId) continue;
|
|
2117
|
-
|
|
2118
|
-
if (changeFrequency === void 0) {
|
|
2119
|
-
const referencesEdges = this.store.getEdges({ to: fileId, type: "references" });
|
|
2120
|
-
changeFrequency = referencesEdges.length;
|
|
2121
|
-
fileChangeFrequency.set(fileId, changeFrequency);
|
|
2122
|
-
}
|
|
2216
|
+
const changeFrequency = this.getChangeFrequency(fileId, fileChangeFrequency);
|
|
2123
2217
|
const hotspotScore = changeFrequency * complexity;
|
|
2124
2218
|
const filePath = fnNode.path ?? fileId.replace(/^file:/, "");
|
|
2125
2219
|
hotspots.push({
|
|
@@ -2137,6 +2231,39 @@ var GraphComplexityAdapter = class {
|
|
|
2137
2231
|
);
|
|
2138
2232
|
return { hotspots, percentile95Score };
|
|
2139
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
|
+
}
|
|
2140
2267
|
computePercentile(descendingScores, percentile) {
|
|
2141
2268
|
if (descendingScores.length === 0) return 0;
|
|
2142
2269
|
const ascending = [...descendingScores].sort((a, b) => a - b);
|
|
@@ -2443,6 +2570,689 @@ var GraphAnomalyAdapter = class {
|
|
|
2443
2570
|
}
|
|
2444
2571
|
};
|
|
2445
2572
|
|
|
2573
|
+
// src/nlq/types.ts
|
|
2574
|
+
var INTENTS = ["impact", "find", "relationships", "explain", "anomaly"];
|
|
2575
|
+
|
|
2576
|
+
// src/nlq/IntentClassifier.ts
|
|
2577
|
+
var SIGNAL_WEIGHTS = {
|
|
2578
|
+
keyword: 0.35,
|
|
2579
|
+
questionWord: 0.2,
|
|
2580
|
+
verbPattern: 0.45
|
|
2581
|
+
};
|
|
2582
|
+
var INTENT_SIGNALS = {
|
|
2583
|
+
impact: {
|
|
2584
|
+
keywords: [
|
|
2585
|
+
"break",
|
|
2586
|
+
"affect",
|
|
2587
|
+
"impact",
|
|
2588
|
+
"change",
|
|
2589
|
+
"depend",
|
|
2590
|
+
"blast",
|
|
2591
|
+
"radius",
|
|
2592
|
+
"risk",
|
|
2593
|
+
"delete",
|
|
2594
|
+
"remove"
|
|
2595
|
+
],
|
|
2596
|
+
questionWords: ["what", "if"],
|
|
2597
|
+
verbPatterns: [
|
|
2598
|
+
/what\s+(breaks|happens|is affected)/,
|
|
2599
|
+
/if\s+i\s+(change|modify|remove|delete)/,
|
|
2600
|
+
/blast\s+radius/,
|
|
2601
|
+
/what\s+(depend|relies)/
|
|
2602
|
+
]
|
|
2603
|
+
},
|
|
2604
|
+
find: {
|
|
2605
|
+
keywords: ["find", "where", "locate", "search", "list", "all", "every"],
|
|
2606
|
+
questionWords: ["where"],
|
|
2607
|
+
verbPatterns: [
|
|
2608
|
+
/where\s+is/,
|
|
2609
|
+
/find\s+(the|all|every)/,
|
|
2610
|
+
/show\s+me/,
|
|
2611
|
+
/show\s+(all|every|the)/,
|
|
2612
|
+
/locate\s+/,
|
|
2613
|
+
/list\s+(all|every|the)/
|
|
2614
|
+
]
|
|
2615
|
+
},
|
|
2616
|
+
relationships: {
|
|
2617
|
+
keywords: [
|
|
2618
|
+
"connect",
|
|
2619
|
+
"call",
|
|
2620
|
+
"import",
|
|
2621
|
+
"use",
|
|
2622
|
+
"depend",
|
|
2623
|
+
"link",
|
|
2624
|
+
"neighbor",
|
|
2625
|
+
"caller",
|
|
2626
|
+
"callee"
|
|
2627
|
+
],
|
|
2628
|
+
questionWords: ["what", "who"],
|
|
2629
|
+
verbPatterns: [/connects?\s+to/, /depends?\s+on/, /\bcalls?\b/, /\bimports?\b/]
|
|
2630
|
+
},
|
|
2631
|
+
explain: {
|
|
2632
|
+
keywords: ["describe", "explain", "tell", "about", "overview", "summary", "work"],
|
|
2633
|
+
questionWords: ["what", "how"],
|
|
2634
|
+
verbPatterns: [
|
|
2635
|
+
/what\s+is\s+\w/,
|
|
2636
|
+
/describe\s+/,
|
|
2637
|
+
/tell\s+me\s+about/,
|
|
2638
|
+
/how\s+does/,
|
|
2639
|
+
/overview\s+of/,
|
|
2640
|
+
/give\s+me\s+/
|
|
2641
|
+
]
|
|
2642
|
+
},
|
|
2643
|
+
anomaly: {
|
|
2644
|
+
keywords: [
|
|
2645
|
+
"wrong",
|
|
2646
|
+
"problem",
|
|
2647
|
+
"anomaly",
|
|
2648
|
+
"smell",
|
|
2649
|
+
"issue",
|
|
2650
|
+
"outlier",
|
|
2651
|
+
"hotspot",
|
|
2652
|
+
"suspicious",
|
|
2653
|
+
"risk"
|
|
2654
|
+
],
|
|
2655
|
+
questionWords: ["what"],
|
|
2656
|
+
verbPatterns: [
|
|
2657
|
+
/what.*(wrong|problem|smell)/,
|
|
2658
|
+
/find.*(issue|anomal|problem)/,
|
|
2659
|
+
/code\s+smell/,
|
|
2660
|
+
/suspicious/,
|
|
2661
|
+
/hotspot/
|
|
2662
|
+
]
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
var IntentClassifier = class {
|
|
2666
|
+
/**
|
|
2667
|
+
* Classify a natural language question into an intent.
|
|
2668
|
+
*
|
|
2669
|
+
* @param question - The natural language question to classify
|
|
2670
|
+
* @returns ClassificationResult with intent, confidence, and per-signal scores
|
|
2671
|
+
*/
|
|
2672
|
+
classify(question) {
|
|
2673
|
+
const normalized = question.toLowerCase().trim();
|
|
2674
|
+
const scores = [];
|
|
2675
|
+
for (const intent of INTENTS) {
|
|
2676
|
+
const signals = this.scoreIntent(normalized, INTENT_SIGNALS[intent]);
|
|
2677
|
+
const confidence = this.combineSignals(signals);
|
|
2678
|
+
scores.push({ intent, confidence, signals });
|
|
2679
|
+
}
|
|
2680
|
+
scores.sort((a, b) => b.confidence - a.confidence);
|
|
2681
|
+
const best = scores[0];
|
|
2682
|
+
return {
|
|
2683
|
+
intent: best.intent,
|
|
2684
|
+
confidence: best.confidence,
|
|
2685
|
+
signals: best.signals
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Score individual signals for an intent against the normalized query.
|
|
2690
|
+
*/
|
|
2691
|
+
scoreIntent(normalized, signalSet) {
|
|
2692
|
+
return {
|
|
2693
|
+
keyword: this.scoreKeywords(normalized, signalSet.keywords),
|
|
2694
|
+
questionWord: this.scoreQuestionWord(normalized, signalSet.questionWords),
|
|
2695
|
+
verbPattern: this.scoreVerbPatterns(normalized, signalSet.verbPatterns)
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* Score keyword signal: uses word-stem matching (checks if any word in the
|
|
2700
|
+
* query starts with the keyword). Saturates at 2 matches to avoid penalizing
|
|
2701
|
+
* intents with many keywords when only a few appear in the query.
|
|
2702
|
+
*/
|
|
2703
|
+
scoreKeywords(normalized, keywords) {
|
|
2704
|
+
if (keywords.length === 0) return 0;
|
|
2705
|
+
const words = normalized.split(/\s+/);
|
|
2706
|
+
let matched = 0;
|
|
2707
|
+
for (const keyword of keywords) {
|
|
2708
|
+
if (words.some((w) => w.startsWith(keyword))) {
|
|
2709
|
+
matched++;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return Math.min(matched / 2, 1);
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Score question-word signal: 1.0 if the query starts with a matching
|
|
2716
|
+
* question word, 0 otherwise.
|
|
2717
|
+
*/
|
|
2718
|
+
scoreQuestionWord(normalized, questionWords) {
|
|
2719
|
+
const firstWord = normalized.split(/\s+/)[0] ?? "";
|
|
2720
|
+
return questionWords.includes(firstWord) ? 1 : 0;
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Score verb-pattern signal: any matching pattern yields a strong score.
|
|
2724
|
+
* Multiple matches increase score but saturate quickly.
|
|
2725
|
+
*/
|
|
2726
|
+
scoreVerbPatterns(normalized, patterns) {
|
|
2727
|
+
if (patterns.length === 0) return 0;
|
|
2728
|
+
let matched = 0;
|
|
2729
|
+
for (const pattern of patterns) {
|
|
2730
|
+
if (pattern.test(normalized)) {
|
|
2731
|
+
matched++;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
return matched === 0 ? 0 : Math.min(0.6 + matched * 0.2, 1);
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Combine individual signal scores into a single confidence score
|
|
2738
|
+
* using additive weighted scoring. Each signal contributes weight * score,
|
|
2739
|
+
* and the total weights sum to 1.0 so the result is naturally bounded [0, 1].
|
|
2740
|
+
*/
|
|
2741
|
+
combineSignals(signals) {
|
|
2742
|
+
let total = 0;
|
|
2743
|
+
for (const key of Object.keys(signals)) {
|
|
2744
|
+
const weight = SIGNAL_WEIGHTS[key];
|
|
2745
|
+
total += signals[key] * weight;
|
|
2746
|
+
}
|
|
2747
|
+
return total;
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
|
|
2751
|
+
// src/nlq/EntityExtractor.ts
|
|
2752
|
+
var INTENT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2753
|
+
// impact
|
|
2754
|
+
"break",
|
|
2755
|
+
"breaks",
|
|
2756
|
+
"affect",
|
|
2757
|
+
"affects",
|
|
2758
|
+
"affected",
|
|
2759
|
+
"impact",
|
|
2760
|
+
"change",
|
|
2761
|
+
"depend",
|
|
2762
|
+
"depends",
|
|
2763
|
+
"blast",
|
|
2764
|
+
"radius",
|
|
2765
|
+
"risk",
|
|
2766
|
+
"delete",
|
|
2767
|
+
"remove",
|
|
2768
|
+
"modify",
|
|
2769
|
+
"happens",
|
|
2770
|
+
// find
|
|
2771
|
+
"find",
|
|
2772
|
+
"where",
|
|
2773
|
+
"locate",
|
|
2774
|
+
"search",
|
|
2775
|
+
"list",
|
|
2776
|
+
"all",
|
|
2777
|
+
"every",
|
|
2778
|
+
"show",
|
|
2779
|
+
// relationships
|
|
2780
|
+
"connect",
|
|
2781
|
+
"connects",
|
|
2782
|
+
"call",
|
|
2783
|
+
"calls",
|
|
2784
|
+
"import",
|
|
2785
|
+
"imports",
|
|
2786
|
+
"use",
|
|
2787
|
+
"uses",
|
|
2788
|
+
"link",
|
|
2789
|
+
"neighbor",
|
|
2790
|
+
"caller",
|
|
2791
|
+
"callers",
|
|
2792
|
+
"callee",
|
|
2793
|
+
"callees",
|
|
2794
|
+
// explain
|
|
2795
|
+
"describe",
|
|
2796
|
+
"explain",
|
|
2797
|
+
"tell",
|
|
2798
|
+
"about",
|
|
2799
|
+
"overview",
|
|
2800
|
+
"summary",
|
|
2801
|
+
"work",
|
|
2802
|
+
"works",
|
|
2803
|
+
// anomaly
|
|
2804
|
+
"wrong",
|
|
2805
|
+
"problem",
|
|
2806
|
+
"problems",
|
|
2807
|
+
"anomaly",
|
|
2808
|
+
"anomalies",
|
|
2809
|
+
"smell",
|
|
2810
|
+
"smells",
|
|
2811
|
+
"issue",
|
|
2812
|
+
"issues",
|
|
2813
|
+
"outlier",
|
|
2814
|
+
"hotspot",
|
|
2815
|
+
"hotspots",
|
|
2816
|
+
"suspicious"
|
|
2817
|
+
]);
|
|
2818
|
+
var STOP_WORDS2 = /* @__PURE__ */ new Set([
|
|
2819
|
+
"a",
|
|
2820
|
+
"an",
|
|
2821
|
+
"the",
|
|
2822
|
+
"is",
|
|
2823
|
+
"are",
|
|
2824
|
+
"was",
|
|
2825
|
+
"were",
|
|
2826
|
+
"be",
|
|
2827
|
+
"been",
|
|
2828
|
+
"being",
|
|
2829
|
+
"have",
|
|
2830
|
+
"has",
|
|
2831
|
+
"had",
|
|
2832
|
+
"do",
|
|
2833
|
+
"does",
|
|
2834
|
+
"did",
|
|
2835
|
+
"will",
|
|
2836
|
+
"would",
|
|
2837
|
+
"could",
|
|
2838
|
+
"should",
|
|
2839
|
+
"may",
|
|
2840
|
+
"might",
|
|
2841
|
+
"shall",
|
|
2842
|
+
"can",
|
|
2843
|
+
"need",
|
|
2844
|
+
"must",
|
|
2845
|
+
"i",
|
|
2846
|
+
"me",
|
|
2847
|
+
"my",
|
|
2848
|
+
"we",
|
|
2849
|
+
"our",
|
|
2850
|
+
"you",
|
|
2851
|
+
"your",
|
|
2852
|
+
"he",
|
|
2853
|
+
"she",
|
|
2854
|
+
"it",
|
|
2855
|
+
"its",
|
|
2856
|
+
"they",
|
|
2857
|
+
"them",
|
|
2858
|
+
"their",
|
|
2859
|
+
"this",
|
|
2860
|
+
"that",
|
|
2861
|
+
"these",
|
|
2862
|
+
"those",
|
|
2863
|
+
"and",
|
|
2864
|
+
"or",
|
|
2865
|
+
"but",
|
|
2866
|
+
"if",
|
|
2867
|
+
"then",
|
|
2868
|
+
"else",
|
|
2869
|
+
"when",
|
|
2870
|
+
"while",
|
|
2871
|
+
"for",
|
|
2872
|
+
"of",
|
|
2873
|
+
"at",
|
|
2874
|
+
"by",
|
|
2875
|
+
"to",
|
|
2876
|
+
"in",
|
|
2877
|
+
"on",
|
|
2878
|
+
"with",
|
|
2879
|
+
"from",
|
|
2880
|
+
"up",
|
|
2881
|
+
"out",
|
|
2882
|
+
"not",
|
|
2883
|
+
"no",
|
|
2884
|
+
"nor",
|
|
2885
|
+
"so",
|
|
2886
|
+
"too",
|
|
2887
|
+
"very",
|
|
2888
|
+
"just",
|
|
2889
|
+
"also",
|
|
2890
|
+
"what",
|
|
2891
|
+
"who",
|
|
2892
|
+
"how",
|
|
2893
|
+
"which",
|
|
2894
|
+
"where",
|
|
2895
|
+
"why",
|
|
2896
|
+
"there",
|
|
2897
|
+
"here",
|
|
2898
|
+
"any",
|
|
2899
|
+
"some",
|
|
2900
|
+
"each",
|
|
2901
|
+
"than",
|
|
2902
|
+
"like",
|
|
2903
|
+
"get",
|
|
2904
|
+
"give",
|
|
2905
|
+
"go",
|
|
2906
|
+
"make",
|
|
2907
|
+
"see",
|
|
2908
|
+
"know",
|
|
2909
|
+
"take"
|
|
2910
|
+
]);
|
|
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;
|
|
2912
|
+
var FILE_PATH_RE = /(?:\.\/|[a-zA-Z0-9_-]+\/)[a-zA-Z0-9_\-./]+\.[a-zA-Z]{1,10}/g;
|
|
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
|
+
}
|
|
2931
|
+
var EntityExtractor = class {
|
|
2932
|
+
/**
|
|
2933
|
+
* Extract candidate entity mentions from a natural language query.
|
|
2934
|
+
*
|
|
2935
|
+
* @param query - The natural language query to extract entities from
|
|
2936
|
+
* @returns Array of raw entity strings in priority order, deduplicated
|
|
2937
|
+
*/
|
|
2938
|
+
extract(query) {
|
|
2939
|
+
const trimmed = query.trim();
|
|
2940
|
+
if (trimmed.length === 0) return [];
|
|
2941
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2942
|
+
const result = [];
|
|
2943
|
+
const add = (entity) => {
|
|
2944
|
+
if (!seen.has(entity)) {
|
|
2945
|
+
seen.add(entity);
|
|
2946
|
+
result.push(entity);
|
|
2947
|
+
}
|
|
2948
|
+
};
|
|
2949
|
+
const quotedConsumed = /* @__PURE__ */ new Set();
|
|
2950
|
+
for (const match of trimmed.matchAll(QUOTED_RE)) {
|
|
2951
|
+
const inner = match[1].trim();
|
|
2952
|
+
if (inner.length > 0) {
|
|
2953
|
+
add(inner);
|
|
2954
|
+
quotedConsumed.add(inner);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
const casingConsumed = /* @__PURE__ */ new Set();
|
|
2958
|
+
for (const match of trimmed.matchAll(PASCAL_OR_CAMEL_RE)) {
|
|
2959
|
+
const token = match[0];
|
|
2960
|
+
if (!quotedConsumed.has(token)) {
|
|
2961
|
+
add(token);
|
|
2962
|
+
casingConsumed.add(token);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
const pathConsumed = /* @__PURE__ */ new Set();
|
|
2966
|
+
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
2967
|
+
const path6 = match[0];
|
|
2968
|
+
add(path6);
|
|
2969
|
+
pathConsumed.add(path6);
|
|
2970
|
+
}
|
|
2971
|
+
const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
|
|
2972
|
+
const words = trimmed.split(/\s+/);
|
|
2973
|
+
for (const raw of words) {
|
|
2974
|
+
const cleaned = raw.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "");
|
|
2975
|
+
if (cleaned.length === 0) continue;
|
|
2976
|
+
if (isSkippableWord(cleaned, allConsumed)) continue;
|
|
2977
|
+
add(cleaned);
|
|
2978
|
+
}
|
|
2979
|
+
return result;
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
|
|
2983
|
+
// src/nlq/EntityResolver.ts
|
|
2984
|
+
var EntityResolver = class {
|
|
2985
|
+
store;
|
|
2986
|
+
fusion;
|
|
2987
|
+
constructor(store, fusion) {
|
|
2988
|
+
this.store = store;
|
|
2989
|
+
this.fusion = fusion;
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Resolve an array of raw entity strings to graph nodes.
|
|
2993
|
+
*
|
|
2994
|
+
* @param raws - Raw entity strings from EntityExtractor
|
|
2995
|
+
* @returns Array of ResolvedEntity for each successfully resolved raw string
|
|
2996
|
+
*/
|
|
2997
|
+
resolve(raws) {
|
|
2998
|
+
const results = [];
|
|
2999
|
+
for (const raw of raws) {
|
|
3000
|
+
const resolved = this.resolveOne(raw);
|
|
3001
|
+
if (resolved !== void 0) {
|
|
3002
|
+
results.push(resolved);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
return results;
|
|
3006
|
+
}
|
|
3007
|
+
resolveOne(raw) {
|
|
3008
|
+
const exactMatches = this.store.findNodes({ name: raw });
|
|
3009
|
+
if (exactMatches.length > 0) {
|
|
3010
|
+
const node = exactMatches[0];
|
|
3011
|
+
return {
|
|
3012
|
+
raw,
|
|
3013
|
+
nodeId: node.id,
|
|
3014
|
+
node,
|
|
3015
|
+
confidence: 1,
|
|
3016
|
+
method: "exact"
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
if (this.fusion) {
|
|
3020
|
+
const fusionResults = this.fusion.search(raw, 5);
|
|
3021
|
+
if (fusionResults.length > 0 && fusionResults[0].score > 0.5) {
|
|
3022
|
+
const top = fusionResults[0];
|
|
3023
|
+
return {
|
|
3024
|
+
raw,
|
|
3025
|
+
nodeId: top.nodeId,
|
|
3026
|
+
node: top.node,
|
|
3027
|
+
confidence: top.score,
|
|
3028
|
+
method: "fusion"
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
if (raw.length < 3) return void 0;
|
|
3033
|
+
const isPathLike = raw.includes("/");
|
|
3034
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
3035
|
+
for (const node of fileNodes) {
|
|
3036
|
+
if (!node.path) continue;
|
|
3037
|
+
if (isPathLike && node.path.includes(raw)) {
|
|
3038
|
+
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3039
|
+
}
|
|
3040
|
+
const basename4 = node.path.split("/").pop() ?? "";
|
|
3041
|
+
if (basename4.includes(raw)) {
|
|
3042
|
+
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3043
|
+
}
|
|
3044
|
+
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
3045
|
+
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
return void 0;
|
|
3049
|
+
}
|
|
3050
|
+
};
|
|
3051
|
+
|
|
3052
|
+
// src/nlq/ResponseFormatter.ts
|
|
3053
|
+
var ResponseFormatter = class {
|
|
3054
|
+
/**
|
|
3055
|
+
* Format graph operation results into a human-readable summary.
|
|
3056
|
+
*
|
|
3057
|
+
* @param intent - The classified intent
|
|
3058
|
+
* @param entities - Resolved entities from the query
|
|
3059
|
+
* @param data - Raw result data (shape varies per intent)
|
|
3060
|
+
* @param query - Original natural language query (optional)
|
|
3061
|
+
* @returns Human-readable summary string
|
|
3062
|
+
*/
|
|
3063
|
+
format(intent, entities, data, query) {
|
|
3064
|
+
if (data === null || data === void 0) {
|
|
3065
|
+
return "No results found.";
|
|
3066
|
+
}
|
|
3067
|
+
const firstEntity = entities[0];
|
|
3068
|
+
const entityName = firstEntity?.raw ?? "the target";
|
|
3069
|
+
switch (intent) {
|
|
3070
|
+
case "impact":
|
|
3071
|
+
return this.formatImpact(entityName, data);
|
|
3072
|
+
case "find":
|
|
3073
|
+
return this.formatFind(data, query);
|
|
3074
|
+
case "relationships":
|
|
3075
|
+
return this.formatRelationships(entityName, entities, data);
|
|
3076
|
+
case "explain":
|
|
3077
|
+
return this.formatExplain(entityName, entities, data);
|
|
3078
|
+
case "anomaly":
|
|
3079
|
+
return this.formatAnomaly(data);
|
|
3080
|
+
default:
|
|
3081
|
+
return `Processed results for "${entityName}".`;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
formatImpact(entityName, data) {
|
|
3085
|
+
const d = data;
|
|
3086
|
+
const code = this.safeArrayLength(d?.code);
|
|
3087
|
+
const tests = this.safeArrayLength(d?.tests);
|
|
3088
|
+
const docs = this.safeArrayLength(d?.docs);
|
|
3089
|
+
return `Changing **${entityName}** affects ${this.p(code, "code file")}, ${this.p(tests, "test")}, and ${this.p(docs, "doc")}.`;
|
|
3090
|
+
}
|
|
3091
|
+
formatFind(data, query) {
|
|
3092
|
+
const count = Array.isArray(data) ? data.length : 0;
|
|
3093
|
+
if (query) {
|
|
3094
|
+
return `Found ${this.p(count, "match", "matches")} for "${query}".`;
|
|
3095
|
+
}
|
|
3096
|
+
return `Found ${this.p(count, "match", "matches")}.`;
|
|
3097
|
+
}
|
|
3098
|
+
formatRelationships(entityName, entities, data) {
|
|
3099
|
+
const d = data;
|
|
3100
|
+
const edges = Array.isArray(d?.edges) ? d.edges : [];
|
|
3101
|
+
const firstEntity = entities[0];
|
|
3102
|
+
const rootId = firstEntity?.nodeId ?? "";
|
|
3103
|
+
let outbound = 0;
|
|
3104
|
+
let inbound = 0;
|
|
3105
|
+
for (const edge of edges) {
|
|
3106
|
+
if (edge.from === rootId) outbound++;
|
|
3107
|
+
if (edge.to === rootId) inbound++;
|
|
3108
|
+
}
|
|
3109
|
+
return `**${entityName}** has ${outbound} outbound and ${inbound} inbound relationships.`;
|
|
3110
|
+
}
|
|
3111
|
+
formatExplain(entityName, entities, data) {
|
|
3112
|
+
const d = data;
|
|
3113
|
+
const context = Array.isArray(d?.context) ? d.context : [];
|
|
3114
|
+
const firstEntity = entities[0];
|
|
3115
|
+
const nodeType = firstEntity?.node.type ?? "node";
|
|
3116
|
+
const path6 = firstEntity?.node.path ?? "unknown";
|
|
3117
|
+
let neighborCount = 0;
|
|
3118
|
+
const firstContext = context[0];
|
|
3119
|
+
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
3120
|
+
neighborCount = firstContext.nodes.length;
|
|
3121
|
+
}
|
|
3122
|
+
return `**${entityName}** is a ${nodeType} at \`${path6}\`. Connected to ${neighborCount} nodes.`;
|
|
3123
|
+
}
|
|
3124
|
+
formatAnomaly(data) {
|
|
3125
|
+
const d = data;
|
|
3126
|
+
const outliers = Array.isArray(d?.statisticalOutliers) ? d.statisticalOutliers : [];
|
|
3127
|
+
const artPoints = Array.isArray(d?.articulationPoints) ? d.articulationPoints : [];
|
|
3128
|
+
const count = outliers.length + artPoints.length;
|
|
3129
|
+
if (count === 0) {
|
|
3130
|
+
return "Found 0 anomalies.";
|
|
3131
|
+
}
|
|
3132
|
+
const topItems = [
|
|
3133
|
+
...outliers.slice(0, 2).map((o) => o.nodeId ?? "unknown outlier"),
|
|
3134
|
+
...artPoints.slice(0, 1).map((a) => a.nodeId ?? "unknown bottleneck")
|
|
3135
|
+
].join(", ");
|
|
3136
|
+
return `Found ${this.p(count, "anomaly", "anomalies")}: ${topItems}.`;
|
|
3137
|
+
}
|
|
3138
|
+
safeArrayLength(value) {
|
|
3139
|
+
return Array.isArray(value) ? value.length : 0;
|
|
3140
|
+
}
|
|
3141
|
+
p(count, singular, plural) {
|
|
3142
|
+
const word = count === 1 ? singular : plural ?? singular + "s";
|
|
3143
|
+
return `${count} ${word}`;
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
3146
|
+
|
|
3147
|
+
// src/nlq/index.ts
|
|
3148
|
+
var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships", "explain"]);
|
|
3149
|
+
var classifier = new IntentClassifier();
|
|
3150
|
+
var extractor = new EntityExtractor();
|
|
3151
|
+
var formatter = new ResponseFormatter();
|
|
3152
|
+
async function askGraph(store, question) {
|
|
3153
|
+
const fusion = new FusionLayer(store);
|
|
3154
|
+
const resolver = new EntityResolver(store, fusion);
|
|
3155
|
+
const classification = classifier.classify(question);
|
|
3156
|
+
if (classification.confidence < 0.3) {
|
|
3157
|
+
return {
|
|
3158
|
+
intent: classification.intent,
|
|
3159
|
+
intentConfidence: classification.confidence,
|
|
3160
|
+
entities: [],
|
|
3161
|
+
summary: "I'm not sure what you're asking. Try rephrasing your question.",
|
|
3162
|
+
data: null,
|
|
3163
|
+
suggestions: [
|
|
3164
|
+
'Try "what breaks if I change <name>?" for impact analysis',
|
|
3165
|
+
'Try "where is <name>?" to find entities',
|
|
3166
|
+
'Try "what calls <name>?" for relationships',
|
|
3167
|
+
'Try "what is <name>?" for explanations',
|
|
3168
|
+
'Try "what looks wrong?" for anomaly detection'
|
|
3169
|
+
]
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
const rawEntities = extractor.extract(question);
|
|
3173
|
+
const entities = resolver.resolve(rawEntities);
|
|
3174
|
+
if (ENTITY_REQUIRED_INTENTS.has(classification.intent) && entities.length === 0) {
|
|
3175
|
+
return {
|
|
3176
|
+
intent: classification.intent,
|
|
3177
|
+
intentConfidence: classification.confidence,
|
|
3178
|
+
entities: [],
|
|
3179
|
+
summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
|
|
3180
|
+
data: null
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
let data;
|
|
3184
|
+
try {
|
|
3185
|
+
data = executeOperation(store, classification.intent, entities, question, fusion);
|
|
3186
|
+
} catch (err) {
|
|
3187
|
+
return {
|
|
3188
|
+
intent: classification.intent,
|
|
3189
|
+
intentConfidence: classification.confidence,
|
|
3190
|
+
entities,
|
|
3191
|
+
summary: `An error occurred while querying the graph: ${err instanceof Error ? err.message : String(err)}`,
|
|
3192
|
+
data: null
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
const summary = formatter.format(classification.intent, entities, data, question);
|
|
3196
|
+
return {
|
|
3197
|
+
intent: classification.intent,
|
|
3198
|
+
intentConfidence: classification.confidence,
|
|
3199
|
+
entities,
|
|
3200
|
+
summary,
|
|
3201
|
+
data
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
function executeOperation(store, intent, entities, question, fusion) {
|
|
3205
|
+
const cql = new ContextQL(store);
|
|
3206
|
+
switch (intent) {
|
|
3207
|
+
case "impact": {
|
|
3208
|
+
const rootId = entities[0].nodeId;
|
|
3209
|
+
const result = cql.execute({
|
|
3210
|
+
rootNodeIds: [rootId],
|
|
3211
|
+
bidirectional: true,
|
|
3212
|
+
maxDepth: 3
|
|
3213
|
+
});
|
|
3214
|
+
return groupNodesByImpact(result.nodes, rootId);
|
|
3215
|
+
}
|
|
3216
|
+
case "find": {
|
|
3217
|
+
return fusion.search(question, 10);
|
|
3218
|
+
}
|
|
3219
|
+
case "relationships": {
|
|
3220
|
+
const rootId = entities[0].nodeId;
|
|
3221
|
+
const result = cql.execute({
|
|
3222
|
+
rootNodeIds: [rootId],
|
|
3223
|
+
bidirectional: true,
|
|
3224
|
+
maxDepth: 1
|
|
3225
|
+
});
|
|
3226
|
+
return { nodes: result.nodes, edges: result.edges };
|
|
3227
|
+
}
|
|
3228
|
+
case "explain": {
|
|
3229
|
+
const searchResults = fusion.search(question, 10);
|
|
3230
|
+
const contextBlocks = [];
|
|
3231
|
+
const rootIds = entities.length > 0 ? [entities[0].nodeId] : searchResults.slice(0, 3).map((r) => r.nodeId);
|
|
3232
|
+
for (const rootId of rootIds) {
|
|
3233
|
+
const expanded = cql.execute({
|
|
3234
|
+
rootNodeIds: [rootId],
|
|
3235
|
+
maxDepth: 2
|
|
3236
|
+
});
|
|
3237
|
+
const matchingResult = searchResults.find((r) => r.nodeId === rootId);
|
|
3238
|
+
contextBlocks.push({
|
|
3239
|
+
rootNode: rootId,
|
|
3240
|
+
score: matchingResult?.score ?? 1,
|
|
3241
|
+
nodes: expanded.nodes,
|
|
3242
|
+
edges: expanded.edges
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
return { searchResults, context: contextBlocks };
|
|
3246
|
+
}
|
|
3247
|
+
case "anomaly": {
|
|
3248
|
+
const adapter = new GraphAnomalyAdapter(store);
|
|
3249
|
+
return adapter.detect();
|
|
3250
|
+
}
|
|
3251
|
+
default:
|
|
3252
|
+
return null;
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
|
|
2446
3256
|
// src/context/Assembler.ts
|
|
2447
3257
|
var PHASE_NODE_TYPES = {
|
|
2448
3258
|
implement: ["file", "function", "class", "method", "interface", "variable"],
|
|
@@ -2486,14 +3296,20 @@ var Assembler = class {
|
|
|
2486
3296
|
const fusion = this.getFusionLayer();
|
|
2487
3297
|
const topResults = fusion.search(intent, 10);
|
|
2488
3298
|
if (topResults.length === 0) {
|
|
2489
|
-
return {
|
|
2490
|
-
nodes: [],
|
|
2491
|
-
edges: [],
|
|
2492
|
-
tokenEstimate: 0,
|
|
2493
|
-
intent,
|
|
2494
|
-
truncated: false
|
|
2495
|
-
};
|
|
3299
|
+
return { nodes: [], edges: [], tokenEstimate: 0, intent, truncated: false };
|
|
2496
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) {
|
|
2497
3313
|
const contextQL = new ContextQL(this.store);
|
|
2498
3314
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
2499
3315
|
const edgeSet = /* @__PURE__ */ new Set();
|
|
@@ -2521,9 +3337,9 @@ var Assembler = class {
|
|
|
2521
3337
|
}
|
|
2522
3338
|
}
|
|
2523
3339
|
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
3340
|
+
return { nodeMap, collectedEdges, nodeScores };
|
|
3341
|
+
}
|
|
3342
|
+
truncateToFit(sortedNodes, tokenBudget) {
|
|
2527
3343
|
let tokenEstimate = 0;
|
|
2528
3344
|
const keptNodes = [];
|
|
2529
3345
|
let truncated = false;
|
|
@@ -2536,17 +3352,7 @@ var Assembler = class {
|
|
|
2536
3352
|
tokenEstimate += nodeTokens;
|
|
2537
3353
|
keptNodes.push(node);
|
|
2538
3354
|
}
|
|
2539
|
-
|
|
2540
|
-
const keptEdges = collectedEdges.filter(
|
|
2541
|
-
(e) => keptNodeIds.has(e.from) && keptNodeIds.has(e.to)
|
|
2542
|
-
);
|
|
2543
|
-
return {
|
|
2544
|
-
nodes: keptNodes,
|
|
2545
|
-
edges: keptEdges,
|
|
2546
|
-
tokenEstimate,
|
|
2547
|
-
intent,
|
|
2548
|
-
truncated
|
|
2549
|
-
};
|
|
3355
|
+
return { keptNodes, tokenEstimate, truncated };
|
|
2550
3356
|
}
|
|
2551
3357
|
/**
|
|
2552
3358
|
* Compute a token budget allocation across node types.
|
|
@@ -2719,8 +3525,8 @@ var GraphConstraintAdapter = class {
|
|
|
2719
3525
|
const { edges } = this.computeDependencyGraph();
|
|
2720
3526
|
const violations = [];
|
|
2721
3527
|
for (const edge of edges) {
|
|
2722
|
-
const fromRelative = relative2(rootDir, edge.from);
|
|
2723
|
-
const toRelative = relative2(rootDir, edge.to);
|
|
3528
|
+
const fromRelative = relative2(rootDir, edge.from).replaceAll("\\", "/");
|
|
3529
|
+
const toRelative = relative2(rootDir, edge.to).replaceAll("\\", "/");
|
|
2724
3530
|
const fromLayer = this.resolveLayer(fromRelative, layers);
|
|
2725
3531
|
const toLayer = this.resolveLayer(toRelative, layers);
|
|
2726
3532
|
if (!fromLayer || !toLayer) continue;
|
|
@@ -3065,6 +3871,447 @@ var GraphFeedbackAdapter = class {
|
|
|
3065
3871
|
}
|
|
3066
3872
|
};
|
|
3067
3873
|
|
|
3874
|
+
// src/independence/TaskIndependenceAnalyzer.ts
|
|
3875
|
+
var DEFAULT_EDGE_TYPES = ["imports", "calls", "references"];
|
|
3876
|
+
var TaskIndependenceAnalyzer = class {
|
|
3877
|
+
store;
|
|
3878
|
+
constructor(store) {
|
|
3879
|
+
this.store = store;
|
|
3880
|
+
}
|
|
3881
|
+
analyze(params) {
|
|
3882
|
+
const { tasks } = params;
|
|
3883
|
+
const depth = params.depth ?? 1;
|
|
3884
|
+
const edgeTypes = params.edgeTypes ?? DEFAULT_EDGE_TYPES;
|
|
3885
|
+
this.validate(tasks);
|
|
3886
|
+
const useGraph = this.store != null && depth > 0;
|
|
3887
|
+
const analysisLevel = useGraph ? "graph-expanded" : "file-only";
|
|
3888
|
+
const originalFiles = /* @__PURE__ */ new Map();
|
|
3889
|
+
const expandedFiles = /* @__PURE__ */ new Map();
|
|
3890
|
+
for (const task of tasks) {
|
|
3891
|
+
const origSet = new Set(task.files);
|
|
3892
|
+
originalFiles.set(task.id, origSet);
|
|
3893
|
+
if (useGraph) {
|
|
3894
|
+
const expanded = this.expandViaGraph(task.files, depth, edgeTypes);
|
|
3895
|
+
expandedFiles.set(task.id, expanded);
|
|
3896
|
+
} else {
|
|
3897
|
+
expandedFiles.set(task.id, /* @__PURE__ */ new Map());
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
const taskIds = tasks.map((t) => t.id);
|
|
3901
|
+
const pairs = [];
|
|
3902
|
+
for (let i = 0; i < taskIds.length; i++) {
|
|
3903
|
+
for (let j = i + 1; j < taskIds.length; j++) {
|
|
3904
|
+
const idA = taskIds[i];
|
|
3905
|
+
const idB = taskIds[j];
|
|
3906
|
+
const pair = this.computePairOverlap(
|
|
3907
|
+
idA,
|
|
3908
|
+
idB,
|
|
3909
|
+
originalFiles.get(idA),
|
|
3910
|
+
originalFiles.get(idB),
|
|
3911
|
+
expandedFiles.get(idA),
|
|
3912
|
+
expandedFiles.get(idB)
|
|
3913
|
+
);
|
|
3914
|
+
pairs.push(pair);
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
const groups = this.buildGroups(taskIds, pairs);
|
|
3918
|
+
const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
|
|
3919
|
+
return {
|
|
3920
|
+
tasks: taskIds,
|
|
3921
|
+
analysisLevel,
|
|
3922
|
+
depth,
|
|
3923
|
+
pairs,
|
|
3924
|
+
groups,
|
|
3925
|
+
verdict
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3928
|
+
// --- Private methods ---
|
|
3929
|
+
validate(tasks) {
|
|
3930
|
+
if (tasks.length < 2) {
|
|
3931
|
+
throw new Error("At least 2 tasks are required for independence analysis");
|
|
3932
|
+
}
|
|
3933
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
3934
|
+
for (const task of tasks) {
|
|
3935
|
+
if (seenIds.has(task.id)) {
|
|
3936
|
+
throw new Error(`Duplicate task ID: "${task.id}"`);
|
|
3937
|
+
}
|
|
3938
|
+
seenIds.add(task.id);
|
|
3939
|
+
if (task.files.length === 0) {
|
|
3940
|
+
throw new Error(`Task "${task.id}" has an empty files array`);
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
expandViaGraph(files, depth, edgeTypes) {
|
|
3945
|
+
const result = /* @__PURE__ */ new Map();
|
|
3946
|
+
const store = this.store;
|
|
3947
|
+
const cql = new ContextQL(store);
|
|
3948
|
+
const fileSet = new Set(files);
|
|
3949
|
+
for (const file of files) {
|
|
3950
|
+
const nodeId = `file:${file}`;
|
|
3951
|
+
const node = store.getNode(nodeId);
|
|
3952
|
+
if (!node) continue;
|
|
3953
|
+
const queryResult = cql.execute({
|
|
3954
|
+
rootNodeIds: [nodeId],
|
|
3955
|
+
maxDepth: depth,
|
|
3956
|
+
includeEdges: edgeTypes,
|
|
3957
|
+
includeTypes: ["file"]
|
|
3958
|
+
});
|
|
3959
|
+
for (const n of queryResult.nodes) {
|
|
3960
|
+
const path6 = n.path ?? n.id.replace(/^file:/, "");
|
|
3961
|
+
if (!fileSet.has(path6)) {
|
|
3962
|
+
if (!result.has(path6)) {
|
|
3963
|
+
result.set(path6, file);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
return result;
|
|
3969
|
+
}
|
|
3970
|
+
computePairOverlap(idA, idB, origA, origB, expandedA, expandedB) {
|
|
3971
|
+
const overlaps = [];
|
|
3972
|
+
for (const file of origA) {
|
|
3973
|
+
if (origB.has(file)) {
|
|
3974
|
+
overlaps.push({ file, type: "direct" });
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
const directFiles = new Set(overlaps.map((o) => o.file));
|
|
3978
|
+
const transitiveFiles = /* @__PURE__ */ new Set();
|
|
3979
|
+
for (const [file, via] of expandedA) {
|
|
3980
|
+
if (origB.has(file) && !directFiles.has(file) && !transitiveFiles.has(file)) {
|
|
3981
|
+
transitiveFiles.add(file);
|
|
3982
|
+
overlaps.push({ file, type: "transitive", via });
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
for (const [file, via] of expandedB) {
|
|
3986
|
+
if (origA.has(file) && !directFiles.has(file) && !transitiveFiles.has(file)) {
|
|
3987
|
+
transitiveFiles.add(file);
|
|
3988
|
+
overlaps.push({ file, type: "transitive", via });
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
for (const [file, viaA] of expandedA) {
|
|
3992
|
+
if (expandedB.has(file) && !directFiles.has(file) && !transitiveFiles.has(file)) {
|
|
3993
|
+
transitiveFiles.add(file);
|
|
3994
|
+
overlaps.push({ file, type: "transitive", via: viaA });
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
return {
|
|
3998
|
+
taskA: idA,
|
|
3999
|
+
taskB: idB,
|
|
4000
|
+
independent: overlaps.length === 0,
|
|
4001
|
+
overlaps
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
buildGroups(taskIds, pairs) {
|
|
4005
|
+
const parent = /* @__PURE__ */ new Map();
|
|
4006
|
+
const rank = /* @__PURE__ */ new Map();
|
|
4007
|
+
for (const id of taskIds) {
|
|
4008
|
+
parent.set(id, id);
|
|
4009
|
+
rank.set(id, 0);
|
|
4010
|
+
}
|
|
4011
|
+
const find = (x) => {
|
|
4012
|
+
let root = x;
|
|
4013
|
+
while (parent.get(root) !== root) {
|
|
4014
|
+
root = parent.get(root);
|
|
4015
|
+
}
|
|
4016
|
+
let current = x;
|
|
4017
|
+
while (current !== root) {
|
|
4018
|
+
const next = parent.get(current);
|
|
4019
|
+
parent.set(current, root);
|
|
4020
|
+
current = next;
|
|
4021
|
+
}
|
|
4022
|
+
return root;
|
|
4023
|
+
};
|
|
4024
|
+
const union = (a, b) => {
|
|
4025
|
+
const rootA = find(a);
|
|
4026
|
+
const rootB = find(b);
|
|
4027
|
+
if (rootA === rootB) return;
|
|
4028
|
+
const rankA = rank.get(rootA);
|
|
4029
|
+
const rankB = rank.get(rootB);
|
|
4030
|
+
if (rankA < rankB) {
|
|
4031
|
+
parent.set(rootA, rootB);
|
|
4032
|
+
} else if (rankA > rankB) {
|
|
4033
|
+
parent.set(rootB, rootA);
|
|
4034
|
+
} else {
|
|
4035
|
+
parent.set(rootB, rootA);
|
|
4036
|
+
rank.set(rootA, rankA + 1);
|
|
4037
|
+
}
|
|
4038
|
+
};
|
|
4039
|
+
for (const pair of pairs) {
|
|
4040
|
+
if (!pair.independent) {
|
|
4041
|
+
union(pair.taskA, pair.taskB);
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
4045
|
+
for (const id of taskIds) {
|
|
4046
|
+
const root = find(id);
|
|
4047
|
+
if (!groupMap.has(root)) {
|
|
4048
|
+
groupMap.set(root, []);
|
|
4049
|
+
}
|
|
4050
|
+
groupMap.get(root).push(id);
|
|
4051
|
+
}
|
|
4052
|
+
return Array.from(groupMap.values());
|
|
4053
|
+
}
|
|
4054
|
+
generateVerdict(taskIds, groups, analysisLevel) {
|
|
4055
|
+
const total = taskIds.length;
|
|
4056
|
+
const groupCount = groups.length;
|
|
4057
|
+
let verdict;
|
|
4058
|
+
if (groupCount === 1) {
|
|
4059
|
+
verdict = `All ${total} tasks conflict \u2014 must run serially.`;
|
|
4060
|
+
} else if (groupCount === total) {
|
|
4061
|
+
verdict = `All ${total} tasks are independent \u2014 can all run in parallel.`;
|
|
4062
|
+
} else {
|
|
4063
|
+
verdict = `${total} tasks form ${groupCount} independent groups \u2014 ${groupCount} parallel waves possible.`;
|
|
4064
|
+
}
|
|
4065
|
+
if (analysisLevel === "file-only") {
|
|
4066
|
+
verdict += " Graph unavailable \u2014 transitive dependencies not checked.";
|
|
4067
|
+
}
|
|
4068
|
+
return verdict;
|
|
4069
|
+
}
|
|
4070
|
+
};
|
|
4071
|
+
|
|
4072
|
+
// src/independence/ConflictPredictor.ts
|
|
4073
|
+
var ConflictPredictor = class {
|
|
4074
|
+
store;
|
|
4075
|
+
constructor(store) {
|
|
4076
|
+
this.store = store;
|
|
4077
|
+
}
|
|
4078
|
+
predict(params) {
|
|
4079
|
+
const analyzer = new TaskIndependenceAnalyzer(this.store);
|
|
4080
|
+
const result = analyzer.analyze(params);
|
|
4081
|
+
const churnMap = /* @__PURE__ */ new Map();
|
|
4082
|
+
const couplingMap = /* @__PURE__ */ new Map();
|
|
4083
|
+
let churnThreshold = Infinity;
|
|
4084
|
+
let couplingThreshold = Infinity;
|
|
4085
|
+
if (this.store != null) {
|
|
4086
|
+
const complexityResult = new GraphComplexityAdapter(this.store).computeComplexityHotspots();
|
|
4087
|
+
for (const hotspot of complexityResult.hotspots) {
|
|
4088
|
+
const existing = churnMap.get(hotspot.file);
|
|
4089
|
+
if (existing === void 0 || hotspot.changeFrequency > existing) {
|
|
4090
|
+
churnMap.set(hotspot.file, hotspot.changeFrequency);
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
|
|
4094
|
+
for (const fileData of couplingResult.files) {
|
|
4095
|
+
couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
|
|
4096
|
+
}
|
|
4097
|
+
churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
|
|
4098
|
+
couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
|
|
4099
|
+
}
|
|
4100
|
+
const conflicts = [];
|
|
4101
|
+
for (const pair of result.pairs) {
|
|
4102
|
+
if (pair.independent) continue;
|
|
4103
|
+
const { severity, reason, mitigation } = this.classifyPair(
|
|
4104
|
+
pair.taskA,
|
|
4105
|
+
pair.taskB,
|
|
4106
|
+
pair.overlaps,
|
|
4107
|
+
churnMap,
|
|
4108
|
+
couplingMap,
|
|
4109
|
+
churnThreshold,
|
|
4110
|
+
couplingThreshold
|
|
4111
|
+
);
|
|
4112
|
+
conflicts.push({
|
|
4113
|
+
taskA: pair.taskA,
|
|
4114
|
+
taskB: pair.taskB,
|
|
4115
|
+
severity,
|
|
4116
|
+
reason,
|
|
4117
|
+
mitigation,
|
|
4118
|
+
overlaps: pair.overlaps
|
|
4119
|
+
});
|
|
4120
|
+
}
|
|
4121
|
+
const taskIds = result.tasks;
|
|
4122
|
+
const groups = this.buildHighSeverityGroups(taskIds, conflicts);
|
|
4123
|
+
const regrouped = !this.groupsEqual(result.groups, groups);
|
|
4124
|
+
let highCount = 0;
|
|
4125
|
+
let mediumCount = 0;
|
|
4126
|
+
let lowCount = 0;
|
|
4127
|
+
for (const c of conflicts) {
|
|
4128
|
+
if (c.severity === "high") highCount++;
|
|
4129
|
+
else if (c.severity === "medium") mediumCount++;
|
|
4130
|
+
else lowCount++;
|
|
4131
|
+
}
|
|
4132
|
+
const verdict = this.generateVerdict(
|
|
4133
|
+
taskIds,
|
|
4134
|
+
groups,
|
|
4135
|
+
result.analysisLevel,
|
|
4136
|
+
highCount,
|
|
4137
|
+
mediumCount,
|
|
4138
|
+
lowCount,
|
|
4139
|
+
regrouped
|
|
4140
|
+
);
|
|
4141
|
+
return {
|
|
4142
|
+
tasks: taskIds,
|
|
4143
|
+
analysisLevel: result.analysisLevel,
|
|
4144
|
+
depth: result.depth,
|
|
4145
|
+
conflicts,
|
|
4146
|
+
groups,
|
|
4147
|
+
summary: {
|
|
4148
|
+
high: highCount,
|
|
4149
|
+
medium: mediumCount,
|
|
4150
|
+
low: lowCount,
|
|
4151
|
+
regrouped
|
|
4152
|
+
},
|
|
4153
|
+
verdict
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
// --- Private helpers ---
|
|
4157
|
+
classifyPair(taskA, taskB, overlaps, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
4158
|
+
let maxSeverity = "low";
|
|
4159
|
+
let primaryReason = "";
|
|
4160
|
+
let primaryMitigation = "";
|
|
4161
|
+
for (const overlap of overlaps) {
|
|
4162
|
+
let overlapSeverity;
|
|
4163
|
+
let reason;
|
|
4164
|
+
let mitigation;
|
|
4165
|
+
if (overlap.type === "direct") {
|
|
4166
|
+
overlapSeverity = "high";
|
|
4167
|
+
reason = `Both tasks write to ${overlap.file}`;
|
|
4168
|
+
mitigation = `Serialize: run ${taskA} before ${taskB}`;
|
|
4169
|
+
} else {
|
|
4170
|
+
const churn = churnMap.get(overlap.file);
|
|
4171
|
+
const coupling = couplingMap.get(overlap.file);
|
|
4172
|
+
const via = overlap.via ?? "unknown";
|
|
4173
|
+
if (churn !== void 0 && churn >= churnThreshold && churnThreshold !== Infinity) {
|
|
4174
|
+
overlapSeverity = "medium";
|
|
4175
|
+
reason = `Transitive overlap on high-churn file ${overlap.file} (via ${via})`;
|
|
4176
|
+
mitigation = `Review: ${overlap.file} changes frequently \u2014 coordinate edits between ${taskA} and ${taskB}`;
|
|
4177
|
+
} else if (coupling !== void 0 && coupling >= couplingThreshold && couplingThreshold !== Infinity) {
|
|
4178
|
+
overlapSeverity = "medium";
|
|
4179
|
+
reason = `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`;
|
|
4180
|
+
mitigation = `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`;
|
|
4181
|
+
} else {
|
|
4182
|
+
overlapSeverity = "low";
|
|
4183
|
+
reason = `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`;
|
|
4184
|
+
mitigation = `Info: transitive overlap unlikely to cause conflicts`;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
if (this.severityRank(overlapSeverity) > this.severityRank(maxSeverity)) {
|
|
4188
|
+
maxSeverity = overlapSeverity;
|
|
4189
|
+
primaryReason = reason;
|
|
4190
|
+
primaryMitigation = mitigation;
|
|
4191
|
+
} else if (primaryReason === "") {
|
|
4192
|
+
primaryReason = reason;
|
|
4193
|
+
primaryMitigation = mitigation;
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
return { severity: maxSeverity, reason: primaryReason, mitigation: primaryMitigation };
|
|
4197
|
+
}
|
|
4198
|
+
severityRank(severity) {
|
|
4199
|
+
switch (severity) {
|
|
4200
|
+
case "high":
|
|
4201
|
+
return 3;
|
|
4202
|
+
case "medium":
|
|
4203
|
+
return 2;
|
|
4204
|
+
case "low":
|
|
4205
|
+
return 1;
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
computePercentile(values, percentile) {
|
|
4209
|
+
if (values.length === 0) return Infinity;
|
|
4210
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4211
|
+
const index = Math.ceil(percentile / 100 * sorted.length) - 1;
|
|
4212
|
+
return sorted[Math.min(index, sorted.length - 1)];
|
|
4213
|
+
}
|
|
4214
|
+
buildHighSeverityGroups(taskIds, conflicts) {
|
|
4215
|
+
const parent = /* @__PURE__ */ new Map();
|
|
4216
|
+
const rank = /* @__PURE__ */ new Map();
|
|
4217
|
+
for (const id of taskIds) {
|
|
4218
|
+
parent.set(id, id);
|
|
4219
|
+
rank.set(id, 0);
|
|
4220
|
+
}
|
|
4221
|
+
const find = (x) => {
|
|
4222
|
+
let root = x;
|
|
4223
|
+
while (parent.get(root) !== root) {
|
|
4224
|
+
root = parent.get(root);
|
|
4225
|
+
}
|
|
4226
|
+
let current = x;
|
|
4227
|
+
while (current !== root) {
|
|
4228
|
+
const next = parent.get(current);
|
|
4229
|
+
parent.set(current, root);
|
|
4230
|
+
current = next;
|
|
4231
|
+
}
|
|
4232
|
+
return root;
|
|
4233
|
+
};
|
|
4234
|
+
const union = (a, b) => {
|
|
4235
|
+
const rootA = find(a);
|
|
4236
|
+
const rootB = find(b);
|
|
4237
|
+
if (rootA === rootB) return;
|
|
4238
|
+
const rankA = rank.get(rootA);
|
|
4239
|
+
const rankB = rank.get(rootB);
|
|
4240
|
+
if (rankA < rankB) {
|
|
4241
|
+
parent.set(rootA, rootB);
|
|
4242
|
+
} else if (rankA > rankB) {
|
|
4243
|
+
parent.set(rootB, rootA);
|
|
4244
|
+
} else {
|
|
4245
|
+
parent.set(rootB, rootA);
|
|
4246
|
+
rank.set(rootA, rankA + 1);
|
|
4247
|
+
}
|
|
4248
|
+
};
|
|
4249
|
+
for (const conflict of conflicts) {
|
|
4250
|
+
if (conflict.severity === "high") {
|
|
4251
|
+
union(conflict.taskA, conflict.taskB);
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
4255
|
+
for (const id of taskIds) {
|
|
4256
|
+
const root = find(id);
|
|
4257
|
+
let group = groupMap.get(root);
|
|
4258
|
+
if (group === void 0) {
|
|
4259
|
+
group = [];
|
|
4260
|
+
groupMap.set(root, group);
|
|
4261
|
+
}
|
|
4262
|
+
group.push(id);
|
|
4263
|
+
}
|
|
4264
|
+
return Array.from(groupMap.values());
|
|
4265
|
+
}
|
|
4266
|
+
groupsEqual(a, b) {
|
|
4267
|
+
if (a.length !== b.length) return false;
|
|
4268
|
+
const normalize2 = (groups) => groups.map((g) => [...g].sort()).sort((x, y) => {
|
|
4269
|
+
const xFirst = x[0];
|
|
4270
|
+
const yFirst = y[0];
|
|
4271
|
+
return xFirst.localeCompare(yFirst);
|
|
4272
|
+
});
|
|
4273
|
+
const normA = normalize2(a);
|
|
4274
|
+
const normB = normalize2(b);
|
|
4275
|
+
for (let i = 0; i < normA.length; i++) {
|
|
4276
|
+
const groupA = normA[i];
|
|
4277
|
+
const groupB = normB[i];
|
|
4278
|
+
if (groupA.length !== groupB.length) return false;
|
|
4279
|
+
for (let j = 0; j < groupA.length; j++) {
|
|
4280
|
+
if (groupA[j] !== groupB[j]) return false;
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
return true;
|
|
4284
|
+
}
|
|
4285
|
+
generateVerdict(taskIds, groups, analysisLevel, highCount, mediumCount, lowCount, regrouped) {
|
|
4286
|
+
const total = taskIds.length;
|
|
4287
|
+
const groupCount = groups.length;
|
|
4288
|
+
const parts = [];
|
|
4289
|
+
const conflictParts = [];
|
|
4290
|
+
if (highCount > 0) conflictParts.push(`${highCount} high`);
|
|
4291
|
+
if (mediumCount > 0) conflictParts.push(`${mediumCount} medium`);
|
|
4292
|
+
if (lowCount > 0) conflictParts.push(`${lowCount} low`);
|
|
4293
|
+
if (conflictParts.length === 0) {
|
|
4294
|
+
parts.push(`${total} tasks have no conflicts \u2014 can all run in parallel.`);
|
|
4295
|
+
} else {
|
|
4296
|
+
parts.push(`${total} tasks have ${conflictParts.join(", ")} severity conflicts.`);
|
|
4297
|
+
}
|
|
4298
|
+
if (groupCount === 1) {
|
|
4299
|
+
parts.push(`All tasks must run serially.`);
|
|
4300
|
+
} else if (groupCount === total) {
|
|
4301
|
+
parts.push(`${groupCount} parallel groups (all independent).`);
|
|
4302
|
+
} else {
|
|
4303
|
+
parts.push(`${groupCount} parallel groups possible.`);
|
|
4304
|
+
}
|
|
4305
|
+
if (regrouped) {
|
|
4306
|
+
parts.push(`Tasks were regrouped due to high-severity conflicts.`);
|
|
4307
|
+
}
|
|
4308
|
+
if (analysisLevel === "file-only") {
|
|
4309
|
+
parts.push(`Graph unavailable \u2014 severity based on file overlaps only.`);
|
|
4310
|
+
}
|
|
4311
|
+
return parts.join(" ");
|
|
4312
|
+
}
|
|
4313
|
+
};
|
|
4314
|
+
|
|
3068
4315
|
// src/index.ts
|
|
3069
4316
|
var VERSION = "0.2.0";
|
|
3070
4317
|
export {
|
|
@@ -3072,11 +4319,14 @@ export {
|
|
|
3072
4319
|
CIConnector,
|
|
3073
4320
|
CURRENT_SCHEMA_VERSION,
|
|
3074
4321
|
CodeIngestor,
|
|
4322
|
+
ConflictPredictor,
|
|
3075
4323
|
ConfluenceConnector,
|
|
3076
4324
|
ContextQL,
|
|
3077
4325
|
DesignConstraintAdapter,
|
|
3078
4326
|
DesignIngestor,
|
|
3079
4327
|
EDGE_TYPES,
|
|
4328
|
+
EntityExtractor,
|
|
4329
|
+
EntityResolver,
|
|
3080
4330
|
FusionLayer,
|
|
3081
4331
|
GitIngestor,
|
|
3082
4332
|
GraphAnomalyAdapter,
|
|
@@ -3088,15 +4338,21 @@ export {
|
|
|
3088
4338
|
GraphFeedbackAdapter,
|
|
3089
4339
|
GraphNodeSchema,
|
|
3090
4340
|
GraphStore,
|
|
4341
|
+
INTENTS,
|
|
4342
|
+
IntentClassifier,
|
|
3091
4343
|
JiraConnector,
|
|
3092
4344
|
KnowledgeIngestor,
|
|
3093
4345
|
NODE_TYPES,
|
|
3094
4346
|
OBSERVABILITY_TYPES,
|
|
4347
|
+
ResponseFormatter,
|
|
3095
4348
|
SlackConnector,
|
|
3096
4349
|
SyncManager,
|
|
4350
|
+
TaskIndependenceAnalyzer,
|
|
3097
4351
|
TopologicalLinker,
|
|
3098
4352
|
VERSION,
|
|
3099
4353
|
VectorStore,
|
|
4354
|
+
askGraph,
|
|
4355
|
+
groupNodesByImpact,
|
|
3100
4356
|
linkToCode,
|
|
3101
4357
|
loadGraph,
|
|
3102
4358
|
project,
|