@harness-engineering/graph 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -33,7 +33,9 @@ __export(index_exports, {
33
33
  Assembler: () => Assembler,
34
34
  CIConnector: () => CIConnector,
35
35
  CURRENT_SCHEMA_VERSION: () => CURRENT_SCHEMA_VERSION,
36
+ CascadeSimulator: () => CascadeSimulator,
36
37
  CodeIngestor: () => CodeIngestor,
38
+ CompositeProbabilityStrategy: () => CompositeProbabilityStrategy,
37
39
  ConflictPredictor: () => ConflictPredictor,
38
40
  ConfluenceConnector: () => ConfluenceConnector,
39
41
  ContextQL: () => ContextQL,
@@ -68,6 +70,7 @@ __export(index_exports, {
68
70
  VERSION: () => VERSION,
69
71
  VectorStore: () => VectorStore,
70
72
  askGraph: () => askGraph,
73
+ classifyNodeCategory: () => classifyNodeCategory,
71
74
  groupNodesByImpact: () => groupNodesByImpact,
72
75
  linkToCode: () => linkToCode,
73
76
  loadGraph: () => loadGraph,
@@ -241,6 +244,16 @@ function removeFromIndex(index, key, edge) {
241
244
  if (idx !== -1) list.splice(idx, 1);
242
245
  if (list.length === 0) index.delete(key);
243
246
  }
247
+ function filterEdges(candidates, query) {
248
+ const results = [];
249
+ for (const edge of candidates) {
250
+ if (query.from !== void 0 && edge.from !== query.from) continue;
251
+ if (query.to !== void 0 && edge.to !== query.to) continue;
252
+ if (query.type !== void 0 && edge.type !== query.type) continue;
253
+ results.push({ ...edge });
254
+ }
255
+ return results;
256
+ }
244
257
  var GraphStore = class {
245
258
  nodeMap = /* @__PURE__ */ new Map();
246
259
  edgeMap = /* @__PURE__ */ new Map();
@@ -308,44 +321,47 @@ var GraphStore = class {
308
321
  }
309
322
  }
310
323
  getEdges(query) {
311
- let candidates;
312
324
  if (query.from !== void 0 && query.to !== void 0 && query.type !== void 0) {
313
325
  const edge = this.edgeMap.get(edgeKey(query.from, query.to, query.type));
314
326
  return edge ? [{ ...edge }] : [];
315
- } else if (query.from !== void 0) {
316
- candidates = this.edgesByFrom.get(query.from) ?? [];
317
- } else if (query.to !== void 0) {
318
- candidates = this.edgesByTo.get(query.to) ?? [];
319
- } else if (query.type !== void 0) {
320
- candidates = this.edgesByType.get(query.type) ?? [];
321
- } else {
322
- candidates = this.edgeMap.values();
323
327
  }
324
- const results = [];
325
- for (const edge of candidates) {
326
- if (query.from !== void 0 && edge.from !== query.from) continue;
327
- if (query.to !== void 0 && edge.to !== query.to) continue;
328
- if (query.type !== void 0 && edge.type !== query.type) continue;
329
- results.push({ ...edge });
328
+ const candidates = this.selectCandidates(query);
329
+ return filterEdges(candidates, query);
330
+ }
331
+ /** Pick the most selective index to start from. */
332
+ selectCandidates(query) {
333
+ if (query.from !== void 0) {
334
+ return this.edgesByFrom.get(query.from) ?? [];
330
335
  }
331
- return results;
336
+ if (query.to !== void 0) {
337
+ return this.edgesByTo.get(query.to) ?? [];
338
+ }
339
+ if (query.type !== void 0) {
340
+ return this.edgesByType.get(query.type) ?? [];
341
+ }
342
+ return this.edgeMap.values();
332
343
  }
333
344
  getNeighbors(nodeId, direction = "both") {
334
- const neighborIds = /* @__PURE__ */ new Set();
345
+ const neighborIds = this.collectNeighborIds(nodeId, direction);
346
+ return this.resolveNodes(neighborIds);
347
+ }
348
+ collectNeighborIds(nodeId, direction) {
349
+ const ids = /* @__PURE__ */ new Set();
335
350
  if (direction === "outbound" || direction === "both") {
336
- const outEdges = this.edgesByFrom.get(nodeId) ?? [];
337
- for (const edge of outEdges) {
338
- neighborIds.add(edge.to);
351
+ for (const edge of this.edgesByFrom.get(nodeId) ?? []) {
352
+ ids.add(edge.to);
339
353
  }
340
354
  }
341
355
  if (direction === "inbound" || direction === "both") {
342
- const inEdges = this.edgesByTo.get(nodeId) ?? [];
343
- for (const edge of inEdges) {
344
- neighborIds.add(edge.from);
356
+ for (const edge of this.edgesByTo.get(nodeId) ?? []) {
357
+ ids.add(edge.from);
345
358
  }
346
359
  }
360
+ return ids;
361
+ }
362
+ resolveNodes(ids) {
347
363
  const results = [];
348
- for (const nid of neighborIds) {
364
+ for (const nid of ids) {
349
365
  const node = this.getNode(nid);
350
366
  if (node) results.push(node);
351
367
  }
@@ -624,6 +640,12 @@ var CODE_TYPES = /* @__PURE__ */ new Set([
624
640
  "method",
625
641
  "variable"
626
642
  ]);
643
+ function classifyNodeCategory(node) {
644
+ if (TEST_TYPES.has(node.type)) return "tests";
645
+ if (DOC_TYPES.has(node.type)) return "docs";
646
+ if (CODE_TYPES.has(node.type)) return "code";
647
+ return "other";
648
+ }
627
649
  function groupNodesByImpact(nodes, excludeId) {
628
650
  const tests = [];
629
651
  const docs = [];
@@ -631,15 +653,11 @@ function groupNodesByImpact(nodes, excludeId) {
631
653
  const other = [];
632
654
  for (const node of nodes) {
633
655
  if (excludeId && node.id === excludeId) continue;
634
- if (TEST_TYPES.has(node.type)) {
635
- tests.push(node);
636
- } else if (DOC_TYPES.has(node.type)) {
637
- docs.push(node);
638
- } else if (CODE_TYPES.has(node.type)) {
639
- code.push(node);
640
- } else {
641
- other.push(node);
642
- }
656
+ const category = classifyNodeCategory(node);
657
+ if (category === "tests") tests.push(node);
658
+ else if (category === "docs") docs.push(node);
659
+ else if (category === "code") code.push(node);
660
+ else other.push(node);
643
661
  }
644
662
  return { tests, docs, code, other };
645
663
  }
@@ -1090,6 +1108,17 @@ var CodeIngestor = class {
1090
1108
  var import_node_child_process = require("child_process");
1091
1109
  var import_node_util = require("util");
1092
1110
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
1111
+ function finalizeCommit(current) {
1112
+ return {
1113
+ hash: current.hash,
1114
+ shortHash: current.shortHash,
1115
+ author: current.author,
1116
+ email: current.email,
1117
+ date: current.date,
1118
+ message: current.message,
1119
+ files: current.files
1120
+ };
1121
+ }
1093
1122
  var GitIngestor = class {
1094
1123
  constructor(store, gitRunner) {
1095
1124
  this.store = store;
@@ -1126,39 +1155,49 @@ var GitIngestor = class {
1126
1155
  }
1127
1156
  const commits = this.parseGitLog(output);
1128
1157
  for (const commit of commits) {
1129
- const nodeId = `commit:${commit.shortHash}`;
1130
- this.store.addNode({
1131
- id: nodeId,
1132
- type: "commit",
1133
- name: commit.message,
1134
- metadata: {
1135
- author: commit.author,
1136
- email: commit.email,
1137
- date: commit.date,
1138
- hash: commit.hash
1139
- }
1140
- });
1141
- nodesAdded++;
1142
- for (const file of commit.files) {
1143
- const fileNodeId = `file:${file}`;
1144
- const existingNode = this.store.getNode(fileNodeId);
1145
- if (existingNode) {
1146
- this.store.addEdge({
1147
- from: fileNodeId,
1148
- to: nodeId,
1149
- type: "triggered_by"
1150
- });
1151
- edgesAdded++;
1152
- }
1158
+ const counts = this.ingestCommit(commit);
1159
+ nodesAdded += counts.nodesAdded;
1160
+ edgesAdded += counts.edgesAdded;
1161
+ }
1162
+ edgesAdded += this.ingestCoChanges(commits);
1163
+ return {
1164
+ nodesAdded,
1165
+ nodesUpdated,
1166
+ edgesAdded,
1167
+ edgesUpdated,
1168
+ errors,
1169
+ durationMs: Date.now() - start
1170
+ };
1171
+ }
1172
+ ingestCommit(commit) {
1173
+ const nodeId = `commit:${commit.shortHash}`;
1174
+ this.store.addNode({
1175
+ id: nodeId,
1176
+ type: "commit",
1177
+ name: commit.message,
1178
+ metadata: {
1179
+ author: commit.author,
1180
+ email: commit.email,
1181
+ date: commit.date,
1182
+ hash: commit.hash
1183
+ }
1184
+ });
1185
+ let edgesAdded = 0;
1186
+ for (const file of commit.files) {
1187
+ const fileNodeId = `file:${file}`;
1188
+ if (this.store.getNode(fileNodeId)) {
1189
+ this.store.addEdge({ from: fileNodeId, to: nodeId, type: "triggered_by" });
1190
+ edgesAdded++;
1153
1191
  }
1154
1192
  }
1155
- const coChanges = this.computeCoChanges(commits);
1156
- for (const { fileA, fileB, count } of coChanges) {
1193
+ return { nodesAdded: 1, edgesAdded };
1194
+ }
1195
+ ingestCoChanges(commits) {
1196
+ let edgesAdded = 0;
1197
+ for (const { fileA, fileB, count } of this.computeCoChanges(commits)) {
1157
1198
  const fileAId = `file:${fileA}`;
1158
1199
  const fileBId = `file:${fileB}`;
1159
- const nodeA = this.store.getNode(fileAId);
1160
- const nodeB = this.store.getNode(fileBId);
1161
- if (nodeA && nodeB) {
1200
+ if (this.store.getNode(fileAId) && this.store.getNode(fileBId)) {
1162
1201
  this.store.addEdge({
1163
1202
  from: fileAId,
1164
1203
  to: fileBId,
@@ -1168,14 +1207,7 @@ var GitIngestor = class {
1168
1207
  edgesAdded++;
1169
1208
  }
1170
1209
  }
1171
- return {
1172
- nodesAdded,
1173
- nodesUpdated,
1174
- edgesAdded,
1175
- edgesUpdated,
1176
- errors,
1177
- durationMs: Date.now() - start
1178
- };
1210
+ return edgesAdded;
1179
1211
  }
1180
1212
  async runGit(rootDir, args) {
1181
1213
  if (this.gitRunner) {
@@ -1190,63 +1222,49 @@ var GitIngestor = class {
1190
1222
  const lines = output.split("\n");
1191
1223
  let current = null;
1192
1224
  for (const line of lines) {
1193
- const trimmed = line.trim();
1194
- if (!trimmed) {
1195
- if (current && current.hasFiles) {
1196
- commits.push({
1197
- hash: current.hash,
1198
- shortHash: current.shortHash,
1199
- author: current.author,
1200
- email: current.email,
1201
- date: current.date,
1202
- message: current.message,
1203
- files: current.files
1204
- });
1205
- current = null;
1206
- }
1207
- continue;
1225
+ current = this.processLogLine(line, current, commits);
1226
+ }
1227
+ if (current) {
1228
+ commits.push(finalizeCommit(current));
1229
+ }
1230
+ return commits;
1231
+ }
1232
+ /**
1233
+ * Process one line from git log output, updating the in-progress commit builder
1234
+ * and flushing completed commits into the accumulator.
1235
+ * Returns the updated current builder (null if flushed and not replaced).
1236
+ */
1237
+ processLogLine(line, current, commits) {
1238
+ const trimmed = line.trim();
1239
+ if (!trimmed) {
1240
+ if (current?.hasFiles) {
1241
+ commits.push(finalizeCommit(current));
1242
+ return null;
1208
1243
  }
1209
- const parts = trimmed.split("|");
1210
- if (parts.length >= 5 && /^[0-9a-f]{7,40}$/.test(parts[0])) {
1211
- if (current) {
1212
- commits.push({
1213
- hash: current.hash,
1214
- shortHash: current.shortHash,
1215
- author: current.author,
1216
- email: current.email,
1217
- date: current.date,
1218
- message: current.message,
1219
- files: current.files
1220
- });
1221
- }
1222
- current = {
1223
- hash: parts[0],
1224
- shortHash: parts[0].substring(0, 7),
1225
- author: parts[1],
1226
- email: parts[2],
1227
- date: parts[3],
1228
- message: parts.slice(4).join("|"),
1229
- // message may contain |
1230
- files: [],
1231
- hasFiles: false
1232
- };
1233
- } else if (current) {
1234
- current.files.push(trimmed);
1235
- current.hasFiles = true;
1244
+ return current;
1245
+ }
1246
+ const parts = trimmed.split("|");
1247
+ if (parts.length >= 5 && /^[0-9a-f]{7,40}$/.test(parts[0])) {
1248
+ if (current) {
1249
+ commits.push(finalizeCommit(current));
1236
1250
  }
1251
+ return {
1252
+ hash: parts[0],
1253
+ shortHash: parts[0].substring(0, 7),
1254
+ author: parts[1],
1255
+ email: parts[2],
1256
+ date: parts[3],
1257
+ message: parts.slice(4).join("|"),
1258
+ // message may contain |
1259
+ files: [],
1260
+ hasFiles: false
1261
+ };
1237
1262
  }
1238
1263
  if (current) {
1239
- commits.push({
1240
- hash: current.hash,
1241
- shortHash: current.shortHash,
1242
- author: current.author,
1243
- email: current.email,
1244
- date: current.date,
1245
- message: current.message,
1246
- files: current.files
1247
- });
1264
+ current.files.push(trimmed);
1265
+ current.hasFiles = true;
1248
1266
  }
1249
- return commits;
1267
+ return current;
1250
1268
  }
1251
1269
  computeCoChanges(commits) {
1252
1270
  const pairCounts = /* @__PURE__ */ new Map();
@@ -1390,50 +1408,25 @@ var KnowledgeIngestor = class {
1390
1408
  try {
1391
1409
  const content = await fs2.readFile(filePath, "utf-8");
1392
1410
  const filename = path3.basename(filePath, ".md");
1393
- const titleMatch = content.match(/^#\s+(.+)$/m);
1394
- const title = titleMatch ? titleMatch[1].trim() : filename;
1395
- const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
1396
- const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
1397
- const date = dateMatch ? dateMatch[1].trim() : void 0;
1398
- const status = statusMatch ? statusMatch[1].trim() : void 0;
1399
1411
  const nodeId = `adr:${filename}`;
1400
- this.store.addNode({
1401
- id: nodeId,
1402
- type: "adr",
1403
- name: title,
1404
- path: filePath,
1405
- metadata: { date, status }
1406
- });
1412
+ this.store.addNode(parseADRNode(nodeId, filePath, filename, content));
1407
1413
  nodesAdded++;
1408
1414
  edgesAdded += this.linkToCode(content, nodeId, "documents");
1409
1415
  } catch (err) {
1410
1416
  errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
1411
1417
  }
1412
1418
  }
1413
- return {
1414
- nodesAdded,
1415
- nodesUpdated: 0,
1416
- edgesAdded,
1417
- edgesUpdated: 0,
1418
- errors,
1419
- durationMs: Date.now() - start
1420
- };
1419
+ return buildResult(nodesAdded, edgesAdded, errors, start);
1421
1420
  }
1422
1421
  async ingestLearnings(projectPath) {
1423
1422
  const start = Date.now();
1424
1423
  const filePath = path3.join(projectPath, ".harness", "learnings.md");
1425
- let content;
1426
- try {
1427
- content = await fs2.readFile(filePath, "utf-8");
1428
- } catch {
1429
- return emptyResult(Date.now() - start);
1430
- }
1431
- const errors = [];
1424
+ const content = await readFileOrEmpty(filePath);
1425
+ if (content === null) return emptyResult(Date.now() - start);
1432
1426
  let nodesAdded = 0;
1433
1427
  let edgesAdded = 0;
1434
- const lines = content.split("\n");
1435
1428
  let currentDate;
1436
- for (const line of lines) {
1429
+ for (const line of content.split("\n")) {
1437
1430
  const headingMatch = line.match(/^##\s+(\S+)/);
1438
1431
  if (headingMatch) {
1439
1432
  currentDate = headingMatch[1];
@@ -1442,70 +1435,29 @@ var KnowledgeIngestor = class {
1442
1435
  const bulletMatch = line.match(/^-\s+(.+)/);
1443
1436
  if (!bulletMatch) continue;
1444
1437
  const text = bulletMatch[1];
1445
- const skillMatch = text.match(/\[skill:([^\]]+)\]/);
1446
- const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
1447
- const skill = skillMatch ? skillMatch[1] : void 0;
1448
- const outcome = outcomeMatch ? outcomeMatch[1] : void 0;
1449
1438
  const nodeId = `learning:${hash(text)}`;
1450
- this.store.addNode({
1451
- id: nodeId,
1452
- type: "learning",
1453
- name: text,
1454
- metadata: { skill, outcome, date: currentDate }
1455
- });
1439
+ this.store.addNode(parseLearningNode(nodeId, text, currentDate));
1456
1440
  nodesAdded++;
1457
1441
  edgesAdded += this.linkToCode(text, nodeId, "applies_to");
1458
1442
  }
1459
- return {
1460
- nodesAdded,
1461
- nodesUpdated: 0,
1462
- edgesAdded,
1463
- edgesUpdated: 0,
1464
- errors,
1465
- durationMs: Date.now() - start
1466
- };
1443
+ return buildResult(nodesAdded, edgesAdded, [], start);
1467
1444
  }
1468
1445
  async ingestFailures(projectPath) {
1469
1446
  const start = Date.now();
1470
1447
  const filePath = path3.join(projectPath, ".harness", "failures.md");
1471
- let content;
1472
- try {
1473
- content = await fs2.readFile(filePath, "utf-8");
1474
- } catch {
1475
- return emptyResult(Date.now() - start);
1476
- }
1477
- const errors = [];
1448
+ const content = await readFileOrEmpty(filePath);
1449
+ if (content === null) return emptyResult(Date.now() - start);
1478
1450
  let nodesAdded = 0;
1479
1451
  let edgesAdded = 0;
1480
- const sections = content.split(/^##\s+/m).filter((s) => s.trim());
1481
- for (const section of sections) {
1482
- const dateMatch = section.match(/\*\*Date:\*\*\s*(.+)/);
1483
- const skillMatch = section.match(/\*\*Skill:\*\*\s*(.+)/);
1484
- const typeMatch = section.match(/\*\*Type:\*\*\s*(.+)/);
1485
- const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
1486
- const date = dateMatch ? dateMatch[1].trim() : void 0;
1487
- const skill = skillMatch ? skillMatch[1].trim() : void 0;
1488
- const failureType = typeMatch ? typeMatch[1].trim() : void 0;
1489
- const description = descMatch ? descMatch[1].trim() : void 0;
1490
- if (!description) continue;
1491
- const nodeId = `failure:${hash(description)}`;
1492
- this.store.addNode({
1493
- id: nodeId,
1494
- type: "failure",
1495
- name: description,
1496
- metadata: { date, skill, type: failureType }
1497
- });
1452
+ for (const section of content.split(/^##\s+/m).filter((s) => s.trim())) {
1453
+ const parsed = parseFailureSection(section);
1454
+ if (!parsed) continue;
1455
+ const { description, node } = parsed;
1456
+ this.store.addNode(node);
1498
1457
  nodesAdded++;
1499
- edgesAdded += this.linkToCode(description, nodeId, "caused_by");
1458
+ edgesAdded += this.linkToCode(description, node.id, "caused_by");
1500
1459
  }
1501
- return {
1502
- nodesAdded,
1503
- nodesUpdated: 0,
1504
- edgesAdded,
1505
- edgesUpdated: 0,
1506
- errors,
1507
- durationMs: Date.now() - start
1508
- };
1460
+ return buildResult(nodesAdded, edgesAdded, [], start);
1509
1461
  }
1510
1462
  async ingestAll(projectPath, opts) {
1511
1463
  const start = Date.now();
@@ -1559,6 +1511,74 @@ var KnowledgeIngestor = class {
1559
1511
  return results;
1560
1512
  }
1561
1513
  };
1514
+ async function readFileOrEmpty(filePath) {
1515
+ try {
1516
+ return await fs2.readFile(filePath, "utf-8");
1517
+ } catch {
1518
+ return null;
1519
+ }
1520
+ }
1521
+ function buildResult(nodesAdded, edgesAdded, errors, start) {
1522
+ return {
1523
+ nodesAdded,
1524
+ nodesUpdated: 0,
1525
+ edgesAdded,
1526
+ edgesUpdated: 0,
1527
+ errors,
1528
+ durationMs: Date.now() - start
1529
+ };
1530
+ }
1531
+ function parseADRNode(nodeId, filePath, filename, content) {
1532
+ const titleMatch = content.match(/^#\s+(.+)$/m);
1533
+ const title = titleMatch ? titleMatch[1].trim() : filename;
1534
+ const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
1535
+ const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
1536
+ return {
1537
+ id: nodeId,
1538
+ type: "adr",
1539
+ name: title,
1540
+ path: filePath,
1541
+ metadata: {
1542
+ date: dateMatch ? dateMatch[1].trim() : void 0,
1543
+ status: statusMatch ? statusMatch[1].trim() : void 0
1544
+ }
1545
+ };
1546
+ }
1547
+ function parseLearningNode(nodeId, text, currentDate) {
1548
+ const skillMatch = text.match(/\[skill:([^\]]+)\]/);
1549
+ const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
1550
+ return {
1551
+ id: nodeId,
1552
+ type: "learning",
1553
+ name: text,
1554
+ metadata: {
1555
+ skill: skillMatch ? skillMatch[1] : void 0,
1556
+ outcome: outcomeMatch ? outcomeMatch[1] : void 0,
1557
+ date: currentDate
1558
+ }
1559
+ };
1560
+ }
1561
+ function parseFailureSection(section) {
1562
+ const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
1563
+ const description = descMatch ? descMatch[1].trim() : void 0;
1564
+ if (!description) return null;
1565
+ const dateMatch = section.match(/\*\*Date:\*\*\s*(.+)/);
1566
+ const skillMatch = section.match(/\*\*Skill:\*\*\s*(.+)/);
1567
+ const typeMatch = section.match(/\*\*Type:\*\*\s*(.+)/);
1568
+ return {
1569
+ description,
1570
+ node: {
1571
+ id: `failure:${hash(description)}`,
1572
+ type: "failure",
1573
+ name: description,
1574
+ metadata: {
1575
+ date: dateMatch ? dateMatch[1].trim() : void 0,
1576
+ skill: skillMatch ? skillMatch[1].trim() : void 0,
1577
+ type: typeMatch ? typeMatch[1].trim() : void 0
1578
+ }
1579
+ }
1580
+ };
1581
+ }
1562
1582
 
1563
1583
  // src/ingest/RequirementIngestor.ts
1564
1584
  var fs3 = __toESM(require("fs/promises"));
@@ -1603,40 +1623,9 @@ var RequirementIngestor = class {
1603
1623
  return emptyResult(Date.now() - start);
1604
1624
  }
1605
1625
  for (const featureDir of featureDirs) {
1606
- const featureName = path4.basename(featureDir);
1607
- const specPath = path4.join(featureDir, "proposal.md");
1608
- let content;
1609
- try {
1610
- content = await fs3.readFile(specPath, "utf-8");
1611
- } catch {
1612
- continue;
1613
- }
1614
- try {
1615
- const specHash = hash(specPath);
1616
- const specNodeId = `file:${specPath}`;
1617
- this.store.addNode({
1618
- id: specNodeId,
1619
- type: "document",
1620
- name: path4.basename(specPath),
1621
- path: specPath,
1622
- metadata: { featureName }
1623
- });
1624
- const requirements = this.extractRequirements(content, specPath, specHash, featureName);
1625
- for (const req of requirements) {
1626
- this.store.addNode(req.node);
1627
- nodesAdded++;
1628
- this.store.addEdge({
1629
- from: req.node.id,
1630
- to: specNodeId,
1631
- type: "specifies"
1632
- });
1633
- edgesAdded++;
1634
- edgesAdded += this.linkByPathPattern(req.node.id, featureName);
1635
- edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
1636
- }
1637
- } catch (err) {
1638
- errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
1639
- }
1626
+ const counts = await this.ingestFeatureDir(featureDir, errors);
1627
+ nodesAdded += counts.nodesAdded;
1628
+ edgesAdded += counts.edgesAdded;
1640
1629
  }
1641
1630
  return {
1642
1631
  nodesAdded,
@@ -1647,6 +1636,48 @@ var RequirementIngestor = class {
1647
1636
  durationMs: Date.now() - start
1648
1637
  };
1649
1638
  }
1639
+ async ingestFeatureDir(featureDir, errors) {
1640
+ const featureName = path4.basename(featureDir);
1641
+ const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
1642
+ let content;
1643
+ try {
1644
+ content = await fs3.readFile(specPath, "utf-8");
1645
+ } catch {
1646
+ return { nodesAdded: 0, edgesAdded: 0 };
1647
+ }
1648
+ try {
1649
+ return this.ingestSpec(specPath, content, featureName);
1650
+ } catch (err) {
1651
+ errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
1652
+ return { nodesAdded: 0, edgesAdded: 0 };
1653
+ }
1654
+ }
1655
+ ingestSpec(specPath, content, featureName) {
1656
+ const specHash = hash(specPath);
1657
+ const specNodeId = `file:${specPath}`;
1658
+ this.store.addNode({
1659
+ id: specNodeId,
1660
+ type: "document",
1661
+ name: path4.basename(specPath),
1662
+ path: specPath,
1663
+ metadata: { featureName }
1664
+ });
1665
+ const requirements = this.extractRequirements(content, specPath, specHash, featureName);
1666
+ let nodesAdded = 0;
1667
+ let edgesAdded = 0;
1668
+ for (const req of requirements) {
1669
+ const counts = this.ingestRequirement(req.node, specNodeId, featureName);
1670
+ nodesAdded += counts.nodesAdded;
1671
+ edgesAdded += counts.edgesAdded;
1672
+ }
1673
+ return { nodesAdded, edgesAdded };
1674
+ }
1675
+ ingestRequirement(node, specNodeId, featureName) {
1676
+ this.store.addNode(node);
1677
+ this.store.addEdge({ from: node.id, to: specNodeId, type: "specifies" });
1678
+ const edgesAdded = 1 + this.linkByPathPattern(node.id, featureName) + this.linkByKeywordOverlap(node.id, node.name);
1679
+ return { nodesAdded: 1, edgesAdded };
1680
+ }
1650
1681
  /**
1651
1682
  * Parse markdown content and extract numbered items from recognized sections.
1652
1683
  */
@@ -1658,54 +1689,80 @@ var RequirementIngestor = class {
1658
1689
  let globalIndex = 0;
1659
1690
  for (let i = 0; i < lines.length; i++) {
1660
1691
  const line = lines[i];
1661
- const headingMatch = line.match(SECTION_HEADING_RE);
1662
- if (headingMatch) {
1663
- const heading = headingMatch[1].trim();
1664
- const isReqSection = REQUIREMENT_SECTIONS.some(
1665
- (s) => heading.toLowerCase() === s.toLowerCase()
1666
- );
1667
- if (isReqSection) {
1668
- currentSection = heading;
1669
- inRequirementSection = true;
1670
- } else {
1671
- inRequirementSection = false;
1692
+ const sectionResult = this.processHeadingLine(line, inRequirementSection);
1693
+ if (sectionResult !== null) {
1694
+ inRequirementSection = sectionResult.inRequirementSection;
1695
+ if (sectionResult.currentSection !== void 0) {
1696
+ currentSection = sectionResult.currentSection;
1672
1697
  }
1673
1698
  continue;
1674
1699
  }
1675
1700
  if (!inRequirementSection) continue;
1676
1701
  const itemMatch = line.match(NUMBERED_ITEM_RE);
1677
1702
  if (!itemMatch) continue;
1678
- const index = parseInt(itemMatch[1], 10);
1679
- const text = itemMatch[2].trim();
1680
- const rawText = line.trim();
1681
- const lineNumber = i + 1;
1682
1703
  globalIndex++;
1683
- const nodeId = `req:${specHash}:${globalIndex}`;
1684
- const earsPattern = detectEarsPattern(text);
1685
- results.push({
1686
- node: {
1687
- id: nodeId,
1688
- type: "requirement",
1689
- name: text,
1690
- path: specPath,
1691
- location: {
1692
- fileId: `file:${specPath}`,
1693
- startLine: lineNumber,
1694
- endLine: lineNumber
1695
- },
1696
- metadata: {
1697
- specPath,
1698
- index,
1699
- section: currentSection,
1700
- rawText,
1701
- earsPattern,
1702
- featureName
1703
- }
1704
- }
1705
- });
1704
+ results.push(
1705
+ this.buildRequirementNode(
1706
+ line,
1707
+ itemMatch,
1708
+ i + 1,
1709
+ specPath,
1710
+ specHash,
1711
+ globalIndex,
1712
+ featureName,
1713
+ currentSection
1714
+ )
1715
+ );
1706
1716
  }
1707
1717
  return results;
1708
1718
  }
1719
+ /**
1720
+ * Check if a line is a section heading and return updated section state,
1721
+ * or return null if the line is not a heading.
1722
+ */
1723
+ processHeadingLine(line, _inRequirementSection) {
1724
+ const headingMatch = line.match(SECTION_HEADING_RE);
1725
+ if (!headingMatch) return null;
1726
+ const heading = headingMatch[1].trim();
1727
+ const isReqSection = REQUIREMENT_SECTIONS.some(
1728
+ (s) => heading.toLowerCase() === s.toLowerCase()
1729
+ );
1730
+ if (isReqSection) {
1731
+ return { inRequirementSection: true, currentSection: heading };
1732
+ }
1733
+ return { inRequirementSection: false };
1734
+ }
1735
+ /**
1736
+ * Build a requirement GraphNode from a matched numbered-item line.
1737
+ */
1738
+ buildRequirementNode(line, itemMatch, lineNumber, specPath, specHash, globalIndex, featureName, currentSection) {
1739
+ const index = parseInt(itemMatch[1], 10);
1740
+ const text = itemMatch[2].trim();
1741
+ const rawText = line.trim();
1742
+ const nodeId = `req:${specHash}:${globalIndex}`;
1743
+ const earsPattern = detectEarsPattern(text);
1744
+ return {
1745
+ node: {
1746
+ id: nodeId,
1747
+ type: "requirement",
1748
+ name: text,
1749
+ path: specPath,
1750
+ location: {
1751
+ fileId: `file:${specPath}`,
1752
+ startLine: lineNumber,
1753
+ endLine: lineNumber
1754
+ },
1755
+ metadata: {
1756
+ specPath,
1757
+ index,
1758
+ section: currentSection,
1759
+ rawText,
1760
+ earsPattern,
1761
+ featureName
1762
+ }
1763
+ }
1764
+ };
1765
+ }
1709
1766
  /**
1710
1767
  * Convention-based linking: match requirement to code/test files
1711
1768
  * by feature name in their path.
@@ -1754,7 +1811,7 @@ var RequirementIngestor = class {
1754
1811
  const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1755
1812
  const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
1756
1813
  if (namePattern.test(reqText)) {
1757
- const edgeType = node.path && node.path.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
1814
+ const edgeType = node.path?.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
1758
1815
  this.store.addEdge({
1759
1816
  from: reqId,
1760
1817
  to: node.id,
@@ -1909,15 +1966,18 @@ function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
1909
1966
  durationMs: Date.now() - start
1910
1967
  };
1911
1968
  }
1969
+ function appendJqlClause(jql, clause) {
1970
+ return jql ? `${jql} AND ${clause}` : clause;
1971
+ }
1912
1972
  function buildJql(config) {
1913
1973
  const project2 = config.project;
1914
1974
  let jql = project2 ? `project=${project2}` : "";
1915
1975
  const filters = config.filters;
1916
1976
  if (filters?.status?.length) {
1917
- jql += `${jql ? " AND " : ""}status IN (${filters.status.map((s) => `"${s}"`).join(",")})`;
1977
+ jql = appendJqlClause(jql, `status IN (${filters.status.map((s) => `"${s}"`).join(",")})`);
1918
1978
  }
1919
1979
  if (filters?.labels?.length) {
1920
- jql += `${jql ? " AND " : ""}labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`;
1980
+ jql = appendJqlClause(jql, `labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`);
1921
1981
  }
1922
1982
  return jql;
1923
1983
  }
@@ -1930,8 +1990,6 @@ var JiraConnector = class {
1930
1990
  }
1931
1991
  async ingest(store, config) {
1932
1992
  const start = Date.now();
1933
- let nodesAdded = 0;
1934
- let edgesAdded = 0;
1935
1993
  const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
1936
1994
  const apiKey = process.env[apiKeyEnv];
1937
1995
  if (!apiKey) {
@@ -1953,38 +2011,39 @@ var JiraConnector = class {
1953
2011
  );
1954
2012
  }
1955
2013
  const jql = buildJql(config);
1956
- const headers = {
1957
- Authorization: `Basic ${apiKey}`,
1958
- "Content-Type": "application/json"
1959
- };
2014
+ const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
1960
2015
  try {
1961
- let startAt = 0;
1962
- const maxResults = 50;
1963
- let total = Infinity;
1964
- while (startAt < total) {
1965
- const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
1966
- const response = await this.httpClient(url, { headers });
1967
- if (!response.ok) {
1968
- return buildIngestResult(nodesAdded, edgesAdded, ["Jira API request failed"], start);
1969
- }
1970
- const data = await response.json();
1971
- total = data.total;
1972
- for (const issue of data.issues) {
1973
- const counts = this.processIssue(store, issue);
1974
- nodesAdded += counts.nodesAdded;
1975
- edgesAdded += counts.edgesAdded;
1976
- }
1977
- startAt += maxResults;
1978
- }
2016
+ const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
2017
+ return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
1979
2018
  } catch (err) {
1980
2019
  return buildIngestResult(
1981
- nodesAdded,
1982
- edgesAdded,
2020
+ 0,
2021
+ 0,
1983
2022
  [`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
1984
2023
  start
1985
2024
  );
1986
2025
  }
1987
- return buildIngestResult(nodesAdded, edgesAdded, [], start);
2026
+ }
2027
+ async fetchAllIssues(store, baseUrl, jql, headers) {
2028
+ let nodesAdded = 0;
2029
+ let edgesAdded = 0;
2030
+ let startAt = 0;
2031
+ const maxResults = 50;
2032
+ let total = Infinity;
2033
+ while (startAt < total) {
2034
+ const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
2035
+ const response = await this.httpClient(url, { headers });
2036
+ if (!response.ok) throw new Error("Jira API request failed");
2037
+ const data = await response.json();
2038
+ total = data.total;
2039
+ for (const issue of data.issues) {
2040
+ const counts = this.processIssue(store, issue);
2041
+ nodesAdded += counts.nodesAdded;
2042
+ edgesAdded += counts.edgesAdded;
2043
+ }
2044
+ startAt += maxResults;
2045
+ }
2046
+ return { nodesAdded, edgesAdded };
1988
2047
  }
1989
2048
  processIssue(store, issue) {
1990
2049
  const nodeId = `issue:jira:${issue.key}`;
@@ -2105,6 +2164,16 @@ var SlackConnector = class {
2105
2164
  };
2106
2165
 
2107
2166
  // src/ingest/connectors/ConfluenceConnector.ts
2167
+ function missingApiKeyResult(envVar, start) {
2168
+ return {
2169
+ nodesAdded: 0,
2170
+ nodesUpdated: 0,
2171
+ edgesAdded: 0,
2172
+ edgesUpdated: 0,
2173
+ errors: [`Missing API key: environment variable "${envVar}" is not set`],
2174
+ durationMs: Date.now() - start
2175
+ };
2176
+ }
2108
2177
  var ConfluenceConnector = class {
2109
2178
  name = "confluence";
2110
2179
  source = "confluence";
@@ -2115,40 +2184,34 @@ var ConfluenceConnector = class {
2115
2184
  async ingest(store, config) {
2116
2185
  const start = Date.now();
2117
2186
  const errors = [];
2118
- let nodesAdded = 0;
2119
- let edgesAdded = 0;
2120
2187
  const apiKeyEnv = config.apiKeyEnv ?? "CONFLUENCE_API_KEY";
2121
2188
  const apiKey = process.env[apiKeyEnv];
2122
2189
  if (!apiKey) {
2123
- return {
2124
- nodesAdded: 0,
2125
- nodesUpdated: 0,
2126
- edgesAdded: 0,
2127
- edgesUpdated: 0,
2128
- errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
2129
- durationMs: Date.now() - start
2130
- };
2190
+ return missingApiKeyResult(apiKeyEnv, start);
2131
2191
  }
2132
2192
  const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
2133
2193
  const baseUrl = process.env[baseUrlEnv] ?? "";
2134
2194
  const spaceKey = config.spaceKey ?? "";
2135
- try {
2136
- const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
2137
- nodesAdded = result.nodesAdded;
2138
- edgesAdded = result.edgesAdded;
2139
- errors.push(...result.errors);
2140
- } catch (err) {
2141
- errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
2142
- }
2195
+ const counts = await this.fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors);
2143
2196
  return {
2144
- nodesAdded,
2197
+ nodesAdded: counts.nodesAdded,
2145
2198
  nodesUpdated: 0,
2146
- edgesAdded,
2199
+ edgesAdded: counts.edgesAdded,
2147
2200
  edgesUpdated: 0,
2148
2201
  errors,
2149
2202
  durationMs: Date.now() - start
2150
2203
  };
2151
2204
  }
2205
+ async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
2206
+ try {
2207
+ const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
2208
+ errors.push(...result.errors);
2209
+ return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
2210
+ } catch (err) {
2211
+ errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
2212
+ return { nodesAdded: 0, edgesAdded: 0 };
2213
+ }
2214
+ }
2152
2215
  async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
2153
2216
  const errors = [];
2154
2217
  let nodesAdded = 0;
@@ -2193,7 +2256,62 @@ var ConfluenceConnector = class {
2193
2256
  };
2194
2257
 
2195
2258
  // src/ingest/connectors/CIConnector.ts
2196
- var CIConnector = class {
2259
+ function emptyResult2(errors, start) {
2260
+ return {
2261
+ nodesAdded: 0,
2262
+ nodesUpdated: 0,
2263
+ edgesAdded: 0,
2264
+ edgesUpdated: 0,
2265
+ errors,
2266
+ durationMs: Date.now() - start
2267
+ };
2268
+ }
2269
+ function ingestRun(store, run) {
2270
+ const buildId = `build:${run.id}`;
2271
+ const safeName = sanitizeExternalText(run.name, 200);
2272
+ let nodesAdded = 0;
2273
+ let edgesAdded = 0;
2274
+ store.addNode({
2275
+ id: buildId,
2276
+ type: "build",
2277
+ name: `${safeName} #${run.id}`,
2278
+ metadata: {
2279
+ source: "github-actions",
2280
+ status: run.status,
2281
+ conclusion: run.conclusion,
2282
+ branch: run.head_branch,
2283
+ sha: run.head_sha,
2284
+ url: run.html_url,
2285
+ createdAt: run.created_at
2286
+ }
2287
+ });
2288
+ nodesAdded++;
2289
+ const commitNode = store.getNode(`commit:${run.head_sha}`);
2290
+ if (commitNode) {
2291
+ store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
2292
+ edgesAdded++;
2293
+ }
2294
+ if (run.conclusion === "failure") {
2295
+ const testResultId = `test_result:${run.id}`;
2296
+ store.addNode({
2297
+ id: testResultId,
2298
+ type: "test_result",
2299
+ name: `Failed: ${safeName} #${run.id}`,
2300
+ metadata: {
2301
+ source: "github-actions",
2302
+ buildId: String(run.id),
2303
+ conclusion: "failure",
2304
+ branch: run.head_branch,
2305
+ sha: run.head_sha
2306
+ }
2307
+ });
2308
+ nodesAdded++;
2309
+ store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
2310
+ edgesAdded++;
2311
+ }
2312
+ return { nodesAdded, edgesAdded };
2313
+ }
2314
+ var CIConnector = class {
2197
2315
  name = "ci";
2198
2316
  source = "github-actions";
2199
2317
  httpClient;
@@ -2203,22 +2321,29 @@ var CIConnector = class {
2203
2321
  async ingest(store, config) {
2204
2322
  const start = Date.now();
2205
2323
  const errors = [];
2206
- let nodesAdded = 0;
2207
- let edgesAdded = 0;
2208
2324
  const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
2209
2325
  const apiKey = process.env[apiKeyEnv];
2210
2326
  if (!apiKey) {
2211
- return {
2212
- nodesAdded: 0,
2213
- nodesUpdated: 0,
2214
- edgesAdded: 0,
2215
- edgesUpdated: 0,
2216
- errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
2217
- durationMs: Date.now() - start
2218
- };
2327
+ return emptyResult2(
2328
+ [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
2329
+ start
2330
+ );
2219
2331
  }
2220
2332
  const repo = config.repo ?? "";
2221
2333
  const maxRuns = config.maxRuns ?? 10;
2334
+ const counts = await this.fetchAndIngestRuns(store, repo, maxRuns, apiKey, errors);
2335
+ return {
2336
+ nodesAdded: counts.nodesAdded,
2337
+ nodesUpdated: 0,
2338
+ edgesAdded: counts.edgesAdded,
2339
+ edgesUpdated: 0,
2340
+ errors,
2341
+ durationMs: Date.now() - start
2342
+ };
2343
+ }
2344
+ async fetchAndIngestRuns(store, repo, maxRuns, apiKey, errors) {
2345
+ let nodesAdded = 0;
2346
+ let edgesAdded = 0;
2222
2347
  try {
2223
2348
  const url = `https://api.github.com/repos/${repo}/actions/runs?per_page=${maxRuns}`;
2224
2349
  const response = await this.httpClient(url, {
@@ -2226,71 +2351,20 @@ var CIConnector = class {
2226
2351
  });
2227
2352
  if (!response.ok) {
2228
2353
  errors.push(`GitHub Actions API error: status ${response.status}`);
2229
- return {
2230
- nodesAdded: 0,
2231
- nodesUpdated: 0,
2232
- edgesAdded: 0,
2233
- edgesUpdated: 0,
2234
- errors,
2235
- durationMs: Date.now() - start
2236
- };
2354
+ return { nodesAdded, edgesAdded };
2237
2355
  }
2238
2356
  const data = await response.json();
2239
2357
  for (const run of data.workflow_runs) {
2240
- const buildId = `build:${run.id}`;
2241
- const safeName = sanitizeExternalText(run.name, 200);
2242
- store.addNode({
2243
- id: buildId,
2244
- type: "build",
2245
- name: `${safeName} #${run.id}`,
2246
- metadata: {
2247
- source: "github-actions",
2248
- status: run.status,
2249
- conclusion: run.conclusion,
2250
- branch: run.head_branch,
2251
- sha: run.head_sha,
2252
- url: run.html_url,
2253
- createdAt: run.created_at
2254
- }
2255
- });
2256
- nodesAdded++;
2257
- const commitNode = store.getNode(`commit:${run.head_sha}`);
2258
- if (commitNode) {
2259
- store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
2260
- edgesAdded++;
2261
- }
2262
- if (run.conclusion === "failure") {
2263
- const testResultId = `test_result:${run.id}`;
2264
- store.addNode({
2265
- id: testResultId,
2266
- type: "test_result",
2267
- name: `Failed: ${safeName} #${run.id}`,
2268
- metadata: {
2269
- source: "github-actions",
2270
- buildId: String(run.id),
2271
- conclusion: "failure",
2272
- branch: run.head_branch,
2273
- sha: run.head_sha
2274
- }
2275
- });
2276
- nodesAdded++;
2277
- store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
2278
- edgesAdded++;
2279
- }
2358
+ const counts = ingestRun(store, run);
2359
+ nodesAdded += counts.nodesAdded;
2360
+ edgesAdded += counts.edgesAdded;
2280
2361
  }
2281
2362
  } catch (err) {
2282
2363
  errors.push(
2283
2364
  `GitHub Actions fetch error: ${err instanceof Error ? err.message : String(err)}`
2284
2365
  );
2285
2366
  }
2286
- return {
2287
- nodesAdded,
2288
- nodesUpdated: 0,
2289
- edgesAdded,
2290
- edgesUpdated: 0,
2291
- errors,
2292
- durationMs: Date.now() - start
2293
- };
2367
+ return { nodesAdded, edgesAdded };
2294
2368
  }
2295
2369
  };
2296
2370
 
@@ -2360,16 +2434,29 @@ var FusionLayer = class {
2360
2434
  return [];
2361
2435
  }
2362
2436
  const allNodes = this.store.findNodes({});
2437
+ const semanticScores = this.buildSemanticScores(queryEmbedding, allNodes.length);
2438
+ const { kwWeight, semWeight } = this.resolveWeights(semanticScores.size > 0);
2439
+ const results = this.scoreNodes(allNodes, keywords, semanticScores, kwWeight, semWeight);
2440
+ results.sort((a, b) => b.score - a.score);
2441
+ return results.slice(0, topK);
2442
+ }
2443
+ buildSemanticScores(queryEmbedding, nodeCount) {
2363
2444
  const semanticScores = /* @__PURE__ */ new Map();
2364
2445
  if (queryEmbedding && this.vectorStore) {
2365
- const vectorResults = this.vectorStore.search(queryEmbedding, allNodes.length);
2446
+ const vectorResults = this.vectorStore.search(queryEmbedding, nodeCount);
2366
2447
  for (const vr of vectorResults) {
2367
2448
  semanticScores.set(vr.id, vr.score);
2368
2449
  }
2369
2450
  }
2370
- const hasSemanticScores = semanticScores.size > 0;
2371
- const kwWeight = hasSemanticScores ? this.keywordWeight : 1;
2372
- const semWeight = hasSemanticScores ? this.semanticWeight : 0;
2451
+ return semanticScores;
2452
+ }
2453
+ resolveWeights(hasSemanticScores) {
2454
+ return {
2455
+ kwWeight: hasSemanticScores ? this.keywordWeight : 1,
2456
+ semWeight: hasSemanticScores ? this.semanticWeight : 0
2457
+ };
2458
+ }
2459
+ scoreNodes(allNodes, keywords, semanticScores, kwWeight, semWeight) {
2373
2460
  const results = [];
2374
2461
  for (const node of allNodes) {
2375
2462
  const kwScore = this.keywordScore(keywords, node);
@@ -2380,15 +2467,11 @@ var FusionLayer = class {
2380
2467
  nodeId: node.id,
2381
2468
  node,
2382
2469
  score: fusedScore,
2383
- signals: {
2384
- keyword: kwScore,
2385
- semantic: semScore
2386
- }
2470
+ signals: { keyword: kwScore, semantic: semScore }
2387
2471
  });
2388
2472
  }
2389
2473
  }
2390
- results.sort((a, b) => b.score - a.score);
2391
- return results.slice(0, topK);
2474
+ return results;
2392
2475
  }
2393
2476
  extractKeywords(query) {
2394
2477
  const tokens = query.toLowerCase().split(/[\s\-_.,:;!?()[\]{}"'`/\\|@#$%^&*+=<>~]+/).filter((t) => t.length >= 2).filter((t) => !STOP_WORDS.has(t));
@@ -2443,37 +2526,50 @@ var GraphEntropyAdapter = class {
2443
2526
  const missingTargets = [];
2444
2527
  let freshEdges = 0;
2445
2528
  for (const edge of documentsEdges) {
2446
- const codeNode = this.store.getNode(edge.to);
2447
- if (!codeNode) {
2529
+ const result = this.classifyDocEdge(edge);
2530
+ if (result.kind === "missing") {
2448
2531
  missingTargets.push(edge.to);
2449
- continue;
2532
+ } else if (result.kind === "fresh") {
2533
+ freshEdges++;
2534
+ } else {
2535
+ staleEdges.push(result.entry);
2450
2536
  }
2451
- const docNode = this.store.getNode(edge.from);
2452
- const codeLastModified = codeNode.lastModified;
2453
- const docLastModified = docNode?.lastModified;
2454
- if (codeLastModified && docLastModified) {
2455
- if (codeLastModified > docLastModified) {
2456
- staleEdges.push({
2537
+ }
2538
+ return { staleEdges, missingTargets, freshEdges };
2539
+ }
2540
+ classifyDocEdge(edge) {
2541
+ const codeNode = this.store.getNode(edge.to);
2542
+ if (!codeNode) {
2543
+ return { kind: "missing" };
2544
+ }
2545
+ const docNode = this.store.getNode(edge.from);
2546
+ const codeLastModified = codeNode.lastModified;
2547
+ const docLastModified = docNode?.lastModified;
2548
+ if (codeLastModified && docLastModified) {
2549
+ if (codeLastModified > docLastModified) {
2550
+ return {
2551
+ kind: "stale",
2552
+ entry: {
2457
2553
  docNodeId: edge.from,
2458
2554
  codeNodeId: edge.to,
2459
2555
  edgeType: edge.type,
2460
2556
  codeLastModified,
2461
2557
  docLastModified
2462
- });
2463
- } else {
2464
- freshEdges++;
2465
- }
2466
- } else {
2467
- staleEdges.push({
2468
- docNodeId: edge.from,
2469
- codeNodeId: edge.to,
2470
- edgeType: edge.type,
2471
- codeLastModified,
2472
- docLastModified
2473
- });
2558
+ }
2559
+ };
2474
2560
  }
2561
+ return { kind: "fresh" };
2475
2562
  }
2476
- return { staleEdges, missingTargets, freshEdges };
2563
+ return {
2564
+ kind: "stale",
2565
+ entry: {
2566
+ docNodeId: edge.from,
2567
+ codeNodeId: edge.to,
2568
+ edgeType: edge.type,
2569
+ codeLastModified,
2570
+ docLastModified
2571
+ }
2572
+ };
2477
2573
  }
2478
2574
  /**
2479
2575
  * BFS from entry points to find reachable vs unreachable code nodes.
@@ -2730,36 +2826,12 @@ var GraphAnomalyAdapter = class {
2730
2826
  store;
2731
2827
  detect(options) {
2732
2828
  const threshold = options?.threshold != null && options.threshold > 0 ? options.threshold : DEFAULT_THRESHOLD;
2733
- const requestedMetrics = options?.metrics ?? [...DEFAULT_METRICS];
2734
- const warnings = [];
2735
- const metricsToAnalyze = [];
2736
- for (const m of requestedMetrics) {
2737
- if (RECOGNIZED_METRICS.has(m)) {
2738
- metricsToAnalyze.push(m);
2739
- } else {
2740
- warnings.push(m);
2741
- }
2742
- }
2743
- const allOutliers = [];
2744
- const analyzedNodeIds = /* @__PURE__ */ new Set();
2745
- const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
2746
- const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
2747
- const needsComplexity = metricsToAnalyze.includes("hotspotScore");
2748
- const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
2749
- const cachedHotspotData = needsComplexity ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
2750
- for (const metric of metricsToAnalyze) {
2751
- const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
2752
- for (const e of entries) {
2753
- analyzedNodeIds.add(e.nodeId);
2754
- }
2755
- const outliers = this.computeZScoreOutliers(entries, metric, threshold);
2756
- allOutliers.push(...outliers);
2757
- }
2758
- allOutliers.sort((a, b) => b.zScore - a.zScore);
2829
+ const { metricsToAnalyze, warnings } = this.filterMetrics(
2830
+ options?.metrics ?? [...DEFAULT_METRICS]
2831
+ );
2832
+ const { allOutliers, analyzedNodeIds } = this.computeAllOutliers(metricsToAnalyze, threshold);
2759
2833
  const articulationPoints = this.findArticulationPoints();
2760
- const outlierNodeIds = new Set(allOutliers.map((o) => o.nodeId));
2761
- const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
2762
- const overlapping = [...outlierNodeIds].filter((id) => apNodeIds.has(id));
2834
+ const overlapping = this.computeOverlap(allOutliers, articulationPoints);
2763
2835
  return {
2764
2836
  statisticalOutliers: allOutliers,
2765
2837
  articulationPoints,
@@ -2775,6 +2847,38 @@ var GraphAnomalyAdapter = class {
2775
2847
  }
2776
2848
  };
2777
2849
  }
2850
+ filterMetrics(requested) {
2851
+ const metricsToAnalyze = [];
2852
+ const warnings = [];
2853
+ for (const m of requested) {
2854
+ if (RECOGNIZED_METRICS.has(m)) {
2855
+ metricsToAnalyze.push(m);
2856
+ } else {
2857
+ warnings.push(m);
2858
+ }
2859
+ }
2860
+ return { metricsToAnalyze, warnings };
2861
+ }
2862
+ computeAllOutliers(metricsToAnalyze, threshold) {
2863
+ const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
2864
+ const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
2865
+ const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
2866
+ const cachedHotspotData = metricsToAnalyze.includes("hotspotScore") ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
2867
+ const allOutliers = [];
2868
+ const analyzedNodeIds = /* @__PURE__ */ new Set();
2869
+ for (const metric of metricsToAnalyze) {
2870
+ const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
2871
+ for (const e of entries) analyzedNodeIds.add(e.nodeId);
2872
+ allOutliers.push(...this.computeZScoreOutliers(entries, metric, threshold));
2873
+ }
2874
+ allOutliers.sort((a, b) => b.zScore - a.zScore);
2875
+ return { allOutliers, analyzedNodeIds };
2876
+ }
2877
+ computeOverlap(outliers, articulationPoints) {
2878
+ const outlierNodeIds = new Set(outliers.map((o) => o.nodeId));
2879
+ const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
2880
+ return [...outlierNodeIds].filter((id) => apNodeIds.has(id));
2881
+ }
2778
2882
  collectMetricValues(metric, cachedCouplingData, cachedHotspotData) {
2779
2883
  const entries = [];
2780
2884
  if (metric === "cyclomaticComplexity") {
@@ -2971,6 +3075,7 @@ var INTENT_SIGNALS = {
2971
3075
  "depend",
2972
3076
  "blast",
2973
3077
  "radius",
3078
+ "cascade",
2974
3079
  "risk",
2975
3080
  "delete",
2976
3081
  "remove"
@@ -2980,6 +3085,7 @@ var INTENT_SIGNALS = {
2980
3085
  /what\s+(breaks|happens|is affected)/,
2981
3086
  /if\s+i\s+(change|modify|remove|delete)/,
2982
3087
  /blast\s+radius/,
3088
+ /cascad/,
2983
3089
  /what\s+(depend|relies)/
2984
3090
  ]
2985
3091
  },
@@ -3328,37 +3434,54 @@ var EntityExtractor = class {
3328
3434
  result.push(entity);
3329
3435
  }
3330
3436
  };
3331
- const quotedConsumed = /* @__PURE__ */ new Set();
3437
+ const quotedConsumed = this.extractQuoted(trimmed, add);
3438
+ const casingConsumed = this.extractCasing(trimmed, quotedConsumed, add);
3439
+ const pathConsumed = this.extractPaths(trimmed, add);
3440
+ this.extractNouns(trimmed, buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed), add);
3441
+ return result;
3442
+ }
3443
+ /** Strategy 1: Quoted strings. Returns the set of consumed tokens. */
3444
+ extractQuoted(trimmed, add) {
3445
+ const consumed = /* @__PURE__ */ new Set();
3332
3446
  for (const match of trimmed.matchAll(QUOTED_RE)) {
3333
3447
  const inner = match[1].trim();
3334
3448
  if (inner.length > 0) {
3335
3449
  add(inner);
3336
- quotedConsumed.add(inner);
3450
+ consumed.add(inner);
3337
3451
  }
3338
3452
  }
3339
- const casingConsumed = /* @__PURE__ */ new Set();
3453
+ return consumed;
3454
+ }
3455
+ /** Strategy 2: PascalCase/camelCase tokens. Returns the set of consumed tokens. */
3456
+ extractCasing(trimmed, quotedConsumed, add) {
3457
+ const consumed = /* @__PURE__ */ new Set();
3340
3458
  for (const match of trimmed.matchAll(PASCAL_OR_CAMEL_RE)) {
3341
3459
  const token = match[0];
3342
3460
  if (!quotedConsumed.has(token)) {
3343
3461
  add(token);
3344
- casingConsumed.add(token);
3462
+ consumed.add(token);
3345
3463
  }
3346
3464
  }
3347
- const pathConsumed = /* @__PURE__ */ new Set();
3465
+ return consumed;
3466
+ }
3467
+ /** Strategy 3: File paths. Returns the set of consumed tokens. */
3468
+ extractPaths(trimmed, add) {
3469
+ const consumed = /* @__PURE__ */ new Set();
3348
3470
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
3349
3471
  const path7 = match[0];
3350
3472
  add(path7);
3351
- pathConsumed.add(path7);
3473
+ consumed.add(path7);
3352
3474
  }
3353
- const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
3354
- const words = trimmed.split(/\s+/);
3355
- for (const raw of words) {
3475
+ return consumed;
3476
+ }
3477
+ /** Strategy 4: Remaining significant nouns after stop-word and intent-keyword removal. */
3478
+ extractNouns(trimmed, allConsumed, add) {
3479
+ for (const raw of trimmed.split(/\s+/)) {
3356
3480
  const cleaned = raw.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "");
3357
3481
  if (cleaned.length === 0) continue;
3358
3482
  if (isSkippableWord(cleaned, allConsumed)) continue;
3359
3483
  add(cleaned);
3360
3484
  }
3361
- return result;
3362
3485
  }
3363
3486
  };
3364
3487
 
@@ -3465,6 +3588,10 @@ var ResponseFormatter = class {
3465
3588
  }
3466
3589
  formatImpact(entityName, data) {
3467
3590
  const d = data;
3591
+ if ("sourceNodeId" in d && "summary" in d) {
3592
+ const summary = d.summary;
3593
+ return `Blast radius of **${entityName}**: ${summary.totalAffected} affected nodes (${summary.highRisk} high risk, ${summary.mediumRisk} medium, ${summary.lowRisk} low).`;
3594
+ }
3468
3595
  const code = this.safeArrayLength(d?.code);
3469
3596
  const tests = this.safeArrayLength(d?.tests);
3470
3597
  const docs = this.safeArrayLength(d?.docs);
@@ -3526,41 +3653,286 @@ var ResponseFormatter = class {
3526
3653
  }
3527
3654
  };
3528
3655
 
3656
+ // src/blast-radius/CompositeProbabilityStrategy.ts
3657
+ var CompositeProbabilityStrategy = class _CompositeProbabilityStrategy {
3658
+ constructor(changeFreqMap, couplingMap) {
3659
+ this.changeFreqMap = changeFreqMap;
3660
+ this.couplingMap = couplingMap;
3661
+ }
3662
+ changeFreqMap;
3663
+ couplingMap;
3664
+ static BASE_WEIGHTS = {
3665
+ imports: 0.7,
3666
+ calls: 0.5,
3667
+ implements: 0.6,
3668
+ inherits: 0.6,
3669
+ co_changes_with: 0.4,
3670
+ references: 0.2,
3671
+ contains: 0.3
3672
+ };
3673
+ static FALLBACK_WEIGHT = 0.1;
3674
+ static EDGE_TYPE_BLEND = 0.5;
3675
+ static CHANGE_FREQ_BLEND = 0.3;
3676
+ static COUPLING_BLEND = 0.2;
3677
+ getEdgeProbability(edge, _fromNode, toNode) {
3678
+ const base = _CompositeProbabilityStrategy.BASE_WEIGHTS[edge.type] ?? _CompositeProbabilityStrategy.FALLBACK_WEIGHT;
3679
+ const changeFreq = this.changeFreqMap.get(toNode.id) ?? 0;
3680
+ const coupling = this.couplingMap.get(toNode.id) ?? 0;
3681
+ return Math.min(
3682
+ 1,
3683
+ base * _CompositeProbabilityStrategy.EDGE_TYPE_BLEND + changeFreq * _CompositeProbabilityStrategy.CHANGE_FREQ_BLEND + coupling * _CompositeProbabilityStrategy.COUPLING_BLEND
3684
+ );
3685
+ }
3686
+ };
3687
+
3688
+ // src/blast-radius/CascadeSimulator.ts
3689
+ var DEFAULT_PROBABILITY_FLOOR = 0.05;
3690
+ var DEFAULT_MAX_DEPTH = 10;
3691
+ var CascadeSimulator = class {
3692
+ constructor(store) {
3693
+ this.store = store;
3694
+ }
3695
+ store;
3696
+ simulate(sourceNodeId, options = {}) {
3697
+ const sourceNode = this.store.getNode(sourceNodeId);
3698
+ if (!sourceNode) {
3699
+ throw new Error(`Node not found: ${sourceNodeId}. Ensure the file has been ingested.`);
3700
+ }
3701
+ const probabilityFloor = options.probabilityFloor ?? DEFAULT_PROBABILITY_FLOOR;
3702
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
3703
+ const edgeTypeFilter = options.edgeTypes ? new Set(options.edgeTypes) : null;
3704
+ const strategy = options.strategy ?? this.buildDefaultStrategy();
3705
+ const visited = /* @__PURE__ */ new Map();
3706
+ const queue = [];
3707
+ const fanOutCount = /* @__PURE__ */ new Map();
3708
+ this.seedQueue(
3709
+ sourceNodeId,
3710
+ sourceNode,
3711
+ strategy,
3712
+ edgeTypeFilter,
3713
+ probabilityFloor,
3714
+ queue,
3715
+ fanOutCount
3716
+ );
3717
+ const truncated = this.runBfs(
3718
+ queue,
3719
+ visited,
3720
+ fanOutCount,
3721
+ sourceNodeId,
3722
+ strategy,
3723
+ edgeTypeFilter,
3724
+ probabilityFloor,
3725
+ maxDepth
3726
+ );
3727
+ return this.buildResult(sourceNodeId, sourceNode.name, visited, fanOutCount, truncated);
3728
+ }
3729
+ seedQueue(sourceNodeId, sourceNode, strategy, edgeTypeFilter, probabilityFloor, queue, fanOutCount) {
3730
+ const sourceEdges = this.store.getEdges({ from: sourceNodeId });
3731
+ for (const edge of sourceEdges) {
3732
+ if (edge.to === sourceNodeId) continue;
3733
+ if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
3734
+ const targetNode = this.store.getNode(edge.to);
3735
+ if (!targetNode) continue;
3736
+ const cumProb = strategy.getEdgeProbability(edge, sourceNode, targetNode);
3737
+ if (cumProb < probabilityFloor) continue;
3738
+ queue.push({
3739
+ nodeId: edge.to,
3740
+ cumProb,
3741
+ depth: 1,
3742
+ parentId: sourceNodeId,
3743
+ incomingEdge: edge.type
3744
+ });
3745
+ }
3746
+ fanOutCount.set(
3747
+ sourceNodeId,
3748
+ sourceEdges.filter(
3749
+ (e) => e.to !== sourceNodeId && (!edgeTypeFilter || edgeTypeFilter.has(e.type))
3750
+ ).length
3751
+ );
3752
+ }
3753
+ runBfs(queue, visited, fanOutCount, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, maxDepth) {
3754
+ const MAX_QUEUE_SIZE = 1e4;
3755
+ let head = 0;
3756
+ while (head < queue.length) {
3757
+ if (queue.length > MAX_QUEUE_SIZE) return true;
3758
+ const entry = queue[head++];
3759
+ const existing = visited.get(entry.nodeId);
3760
+ if (existing && existing.cumulativeProbability >= entry.cumProb) continue;
3761
+ const targetNode = this.store.getNode(entry.nodeId);
3762
+ if (!targetNode) continue;
3763
+ visited.set(entry.nodeId, {
3764
+ nodeId: entry.nodeId,
3765
+ name: targetNode.name,
3766
+ ...targetNode.path !== void 0 && { path: targetNode.path },
3767
+ type: targetNode.type,
3768
+ cumulativeProbability: entry.cumProb,
3769
+ depth: entry.depth,
3770
+ incomingEdge: entry.incomingEdge,
3771
+ parentId: entry.parentId
3772
+ });
3773
+ if (entry.depth < maxDepth) {
3774
+ const childCount = this.expandNode(
3775
+ entry,
3776
+ targetNode,
3777
+ sourceNodeId,
3778
+ strategy,
3779
+ edgeTypeFilter,
3780
+ probabilityFloor,
3781
+ queue
3782
+ );
3783
+ fanOutCount.set(entry.nodeId, (fanOutCount.get(entry.nodeId) ?? 0) + childCount);
3784
+ }
3785
+ }
3786
+ return false;
3787
+ }
3788
+ expandNode(entry, fromNode, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, queue) {
3789
+ const outEdges = this.store.getEdges({ from: entry.nodeId });
3790
+ let childCount = 0;
3791
+ for (const edge of outEdges) {
3792
+ if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
3793
+ if (edge.to === sourceNodeId) continue;
3794
+ const childNode = this.store.getNode(edge.to);
3795
+ if (!childNode) continue;
3796
+ const newCumProb = entry.cumProb * strategy.getEdgeProbability(edge, fromNode, childNode);
3797
+ if (newCumProb < probabilityFloor) continue;
3798
+ childCount++;
3799
+ queue.push({
3800
+ nodeId: edge.to,
3801
+ cumProb: newCumProb,
3802
+ depth: entry.depth + 1,
3803
+ parentId: entry.nodeId,
3804
+ incomingEdge: edge.type
3805
+ });
3806
+ }
3807
+ return childCount;
3808
+ }
3809
+ buildDefaultStrategy() {
3810
+ return new CompositeProbabilityStrategy(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
3811
+ }
3812
+ buildResult(sourceNodeId, sourceName, visited, fanOutCount, truncated = false) {
3813
+ if (visited.size === 0) {
3814
+ return {
3815
+ sourceNodeId,
3816
+ sourceName,
3817
+ layers: [],
3818
+ flatSummary: [],
3819
+ summary: {
3820
+ totalAffected: 0,
3821
+ maxDepthReached: 0,
3822
+ highRisk: 0,
3823
+ mediumRisk: 0,
3824
+ lowRisk: 0,
3825
+ categoryBreakdown: { code: 0, tests: 0, docs: 0, other: 0 },
3826
+ amplificationPoints: [],
3827
+ truncated
3828
+ }
3829
+ };
3830
+ }
3831
+ const allNodes = Array.from(visited.values());
3832
+ const flatSummary = [...allNodes].sort(
3833
+ (a, b) => b.cumulativeProbability - a.cumulativeProbability
3834
+ );
3835
+ const depthMap = /* @__PURE__ */ new Map();
3836
+ for (const node of allNodes) {
3837
+ let list = depthMap.get(node.depth);
3838
+ if (!list) {
3839
+ list = [];
3840
+ depthMap.set(node.depth, list);
3841
+ }
3842
+ list.push(node);
3843
+ }
3844
+ const layers = [];
3845
+ const depths = Array.from(depthMap.keys()).sort((a, b) => a - b);
3846
+ for (const depth of depths) {
3847
+ const nodes = depthMap.get(depth);
3848
+ const breakdown = { code: 0, tests: 0, docs: 0, other: 0 };
3849
+ for (const n of nodes) {
3850
+ const graphNode = this.store.getNode(n.nodeId);
3851
+ if (graphNode) {
3852
+ breakdown[classifyNodeCategory(graphNode)]++;
3853
+ }
3854
+ }
3855
+ layers.push({ depth, nodes, categoryBreakdown: breakdown });
3856
+ }
3857
+ let highRisk = 0;
3858
+ let mediumRisk = 0;
3859
+ let lowRisk = 0;
3860
+ const catBreakdown = { code: 0, tests: 0, docs: 0, other: 0 };
3861
+ for (const node of allNodes) {
3862
+ if (node.cumulativeProbability >= 0.5) highRisk++;
3863
+ else if (node.cumulativeProbability >= 0.2) mediumRisk++;
3864
+ else lowRisk++;
3865
+ const graphNode = this.store.getNode(node.nodeId);
3866
+ if (graphNode) {
3867
+ catBreakdown[classifyNodeCategory(graphNode)]++;
3868
+ }
3869
+ }
3870
+ const amplificationPoints = [];
3871
+ for (const [nodeId, count] of fanOutCount) {
3872
+ if (count > 3) {
3873
+ amplificationPoints.push(nodeId);
3874
+ }
3875
+ }
3876
+ const maxDepthReached = allNodes.reduce((max, n) => Math.max(max, n.depth), 0);
3877
+ return {
3878
+ sourceNodeId,
3879
+ sourceName,
3880
+ layers,
3881
+ flatSummary,
3882
+ summary: {
3883
+ totalAffected: allNodes.length,
3884
+ maxDepthReached,
3885
+ highRisk,
3886
+ mediumRisk,
3887
+ lowRisk,
3888
+ categoryBreakdown: catBreakdown,
3889
+ amplificationPoints,
3890
+ truncated
3891
+ }
3892
+ };
3893
+ }
3894
+ };
3895
+
3529
3896
  // src/nlq/index.ts
3530
3897
  var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships", "explain"]);
3531
3898
  var classifier = new IntentClassifier();
3532
3899
  var extractor = new EntityExtractor();
3533
3900
  var formatter = new ResponseFormatter();
3901
+ function lowConfidenceResult(intent, confidence) {
3902
+ return {
3903
+ intent,
3904
+ intentConfidence: confidence,
3905
+ entities: [],
3906
+ summary: "I'm not sure what you're asking. Try rephrasing your question.",
3907
+ data: null,
3908
+ suggestions: [
3909
+ 'Try "what breaks if I change <name>?" for impact analysis',
3910
+ 'Try "where is <name>?" to find entities',
3911
+ 'Try "what calls <name>?" for relationships',
3912
+ 'Try "what is <name>?" for explanations',
3913
+ 'Try "what looks wrong?" for anomaly detection'
3914
+ ]
3915
+ };
3916
+ }
3917
+ function noEntityResult(intent, confidence) {
3918
+ return {
3919
+ intent,
3920
+ intentConfidence: confidence,
3921
+ entities: [],
3922
+ summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
3923
+ data: null
3924
+ };
3925
+ }
3534
3926
  async function askGraph(store, question) {
3535
3927
  const fusion = new FusionLayer(store);
3536
3928
  const resolver = new EntityResolver(store, fusion);
3537
3929
  const classification = classifier.classify(question);
3538
3930
  if (classification.confidence < 0.3) {
3539
- return {
3540
- intent: classification.intent,
3541
- intentConfidence: classification.confidence,
3542
- entities: [],
3543
- summary: "I'm not sure what you're asking. Try rephrasing your question.",
3544
- data: null,
3545
- suggestions: [
3546
- 'Try "what breaks if I change <name>?" for impact analysis',
3547
- 'Try "where is <name>?" to find entities',
3548
- 'Try "what calls <name>?" for relationships',
3549
- 'Try "what is <name>?" for explanations',
3550
- 'Try "what looks wrong?" for anomaly detection'
3551
- ]
3552
- };
3931
+ return lowConfidenceResult(classification.intent, classification.confidence);
3553
3932
  }
3554
- const rawEntities = extractor.extract(question);
3555
- const entities = resolver.resolve(rawEntities);
3933
+ const entities = resolver.resolve(extractor.extract(question));
3556
3934
  if (ENTITY_REQUIRED_INTENTS.has(classification.intent) && entities.length === 0) {
3557
- return {
3558
- intent: classification.intent,
3559
- intentConfidence: classification.confidence,
3560
- entities: [],
3561
- summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
3562
- data: null
3563
- };
3935
+ return noEntityResult(classification.intent, classification.confidence);
3564
3936
  }
3565
3937
  let data;
3566
3938
  try {
@@ -3574,62 +3946,59 @@ async function askGraph(store, question) {
3574
3946
  data: null
3575
3947
  };
3576
3948
  }
3577
- const summary = formatter.format(classification.intent, entities, data, question);
3578
3949
  return {
3579
3950
  intent: classification.intent,
3580
3951
  intentConfidence: classification.confidence,
3581
3952
  entities,
3582
- summary,
3953
+ summary: formatter.format(classification.intent, entities, data, question),
3583
3954
  data
3584
3955
  };
3585
3956
  }
3957
+ function buildContextBlocks(cql, rootIds, searchResults) {
3958
+ return rootIds.map((rootId) => {
3959
+ const expanded = cql.execute({ rootNodeIds: [rootId], maxDepth: 2 });
3960
+ const match = searchResults.find((r) => r.nodeId === rootId);
3961
+ return {
3962
+ rootNode: rootId,
3963
+ score: match?.score ?? 1,
3964
+ nodes: expanded.nodes,
3965
+ edges: expanded.edges
3966
+ };
3967
+ });
3968
+ }
3969
+ function executeImpact(store, cql, entities, question) {
3970
+ const rootId = entities[0].nodeId;
3971
+ const lower = question.toLowerCase();
3972
+ if (lower.includes("blast radius") || lower.includes("cascade")) {
3973
+ return new CascadeSimulator(store).simulate(rootId);
3974
+ }
3975
+ const result = cql.execute({ rootNodeIds: [rootId], bidirectional: true, maxDepth: 3 });
3976
+ return groupNodesByImpact(result.nodes, rootId);
3977
+ }
3978
+ function executeExplain(cql, entities, question, fusion) {
3979
+ const searchResults = fusion.search(question, 10);
3980
+ const rootIds = entities.length > 0 ? [entities[0].nodeId] : searchResults.slice(0, 3).map((r) => r.nodeId);
3981
+ return { searchResults, context: buildContextBlocks(cql, rootIds, searchResults) };
3982
+ }
3586
3983
  function executeOperation(store, intent, entities, question, fusion) {
3587
3984
  const cql = new ContextQL(store);
3588
3985
  switch (intent) {
3589
- case "impact": {
3590
- const rootId = entities[0].nodeId;
3591
- const result = cql.execute({
3592
- rootNodeIds: [rootId],
3593
- bidirectional: true,
3594
- maxDepth: 3
3595
- });
3596
- return groupNodesByImpact(result.nodes, rootId);
3597
- }
3598
- case "find": {
3986
+ case "impact":
3987
+ return executeImpact(store, cql, entities, question);
3988
+ case "find":
3599
3989
  return fusion.search(question, 10);
3600
- }
3601
3990
  case "relationships": {
3602
- const rootId = entities[0].nodeId;
3603
3991
  const result = cql.execute({
3604
- rootNodeIds: [rootId],
3992
+ rootNodeIds: [entities[0].nodeId],
3605
3993
  bidirectional: true,
3606
3994
  maxDepth: 1
3607
3995
  });
3608
3996
  return { nodes: result.nodes, edges: result.edges };
3609
3997
  }
3610
- case "explain": {
3611
- const searchResults = fusion.search(question, 10);
3612
- const contextBlocks = [];
3613
- const rootIds = entities.length > 0 ? [entities[0].nodeId] : searchResults.slice(0, 3).map((r) => r.nodeId);
3614
- for (const rootId of rootIds) {
3615
- const expanded = cql.execute({
3616
- rootNodeIds: [rootId],
3617
- maxDepth: 2
3618
- });
3619
- const matchingResult = searchResults.find((r) => r.nodeId === rootId);
3620
- contextBlocks.push({
3621
- rootNode: rootId,
3622
- score: matchingResult?.score ?? 1,
3623
- nodes: expanded.nodes,
3624
- edges: expanded.edges
3625
- });
3626
- }
3627
- return { searchResults, context: contextBlocks };
3628
- }
3629
- case "anomaly": {
3630
- const adapter = new GraphAnomalyAdapter(store);
3631
- return adapter.detect();
3632
- }
3998
+ case "explain":
3999
+ return executeExplain(cql, entities, question, fusion);
4000
+ case "anomaly":
4001
+ return new GraphAnomalyAdapter(store).detect();
3633
4002
  default:
3634
4003
  return null;
3635
4004
  }
@@ -3650,12 +4019,14 @@ var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
3650
4019
  "method",
3651
4020
  "variable"
3652
4021
  ]);
4022
+ function countMetadataChars(node) {
4023
+ return node.metadata ? JSON.stringify(node.metadata).length : 0;
4024
+ }
4025
+ function countBaseChars(node) {
4026
+ return (node.name?.length ?? 0) + (node.path?.length ?? 0) + (node.type?.length ?? 0);
4027
+ }
3653
4028
  function estimateNodeTokens(node) {
3654
- let chars = (node.name?.length ?? 0) + (node.path?.length ?? 0) + (node.type?.length ?? 0);
3655
- if (node.metadata) {
3656
- chars += JSON.stringify(node.metadata).length;
3657
- }
3658
- return Math.ceil(chars / 4);
4029
+ return Math.ceil((countBaseChars(node) + countMetadataChars(node)) / 4);
3659
4030
  }
3660
4031
  var Assembler = class {
3661
4032
  store;
@@ -3736,47 +4107,55 @@ var Assembler = class {
3736
4107
  }
3737
4108
  return { keptNodes, tokenEstimate, truncated };
3738
4109
  }
3739
- /**
3740
- * Compute a token budget allocation across node types.
3741
- */
3742
- computeBudget(totalTokens, phase) {
3743
- const allNodes = this.store.findNodes({});
4110
+ countNodesByType() {
3744
4111
  const typeCounts = {};
3745
- for (const node of allNodes) {
4112
+ for (const node of this.store.findNodes({})) {
3746
4113
  typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;
3747
4114
  }
4115
+ return typeCounts;
4116
+ }
4117
+ computeModuleDensity() {
3748
4118
  const density = {};
3749
- const moduleNodes = this.store.findNodes({ type: "module" });
3750
- for (const mod of moduleNodes) {
3751
- const outEdges = this.store.getEdges({ from: mod.id });
3752
- const inEdges = this.store.getEdges({ to: mod.id });
3753
- density[mod.name] = outEdges.length + inEdges.length;
4119
+ for (const mod of this.store.findNodes({ type: "module" })) {
4120
+ const out = this.store.getEdges({ from: mod.id }).length;
4121
+ const inn = this.store.getEdges({ to: mod.id }).length;
4122
+ density[mod.name] = out + inn;
3754
4123
  }
3755
- const boostTypes = phase ? PHASE_NODE_TYPES[phase] : void 0;
3756
- const boostFactor = 2;
3757
- let weightedTotal = 0;
4124
+ return density;
4125
+ }
4126
+ computeTypeWeights(typeCounts, boostTypes) {
3758
4127
  const weights = {};
4128
+ let weightedTotal = 0;
3759
4129
  for (const [type, count] of Object.entries(typeCounts)) {
3760
- const isBoosted = boostTypes?.includes(type);
3761
- const weight = count * (isBoosted ? boostFactor : 1);
4130
+ const weight = count * (boostTypes?.includes(type) ? 2 : 1);
3762
4131
  weights[type] = weight;
3763
4132
  weightedTotal += weight;
3764
4133
  }
4134
+ return { weights, weightedTotal };
4135
+ }
4136
+ allocateProportionally(weights, weightedTotal, totalTokens) {
3765
4137
  const allocations = {};
3766
- if (weightedTotal > 0) {
3767
- let allocated = 0;
3768
- const types = Object.keys(weights);
3769
- for (let i = 0; i < types.length; i++) {
3770
- const type = types[i];
3771
- if (i === types.length - 1) {
3772
- allocations[type] = totalTokens - allocated;
3773
- } else {
3774
- const share = Math.round(weights[type] / weightedTotal * totalTokens);
3775
- allocations[type] = share;
3776
- allocated += share;
3777
- }
4138
+ if (weightedTotal === 0) return allocations;
4139
+ let allocated = 0;
4140
+ const types = Object.keys(weights);
4141
+ for (let i = 0; i < types.length; i++) {
4142
+ const type = types[i];
4143
+ if (i === types.length - 1) {
4144
+ allocations[type] = totalTokens - allocated;
4145
+ } else {
4146
+ const share = Math.round(weights[type] / weightedTotal * totalTokens);
4147
+ allocations[type] = share;
4148
+ allocated += share;
3778
4149
  }
3779
4150
  }
4151
+ return allocations;
4152
+ }
4153
+ computeBudget(totalTokens, phase) {
4154
+ const typeCounts = this.countNodesByType();
4155
+ const density = this.computeModuleDensity();
4156
+ const boostTypes = phase ? PHASE_NODE_TYPES[phase] : void 0;
4157
+ const { weights, weightedTotal } = this.computeTypeWeights(typeCounts, boostTypes);
4158
+ const allocations = this.allocateProportionally(weights, weightedTotal, totalTokens);
3780
4159
  return { total: totalTokens, allocations, density };
3781
4160
  }
3782
4161
  /**
@@ -3807,49 +4186,43 @@ var Assembler = class {
3807
4186
  filePaths: Array.from(filePathSet)
3808
4187
  };
3809
4188
  }
3810
- /**
3811
- * Generate a markdown repository map from graph structure.
3812
- */
3813
- generateMap() {
3814
- const moduleNodes = this.store.findNodes({ type: "module" });
3815
- const modulesWithEdgeCount = moduleNodes.map((mod) => {
3816
- const outEdges = this.store.getEdges({ from: mod.id });
3817
- const inEdges = this.store.getEdges({ to: mod.id });
3818
- return { module: mod, edgeCount: outEdges.length + inEdges.length };
4189
+ buildModuleLines() {
4190
+ const modulesWithEdgeCount = this.store.findNodes({ type: "module" }).map((mod) => {
4191
+ const edgeCount = this.store.getEdges({ from: mod.id }).length + this.store.getEdges({ to: mod.id }).length;
4192
+ return { module: mod, edgeCount };
3819
4193
  });
3820
4194
  modulesWithEdgeCount.sort((a, b) => b.edgeCount - a.edgeCount);
3821
- const lines = ["# Repository Structure", ""];
3822
- if (modulesWithEdgeCount.length > 0) {
3823
- lines.push("## Modules", "");
3824
- for (const { module: mod, edgeCount } of modulesWithEdgeCount) {
3825
- lines.push(`### ${mod.name} (${edgeCount} connections)`);
3826
- lines.push("");
3827
- const containsEdges = this.store.getEdges({ from: mod.id, type: "contains" });
3828
- for (const edge of containsEdges) {
3829
- const fileNode = this.store.getNode(edge.to);
3830
- if (fileNode && fileNode.type === "file") {
3831
- const symbolEdges = this.store.getEdges({ from: fileNode.id, type: "contains" });
3832
- lines.push(`- ${fileNode.path ?? fileNode.name} (${symbolEdges.length} symbols)`);
3833
- }
4195
+ if (modulesWithEdgeCount.length === 0) return [];
4196
+ const lines = ["## Modules", ""];
4197
+ for (const { module: mod, edgeCount } of modulesWithEdgeCount) {
4198
+ lines.push(`### ${mod.name} (${edgeCount} connections)`, "");
4199
+ for (const edge of this.store.getEdges({ from: mod.id, type: "contains" })) {
4200
+ const fileNode = this.store.getNode(edge.to);
4201
+ if (fileNode?.type === "file") {
4202
+ const symbols = this.store.getEdges({ from: fileNode.id, type: "contains" }).length;
4203
+ lines.push(`- ${fileNode.path ?? fileNode.name} (${symbols} symbols)`);
3834
4204
  }
3835
- lines.push("");
3836
4205
  }
4206
+ lines.push("");
3837
4207
  }
3838
- const fileNodes = this.store.findNodes({ type: "file" });
3839
- const nonBarrelFiles = fileNodes.filter((n) => !n.name.startsWith("index."));
3840
- const filesWithOutDegree = nonBarrelFiles.map((f) => {
3841
- const outEdges = this.store.getEdges({ from: f.id });
3842
- return { file: f, outDegree: outEdges.length };
3843
- });
4208
+ return lines;
4209
+ }
4210
+ buildEntryPointLines() {
4211
+ const filesWithOutDegree = this.store.findNodes({ type: "file" }).filter((n) => !n.name.startsWith("index.")).map((f) => ({ file: f, outDegree: this.store.getEdges({ from: f.id }).length }));
3844
4212
  filesWithOutDegree.sort((a, b) => b.outDegree - a.outDegree);
3845
4213
  const entryPoints = filesWithOutDegree.filter((f) => f.outDegree > 0).slice(0, 5);
3846
- if (entryPoints.length > 0) {
3847
- lines.push("## Entry Points", "");
3848
- for (const { file, outDegree } of entryPoints) {
3849
- lines.push(`- ${file.path ?? file.name} (${outDegree} outbound edges)`);
3850
- }
3851
- lines.push("");
4214
+ if (entryPoints.length === 0) return [];
4215
+ const lines = ["## Entry Points", ""];
4216
+ for (const { file, outDegree } of entryPoints) {
4217
+ lines.push(`- ${file.path ?? file.name} (${outDegree} outbound edges)`);
3852
4218
  }
4219
+ lines.push("");
4220
+ return lines;
4221
+ }
4222
+ generateMap() {
4223
+ const lines = ["# Repository Structure", ""];
4224
+ lines.push(...this.buildModuleLines());
4225
+ lines.push(...this.buildEntryPointLines());
3853
4226
  return lines.join("\n");
3854
4227
  }
3855
4228
  /**
@@ -3882,6 +4255,59 @@ var Assembler = class {
3882
4255
  };
3883
4256
 
3884
4257
  // src/query/Traceability.ts
4258
+ function extractConfidence(edge) {
4259
+ return edge.confidence ?? edge.metadata?.confidence ?? 0;
4260
+ }
4261
+ function extractMethod(edge) {
4262
+ return edge.metadata?.method ?? "convention";
4263
+ }
4264
+ function edgesToTracedFiles(store, edges) {
4265
+ return edges.map((edge) => ({
4266
+ path: store.getNode(edge.to)?.path ?? edge.to,
4267
+ confidence: extractConfidence(edge),
4268
+ method: extractMethod(edge)
4269
+ }));
4270
+ }
4271
+ function determineCoverageStatus(hasCode, hasTests) {
4272
+ if (hasCode && hasTests) return "full";
4273
+ if (hasCode) return "code-only";
4274
+ if (hasTests) return "test-only";
4275
+ return "none";
4276
+ }
4277
+ function computeMaxConfidence(codeFiles, testFiles) {
4278
+ const allConfidences = [
4279
+ ...codeFiles.map((f) => f.confidence),
4280
+ ...testFiles.map((f) => f.confidence)
4281
+ ];
4282
+ return allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
4283
+ }
4284
+ function buildRequirementCoverage(store, req) {
4285
+ const codeFiles = edgesToTracedFiles(store, store.getEdges({ from: req.id, type: "requires" }));
4286
+ const testFiles = edgesToTracedFiles(
4287
+ store,
4288
+ store.getEdges({ from: req.id, type: "verified_by" })
4289
+ );
4290
+ const hasCode = codeFiles.length > 0;
4291
+ const hasTests = testFiles.length > 0;
4292
+ return {
4293
+ requirementId: req.id,
4294
+ requirementName: req.name,
4295
+ index: req.metadata?.index ?? 0,
4296
+ codeFiles,
4297
+ testFiles,
4298
+ status: determineCoverageStatus(hasCode, hasTests),
4299
+ maxConfidence: computeMaxConfidence(codeFiles, testFiles)
4300
+ };
4301
+ }
4302
+ function computeSummary(requirements) {
4303
+ const total = requirements.length;
4304
+ const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
4305
+ const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
4306
+ const fullyTraced = requirements.filter((r) => r.status === "full").length;
4307
+ const untraceable = requirements.filter((r) => r.status === "none").length;
4308
+ const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
4309
+ return { total, withCode, withTests, fullyTraced, untraceable, coveragePercent };
4310
+ }
3885
4311
  function queryTraceability(store, options) {
3886
4312
  const allRequirements = store.findNodes({ type: "requirement" });
3887
4313
  const filtered = allRequirements.filter((node) => {
@@ -3909,56 +4335,13 @@ function queryTraceability(store, options) {
3909
4335
  const firstMeta = firstReq.metadata;
3910
4336
  const specPath = firstMeta?.specPath ?? "";
3911
4337
  const featureName = firstMeta?.featureName ?? "";
3912
- const requirements = [];
3913
- for (const req of reqs) {
3914
- const requiresEdges = store.getEdges({ from: req.id, type: "requires" });
3915
- const codeFiles = requiresEdges.map((edge) => {
3916
- const targetNode = store.getNode(edge.to);
3917
- return {
3918
- path: targetNode?.path ?? edge.to,
3919
- confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
3920
- method: edge.metadata?.method ?? "convention"
3921
- };
3922
- });
3923
- const verifiedByEdges = store.getEdges({ from: req.id, type: "verified_by" });
3924
- const testFiles = verifiedByEdges.map((edge) => {
3925
- const targetNode = store.getNode(edge.to);
3926
- return {
3927
- path: targetNode?.path ?? edge.to,
3928
- confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
3929
- method: edge.metadata?.method ?? "convention"
3930
- };
3931
- });
3932
- const hasCode = codeFiles.length > 0;
3933
- const hasTests = testFiles.length > 0;
3934
- const status = hasCode && hasTests ? "full" : hasCode ? "code-only" : hasTests ? "test-only" : "none";
3935
- const allConfidences = [
3936
- ...codeFiles.map((f) => f.confidence),
3937
- ...testFiles.map((f) => f.confidence)
3938
- ];
3939
- const maxConfidence = allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
3940
- requirements.push({
3941
- requirementId: req.id,
3942
- requirementName: req.name,
3943
- index: req.metadata?.index ?? 0,
3944
- codeFiles,
3945
- testFiles,
3946
- status,
3947
- maxConfidence
3948
- });
3949
- }
4338
+ const requirements = reqs.map((req) => buildRequirementCoverage(store, req));
3950
4339
  requirements.sort((a, b) => a.index - b.index);
3951
- const total = requirements.length;
3952
- const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
3953
- const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
3954
- const fullyTraced = requirements.filter((r) => r.status === "full").length;
3955
- const untraceable = requirements.filter((r) => r.status === "none").length;
3956
- const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
3957
4340
  results.push({
3958
4341
  specPath,
3959
4342
  featureName,
3960
4343
  requirements,
3961
- summary: { total, withCode, withTests, fullyTraced, untraceable, coveragePercent }
4344
+ summary: computeSummary(requirements)
3962
4345
  });
3963
4346
  }
3964
4347
  return results;
@@ -3973,10 +4356,15 @@ var GraphConstraintAdapter = class {
3973
4356
  }
3974
4357
  store;
3975
4358
  computeDependencyGraph() {
3976
- const fileNodes = this.store.findNodes({ type: "file" });
3977
- const nodes = fileNodes.map((n) => n.path ?? n.id);
3978
- const importsEdges = this.store.getEdges({ type: "imports" });
3979
- const edges = importsEdges.map((e) => {
4359
+ const nodes = this.collectFileNodePaths();
4360
+ const edges = this.collectImportEdges();
4361
+ return { nodes, edges };
4362
+ }
4363
+ collectFileNodePaths() {
4364
+ return this.store.findNodes({ type: "file" }).map((n) => n.path ?? n.id);
4365
+ }
4366
+ collectImportEdges() {
4367
+ return this.store.getEdges({ type: "imports" }).map((e) => {
3980
4368
  const fromNode = this.store.getNode(e.from);
3981
4369
  const toNode = this.store.getNode(e.to);
3982
4370
  const fromPath = fromNode?.path ?? e.from;
@@ -3985,7 +4373,6 @@ var GraphConstraintAdapter = class {
3985
4373
  const line = e.metadata?.line ?? 0;
3986
4374
  return { from: fromPath, to: toPath, importType, line };
3987
4375
  });
3988
- return { nodes, edges };
3989
4376
  }
3990
4377
  computeLayerViolations(layers, rootDir) {
3991
4378
  const { edges } = this.computeDependencyGraph();
@@ -4279,65 +4666,53 @@ var GraphFeedbackAdapter = class {
4279
4666
  const affectedDocs = [];
4280
4667
  let impactScope = 0;
4281
4668
  for (const filePath of changedFiles) {
4282
- const fileNodes = this.store.findNodes({ path: filePath });
4283
- if (fileNodes.length === 0) continue;
4284
- const fileNode = fileNodes[0];
4285
- const inboundImports = this.store.getEdges({ to: fileNode.id, type: "imports" });
4286
- for (const edge of inboundImports) {
4287
- const importerNode = this.store.getNode(edge.from);
4288
- if (importerNode?.path && /test/i.test(importerNode.path)) {
4289
- affectedTests.push({
4290
- testFile: importerNode.path,
4291
- coversFile: filePath
4292
- });
4293
- }
4294
- impactScope++;
4295
- }
4296
- const docsEdges = this.store.getEdges({ to: fileNode.id, type: "documents" });
4297
- for (const edge of docsEdges) {
4298
- const docNode = this.store.getNode(edge.from);
4299
- if (docNode) {
4300
- affectedDocs.push({
4301
- docFile: docNode.path ?? docNode.name,
4302
- documentsFile: filePath
4303
- });
4304
- }
4305
- }
4669
+ const fileNode = this.store.findNodes({ path: filePath })[0];
4670
+ if (!fileNode) continue;
4671
+ const counts = this.collectFileImpact(fileNode.id, filePath, affectedTests, affectedDocs);
4672
+ impactScope += counts.impactScope;
4306
4673
  }
4307
4674
  return { affectedTests, affectedDocs, impactScope };
4308
4675
  }
4309
- computeHarnessCheckData() {
4310
- const nodeCount = this.store.nodeCount;
4311
- const edgeCount = this.store.edgeCount;
4312
- const violatesEdges = this.store.getEdges({ type: "violates" });
4313
- const constraintViolations = violatesEdges.length;
4314
- const fileNodes = this.store.findNodes({ type: "file" });
4315
- let undocumentedFiles = 0;
4316
- for (const node of fileNodes) {
4317
- const docsEdges = this.store.getEdges({ to: node.id, type: "documents" });
4318
- if (docsEdges.length === 0) {
4319
- undocumentedFiles++;
4676
+ collectFileImpact(fileNodeId, filePath, affectedTests, affectedDocs) {
4677
+ const inboundImports = this.store.getEdges({ to: fileNodeId, type: "imports" });
4678
+ for (const edge of inboundImports) {
4679
+ const importerNode = this.store.getNode(edge.from);
4680
+ if (importerNode?.path && /test/i.test(importerNode.path)) {
4681
+ affectedTests.push({ testFile: importerNode.path, coversFile: filePath });
4320
4682
  }
4321
4683
  }
4322
- let unreachableNodes = 0;
4323
- for (const node of fileNodes) {
4324
- const inboundImports = this.store.getEdges({ to: node.id, type: "imports" });
4325
- if (inboundImports.length === 0) {
4326
- const isEntryPoint = node.name === "index.ts" || node.path !== void 0 && node.path.endsWith("/index.ts") || node.metadata?.entryPoint === true;
4327
- if (!isEntryPoint) {
4328
- unreachableNodes++;
4329
- }
4684
+ const docsEdges = this.store.getEdges({ to: fileNodeId, type: "documents" });
4685
+ for (const edge of docsEdges) {
4686
+ const docNode = this.store.getNode(edge.from);
4687
+ if (docNode) {
4688
+ affectedDocs.push({ docFile: docNode.path ?? docNode.name, documentsFile: filePath });
4330
4689
  }
4331
4690
  }
4691
+ return { impactScope: inboundImports.length };
4692
+ }
4693
+ computeHarnessCheckData() {
4694
+ const fileNodes = this.store.findNodes({ type: "file" });
4332
4695
  return {
4333
4696
  graphExists: true,
4334
- nodeCount,
4335
- edgeCount,
4336
- constraintViolations,
4337
- undocumentedFiles,
4338
- unreachableNodes
4697
+ nodeCount: this.store.nodeCount,
4698
+ edgeCount: this.store.edgeCount,
4699
+ constraintViolations: this.store.getEdges({ type: "violates" }).length,
4700
+ undocumentedFiles: this.countUndocumentedFiles(fileNodes),
4701
+ unreachableNodes: this.countUnreachableNodes(fileNodes)
4339
4702
  };
4340
4703
  }
4704
+ countUndocumentedFiles(fileNodes) {
4705
+ return fileNodes.filter(
4706
+ (node) => this.store.getEdges({ to: node.id, type: "documents" }).length === 0
4707
+ ).length;
4708
+ }
4709
+ countUnreachableNodes(fileNodes) {
4710
+ return fileNodes.filter((node) => {
4711
+ if (this.store.getEdges({ to: node.id, type: "imports" }).length > 0) return false;
4712
+ const isEntryPoint = node.name === "index.ts" || node.path !== void 0 && node.path.endsWith("/index.ts") || node.metadata?.entryPoint === true;
4713
+ return !isEntryPoint;
4714
+ }).length;
4715
+ }
4341
4716
  };
4342
4717
 
4343
4718
  // src/independence/TaskIndependenceAnalyzer.ts
@@ -4354,47 +4729,46 @@ var TaskIndependenceAnalyzer = class {
4354
4729
  this.validate(tasks);
4355
4730
  const useGraph = this.store != null && depth > 0;
4356
4731
  const analysisLevel = useGraph ? "graph-expanded" : "file-only";
4732
+ const { originalFiles, expandedFiles } = this.buildFileSets(tasks, useGraph, depth, edgeTypes);
4733
+ const taskIds = tasks.map((t) => t.id);
4734
+ const pairs = this.computeAllPairs(taskIds, originalFiles, expandedFiles);
4735
+ const groups = this.buildGroups(taskIds, pairs);
4736
+ const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
4737
+ return { tasks: taskIds, analysisLevel, depth, pairs, groups, verdict };
4738
+ }
4739
+ // --- Private methods ---
4740
+ buildFileSets(tasks, useGraph, depth, edgeTypes) {
4357
4741
  const originalFiles = /* @__PURE__ */ new Map();
4358
4742
  const expandedFiles = /* @__PURE__ */ new Map();
4359
4743
  for (const task of tasks) {
4360
- const origSet = new Set(task.files);
4361
- originalFiles.set(task.id, origSet);
4362
- if (useGraph) {
4363
- const expanded = this.expandViaGraph(task.files, depth, edgeTypes);
4364
- expandedFiles.set(task.id, expanded);
4365
- } else {
4366
- expandedFiles.set(task.id, /* @__PURE__ */ new Map());
4367
- }
4744
+ originalFiles.set(task.id, new Set(task.files));
4745
+ expandedFiles.set(
4746
+ task.id,
4747
+ useGraph ? this.expandViaGraph(task.files, depth, edgeTypes) : /* @__PURE__ */ new Map()
4748
+ );
4368
4749
  }
4369
- const taskIds = tasks.map((t) => t.id);
4750
+ return { originalFiles, expandedFiles };
4751
+ }
4752
+ computeAllPairs(taskIds, originalFiles, expandedFiles) {
4370
4753
  const pairs = [];
4371
4754
  for (let i = 0; i < taskIds.length; i++) {
4372
4755
  for (let j = i + 1; j < taskIds.length; j++) {
4373
4756
  const idA = taskIds[i];
4374
4757
  const idB = taskIds[j];
4375
- const pair = this.computePairOverlap(
4376
- idA,
4377
- idB,
4378
- originalFiles.get(idA),
4379
- originalFiles.get(idB),
4380
- expandedFiles.get(idA),
4381
- expandedFiles.get(idB)
4758
+ pairs.push(
4759
+ this.computePairOverlap(
4760
+ idA,
4761
+ idB,
4762
+ originalFiles.get(idA),
4763
+ originalFiles.get(idB),
4764
+ expandedFiles.get(idA),
4765
+ expandedFiles.get(idB)
4766
+ )
4382
4767
  );
4383
- pairs.push(pair);
4384
4768
  }
4385
4769
  }
4386
- const groups = this.buildGroups(taskIds, pairs);
4387
- const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
4388
- return {
4389
- tasks: taskIds,
4390
- analysisLevel,
4391
- depth,
4392
- pairs,
4393
- groups,
4394
- verdict
4395
- };
4770
+ return pairs;
4396
4771
  }
4397
- // --- Private methods ---
4398
4772
  validate(tasks) {
4399
4773
  if (tasks.length < 2) {
4400
4774
  throw new Error("At least 2 tasks are required for independence analysis");
@@ -4547,27 +4921,62 @@ var ConflictPredictor = class {
4547
4921
  predict(params) {
4548
4922
  const analyzer = new TaskIndependenceAnalyzer(this.store);
4549
4923
  const result = analyzer.analyze(params);
4924
+ const { churnMap, couplingMap, churnThreshold, couplingThreshold } = this.buildMetricMaps();
4925
+ const conflicts = this.classifyConflicts(
4926
+ result.pairs,
4927
+ churnMap,
4928
+ couplingMap,
4929
+ churnThreshold,
4930
+ couplingThreshold
4931
+ );
4932
+ const taskIds = result.tasks;
4933
+ const groups = this.buildHighSeverityGroups(taskIds, conflicts);
4934
+ const regrouped = !this.groupsEqual(result.groups, groups);
4935
+ const { highCount, mediumCount, lowCount } = this.countBySeverity(conflicts);
4936
+ const verdict = this.generateVerdict(
4937
+ taskIds,
4938
+ groups,
4939
+ result.analysisLevel,
4940
+ highCount,
4941
+ mediumCount,
4942
+ lowCount,
4943
+ regrouped
4944
+ );
4945
+ return {
4946
+ tasks: taskIds,
4947
+ analysisLevel: result.analysisLevel,
4948
+ depth: result.depth,
4949
+ conflicts,
4950
+ groups,
4951
+ summary: { high: highCount, medium: mediumCount, low: lowCount, regrouped },
4952
+ verdict
4953
+ };
4954
+ }
4955
+ // --- Private helpers ---
4956
+ buildMetricMaps() {
4550
4957
  const churnMap = /* @__PURE__ */ new Map();
4551
4958
  const couplingMap = /* @__PURE__ */ new Map();
4552
- let churnThreshold = Infinity;
4553
- let couplingThreshold = Infinity;
4554
- if (this.store != null) {
4555
- const complexityResult = new GraphComplexityAdapter(this.store).computeComplexityHotspots();
4556
- for (const hotspot of complexityResult.hotspots) {
4557
- const existing = churnMap.get(hotspot.file);
4558
- if (existing === void 0 || hotspot.changeFrequency > existing) {
4559
- churnMap.set(hotspot.file, hotspot.changeFrequency);
4560
- }
4959
+ if (this.store == null) {
4960
+ return { churnMap, couplingMap, churnThreshold: Infinity, couplingThreshold: Infinity };
4961
+ }
4962
+ const complexityResult = new GraphComplexityAdapter(this.store).computeComplexityHotspots();
4963
+ for (const hotspot of complexityResult.hotspots) {
4964
+ const existing = churnMap.get(hotspot.file);
4965
+ if (existing === void 0 || hotspot.changeFrequency > existing) {
4966
+ churnMap.set(hotspot.file, hotspot.changeFrequency);
4561
4967
  }
4562
- const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
4563
- for (const fileData of couplingResult.files) {
4564
- couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
4565
- }
4566
- churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
4567
- couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
4568
4968
  }
4969
+ const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
4970
+ for (const fileData of couplingResult.files) {
4971
+ couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
4972
+ }
4973
+ const churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
4974
+ const couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
4975
+ return { churnMap, couplingMap, churnThreshold, couplingThreshold };
4976
+ }
4977
+ classifyConflicts(pairs, churnMap, couplingMap, churnThreshold, couplingThreshold) {
4569
4978
  const conflicts = [];
4570
- for (const pair of result.pairs) {
4979
+ for (const pair of pairs) {
4571
4980
  if (pair.independent) continue;
4572
4981
  const { severity, reason, mitigation } = this.classifyPair(
4573
4982
  pair.taskA,
@@ -4587,9 +4996,9 @@ var ConflictPredictor = class {
4587
4996
  overlaps: pair.overlaps
4588
4997
  });
4589
4998
  }
4590
- const taskIds = result.tasks;
4591
- const groups = this.buildHighSeverityGroups(taskIds, conflicts);
4592
- const regrouped = !this.groupsEqual(result.groups, groups);
4999
+ return conflicts;
5000
+ }
5001
+ countBySeverity(conflicts) {
4593
5002
  let highCount = 0;
4594
5003
  let mediumCount = 0;
4595
5004
  let lowCount = 0;
@@ -4598,68 +5007,57 @@ var ConflictPredictor = class {
4598
5007
  else if (c.severity === "medium") mediumCount++;
4599
5008
  else lowCount++;
4600
5009
  }
4601
- const verdict = this.generateVerdict(
4602
- taskIds,
4603
- groups,
4604
- result.analysisLevel,
4605
- highCount,
4606
- mediumCount,
4607
- lowCount,
4608
- regrouped
4609
- );
5010
+ return { highCount, mediumCount, lowCount };
5011
+ }
5012
+ classifyTransitiveOverlap(taskA, taskB, overlap, churnMap, couplingMap, churnThreshold, couplingThreshold) {
5013
+ const churn = churnMap.get(overlap.file);
5014
+ const coupling = couplingMap.get(overlap.file);
5015
+ const via = overlap.via ?? "unknown";
5016
+ if (churn !== void 0 && churn >= churnThreshold && churnThreshold !== Infinity) {
5017
+ return {
5018
+ severity: "medium",
5019
+ reason: `Transitive overlap on high-churn file ${overlap.file} (via ${via})`,
5020
+ mitigation: `Review: ${overlap.file} changes frequently \u2014 coordinate edits between ${taskA} and ${taskB}`
5021
+ };
5022
+ }
5023
+ if (coupling !== void 0 && coupling >= couplingThreshold && couplingThreshold !== Infinity) {
5024
+ return {
5025
+ severity: "medium",
5026
+ reason: `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`,
5027
+ mitigation: `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`
5028
+ };
5029
+ }
4610
5030
  return {
4611
- tasks: taskIds,
4612
- analysisLevel: result.analysisLevel,
4613
- depth: result.depth,
4614
- conflicts,
4615
- groups,
4616
- summary: {
4617
- high: highCount,
4618
- medium: mediumCount,
4619
- low: lowCount,
4620
- regrouped
4621
- },
4622
- verdict
5031
+ severity: "low",
5032
+ reason: `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`,
5033
+ mitigation: `Info: transitive overlap unlikely to cause conflicts`
4623
5034
  };
4624
5035
  }
4625
- // --- Private helpers ---
4626
5036
  classifyPair(taskA, taskB, overlaps, churnMap, couplingMap, churnThreshold, couplingThreshold) {
4627
5037
  let maxSeverity = "low";
4628
5038
  let primaryReason = "";
4629
5039
  let primaryMitigation = "";
4630
5040
  for (const overlap of overlaps) {
4631
- let overlapSeverity;
4632
- let reason;
4633
- let mitigation;
4634
- if (overlap.type === "direct") {
4635
- overlapSeverity = "high";
4636
- reason = `Both tasks write to ${overlap.file}`;
4637
- mitigation = `Serialize: run ${taskA} before ${taskB}`;
4638
- } else {
4639
- const churn = churnMap.get(overlap.file);
4640
- const coupling = couplingMap.get(overlap.file);
4641
- const via = overlap.via ?? "unknown";
4642
- if (churn !== void 0 && churn >= churnThreshold && churnThreshold !== Infinity) {
4643
- overlapSeverity = "medium";
4644
- reason = `Transitive overlap on high-churn file ${overlap.file} (via ${via})`;
4645
- mitigation = `Review: ${overlap.file} changes frequently \u2014 coordinate edits between ${taskA} and ${taskB}`;
4646
- } else if (coupling !== void 0 && coupling >= couplingThreshold && couplingThreshold !== Infinity) {
4647
- overlapSeverity = "medium";
4648
- reason = `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`;
4649
- mitigation = `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`;
4650
- } else {
4651
- overlapSeverity = "low";
4652
- reason = `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`;
4653
- mitigation = `Info: transitive overlap unlikely to cause conflicts`;
4654
- }
4655
- }
4656
- if (this.severityRank(overlapSeverity) > this.severityRank(maxSeverity)) {
4657
- maxSeverity = overlapSeverity;
4658
- primaryReason = reason;
4659
- primaryMitigation = mitigation;
5041
+ const classified = overlap.type === "direct" ? {
5042
+ severity: "high",
5043
+ reason: `Both tasks write to ${overlap.file}`,
5044
+ mitigation: `Serialize: run ${taskA} before ${taskB}`
5045
+ } : this.classifyTransitiveOverlap(
5046
+ taskA,
5047
+ taskB,
5048
+ overlap,
5049
+ churnMap,
5050
+ couplingMap,
5051
+ churnThreshold,
5052
+ couplingThreshold
5053
+ );
5054
+ if (this.severityRank(classified.severity) > this.severityRank(maxSeverity)) {
5055
+ maxSeverity = classified.severity;
5056
+ primaryReason = classified.reason;
5057
+ primaryMitigation = classified.mitigation;
4660
5058
  } else if (primaryReason === "") {
4661
- primaryReason = reason;
4662
- primaryMitigation = mitigation;
5059
+ primaryReason = classified.reason;
5060
+ primaryMitigation = classified.mitigation;
4663
5061
  }
4664
5062
  }
4665
5063
  return { severity: maxSeverity, reason: primaryReason, mitigation: primaryMitigation };
@@ -4782,13 +5180,15 @@ var ConflictPredictor = class {
4782
5180
  };
4783
5181
 
4784
5182
  // src/index.ts
4785
- var VERSION = "0.2.0";
5183
+ var VERSION = "0.4.1";
4786
5184
  // Annotate the CommonJS export names for ESM import in node:
4787
5185
  0 && (module.exports = {
4788
5186
  Assembler,
4789
5187
  CIConnector,
4790
5188
  CURRENT_SCHEMA_VERSION,
5189
+ CascadeSimulator,
4791
5190
  CodeIngestor,
5191
+ CompositeProbabilityStrategy,
4792
5192
  ConflictPredictor,
4793
5193
  ConfluenceConnector,
4794
5194
  ContextQL,
@@ -4823,6 +5223,7 @@ var VERSION = "0.2.0";
4823
5223
  VERSION,
4824
5224
  VectorStore,
4825
5225
  askGraph,
5226
+ classifyNodeCategory,
4826
5227
  groupNodesByImpact,
4827
5228
  linkToCode,
4828
5229
  loadGraph,