@harness-engineering/graph 0.4.1 → 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.d.mts +58 -7
- package/dist/index.d.ts +58 -7
- package/dist/index.js +854 -730
- package/dist/index.mjs +854 -730
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -342,21 +342,26 @@ var GraphStore = class {
|
|
|
342
342
|
return this.edgeMap.values();
|
|
343
343
|
}
|
|
344
344
|
getNeighbors(nodeId, direction = "both") {
|
|
345
|
-
const neighborIds =
|
|
345
|
+
const neighborIds = this.collectNeighborIds(nodeId, direction);
|
|
346
|
+
return this.resolveNodes(neighborIds);
|
|
347
|
+
}
|
|
348
|
+
collectNeighborIds(nodeId, direction) {
|
|
349
|
+
const ids = /* @__PURE__ */ new Set();
|
|
346
350
|
if (direction === "outbound" || direction === "both") {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
neighborIds.add(edge.to);
|
|
351
|
+
for (const edge of this.edgesByFrom.get(nodeId) ?? []) {
|
|
352
|
+
ids.add(edge.to);
|
|
350
353
|
}
|
|
351
354
|
}
|
|
352
355
|
if (direction === "inbound" || direction === "both") {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
neighborIds.add(edge.from);
|
|
356
|
+
for (const edge of this.edgesByTo.get(nodeId) ?? []) {
|
|
357
|
+
ids.add(edge.from);
|
|
356
358
|
}
|
|
357
359
|
}
|
|
360
|
+
return ids;
|
|
361
|
+
}
|
|
362
|
+
resolveNodes(ids) {
|
|
358
363
|
const results = [];
|
|
359
|
-
for (const nid of
|
|
364
|
+
for (const nid of ids) {
|
|
360
365
|
const node = this.getNode(nid);
|
|
361
366
|
if (node) results.push(node);
|
|
362
367
|
}
|
|
@@ -1103,6 +1108,17 @@ var CodeIngestor = class {
|
|
|
1103
1108
|
var import_node_child_process = require("child_process");
|
|
1104
1109
|
var import_node_util = require("util");
|
|
1105
1110
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
1111
|
+
function finalizeCommit(current) {
|
|
1112
|
+
return {
|
|
1113
|
+
hash: current.hash,
|
|
1114
|
+
shortHash: current.shortHash,
|
|
1115
|
+
author: current.author,
|
|
1116
|
+
email: current.email,
|
|
1117
|
+
date: current.date,
|
|
1118
|
+
message: current.message,
|
|
1119
|
+
files: current.files
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1106
1122
|
var GitIngestor = class {
|
|
1107
1123
|
constructor(store, gitRunner) {
|
|
1108
1124
|
this.store = store;
|
|
@@ -1139,39 +1155,49 @@ var GitIngestor = class {
|
|
|
1139
1155
|
}
|
|
1140
1156
|
const commits = this.parseGitLog(output);
|
|
1141
1157
|
for (const commit of commits) {
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1158
|
+
const counts = this.ingestCommit(commit);
|
|
1159
|
+
nodesAdded += counts.nodesAdded;
|
|
1160
|
+
edgesAdded += counts.edgesAdded;
|
|
1161
|
+
}
|
|
1162
|
+
edgesAdded += this.ingestCoChanges(commits);
|
|
1163
|
+
return {
|
|
1164
|
+
nodesAdded,
|
|
1165
|
+
nodesUpdated,
|
|
1166
|
+
edgesAdded,
|
|
1167
|
+
edgesUpdated,
|
|
1168
|
+
errors,
|
|
1169
|
+
durationMs: Date.now() - start
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
ingestCommit(commit) {
|
|
1173
|
+
const nodeId = `commit:${commit.shortHash}`;
|
|
1174
|
+
this.store.addNode({
|
|
1175
|
+
id: nodeId,
|
|
1176
|
+
type: "commit",
|
|
1177
|
+
name: commit.message,
|
|
1178
|
+
metadata: {
|
|
1179
|
+
author: commit.author,
|
|
1180
|
+
email: commit.email,
|
|
1181
|
+
date: commit.date,
|
|
1182
|
+
hash: commit.hash
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
let edgesAdded = 0;
|
|
1186
|
+
for (const file of commit.files) {
|
|
1187
|
+
const fileNodeId = `file:${file}`;
|
|
1188
|
+
if (this.store.getNode(fileNodeId)) {
|
|
1189
|
+
this.store.addEdge({ from: fileNodeId, to: nodeId, type: "triggered_by" });
|
|
1190
|
+
edgesAdded++;
|
|
1166
1191
|
}
|
|
1167
1192
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1193
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1194
|
+
}
|
|
1195
|
+
ingestCoChanges(commits) {
|
|
1196
|
+
let edgesAdded = 0;
|
|
1197
|
+
for (const { fileA, fileB, count } of this.computeCoChanges(commits)) {
|
|
1170
1198
|
const fileAId = `file:${fileA}`;
|
|
1171
1199
|
const fileBId = `file:${fileB}`;
|
|
1172
|
-
|
|
1173
|
-
const nodeB = this.store.getNode(fileBId);
|
|
1174
|
-
if (nodeA && nodeB) {
|
|
1200
|
+
if (this.store.getNode(fileAId) && this.store.getNode(fileBId)) {
|
|
1175
1201
|
this.store.addEdge({
|
|
1176
1202
|
from: fileAId,
|
|
1177
1203
|
to: fileBId,
|
|
@@ -1181,14 +1207,7 @@ var GitIngestor = class {
|
|
|
1181
1207
|
edgesAdded++;
|
|
1182
1208
|
}
|
|
1183
1209
|
}
|
|
1184
|
-
return
|
|
1185
|
-
nodesAdded,
|
|
1186
|
-
nodesUpdated,
|
|
1187
|
-
edgesAdded,
|
|
1188
|
-
edgesUpdated,
|
|
1189
|
-
errors,
|
|
1190
|
-
durationMs: Date.now() - start
|
|
1191
|
-
};
|
|
1210
|
+
return edgesAdded;
|
|
1192
1211
|
}
|
|
1193
1212
|
async runGit(rootDir, args) {
|
|
1194
1213
|
if (this.gitRunner) {
|
|
@@ -1203,63 +1222,49 @@ var GitIngestor = class {
|
|
|
1203
1222
|
const lines = output.split("\n");
|
|
1204
1223
|
let current = null;
|
|
1205
1224
|
for (const line of lines) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1225
|
+
current = this.processLogLine(line, current, commits);
|
|
1226
|
+
}
|
|
1227
|
+
if (current) {
|
|
1228
|
+
commits.push(finalizeCommit(current));
|
|
1229
|
+
}
|
|
1230
|
+
return commits;
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Process one line from git log output, updating the in-progress commit builder
|
|
1234
|
+
* and flushing completed commits into the accumulator.
|
|
1235
|
+
* Returns the updated current builder (null if flushed and not replaced).
|
|
1236
|
+
*/
|
|
1237
|
+
processLogLine(line, current, commits) {
|
|
1238
|
+
const trimmed = line.trim();
|
|
1239
|
+
if (!trimmed) {
|
|
1240
|
+
if (current?.hasFiles) {
|
|
1241
|
+
commits.push(finalizeCommit(current));
|
|
1242
|
+
return null;
|
|
1221
1243
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
author: current.author,
|
|
1229
|
-
email: current.email,
|
|
1230
|
-
date: current.date,
|
|
1231
|
-
message: current.message,
|
|
1232
|
-
files: current.files
|
|
1233
|
-
});
|
|
1234
|
-
}
|
|
1235
|
-
current = {
|
|
1236
|
-
hash: parts[0],
|
|
1237
|
-
shortHash: parts[0].substring(0, 7),
|
|
1238
|
-
author: parts[1],
|
|
1239
|
-
email: parts[2],
|
|
1240
|
-
date: parts[3],
|
|
1241
|
-
message: parts.slice(4).join("|"),
|
|
1242
|
-
// message may contain |
|
|
1243
|
-
files: [],
|
|
1244
|
-
hasFiles: false
|
|
1245
|
-
};
|
|
1246
|
-
} else if (current) {
|
|
1247
|
-
current.files.push(trimmed);
|
|
1248
|
-
current.hasFiles = true;
|
|
1244
|
+
return current;
|
|
1245
|
+
}
|
|
1246
|
+
const parts = trimmed.split("|");
|
|
1247
|
+
if (parts.length >= 5 && /^[0-9a-f]{7,40}$/.test(parts[0])) {
|
|
1248
|
+
if (current) {
|
|
1249
|
+
commits.push(finalizeCommit(current));
|
|
1249
1250
|
}
|
|
1251
|
+
return {
|
|
1252
|
+
hash: parts[0],
|
|
1253
|
+
shortHash: parts[0].substring(0, 7),
|
|
1254
|
+
author: parts[1],
|
|
1255
|
+
email: parts[2],
|
|
1256
|
+
date: parts[3],
|
|
1257
|
+
message: parts.slice(4).join("|"),
|
|
1258
|
+
// message may contain |
|
|
1259
|
+
files: [],
|
|
1260
|
+
hasFiles: false
|
|
1261
|
+
};
|
|
1250
1262
|
}
|
|
1251
1263
|
if (current) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
shortHash: current.shortHash,
|
|
1255
|
-
author: current.author,
|
|
1256
|
-
email: current.email,
|
|
1257
|
-
date: current.date,
|
|
1258
|
-
message: current.message,
|
|
1259
|
-
files: current.files
|
|
1260
|
-
});
|
|
1264
|
+
current.files.push(trimmed);
|
|
1265
|
+
current.hasFiles = true;
|
|
1261
1266
|
}
|
|
1262
|
-
return
|
|
1267
|
+
return current;
|
|
1263
1268
|
}
|
|
1264
1269
|
computeCoChanges(commits) {
|
|
1265
1270
|
const pairCounts = /* @__PURE__ */ new Map();
|
|
@@ -1403,50 +1408,25 @@ var KnowledgeIngestor = class {
|
|
|
1403
1408
|
try {
|
|
1404
1409
|
const content = await fs2.readFile(filePath, "utf-8");
|
|
1405
1410
|
const filename = path3.basename(filePath, ".md");
|
|
1406
|
-
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1407
|
-
const title = titleMatch ? titleMatch[1].trim() : filename;
|
|
1408
|
-
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
1409
|
-
const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
|
|
1410
|
-
const date = dateMatch ? dateMatch[1].trim() : void 0;
|
|
1411
|
-
const status = statusMatch ? statusMatch[1].trim() : void 0;
|
|
1412
1411
|
const nodeId = `adr:${filename}`;
|
|
1413
|
-
this.store.addNode(
|
|
1414
|
-
id: nodeId,
|
|
1415
|
-
type: "adr",
|
|
1416
|
-
name: title,
|
|
1417
|
-
path: filePath,
|
|
1418
|
-
metadata: { date, status }
|
|
1419
|
-
});
|
|
1412
|
+
this.store.addNode(parseADRNode(nodeId, filePath, filename, content));
|
|
1420
1413
|
nodesAdded++;
|
|
1421
1414
|
edgesAdded += this.linkToCode(content, nodeId, "documents");
|
|
1422
1415
|
} catch (err) {
|
|
1423
1416
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1424
1417
|
}
|
|
1425
1418
|
}
|
|
1426
|
-
return
|
|
1427
|
-
nodesAdded,
|
|
1428
|
-
nodesUpdated: 0,
|
|
1429
|
-
edgesAdded,
|
|
1430
|
-
edgesUpdated: 0,
|
|
1431
|
-
errors,
|
|
1432
|
-
durationMs: Date.now() - start
|
|
1433
|
-
};
|
|
1419
|
+
return buildResult(nodesAdded, edgesAdded, errors, start);
|
|
1434
1420
|
}
|
|
1435
1421
|
async ingestLearnings(projectPath) {
|
|
1436
1422
|
const start = Date.now();
|
|
1437
1423
|
const filePath = path3.join(projectPath, ".harness", "learnings.md");
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
content = await fs2.readFile(filePath, "utf-8");
|
|
1441
|
-
} catch {
|
|
1442
|
-
return emptyResult(Date.now() - start);
|
|
1443
|
-
}
|
|
1444
|
-
const errors = [];
|
|
1424
|
+
const content = await readFileOrEmpty(filePath);
|
|
1425
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
1445
1426
|
let nodesAdded = 0;
|
|
1446
1427
|
let edgesAdded = 0;
|
|
1447
|
-
const lines = content.split("\n");
|
|
1448
1428
|
let currentDate;
|
|
1449
|
-
for (const line of
|
|
1429
|
+
for (const line of content.split("\n")) {
|
|
1450
1430
|
const headingMatch = line.match(/^##\s+(\S+)/);
|
|
1451
1431
|
if (headingMatch) {
|
|
1452
1432
|
currentDate = headingMatch[1];
|
|
@@ -1455,70 +1435,29 @@ var KnowledgeIngestor = class {
|
|
|
1455
1435
|
const bulletMatch = line.match(/^-\s+(.+)/);
|
|
1456
1436
|
if (!bulletMatch) continue;
|
|
1457
1437
|
const text = bulletMatch[1];
|
|
1458
|
-
const skillMatch = text.match(/\[skill:([^\]]+)\]/);
|
|
1459
|
-
const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
|
|
1460
|
-
const skill = skillMatch ? skillMatch[1] : void 0;
|
|
1461
|
-
const outcome = outcomeMatch ? outcomeMatch[1] : void 0;
|
|
1462
1438
|
const nodeId = `learning:${hash(text)}`;
|
|
1463
|
-
this.store.addNode(
|
|
1464
|
-
id: nodeId,
|
|
1465
|
-
type: "learning",
|
|
1466
|
-
name: text,
|
|
1467
|
-
metadata: { skill, outcome, date: currentDate }
|
|
1468
|
-
});
|
|
1439
|
+
this.store.addNode(parseLearningNode(nodeId, text, currentDate));
|
|
1469
1440
|
nodesAdded++;
|
|
1470
1441
|
edgesAdded += this.linkToCode(text, nodeId, "applies_to");
|
|
1471
1442
|
}
|
|
1472
|
-
return
|
|
1473
|
-
nodesAdded,
|
|
1474
|
-
nodesUpdated: 0,
|
|
1475
|
-
edgesAdded,
|
|
1476
|
-
edgesUpdated: 0,
|
|
1477
|
-
errors,
|
|
1478
|
-
durationMs: Date.now() - start
|
|
1479
|
-
};
|
|
1443
|
+
return buildResult(nodesAdded, edgesAdded, [], start);
|
|
1480
1444
|
}
|
|
1481
1445
|
async ingestFailures(projectPath) {
|
|
1482
1446
|
const start = Date.now();
|
|
1483
1447
|
const filePath = path3.join(projectPath, ".harness", "failures.md");
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
content = await fs2.readFile(filePath, "utf-8");
|
|
1487
|
-
} catch {
|
|
1488
|
-
return emptyResult(Date.now() - start);
|
|
1489
|
-
}
|
|
1490
|
-
const errors = [];
|
|
1448
|
+
const content = await readFileOrEmpty(filePath);
|
|
1449
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
1491
1450
|
let nodesAdded = 0;
|
|
1492
1451
|
let edgesAdded = 0;
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
|
|
1499
|
-
const date = dateMatch ? dateMatch[1].trim() : void 0;
|
|
1500
|
-
const skill = skillMatch ? skillMatch[1].trim() : void 0;
|
|
1501
|
-
const failureType = typeMatch ? typeMatch[1].trim() : void 0;
|
|
1502
|
-
const description = descMatch ? descMatch[1].trim() : void 0;
|
|
1503
|
-
if (!description) continue;
|
|
1504
|
-
const nodeId = `failure:${hash(description)}`;
|
|
1505
|
-
this.store.addNode({
|
|
1506
|
-
id: nodeId,
|
|
1507
|
-
type: "failure",
|
|
1508
|
-
name: description,
|
|
1509
|
-
metadata: { date, skill, type: failureType }
|
|
1510
|
-
});
|
|
1452
|
+
for (const section of content.split(/^##\s+/m).filter((s) => s.trim())) {
|
|
1453
|
+
const parsed = parseFailureSection(section);
|
|
1454
|
+
if (!parsed) continue;
|
|
1455
|
+
const { description, node } = parsed;
|
|
1456
|
+
this.store.addNode(node);
|
|
1511
1457
|
nodesAdded++;
|
|
1512
|
-
edgesAdded += this.linkToCode(description,
|
|
1458
|
+
edgesAdded += this.linkToCode(description, node.id, "caused_by");
|
|
1513
1459
|
}
|
|
1514
|
-
return
|
|
1515
|
-
nodesAdded,
|
|
1516
|
-
nodesUpdated: 0,
|
|
1517
|
-
edgesAdded,
|
|
1518
|
-
edgesUpdated: 0,
|
|
1519
|
-
errors,
|
|
1520
|
-
durationMs: Date.now() - start
|
|
1521
|
-
};
|
|
1460
|
+
return buildResult(nodesAdded, edgesAdded, [], start);
|
|
1522
1461
|
}
|
|
1523
1462
|
async ingestAll(projectPath, opts) {
|
|
1524
1463
|
const start = Date.now();
|
|
@@ -1572,6 +1511,74 @@ var KnowledgeIngestor = class {
|
|
|
1572
1511
|
return results;
|
|
1573
1512
|
}
|
|
1574
1513
|
};
|
|
1514
|
+
async function readFileOrEmpty(filePath) {
|
|
1515
|
+
try {
|
|
1516
|
+
return await fs2.readFile(filePath, "utf-8");
|
|
1517
|
+
} catch {
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
function buildResult(nodesAdded, edgesAdded, errors, start) {
|
|
1522
|
+
return {
|
|
1523
|
+
nodesAdded,
|
|
1524
|
+
nodesUpdated: 0,
|
|
1525
|
+
edgesAdded,
|
|
1526
|
+
edgesUpdated: 0,
|
|
1527
|
+
errors,
|
|
1528
|
+
durationMs: Date.now() - start
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function parseADRNode(nodeId, filePath, filename, content) {
|
|
1532
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1533
|
+
const title = titleMatch ? titleMatch[1].trim() : filename;
|
|
1534
|
+
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
1535
|
+
const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
|
|
1536
|
+
return {
|
|
1537
|
+
id: nodeId,
|
|
1538
|
+
type: "adr",
|
|
1539
|
+
name: title,
|
|
1540
|
+
path: filePath,
|
|
1541
|
+
metadata: {
|
|
1542
|
+
date: dateMatch ? dateMatch[1].trim() : void 0,
|
|
1543
|
+
status: statusMatch ? statusMatch[1].trim() : void 0
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
function parseLearningNode(nodeId, text, currentDate) {
|
|
1548
|
+
const skillMatch = text.match(/\[skill:([^\]]+)\]/);
|
|
1549
|
+
const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
|
|
1550
|
+
return {
|
|
1551
|
+
id: nodeId,
|
|
1552
|
+
type: "learning",
|
|
1553
|
+
name: text,
|
|
1554
|
+
metadata: {
|
|
1555
|
+
skill: skillMatch ? skillMatch[1] : void 0,
|
|
1556
|
+
outcome: outcomeMatch ? outcomeMatch[1] : void 0,
|
|
1557
|
+
date: currentDate
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function parseFailureSection(section) {
|
|
1562
|
+
const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
|
|
1563
|
+
const description = descMatch ? descMatch[1].trim() : void 0;
|
|
1564
|
+
if (!description) return null;
|
|
1565
|
+
const dateMatch = section.match(/\*\*Date:\*\*\s*(.+)/);
|
|
1566
|
+
const skillMatch = section.match(/\*\*Skill:\*\*\s*(.+)/);
|
|
1567
|
+
const typeMatch = section.match(/\*\*Type:\*\*\s*(.+)/);
|
|
1568
|
+
return {
|
|
1569
|
+
description,
|
|
1570
|
+
node: {
|
|
1571
|
+
id: `failure:${hash(description)}`,
|
|
1572
|
+
type: "failure",
|
|
1573
|
+
name: description,
|
|
1574
|
+
metadata: {
|
|
1575
|
+
date: dateMatch ? dateMatch[1].trim() : void 0,
|
|
1576
|
+
skill: skillMatch ? skillMatch[1].trim() : void 0,
|
|
1577
|
+
type: typeMatch ? typeMatch[1].trim() : void 0
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1575
1582
|
|
|
1576
1583
|
// src/ingest/RequirementIngestor.ts
|
|
1577
1584
|
var fs3 = __toESM(require("fs/promises"));
|
|
@@ -1616,40 +1623,9 @@ var RequirementIngestor = class {
|
|
|
1616
1623
|
return emptyResult(Date.now() - start);
|
|
1617
1624
|
}
|
|
1618
1625
|
for (const featureDir of featureDirs) {
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
try {
|
|
1623
|
-
content = await fs3.readFile(specPath, "utf-8");
|
|
1624
|
-
} catch {
|
|
1625
|
-
continue;
|
|
1626
|
-
}
|
|
1627
|
-
try {
|
|
1628
|
-
const specHash = hash(specPath);
|
|
1629
|
-
const specNodeId = `file:${specPath}`;
|
|
1630
|
-
this.store.addNode({
|
|
1631
|
-
id: specNodeId,
|
|
1632
|
-
type: "document",
|
|
1633
|
-
name: path4.basename(specPath),
|
|
1634
|
-
path: specPath,
|
|
1635
|
-
metadata: { featureName }
|
|
1636
|
-
});
|
|
1637
|
-
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1638
|
-
for (const req of requirements) {
|
|
1639
|
-
this.store.addNode(req.node);
|
|
1640
|
-
nodesAdded++;
|
|
1641
|
-
this.store.addEdge({
|
|
1642
|
-
from: req.node.id,
|
|
1643
|
-
to: specNodeId,
|
|
1644
|
-
type: "specifies"
|
|
1645
|
-
});
|
|
1646
|
-
edgesAdded++;
|
|
1647
|
-
edgesAdded += this.linkByPathPattern(req.node.id, featureName);
|
|
1648
|
-
edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
|
|
1649
|
-
}
|
|
1650
|
-
} catch (err) {
|
|
1651
|
-
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1652
|
-
}
|
|
1626
|
+
const counts = await this.ingestFeatureDir(featureDir, errors);
|
|
1627
|
+
nodesAdded += counts.nodesAdded;
|
|
1628
|
+
edgesAdded += counts.edgesAdded;
|
|
1653
1629
|
}
|
|
1654
1630
|
return {
|
|
1655
1631
|
nodesAdded,
|
|
@@ -1660,6 +1636,48 @@ var RequirementIngestor = class {
|
|
|
1660
1636
|
durationMs: Date.now() - start
|
|
1661
1637
|
};
|
|
1662
1638
|
}
|
|
1639
|
+
async ingestFeatureDir(featureDir, errors) {
|
|
1640
|
+
const featureName = path4.basename(featureDir);
|
|
1641
|
+
const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1642
|
+
let content;
|
|
1643
|
+
try {
|
|
1644
|
+
content = await fs3.readFile(specPath, "utf-8");
|
|
1645
|
+
} catch {
|
|
1646
|
+
return { nodesAdded: 0, edgesAdded: 0 };
|
|
1647
|
+
}
|
|
1648
|
+
try {
|
|
1649
|
+
return this.ingestSpec(specPath, content, featureName);
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1652
|
+
return { nodesAdded: 0, edgesAdded: 0 };
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
ingestSpec(specPath, content, featureName) {
|
|
1656
|
+
const specHash = hash(specPath);
|
|
1657
|
+
const specNodeId = `file:${specPath}`;
|
|
1658
|
+
this.store.addNode({
|
|
1659
|
+
id: specNodeId,
|
|
1660
|
+
type: "document",
|
|
1661
|
+
name: path4.basename(specPath),
|
|
1662
|
+
path: specPath,
|
|
1663
|
+
metadata: { featureName }
|
|
1664
|
+
});
|
|
1665
|
+
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1666
|
+
let nodesAdded = 0;
|
|
1667
|
+
let edgesAdded = 0;
|
|
1668
|
+
for (const req of requirements) {
|
|
1669
|
+
const counts = this.ingestRequirement(req.node, specNodeId, featureName);
|
|
1670
|
+
nodesAdded += counts.nodesAdded;
|
|
1671
|
+
edgesAdded += counts.edgesAdded;
|
|
1672
|
+
}
|
|
1673
|
+
return { nodesAdded, edgesAdded };
|
|
1674
|
+
}
|
|
1675
|
+
ingestRequirement(node, specNodeId, featureName) {
|
|
1676
|
+
this.store.addNode(node);
|
|
1677
|
+
this.store.addEdge({ from: node.id, to: specNodeId, type: "specifies" });
|
|
1678
|
+
const edgesAdded = 1 + this.linkByPathPattern(node.id, featureName) + this.linkByKeywordOverlap(node.id, node.name);
|
|
1679
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1680
|
+
}
|
|
1663
1681
|
/**
|
|
1664
1682
|
* Parse markdown content and extract numbered items from recognized sections.
|
|
1665
1683
|
*/
|
|
@@ -1671,54 +1689,80 @@ var RequirementIngestor = class {
|
|
|
1671
1689
|
let globalIndex = 0;
|
|
1672
1690
|
for (let i = 0; i < lines.length; i++) {
|
|
1673
1691
|
const line = lines[i];
|
|
1674
|
-
const
|
|
1675
|
-
if (
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
);
|
|
1680
|
-
if (isReqSection) {
|
|
1681
|
-
currentSection = heading;
|
|
1682
|
-
inRequirementSection = true;
|
|
1683
|
-
} else {
|
|
1684
|
-
inRequirementSection = false;
|
|
1692
|
+
const sectionResult = this.processHeadingLine(line, inRequirementSection);
|
|
1693
|
+
if (sectionResult !== null) {
|
|
1694
|
+
inRequirementSection = sectionResult.inRequirementSection;
|
|
1695
|
+
if (sectionResult.currentSection !== void 0) {
|
|
1696
|
+
currentSection = sectionResult.currentSection;
|
|
1685
1697
|
}
|
|
1686
1698
|
continue;
|
|
1687
1699
|
}
|
|
1688
1700
|
if (!inRequirementSection) continue;
|
|
1689
1701
|
const itemMatch = line.match(NUMBERED_ITEM_RE);
|
|
1690
1702
|
if (!itemMatch) continue;
|
|
1691
|
-
const index = parseInt(itemMatch[1], 10);
|
|
1692
|
-
const text = itemMatch[2].trim();
|
|
1693
|
-
const rawText = line.trim();
|
|
1694
|
-
const lineNumber = i + 1;
|
|
1695
1703
|
globalIndex++;
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
},
|
|
1709
|
-
metadata: {
|
|
1710
|
-
specPath,
|
|
1711
|
-
index,
|
|
1712
|
-
section: currentSection,
|
|
1713
|
-
rawText,
|
|
1714
|
-
earsPattern,
|
|
1715
|
-
featureName
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
});
|
|
1704
|
+
results.push(
|
|
1705
|
+
this.buildRequirementNode(
|
|
1706
|
+
line,
|
|
1707
|
+
itemMatch,
|
|
1708
|
+
i + 1,
|
|
1709
|
+
specPath,
|
|
1710
|
+
specHash,
|
|
1711
|
+
globalIndex,
|
|
1712
|
+
featureName,
|
|
1713
|
+
currentSection
|
|
1714
|
+
)
|
|
1715
|
+
);
|
|
1719
1716
|
}
|
|
1720
1717
|
return results;
|
|
1721
1718
|
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Check if a line is a section heading and return updated section state,
|
|
1721
|
+
* or return null if the line is not a heading.
|
|
1722
|
+
*/
|
|
1723
|
+
processHeadingLine(line, _inRequirementSection) {
|
|
1724
|
+
const headingMatch = line.match(SECTION_HEADING_RE);
|
|
1725
|
+
if (!headingMatch) return null;
|
|
1726
|
+
const heading = headingMatch[1].trim();
|
|
1727
|
+
const isReqSection = REQUIREMENT_SECTIONS.some(
|
|
1728
|
+
(s) => heading.toLowerCase() === s.toLowerCase()
|
|
1729
|
+
);
|
|
1730
|
+
if (isReqSection) {
|
|
1731
|
+
return { inRequirementSection: true, currentSection: heading };
|
|
1732
|
+
}
|
|
1733
|
+
return { inRequirementSection: false };
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Build a requirement GraphNode from a matched numbered-item line.
|
|
1737
|
+
*/
|
|
1738
|
+
buildRequirementNode(line, itemMatch, lineNumber, specPath, specHash, globalIndex, featureName, currentSection) {
|
|
1739
|
+
const index = parseInt(itemMatch[1], 10);
|
|
1740
|
+
const text = itemMatch[2].trim();
|
|
1741
|
+
const rawText = line.trim();
|
|
1742
|
+
const nodeId = `req:${specHash}:${globalIndex}`;
|
|
1743
|
+
const earsPattern = detectEarsPattern(text);
|
|
1744
|
+
return {
|
|
1745
|
+
node: {
|
|
1746
|
+
id: nodeId,
|
|
1747
|
+
type: "requirement",
|
|
1748
|
+
name: text,
|
|
1749
|
+
path: specPath,
|
|
1750
|
+
location: {
|
|
1751
|
+
fileId: `file:${specPath}`,
|
|
1752
|
+
startLine: lineNumber,
|
|
1753
|
+
endLine: lineNumber
|
|
1754
|
+
},
|
|
1755
|
+
metadata: {
|
|
1756
|
+
specPath,
|
|
1757
|
+
index,
|
|
1758
|
+
section: currentSection,
|
|
1759
|
+
rawText,
|
|
1760
|
+
earsPattern,
|
|
1761
|
+
featureName
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1722
1766
|
/**
|
|
1723
1767
|
* Convention-based linking: match requirement to code/test files
|
|
1724
1768
|
* by feature name in their path.
|
|
@@ -1922,15 +1966,18 @@ function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
|
|
|
1922
1966
|
durationMs: Date.now() - start
|
|
1923
1967
|
};
|
|
1924
1968
|
}
|
|
1969
|
+
function appendJqlClause(jql, clause) {
|
|
1970
|
+
return jql ? `${jql} AND ${clause}` : clause;
|
|
1971
|
+
}
|
|
1925
1972
|
function buildJql(config) {
|
|
1926
1973
|
const project2 = config.project;
|
|
1927
1974
|
let jql = project2 ? `project=${project2}` : "";
|
|
1928
1975
|
const filters = config.filters;
|
|
1929
1976
|
if (filters?.status?.length) {
|
|
1930
|
-
jql
|
|
1977
|
+
jql = appendJqlClause(jql, `status IN (${filters.status.map((s) => `"${s}"`).join(",")})`);
|
|
1931
1978
|
}
|
|
1932
1979
|
if (filters?.labels?.length) {
|
|
1933
|
-
jql
|
|
1980
|
+
jql = appendJqlClause(jql, `labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`);
|
|
1934
1981
|
}
|
|
1935
1982
|
return jql;
|
|
1936
1983
|
}
|
|
@@ -1943,8 +1990,6 @@ var JiraConnector = class {
|
|
|
1943
1990
|
}
|
|
1944
1991
|
async ingest(store, config) {
|
|
1945
1992
|
const start = Date.now();
|
|
1946
|
-
let nodesAdded = 0;
|
|
1947
|
-
let edgesAdded = 0;
|
|
1948
1993
|
const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
|
|
1949
1994
|
const apiKey = process.env[apiKeyEnv];
|
|
1950
1995
|
if (!apiKey) {
|
|
@@ -1966,38 +2011,39 @@ var JiraConnector = class {
|
|
|
1966
2011
|
);
|
|
1967
2012
|
}
|
|
1968
2013
|
const jql = buildJql(config);
|
|
1969
|
-
const headers = {
|
|
1970
|
-
Authorization: `Basic ${apiKey}`,
|
|
1971
|
-
"Content-Type": "application/json"
|
|
1972
|
-
};
|
|
2014
|
+
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
1973
2015
|
try {
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
let total = Infinity;
|
|
1977
|
-
while (startAt < total) {
|
|
1978
|
-
const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
|
|
1979
|
-
const response = await this.httpClient(url, { headers });
|
|
1980
|
-
if (!response.ok) {
|
|
1981
|
-
return buildIngestResult(nodesAdded, edgesAdded, ["Jira API request failed"], start);
|
|
1982
|
-
}
|
|
1983
|
-
const data = await response.json();
|
|
1984
|
-
total = data.total;
|
|
1985
|
-
for (const issue of data.issues) {
|
|
1986
|
-
const counts = this.processIssue(store, issue);
|
|
1987
|
-
nodesAdded += counts.nodesAdded;
|
|
1988
|
-
edgesAdded += counts.edgesAdded;
|
|
1989
|
-
}
|
|
1990
|
-
startAt += maxResults;
|
|
1991
|
-
}
|
|
2016
|
+
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
|
|
2017
|
+
return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
|
|
1992
2018
|
} catch (err) {
|
|
1993
2019
|
return buildIngestResult(
|
|
1994
|
-
|
|
1995
|
-
|
|
2020
|
+
0,
|
|
2021
|
+
0,
|
|
1996
2022
|
[`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
|
|
1997
2023
|
start
|
|
1998
2024
|
);
|
|
1999
2025
|
}
|
|
2000
|
-
|
|
2026
|
+
}
|
|
2027
|
+
async fetchAllIssues(store, baseUrl, jql, headers) {
|
|
2028
|
+
let nodesAdded = 0;
|
|
2029
|
+
let edgesAdded = 0;
|
|
2030
|
+
let startAt = 0;
|
|
2031
|
+
const maxResults = 50;
|
|
2032
|
+
let total = Infinity;
|
|
2033
|
+
while (startAt < total) {
|
|
2034
|
+
const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
|
|
2035
|
+
const response = await this.httpClient(url, { headers });
|
|
2036
|
+
if (!response.ok) throw new Error("Jira API request failed");
|
|
2037
|
+
const data = await response.json();
|
|
2038
|
+
total = data.total;
|
|
2039
|
+
for (const issue of data.issues) {
|
|
2040
|
+
const counts = this.processIssue(store, issue);
|
|
2041
|
+
nodesAdded += counts.nodesAdded;
|
|
2042
|
+
edgesAdded += counts.edgesAdded;
|
|
2043
|
+
}
|
|
2044
|
+
startAt += maxResults;
|
|
2045
|
+
}
|
|
2046
|
+
return { nodesAdded, edgesAdded };
|
|
2001
2047
|
}
|
|
2002
2048
|
processIssue(store, issue) {
|
|
2003
2049
|
const nodeId = `issue:jira:${issue.key}`;
|
|
@@ -2118,6 +2164,16 @@ var SlackConnector = class {
|
|
|
2118
2164
|
};
|
|
2119
2165
|
|
|
2120
2166
|
// src/ingest/connectors/ConfluenceConnector.ts
|
|
2167
|
+
function missingApiKeyResult(envVar, start) {
|
|
2168
|
+
return {
|
|
2169
|
+
nodesAdded: 0,
|
|
2170
|
+
nodesUpdated: 0,
|
|
2171
|
+
edgesAdded: 0,
|
|
2172
|
+
edgesUpdated: 0,
|
|
2173
|
+
errors: [`Missing API key: environment variable "${envVar}" is not set`],
|
|
2174
|
+
durationMs: Date.now() - start
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2121
2177
|
var ConfluenceConnector = class {
|
|
2122
2178
|
name = "confluence";
|
|
2123
2179
|
source = "confluence";
|
|
@@ -2128,40 +2184,34 @@ var ConfluenceConnector = class {
|
|
|
2128
2184
|
async ingest(store, config) {
|
|
2129
2185
|
const start = Date.now();
|
|
2130
2186
|
const errors = [];
|
|
2131
|
-
let nodesAdded = 0;
|
|
2132
|
-
let edgesAdded = 0;
|
|
2133
2187
|
const apiKeyEnv = config.apiKeyEnv ?? "CONFLUENCE_API_KEY";
|
|
2134
2188
|
const apiKey = process.env[apiKeyEnv];
|
|
2135
2189
|
if (!apiKey) {
|
|
2136
|
-
return
|
|
2137
|
-
nodesAdded: 0,
|
|
2138
|
-
nodesUpdated: 0,
|
|
2139
|
-
edgesAdded: 0,
|
|
2140
|
-
edgesUpdated: 0,
|
|
2141
|
-
errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2142
|
-
durationMs: Date.now() - start
|
|
2143
|
-
};
|
|
2190
|
+
return missingApiKeyResult(apiKeyEnv, start);
|
|
2144
2191
|
}
|
|
2145
2192
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
2146
2193
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
2147
2194
|
const spaceKey = config.spaceKey ?? "";
|
|
2148
|
-
|
|
2149
|
-
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
2150
|
-
nodesAdded = result.nodesAdded;
|
|
2151
|
-
edgesAdded = result.edgesAdded;
|
|
2152
|
-
errors.push(...result.errors);
|
|
2153
|
-
} catch (err) {
|
|
2154
|
-
errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2155
|
-
}
|
|
2195
|
+
const counts = await this.fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors);
|
|
2156
2196
|
return {
|
|
2157
|
-
nodesAdded,
|
|
2197
|
+
nodesAdded: counts.nodesAdded,
|
|
2158
2198
|
nodesUpdated: 0,
|
|
2159
|
-
edgesAdded,
|
|
2199
|
+
edgesAdded: counts.edgesAdded,
|
|
2160
2200
|
edgesUpdated: 0,
|
|
2161
2201
|
errors,
|
|
2162
2202
|
durationMs: Date.now() - start
|
|
2163
2203
|
};
|
|
2164
2204
|
}
|
|
2205
|
+
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
|
|
2206
|
+
try {
|
|
2207
|
+
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
2208
|
+
errors.push(...result.errors);
|
|
2209
|
+
return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
|
|
2210
|
+
} catch (err) {
|
|
2211
|
+
errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2212
|
+
return { nodesAdded: 0, edgesAdded: 0 };
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2165
2215
|
async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
|
|
2166
2216
|
const errors = [];
|
|
2167
2217
|
let nodesAdded = 0;
|
|
@@ -2206,6 +2256,61 @@ var ConfluenceConnector = class {
|
|
|
2206
2256
|
};
|
|
2207
2257
|
|
|
2208
2258
|
// src/ingest/connectors/CIConnector.ts
|
|
2259
|
+
function emptyResult2(errors, start) {
|
|
2260
|
+
return {
|
|
2261
|
+
nodesAdded: 0,
|
|
2262
|
+
nodesUpdated: 0,
|
|
2263
|
+
edgesAdded: 0,
|
|
2264
|
+
edgesUpdated: 0,
|
|
2265
|
+
errors,
|
|
2266
|
+
durationMs: Date.now() - start
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
function ingestRun(store, run) {
|
|
2270
|
+
const buildId = `build:${run.id}`;
|
|
2271
|
+
const safeName = sanitizeExternalText(run.name, 200);
|
|
2272
|
+
let nodesAdded = 0;
|
|
2273
|
+
let edgesAdded = 0;
|
|
2274
|
+
store.addNode({
|
|
2275
|
+
id: buildId,
|
|
2276
|
+
type: "build",
|
|
2277
|
+
name: `${safeName} #${run.id}`,
|
|
2278
|
+
metadata: {
|
|
2279
|
+
source: "github-actions",
|
|
2280
|
+
status: run.status,
|
|
2281
|
+
conclusion: run.conclusion,
|
|
2282
|
+
branch: run.head_branch,
|
|
2283
|
+
sha: run.head_sha,
|
|
2284
|
+
url: run.html_url,
|
|
2285
|
+
createdAt: run.created_at
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
2288
|
+
nodesAdded++;
|
|
2289
|
+
const commitNode = store.getNode(`commit:${run.head_sha}`);
|
|
2290
|
+
if (commitNode) {
|
|
2291
|
+
store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
|
|
2292
|
+
edgesAdded++;
|
|
2293
|
+
}
|
|
2294
|
+
if (run.conclusion === "failure") {
|
|
2295
|
+
const testResultId = `test_result:${run.id}`;
|
|
2296
|
+
store.addNode({
|
|
2297
|
+
id: testResultId,
|
|
2298
|
+
type: "test_result",
|
|
2299
|
+
name: `Failed: ${safeName} #${run.id}`,
|
|
2300
|
+
metadata: {
|
|
2301
|
+
source: "github-actions",
|
|
2302
|
+
buildId: String(run.id),
|
|
2303
|
+
conclusion: "failure",
|
|
2304
|
+
branch: run.head_branch,
|
|
2305
|
+
sha: run.head_sha
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
nodesAdded++;
|
|
2309
|
+
store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
|
|
2310
|
+
edgesAdded++;
|
|
2311
|
+
}
|
|
2312
|
+
return { nodesAdded, edgesAdded };
|
|
2313
|
+
}
|
|
2209
2314
|
var CIConnector = class {
|
|
2210
2315
|
name = "ci";
|
|
2211
2316
|
source = "github-actions";
|
|
@@ -2216,22 +2321,29 @@ var CIConnector = class {
|
|
|
2216
2321
|
async ingest(store, config) {
|
|
2217
2322
|
const start = Date.now();
|
|
2218
2323
|
const errors = [];
|
|
2219
|
-
let nodesAdded = 0;
|
|
2220
|
-
let edgesAdded = 0;
|
|
2221
2324
|
const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
|
|
2222
2325
|
const apiKey = process.env[apiKeyEnv];
|
|
2223
2326
|
if (!apiKey) {
|
|
2224
|
-
return
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
edgesUpdated: 0,
|
|
2229
|
-
errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2230
|
-
durationMs: Date.now() - start
|
|
2231
|
-
};
|
|
2327
|
+
return emptyResult2(
|
|
2328
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2329
|
+
start
|
|
2330
|
+
);
|
|
2232
2331
|
}
|
|
2233
2332
|
const repo = config.repo ?? "";
|
|
2234
2333
|
const maxRuns = config.maxRuns ?? 10;
|
|
2334
|
+
const counts = await this.fetchAndIngestRuns(store, repo, maxRuns, apiKey, errors);
|
|
2335
|
+
return {
|
|
2336
|
+
nodesAdded: counts.nodesAdded,
|
|
2337
|
+
nodesUpdated: 0,
|
|
2338
|
+
edgesAdded: counts.edgesAdded,
|
|
2339
|
+
edgesUpdated: 0,
|
|
2340
|
+
errors,
|
|
2341
|
+
durationMs: Date.now() - start
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
async fetchAndIngestRuns(store, repo, maxRuns, apiKey, errors) {
|
|
2345
|
+
let nodesAdded = 0;
|
|
2346
|
+
let edgesAdded = 0;
|
|
2235
2347
|
try {
|
|
2236
2348
|
const url = `https://api.github.com/repos/${repo}/actions/runs?per_page=${maxRuns}`;
|
|
2237
2349
|
const response = await this.httpClient(url, {
|
|
@@ -2239,71 +2351,20 @@ var CIConnector = class {
|
|
|
2239
2351
|
});
|
|
2240
2352
|
if (!response.ok) {
|
|
2241
2353
|
errors.push(`GitHub Actions API error: status ${response.status}`);
|
|
2242
|
-
return {
|
|
2243
|
-
nodesAdded: 0,
|
|
2244
|
-
nodesUpdated: 0,
|
|
2245
|
-
edgesAdded: 0,
|
|
2246
|
-
edgesUpdated: 0,
|
|
2247
|
-
errors,
|
|
2248
|
-
durationMs: Date.now() - start
|
|
2249
|
-
};
|
|
2354
|
+
return { nodesAdded, edgesAdded };
|
|
2250
2355
|
}
|
|
2251
2356
|
const data = await response.json();
|
|
2252
2357
|
for (const run of data.workflow_runs) {
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
id: buildId,
|
|
2257
|
-
type: "build",
|
|
2258
|
-
name: `${safeName} #${run.id}`,
|
|
2259
|
-
metadata: {
|
|
2260
|
-
source: "github-actions",
|
|
2261
|
-
status: run.status,
|
|
2262
|
-
conclusion: run.conclusion,
|
|
2263
|
-
branch: run.head_branch,
|
|
2264
|
-
sha: run.head_sha,
|
|
2265
|
-
url: run.html_url,
|
|
2266
|
-
createdAt: run.created_at
|
|
2267
|
-
}
|
|
2268
|
-
});
|
|
2269
|
-
nodesAdded++;
|
|
2270
|
-
const commitNode = store.getNode(`commit:${run.head_sha}`);
|
|
2271
|
-
if (commitNode) {
|
|
2272
|
-
store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
|
|
2273
|
-
edgesAdded++;
|
|
2274
|
-
}
|
|
2275
|
-
if (run.conclusion === "failure") {
|
|
2276
|
-
const testResultId = `test_result:${run.id}`;
|
|
2277
|
-
store.addNode({
|
|
2278
|
-
id: testResultId,
|
|
2279
|
-
type: "test_result",
|
|
2280
|
-
name: `Failed: ${safeName} #${run.id}`,
|
|
2281
|
-
metadata: {
|
|
2282
|
-
source: "github-actions",
|
|
2283
|
-
buildId: String(run.id),
|
|
2284
|
-
conclusion: "failure",
|
|
2285
|
-
branch: run.head_branch,
|
|
2286
|
-
sha: run.head_sha
|
|
2287
|
-
}
|
|
2288
|
-
});
|
|
2289
|
-
nodesAdded++;
|
|
2290
|
-
store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
|
|
2291
|
-
edgesAdded++;
|
|
2292
|
-
}
|
|
2358
|
+
const counts = ingestRun(store, run);
|
|
2359
|
+
nodesAdded += counts.nodesAdded;
|
|
2360
|
+
edgesAdded += counts.edgesAdded;
|
|
2293
2361
|
}
|
|
2294
|
-
} catch (err) {
|
|
2295
|
-
errors.push(
|
|
2296
|
-
`GitHub Actions fetch error: ${err instanceof Error ? err.message : String(err)}`
|
|
2297
|
-
);
|
|
2298
|
-
}
|
|
2299
|
-
return {
|
|
2300
|
-
nodesAdded,
|
|
2301
|
-
nodesUpdated: 0,
|
|
2302
|
-
edgesAdded,
|
|
2303
|
-
edgesUpdated: 0,
|
|
2304
|
-
errors,
|
|
2305
|
-
durationMs: Date.now() - start
|
|
2306
|
-
};
|
|
2362
|
+
} catch (err) {
|
|
2363
|
+
errors.push(
|
|
2364
|
+
`GitHub Actions fetch error: ${err instanceof Error ? err.message : String(err)}`
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
return { nodesAdded, edgesAdded };
|
|
2307
2368
|
}
|
|
2308
2369
|
};
|
|
2309
2370
|
|
|
@@ -2373,16 +2434,29 @@ var FusionLayer = class {
|
|
|
2373
2434
|
return [];
|
|
2374
2435
|
}
|
|
2375
2436
|
const allNodes = this.store.findNodes({});
|
|
2437
|
+
const semanticScores = this.buildSemanticScores(queryEmbedding, allNodes.length);
|
|
2438
|
+
const { kwWeight, semWeight } = this.resolveWeights(semanticScores.size > 0);
|
|
2439
|
+
const results = this.scoreNodes(allNodes, keywords, semanticScores, kwWeight, semWeight);
|
|
2440
|
+
results.sort((a, b) => b.score - a.score);
|
|
2441
|
+
return results.slice(0, topK);
|
|
2442
|
+
}
|
|
2443
|
+
buildSemanticScores(queryEmbedding, nodeCount) {
|
|
2376
2444
|
const semanticScores = /* @__PURE__ */ new Map();
|
|
2377
2445
|
if (queryEmbedding && this.vectorStore) {
|
|
2378
|
-
const vectorResults = this.vectorStore.search(queryEmbedding,
|
|
2446
|
+
const vectorResults = this.vectorStore.search(queryEmbedding, nodeCount);
|
|
2379
2447
|
for (const vr of vectorResults) {
|
|
2380
2448
|
semanticScores.set(vr.id, vr.score);
|
|
2381
2449
|
}
|
|
2382
2450
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2451
|
+
return semanticScores;
|
|
2452
|
+
}
|
|
2453
|
+
resolveWeights(hasSemanticScores) {
|
|
2454
|
+
return {
|
|
2455
|
+
kwWeight: hasSemanticScores ? this.keywordWeight : 1,
|
|
2456
|
+
semWeight: hasSemanticScores ? this.semanticWeight : 0
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
scoreNodes(allNodes, keywords, semanticScores, kwWeight, semWeight) {
|
|
2386
2460
|
const results = [];
|
|
2387
2461
|
for (const node of allNodes) {
|
|
2388
2462
|
const kwScore = this.keywordScore(keywords, node);
|
|
@@ -2393,15 +2467,11 @@ var FusionLayer = class {
|
|
|
2393
2467
|
nodeId: node.id,
|
|
2394
2468
|
node,
|
|
2395
2469
|
score: fusedScore,
|
|
2396
|
-
signals: {
|
|
2397
|
-
keyword: kwScore,
|
|
2398
|
-
semantic: semScore
|
|
2399
|
-
}
|
|
2470
|
+
signals: { keyword: kwScore, semantic: semScore }
|
|
2400
2471
|
});
|
|
2401
2472
|
}
|
|
2402
2473
|
}
|
|
2403
|
-
results
|
|
2404
|
-
return results.slice(0, topK);
|
|
2474
|
+
return results;
|
|
2405
2475
|
}
|
|
2406
2476
|
extractKeywords(query) {
|
|
2407
2477
|
const tokens = query.toLowerCase().split(/[\s\-_.,:;!?()[\]{}"'`/\\|@#$%^&*+=<>~]+/).filter((t) => t.length >= 2).filter((t) => !STOP_WORDS.has(t));
|
|
@@ -2456,37 +2526,50 @@ var GraphEntropyAdapter = class {
|
|
|
2456
2526
|
const missingTargets = [];
|
|
2457
2527
|
let freshEdges = 0;
|
|
2458
2528
|
for (const edge of documentsEdges) {
|
|
2459
|
-
const
|
|
2460
|
-
if (
|
|
2529
|
+
const result = this.classifyDocEdge(edge);
|
|
2530
|
+
if (result.kind === "missing") {
|
|
2461
2531
|
missingTargets.push(edge.to);
|
|
2462
|
-
|
|
2532
|
+
} else if (result.kind === "fresh") {
|
|
2533
|
+
freshEdges++;
|
|
2534
|
+
} else {
|
|
2535
|
+
staleEdges.push(result.entry);
|
|
2463
2536
|
}
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2537
|
+
}
|
|
2538
|
+
return { staleEdges, missingTargets, freshEdges };
|
|
2539
|
+
}
|
|
2540
|
+
classifyDocEdge(edge) {
|
|
2541
|
+
const codeNode = this.store.getNode(edge.to);
|
|
2542
|
+
if (!codeNode) {
|
|
2543
|
+
return { kind: "missing" };
|
|
2544
|
+
}
|
|
2545
|
+
const docNode = this.store.getNode(edge.from);
|
|
2546
|
+
const codeLastModified = codeNode.lastModified;
|
|
2547
|
+
const docLastModified = docNode?.lastModified;
|
|
2548
|
+
if (codeLastModified && docLastModified) {
|
|
2549
|
+
if (codeLastModified > docLastModified) {
|
|
2550
|
+
return {
|
|
2551
|
+
kind: "stale",
|
|
2552
|
+
entry: {
|
|
2470
2553
|
docNodeId: edge.from,
|
|
2471
2554
|
codeNodeId: edge.to,
|
|
2472
2555
|
edgeType: edge.type,
|
|
2473
2556
|
codeLastModified,
|
|
2474
2557
|
docLastModified
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
freshEdges++;
|
|
2478
|
-
}
|
|
2479
|
-
} else {
|
|
2480
|
-
staleEdges.push({
|
|
2481
|
-
docNodeId: edge.from,
|
|
2482
|
-
codeNodeId: edge.to,
|
|
2483
|
-
edgeType: edge.type,
|
|
2484
|
-
codeLastModified,
|
|
2485
|
-
docLastModified
|
|
2486
|
-
});
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2487
2560
|
}
|
|
2561
|
+
return { kind: "fresh" };
|
|
2488
2562
|
}
|
|
2489
|
-
return {
|
|
2563
|
+
return {
|
|
2564
|
+
kind: "stale",
|
|
2565
|
+
entry: {
|
|
2566
|
+
docNodeId: edge.from,
|
|
2567
|
+
codeNodeId: edge.to,
|
|
2568
|
+
edgeType: edge.type,
|
|
2569
|
+
codeLastModified,
|
|
2570
|
+
docLastModified
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2490
2573
|
}
|
|
2491
2574
|
/**
|
|
2492
2575
|
* BFS from entry points to find reachable vs unreachable code nodes.
|
|
@@ -2743,36 +2826,12 @@ var GraphAnomalyAdapter = class {
|
|
|
2743
2826
|
store;
|
|
2744
2827
|
detect(options) {
|
|
2745
2828
|
const threshold = options?.threshold != null && options.threshold > 0 ? options.threshold : DEFAULT_THRESHOLD;
|
|
2746
|
-
const
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
if (RECOGNIZED_METRICS.has(m)) {
|
|
2751
|
-
metricsToAnalyze.push(m);
|
|
2752
|
-
} else {
|
|
2753
|
-
warnings.push(m);
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
const allOutliers = [];
|
|
2757
|
-
const analyzedNodeIds = /* @__PURE__ */ new Set();
|
|
2758
|
-
const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
|
|
2759
|
-
const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
|
|
2760
|
-
const needsComplexity = metricsToAnalyze.includes("hotspotScore");
|
|
2761
|
-
const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
|
|
2762
|
-
const cachedHotspotData = needsComplexity ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
|
|
2763
|
-
for (const metric of metricsToAnalyze) {
|
|
2764
|
-
const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
|
|
2765
|
-
for (const e of entries) {
|
|
2766
|
-
analyzedNodeIds.add(e.nodeId);
|
|
2767
|
-
}
|
|
2768
|
-
const outliers = this.computeZScoreOutliers(entries, metric, threshold);
|
|
2769
|
-
allOutliers.push(...outliers);
|
|
2770
|
-
}
|
|
2771
|
-
allOutliers.sort((a, b) => b.zScore - a.zScore);
|
|
2829
|
+
const { metricsToAnalyze, warnings } = this.filterMetrics(
|
|
2830
|
+
options?.metrics ?? [...DEFAULT_METRICS]
|
|
2831
|
+
);
|
|
2832
|
+
const { allOutliers, analyzedNodeIds } = this.computeAllOutliers(metricsToAnalyze, threshold);
|
|
2772
2833
|
const articulationPoints = this.findArticulationPoints();
|
|
2773
|
-
const
|
|
2774
|
-
const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
|
|
2775
|
-
const overlapping = [...outlierNodeIds].filter((id) => apNodeIds.has(id));
|
|
2834
|
+
const overlapping = this.computeOverlap(allOutliers, articulationPoints);
|
|
2776
2835
|
return {
|
|
2777
2836
|
statisticalOutliers: allOutliers,
|
|
2778
2837
|
articulationPoints,
|
|
@@ -2788,6 +2847,38 @@ var GraphAnomalyAdapter = class {
|
|
|
2788
2847
|
}
|
|
2789
2848
|
};
|
|
2790
2849
|
}
|
|
2850
|
+
filterMetrics(requested) {
|
|
2851
|
+
const metricsToAnalyze = [];
|
|
2852
|
+
const warnings = [];
|
|
2853
|
+
for (const m of requested) {
|
|
2854
|
+
if (RECOGNIZED_METRICS.has(m)) {
|
|
2855
|
+
metricsToAnalyze.push(m);
|
|
2856
|
+
} else {
|
|
2857
|
+
warnings.push(m);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
return { metricsToAnalyze, warnings };
|
|
2861
|
+
}
|
|
2862
|
+
computeAllOutliers(metricsToAnalyze, threshold) {
|
|
2863
|
+
const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
|
|
2864
|
+
const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
|
|
2865
|
+
const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
|
|
2866
|
+
const cachedHotspotData = metricsToAnalyze.includes("hotspotScore") ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
|
|
2867
|
+
const allOutliers = [];
|
|
2868
|
+
const analyzedNodeIds = /* @__PURE__ */ new Set();
|
|
2869
|
+
for (const metric of metricsToAnalyze) {
|
|
2870
|
+
const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
|
|
2871
|
+
for (const e of entries) analyzedNodeIds.add(e.nodeId);
|
|
2872
|
+
allOutliers.push(...this.computeZScoreOutliers(entries, metric, threshold));
|
|
2873
|
+
}
|
|
2874
|
+
allOutliers.sort((a, b) => b.zScore - a.zScore);
|
|
2875
|
+
return { allOutliers, analyzedNodeIds };
|
|
2876
|
+
}
|
|
2877
|
+
computeOverlap(outliers, articulationPoints) {
|
|
2878
|
+
const outlierNodeIds = new Set(outliers.map((o) => o.nodeId));
|
|
2879
|
+
const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
|
|
2880
|
+
return [...outlierNodeIds].filter((id) => apNodeIds.has(id));
|
|
2881
|
+
}
|
|
2791
2882
|
collectMetricValues(metric, cachedCouplingData, cachedHotspotData) {
|
|
2792
2883
|
const entries = [];
|
|
2793
2884
|
if (metric === "cyclomaticComplexity") {
|
|
@@ -3343,37 +3434,54 @@ var EntityExtractor = class {
|
|
|
3343
3434
|
result.push(entity);
|
|
3344
3435
|
}
|
|
3345
3436
|
};
|
|
3346
|
-
const quotedConsumed =
|
|
3437
|
+
const quotedConsumed = this.extractQuoted(trimmed, add);
|
|
3438
|
+
const casingConsumed = this.extractCasing(trimmed, quotedConsumed, add);
|
|
3439
|
+
const pathConsumed = this.extractPaths(trimmed, add);
|
|
3440
|
+
this.extractNouns(trimmed, buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed), add);
|
|
3441
|
+
return result;
|
|
3442
|
+
}
|
|
3443
|
+
/** Strategy 1: Quoted strings. Returns the set of consumed tokens. */
|
|
3444
|
+
extractQuoted(trimmed, add) {
|
|
3445
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
3347
3446
|
for (const match of trimmed.matchAll(QUOTED_RE)) {
|
|
3348
3447
|
const inner = match[1].trim();
|
|
3349
3448
|
if (inner.length > 0) {
|
|
3350
3449
|
add(inner);
|
|
3351
|
-
|
|
3450
|
+
consumed.add(inner);
|
|
3352
3451
|
}
|
|
3353
3452
|
}
|
|
3354
|
-
|
|
3453
|
+
return consumed;
|
|
3454
|
+
}
|
|
3455
|
+
/** Strategy 2: PascalCase/camelCase tokens. Returns the set of consumed tokens. */
|
|
3456
|
+
extractCasing(trimmed, quotedConsumed, add) {
|
|
3457
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
3355
3458
|
for (const match of trimmed.matchAll(PASCAL_OR_CAMEL_RE)) {
|
|
3356
3459
|
const token = match[0];
|
|
3357
3460
|
if (!quotedConsumed.has(token)) {
|
|
3358
3461
|
add(token);
|
|
3359
|
-
|
|
3462
|
+
consumed.add(token);
|
|
3360
3463
|
}
|
|
3361
3464
|
}
|
|
3362
|
-
|
|
3465
|
+
return consumed;
|
|
3466
|
+
}
|
|
3467
|
+
/** Strategy 3: File paths. Returns the set of consumed tokens. */
|
|
3468
|
+
extractPaths(trimmed, add) {
|
|
3469
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
3363
3470
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3364
3471
|
const path7 = match[0];
|
|
3365
3472
|
add(path7);
|
|
3366
|
-
|
|
3473
|
+
consumed.add(path7);
|
|
3367
3474
|
}
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3475
|
+
return consumed;
|
|
3476
|
+
}
|
|
3477
|
+
/** Strategy 4: Remaining significant nouns after stop-word and intent-keyword removal. */
|
|
3478
|
+
extractNouns(trimmed, allConsumed, add) {
|
|
3479
|
+
for (const raw of trimmed.split(/\s+/)) {
|
|
3371
3480
|
const cleaned = raw.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "");
|
|
3372
3481
|
if (cleaned.length === 0) continue;
|
|
3373
3482
|
if (isSkippableWord(cleaned, allConsumed)) continue;
|
|
3374
3483
|
add(cleaned);
|
|
3375
3484
|
}
|
|
3376
|
-
return result;
|
|
3377
3485
|
}
|
|
3378
3486
|
};
|
|
3379
3487
|
|
|
@@ -3790,36 +3898,41 @@ var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships"
|
|
|
3790
3898
|
var classifier = new IntentClassifier();
|
|
3791
3899
|
var extractor = new EntityExtractor();
|
|
3792
3900
|
var formatter = new ResponseFormatter();
|
|
3901
|
+
function lowConfidenceResult(intent, confidence) {
|
|
3902
|
+
return {
|
|
3903
|
+
intent,
|
|
3904
|
+
intentConfidence: confidence,
|
|
3905
|
+
entities: [],
|
|
3906
|
+
summary: "I'm not sure what you're asking. Try rephrasing your question.",
|
|
3907
|
+
data: null,
|
|
3908
|
+
suggestions: [
|
|
3909
|
+
'Try "what breaks if I change <name>?" for impact analysis',
|
|
3910
|
+
'Try "where is <name>?" to find entities',
|
|
3911
|
+
'Try "what calls <name>?" for relationships',
|
|
3912
|
+
'Try "what is <name>?" for explanations',
|
|
3913
|
+
'Try "what looks wrong?" for anomaly detection'
|
|
3914
|
+
]
|
|
3915
|
+
};
|
|
3916
|
+
}
|
|
3917
|
+
function noEntityResult(intent, confidence) {
|
|
3918
|
+
return {
|
|
3919
|
+
intent,
|
|
3920
|
+
intentConfidence: confidence,
|
|
3921
|
+
entities: [],
|
|
3922
|
+
summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
|
|
3923
|
+
data: null
|
|
3924
|
+
};
|
|
3925
|
+
}
|
|
3793
3926
|
async function askGraph(store, question) {
|
|
3794
3927
|
const fusion = new FusionLayer(store);
|
|
3795
3928
|
const resolver = new EntityResolver(store, fusion);
|
|
3796
3929
|
const classification = classifier.classify(question);
|
|
3797
3930
|
if (classification.confidence < 0.3) {
|
|
3798
|
-
return
|
|
3799
|
-
intent: classification.intent,
|
|
3800
|
-
intentConfidence: classification.confidence,
|
|
3801
|
-
entities: [],
|
|
3802
|
-
summary: "I'm not sure what you're asking. Try rephrasing your question.",
|
|
3803
|
-
data: null,
|
|
3804
|
-
suggestions: [
|
|
3805
|
-
'Try "what breaks if I change <name>?" for impact analysis',
|
|
3806
|
-
'Try "where is <name>?" to find entities',
|
|
3807
|
-
'Try "what calls <name>?" for relationships',
|
|
3808
|
-
'Try "what is <name>?" for explanations',
|
|
3809
|
-
'Try "what looks wrong?" for anomaly detection'
|
|
3810
|
-
]
|
|
3811
|
-
};
|
|
3931
|
+
return lowConfidenceResult(classification.intent, classification.confidence);
|
|
3812
3932
|
}
|
|
3813
|
-
const
|
|
3814
|
-
const entities = resolver.resolve(rawEntities);
|
|
3933
|
+
const entities = resolver.resolve(extractor.extract(question));
|
|
3815
3934
|
if (ENTITY_REQUIRED_INTENTS.has(classification.intent) && entities.length === 0) {
|
|
3816
|
-
return
|
|
3817
|
-
intent: classification.intent,
|
|
3818
|
-
intentConfidence: classification.confidence,
|
|
3819
|
-
entities: [],
|
|
3820
|
-
summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
|
|
3821
|
-
data: null
|
|
3822
|
-
};
|
|
3935
|
+
return noEntityResult(classification.intent, classification.confidence);
|
|
3823
3936
|
}
|
|
3824
3937
|
let data;
|
|
3825
3938
|
try {
|
|
@@ -3833,67 +3946,59 @@ async function askGraph(store, question) {
|
|
|
3833
3946
|
data: null
|
|
3834
3947
|
};
|
|
3835
3948
|
}
|
|
3836
|
-
const summary = formatter.format(classification.intent, entities, data, question);
|
|
3837
3949
|
return {
|
|
3838
3950
|
intent: classification.intent,
|
|
3839
3951
|
intentConfidence: classification.confidence,
|
|
3840
3952
|
entities,
|
|
3841
|
-
summary,
|
|
3953
|
+
summary: formatter.format(classification.intent, entities, data, question),
|
|
3842
3954
|
data
|
|
3843
3955
|
};
|
|
3844
3956
|
}
|
|
3957
|
+
function buildContextBlocks(cql, rootIds, searchResults) {
|
|
3958
|
+
return rootIds.map((rootId) => {
|
|
3959
|
+
const expanded = cql.execute({ rootNodeIds: [rootId], maxDepth: 2 });
|
|
3960
|
+
const match = searchResults.find((r) => r.nodeId === rootId);
|
|
3961
|
+
return {
|
|
3962
|
+
rootNode: rootId,
|
|
3963
|
+
score: match?.score ?? 1,
|
|
3964
|
+
nodes: expanded.nodes,
|
|
3965
|
+
edges: expanded.edges
|
|
3966
|
+
};
|
|
3967
|
+
});
|
|
3968
|
+
}
|
|
3969
|
+
function executeImpact(store, cql, entities, question) {
|
|
3970
|
+
const rootId = entities[0].nodeId;
|
|
3971
|
+
const lower = question.toLowerCase();
|
|
3972
|
+
if (lower.includes("blast radius") || lower.includes("cascade")) {
|
|
3973
|
+
return new CascadeSimulator(store).simulate(rootId);
|
|
3974
|
+
}
|
|
3975
|
+
const result = cql.execute({ rootNodeIds: [rootId], bidirectional: true, maxDepth: 3 });
|
|
3976
|
+
return groupNodesByImpact(result.nodes, rootId);
|
|
3977
|
+
}
|
|
3978
|
+
function executeExplain(cql, entities, question, fusion) {
|
|
3979
|
+
const searchResults = fusion.search(question, 10);
|
|
3980
|
+
const rootIds = entities.length > 0 ? [entities[0].nodeId] : searchResults.slice(0, 3).map((r) => r.nodeId);
|
|
3981
|
+
return { searchResults, context: buildContextBlocks(cql, rootIds, searchResults) };
|
|
3982
|
+
}
|
|
3845
3983
|
function executeOperation(store, intent, entities, question, fusion) {
|
|
3846
3984
|
const cql = new ContextQL(store);
|
|
3847
3985
|
switch (intent) {
|
|
3848
|
-
case "impact":
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
if (lowerQuestion.includes("blast radius") || lowerQuestion.includes("cascade")) {
|
|
3852
|
-
const simulator = new CascadeSimulator(store);
|
|
3853
|
-
return simulator.simulate(rootId);
|
|
3854
|
-
}
|
|
3855
|
-
const result = cql.execute({
|
|
3856
|
-
rootNodeIds: [rootId],
|
|
3857
|
-
bidirectional: true,
|
|
3858
|
-
maxDepth: 3
|
|
3859
|
-
});
|
|
3860
|
-
return groupNodesByImpact(result.nodes, rootId);
|
|
3861
|
-
}
|
|
3862
|
-
case "find": {
|
|
3986
|
+
case "impact":
|
|
3987
|
+
return executeImpact(store, cql, entities, question);
|
|
3988
|
+
case "find":
|
|
3863
3989
|
return fusion.search(question, 10);
|
|
3864
|
-
}
|
|
3865
3990
|
case "relationships": {
|
|
3866
|
-
const rootId = entities[0].nodeId;
|
|
3867
3991
|
const result = cql.execute({
|
|
3868
|
-
rootNodeIds: [
|
|
3992
|
+
rootNodeIds: [entities[0].nodeId],
|
|
3869
3993
|
bidirectional: true,
|
|
3870
3994
|
maxDepth: 1
|
|
3871
3995
|
});
|
|
3872
3996
|
return { nodes: result.nodes, edges: result.edges };
|
|
3873
3997
|
}
|
|
3874
|
-
case "explain":
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
for (const rootId of rootIds) {
|
|
3879
|
-
const expanded = cql.execute({
|
|
3880
|
-
rootNodeIds: [rootId],
|
|
3881
|
-
maxDepth: 2
|
|
3882
|
-
});
|
|
3883
|
-
const matchingResult = searchResults.find((r) => r.nodeId === rootId);
|
|
3884
|
-
contextBlocks.push({
|
|
3885
|
-
rootNode: rootId,
|
|
3886
|
-
score: matchingResult?.score ?? 1,
|
|
3887
|
-
nodes: expanded.nodes,
|
|
3888
|
-
edges: expanded.edges
|
|
3889
|
-
});
|
|
3890
|
-
}
|
|
3891
|
-
return { searchResults, context: contextBlocks };
|
|
3892
|
-
}
|
|
3893
|
-
case "anomaly": {
|
|
3894
|
-
const adapter = new GraphAnomalyAdapter(store);
|
|
3895
|
-
return adapter.detect();
|
|
3896
|
-
}
|
|
3998
|
+
case "explain":
|
|
3999
|
+
return executeExplain(cql, entities, question, fusion);
|
|
4000
|
+
case "anomaly":
|
|
4001
|
+
return new GraphAnomalyAdapter(store).detect();
|
|
3897
4002
|
default:
|
|
3898
4003
|
return null;
|
|
3899
4004
|
}
|
|
@@ -3914,12 +4019,14 @@ var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
|
|
|
3914
4019
|
"method",
|
|
3915
4020
|
"variable"
|
|
3916
4021
|
]);
|
|
4022
|
+
function countMetadataChars(node) {
|
|
4023
|
+
return node.metadata ? JSON.stringify(node.metadata).length : 0;
|
|
4024
|
+
}
|
|
4025
|
+
function countBaseChars(node) {
|
|
4026
|
+
return (node.name?.length ?? 0) + (node.path?.length ?? 0) + (node.type?.length ?? 0);
|
|
4027
|
+
}
|
|
3917
4028
|
function estimateNodeTokens(node) {
|
|
3918
|
-
|
|
3919
|
-
if (node.metadata) {
|
|
3920
|
-
chars += JSON.stringify(node.metadata).length;
|
|
3921
|
-
}
|
|
3922
|
-
return Math.ceil(chars / 4);
|
|
4029
|
+
return Math.ceil((countBaseChars(node) + countMetadataChars(node)) / 4);
|
|
3923
4030
|
}
|
|
3924
4031
|
var Assembler = class {
|
|
3925
4032
|
store;
|
|
@@ -4000,47 +4107,55 @@ var Assembler = class {
|
|
|
4000
4107
|
}
|
|
4001
4108
|
return { keptNodes, tokenEstimate, truncated };
|
|
4002
4109
|
}
|
|
4003
|
-
|
|
4004
|
-
* Compute a token budget allocation across node types.
|
|
4005
|
-
*/
|
|
4006
|
-
computeBudget(totalTokens, phase) {
|
|
4007
|
-
const allNodes = this.store.findNodes({});
|
|
4110
|
+
countNodesByType() {
|
|
4008
4111
|
const typeCounts = {};
|
|
4009
|
-
for (const node of
|
|
4112
|
+
for (const node of this.store.findNodes({})) {
|
|
4010
4113
|
typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;
|
|
4011
4114
|
}
|
|
4115
|
+
return typeCounts;
|
|
4116
|
+
}
|
|
4117
|
+
computeModuleDensity() {
|
|
4012
4118
|
const density = {};
|
|
4013
|
-
const
|
|
4014
|
-
|
|
4015
|
-
const
|
|
4016
|
-
|
|
4017
|
-
density[mod.name] = outEdges.length + inEdges.length;
|
|
4119
|
+
for (const mod of this.store.findNodes({ type: "module" })) {
|
|
4120
|
+
const out = this.store.getEdges({ from: mod.id }).length;
|
|
4121
|
+
const inn = this.store.getEdges({ to: mod.id }).length;
|
|
4122
|
+
density[mod.name] = out + inn;
|
|
4018
4123
|
}
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4124
|
+
return density;
|
|
4125
|
+
}
|
|
4126
|
+
computeTypeWeights(typeCounts, boostTypes) {
|
|
4022
4127
|
const weights = {};
|
|
4128
|
+
let weightedTotal = 0;
|
|
4023
4129
|
for (const [type, count] of Object.entries(typeCounts)) {
|
|
4024
|
-
const
|
|
4025
|
-
const weight = count * (isBoosted ? boostFactor : 1);
|
|
4130
|
+
const weight = count * (boostTypes?.includes(type) ? 2 : 1);
|
|
4026
4131
|
weights[type] = weight;
|
|
4027
4132
|
weightedTotal += weight;
|
|
4028
4133
|
}
|
|
4134
|
+
return { weights, weightedTotal };
|
|
4135
|
+
}
|
|
4136
|
+
allocateProportionally(weights, weightedTotal, totalTokens) {
|
|
4029
4137
|
const allocations = {};
|
|
4030
|
-
if (weightedTotal
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
}
|
|
4138
|
+
if (weightedTotal === 0) return allocations;
|
|
4139
|
+
let allocated = 0;
|
|
4140
|
+
const types = Object.keys(weights);
|
|
4141
|
+
for (let i = 0; i < types.length; i++) {
|
|
4142
|
+
const type = types[i];
|
|
4143
|
+
if (i === types.length - 1) {
|
|
4144
|
+
allocations[type] = totalTokens - allocated;
|
|
4145
|
+
} else {
|
|
4146
|
+
const share = Math.round(weights[type] / weightedTotal * totalTokens);
|
|
4147
|
+
allocations[type] = share;
|
|
4148
|
+
allocated += share;
|
|
4042
4149
|
}
|
|
4043
4150
|
}
|
|
4151
|
+
return allocations;
|
|
4152
|
+
}
|
|
4153
|
+
computeBudget(totalTokens, phase) {
|
|
4154
|
+
const typeCounts = this.countNodesByType();
|
|
4155
|
+
const density = this.computeModuleDensity();
|
|
4156
|
+
const boostTypes = phase ? PHASE_NODE_TYPES[phase] : void 0;
|
|
4157
|
+
const { weights, weightedTotal } = this.computeTypeWeights(typeCounts, boostTypes);
|
|
4158
|
+
const allocations = this.allocateProportionally(weights, weightedTotal, totalTokens);
|
|
4044
4159
|
return { total: totalTokens, allocations, density };
|
|
4045
4160
|
}
|
|
4046
4161
|
/**
|
|
@@ -4071,49 +4186,43 @@ var Assembler = class {
|
|
|
4071
4186
|
filePaths: Array.from(filePathSet)
|
|
4072
4187
|
};
|
|
4073
4188
|
}
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
const moduleNodes = this.store.findNodes({ type: "module" });
|
|
4079
|
-
const modulesWithEdgeCount = moduleNodes.map((mod) => {
|
|
4080
|
-
const outEdges = this.store.getEdges({ from: mod.id });
|
|
4081
|
-
const inEdges = this.store.getEdges({ to: mod.id });
|
|
4082
|
-
return { module: mod, edgeCount: outEdges.length + inEdges.length };
|
|
4189
|
+
buildModuleLines() {
|
|
4190
|
+
const modulesWithEdgeCount = this.store.findNodes({ type: "module" }).map((mod) => {
|
|
4191
|
+
const edgeCount = this.store.getEdges({ from: mod.id }).length + this.store.getEdges({ to: mod.id }).length;
|
|
4192
|
+
return { module: mod, edgeCount };
|
|
4083
4193
|
});
|
|
4084
4194
|
modulesWithEdgeCount.sort((a, b) => b.edgeCount - a.edgeCount);
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
if (fileNode && fileNode.type === "file") {
|
|
4095
|
-
const symbolEdges = this.store.getEdges({ from: fileNode.id, type: "contains" });
|
|
4096
|
-
lines.push(`- ${fileNode.path ?? fileNode.name} (${symbolEdges.length} symbols)`);
|
|
4097
|
-
}
|
|
4195
|
+
if (modulesWithEdgeCount.length === 0) return [];
|
|
4196
|
+
const lines = ["## Modules", ""];
|
|
4197
|
+
for (const { module: mod, edgeCount } of modulesWithEdgeCount) {
|
|
4198
|
+
lines.push(`### ${mod.name} (${edgeCount} connections)`, "");
|
|
4199
|
+
for (const edge of this.store.getEdges({ from: mod.id, type: "contains" })) {
|
|
4200
|
+
const fileNode = this.store.getNode(edge.to);
|
|
4201
|
+
if (fileNode?.type === "file") {
|
|
4202
|
+
const symbols = this.store.getEdges({ from: fileNode.id, type: "contains" }).length;
|
|
4203
|
+
lines.push(`- ${fileNode.path ?? fileNode.name} (${symbols} symbols)`);
|
|
4098
4204
|
}
|
|
4099
|
-
lines.push("");
|
|
4100
4205
|
}
|
|
4206
|
+
lines.push("");
|
|
4101
4207
|
}
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
return { file: f, outDegree: outEdges.length };
|
|
4107
|
-
});
|
|
4208
|
+
return lines;
|
|
4209
|
+
}
|
|
4210
|
+
buildEntryPointLines() {
|
|
4211
|
+
const filesWithOutDegree = this.store.findNodes({ type: "file" }).filter((n) => !n.name.startsWith("index.")).map((f) => ({ file: f, outDegree: this.store.getEdges({ from: f.id }).length }));
|
|
4108
4212
|
filesWithOutDegree.sort((a, b) => b.outDegree - a.outDegree);
|
|
4109
4213
|
const entryPoints = filesWithOutDegree.filter((f) => f.outDegree > 0).slice(0, 5);
|
|
4110
|
-
if (entryPoints.length
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
}
|
|
4115
|
-
lines.push("");
|
|
4214
|
+
if (entryPoints.length === 0) return [];
|
|
4215
|
+
const lines = ["## Entry Points", ""];
|
|
4216
|
+
for (const { file, outDegree } of entryPoints) {
|
|
4217
|
+
lines.push(`- ${file.path ?? file.name} (${outDegree} outbound edges)`);
|
|
4116
4218
|
}
|
|
4219
|
+
lines.push("");
|
|
4220
|
+
return lines;
|
|
4221
|
+
}
|
|
4222
|
+
generateMap() {
|
|
4223
|
+
const lines = ["# Repository Structure", ""];
|
|
4224
|
+
lines.push(...this.buildModuleLines());
|
|
4225
|
+
lines.push(...this.buildEntryPointLines());
|
|
4117
4226
|
return lines.join("\n");
|
|
4118
4227
|
}
|
|
4119
4228
|
/**
|
|
@@ -4247,10 +4356,15 @@ var GraphConstraintAdapter = class {
|
|
|
4247
4356
|
}
|
|
4248
4357
|
store;
|
|
4249
4358
|
computeDependencyGraph() {
|
|
4250
|
-
const
|
|
4251
|
-
const
|
|
4252
|
-
|
|
4253
|
-
|
|
4359
|
+
const nodes = this.collectFileNodePaths();
|
|
4360
|
+
const edges = this.collectImportEdges();
|
|
4361
|
+
return { nodes, edges };
|
|
4362
|
+
}
|
|
4363
|
+
collectFileNodePaths() {
|
|
4364
|
+
return this.store.findNodes({ type: "file" }).map((n) => n.path ?? n.id);
|
|
4365
|
+
}
|
|
4366
|
+
collectImportEdges() {
|
|
4367
|
+
return this.store.getEdges({ type: "imports" }).map((e) => {
|
|
4254
4368
|
const fromNode = this.store.getNode(e.from);
|
|
4255
4369
|
const toNode = this.store.getNode(e.to);
|
|
4256
4370
|
const fromPath = fromNode?.path ?? e.from;
|
|
@@ -4259,7 +4373,6 @@ var GraphConstraintAdapter = class {
|
|
|
4259
4373
|
const line = e.metadata?.line ?? 0;
|
|
4260
4374
|
return { from: fromPath, to: toPath, importType, line };
|
|
4261
4375
|
});
|
|
4262
|
-
return { nodes, edges };
|
|
4263
4376
|
}
|
|
4264
4377
|
computeLayerViolations(layers, rootDir) {
|
|
4265
4378
|
const { edges } = this.computeDependencyGraph();
|
|
@@ -4553,65 +4666,53 @@ var GraphFeedbackAdapter = class {
|
|
|
4553
4666
|
const affectedDocs = [];
|
|
4554
4667
|
let impactScope = 0;
|
|
4555
4668
|
for (const filePath of changedFiles) {
|
|
4556
|
-
const
|
|
4557
|
-
if (
|
|
4558
|
-
const
|
|
4559
|
-
|
|
4560
|
-
for (const edge of inboundImports) {
|
|
4561
|
-
const importerNode = this.store.getNode(edge.from);
|
|
4562
|
-
if (importerNode?.path && /test/i.test(importerNode.path)) {
|
|
4563
|
-
affectedTests.push({
|
|
4564
|
-
testFile: importerNode.path,
|
|
4565
|
-
coversFile: filePath
|
|
4566
|
-
});
|
|
4567
|
-
}
|
|
4568
|
-
impactScope++;
|
|
4569
|
-
}
|
|
4570
|
-
const docsEdges = this.store.getEdges({ to: fileNode.id, type: "documents" });
|
|
4571
|
-
for (const edge of docsEdges) {
|
|
4572
|
-
const docNode = this.store.getNode(edge.from);
|
|
4573
|
-
if (docNode) {
|
|
4574
|
-
affectedDocs.push({
|
|
4575
|
-
docFile: docNode.path ?? docNode.name,
|
|
4576
|
-
documentsFile: filePath
|
|
4577
|
-
});
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4669
|
+
const fileNode = this.store.findNodes({ path: filePath })[0];
|
|
4670
|
+
if (!fileNode) continue;
|
|
4671
|
+
const counts = this.collectFileImpact(fileNode.id, filePath, affectedTests, affectedDocs);
|
|
4672
|
+
impactScope += counts.impactScope;
|
|
4580
4673
|
}
|
|
4581
4674
|
return { affectedTests, affectedDocs, impactScope };
|
|
4582
4675
|
}
|
|
4583
|
-
|
|
4584
|
-
const
|
|
4585
|
-
const
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
let undocumentedFiles = 0;
|
|
4590
|
-
for (const node of fileNodes) {
|
|
4591
|
-
const docsEdges = this.store.getEdges({ to: node.id, type: "documents" });
|
|
4592
|
-
if (docsEdges.length === 0) {
|
|
4593
|
-
undocumentedFiles++;
|
|
4676
|
+
collectFileImpact(fileNodeId, filePath, affectedTests, affectedDocs) {
|
|
4677
|
+
const inboundImports = this.store.getEdges({ to: fileNodeId, type: "imports" });
|
|
4678
|
+
for (const edge of inboundImports) {
|
|
4679
|
+
const importerNode = this.store.getNode(edge.from);
|
|
4680
|
+
if (importerNode?.path && /test/i.test(importerNode.path)) {
|
|
4681
|
+
affectedTests.push({ testFile: importerNode.path, coversFile: filePath });
|
|
4594
4682
|
}
|
|
4595
4683
|
}
|
|
4596
|
-
|
|
4597
|
-
for (const
|
|
4598
|
-
const
|
|
4599
|
-
if (
|
|
4600
|
-
|
|
4601
|
-
if (!isEntryPoint) {
|
|
4602
|
-
unreachableNodes++;
|
|
4603
|
-
}
|
|
4684
|
+
const docsEdges = this.store.getEdges({ to: fileNodeId, type: "documents" });
|
|
4685
|
+
for (const edge of docsEdges) {
|
|
4686
|
+
const docNode = this.store.getNode(edge.from);
|
|
4687
|
+
if (docNode) {
|
|
4688
|
+
affectedDocs.push({ docFile: docNode.path ?? docNode.name, documentsFile: filePath });
|
|
4604
4689
|
}
|
|
4605
4690
|
}
|
|
4691
|
+
return { impactScope: inboundImports.length };
|
|
4692
|
+
}
|
|
4693
|
+
computeHarnessCheckData() {
|
|
4694
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
4606
4695
|
return {
|
|
4607
4696
|
graphExists: true,
|
|
4608
|
-
nodeCount,
|
|
4609
|
-
edgeCount,
|
|
4610
|
-
constraintViolations,
|
|
4611
|
-
undocumentedFiles,
|
|
4612
|
-
unreachableNodes
|
|
4697
|
+
nodeCount: this.store.nodeCount,
|
|
4698
|
+
edgeCount: this.store.edgeCount,
|
|
4699
|
+
constraintViolations: this.store.getEdges({ type: "violates" }).length,
|
|
4700
|
+
undocumentedFiles: this.countUndocumentedFiles(fileNodes),
|
|
4701
|
+
unreachableNodes: this.countUnreachableNodes(fileNodes)
|
|
4613
4702
|
};
|
|
4614
4703
|
}
|
|
4704
|
+
countUndocumentedFiles(fileNodes) {
|
|
4705
|
+
return fileNodes.filter(
|
|
4706
|
+
(node) => this.store.getEdges({ to: node.id, type: "documents" }).length === 0
|
|
4707
|
+
).length;
|
|
4708
|
+
}
|
|
4709
|
+
countUnreachableNodes(fileNodes) {
|
|
4710
|
+
return fileNodes.filter((node) => {
|
|
4711
|
+
if (this.store.getEdges({ to: node.id, type: "imports" }).length > 0) return false;
|
|
4712
|
+
const isEntryPoint = node.name === "index.ts" || node.path !== void 0 && node.path.endsWith("/index.ts") || node.metadata?.entryPoint === true;
|
|
4713
|
+
return !isEntryPoint;
|
|
4714
|
+
}).length;
|
|
4715
|
+
}
|
|
4615
4716
|
};
|
|
4616
4717
|
|
|
4617
4718
|
// src/independence/TaskIndependenceAnalyzer.ts
|
|
@@ -4628,47 +4729,46 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4628
4729
|
this.validate(tasks);
|
|
4629
4730
|
const useGraph = this.store != null && depth > 0;
|
|
4630
4731
|
const analysisLevel = useGraph ? "graph-expanded" : "file-only";
|
|
4732
|
+
const { originalFiles, expandedFiles } = this.buildFileSets(tasks, useGraph, depth, edgeTypes);
|
|
4733
|
+
const taskIds = tasks.map((t) => t.id);
|
|
4734
|
+
const pairs = this.computeAllPairs(taskIds, originalFiles, expandedFiles);
|
|
4735
|
+
const groups = this.buildGroups(taskIds, pairs);
|
|
4736
|
+
const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
|
|
4737
|
+
return { tasks: taskIds, analysisLevel, depth, pairs, groups, verdict };
|
|
4738
|
+
}
|
|
4739
|
+
// --- Private methods ---
|
|
4740
|
+
buildFileSets(tasks, useGraph, depth, edgeTypes) {
|
|
4631
4741
|
const originalFiles = /* @__PURE__ */ new Map();
|
|
4632
4742
|
const expandedFiles = /* @__PURE__ */ new Map();
|
|
4633
4743
|
for (const task of tasks) {
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
} else {
|
|
4640
|
-
expandedFiles.set(task.id, /* @__PURE__ */ new Map());
|
|
4641
|
-
}
|
|
4744
|
+
originalFiles.set(task.id, new Set(task.files));
|
|
4745
|
+
expandedFiles.set(
|
|
4746
|
+
task.id,
|
|
4747
|
+
useGraph ? this.expandViaGraph(task.files, depth, edgeTypes) : /* @__PURE__ */ new Map()
|
|
4748
|
+
);
|
|
4642
4749
|
}
|
|
4643
|
-
|
|
4750
|
+
return { originalFiles, expandedFiles };
|
|
4751
|
+
}
|
|
4752
|
+
computeAllPairs(taskIds, originalFiles, expandedFiles) {
|
|
4644
4753
|
const pairs = [];
|
|
4645
4754
|
for (let i = 0; i < taskIds.length; i++) {
|
|
4646
4755
|
for (let j = i + 1; j < taskIds.length; j++) {
|
|
4647
4756
|
const idA = taskIds[i];
|
|
4648
4757
|
const idB = taskIds[j];
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4758
|
+
pairs.push(
|
|
4759
|
+
this.computePairOverlap(
|
|
4760
|
+
idA,
|
|
4761
|
+
idB,
|
|
4762
|
+
originalFiles.get(idA),
|
|
4763
|
+
originalFiles.get(idB),
|
|
4764
|
+
expandedFiles.get(idA),
|
|
4765
|
+
expandedFiles.get(idB)
|
|
4766
|
+
)
|
|
4656
4767
|
);
|
|
4657
|
-
pairs.push(pair);
|
|
4658
4768
|
}
|
|
4659
4769
|
}
|
|
4660
|
-
|
|
4661
|
-
const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
|
|
4662
|
-
return {
|
|
4663
|
-
tasks: taskIds,
|
|
4664
|
-
analysisLevel,
|
|
4665
|
-
depth,
|
|
4666
|
-
pairs,
|
|
4667
|
-
groups,
|
|
4668
|
-
verdict
|
|
4669
|
-
};
|
|
4770
|
+
return pairs;
|
|
4670
4771
|
}
|
|
4671
|
-
// --- Private methods ---
|
|
4672
4772
|
validate(tasks) {
|
|
4673
4773
|
if (tasks.length < 2) {
|
|
4674
4774
|
throw new Error("At least 2 tasks are required for independence analysis");
|
|
@@ -4821,27 +4921,62 @@ var ConflictPredictor = class {
|
|
|
4821
4921
|
predict(params) {
|
|
4822
4922
|
const analyzer = new TaskIndependenceAnalyzer(this.store);
|
|
4823
4923
|
const result = analyzer.analyze(params);
|
|
4924
|
+
const { churnMap, couplingMap, churnThreshold, couplingThreshold } = this.buildMetricMaps();
|
|
4925
|
+
const conflicts = this.classifyConflicts(
|
|
4926
|
+
result.pairs,
|
|
4927
|
+
churnMap,
|
|
4928
|
+
couplingMap,
|
|
4929
|
+
churnThreshold,
|
|
4930
|
+
couplingThreshold
|
|
4931
|
+
);
|
|
4932
|
+
const taskIds = result.tasks;
|
|
4933
|
+
const groups = this.buildHighSeverityGroups(taskIds, conflicts);
|
|
4934
|
+
const regrouped = !this.groupsEqual(result.groups, groups);
|
|
4935
|
+
const { highCount, mediumCount, lowCount } = this.countBySeverity(conflicts);
|
|
4936
|
+
const verdict = this.generateVerdict(
|
|
4937
|
+
taskIds,
|
|
4938
|
+
groups,
|
|
4939
|
+
result.analysisLevel,
|
|
4940
|
+
highCount,
|
|
4941
|
+
mediumCount,
|
|
4942
|
+
lowCount,
|
|
4943
|
+
regrouped
|
|
4944
|
+
);
|
|
4945
|
+
return {
|
|
4946
|
+
tasks: taskIds,
|
|
4947
|
+
analysisLevel: result.analysisLevel,
|
|
4948
|
+
depth: result.depth,
|
|
4949
|
+
conflicts,
|
|
4950
|
+
groups,
|
|
4951
|
+
summary: { high: highCount, medium: mediumCount, low: lowCount, regrouped },
|
|
4952
|
+
verdict
|
|
4953
|
+
};
|
|
4954
|
+
}
|
|
4955
|
+
// --- Private helpers ---
|
|
4956
|
+
buildMetricMaps() {
|
|
4824
4957
|
const churnMap = /* @__PURE__ */ new Map();
|
|
4825
4958
|
const couplingMap = /* @__PURE__ */ new Map();
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
}
|
|
4835
|
-
}
|
|
4836
|
-
const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
|
|
4837
|
-
for (const fileData of couplingResult.files) {
|
|
4838
|
-
couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
|
|
4959
|
+
if (this.store == null) {
|
|
4960
|
+
return { churnMap, couplingMap, churnThreshold: Infinity, couplingThreshold: Infinity };
|
|
4961
|
+
}
|
|
4962
|
+
const complexityResult = new GraphComplexityAdapter(this.store).computeComplexityHotspots();
|
|
4963
|
+
for (const hotspot of complexityResult.hotspots) {
|
|
4964
|
+
const existing = churnMap.get(hotspot.file);
|
|
4965
|
+
if (existing === void 0 || hotspot.changeFrequency > existing) {
|
|
4966
|
+
churnMap.set(hotspot.file, hotspot.changeFrequency);
|
|
4839
4967
|
}
|
|
4840
|
-
churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
|
|
4841
|
-
couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
|
|
4842
4968
|
}
|
|
4969
|
+
const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
|
|
4970
|
+
for (const fileData of couplingResult.files) {
|
|
4971
|
+
couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
|
|
4972
|
+
}
|
|
4973
|
+
const churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
|
|
4974
|
+
const couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
|
|
4975
|
+
return { churnMap, couplingMap, churnThreshold, couplingThreshold };
|
|
4976
|
+
}
|
|
4977
|
+
classifyConflicts(pairs, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
4843
4978
|
const conflicts = [];
|
|
4844
|
-
for (const pair of
|
|
4979
|
+
for (const pair of pairs) {
|
|
4845
4980
|
if (pair.independent) continue;
|
|
4846
4981
|
const { severity, reason, mitigation } = this.classifyPair(
|
|
4847
4982
|
pair.taskA,
|
|
@@ -4861,9 +4996,9 @@ var ConflictPredictor = class {
|
|
|
4861
4996
|
overlaps: pair.overlaps
|
|
4862
4997
|
});
|
|
4863
4998
|
}
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4999
|
+
return conflicts;
|
|
5000
|
+
}
|
|
5001
|
+
countBySeverity(conflicts) {
|
|
4867
5002
|
let highCount = 0;
|
|
4868
5003
|
let mediumCount = 0;
|
|
4869
5004
|
let lowCount = 0;
|
|
@@ -4872,68 +5007,57 @@ var ConflictPredictor = class {
|
|
|
4872
5007
|
else if (c.severity === "medium") mediumCount++;
|
|
4873
5008
|
else lowCount++;
|
|
4874
5009
|
}
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5010
|
+
return { highCount, mediumCount, lowCount };
|
|
5011
|
+
}
|
|
5012
|
+
classifyTransitiveOverlap(taskA, taskB, overlap, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
5013
|
+
const churn = churnMap.get(overlap.file);
|
|
5014
|
+
const coupling = couplingMap.get(overlap.file);
|
|
5015
|
+
const via = overlap.via ?? "unknown";
|
|
5016
|
+
if (churn !== void 0 && churn >= churnThreshold && churnThreshold !== Infinity) {
|
|
5017
|
+
return {
|
|
5018
|
+
severity: "medium",
|
|
5019
|
+
reason: `Transitive overlap on high-churn file ${overlap.file} (via ${via})`,
|
|
5020
|
+
mitigation: `Review: ${overlap.file} changes frequently \u2014 coordinate edits between ${taskA} and ${taskB}`
|
|
5021
|
+
};
|
|
5022
|
+
}
|
|
5023
|
+
if (coupling !== void 0 && coupling >= couplingThreshold && couplingThreshold !== Infinity) {
|
|
5024
|
+
return {
|
|
5025
|
+
severity: "medium",
|
|
5026
|
+
reason: `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`,
|
|
5027
|
+
mitigation: `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`
|
|
5028
|
+
};
|
|
5029
|
+
}
|
|
4884
5030
|
return {
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
conflicts,
|
|
4889
|
-
groups,
|
|
4890
|
-
summary: {
|
|
4891
|
-
high: highCount,
|
|
4892
|
-
medium: mediumCount,
|
|
4893
|
-
low: lowCount,
|
|
4894
|
-
regrouped
|
|
4895
|
-
},
|
|
4896
|
-
verdict
|
|
5031
|
+
severity: "low",
|
|
5032
|
+
reason: `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`,
|
|
5033
|
+
mitigation: `Info: transitive overlap unlikely to cause conflicts`
|
|
4897
5034
|
};
|
|
4898
5035
|
}
|
|
4899
|
-
// --- Private helpers ---
|
|
4900
5036
|
classifyPair(taskA, taskB, overlaps, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
4901
5037
|
let maxSeverity = "low";
|
|
4902
5038
|
let primaryReason = "";
|
|
4903
5039
|
let primaryMitigation = "";
|
|
4904
5040
|
for (const overlap of overlaps) {
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
reason = `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`;
|
|
4923
|
-
mitigation = `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`;
|
|
4924
|
-
} else {
|
|
4925
|
-
overlapSeverity = "low";
|
|
4926
|
-
reason = `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`;
|
|
4927
|
-
mitigation = `Info: transitive overlap unlikely to cause conflicts`;
|
|
4928
|
-
}
|
|
4929
|
-
}
|
|
4930
|
-
if (this.severityRank(overlapSeverity) > this.severityRank(maxSeverity)) {
|
|
4931
|
-
maxSeverity = overlapSeverity;
|
|
4932
|
-
primaryReason = reason;
|
|
4933
|
-
primaryMitigation = mitigation;
|
|
5041
|
+
const classified = overlap.type === "direct" ? {
|
|
5042
|
+
severity: "high",
|
|
5043
|
+
reason: `Both tasks write to ${overlap.file}`,
|
|
5044
|
+
mitigation: `Serialize: run ${taskA} before ${taskB}`
|
|
5045
|
+
} : this.classifyTransitiveOverlap(
|
|
5046
|
+
taskA,
|
|
5047
|
+
taskB,
|
|
5048
|
+
overlap,
|
|
5049
|
+
churnMap,
|
|
5050
|
+
couplingMap,
|
|
5051
|
+
churnThreshold,
|
|
5052
|
+
couplingThreshold
|
|
5053
|
+
);
|
|
5054
|
+
if (this.severityRank(classified.severity) > this.severityRank(maxSeverity)) {
|
|
5055
|
+
maxSeverity = classified.severity;
|
|
5056
|
+
primaryReason = classified.reason;
|
|
5057
|
+
primaryMitigation = classified.mitigation;
|
|
4934
5058
|
} else if (primaryReason === "") {
|
|
4935
|
-
primaryReason = reason;
|
|
4936
|
-
primaryMitigation = mitigation;
|
|
5059
|
+
primaryReason = classified.reason;
|
|
5060
|
+
primaryMitigation = classified.mitigation;
|
|
4937
5061
|
}
|
|
4938
5062
|
}
|
|
4939
5063
|
return { severity: maxSeverity, reason: primaryReason, mitigation: primaryMitigation };
|
|
@@ -5056,7 +5180,7 @@ var ConflictPredictor = class {
|
|
|
5056
5180
|
};
|
|
5057
5181
|
|
|
5058
5182
|
// src/index.ts
|
|
5059
|
-
var VERSION = "0.4.
|
|
5183
|
+
var VERSION = "0.4.1";
|
|
5060
5184
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5061
5185
|
0 && (module.exports = {
|
|
5062
5186
|
Assembler,
|