@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.mjs CHANGED
@@ -162,6 +162,16 @@ function removeFromIndex(index, key, edge) {
162
162
  if (idx !== -1) list.splice(idx, 1);
163
163
  if (list.length === 0) index.delete(key);
164
164
  }
165
+ function filterEdges(candidates, query) {
166
+ const results = [];
167
+ for (const edge of candidates) {
168
+ if (query.from !== void 0 && edge.from !== query.from) continue;
169
+ if (query.to !== void 0 && edge.to !== query.to) continue;
170
+ if (query.type !== void 0 && edge.type !== query.type) continue;
171
+ results.push({ ...edge });
172
+ }
173
+ return results;
174
+ }
165
175
  var GraphStore = class {
166
176
  nodeMap = /* @__PURE__ */ new Map();
167
177
  edgeMap = /* @__PURE__ */ new Map();
@@ -229,44 +239,47 @@ var GraphStore = class {
229
239
  }
230
240
  }
231
241
  getEdges(query) {
232
- let candidates;
233
242
  if (query.from !== void 0 && query.to !== void 0 && query.type !== void 0) {
234
243
  const edge = this.edgeMap.get(edgeKey(query.from, query.to, query.type));
235
244
  return edge ? [{ ...edge }] : [];
236
- } else if (query.from !== void 0) {
237
- candidates = this.edgesByFrom.get(query.from) ?? [];
238
- } else if (query.to !== void 0) {
239
- candidates = this.edgesByTo.get(query.to) ?? [];
240
- } else if (query.type !== void 0) {
241
- candidates = this.edgesByType.get(query.type) ?? [];
242
- } else {
243
- candidates = this.edgeMap.values();
244
245
  }
245
- const results = [];
246
- for (const edge of candidates) {
247
- if (query.from !== void 0 && edge.from !== query.from) continue;
248
- if (query.to !== void 0 && edge.to !== query.to) continue;
249
- if (query.type !== void 0 && edge.type !== query.type) continue;
250
- results.push({ ...edge });
246
+ const candidates = this.selectCandidates(query);
247
+ return filterEdges(candidates, query);
248
+ }
249
+ /** Pick the most selective index to start from. */
250
+ selectCandidates(query) {
251
+ if (query.from !== void 0) {
252
+ return this.edgesByFrom.get(query.from) ?? [];
251
253
  }
252
- return results;
254
+ if (query.to !== void 0) {
255
+ return this.edgesByTo.get(query.to) ?? [];
256
+ }
257
+ if (query.type !== void 0) {
258
+ return this.edgesByType.get(query.type) ?? [];
259
+ }
260
+ return this.edgeMap.values();
253
261
  }
254
262
  getNeighbors(nodeId, direction = "both") {
255
- const neighborIds = /* @__PURE__ */ new Set();
263
+ const neighborIds = this.collectNeighborIds(nodeId, direction);
264
+ return this.resolveNodes(neighborIds);
265
+ }
266
+ collectNeighborIds(nodeId, direction) {
267
+ const ids = /* @__PURE__ */ new Set();
256
268
  if (direction === "outbound" || direction === "both") {
257
- const outEdges = this.edgesByFrom.get(nodeId) ?? [];
258
- for (const edge of outEdges) {
259
- neighborIds.add(edge.to);
269
+ for (const edge of this.edgesByFrom.get(nodeId) ?? []) {
270
+ ids.add(edge.to);
260
271
  }
261
272
  }
262
273
  if (direction === "inbound" || direction === "both") {
263
- const inEdges = this.edgesByTo.get(nodeId) ?? [];
264
- for (const edge of inEdges) {
265
- neighborIds.add(edge.from);
274
+ for (const edge of this.edgesByTo.get(nodeId) ?? []) {
275
+ ids.add(edge.from);
266
276
  }
267
277
  }
278
+ return ids;
279
+ }
280
+ resolveNodes(ids) {
268
281
  const results = [];
269
- for (const nid of neighborIds) {
282
+ for (const nid of ids) {
270
283
  const node = this.getNode(nid);
271
284
  if (node) results.push(node);
272
285
  }
@@ -545,6 +558,12 @@ var CODE_TYPES = /* @__PURE__ */ new Set([
545
558
  "method",
546
559
  "variable"
547
560
  ]);
561
+ function classifyNodeCategory(node) {
562
+ if (TEST_TYPES.has(node.type)) return "tests";
563
+ if (DOC_TYPES.has(node.type)) return "docs";
564
+ if (CODE_TYPES.has(node.type)) return "code";
565
+ return "other";
566
+ }
548
567
  function groupNodesByImpact(nodes, excludeId) {
549
568
  const tests = [];
550
569
  const docs = [];
@@ -552,15 +571,11 @@ function groupNodesByImpact(nodes, excludeId) {
552
571
  const other = [];
553
572
  for (const node of nodes) {
554
573
  if (excludeId && node.id === excludeId) continue;
555
- if (TEST_TYPES.has(node.type)) {
556
- tests.push(node);
557
- } else if (DOC_TYPES.has(node.type)) {
558
- docs.push(node);
559
- } else if (CODE_TYPES.has(node.type)) {
560
- code.push(node);
561
- } else {
562
- other.push(node);
563
- }
574
+ const category = classifyNodeCategory(node);
575
+ if (category === "tests") tests.push(node);
576
+ else if (category === "docs") docs.push(node);
577
+ else if (category === "code") code.push(node);
578
+ else other.push(node);
564
579
  }
565
580
  return { tests, docs, code, other };
566
581
  }
@@ -1011,6 +1026,17 @@ var CodeIngestor = class {
1011
1026
  import { execFile } from "child_process";
1012
1027
  import { promisify } from "util";
1013
1028
  var execFileAsync = promisify(execFile);
1029
+ function finalizeCommit(current) {
1030
+ return {
1031
+ hash: current.hash,
1032
+ shortHash: current.shortHash,
1033
+ author: current.author,
1034
+ email: current.email,
1035
+ date: current.date,
1036
+ message: current.message,
1037
+ files: current.files
1038
+ };
1039
+ }
1014
1040
  var GitIngestor = class {
1015
1041
  constructor(store, gitRunner) {
1016
1042
  this.store = store;
@@ -1047,39 +1073,49 @@ var GitIngestor = class {
1047
1073
  }
1048
1074
  const commits = this.parseGitLog(output);
1049
1075
  for (const commit of commits) {
1050
- const nodeId = `commit:${commit.shortHash}`;
1051
- this.store.addNode({
1052
- id: nodeId,
1053
- type: "commit",
1054
- name: commit.message,
1055
- metadata: {
1056
- author: commit.author,
1057
- email: commit.email,
1058
- date: commit.date,
1059
- hash: commit.hash
1060
- }
1061
- });
1062
- nodesAdded++;
1063
- for (const file of commit.files) {
1064
- const fileNodeId = `file:${file}`;
1065
- const existingNode = this.store.getNode(fileNodeId);
1066
- if (existingNode) {
1067
- this.store.addEdge({
1068
- from: fileNodeId,
1069
- to: nodeId,
1070
- type: "triggered_by"
1071
- });
1072
- edgesAdded++;
1073
- }
1076
+ const counts = this.ingestCommit(commit);
1077
+ nodesAdded += counts.nodesAdded;
1078
+ edgesAdded += counts.edgesAdded;
1079
+ }
1080
+ edgesAdded += this.ingestCoChanges(commits);
1081
+ return {
1082
+ nodesAdded,
1083
+ nodesUpdated,
1084
+ edgesAdded,
1085
+ edgesUpdated,
1086
+ errors,
1087
+ durationMs: Date.now() - start
1088
+ };
1089
+ }
1090
+ ingestCommit(commit) {
1091
+ const nodeId = `commit:${commit.shortHash}`;
1092
+ this.store.addNode({
1093
+ id: nodeId,
1094
+ type: "commit",
1095
+ name: commit.message,
1096
+ metadata: {
1097
+ author: commit.author,
1098
+ email: commit.email,
1099
+ date: commit.date,
1100
+ hash: commit.hash
1101
+ }
1102
+ });
1103
+ let edgesAdded = 0;
1104
+ for (const file of commit.files) {
1105
+ const fileNodeId = `file:${file}`;
1106
+ if (this.store.getNode(fileNodeId)) {
1107
+ this.store.addEdge({ from: fileNodeId, to: nodeId, type: "triggered_by" });
1108
+ edgesAdded++;
1074
1109
  }
1075
1110
  }
1076
- const coChanges = this.computeCoChanges(commits);
1077
- for (const { fileA, fileB, count } of coChanges) {
1111
+ return { nodesAdded: 1, edgesAdded };
1112
+ }
1113
+ ingestCoChanges(commits) {
1114
+ let edgesAdded = 0;
1115
+ for (const { fileA, fileB, count } of this.computeCoChanges(commits)) {
1078
1116
  const fileAId = `file:${fileA}`;
1079
1117
  const fileBId = `file:${fileB}`;
1080
- const nodeA = this.store.getNode(fileAId);
1081
- const nodeB = this.store.getNode(fileBId);
1082
- if (nodeA && nodeB) {
1118
+ if (this.store.getNode(fileAId) && this.store.getNode(fileBId)) {
1083
1119
  this.store.addEdge({
1084
1120
  from: fileAId,
1085
1121
  to: fileBId,
@@ -1089,14 +1125,7 @@ var GitIngestor = class {
1089
1125
  edgesAdded++;
1090
1126
  }
1091
1127
  }
1092
- return {
1093
- nodesAdded,
1094
- nodesUpdated,
1095
- edgesAdded,
1096
- edgesUpdated,
1097
- errors,
1098
- durationMs: Date.now() - start
1099
- };
1128
+ return edgesAdded;
1100
1129
  }
1101
1130
  async runGit(rootDir, args) {
1102
1131
  if (this.gitRunner) {
@@ -1111,63 +1140,49 @@ var GitIngestor = class {
1111
1140
  const lines = output.split("\n");
1112
1141
  let current = null;
1113
1142
  for (const line of lines) {
1114
- const trimmed = line.trim();
1115
- if (!trimmed) {
1116
- if (current && current.hasFiles) {
1117
- commits.push({
1118
- hash: current.hash,
1119
- shortHash: current.shortHash,
1120
- author: current.author,
1121
- email: current.email,
1122
- date: current.date,
1123
- message: current.message,
1124
- files: current.files
1125
- });
1126
- current = null;
1127
- }
1128
- continue;
1143
+ current = this.processLogLine(line, current, commits);
1144
+ }
1145
+ if (current) {
1146
+ commits.push(finalizeCommit(current));
1147
+ }
1148
+ return commits;
1149
+ }
1150
+ /**
1151
+ * Process one line from git log output, updating the in-progress commit builder
1152
+ * and flushing completed commits into the accumulator.
1153
+ * Returns the updated current builder (null if flushed and not replaced).
1154
+ */
1155
+ processLogLine(line, current, commits) {
1156
+ const trimmed = line.trim();
1157
+ if (!trimmed) {
1158
+ if (current?.hasFiles) {
1159
+ commits.push(finalizeCommit(current));
1160
+ return null;
1129
1161
  }
1130
- const parts = trimmed.split("|");
1131
- if (parts.length >= 5 && /^[0-9a-f]{7,40}$/.test(parts[0])) {
1132
- if (current) {
1133
- commits.push({
1134
- hash: current.hash,
1135
- shortHash: current.shortHash,
1136
- author: current.author,
1137
- email: current.email,
1138
- date: current.date,
1139
- message: current.message,
1140
- files: current.files
1141
- });
1142
- }
1143
- current = {
1144
- hash: parts[0],
1145
- shortHash: parts[0].substring(0, 7),
1146
- author: parts[1],
1147
- email: parts[2],
1148
- date: parts[3],
1149
- message: parts.slice(4).join("|"),
1150
- // message may contain |
1151
- files: [],
1152
- hasFiles: false
1153
- };
1154
- } else if (current) {
1155
- current.files.push(trimmed);
1156
- current.hasFiles = true;
1162
+ return current;
1163
+ }
1164
+ const parts = trimmed.split("|");
1165
+ if (parts.length >= 5 && /^[0-9a-f]{7,40}$/.test(parts[0])) {
1166
+ if (current) {
1167
+ commits.push(finalizeCommit(current));
1157
1168
  }
1169
+ return {
1170
+ hash: parts[0],
1171
+ shortHash: parts[0].substring(0, 7),
1172
+ author: parts[1],
1173
+ email: parts[2],
1174
+ date: parts[3],
1175
+ message: parts.slice(4).join("|"),
1176
+ // message may contain |
1177
+ files: [],
1178
+ hasFiles: false
1179
+ };
1158
1180
  }
1159
1181
  if (current) {
1160
- commits.push({
1161
- hash: current.hash,
1162
- shortHash: current.shortHash,
1163
- author: current.author,
1164
- email: current.email,
1165
- date: current.date,
1166
- message: current.message,
1167
- files: current.files
1168
- });
1182
+ current.files.push(trimmed);
1183
+ current.hasFiles = true;
1169
1184
  }
1170
- return commits;
1185
+ return current;
1171
1186
  }
1172
1187
  computeCoChanges(commits) {
1173
1188
  const pairCounts = /* @__PURE__ */ new Map();
@@ -1311,50 +1326,25 @@ var KnowledgeIngestor = class {
1311
1326
  try {
1312
1327
  const content = await fs2.readFile(filePath, "utf-8");
1313
1328
  const filename = path3.basename(filePath, ".md");
1314
- const titleMatch = content.match(/^#\s+(.+)$/m);
1315
- const title = titleMatch ? titleMatch[1].trim() : filename;
1316
- const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
1317
- const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
1318
- const date = dateMatch ? dateMatch[1].trim() : void 0;
1319
- const status = statusMatch ? statusMatch[1].trim() : void 0;
1320
1329
  const nodeId = `adr:${filename}`;
1321
- this.store.addNode({
1322
- id: nodeId,
1323
- type: "adr",
1324
- name: title,
1325
- path: filePath,
1326
- metadata: { date, status }
1327
- });
1330
+ this.store.addNode(parseADRNode(nodeId, filePath, filename, content));
1328
1331
  nodesAdded++;
1329
1332
  edgesAdded += this.linkToCode(content, nodeId, "documents");
1330
1333
  } catch (err) {
1331
1334
  errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
1332
1335
  }
1333
1336
  }
1334
- return {
1335
- nodesAdded,
1336
- nodesUpdated: 0,
1337
- edgesAdded,
1338
- edgesUpdated: 0,
1339
- errors,
1340
- durationMs: Date.now() - start
1341
- };
1337
+ return buildResult(nodesAdded, edgesAdded, errors, start);
1342
1338
  }
1343
1339
  async ingestLearnings(projectPath) {
1344
1340
  const start = Date.now();
1345
1341
  const filePath = path3.join(projectPath, ".harness", "learnings.md");
1346
- let content;
1347
- try {
1348
- content = await fs2.readFile(filePath, "utf-8");
1349
- } catch {
1350
- return emptyResult(Date.now() - start);
1351
- }
1352
- const errors = [];
1342
+ const content = await readFileOrEmpty(filePath);
1343
+ if (content === null) return emptyResult(Date.now() - start);
1353
1344
  let nodesAdded = 0;
1354
1345
  let edgesAdded = 0;
1355
- const lines = content.split("\n");
1356
1346
  let currentDate;
1357
- for (const line of lines) {
1347
+ for (const line of content.split("\n")) {
1358
1348
  const headingMatch = line.match(/^##\s+(\S+)/);
1359
1349
  if (headingMatch) {
1360
1350
  currentDate = headingMatch[1];
@@ -1363,70 +1353,29 @@ var KnowledgeIngestor = class {
1363
1353
  const bulletMatch = line.match(/^-\s+(.+)/);
1364
1354
  if (!bulletMatch) continue;
1365
1355
  const text = bulletMatch[1];
1366
- const skillMatch = text.match(/\[skill:([^\]]+)\]/);
1367
- const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
1368
- const skill = skillMatch ? skillMatch[1] : void 0;
1369
- const outcome = outcomeMatch ? outcomeMatch[1] : void 0;
1370
1356
  const nodeId = `learning:${hash(text)}`;
1371
- this.store.addNode({
1372
- id: nodeId,
1373
- type: "learning",
1374
- name: text,
1375
- metadata: { skill, outcome, date: currentDate }
1376
- });
1357
+ this.store.addNode(parseLearningNode(nodeId, text, currentDate));
1377
1358
  nodesAdded++;
1378
1359
  edgesAdded += this.linkToCode(text, nodeId, "applies_to");
1379
1360
  }
1380
- return {
1381
- nodesAdded,
1382
- nodesUpdated: 0,
1383
- edgesAdded,
1384
- edgesUpdated: 0,
1385
- errors,
1386
- durationMs: Date.now() - start
1387
- };
1361
+ return buildResult(nodesAdded, edgesAdded, [], start);
1388
1362
  }
1389
1363
  async ingestFailures(projectPath) {
1390
1364
  const start = Date.now();
1391
1365
  const filePath = path3.join(projectPath, ".harness", "failures.md");
1392
- let content;
1393
- try {
1394
- content = await fs2.readFile(filePath, "utf-8");
1395
- } catch {
1396
- return emptyResult(Date.now() - start);
1397
- }
1398
- const errors = [];
1366
+ const content = await readFileOrEmpty(filePath);
1367
+ if (content === null) return emptyResult(Date.now() - start);
1399
1368
  let nodesAdded = 0;
1400
1369
  let edgesAdded = 0;
1401
- const sections = content.split(/^##\s+/m).filter((s) => s.trim());
1402
- for (const section of sections) {
1403
- const dateMatch = section.match(/\*\*Date:\*\*\s*(.+)/);
1404
- const skillMatch = section.match(/\*\*Skill:\*\*\s*(.+)/);
1405
- const typeMatch = section.match(/\*\*Type:\*\*\s*(.+)/);
1406
- const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
1407
- const date = dateMatch ? dateMatch[1].trim() : void 0;
1408
- const skill = skillMatch ? skillMatch[1].trim() : void 0;
1409
- const failureType = typeMatch ? typeMatch[1].trim() : void 0;
1410
- const description = descMatch ? descMatch[1].trim() : void 0;
1411
- if (!description) continue;
1412
- const nodeId = `failure:${hash(description)}`;
1413
- this.store.addNode({
1414
- id: nodeId,
1415
- type: "failure",
1416
- name: description,
1417
- metadata: { date, skill, type: failureType }
1418
- });
1370
+ for (const section of content.split(/^##\s+/m).filter((s) => s.trim())) {
1371
+ const parsed = parseFailureSection(section);
1372
+ if (!parsed) continue;
1373
+ const { description, node } = parsed;
1374
+ this.store.addNode(node);
1419
1375
  nodesAdded++;
1420
- edgesAdded += this.linkToCode(description, nodeId, "caused_by");
1376
+ edgesAdded += this.linkToCode(description, node.id, "caused_by");
1421
1377
  }
1422
- return {
1423
- nodesAdded,
1424
- nodesUpdated: 0,
1425
- edgesAdded,
1426
- edgesUpdated: 0,
1427
- errors,
1428
- durationMs: Date.now() - start
1429
- };
1378
+ return buildResult(nodesAdded, edgesAdded, [], start);
1430
1379
  }
1431
1380
  async ingestAll(projectPath, opts) {
1432
1381
  const start = Date.now();
@@ -1480,6 +1429,74 @@ var KnowledgeIngestor = class {
1480
1429
  return results;
1481
1430
  }
1482
1431
  };
1432
+ async function readFileOrEmpty(filePath) {
1433
+ try {
1434
+ return await fs2.readFile(filePath, "utf-8");
1435
+ } catch {
1436
+ return null;
1437
+ }
1438
+ }
1439
+ function buildResult(nodesAdded, edgesAdded, errors, start) {
1440
+ return {
1441
+ nodesAdded,
1442
+ nodesUpdated: 0,
1443
+ edgesAdded,
1444
+ edgesUpdated: 0,
1445
+ errors,
1446
+ durationMs: Date.now() - start
1447
+ };
1448
+ }
1449
+ function parseADRNode(nodeId, filePath, filename, content) {
1450
+ const titleMatch = content.match(/^#\s+(.+)$/m);
1451
+ const title = titleMatch ? titleMatch[1].trim() : filename;
1452
+ const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
1453
+ const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
1454
+ return {
1455
+ id: nodeId,
1456
+ type: "adr",
1457
+ name: title,
1458
+ path: filePath,
1459
+ metadata: {
1460
+ date: dateMatch ? dateMatch[1].trim() : void 0,
1461
+ status: statusMatch ? statusMatch[1].trim() : void 0
1462
+ }
1463
+ };
1464
+ }
1465
+ function parseLearningNode(nodeId, text, currentDate) {
1466
+ const skillMatch = text.match(/\[skill:([^\]]+)\]/);
1467
+ const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
1468
+ return {
1469
+ id: nodeId,
1470
+ type: "learning",
1471
+ name: text,
1472
+ metadata: {
1473
+ skill: skillMatch ? skillMatch[1] : void 0,
1474
+ outcome: outcomeMatch ? outcomeMatch[1] : void 0,
1475
+ date: currentDate
1476
+ }
1477
+ };
1478
+ }
1479
+ function parseFailureSection(section) {
1480
+ const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
1481
+ const description = descMatch ? descMatch[1].trim() : void 0;
1482
+ if (!description) return null;
1483
+ const dateMatch = section.match(/\*\*Date:\*\*\s*(.+)/);
1484
+ const skillMatch = section.match(/\*\*Skill:\*\*\s*(.+)/);
1485
+ const typeMatch = section.match(/\*\*Type:\*\*\s*(.+)/);
1486
+ return {
1487
+ description,
1488
+ node: {
1489
+ id: `failure:${hash(description)}`,
1490
+ type: "failure",
1491
+ name: description,
1492
+ metadata: {
1493
+ date: dateMatch ? dateMatch[1].trim() : void 0,
1494
+ skill: skillMatch ? skillMatch[1].trim() : void 0,
1495
+ type: typeMatch ? typeMatch[1].trim() : void 0
1496
+ }
1497
+ }
1498
+ };
1499
+ }
1483
1500
 
1484
1501
  // src/ingest/RequirementIngestor.ts
1485
1502
  import * as fs3 from "fs/promises";
@@ -1524,40 +1541,9 @@ var RequirementIngestor = class {
1524
1541
  return emptyResult(Date.now() - start);
1525
1542
  }
1526
1543
  for (const featureDir of featureDirs) {
1527
- const featureName = path4.basename(featureDir);
1528
- const specPath = path4.join(featureDir, "proposal.md");
1529
- let content;
1530
- try {
1531
- content = await fs3.readFile(specPath, "utf-8");
1532
- } catch {
1533
- continue;
1534
- }
1535
- try {
1536
- const specHash = hash(specPath);
1537
- const specNodeId = `file:${specPath}`;
1538
- this.store.addNode({
1539
- id: specNodeId,
1540
- type: "document",
1541
- name: path4.basename(specPath),
1542
- path: specPath,
1543
- metadata: { featureName }
1544
- });
1545
- const requirements = this.extractRequirements(content, specPath, specHash, featureName);
1546
- for (const req of requirements) {
1547
- this.store.addNode(req.node);
1548
- nodesAdded++;
1549
- this.store.addEdge({
1550
- from: req.node.id,
1551
- to: specNodeId,
1552
- type: "specifies"
1553
- });
1554
- edgesAdded++;
1555
- edgesAdded += this.linkByPathPattern(req.node.id, featureName);
1556
- edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
1557
- }
1558
- } catch (err) {
1559
- errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
1560
- }
1544
+ const counts = await this.ingestFeatureDir(featureDir, errors);
1545
+ nodesAdded += counts.nodesAdded;
1546
+ edgesAdded += counts.edgesAdded;
1561
1547
  }
1562
1548
  return {
1563
1549
  nodesAdded,
@@ -1568,6 +1554,48 @@ var RequirementIngestor = class {
1568
1554
  durationMs: Date.now() - start
1569
1555
  };
1570
1556
  }
1557
+ async ingestFeatureDir(featureDir, errors) {
1558
+ const featureName = path4.basename(featureDir);
1559
+ const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
1560
+ let content;
1561
+ try {
1562
+ content = await fs3.readFile(specPath, "utf-8");
1563
+ } catch {
1564
+ return { nodesAdded: 0, edgesAdded: 0 };
1565
+ }
1566
+ try {
1567
+ return this.ingestSpec(specPath, content, featureName);
1568
+ } catch (err) {
1569
+ errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
1570
+ return { nodesAdded: 0, edgesAdded: 0 };
1571
+ }
1572
+ }
1573
+ ingestSpec(specPath, content, featureName) {
1574
+ const specHash = hash(specPath);
1575
+ const specNodeId = `file:${specPath}`;
1576
+ this.store.addNode({
1577
+ id: specNodeId,
1578
+ type: "document",
1579
+ name: path4.basename(specPath),
1580
+ path: specPath,
1581
+ metadata: { featureName }
1582
+ });
1583
+ const requirements = this.extractRequirements(content, specPath, specHash, featureName);
1584
+ let nodesAdded = 0;
1585
+ let edgesAdded = 0;
1586
+ for (const req of requirements) {
1587
+ const counts = this.ingestRequirement(req.node, specNodeId, featureName);
1588
+ nodesAdded += counts.nodesAdded;
1589
+ edgesAdded += counts.edgesAdded;
1590
+ }
1591
+ return { nodesAdded, edgesAdded };
1592
+ }
1593
+ ingestRequirement(node, specNodeId, featureName) {
1594
+ this.store.addNode(node);
1595
+ this.store.addEdge({ from: node.id, to: specNodeId, type: "specifies" });
1596
+ const edgesAdded = 1 + this.linkByPathPattern(node.id, featureName) + this.linkByKeywordOverlap(node.id, node.name);
1597
+ return { nodesAdded: 1, edgesAdded };
1598
+ }
1571
1599
  /**
1572
1600
  * Parse markdown content and extract numbered items from recognized sections.
1573
1601
  */
@@ -1579,54 +1607,80 @@ var RequirementIngestor = class {
1579
1607
  let globalIndex = 0;
1580
1608
  for (let i = 0; i < lines.length; i++) {
1581
1609
  const line = lines[i];
1582
- const headingMatch = line.match(SECTION_HEADING_RE);
1583
- if (headingMatch) {
1584
- const heading = headingMatch[1].trim();
1585
- const isReqSection = REQUIREMENT_SECTIONS.some(
1586
- (s) => heading.toLowerCase() === s.toLowerCase()
1587
- );
1588
- if (isReqSection) {
1589
- currentSection = heading;
1590
- inRequirementSection = true;
1591
- } else {
1592
- inRequirementSection = false;
1610
+ const sectionResult = this.processHeadingLine(line, inRequirementSection);
1611
+ if (sectionResult !== null) {
1612
+ inRequirementSection = sectionResult.inRequirementSection;
1613
+ if (sectionResult.currentSection !== void 0) {
1614
+ currentSection = sectionResult.currentSection;
1593
1615
  }
1594
1616
  continue;
1595
1617
  }
1596
1618
  if (!inRequirementSection) continue;
1597
1619
  const itemMatch = line.match(NUMBERED_ITEM_RE);
1598
1620
  if (!itemMatch) continue;
1599
- const index = parseInt(itemMatch[1], 10);
1600
- const text = itemMatch[2].trim();
1601
- const rawText = line.trim();
1602
- const lineNumber = i + 1;
1603
1621
  globalIndex++;
1604
- const nodeId = `req:${specHash}:${globalIndex}`;
1605
- const earsPattern = detectEarsPattern(text);
1606
- results.push({
1607
- node: {
1608
- id: nodeId,
1609
- type: "requirement",
1610
- name: text,
1611
- path: specPath,
1612
- location: {
1613
- fileId: `file:${specPath}`,
1614
- startLine: lineNumber,
1615
- endLine: lineNumber
1616
- },
1617
- metadata: {
1618
- specPath,
1619
- index,
1620
- section: currentSection,
1621
- rawText,
1622
- earsPattern,
1623
- featureName
1624
- }
1625
- }
1626
- });
1622
+ results.push(
1623
+ this.buildRequirementNode(
1624
+ line,
1625
+ itemMatch,
1626
+ i + 1,
1627
+ specPath,
1628
+ specHash,
1629
+ globalIndex,
1630
+ featureName,
1631
+ currentSection
1632
+ )
1633
+ );
1627
1634
  }
1628
1635
  return results;
1629
1636
  }
1637
+ /**
1638
+ * Check if a line is a section heading and return updated section state,
1639
+ * or return null if the line is not a heading.
1640
+ */
1641
+ processHeadingLine(line, _inRequirementSection) {
1642
+ const headingMatch = line.match(SECTION_HEADING_RE);
1643
+ if (!headingMatch) return null;
1644
+ const heading = headingMatch[1].trim();
1645
+ const isReqSection = REQUIREMENT_SECTIONS.some(
1646
+ (s) => heading.toLowerCase() === s.toLowerCase()
1647
+ );
1648
+ if (isReqSection) {
1649
+ return { inRequirementSection: true, currentSection: heading };
1650
+ }
1651
+ return { inRequirementSection: false };
1652
+ }
1653
+ /**
1654
+ * Build a requirement GraphNode from a matched numbered-item line.
1655
+ */
1656
+ buildRequirementNode(line, itemMatch, lineNumber, specPath, specHash, globalIndex, featureName, currentSection) {
1657
+ const index = parseInt(itemMatch[1], 10);
1658
+ const text = itemMatch[2].trim();
1659
+ const rawText = line.trim();
1660
+ const nodeId = `req:${specHash}:${globalIndex}`;
1661
+ const earsPattern = detectEarsPattern(text);
1662
+ return {
1663
+ node: {
1664
+ id: nodeId,
1665
+ type: "requirement",
1666
+ name: text,
1667
+ path: specPath,
1668
+ location: {
1669
+ fileId: `file:${specPath}`,
1670
+ startLine: lineNumber,
1671
+ endLine: lineNumber
1672
+ },
1673
+ metadata: {
1674
+ specPath,
1675
+ index,
1676
+ section: currentSection,
1677
+ rawText,
1678
+ earsPattern,
1679
+ featureName
1680
+ }
1681
+ }
1682
+ };
1683
+ }
1630
1684
  /**
1631
1685
  * Convention-based linking: match requirement to code/test files
1632
1686
  * by feature name in their path.
@@ -1675,7 +1729,7 @@ var RequirementIngestor = class {
1675
1729
  const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1676
1730
  const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
1677
1731
  if (namePattern.test(reqText)) {
1678
- const edgeType = node.path && node.path.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
1732
+ const edgeType = node.path?.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
1679
1733
  this.store.addEdge({
1680
1734
  from: reqId,
1681
1735
  to: node.id,
@@ -1830,15 +1884,18 @@ function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
1830
1884
  durationMs: Date.now() - start
1831
1885
  };
1832
1886
  }
1887
+ function appendJqlClause(jql, clause) {
1888
+ return jql ? `${jql} AND ${clause}` : clause;
1889
+ }
1833
1890
  function buildJql(config) {
1834
1891
  const project2 = config.project;
1835
1892
  let jql = project2 ? `project=${project2}` : "";
1836
1893
  const filters = config.filters;
1837
1894
  if (filters?.status?.length) {
1838
- jql += `${jql ? " AND " : ""}status IN (${filters.status.map((s) => `"${s}"`).join(",")})`;
1895
+ jql = appendJqlClause(jql, `status IN (${filters.status.map((s) => `"${s}"`).join(",")})`);
1839
1896
  }
1840
1897
  if (filters?.labels?.length) {
1841
- jql += `${jql ? " AND " : ""}labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`;
1898
+ jql = appendJqlClause(jql, `labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`);
1842
1899
  }
1843
1900
  return jql;
1844
1901
  }
@@ -1851,8 +1908,6 @@ var JiraConnector = class {
1851
1908
  }
1852
1909
  async ingest(store, config) {
1853
1910
  const start = Date.now();
1854
- let nodesAdded = 0;
1855
- let edgesAdded = 0;
1856
1911
  const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
1857
1912
  const apiKey = process.env[apiKeyEnv];
1858
1913
  if (!apiKey) {
@@ -1874,38 +1929,39 @@ var JiraConnector = class {
1874
1929
  );
1875
1930
  }
1876
1931
  const jql = buildJql(config);
1877
- const headers = {
1878
- Authorization: `Basic ${apiKey}`,
1879
- "Content-Type": "application/json"
1880
- };
1932
+ const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
1881
1933
  try {
1882
- let startAt = 0;
1883
- const maxResults = 50;
1884
- let total = Infinity;
1885
- while (startAt < total) {
1886
- const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
1887
- const response = await this.httpClient(url, { headers });
1888
- if (!response.ok) {
1889
- return buildIngestResult(nodesAdded, edgesAdded, ["Jira API request failed"], start);
1890
- }
1891
- const data = await response.json();
1892
- total = data.total;
1893
- for (const issue of data.issues) {
1894
- const counts = this.processIssue(store, issue);
1895
- nodesAdded += counts.nodesAdded;
1896
- edgesAdded += counts.edgesAdded;
1897
- }
1898
- startAt += maxResults;
1899
- }
1934
+ const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
1935
+ return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
1900
1936
  } catch (err) {
1901
1937
  return buildIngestResult(
1902
- nodesAdded,
1903
- edgesAdded,
1938
+ 0,
1939
+ 0,
1904
1940
  [`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
1905
1941
  start
1906
1942
  );
1907
1943
  }
1908
- return buildIngestResult(nodesAdded, edgesAdded, [], start);
1944
+ }
1945
+ async fetchAllIssues(store, baseUrl, jql, headers) {
1946
+ let nodesAdded = 0;
1947
+ let edgesAdded = 0;
1948
+ let startAt = 0;
1949
+ const maxResults = 50;
1950
+ let total = Infinity;
1951
+ while (startAt < total) {
1952
+ const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
1953
+ const response = await this.httpClient(url, { headers });
1954
+ if (!response.ok) throw new Error("Jira API request failed");
1955
+ const data = await response.json();
1956
+ total = data.total;
1957
+ for (const issue of data.issues) {
1958
+ const counts = this.processIssue(store, issue);
1959
+ nodesAdded += counts.nodesAdded;
1960
+ edgesAdded += counts.edgesAdded;
1961
+ }
1962
+ startAt += maxResults;
1963
+ }
1964
+ return { nodesAdded, edgesAdded };
1909
1965
  }
1910
1966
  processIssue(store, issue) {
1911
1967
  const nodeId = `issue:jira:${issue.key}`;
@@ -2026,6 +2082,16 @@ var SlackConnector = class {
2026
2082
  };
2027
2083
 
2028
2084
  // src/ingest/connectors/ConfluenceConnector.ts
2085
+ function missingApiKeyResult(envVar, start) {
2086
+ return {
2087
+ nodesAdded: 0,
2088
+ nodesUpdated: 0,
2089
+ edgesAdded: 0,
2090
+ edgesUpdated: 0,
2091
+ errors: [`Missing API key: environment variable "${envVar}" is not set`],
2092
+ durationMs: Date.now() - start
2093
+ };
2094
+ }
2029
2095
  var ConfluenceConnector = class {
2030
2096
  name = "confluence";
2031
2097
  source = "confluence";
@@ -2036,40 +2102,34 @@ var ConfluenceConnector = class {
2036
2102
  async ingest(store, config) {
2037
2103
  const start = Date.now();
2038
2104
  const errors = [];
2039
- let nodesAdded = 0;
2040
- let edgesAdded = 0;
2041
2105
  const apiKeyEnv = config.apiKeyEnv ?? "CONFLUENCE_API_KEY";
2042
2106
  const apiKey = process.env[apiKeyEnv];
2043
2107
  if (!apiKey) {
2044
- return {
2045
- nodesAdded: 0,
2046
- nodesUpdated: 0,
2047
- edgesAdded: 0,
2048
- edgesUpdated: 0,
2049
- errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
2050
- durationMs: Date.now() - start
2051
- };
2108
+ return missingApiKeyResult(apiKeyEnv, start);
2052
2109
  }
2053
2110
  const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
2054
2111
  const baseUrl = process.env[baseUrlEnv] ?? "";
2055
2112
  const spaceKey = config.spaceKey ?? "";
2056
- try {
2057
- const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
2058
- nodesAdded = result.nodesAdded;
2059
- edgesAdded = result.edgesAdded;
2060
- errors.push(...result.errors);
2061
- } catch (err) {
2062
- errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
2063
- }
2113
+ const counts = await this.fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors);
2064
2114
  return {
2065
- nodesAdded,
2115
+ nodesAdded: counts.nodesAdded,
2066
2116
  nodesUpdated: 0,
2067
- edgesAdded,
2117
+ edgesAdded: counts.edgesAdded,
2068
2118
  edgesUpdated: 0,
2069
2119
  errors,
2070
2120
  durationMs: Date.now() - start
2071
2121
  };
2072
2122
  }
2123
+ async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
2124
+ try {
2125
+ const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
2126
+ errors.push(...result.errors);
2127
+ return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
2128
+ } catch (err) {
2129
+ errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
2130
+ return { nodesAdded: 0, edgesAdded: 0 };
2131
+ }
2132
+ }
2073
2133
  async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
2074
2134
  const errors = [];
2075
2135
  let nodesAdded = 0;
@@ -2114,7 +2174,62 @@ var ConfluenceConnector = class {
2114
2174
  };
2115
2175
 
2116
2176
  // src/ingest/connectors/CIConnector.ts
2117
- var CIConnector = class {
2177
+ function emptyResult2(errors, start) {
2178
+ return {
2179
+ nodesAdded: 0,
2180
+ nodesUpdated: 0,
2181
+ edgesAdded: 0,
2182
+ edgesUpdated: 0,
2183
+ errors,
2184
+ durationMs: Date.now() - start
2185
+ };
2186
+ }
2187
+ function ingestRun(store, run) {
2188
+ const buildId = `build:${run.id}`;
2189
+ const safeName = sanitizeExternalText(run.name, 200);
2190
+ let nodesAdded = 0;
2191
+ let edgesAdded = 0;
2192
+ store.addNode({
2193
+ id: buildId,
2194
+ type: "build",
2195
+ name: `${safeName} #${run.id}`,
2196
+ metadata: {
2197
+ source: "github-actions",
2198
+ status: run.status,
2199
+ conclusion: run.conclusion,
2200
+ branch: run.head_branch,
2201
+ sha: run.head_sha,
2202
+ url: run.html_url,
2203
+ createdAt: run.created_at
2204
+ }
2205
+ });
2206
+ nodesAdded++;
2207
+ const commitNode = store.getNode(`commit:${run.head_sha}`);
2208
+ if (commitNode) {
2209
+ store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
2210
+ edgesAdded++;
2211
+ }
2212
+ if (run.conclusion === "failure") {
2213
+ const testResultId = `test_result:${run.id}`;
2214
+ store.addNode({
2215
+ id: testResultId,
2216
+ type: "test_result",
2217
+ name: `Failed: ${safeName} #${run.id}`,
2218
+ metadata: {
2219
+ source: "github-actions",
2220
+ buildId: String(run.id),
2221
+ conclusion: "failure",
2222
+ branch: run.head_branch,
2223
+ sha: run.head_sha
2224
+ }
2225
+ });
2226
+ nodesAdded++;
2227
+ store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
2228
+ edgesAdded++;
2229
+ }
2230
+ return { nodesAdded, edgesAdded };
2231
+ }
2232
+ var CIConnector = class {
2118
2233
  name = "ci";
2119
2234
  source = "github-actions";
2120
2235
  httpClient;
@@ -2124,22 +2239,29 @@ var CIConnector = class {
2124
2239
  async ingest(store, config) {
2125
2240
  const start = Date.now();
2126
2241
  const errors = [];
2127
- let nodesAdded = 0;
2128
- let edgesAdded = 0;
2129
2242
  const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
2130
2243
  const apiKey = process.env[apiKeyEnv];
2131
2244
  if (!apiKey) {
2132
- return {
2133
- nodesAdded: 0,
2134
- nodesUpdated: 0,
2135
- edgesAdded: 0,
2136
- edgesUpdated: 0,
2137
- errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
2138
- durationMs: Date.now() - start
2139
- };
2245
+ return emptyResult2(
2246
+ [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
2247
+ start
2248
+ );
2140
2249
  }
2141
2250
  const repo = config.repo ?? "";
2142
2251
  const maxRuns = config.maxRuns ?? 10;
2252
+ const counts = await this.fetchAndIngestRuns(store, repo, maxRuns, apiKey, errors);
2253
+ return {
2254
+ nodesAdded: counts.nodesAdded,
2255
+ nodesUpdated: 0,
2256
+ edgesAdded: counts.edgesAdded,
2257
+ edgesUpdated: 0,
2258
+ errors,
2259
+ durationMs: Date.now() - start
2260
+ };
2261
+ }
2262
+ async fetchAndIngestRuns(store, repo, maxRuns, apiKey, errors) {
2263
+ let nodesAdded = 0;
2264
+ let edgesAdded = 0;
2143
2265
  try {
2144
2266
  const url = `https://api.github.com/repos/${repo}/actions/runs?per_page=${maxRuns}`;
2145
2267
  const response = await this.httpClient(url, {
@@ -2147,71 +2269,20 @@ var CIConnector = class {
2147
2269
  });
2148
2270
  if (!response.ok) {
2149
2271
  errors.push(`GitHub Actions API error: status ${response.status}`);
2150
- return {
2151
- nodesAdded: 0,
2152
- nodesUpdated: 0,
2153
- edgesAdded: 0,
2154
- edgesUpdated: 0,
2155
- errors,
2156
- durationMs: Date.now() - start
2157
- };
2272
+ return { nodesAdded, edgesAdded };
2158
2273
  }
2159
2274
  const data = await response.json();
2160
2275
  for (const run of data.workflow_runs) {
2161
- const buildId = `build:${run.id}`;
2162
- const safeName = sanitizeExternalText(run.name, 200);
2163
- store.addNode({
2164
- id: buildId,
2165
- type: "build",
2166
- name: `${safeName} #${run.id}`,
2167
- metadata: {
2168
- source: "github-actions",
2169
- status: run.status,
2170
- conclusion: run.conclusion,
2171
- branch: run.head_branch,
2172
- sha: run.head_sha,
2173
- url: run.html_url,
2174
- createdAt: run.created_at
2175
- }
2176
- });
2177
- nodesAdded++;
2178
- const commitNode = store.getNode(`commit:${run.head_sha}`);
2179
- if (commitNode) {
2180
- store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
2181
- edgesAdded++;
2182
- }
2183
- if (run.conclusion === "failure") {
2184
- const testResultId = `test_result:${run.id}`;
2185
- store.addNode({
2186
- id: testResultId,
2187
- type: "test_result",
2188
- name: `Failed: ${safeName} #${run.id}`,
2189
- metadata: {
2190
- source: "github-actions",
2191
- buildId: String(run.id),
2192
- conclusion: "failure",
2193
- branch: run.head_branch,
2194
- sha: run.head_sha
2195
- }
2196
- });
2197
- nodesAdded++;
2198
- store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
2199
- edgesAdded++;
2200
- }
2276
+ const counts = ingestRun(store, run);
2277
+ nodesAdded += counts.nodesAdded;
2278
+ edgesAdded += counts.edgesAdded;
2201
2279
  }
2202
2280
  } catch (err) {
2203
2281
  errors.push(
2204
2282
  `GitHub Actions fetch error: ${err instanceof Error ? err.message : String(err)}`
2205
2283
  );
2206
2284
  }
2207
- return {
2208
- nodesAdded,
2209
- nodesUpdated: 0,
2210
- edgesAdded,
2211
- edgesUpdated: 0,
2212
- errors,
2213
- durationMs: Date.now() - start
2214
- };
2285
+ return { nodesAdded, edgesAdded };
2215
2286
  }
2216
2287
  };
2217
2288
 
@@ -2281,16 +2352,29 @@ var FusionLayer = class {
2281
2352
  return [];
2282
2353
  }
2283
2354
  const allNodes = this.store.findNodes({});
2355
+ const semanticScores = this.buildSemanticScores(queryEmbedding, allNodes.length);
2356
+ const { kwWeight, semWeight } = this.resolveWeights(semanticScores.size > 0);
2357
+ const results = this.scoreNodes(allNodes, keywords, semanticScores, kwWeight, semWeight);
2358
+ results.sort((a, b) => b.score - a.score);
2359
+ return results.slice(0, topK);
2360
+ }
2361
+ buildSemanticScores(queryEmbedding, nodeCount) {
2284
2362
  const semanticScores = /* @__PURE__ */ new Map();
2285
2363
  if (queryEmbedding && this.vectorStore) {
2286
- const vectorResults = this.vectorStore.search(queryEmbedding, allNodes.length);
2364
+ const vectorResults = this.vectorStore.search(queryEmbedding, nodeCount);
2287
2365
  for (const vr of vectorResults) {
2288
2366
  semanticScores.set(vr.id, vr.score);
2289
2367
  }
2290
2368
  }
2291
- const hasSemanticScores = semanticScores.size > 0;
2292
- const kwWeight = hasSemanticScores ? this.keywordWeight : 1;
2293
- const semWeight = hasSemanticScores ? this.semanticWeight : 0;
2369
+ return semanticScores;
2370
+ }
2371
+ resolveWeights(hasSemanticScores) {
2372
+ return {
2373
+ kwWeight: hasSemanticScores ? this.keywordWeight : 1,
2374
+ semWeight: hasSemanticScores ? this.semanticWeight : 0
2375
+ };
2376
+ }
2377
+ scoreNodes(allNodes, keywords, semanticScores, kwWeight, semWeight) {
2294
2378
  const results = [];
2295
2379
  for (const node of allNodes) {
2296
2380
  const kwScore = this.keywordScore(keywords, node);
@@ -2301,15 +2385,11 @@ var FusionLayer = class {
2301
2385
  nodeId: node.id,
2302
2386
  node,
2303
2387
  score: fusedScore,
2304
- signals: {
2305
- keyword: kwScore,
2306
- semantic: semScore
2307
- }
2388
+ signals: { keyword: kwScore, semantic: semScore }
2308
2389
  });
2309
2390
  }
2310
2391
  }
2311
- results.sort((a, b) => b.score - a.score);
2312
- return results.slice(0, topK);
2392
+ return results;
2313
2393
  }
2314
2394
  extractKeywords(query) {
2315
2395
  const tokens = query.toLowerCase().split(/[\s\-_.,:;!?()[\]{}"'`/\\|@#$%^&*+=<>~]+/).filter((t) => t.length >= 2).filter((t) => !STOP_WORDS.has(t));
@@ -2364,37 +2444,50 @@ var GraphEntropyAdapter = class {
2364
2444
  const missingTargets = [];
2365
2445
  let freshEdges = 0;
2366
2446
  for (const edge of documentsEdges) {
2367
- const codeNode = this.store.getNode(edge.to);
2368
- if (!codeNode) {
2447
+ const result = this.classifyDocEdge(edge);
2448
+ if (result.kind === "missing") {
2369
2449
  missingTargets.push(edge.to);
2370
- continue;
2450
+ } else if (result.kind === "fresh") {
2451
+ freshEdges++;
2452
+ } else {
2453
+ staleEdges.push(result.entry);
2371
2454
  }
2372
- const docNode = this.store.getNode(edge.from);
2373
- const codeLastModified = codeNode.lastModified;
2374
- const docLastModified = docNode?.lastModified;
2375
- if (codeLastModified && docLastModified) {
2376
- if (codeLastModified > docLastModified) {
2377
- staleEdges.push({
2455
+ }
2456
+ return { staleEdges, missingTargets, freshEdges };
2457
+ }
2458
+ classifyDocEdge(edge) {
2459
+ const codeNode = this.store.getNode(edge.to);
2460
+ if (!codeNode) {
2461
+ return { kind: "missing" };
2462
+ }
2463
+ const docNode = this.store.getNode(edge.from);
2464
+ const codeLastModified = codeNode.lastModified;
2465
+ const docLastModified = docNode?.lastModified;
2466
+ if (codeLastModified && docLastModified) {
2467
+ if (codeLastModified > docLastModified) {
2468
+ return {
2469
+ kind: "stale",
2470
+ entry: {
2378
2471
  docNodeId: edge.from,
2379
2472
  codeNodeId: edge.to,
2380
2473
  edgeType: edge.type,
2381
2474
  codeLastModified,
2382
2475
  docLastModified
2383
- });
2384
- } else {
2385
- freshEdges++;
2386
- }
2387
- } else {
2388
- staleEdges.push({
2389
- docNodeId: edge.from,
2390
- codeNodeId: edge.to,
2391
- edgeType: edge.type,
2392
- codeLastModified,
2393
- docLastModified
2394
- });
2476
+ }
2477
+ };
2395
2478
  }
2479
+ return { kind: "fresh" };
2396
2480
  }
2397
- return { staleEdges, missingTargets, freshEdges };
2481
+ return {
2482
+ kind: "stale",
2483
+ entry: {
2484
+ docNodeId: edge.from,
2485
+ codeNodeId: edge.to,
2486
+ edgeType: edge.type,
2487
+ codeLastModified,
2488
+ docLastModified
2489
+ }
2490
+ };
2398
2491
  }
2399
2492
  /**
2400
2493
  * BFS from entry points to find reachable vs unreachable code nodes.
@@ -2651,36 +2744,12 @@ var GraphAnomalyAdapter = class {
2651
2744
  store;
2652
2745
  detect(options) {
2653
2746
  const threshold = options?.threshold != null && options.threshold > 0 ? options.threshold : DEFAULT_THRESHOLD;
2654
- const requestedMetrics = options?.metrics ?? [...DEFAULT_METRICS];
2655
- const warnings = [];
2656
- const metricsToAnalyze = [];
2657
- for (const m of requestedMetrics) {
2658
- if (RECOGNIZED_METRICS.has(m)) {
2659
- metricsToAnalyze.push(m);
2660
- } else {
2661
- warnings.push(m);
2662
- }
2663
- }
2664
- const allOutliers = [];
2665
- const analyzedNodeIds = /* @__PURE__ */ new Set();
2666
- const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
2667
- const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
2668
- const needsComplexity = metricsToAnalyze.includes("hotspotScore");
2669
- const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
2670
- const cachedHotspotData = needsComplexity ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
2671
- for (const metric of metricsToAnalyze) {
2672
- const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
2673
- for (const e of entries) {
2674
- analyzedNodeIds.add(e.nodeId);
2675
- }
2676
- const outliers = this.computeZScoreOutliers(entries, metric, threshold);
2677
- allOutliers.push(...outliers);
2678
- }
2679
- allOutliers.sort((a, b) => b.zScore - a.zScore);
2747
+ const { metricsToAnalyze, warnings } = this.filterMetrics(
2748
+ options?.metrics ?? [...DEFAULT_METRICS]
2749
+ );
2750
+ const { allOutliers, analyzedNodeIds } = this.computeAllOutliers(metricsToAnalyze, threshold);
2680
2751
  const articulationPoints = this.findArticulationPoints();
2681
- const outlierNodeIds = new Set(allOutliers.map((o) => o.nodeId));
2682
- const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
2683
- const overlapping = [...outlierNodeIds].filter((id) => apNodeIds.has(id));
2752
+ const overlapping = this.computeOverlap(allOutliers, articulationPoints);
2684
2753
  return {
2685
2754
  statisticalOutliers: allOutliers,
2686
2755
  articulationPoints,
@@ -2696,6 +2765,38 @@ var GraphAnomalyAdapter = class {
2696
2765
  }
2697
2766
  };
2698
2767
  }
2768
+ filterMetrics(requested) {
2769
+ const metricsToAnalyze = [];
2770
+ const warnings = [];
2771
+ for (const m of requested) {
2772
+ if (RECOGNIZED_METRICS.has(m)) {
2773
+ metricsToAnalyze.push(m);
2774
+ } else {
2775
+ warnings.push(m);
2776
+ }
2777
+ }
2778
+ return { metricsToAnalyze, warnings };
2779
+ }
2780
+ computeAllOutliers(metricsToAnalyze, threshold) {
2781
+ const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
2782
+ const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
2783
+ const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
2784
+ const cachedHotspotData = metricsToAnalyze.includes("hotspotScore") ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
2785
+ const allOutliers = [];
2786
+ const analyzedNodeIds = /* @__PURE__ */ new Set();
2787
+ for (const metric of metricsToAnalyze) {
2788
+ const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
2789
+ for (const e of entries) analyzedNodeIds.add(e.nodeId);
2790
+ allOutliers.push(...this.computeZScoreOutliers(entries, metric, threshold));
2791
+ }
2792
+ allOutliers.sort((a, b) => b.zScore - a.zScore);
2793
+ return { allOutliers, analyzedNodeIds };
2794
+ }
2795
+ computeOverlap(outliers, articulationPoints) {
2796
+ const outlierNodeIds = new Set(outliers.map((o) => o.nodeId));
2797
+ const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
2798
+ return [...outlierNodeIds].filter((id) => apNodeIds.has(id));
2799
+ }
2699
2800
  collectMetricValues(metric, cachedCouplingData, cachedHotspotData) {
2700
2801
  const entries = [];
2701
2802
  if (metric === "cyclomaticComplexity") {
@@ -2892,6 +2993,7 @@ var INTENT_SIGNALS = {
2892
2993
  "depend",
2893
2994
  "blast",
2894
2995
  "radius",
2996
+ "cascade",
2895
2997
  "risk",
2896
2998
  "delete",
2897
2999
  "remove"
@@ -2901,6 +3003,7 @@ var INTENT_SIGNALS = {
2901
3003
  /what\s+(breaks|happens|is affected)/,
2902
3004
  /if\s+i\s+(change|modify|remove|delete)/,
2903
3005
  /blast\s+radius/,
3006
+ /cascad/,
2904
3007
  /what\s+(depend|relies)/
2905
3008
  ]
2906
3009
  },
@@ -3249,37 +3352,54 @@ var EntityExtractor = class {
3249
3352
  result.push(entity);
3250
3353
  }
3251
3354
  };
3252
- const quotedConsumed = /* @__PURE__ */ new Set();
3355
+ const quotedConsumed = this.extractQuoted(trimmed, add);
3356
+ const casingConsumed = this.extractCasing(trimmed, quotedConsumed, add);
3357
+ const pathConsumed = this.extractPaths(trimmed, add);
3358
+ this.extractNouns(trimmed, buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed), add);
3359
+ return result;
3360
+ }
3361
+ /** Strategy 1: Quoted strings. Returns the set of consumed tokens. */
3362
+ extractQuoted(trimmed, add) {
3363
+ const consumed = /* @__PURE__ */ new Set();
3253
3364
  for (const match of trimmed.matchAll(QUOTED_RE)) {
3254
3365
  const inner = match[1].trim();
3255
3366
  if (inner.length > 0) {
3256
3367
  add(inner);
3257
- quotedConsumed.add(inner);
3368
+ consumed.add(inner);
3258
3369
  }
3259
3370
  }
3260
- const casingConsumed = /* @__PURE__ */ new Set();
3371
+ return consumed;
3372
+ }
3373
+ /** Strategy 2: PascalCase/camelCase tokens. Returns the set of consumed tokens. */
3374
+ extractCasing(trimmed, quotedConsumed, add) {
3375
+ const consumed = /* @__PURE__ */ new Set();
3261
3376
  for (const match of trimmed.matchAll(PASCAL_OR_CAMEL_RE)) {
3262
3377
  const token = match[0];
3263
3378
  if (!quotedConsumed.has(token)) {
3264
3379
  add(token);
3265
- casingConsumed.add(token);
3380
+ consumed.add(token);
3266
3381
  }
3267
3382
  }
3268
- const pathConsumed = /* @__PURE__ */ new Set();
3383
+ return consumed;
3384
+ }
3385
+ /** Strategy 3: File paths. Returns the set of consumed tokens. */
3386
+ extractPaths(trimmed, add) {
3387
+ const consumed = /* @__PURE__ */ new Set();
3269
3388
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
3270
3389
  const path7 = match[0];
3271
3390
  add(path7);
3272
- pathConsumed.add(path7);
3391
+ consumed.add(path7);
3273
3392
  }
3274
- const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
3275
- const words = trimmed.split(/\s+/);
3276
- for (const raw of words) {
3393
+ return consumed;
3394
+ }
3395
+ /** Strategy 4: Remaining significant nouns after stop-word and intent-keyword removal. */
3396
+ extractNouns(trimmed, allConsumed, add) {
3397
+ for (const raw of trimmed.split(/\s+/)) {
3277
3398
  const cleaned = raw.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "");
3278
3399
  if (cleaned.length === 0) continue;
3279
3400
  if (isSkippableWord(cleaned, allConsumed)) continue;
3280
3401
  add(cleaned);
3281
3402
  }
3282
- return result;
3283
3403
  }
3284
3404
  };
3285
3405
 
@@ -3386,6 +3506,10 @@ var ResponseFormatter = class {
3386
3506
  }
3387
3507
  formatImpact(entityName, data) {
3388
3508
  const d = data;
3509
+ if ("sourceNodeId" in d && "summary" in d) {
3510
+ const summary = d.summary;
3511
+ return `Blast radius of **${entityName}**: ${summary.totalAffected} affected nodes (${summary.highRisk} high risk, ${summary.mediumRisk} medium, ${summary.lowRisk} low).`;
3512
+ }
3389
3513
  const code = this.safeArrayLength(d?.code);
3390
3514
  const tests = this.safeArrayLength(d?.tests);
3391
3515
  const docs = this.safeArrayLength(d?.docs);
@@ -3447,41 +3571,286 @@ var ResponseFormatter = class {
3447
3571
  }
3448
3572
  };
3449
3573
 
3574
+ // src/blast-radius/CompositeProbabilityStrategy.ts
3575
+ var CompositeProbabilityStrategy = class _CompositeProbabilityStrategy {
3576
+ constructor(changeFreqMap, couplingMap) {
3577
+ this.changeFreqMap = changeFreqMap;
3578
+ this.couplingMap = couplingMap;
3579
+ }
3580
+ changeFreqMap;
3581
+ couplingMap;
3582
+ static BASE_WEIGHTS = {
3583
+ imports: 0.7,
3584
+ calls: 0.5,
3585
+ implements: 0.6,
3586
+ inherits: 0.6,
3587
+ co_changes_with: 0.4,
3588
+ references: 0.2,
3589
+ contains: 0.3
3590
+ };
3591
+ static FALLBACK_WEIGHT = 0.1;
3592
+ static EDGE_TYPE_BLEND = 0.5;
3593
+ static CHANGE_FREQ_BLEND = 0.3;
3594
+ static COUPLING_BLEND = 0.2;
3595
+ getEdgeProbability(edge, _fromNode, toNode) {
3596
+ const base = _CompositeProbabilityStrategy.BASE_WEIGHTS[edge.type] ?? _CompositeProbabilityStrategy.FALLBACK_WEIGHT;
3597
+ const changeFreq = this.changeFreqMap.get(toNode.id) ?? 0;
3598
+ const coupling = this.couplingMap.get(toNode.id) ?? 0;
3599
+ return Math.min(
3600
+ 1,
3601
+ base * _CompositeProbabilityStrategy.EDGE_TYPE_BLEND + changeFreq * _CompositeProbabilityStrategy.CHANGE_FREQ_BLEND + coupling * _CompositeProbabilityStrategy.COUPLING_BLEND
3602
+ );
3603
+ }
3604
+ };
3605
+
3606
+ // src/blast-radius/CascadeSimulator.ts
3607
+ var DEFAULT_PROBABILITY_FLOOR = 0.05;
3608
+ var DEFAULT_MAX_DEPTH = 10;
3609
+ var CascadeSimulator = class {
3610
+ constructor(store) {
3611
+ this.store = store;
3612
+ }
3613
+ store;
3614
+ simulate(sourceNodeId, options = {}) {
3615
+ const sourceNode = this.store.getNode(sourceNodeId);
3616
+ if (!sourceNode) {
3617
+ throw new Error(`Node not found: ${sourceNodeId}. Ensure the file has been ingested.`);
3618
+ }
3619
+ const probabilityFloor = options.probabilityFloor ?? DEFAULT_PROBABILITY_FLOOR;
3620
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
3621
+ const edgeTypeFilter = options.edgeTypes ? new Set(options.edgeTypes) : null;
3622
+ const strategy = options.strategy ?? this.buildDefaultStrategy();
3623
+ const visited = /* @__PURE__ */ new Map();
3624
+ const queue = [];
3625
+ const fanOutCount = /* @__PURE__ */ new Map();
3626
+ this.seedQueue(
3627
+ sourceNodeId,
3628
+ sourceNode,
3629
+ strategy,
3630
+ edgeTypeFilter,
3631
+ probabilityFloor,
3632
+ queue,
3633
+ fanOutCount
3634
+ );
3635
+ const truncated = this.runBfs(
3636
+ queue,
3637
+ visited,
3638
+ fanOutCount,
3639
+ sourceNodeId,
3640
+ strategy,
3641
+ edgeTypeFilter,
3642
+ probabilityFloor,
3643
+ maxDepth
3644
+ );
3645
+ return this.buildResult(sourceNodeId, sourceNode.name, visited, fanOutCount, truncated);
3646
+ }
3647
+ seedQueue(sourceNodeId, sourceNode, strategy, edgeTypeFilter, probabilityFloor, queue, fanOutCount) {
3648
+ const sourceEdges = this.store.getEdges({ from: sourceNodeId });
3649
+ for (const edge of sourceEdges) {
3650
+ if (edge.to === sourceNodeId) continue;
3651
+ if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
3652
+ const targetNode = this.store.getNode(edge.to);
3653
+ if (!targetNode) continue;
3654
+ const cumProb = strategy.getEdgeProbability(edge, sourceNode, targetNode);
3655
+ if (cumProb < probabilityFloor) continue;
3656
+ queue.push({
3657
+ nodeId: edge.to,
3658
+ cumProb,
3659
+ depth: 1,
3660
+ parentId: sourceNodeId,
3661
+ incomingEdge: edge.type
3662
+ });
3663
+ }
3664
+ fanOutCount.set(
3665
+ sourceNodeId,
3666
+ sourceEdges.filter(
3667
+ (e) => e.to !== sourceNodeId && (!edgeTypeFilter || edgeTypeFilter.has(e.type))
3668
+ ).length
3669
+ );
3670
+ }
3671
+ runBfs(queue, visited, fanOutCount, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, maxDepth) {
3672
+ const MAX_QUEUE_SIZE = 1e4;
3673
+ let head = 0;
3674
+ while (head < queue.length) {
3675
+ if (queue.length > MAX_QUEUE_SIZE) return true;
3676
+ const entry = queue[head++];
3677
+ const existing = visited.get(entry.nodeId);
3678
+ if (existing && existing.cumulativeProbability >= entry.cumProb) continue;
3679
+ const targetNode = this.store.getNode(entry.nodeId);
3680
+ if (!targetNode) continue;
3681
+ visited.set(entry.nodeId, {
3682
+ nodeId: entry.nodeId,
3683
+ name: targetNode.name,
3684
+ ...targetNode.path !== void 0 && { path: targetNode.path },
3685
+ type: targetNode.type,
3686
+ cumulativeProbability: entry.cumProb,
3687
+ depth: entry.depth,
3688
+ incomingEdge: entry.incomingEdge,
3689
+ parentId: entry.parentId
3690
+ });
3691
+ if (entry.depth < maxDepth) {
3692
+ const childCount = this.expandNode(
3693
+ entry,
3694
+ targetNode,
3695
+ sourceNodeId,
3696
+ strategy,
3697
+ edgeTypeFilter,
3698
+ probabilityFloor,
3699
+ queue
3700
+ );
3701
+ fanOutCount.set(entry.nodeId, (fanOutCount.get(entry.nodeId) ?? 0) + childCount);
3702
+ }
3703
+ }
3704
+ return false;
3705
+ }
3706
+ expandNode(entry, fromNode, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, queue) {
3707
+ const outEdges = this.store.getEdges({ from: entry.nodeId });
3708
+ let childCount = 0;
3709
+ for (const edge of outEdges) {
3710
+ if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
3711
+ if (edge.to === sourceNodeId) continue;
3712
+ const childNode = this.store.getNode(edge.to);
3713
+ if (!childNode) continue;
3714
+ const newCumProb = entry.cumProb * strategy.getEdgeProbability(edge, fromNode, childNode);
3715
+ if (newCumProb < probabilityFloor) continue;
3716
+ childCount++;
3717
+ queue.push({
3718
+ nodeId: edge.to,
3719
+ cumProb: newCumProb,
3720
+ depth: entry.depth + 1,
3721
+ parentId: entry.nodeId,
3722
+ incomingEdge: edge.type
3723
+ });
3724
+ }
3725
+ return childCount;
3726
+ }
3727
+ buildDefaultStrategy() {
3728
+ return new CompositeProbabilityStrategy(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
3729
+ }
3730
+ buildResult(sourceNodeId, sourceName, visited, fanOutCount, truncated = false) {
3731
+ if (visited.size === 0) {
3732
+ return {
3733
+ sourceNodeId,
3734
+ sourceName,
3735
+ layers: [],
3736
+ flatSummary: [],
3737
+ summary: {
3738
+ totalAffected: 0,
3739
+ maxDepthReached: 0,
3740
+ highRisk: 0,
3741
+ mediumRisk: 0,
3742
+ lowRisk: 0,
3743
+ categoryBreakdown: { code: 0, tests: 0, docs: 0, other: 0 },
3744
+ amplificationPoints: [],
3745
+ truncated
3746
+ }
3747
+ };
3748
+ }
3749
+ const allNodes = Array.from(visited.values());
3750
+ const flatSummary = [...allNodes].sort(
3751
+ (a, b) => b.cumulativeProbability - a.cumulativeProbability
3752
+ );
3753
+ const depthMap = /* @__PURE__ */ new Map();
3754
+ for (const node of allNodes) {
3755
+ let list = depthMap.get(node.depth);
3756
+ if (!list) {
3757
+ list = [];
3758
+ depthMap.set(node.depth, list);
3759
+ }
3760
+ list.push(node);
3761
+ }
3762
+ const layers = [];
3763
+ const depths = Array.from(depthMap.keys()).sort((a, b) => a - b);
3764
+ for (const depth of depths) {
3765
+ const nodes = depthMap.get(depth);
3766
+ const breakdown = { code: 0, tests: 0, docs: 0, other: 0 };
3767
+ for (const n of nodes) {
3768
+ const graphNode = this.store.getNode(n.nodeId);
3769
+ if (graphNode) {
3770
+ breakdown[classifyNodeCategory(graphNode)]++;
3771
+ }
3772
+ }
3773
+ layers.push({ depth, nodes, categoryBreakdown: breakdown });
3774
+ }
3775
+ let highRisk = 0;
3776
+ let mediumRisk = 0;
3777
+ let lowRisk = 0;
3778
+ const catBreakdown = { code: 0, tests: 0, docs: 0, other: 0 };
3779
+ for (const node of allNodes) {
3780
+ if (node.cumulativeProbability >= 0.5) highRisk++;
3781
+ else if (node.cumulativeProbability >= 0.2) mediumRisk++;
3782
+ else lowRisk++;
3783
+ const graphNode = this.store.getNode(node.nodeId);
3784
+ if (graphNode) {
3785
+ catBreakdown[classifyNodeCategory(graphNode)]++;
3786
+ }
3787
+ }
3788
+ const amplificationPoints = [];
3789
+ for (const [nodeId, count] of fanOutCount) {
3790
+ if (count > 3) {
3791
+ amplificationPoints.push(nodeId);
3792
+ }
3793
+ }
3794
+ const maxDepthReached = allNodes.reduce((max, n) => Math.max(max, n.depth), 0);
3795
+ return {
3796
+ sourceNodeId,
3797
+ sourceName,
3798
+ layers,
3799
+ flatSummary,
3800
+ summary: {
3801
+ totalAffected: allNodes.length,
3802
+ maxDepthReached,
3803
+ highRisk,
3804
+ mediumRisk,
3805
+ lowRisk,
3806
+ categoryBreakdown: catBreakdown,
3807
+ amplificationPoints,
3808
+ truncated
3809
+ }
3810
+ };
3811
+ }
3812
+ };
3813
+
3450
3814
  // src/nlq/index.ts
3451
3815
  var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships", "explain"]);
3452
3816
  var classifier = new IntentClassifier();
3453
3817
  var extractor = new EntityExtractor();
3454
3818
  var formatter = new ResponseFormatter();
3819
+ function lowConfidenceResult(intent, confidence) {
3820
+ return {
3821
+ intent,
3822
+ intentConfidence: confidence,
3823
+ entities: [],
3824
+ summary: "I'm not sure what you're asking. Try rephrasing your question.",
3825
+ data: null,
3826
+ suggestions: [
3827
+ 'Try "what breaks if I change <name>?" for impact analysis',
3828
+ 'Try "where is <name>?" to find entities',
3829
+ 'Try "what calls <name>?" for relationships',
3830
+ 'Try "what is <name>?" for explanations',
3831
+ 'Try "what looks wrong?" for anomaly detection'
3832
+ ]
3833
+ };
3834
+ }
3835
+ function noEntityResult(intent, confidence) {
3836
+ return {
3837
+ intent,
3838
+ intentConfidence: confidence,
3839
+ entities: [],
3840
+ summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
3841
+ data: null
3842
+ };
3843
+ }
3455
3844
  async function askGraph(store, question) {
3456
3845
  const fusion = new FusionLayer(store);
3457
3846
  const resolver = new EntityResolver(store, fusion);
3458
3847
  const classification = classifier.classify(question);
3459
3848
  if (classification.confidence < 0.3) {
3460
- return {
3461
- intent: classification.intent,
3462
- intentConfidence: classification.confidence,
3463
- entities: [],
3464
- summary: "I'm not sure what you're asking. Try rephrasing your question.",
3465
- data: null,
3466
- suggestions: [
3467
- 'Try "what breaks if I change <name>?" for impact analysis',
3468
- 'Try "where is <name>?" to find entities',
3469
- 'Try "what calls <name>?" for relationships',
3470
- 'Try "what is <name>?" for explanations',
3471
- 'Try "what looks wrong?" for anomaly detection'
3472
- ]
3473
- };
3849
+ return lowConfidenceResult(classification.intent, classification.confidence);
3474
3850
  }
3475
- const rawEntities = extractor.extract(question);
3476
- const entities = resolver.resolve(rawEntities);
3851
+ const entities = resolver.resolve(extractor.extract(question));
3477
3852
  if (ENTITY_REQUIRED_INTENTS.has(classification.intent) && entities.length === 0) {
3478
- return {
3479
- intent: classification.intent,
3480
- intentConfidence: classification.confidence,
3481
- entities: [],
3482
- summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
3483
- data: null
3484
- };
3853
+ return noEntityResult(classification.intent, classification.confidence);
3485
3854
  }
3486
3855
  let data;
3487
3856
  try {
@@ -3495,62 +3864,59 @@ async function askGraph(store, question) {
3495
3864
  data: null
3496
3865
  };
3497
3866
  }
3498
- const summary = formatter.format(classification.intent, entities, data, question);
3499
3867
  return {
3500
3868
  intent: classification.intent,
3501
3869
  intentConfidence: classification.confidence,
3502
3870
  entities,
3503
- summary,
3871
+ summary: formatter.format(classification.intent, entities, data, question),
3504
3872
  data
3505
3873
  };
3506
3874
  }
3875
+ function buildContextBlocks(cql, rootIds, searchResults) {
3876
+ return rootIds.map((rootId) => {
3877
+ const expanded = cql.execute({ rootNodeIds: [rootId], maxDepth: 2 });
3878
+ const match = searchResults.find((r) => r.nodeId === rootId);
3879
+ return {
3880
+ rootNode: rootId,
3881
+ score: match?.score ?? 1,
3882
+ nodes: expanded.nodes,
3883
+ edges: expanded.edges
3884
+ };
3885
+ });
3886
+ }
3887
+ function executeImpact(store, cql, entities, question) {
3888
+ const rootId = entities[0].nodeId;
3889
+ const lower = question.toLowerCase();
3890
+ if (lower.includes("blast radius") || lower.includes("cascade")) {
3891
+ return new CascadeSimulator(store).simulate(rootId);
3892
+ }
3893
+ const result = cql.execute({ rootNodeIds: [rootId], bidirectional: true, maxDepth: 3 });
3894
+ return groupNodesByImpact(result.nodes, rootId);
3895
+ }
3896
+ function executeExplain(cql, entities, question, fusion) {
3897
+ const searchResults = fusion.search(question, 10);
3898
+ const rootIds = entities.length > 0 ? [entities[0].nodeId] : searchResults.slice(0, 3).map((r) => r.nodeId);
3899
+ return { searchResults, context: buildContextBlocks(cql, rootIds, searchResults) };
3900
+ }
3507
3901
  function executeOperation(store, intent, entities, question, fusion) {
3508
3902
  const cql = new ContextQL(store);
3509
3903
  switch (intent) {
3510
- case "impact": {
3511
- const rootId = entities[0].nodeId;
3512
- const result = cql.execute({
3513
- rootNodeIds: [rootId],
3514
- bidirectional: true,
3515
- maxDepth: 3
3516
- });
3517
- return groupNodesByImpact(result.nodes, rootId);
3518
- }
3519
- case "find": {
3904
+ case "impact":
3905
+ return executeImpact(store, cql, entities, question);
3906
+ case "find":
3520
3907
  return fusion.search(question, 10);
3521
- }
3522
3908
  case "relationships": {
3523
- const rootId = entities[0].nodeId;
3524
3909
  const result = cql.execute({
3525
- rootNodeIds: [rootId],
3910
+ rootNodeIds: [entities[0].nodeId],
3526
3911
  bidirectional: true,
3527
3912
  maxDepth: 1
3528
3913
  });
3529
3914
  return { nodes: result.nodes, edges: result.edges };
3530
3915
  }
3531
- case "explain": {
3532
- const searchResults = fusion.search(question, 10);
3533
- const contextBlocks = [];
3534
- const rootIds = entities.length > 0 ? [entities[0].nodeId] : searchResults.slice(0, 3).map((r) => r.nodeId);
3535
- for (const rootId of rootIds) {
3536
- const expanded = cql.execute({
3537
- rootNodeIds: [rootId],
3538
- maxDepth: 2
3539
- });
3540
- const matchingResult = searchResults.find((r) => r.nodeId === rootId);
3541
- contextBlocks.push({
3542
- rootNode: rootId,
3543
- score: matchingResult?.score ?? 1,
3544
- nodes: expanded.nodes,
3545
- edges: expanded.edges
3546
- });
3547
- }
3548
- return { searchResults, context: contextBlocks };
3549
- }
3550
- case "anomaly": {
3551
- const adapter = new GraphAnomalyAdapter(store);
3552
- return adapter.detect();
3553
- }
3916
+ case "explain":
3917
+ return executeExplain(cql, entities, question, fusion);
3918
+ case "anomaly":
3919
+ return new GraphAnomalyAdapter(store).detect();
3554
3920
  default:
3555
3921
  return null;
3556
3922
  }
@@ -3571,12 +3937,14 @@ var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
3571
3937
  "method",
3572
3938
  "variable"
3573
3939
  ]);
3940
+ function countMetadataChars(node) {
3941
+ return node.metadata ? JSON.stringify(node.metadata).length : 0;
3942
+ }
3943
+ function countBaseChars(node) {
3944
+ return (node.name?.length ?? 0) + (node.path?.length ?? 0) + (node.type?.length ?? 0);
3945
+ }
3574
3946
  function estimateNodeTokens(node) {
3575
- let chars = (node.name?.length ?? 0) + (node.path?.length ?? 0) + (node.type?.length ?? 0);
3576
- if (node.metadata) {
3577
- chars += JSON.stringify(node.metadata).length;
3578
- }
3579
- return Math.ceil(chars / 4);
3947
+ return Math.ceil((countBaseChars(node) + countMetadataChars(node)) / 4);
3580
3948
  }
3581
3949
  var Assembler = class {
3582
3950
  store;
@@ -3657,47 +4025,55 @@ var Assembler = class {
3657
4025
  }
3658
4026
  return { keptNodes, tokenEstimate, truncated };
3659
4027
  }
3660
- /**
3661
- * Compute a token budget allocation across node types.
3662
- */
3663
- computeBudget(totalTokens, phase) {
3664
- const allNodes = this.store.findNodes({});
4028
+ countNodesByType() {
3665
4029
  const typeCounts = {};
3666
- for (const node of allNodes) {
4030
+ for (const node of this.store.findNodes({})) {
3667
4031
  typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;
3668
4032
  }
4033
+ return typeCounts;
4034
+ }
4035
+ computeModuleDensity() {
3669
4036
  const density = {};
3670
- const moduleNodes = this.store.findNodes({ type: "module" });
3671
- for (const mod of moduleNodes) {
3672
- const outEdges = this.store.getEdges({ from: mod.id });
3673
- const inEdges = this.store.getEdges({ to: mod.id });
3674
- density[mod.name] = outEdges.length + inEdges.length;
4037
+ for (const mod of this.store.findNodes({ type: "module" })) {
4038
+ const out = this.store.getEdges({ from: mod.id }).length;
4039
+ const inn = this.store.getEdges({ to: mod.id }).length;
4040
+ density[mod.name] = out + inn;
3675
4041
  }
3676
- const boostTypes = phase ? PHASE_NODE_TYPES[phase] : void 0;
3677
- const boostFactor = 2;
3678
- let weightedTotal = 0;
4042
+ return density;
4043
+ }
4044
+ computeTypeWeights(typeCounts, boostTypes) {
3679
4045
  const weights = {};
4046
+ let weightedTotal = 0;
3680
4047
  for (const [type, count] of Object.entries(typeCounts)) {
3681
- const isBoosted = boostTypes?.includes(type);
3682
- const weight = count * (isBoosted ? boostFactor : 1);
4048
+ const weight = count * (boostTypes?.includes(type) ? 2 : 1);
3683
4049
  weights[type] = weight;
3684
4050
  weightedTotal += weight;
3685
4051
  }
4052
+ return { weights, weightedTotal };
4053
+ }
4054
+ allocateProportionally(weights, weightedTotal, totalTokens) {
3686
4055
  const allocations = {};
3687
- if (weightedTotal > 0) {
3688
- let allocated = 0;
3689
- const types = Object.keys(weights);
3690
- for (let i = 0; i < types.length; i++) {
3691
- const type = types[i];
3692
- if (i === types.length - 1) {
3693
- allocations[type] = totalTokens - allocated;
3694
- } else {
3695
- const share = Math.round(weights[type] / weightedTotal * totalTokens);
3696
- allocations[type] = share;
3697
- allocated += share;
3698
- }
4056
+ if (weightedTotal === 0) return allocations;
4057
+ let allocated = 0;
4058
+ const types = Object.keys(weights);
4059
+ for (let i = 0; i < types.length; i++) {
4060
+ const type = types[i];
4061
+ if (i === types.length - 1) {
4062
+ allocations[type] = totalTokens - allocated;
4063
+ } else {
4064
+ const share = Math.round(weights[type] / weightedTotal * totalTokens);
4065
+ allocations[type] = share;
4066
+ allocated += share;
3699
4067
  }
3700
4068
  }
4069
+ return allocations;
4070
+ }
4071
+ computeBudget(totalTokens, phase) {
4072
+ const typeCounts = this.countNodesByType();
4073
+ const density = this.computeModuleDensity();
4074
+ const boostTypes = phase ? PHASE_NODE_TYPES[phase] : void 0;
4075
+ const { weights, weightedTotal } = this.computeTypeWeights(typeCounts, boostTypes);
4076
+ const allocations = this.allocateProportionally(weights, weightedTotal, totalTokens);
3701
4077
  return { total: totalTokens, allocations, density };
3702
4078
  }
3703
4079
  /**
@@ -3728,49 +4104,43 @@ var Assembler = class {
3728
4104
  filePaths: Array.from(filePathSet)
3729
4105
  };
3730
4106
  }
3731
- /**
3732
- * Generate a markdown repository map from graph structure.
3733
- */
3734
- generateMap() {
3735
- const moduleNodes = this.store.findNodes({ type: "module" });
3736
- const modulesWithEdgeCount = moduleNodes.map((mod) => {
3737
- const outEdges = this.store.getEdges({ from: mod.id });
3738
- const inEdges = this.store.getEdges({ to: mod.id });
3739
- return { module: mod, edgeCount: outEdges.length + inEdges.length };
4107
+ buildModuleLines() {
4108
+ const modulesWithEdgeCount = this.store.findNodes({ type: "module" }).map((mod) => {
4109
+ const edgeCount = this.store.getEdges({ from: mod.id }).length + this.store.getEdges({ to: mod.id }).length;
4110
+ return { module: mod, edgeCount };
3740
4111
  });
3741
4112
  modulesWithEdgeCount.sort((a, b) => b.edgeCount - a.edgeCount);
3742
- const lines = ["# Repository Structure", ""];
3743
- if (modulesWithEdgeCount.length > 0) {
3744
- lines.push("## Modules", "");
3745
- for (const { module: mod, edgeCount } of modulesWithEdgeCount) {
3746
- lines.push(`### ${mod.name} (${edgeCount} connections)`);
3747
- lines.push("");
3748
- const containsEdges = this.store.getEdges({ from: mod.id, type: "contains" });
3749
- for (const edge of containsEdges) {
3750
- const fileNode = this.store.getNode(edge.to);
3751
- if (fileNode && fileNode.type === "file") {
3752
- const symbolEdges = this.store.getEdges({ from: fileNode.id, type: "contains" });
3753
- lines.push(`- ${fileNode.path ?? fileNode.name} (${symbolEdges.length} symbols)`);
3754
- }
4113
+ if (modulesWithEdgeCount.length === 0) return [];
4114
+ const lines = ["## Modules", ""];
4115
+ for (const { module: mod, edgeCount } of modulesWithEdgeCount) {
4116
+ lines.push(`### ${mod.name} (${edgeCount} connections)`, "");
4117
+ for (const edge of this.store.getEdges({ from: mod.id, type: "contains" })) {
4118
+ const fileNode = this.store.getNode(edge.to);
4119
+ if (fileNode?.type === "file") {
4120
+ const symbols = this.store.getEdges({ from: fileNode.id, type: "contains" }).length;
4121
+ lines.push(`- ${fileNode.path ?? fileNode.name} (${symbols} symbols)`);
3755
4122
  }
3756
- lines.push("");
3757
4123
  }
4124
+ lines.push("");
3758
4125
  }
3759
- const fileNodes = this.store.findNodes({ type: "file" });
3760
- const nonBarrelFiles = fileNodes.filter((n) => !n.name.startsWith("index."));
3761
- const filesWithOutDegree = nonBarrelFiles.map((f) => {
3762
- const outEdges = this.store.getEdges({ from: f.id });
3763
- return { file: f, outDegree: outEdges.length };
3764
- });
4126
+ return lines;
4127
+ }
4128
+ buildEntryPointLines() {
4129
+ 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 }));
3765
4130
  filesWithOutDegree.sort((a, b) => b.outDegree - a.outDegree);
3766
4131
  const entryPoints = filesWithOutDegree.filter((f) => f.outDegree > 0).slice(0, 5);
3767
- if (entryPoints.length > 0) {
3768
- lines.push("## Entry Points", "");
3769
- for (const { file, outDegree } of entryPoints) {
3770
- lines.push(`- ${file.path ?? file.name} (${outDegree} outbound edges)`);
3771
- }
3772
- lines.push("");
4132
+ if (entryPoints.length === 0) return [];
4133
+ const lines = ["## Entry Points", ""];
4134
+ for (const { file, outDegree } of entryPoints) {
4135
+ lines.push(`- ${file.path ?? file.name} (${outDegree} outbound edges)`);
3773
4136
  }
4137
+ lines.push("");
4138
+ return lines;
4139
+ }
4140
+ generateMap() {
4141
+ const lines = ["# Repository Structure", ""];
4142
+ lines.push(...this.buildModuleLines());
4143
+ lines.push(...this.buildEntryPointLines());
3774
4144
  return lines.join("\n");
3775
4145
  }
3776
4146
  /**
@@ -3803,6 +4173,59 @@ var Assembler = class {
3803
4173
  };
3804
4174
 
3805
4175
  // src/query/Traceability.ts
4176
+ function extractConfidence(edge) {
4177
+ return edge.confidence ?? edge.metadata?.confidence ?? 0;
4178
+ }
4179
+ function extractMethod(edge) {
4180
+ return edge.metadata?.method ?? "convention";
4181
+ }
4182
+ function edgesToTracedFiles(store, edges) {
4183
+ return edges.map((edge) => ({
4184
+ path: store.getNode(edge.to)?.path ?? edge.to,
4185
+ confidence: extractConfidence(edge),
4186
+ method: extractMethod(edge)
4187
+ }));
4188
+ }
4189
+ function determineCoverageStatus(hasCode, hasTests) {
4190
+ if (hasCode && hasTests) return "full";
4191
+ if (hasCode) return "code-only";
4192
+ if (hasTests) return "test-only";
4193
+ return "none";
4194
+ }
4195
+ function computeMaxConfidence(codeFiles, testFiles) {
4196
+ const allConfidences = [
4197
+ ...codeFiles.map((f) => f.confidence),
4198
+ ...testFiles.map((f) => f.confidence)
4199
+ ];
4200
+ return allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
4201
+ }
4202
+ function buildRequirementCoverage(store, req) {
4203
+ const codeFiles = edgesToTracedFiles(store, store.getEdges({ from: req.id, type: "requires" }));
4204
+ const testFiles = edgesToTracedFiles(
4205
+ store,
4206
+ store.getEdges({ from: req.id, type: "verified_by" })
4207
+ );
4208
+ const hasCode = codeFiles.length > 0;
4209
+ const hasTests = testFiles.length > 0;
4210
+ return {
4211
+ requirementId: req.id,
4212
+ requirementName: req.name,
4213
+ index: req.metadata?.index ?? 0,
4214
+ codeFiles,
4215
+ testFiles,
4216
+ status: determineCoverageStatus(hasCode, hasTests),
4217
+ maxConfidence: computeMaxConfidence(codeFiles, testFiles)
4218
+ };
4219
+ }
4220
+ function computeSummary(requirements) {
4221
+ const total = requirements.length;
4222
+ const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
4223
+ const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
4224
+ const fullyTraced = requirements.filter((r) => r.status === "full").length;
4225
+ const untraceable = requirements.filter((r) => r.status === "none").length;
4226
+ const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
4227
+ return { total, withCode, withTests, fullyTraced, untraceable, coveragePercent };
4228
+ }
3806
4229
  function queryTraceability(store, options) {
3807
4230
  const allRequirements = store.findNodes({ type: "requirement" });
3808
4231
  const filtered = allRequirements.filter((node) => {
@@ -3830,56 +4253,13 @@ function queryTraceability(store, options) {
3830
4253
  const firstMeta = firstReq.metadata;
3831
4254
  const specPath = firstMeta?.specPath ?? "";
3832
4255
  const featureName = firstMeta?.featureName ?? "";
3833
- const requirements = [];
3834
- for (const req of reqs) {
3835
- const requiresEdges = store.getEdges({ from: req.id, type: "requires" });
3836
- const codeFiles = requiresEdges.map((edge) => {
3837
- const targetNode = store.getNode(edge.to);
3838
- return {
3839
- path: targetNode?.path ?? edge.to,
3840
- confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
3841
- method: edge.metadata?.method ?? "convention"
3842
- };
3843
- });
3844
- const verifiedByEdges = store.getEdges({ from: req.id, type: "verified_by" });
3845
- const testFiles = verifiedByEdges.map((edge) => {
3846
- const targetNode = store.getNode(edge.to);
3847
- return {
3848
- path: targetNode?.path ?? edge.to,
3849
- confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
3850
- method: edge.metadata?.method ?? "convention"
3851
- };
3852
- });
3853
- const hasCode = codeFiles.length > 0;
3854
- const hasTests = testFiles.length > 0;
3855
- const status = hasCode && hasTests ? "full" : hasCode ? "code-only" : hasTests ? "test-only" : "none";
3856
- const allConfidences = [
3857
- ...codeFiles.map((f) => f.confidence),
3858
- ...testFiles.map((f) => f.confidence)
3859
- ];
3860
- const maxConfidence = allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
3861
- requirements.push({
3862
- requirementId: req.id,
3863
- requirementName: req.name,
3864
- index: req.metadata?.index ?? 0,
3865
- codeFiles,
3866
- testFiles,
3867
- status,
3868
- maxConfidence
3869
- });
3870
- }
4256
+ const requirements = reqs.map((req) => buildRequirementCoverage(store, req));
3871
4257
  requirements.sort((a, b) => a.index - b.index);
3872
- const total = requirements.length;
3873
- const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
3874
- const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
3875
- const fullyTraced = requirements.filter((r) => r.status === "full").length;
3876
- const untraceable = requirements.filter((r) => r.status === "none").length;
3877
- const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
3878
4258
  results.push({
3879
4259
  specPath,
3880
4260
  featureName,
3881
4261
  requirements,
3882
- summary: { total, withCode, withTests, fullyTraced, untraceable, coveragePercent }
4262
+ summary: computeSummary(requirements)
3883
4263
  });
3884
4264
  }
3885
4265
  return results;
@@ -3894,10 +4274,15 @@ var GraphConstraintAdapter = class {
3894
4274
  }
3895
4275
  store;
3896
4276
  computeDependencyGraph() {
3897
- const fileNodes = this.store.findNodes({ type: "file" });
3898
- const nodes = fileNodes.map((n) => n.path ?? n.id);
3899
- const importsEdges = this.store.getEdges({ type: "imports" });
3900
- const edges = importsEdges.map((e) => {
4277
+ const nodes = this.collectFileNodePaths();
4278
+ const edges = this.collectImportEdges();
4279
+ return { nodes, edges };
4280
+ }
4281
+ collectFileNodePaths() {
4282
+ return this.store.findNodes({ type: "file" }).map((n) => n.path ?? n.id);
4283
+ }
4284
+ collectImportEdges() {
4285
+ return this.store.getEdges({ type: "imports" }).map((e) => {
3901
4286
  const fromNode = this.store.getNode(e.from);
3902
4287
  const toNode = this.store.getNode(e.to);
3903
4288
  const fromPath = fromNode?.path ?? e.from;
@@ -3906,7 +4291,6 @@ var GraphConstraintAdapter = class {
3906
4291
  const line = e.metadata?.line ?? 0;
3907
4292
  return { from: fromPath, to: toPath, importType, line };
3908
4293
  });
3909
- return { nodes, edges };
3910
4294
  }
3911
4295
  computeLayerViolations(layers, rootDir) {
3912
4296
  const { edges } = this.computeDependencyGraph();
@@ -4200,65 +4584,53 @@ var GraphFeedbackAdapter = class {
4200
4584
  const affectedDocs = [];
4201
4585
  let impactScope = 0;
4202
4586
  for (const filePath of changedFiles) {
4203
- const fileNodes = this.store.findNodes({ path: filePath });
4204
- if (fileNodes.length === 0) continue;
4205
- const fileNode = fileNodes[0];
4206
- const inboundImports = this.store.getEdges({ to: fileNode.id, type: "imports" });
4207
- for (const edge of inboundImports) {
4208
- const importerNode = this.store.getNode(edge.from);
4209
- if (importerNode?.path && /test/i.test(importerNode.path)) {
4210
- affectedTests.push({
4211
- testFile: importerNode.path,
4212
- coversFile: filePath
4213
- });
4214
- }
4215
- impactScope++;
4216
- }
4217
- const docsEdges = this.store.getEdges({ to: fileNode.id, type: "documents" });
4218
- for (const edge of docsEdges) {
4219
- const docNode = this.store.getNode(edge.from);
4220
- if (docNode) {
4221
- affectedDocs.push({
4222
- docFile: docNode.path ?? docNode.name,
4223
- documentsFile: filePath
4224
- });
4225
- }
4226
- }
4587
+ const fileNode = this.store.findNodes({ path: filePath })[0];
4588
+ if (!fileNode) continue;
4589
+ const counts = this.collectFileImpact(fileNode.id, filePath, affectedTests, affectedDocs);
4590
+ impactScope += counts.impactScope;
4227
4591
  }
4228
4592
  return { affectedTests, affectedDocs, impactScope };
4229
4593
  }
4230
- computeHarnessCheckData() {
4231
- const nodeCount = this.store.nodeCount;
4232
- const edgeCount = this.store.edgeCount;
4233
- const violatesEdges = this.store.getEdges({ type: "violates" });
4234
- const constraintViolations = violatesEdges.length;
4235
- const fileNodes = this.store.findNodes({ type: "file" });
4236
- let undocumentedFiles = 0;
4237
- for (const node of fileNodes) {
4238
- const docsEdges = this.store.getEdges({ to: node.id, type: "documents" });
4239
- if (docsEdges.length === 0) {
4240
- undocumentedFiles++;
4594
+ collectFileImpact(fileNodeId, filePath, affectedTests, affectedDocs) {
4595
+ const inboundImports = this.store.getEdges({ to: fileNodeId, type: "imports" });
4596
+ for (const edge of inboundImports) {
4597
+ const importerNode = this.store.getNode(edge.from);
4598
+ if (importerNode?.path && /test/i.test(importerNode.path)) {
4599
+ affectedTests.push({ testFile: importerNode.path, coversFile: filePath });
4241
4600
  }
4242
4601
  }
4243
- let unreachableNodes = 0;
4244
- for (const node of fileNodes) {
4245
- const inboundImports = this.store.getEdges({ to: node.id, type: "imports" });
4246
- if (inboundImports.length === 0) {
4247
- const isEntryPoint = node.name === "index.ts" || node.path !== void 0 && node.path.endsWith("/index.ts") || node.metadata?.entryPoint === true;
4248
- if (!isEntryPoint) {
4249
- unreachableNodes++;
4250
- }
4602
+ const docsEdges = this.store.getEdges({ to: fileNodeId, type: "documents" });
4603
+ for (const edge of docsEdges) {
4604
+ const docNode = this.store.getNode(edge.from);
4605
+ if (docNode) {
4606
+ affectedDocs.push({ docFile: docNode.path ?? docNode.name, documentsFile: filePath });
4251
4607
  }
4252
4608
  }
4609
+ return { impactScope: inboundImports.length };
4610
+ }
4611
+ computeHarnessCheckData() {
4612
+ const fileNodes = this.store.findNodes({ type: "file" });
4253
4613
  return {
4254
4614
  graphExists: true,
4255
- nodeCount,
4256
- edgeCount,
4257
- constraintViolations,
4258
- undocumentedFiles,
4259
- unreachableNodes
4615
+ nodeCount: this.store.nodeCount,
4616
+ edgeCount: this.store.edgeCount,
4617
+ constraintViolations: this.store.getEdges({ type: "violates" }).length,
4618
+ undocumentedFiles: this.countUndocumentedFiles(fileNodes),
4619
+ unreachableNodes: this.countUnreachableNodes(fileNodes)
4260
4620
  };
4261
4621
  }
4622
+ countUndocumentedFiles(fileNodes) {
4623
+ return fileNodes.filter(
4624
+ (node) => this.store.getEdges({ to: node.id, type: "documents" }).length === 0
4625
+ ).length;
4626
+ }
4627
+ countUnreachableNodes(fileNodes) {
4628
+ return fileNodes.filter((node) => {
4629
+ if (this.store.getEdges({ to: node.id, type: "imports" }).length > 0) return false;
4630
+ const isEntryPoint = node.name === "index.ts" || node.path !== void 0 && node.path.endsWith("/index.ts") || node.metadata?.entryPoint === true;
4631
+ return !isEntryPoint;
4632
+ }).length;
4633
+ }
4262
4634
  };
4263
4635
 
4264
4636
  // src/independence/TaskIndependenceAnalyzer.ts
@@ -4275,47 +4647,46 @@ var TaskIndependenceAnalyzer = class {
4275
4647
  this.validate(tasks);
4276
4648
  const useGraph = this.store != null && depth > 0;
4277
4649
  const analysisLevel = useGraph ? "graph-expanded" : "file-only";
4650
+ const { originalFiles, expandedFiles } = this.buildFileSets(tasks, useGraph, depth, edgeTypes);
4651
+ const taskIds = tasks.map((t) => t.id);
4652
+ const pairs = this.computeAllPairs(taskIds, originalFiles, expandedFiles);
4653
+ const groups = this.buildGroups(taskIds, pairs);
4654
+ const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
4655
+ return { tasks: taskIds, analysisLevel, depth, pairs, groups, verdict };
4656
+ }
4657
+ // --- Private methods ---
4658
+ buildFileSets(tasks, useGraph, depth, edgeTypes) {
4278
4659
  const originalFiles = /* @__PURE__ */ new Map();
4279
4660
  const expandedFiles = /* @__PURE__ */ new Map();
4280
4661
  for (const task of tasks) {
4281
- const origSet = new Set(task.files);
4282
- originalFiles.set(task.id, origSet);
4283
- if (useGraph) {
4284
- const expanded = this.expandViaGraph(task.files, depth, edgeTypes);
4285
- expandedFiles.set(task.id, expanded);
4286
- } else {
4287
- expandedFiles.set(task.id, /* @__PURE__ */ new Map());
4288
- }
4662
+ originalFiles.set(task.id, new Set(task.files));
4663
+ expandedFiles.set(
4664
+ task.id,
4665
+ useGraph ? this.expandViaGraph(task.files, depth, edgeTypes) : /* @__PURE__ */ new Map()
4666
+ );
4289
4667
  }
4290
- const taskIds = tasks.map((t) => t.id);
4668
+ return { originalFiles, expandedFiles };
4669
+ }
4670
+ computeAllPairs(taskIds, originalFiles, expandedFiles) {
4291
4671
  const pairs = [];
4292
4672
  for (let i = 0; i < taskIds.length; i++) {
4293
4673
  for (let j = i + 1; j < taskIds.length; j++) {
4294
4674
  const idA = taskIds[i];
4295
4675
  const idB = taskIds[j];
4296
- const pair = this.computePairOverlap(
4297
- idA,
4298
- idB,
4299
- originalFiles.get(idA),
4300
- originalFiles.get(idB),
4301
- expandedFiles.get(idA),
4302
- expandedFiles.get(idB)
4676
+ pairs.push(
4677
+ this.computePairOverlap(
4678
+ idA,
4679
+ idB,
4680
+ originalFiles.get(idA),
4681
+ originalFiles.get(idB),
4682
+ expandedFiles.get(idA),
4683
+ expandedFiles.get(idB)
4684
+ )
4303
4685
  );
4304
- pairs.push(pair);
4305
4686
  }
4306
4687
  }
4307
- const groups = this.buildGroups(taskIds, pairs);
4308
- const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
4309
- return {
4310
- tasks: taskIds,
4311
- analysisLevel,
4312
- depth,
4313
- pairs,
4314
- groups,
4315
- verdict
4316
- };
4688
+ return pairs;
4317
4689
  }
4318
- // --- Private methods ---
4319
4690
  validate(tasks) {
4320
4691
  if (tasks.length < 2) {
4321
4692
  throw new Error("At least 2 tasks are required for independence analysis");
@@ -4468,27 +4839,62 @@ var ConflictPredictor = class {
4468
4839
  predict(params) {
4469
4840
  const analyzer = new TaskIndependenceAnalyzer(this.store);
4470
4841
  const result = analyzer.analyze(params);
4842
+ const { churnMap, couplingMap, churnThreshold, couplingThreshold } = this.buildMetricMaps();
4843
+ const conflicts = this.classifyConflicts(
4844
+ result.pairs,
4845
+ churnMap,
4846
+ couplingMap,
4847
+ churnThreshold,
4848
+ couplingThreshold
4849
+ );
4850
+ const taskIds = result.tasks;
4851
+ const groups = this.buildHighSeverityGroups(taskIds, conflicts);
4852
+ const regrouped = !this.groupsEqual(result.groups, groups);
4853
+ const { highCount, mediumCount, lowCount } = this.countBySeverity(conflicts);
4854
+ const verdict = this.generateVerdict(
4855
+ taskIds,
4856
+ groups,
4857
+ result.analysisLevel,
4858
+ highCount,
4859
+ mediumCount,
4860
+ lowCount,
4861
+ regrouped
4862
+ );
4863
+ return {
4864
+ tasks: taskIds,
4865
+ analysisLevel: result.analysisLevel,
4866
+ depth: result.depth,
4867
+ conflicts,
4868
+ groups,
4869
+ summary: { high: highCount, medium: mediumCount, low: lowCount, regrouped },
4870
+ verdict
4871
+ };
4872
+ }
4873
+ // --- Private helpers ---
4874
+ buildMetricMaps() {
4471
4875
  const churnMap = /* @__PURE__ */ new Map();
4472
4876
  const couplingMap = /* @__PURE__ */ new Map();
4473
- let churnThreshold = Infinity;
4474
- let couplingThreshold = Infinity;
4475
- if (this.store != null) {
4476
- const complexityResult = new GraphComplexityAdapter(this.store).computeComplexityHotspots();
4477
- for (const hotspot of complexityResult.hotspots) {
4478
- const existing = churnMap.get(hotspot.file);
4479
- if (existing === void 0 || hotspot.changeFrequency > existing) {
4480
- churnMap.set(hotspot.file, hotspot.changeFrequency);
4481
- }
4877
+ if (this.store == null) {
4878
+ return { churnMap, couplingMap, churnThreshold: Infinity, couplingThreshold: Infinity };
4879
+ }
4880
+ const complexityResult = new GraphComplexityAdapter(this.store).computeComplexityHotspots();
4881
+ for (const hotspot of complexityResult.hotspots) {
4882
+ const existing = churnMap.get(hotspot.file);
4883
+ if (existing === void 0 || hotspot.changeFrequency > existing) {
4884
+ churnMap.set(hotspot.file, hotspot.changeFrequency);
4482
4885
  }
4483
- const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
4484
- for (const fileData of couplingResult.files) {
4485
- couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
4486
- }
4487
- churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
4488
- couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
4489
4886
  }
4887
+ const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
4888
+ for (const fileData of couplingResult.files) {
4889
+ couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
4890
+ }
4891
+ const churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
4892
+ const couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
4893
+ return { churnMap, couplingMap, churnThreshold, couplingThreshold };
4894
+ }
4895
+ classifyConflicts(pairs, churnMap, couplingMap, churnThreshold, couplingThreshold) {
4490
4896
  const conflicts = [];
4491
- for (const pair of result.pairs) {
4897
+ for (const pair of pairs) {
4492
4898
  if (pair.independent) continue;
4493
4899
  const { severity, reason, mitigation } = this.classifyPair(
4494
4900
  pair.taskA,
@@ -4508,9 +4914,9 @@ var ConflictPredictor = class {
4508
4914
  overlaps: pair.overlaps
4509
4915
  });
4510
4916
  }
4511
- const taskIds = result.tasks;
4512
- const groups = this.buildHighSeverityGroups(taskIds, conflicts);
4513
- const regrouped = !this.groupsEqual(result.groups, groups);
4917
+ return conflicts;
4918
+ }
4919
+ countBySeverity(conflicts) {
4514
4920
  let highCount = 0;
4515
4921
  let mediumCount = 0;
4516
4922
  let lowCount = 0;
@@ -4519,68 +4925,57 @@ var ConflictPredictor = class {
4519
4925
  else if (c.severity === "medium") mediumCount++;
4520
4926
  else lowCount++;
4521
4927
  }
4522
- const verdict = this.generateVerdict(
4523
- taskIds,
4524
- groups,
4525
- result.analysisLevel,
4526
- highCount,
4527
- mediumCount,
4528
- lowCount,
4529
- regrouped
4530
- );
4928
+ return { highCount, mediumCount, lowCount };
4929
+ }
4930
+ classifyTransitiveOverlap(taskA, taskB, overlap, churnMap, couplingMap, churnThreshold, couplingThreshold) {
4931
+ const churn = churnMap.get(overlap.file);
4932
+ const coupling = couplingMap.get(overlap.file);
4933
+ const via = overlap.via ?? "unknown";
4934
+ if (churn !== void 0 && churn >= churnThreshold && churnThreshold !== Infinity) {
4935
+ return {
4936
+ severity: "medium",
4937
+ reason: `Transitive overlap on high-churn file ${overlap.file} (via ${via})`,
4938
+ mitigation: `Review: ${overlap.file} changes frequently \u2014 coordinate edits between ${taskA} and ${taskB}`
4939
+ };
4940
+ }
4941
+ if (coupling !== void 0 && coupling >= couplingThreshold && couplingThreshold !== Infinity) {
4942
+ return {
4943
+ severity: "medium",
4944
+ reason: `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`,
4945
+ mitigation: `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`
4946
+ };
4947
+ }
4531
4948
  return {
4532
- tasks: taskIds,
4533
- analysisLevel: result.analysisLevel,
4534
- depth: result.depth,
4535
- conflicts,
4536
- groups,
4537
- summary: {
4538
- high: highCount,
4539
- medium: mediumCount,
4540
- low: lowCount,
4541
- regrouped
4542
- },
4543
- verdict
4949
+ severity: "low",
4950
+ reason: `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`,
4951
+ mitigation: `Info: transitive overlap unlikely to cause conflicts`
4544
4952
  };
4545
4953
  }
4546
- // --- Private helpers ---
4547
4954
  classifyPair(taskA, taskB, overlaps, churnMap, couplingMap, churnThreshold, couplingThreshold) {
4548
4955
  let maxSeverity = "low";
4549
4956
  let primaryReason = "";
4550
4957
  let primaryMitigation = "";
4551
4958
  for (const overlap of overlaps) {
4552
- let overlapSeverity;
4553
- let reason;
4554
- let mitigation;
4555
- if (overlap.type === "direct") {
4556
- overlapSeverity = "high";
4557
- reason = `Both tasks write to ${overlap.file}`;
4558
- mitigation = `Serialize: run ${taskA} before ${taskB}`;
4559
- } else {
4560
- const churn = churnMap.get(overlap.file);
4561
- const coupling = couplingMap.get(overlap.file);
4562
- const via = overlap.via ?? "unknown";
4563
- if (churn !== void 0 && churn >= churnThreshold && churnThreshold !== Infinity) {
4564
- overlapSeverity = "medium";
4565
- reason = `Transitive overlap on high-churn file ${overlap.file} (via ${via})`;
4566
- mitigation = `Review: ${overlap.file} changes frequently \u2014 coordinate edits between ${taskA} and ${taskB}`;
4567
- } else if (coupling !== void 0 && coupling >= couplingThreshold && couplingThreshold !== Infinity) {
4568
- overlapSeverity = "medium";
4569
- reason = `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`;
4570
- mitigation = `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`;
4571
- } else {
4572
- overlapSeverity = "low";
4573
- reason = `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`;
4574
- mitigation = `Info: transitive overlap unlikely to cause conflicts`;
4575
- }
4576
- }
4577
- if (this.severityRank(overlapSeverity) > this.severityRank(maxSeverity)) {
4578
- maxSeverity = overlapSeverity;
4579
- primaryReason = reason;
4580
- primaryMitigation = mitigation;
4959
+ const classified = overlap.type === "direct" ? {
4960
+ severity: "high",
4961
+ reason: `Both tasks write to ${overlap.file}`,
4962
+ mitigation: `Serialize: run ${taskA} before ${taskB}`
4963
+ } : this.classifyTransitiveOverlap(
4964
+ taskA,
4965
+ taskB,
4966
+ overlap,
4967
+ churnMap,
4968
+ couplingMap,
4969
+ churnThreshold,
4970
+ couplingThreshold
4971
+ );
4972
+ if (this.severityRank(classified.severity) > this.severityRank(maxSeverity)) {
4973
+ maxSeverity = classified.severity;
4974
+ primaryReason = classified.reason;
4975
+ primaryMitigation = classified.mitigation;
4581
4976
  } else if (primaryReason === "") {
4582
- primaryReason = reason;
4583
- primaryMitigation = mitigation;
4977
+ primaryReason = classified.reason;
4978
+ primaryMitigation = classified.mitigation;
4584
4979
  }
4585
4980
  }
4586
4981
  return { severity: maxSeverity, reason: primaryReason, mitigation: primaryMitigation };
@@ -4703,12 +5098,14 @@ var ConflictPredictor = class {
4703
5098
  };
4704
5099
 
4705
5100
  // src/index.ts
4706
- var VERSION = "0.2.0";
5101
+ var VERSION = "0.4.1";
4707
5102
  export {
4708
5103
  Assembler,
4709
5104
  CIConnector,
4710
5105
  CURRENT_SCHEMA_VERSION,
5106
+ CascadeSimulator,
4711
5107
  CodeIngestor,
5108
+ CompositeProbabilityStrategy,
4712
5109
  ConflictPredictor,
4713
5110
  ConfluenceConnector,
4714
5111
  ContextQL,
@@ -4743,6 +5140,7 @@ export {
4743
5140
  VERSION,
4744
5141
  VectorStore,
4745
5142
  askGraph,
5143
+ classifyNodeCategory,
4746
5144
  groupNodesByImpact,
4747
5145
  linkToCode,
4748
5146
  loadGraph,