@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.mjs
CHANGED
|
@@ -260,21 +260,26 @@ var GraphStore = class {
|
|
|
260
260
|
return this.edgeMap.values();
|
|
261
261
|
}
|
|
262
262
|
getNeighbors(nodeId, direction = "both") {
|
|
263
|
-
const neighborIds =
|
|
263
|
+
const neighborIds = this.collectNeighborIds(nodeId, direction);
|
|
264
|
+
return this.resolveNodes(neighborIds);
|
|
265
|
+
}
|
|
266
|
+
collectNeighborIds(nodeId, direction) {
|
|
267
|
+
const ids = /* @__PURE__ */ new Set();
|
|
264
268
|
if (direction === "outbound" || direction === "both") {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
neighborIds.add(edge.to);
|
|
269
|
+
for (const edge of this.edgesByFrom.get(nodeId) ?? []) {
|
|
270
|
+
ids.add(edge.to);
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
if (direction === "inbound" || direction === "both") {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
neighborIds.add(edge.from);
|
|
274
|
+
for (const edge of this.edgesByTo.get(nodeId) ?? []) {
|
|
275
|
+
ids.add(edge.from);
|
|
274
276
|
}
|
|
275
277
|
}
|
|
278
|
+
return ids;
|
|
279
|
+
}
|
|
280
|
+
resolveNodes(ids) {
|
|
276
281
|
const results = [];
|
|
277
|
-
for (const nid of
|
|
282
|
+
for (const nid of ids) {
|
|
278
283
|
const node = this.getNode(nid);
|
|
279
284
|
if (node) results.push(node);
|
|
280
285
|
}
|
|
@@ -1021,6 +1026,17 @@ var CodeIngestor = class {
|
|
|
1021
1026
|
import { execFile } from "child_process";
|
|
1022
1027
|
import { promisify } from "util";
|
|
1023
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
|
+
}
|
|
1024
1040
|
var GitIngestor = class {
|
|
1025
1041
|
constructor(store, gitRunner) {
|
|
1026
1042
|
this.store = store;
|
|
@@ -1057,39 +1073,49 @@ var GitIngestor = class {
|
|
|
1057
1073
|
}
|
|
1058
1074
|
const commits = this.parseGitLog(output);
|
|
1059
1075
|
for (const commit of commits) {
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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++;
|
|
1084
1109
|
}
|
|
1085
1110
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1111
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1112
|
+
}
|
|
1113
|
+
ingestCoChanges(commits) {
|
|
1114
|
+
let edgesAdded = 0;
|
|
1115
|
+
for (const { fileA, fileB, count } of this.computeCoChanges(commits)) {
|
|
1088
1116
|
const fileAId = `file:${fileA}`;
|
|
1089
1117
|
const fileBId = `file:${fileB}`;
|
|
1090
|
-
|
|
1091
|
-
const nodeB = this.store.getNode(fileBId);
|
|
1092
|
-
if (nodeA && nodeB) {
|
|
1118
|
+
if (this.store.getNode(fileAId) && this.store.getNode(fileBId)) {
|
|
1093
1119
|
this.store.addEdge({
|
|
1094
1120
|
from: fileAId,
|
|
1095
1121
|
to: fileBId,
|
|
@@ -1099,14 +1125,7 @@ var GitIngestor = class {
|
|
|
1099
1125
|
edgesAdded++;
|
|
1100
1126
|
}
|
|
1101
1127
|
}
|
|
1102
|
-
return
|
|
1103
|
-
nodesAdded,
|
|
1104
|
-
nodesUpdated,
|
|
1105
|
-
edgesAdded,
|
|
1106
|
-
edgesUpdated,
|
|
1107
|
-
errors,
|
|
1108
|
-
durationMs: Date.now() - start
|
|
1109
|
-
};
|
|
1128
|
+
return edgesAdded;
|
|
1110
1129
|
}
|
|
1111
1130
|
async runGit(rootDir, args) {
|
|
1112
1131
|
if (this.gitRunner) {
|
|
@@ -1121,63 +1140,49 @@ var GitIngestor = class {
|
|
|
1121
1140
|
const lines = output.split("\n");
|
|
1122
1141
|
let current = null;
|
|
1123
1142
|
for (const line of lines) {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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;
|
|
1139
1161
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
author: current.author,
|
|
1147
|
-
email: current.email,
|
|
1148
|
-
date: current.date,
|
|
1149
|
-
message: current.message,
|
|
1150
|
-
files: current.files
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
current = {
|
|
1154
|
-
hash: parts[0],
|
|
1155
|
-
shortHash: parts[0].substring(0, 7),
|
|
1156
|
-
author: parts[1],
|
|
1157
|
-
email: parts[2],
|
|
1158
|
-
date: parts[3],
|
|
1159
|
-
message: parts.slice(4).join("|"),
|
|
1160
|
-
// message may contain |
|
|
1161
|
-
files: [],
|
|
1162
|
-
hasFiles: false
|
|
1163
|
-
};
|
|
1164
|
-
} else if (current) {
|
|
1165
|
-
current.files.push(trimmed);
|
|
1166
|
-
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));
|
|
1167
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
|
+
};
|
|
1168
1180
|
}
|
|
1169
1181
|
if (current) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
shortHash: current.shortHash,
|
|
1173
|
-
author: current.author,
|
|
1174
|
-
email: current.email,
|
|
1175
|
-
date: current.date,
|
|
1176
|
-
message: current.message,
|
|
1177
|
-
files: current.files
|
|
1178
|
-
});
|
|
1182
|
+
current.files.push(trimmed);
|
|
1183
|
+
current.hasFiles = true;
|
|
1179
1184
|
}
|
|
1180
|
-
return
|
|
1185
|
+
return current;
|
|
1181
1186
|
}
|
|
1182
1187
|
computeCoChanges(commits) {
|
|
1183
1188
|
const pairCounts = /* @__PURE__ */ new Map();
|
|
@@ -1321,50 +1326,25 @@ var KnowledgeIngestor = class {
|
|
|
1321
1326
|
try {
|
|
1322
1327
|
const content = await fs2.readFile(filePath, "utf-8");
|
|
1323
1328
|
const filename = path3.basename(filePath, ".md");
|
|
1324
|
-
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1325
|
-
const title = titleMatch ? titleMatch[1].trim() : filename;
|
|
1326
|
-
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
1327
|
-
const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
|
|
1328
|
-
const date = dateMatch ? dateMatch[1].trim() : void 0;
|
|
1329
|
-
const status = statusMatch ? statusMatch[1].trim() : void 0;
|
|
1330
1329
|
const nodeId = `adr:${filename}`;
|
|
1331
|
-
this.store.addNode(
|
|
1332
|
-
id: nodeId,
|
|
1333
|
-
type: "adr",
|
|
1334
|
-
name: title,
|
|
1335
|
-
path: filePath,
|
|
1336
|
-
metadata: { date, status }
|
|
1337
|
-
});
|
|
1330
|
+
this.store.addNode(parseADRNode(nodeId, filePath, filename, content));
|
|
1338
1331
|
nodesAdded++;
|
|
1339
1332
|
edgesAdded += this.linkToCode(content, nodeId, "documents");
|
|
1340
1333
|
} catch (err) {
|
|
1341
1334
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1342
1335
|
}
|
|
1343
1336
|
}
|
|
1344
|
-
return
|
|
1345
|
-
nodesAdded,
|
|
1346
|
-
nodesUpdated: 0,
|
|
1347
|
-
edgesAdded,
|
|
1348
|
-
edgesUpdated: 0,
|
|
1349
|
-
errors,
|
|
1350
|
-
durationMs: Date.now() - start
|
|
1351
|
-
};
|
|
1337
|
+
return buildResult(nodesAdded, edgesAdded, errors, start);
|
|
1352
1338
|
}
|
|
1353
1339
|
async ingestLearnings(projectPath) {
|
|
1354
1340
|
const start = Date.now();
|
|
1355
1341
|
const filePath = path3.join(projectPath, ".harness", "learnings.md");
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
content = await fs2.readFile(filePath, "utf-8");
|
|
1359
|
-
} catch {
|
|
1360
|
-
return emptyResult(Date.now() - start);
|
|
1361
|
-
}
|
|
1362
|
-
const errors = [];
|
|
1342
|
+
const content = await readFileOrEmpty(filePath);
|
|
1343
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
1363
1344
|
let nodesAdded = 0;
|
|
1364
1345
|
let edgesAdded = 0;
|
|
1365
|
-
const lines = content.split("\n");
|
|
1366
1346
|
let currentDate;
|
|
1367
|
-
for (const line of
|
|
1347
|
+
for (const line of content.split("\n")) {
|
|
1368
1348
|
const headingMatch = line.match(/^##\s+(\S+)/);
|
|
1369
1349
|
if (headingMatch) {
|
|
1370
1350
|
currentDate = headingMatch[1];
|
|
@@ -1373,70 +1353,29 @@ var KnowledgeIngestor = class {
|
|
|
1373
1353
|
const bulletMatch = line.match(/^-\s+(.+)/);
|
|
1374
1354
|
if (!bulletMatch) continue;
|
|
1375
1355
|
const text = bulletMatch[1];
|
|
1376
|
-
const skillMatch = text.match(/\[skill:([^\]]+)\]/);
|
|
1377
|
-
const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
|
|
1378
|
-
const skill = skillMatch ? skillMatch[1] : void 0;
|
|
1379
|
-
const outcome = outcomeMatch ? outcomeMatch[1] : void 0;
|
|
1380
1356
|
const nodeId = `learning:${hash(text)}`;
|
|
1381
|
-
this.store.addNode(
|
|
1382
|
-
id: nodeId,
|
|
1383
|
-
type: "learning",
|
|
1384
|
-
name: text,
|
|
1385
|
-
metadata: { skill, outcome, date: currentDate }
|
|
1386
|
-
});
|
|
1357
|
+
this.store.addNode(parseLearningNode(nodeId, text, currentDate));
|
|
1387
1358
|
nodesAdded++;
|
|
1388
1359
|
edgesAdded += this.linkToCode(text, nodeId, "applies_to");
|
|
1389
1360
|
}
|
|
1390
|
-
return
|
|
1391
|
-
nodesAdded,
|
|
1392
|
-
nodesUpdated: 0,
|
|
1393
|
-
edgesAdded,
|
|
1394
|
-
edgesUpdated: 0,
|
|
1395
|
-
errors,
|
|
1396
|
-
durationMs: Date.now() - start
|
|
1397
|
-
};
|
|
1361
|
+
return buildResult(nodesAdded, edgesAdded, [], start);
|
|
1398
1362
|
}
|
|
1399
1363
|
async ingestFailures(projectPath) {
|
|
1400
1364
|
const start = Date.now();
|
|
1401
1365
|
const filePath = path3.join(projectPath, ".harness", "failures.md");
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
content = await fs2.readFile(filePath, "utf-8");
|
|
1405
|
-
} catch {
|
|
1406
|
-
return emptyResult(Date.now() - start);
|
|
1407
|
-
}
|
|
1408
|
-
const errors = [];
|
|
1366
|
+
const content = await readFileOrEmpty(filePath);
|
|
1367
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
1409
1368
|
let nodesAdded = 0;
|
|
1410
1369
|
let edgesAdded = 0;
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
|
|
1417
|
-
const date = dateMatch ? dateMatch[1].trim() : void 0;
|
|
1418
|
-
const skill = skillMatch ? skillMatch[1].trim() : void 0;
|
|
1419
|
-
const failureType = typeMatch ? typeMatch[1].trim() : void 0;
|
|
1420
|
-
const description = descMatch ? descMatch[1].trim() : void 0;
|
|
1421
|
-
if (!description) continue;
|
|
1422
|
-
const nodeId = `failure:${hash(description)}`;
|
|
1423
|
-
this.store.addNode({
|
|
1424
|
-
id: nodeId,
|
|
1425
|
-
type: "failure",
|
|
1426
|
-
name: description,
|
|
1427
|
-
metadata: { date, skill, type: failureType }
|
|
1428
|
-
});
|
|
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);
|
|
1429
1375
|
nodesAdded++;
|
|
1430
|
-
edgesAdded += this.linkToCode(description,
|
|
1376
|
+
edgesAdded += this.linkToCode(description, node.id, "caused_by");
|
|
1431
1377
|
}
|
|
1432
|
-
return
|
|
1433
|
-
nodesAdded,
|
|
1434
|
-
nodesUpdated: 0,
|
|
1435
|
-
edgesAdded,
|
|
1436
|
-
edgesUpdated: 0,
|
|
1437
|
-
errors,
|
|
1438
|
-
durationMs: Date.now() - start
|
|
1439
|
-
};
|
|
1378
|
+
return buildResult(nodesAdded, edgesAdded, [], start);
|
|
1440
1379
|
}
|
|
1441
1380
|
async ingestAll(projectPath, opts) {
|
|
1442
1381
|
const start = Date.now();
|
|
@@ -1490,6 +1429,74 @@ var KnowledgeIngestor = class {
|
|
|
1490
1429
|
return results;
|
|
1491
1430
|
}
|
|
1492
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
|
+
}
|
|
1493
1500
|
|
|
1494
1501
|
// src/ingest/RequirementIngestor.ts
|
|
1495
1502
|
import * as fs3 from "fs/promises";
|
|
@@ -1534,40 +1541,9 @@ var RequirementIngestor = class {
|
|
|
1534
1541
|
return emptyResult(Date.now() - start);
|
|
1535
1542
|
}
|
|
1536
1543
|
for (const featureDir of featureDirs) {
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
try {
|
|
1541
|
-
content = await fs3.readFile(specPath, "utf-8");
|
|
1542
|
-
} catch {
|
|
1543
|
-
continue;
|
|
1544
|
-
}
|
|
1545
|
-
try {
|
|
1546
|
-
const specHash = hash(specPath);
|
|
1547
|
-
const specNodeId = `file:${specPath}`;
|
|
1548
|
-
this.store.addNode({
|
|
1549
|
-
id: specNodeId,
|
|
1550
|
-
type: "document",
|
|
1551
|
-
name: path4.basename(specPath),
|
|
1552
|
-
path: specPath,
|
|
1553
|
-
metadata: { featureName }
|
|
1554
|
-
});
|
|
1555
|
-
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1556
|
-
for (const req of requirements) {
|
|
1557
|
-
this.store.addNode(req.node);
|
|
1558
|
-
nodesAdded++;
|
|
1559
|
-
this.store.addEdge({
|
|
1560
|
-
from: req.node.id,
|
|
1561
|
-
to: specNodeId,
|
|
1562
|
-
type: "specifies"
|
|
1563
|
-
});
|
|
1564
|
-
edgesAdded++;
|
|
1565
|
-
edgesAdded += this.linkByPathPattern(req.node.id, featureName);
|
|
1566
|
-
edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
|
|
1567
|
-
}
|
|
1568
|
-
} catch (err) {
|
|
1569
|
-
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1570
|
-
}
|
|
1544
|
+
const counts = await this.ingestFeatureDir(featureDir, errors);
|
|
1545
|
+
nodesAdded += counts.nodesAdded;
|
|
1546
|
+
edgesAdded += counts.edgesAdded;
|
|
1571
1547
|
}
|
|
1572
1548
|
return {
|
|
1573
1549
|
nodesAdded,
|
|
@@ -1578,6 +1554,48 @@ var RequirementIngestor = class {
|
|
|
1578
1554
|
durationMs: Date.now() - start
|
|
1579
1555
|
};
|
|
1580
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
|
+
}
|
|
1581
1599
|
/**
|
|
1582
1600
|
* Parse markdown content and extract numbered items from recognized sections.
|
|
1583
1601
|
*/
|
|
@@ -1589,54 +1607,80 @@ var RequirementIngestor = class {
|
|
|
1589
1607
|
let globalIndex = 0;
|
|
1590
1608
|
for (let i = 0; i < lines.length; i++) {
|
|
1591
1609
|
const line = lines[i];
|
|
1592
|
-
const
|
|
1593
|
-
if (
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
);
|
|
1598
|
-
if (isReqSection) {
|
|
1599
|
-
currentSection = heading;
|
|
1600
|
-
inRequirementSection = true;
|
|
1601
|
-
} else {
|
|
1602
|
-
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;
|
|
1603
1615
|
}
|
|
1604
1616
|
continue;
|
|
1605
1617
|
}
|
|
1606
1618
|
if (!inRequirementSection) continue;
|
|
1607
1619
|
const itemMatch = line.match(NUMBERED_ITEM_RE);
|
|
1608
1620
|
if (!itemMatch) continue;
|
|
1609
|
-
const index = parseInt(itemMatch[1], 10);
|
|
1610
|
-
const text = itemMatch[2].trim();
|
|
1611
|
-
const rawText = line.trim();
|
|
1612
|
-
const lineNumber = i + 1;
|
|
1613
1621
|
globalIndex++;
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
},
|
|
1627
|
-
metadata: {
|
|
1628
|
-
specPath,
|
|
1629
|
-
index,
|
|
1630
|
-
section: currentSection,
|
|
1631
|
-
rawText,
|
|
1632
|
-
earsPattern,
|
|
1633
|
-
featureName
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
});
|
|
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
|
+
);
|
|
1637
1634
|
}
|
|
1638
1635
|
return results;
|
|
1639
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
|
+
}
|
|
1640
1684
|
/**
|
|
1641
1685
|
* Convention-based linking: match requirement to code/test files
|
|
1642
1686
|
* by feature name in their path.
|
|
@@ -1840,15 +1884,18 @@ function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
|
|
|
1840
1884
|
durationMs: Date.now() - start
|
|
1841
1885
|
};
|
|
1842
1886
|
}
|
|
1887
|
+
function appendJqlClause(jql, clause) {
|
|
1888
|
+
return jql ? `${jql} AND ${clause}` : clause;
|
|
1889
|
+
}
|
|
1843
1890
|
function buildJql(config) {
|
|
1844
1891
|
const project2 = config.project;
|
|
1845
1892
|
let jql = project2 ? `project=${project2}` : "";
|
|
1846
1893
|
const filters = config.filters;
|
|
1847
1894
|
if (filters?.status?.length) {
|
|
1848
|
-
jql
|
|
1895
|
+
jql = appendJqlClause(jql, `status IN (${filters.status.map((s) => `"${s}"`).join(",")})`);
|
|
1849
1896
|
}
|
|
1850
1897
|
if (filters?.labels?.length) {
|
|
1851
|
-
jql
|
|
1898
|
+
jql = appendJqlClause(jql, `labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`);
|
|
1852
1899
|
}
|
|
1853
1900
|
return jql;
|
|
1854
1901
|
}
|
|
@@ -1861,8 +1908,6 @@ var JiraConnector = class {
|
|
|
1861
1908
|
}
|
|
1862
1909
|
async ingest(store, config) {
|
|
1863
1910
|
const start = Date.now();
|
|
1864
|
-
let nodesAdded = 0;
|
|
1865
|
-
let edgesAdded = 0;
|
|
1866
1911
|
const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
|
|
1867
1912
|
const apiKey = process.env[apiKeyEnv];
|
|
1868
1913
|
if (!apiKey) {
|
|
@@ -1884,38 +1929,39 @@ var JiraConnector = class {
|
|
|
1884
1929
|
);
|
|
1885
1930
|
}
|
|
1886
1931
|
const jql = buildJql(config);
|
|
1887
|
-
const headers = {
|
|
1888
|
-
Authorization: `Basic ${apiKey}`,
|
|
1889
|
-
"Content-Type": "application/json"
|
|
1890
|
-
};
|
|
1932
|
+
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
1891
1933
|
try {
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
let total = Infinity;
|
|
1895
|
-
while (startAt < total) {
|
|
1896
|
-
const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
|
|
1897
|
-
const response = await this.httpClient(url, { headers });
|
|
1898
|
-
if (!response.ok) {
|
|
1899
|
-
return buildIngestResult(nodesAdded, edgesAdded, ["Jira API request failed"], start);
|
|
1900
|
-
}
|
|
1901
|
-
const data = await response.json();
|
|
1902
|
-
total = data.total;
|
|
1903
|
-
for (const issue of data.issues) {
|
|
1904
|
-
const counts = this.processIssue(store, issue);
|
|
1905
|
-
nodesAdded += counts.nodesAdded;
|
|
1906
|
-
edgesAdded += counts.edgesAdded;
|
|
1907
|
-
}
|
|
1908
|
-
startAt += maxResults;
|
|
1909
|
-
}
|
|
1934
|
+
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
|
|
1935
|
+
return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
|
|
1910
1936
|
} catch (err) {
|
|
1911
1937
|
return buildIngestResult(
|
|
1912
|
-
|
|
1913
|
-
|
|
1938
|
+
0,
|
|
1939
|
+
0,
|
|
1914
1940
|
[`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
|
|
1915
1941
|
start
|
|
1916
1942
|
);
|
|
1917
1943
|
}
|
|
1918
|
-
|
|
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 };
|
|
1919
1965
|
}
|
|
1920
1966
|
processIssue(store, issue) {
|
|
1921
1967
|
const nodeId = `issue:jira:${issue.key}`;
|
|
@@ -2036,6 +2082,16 @@ var SlackConnector = class {
|
|
|
2036
2082
|
};
|
|
2037
2083
|
|
|
2038
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
|
+
}
|
|
2039
2095
|
var ConfluenceConnector = class {
|
|
2040
2096
|
name = "confluence";
|
|
2041
2097
|
source = "confluence";
|
|
@@ -2046,40 +2102,34 @@ var ConfluenceConnector = class {
|
|
|
2046
2102
|
async ingest(store, config) {
|
|
2047
2103
|
const start = Date.now();
|
|
2048
2104
|
const errors = [];
|
|
2049
|
-
let nodesAdded = 0;
|
|
2050
|
-
let edgesAdded = 0;
|
|
2051
2105
|
const apiKeyEnv = config.apiKeyEnv ?? "CONFLUENCE_API_KEY";
|
|
2052
2106
|
const apiKey = process.env[apiKeyEnv];
|
|
2053
2107
|
if (!apiKey) {
|
|
2054
|
-
return
|
|
2055
|
-
nodesAdded: 0,
|
|
2056
|
-
nodesUpdated: 0,
|
|
2057
|
-
edgesAdded: 0,
|
|
2058
|
-
edgesUpdated: 0,
|
|
2059
|
-
errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2060
|
-
durationMs: Date.now() - start
|
|
2061
|
-
};
|
|
2108
|
+
return missingApiKeyResult(apiKeyEnv, start);
|
|
2062
2109
|
}
|
|
2063
2110
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
2064
2111
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
2065
2112
|
const spaceKey = config.spaceKey ?? "";
|
|
2066
|
-
|
|
2067
|
-
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
2068
|
-
nodesAdded = result.nodesAdded;
|
|
2069
|
-
edgesAdded = result.edgesAdded;
|
|
2070
|
-
errors.push(...result.errors);
|
|
2071
|
-
} catch (err) {
|
|
2072
|
-
errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2073
|
-
}
|
|
2113
|
+
const counts = await this.fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors);
|
|
2074
2114
|
return {
|
|
2075
|
-
nodesAdded,
|
|
2115
|
+
nodesAdded: counts.nodesAdded,
|
|
2076
2116
|
nodesUpdated: 0,
|
|
2077
|
-
edgesAdded,
|
|
2117
|
+
edgesAdded: counts.edgesAdded,
|
|
2078
2118
|
edgesUpdated: 0,
|
|
2079
2119
|
errors,
|
|
2080
2120
|
durationMs: Date.now() - start
|
|
2081
2121
|
};
|
|
2082
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
|
+
}
|
|
2083
2133
|
async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
|
|
2084
2134
|
const errors = [];
|
|
2085
2135
|
let nodesAdded = 0;
|
|
@@ -2124,6 +2174,61 @@ var ConfluenceConnector = class {
|
|
|
2124
2174
|
};
|
|
2125
2175
|
|
|
2126
2176
|
// src/ingest/connectors/CIConnector.ts
|
|
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
|
+
}
|
|
2127
2232
|
var CIConnector = class {
|
|
2128
2233
|
name = "ci";
|
|
2129
2234
|
source = "github-actions";
|
|
@@ -2134,22 +2239,29 @@ var CIConnector = class {
|
|
|
2134
2239
|
async ingest(store, config) {
|
|
2135
2240
|
const start = Date.now();
|
|
2136
2241
|
const errors = [];
|
|
2137
|
-
let nodesAdded = 0;
|
|
2138
|
-
let edgesAdded = 0;
|
|
2139
2242
|
const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
|
|
2140
2243
|
const apiKey = process.env[apiKeyEnv];
|
|
2141
2244
|
if (!apiKey) {
|
|
2142
|
-
return
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
edgesUpdated: 0,
|
|
2147
|
-
errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2148
|
-
durationMs: Date.now() - start
|
|
2149
|
-
};
|
|
2245
|
+
return emptyResult2(
|
|
2246
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2247
|
+
start
|
|
2248
|
+
);
|
|
2150
2249
|
}
|
|
2151
2250
|
const repo = config.repo ?? "";
|
|
2152
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;
|
|
2153
2265
|
try {
|
|
2154
2266
|
const url = `https://api.github.com/repos/${repo}/actions/runs?per_page=${maxRuns}`;
|
|
2155
2267
|
const response = await this.httpClient(url, {
|
|
@@ -2157,71 +2269,20 @@ var CIConnector = class {
|
|
|
2157
2269
|
});
|
|
2158
2270
|
if (!response.ok) {
|
|
2159
2271
|
errors.push(`GitHub Actions API error: status ${response.status}`);
|
|
2160
|
-
return {
|
|
2161
|
-
nodesAdded: 0,
|
|
2162
|
-
nodesUpdated: 0,
|
|
2163
|
-
edgesAdded: 0,
|
|
2164
|
-
edgesUpdated: 0,
|
|
2165
|
-
errors,
|
|
2166
|
-
durationMs: Date.now() - start
|
|
2167
|
-
};
|
|
2272
|
+
return { nodesAdded, edgesAdded };
|
|
2168
2273
|
}
|
|
2169
2274
|
const data = await response.json();
|
|
2170
2275
|
for (const run of data.workflow_runs) {
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
id: buildId,
|
|
2175
|
-
type: "build",
|
|
2176
|
-
name: `${safeName} #${run.id}`,
|
|
2177
|
-
metadata: {
|
|
2178
|
-
source: "github-actions",
|
|
2179
|
-
status: run.status,
|
|
2180
|
-
conclusion: run.conclusion,
|
|
2181
|
-
branch: run.head_branch,
|
|
2182
|
-
sha: run.head_sha,
|
|
2183
|
-
url: run.html_url,
|
|
2184
|
-
createdAt: run.created_at
|
|
2185
|
-
}
|
|
2186
|
-
});
|
|
2187
|
-
nodesAdded++;
|
|
2188
|
-
const commitNode = store.getNode(`commit:${run.head_sha}`);
|
|
2189
|
-
if (commitNode) {
|
|
2190
|
-
store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
|
|
2191
|
-
edgesAdded++;
|
|
2192
|
-
}
|
|
2193
|
-
if (run.conclusion === "failure") {
|
|
2194
|
-
const testResultId = `test_result:${run.id}`;
|
|
2195
|
-
store.addNode({
|
|
2196
|
-
id: testResultId,
|
|
2197
|
-
type: "test_result",
|
|
2198
|
-
name: `Failed: ${safeName} #${run.id}`,
|
|
2199
|
-
metadata: {
|
|
2200
|
-
source: "github-actions",
|
|
2201
|
-
buildId: String(run.id),
|
|
2202
|
-
conclusion: "failure",
|
|
2203
|
-
branch: run.head_branch,
|
|
2204
|
-
sha: run.head_sha
|
|
2205
|
-
}
|
|
2206
|
-
});
|
|
2207
|
-
nodesAdded++;
|
|
2208
|
-
store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
|
|
2209
|
-
edgesAdded++;
|
|
2210
|
-
}
|
|
2276
|
+
const counts = ingestRun(store, run);
|
|
2277
|
+
nodesAdded += counts.nodesAdded;
|
|
2278
|
+
edgesAdded += counts.edgesAdded;
|
|
2211
2279
|
}
|
|
2212
|
-
} catch (err) {
|
|
2213
|
-
errors.push(
|
|
2214
|
-
`GitHub Actions fetch error: ${err instanceof Error ? err.message : String(err)}`
|
|
2215
|
-
);
|
|
2216
|
-
}
|
|
2217
|
-
return {
|
|
2218
|
-
nodesAdded,
|
|
2219
|
-
nodesUpdated: 0,
|
|
2220
|
-
edgesAdded,
|
|
2221
|
-
edgesUpdated: 0,
|
|
2222
|
-
errors,
|
|
2223
|
-
durationMs: Date.now() - start
|
|
2224
|
-
};
|
|
2280
|
+
} catch (err) {
|
|
2281
|
+
errors.push(
|
|
2282
|
+
`GitHub Actions fetch error: ${err instanceof Error ? err.message : String(err)}`
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
return { nodesAdded, edgesAdded };
|
|
2225
2286
|
}
|
|
2226
2287
|
};
|
|
2227
2288
|
|
|
@@ -2291,16 +2352,29 @@ var FusionLayer = class {
|
|
|
2291
2352
|
return [];
|
|
2292
2353
|
}
|
|
2293
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) {
|
|
2294
2362
|
const semanticScores = /* @__PURE__ */ new Map();
|
|
2295
2363
|
if (queryEmbedding && this.vectorStore) {
|
|
2296
|
-
const vectorResults = this.vectorStore.search(queryEmbedding,
|
|
2364
|
+
const vectorResults = this.vectorStore.search(queryEmbedding, nodeCount);
|
|
2297
2365
|
for (const vr of vectorResults) {
|
|
2298
2366
|
semanticScores.set(vr.id, vr.score);
|
|
2299
2367
|
}
|
|
2300
2368
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
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) {
|
|
2304
2378
|
const results = [];
|
|
2305
2379
|
for (const node of allNodes) {
|
|
2306
2380
|
const kwScore = this.keywordScore(keywords, node);
|
|
@@ -2311,15 +2385,11 @@ var FusionLayer = class {
|
|
|
2311
2385
|
nodeId: node.id,
|
|
2312
2386
|
node,
|
|
2313
2387
|
score: fusedScore,
|
|
2314
|
-
signals: {
|
|
2315
|
-
keyword: kwScore,
|
|
2316
|
-
semantic: semScore
|
|
2317
|
-
}
|
|
2388
|
+
signals: { keyword: kwScore, semantic: semScore }
|
|
2318
2389
|
});
|
|
2319
2390
|
}
|
|
2320
2391
|
}
|
|
2321
|
-
results
|
|
2322
|
-
return results.slice(0, topK);
|
|
2392
|
+
return results;
|
|
2323
2393
|
}
|
|
2324
2394
|
extractKeywords(query) {
|
|
2325
2395
|
const tokens = query.toLowerCase().split(/[\s\-_.,:;!?()[\]{}"'`/\\|@#$%^&*+=<>~]+/).filter((t) => t.length >= 2).filter((t) => !STOP_WORDS.has(t));
|
|
@@ -2374,37 +2444,50 @@ var GraphEntropyAdapter = class {
|
|
|
2374
2444
|
const missingTargets = [];
|
|
2375
2445
|
let freshEdges = 0;
|
|
2376
2446
|
for (const edge of documentsEdges) {
|
|
2377
|
-
const
|
|
2378
|
-
if (
|
|
2447
|
+
const result = this.classifyDocEdge(edge);
|
|
2448
|
+
if (result.kind === "missing") {
|
|
2379
2449
|
missingTargets.push(edge.to);
|
|
2380
|
-
|
|
2450
|
+
} else if (result.kind === "fresh") {
|
|
2451
|
+
freshEdges++;
|
|
2452
|
+
} else {
|
|
2453
|
+
staleEdges.push(result.entry);
|
|
2381
2454
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
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: {
|
|
2388
2471
|
docNodeId: edge.from,
|
|
2389
2472
|
codeNodeId: edge.to,
|
|
2390
2473
|
edgeType: edge.type,
|
|
2391
2474
|
codeLastModified,
|
|
2392
2475
|
docLastModified
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
freshEdges++;
|
|
2396
|
-
}
|
|
2397
|
-
} else {
|
|
2398
|
-
staleEdges.push({
|
|
2399
|
-
docNodeId: edge.from,
|
|
2400
|
-
codeNodeId: edge.to,
|
|
2401
|
-
edgeType: edge.type,
|
|
2402
|
-
codeLastModified,
|
|
2403
|
-
docLastModified
|
|
2404
|
-
});
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2405
2478
|
}
|
|
2479
|
+
return { kind: "fresh" };
|
|
2406
2480
|
}
|
|
2407
|
-
return {
|
|
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
|
+
};
|
|
2408
2491
|
}
|
|
2409
2492
|
/**
|
|
2410
2493
|
* BFS from entry points to find reachable vs unreachable code nodes.
|
|
@@ -2661,36 +2744,12 @@ var GraphAnomalyAdapter = class {
|
|
|
2661
2744
|
store;
|
|
2662
2745
|
detect(options) {
|
|
2663
2746
|
const threshold = options?.threshold != null && options.threshold > 0 ? options.threshold : DEFAULT_THRESHOLD;
|
|
2664
|
-
const
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
if (RECOGNIZED_METRICS.has(m)) {
|
|
2669
|
-
metricsToAnalyze.push(m);
|
|
2670
|
-
} else {
|
|
2671
|
-
warnings.push(m);
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
const allOutliers = [];
|
|
2675
|
-
const analyzedNodeIds = /* @__PURE__ */ new Set();
|
|
2676
|
-
const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
|
|
2677
|
-
const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
|
|
2678
|
-
const needsComplexity = metricsToAnalyze.includes("hotspotScore");
|
|
2679
|
-
const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
|
|
2680
|
-
const cachedHotspotData = needsComplexity ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
|
|
2681
|
-
for (const metric of metricsToAnalyze) {
|
|
2682
|
-
const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
|
|
2683
|
-
for (const e of entries) {
|
|
2684
|
-
analyzedNodeIds.add(e.nodeId);
|
|
2685
|
-
}
|
|
2686
|
-
const outliers = this.computeZScoreOutliers(entries, metric, threshold);
|
|
2687
|
-
allOutliers.push(...outliers);
|
|
2688
|
-
}
|
|
2689
|
-
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);
|
|
2690
2751
|
const articulationPoints = this.findArticulationPoints();
|
|
2691
|
-
const
|
|
2692
|
-
const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
|
|
2693
|
-
const overlapping = [...outlierNodeIds].filter((id) => apNodeIds.has(id));
|
|
2752
|
+
const overlapping = this.computeOverlap(allOutliers, articulationPoints);
|
|
2694
2753
|
return {
|
|
2695
2754
|
statisticalOutliers: allOutliers,
|
|
2696
2755
|
articulationPoints,
|
|
@@ -2706,6 +2765,38 @@ var GraphAnomalyAdapter = class {
|
|
|
2706
2765
|
}
|
|
2707
2766
|
};
|
|
2708
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
|
+
}
|
|
2709
2800
|
collectMetricValues(metric, cachedCouplingData, cachedHotspotData) {
|
|
2710
2801
|
const entries = [];
|
|
2711
2802
|
if (metric === "cyclomaticComplexity") {
|
|
@@ -3261,37 +3352,54 @@ var EntityExtractor = class {
|
|
|
3261
3352
|
result.push(entity);
|
|
3262
3353
|
}
|
|
3263
3354
|
};
|
|
3264
|
-
const quotedConsumed =
|
|
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();
|
|
3265
3364
|
for (const match of trimmed.matchAll(QUOTED_RE)) {
|
|
3266
3365
|
const inner = match[1].trim();
|
|
3267
3366
|
if (inner.length > 0) {
|
|
3268
3367
|
add(inner);
|
|
3269
|
-
|
|
3368
|
+
consumed.add(inner);
|
|
3270
3369
|
}
|
|
3271
3370
|
}
|
|
3272
|
-
|
|
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();
|
|
3273
3376
|
for (const match of trimmed.matchAll(PASCAL_OR_CAMEL_RE)) {
|
|
3274
3377
|
const token = match[0];
|
|
3275
3378
|
if (!quotedConsumed.has(token)) {
|
|
3276
3379
|
add(token);
|
|
3277
|
-
|
|
3380
|
+
consumed.add(token);
|
|
3278
3381
|
}
|
|
3279
3382
|
}
|
|
3280
|
-
|
|
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();
|
|
3281
3388
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3282
3389
|
const path7 = match[0];
|
|
3283
3390
|
add(path7);
|
|
3284
|
-
|
|
3391
|
+
consumed.add(path7);
|
|
3285
3392
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
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+/)) {
|
|
3289
3398
|
const cleaned = raw.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "");
|
|
3290
3399
|
if (cleaned.length === 0) continue;
|
|
3291
3400
|
if (isSkippableWord(cleaned, allConsumed)) continue;
|
|
3292
3401
|
add(cleaned);
|
|
3293
3402
|
}
|
|
3294
|
-
return result;
|
|
3295
3403
|
}
|
|
3296
3404
|
};
|
|
3297
3405
|
|
|
@@ -3708,36 +3816,41 @@ var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships"
|
|
|
3708
3816
|
var classifier = new IntentClassifier();
|
|
3709
3817
|
var extractor = new EntityExtractor();
|
|
3710
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
|
+
}
|
|
3711
3844
|
async function askGraph(store, question) {
|
|
3712
3845
|
const fusion = new FusionLayer(store);
|
|
3713
3846
|
const resolver = new EntityResolver(store, fusion);
|
|
3714
3847
|
const classification = classifier.classify(question);
|
|
3715
3848
|
if (classification.confidence < 0.3) {
|
|
3716
|
-
return
|
|
3717
|
-
intent: classification.intent,
|
|
3718
|
-
intentConfidence: classification.confidence,
|
|
3719
|
-
entities: [],
|
|
3720
|
-
summary: "I'm not sure what you're asking. Try rephrasing your question.",
|
|
3721
|
-
data: null,
|
|
3722
|
-
suggestions: [
|
|
3723
|
-
'Try "what breaks if I change <name>?" for impact analysis',
|
|
3724
|
-
'Try "where is <name>?" to find entities',
|
|
3725
|
-
'Try "what calls <name>?" for relationships',
|
|
3726
|
-
'Try "what is <name>?" for explanations',
|
|
3727
|
-
'Try "what looks wrong?" for anomaly detection'
|
|
3728
|
-
]
|
|
3729
|
-
};
|
|
3849
|
+
return lowConfidenceResult(classification.intent, classification.confidence);
|
|
3730
3850
|
}
|
|
3731
|
-
const
|
|
3732
|
-
const entities = resolver.resolve(rawEntities);
|
|
3851
|
+
const entities = resolver.resolve(extractor.extract(question));
|
|
3733
3852
|
if (ENTITY_REQUIRED_INTENTS.has(classification.intent) && entities.length === 0) {
|
|
3734
|
-
return
|
|
3735
|
-
intent: classification.intent,
|
|
3736
|
-
intentConfidence: classification.confidence,
|
|
3737
|
-
entities: [],
|
|
3738
|
-
summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
|
|
3739
|
-
data: null
|
|
3740
|
-
};
|
|
3853
|
+
return noEntityResult(classification.intent, classification.confidence);
|
|
3741
3854
|
}
|
|
3742
3855
|
let data;
|
|
3743
3856
|
try {
|
|
@@ -3751,67 +3864,59 @@ async function askGraph(store, question) {
|
|
|
3751
3864
|
data: null
|
|
3752
3865
|
};
|
|
3753
3866
|
}
|
|
3754
|
-
const summary = formatter.format(classification.intent, entities, data, question);
|
|
3755
3867
|
return {
|
|
3756
3868
|
intent: classification.intent,
|
|
3757
3869
|
intentConfidence: classification.confidence,
|
|
3758
3870
|
entities,
|
|
3759
|
-
summary,
|
|
3871
|
+
summary: formatter.format(classification.intent, entities, data, question),
|
|
3760
3872
|
data
|
|
3761
3873
|
};
|
|
3762
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
|
+
}
|
|
3763
3901
|
function executeOperation(store, intent, entities, question, fusion) {
|
|
3764
3902
|
const cql = new ContextQL(store);
|
|
3765
3903
|
switch (intent) {
|
|
3766
|
-
case "impact":
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
if (lowerQuestion.includes("blast radius") || lowerQuestion.includes("cascade")) {
|
|
3770
|
-
const simulator = new CascadeSimulator(store);
|
|
3771
|
-
return simulator.simulate(rootId);
|
|
3772
|
-
}
|
|
3773
|
-
const result = cql.execute({
|
|
3774
|
-
rootNodeIds: [rootId],
|
|
3775
|
-
bidirectional: true,
|
|
3776
|
-
maxDepth: 3
|
|
3777
|
-
});
|
|
3778
|
-
return groupNodesByImpact(result.nodes, rootId);
|
|
3779
|
-
}
|
|
3780
|
-
case "find": {
|
|
3904
|
+
case "impact":
|
|
3905
|
+
return executeImpact(store, cql, entities, question);
|
|
3906
|
+
case "find":
|
|
3781
3907
|
return fusion.search(question, 10);
|
|
3782
|
-
}
|
|
3783
3908
|
case "relationships": {
|
|
3784
|
-
const rootId = entities[0].nodeId;
|
|
3785
3909
|
const result = cql.execute({
|
|
3786
|
-
rootNodeIds: [
|
|
3910
|
+
rootNodeIds: [entities[0].nodeId],
|
|
3787
3911
|
bidirectional: true,
|
|
3788
3912
|
maxDepth: 1
|
|
3789
3913
|
});
|
|
3790
3914
|
return { nodes: result.nodes, edges: result.edges };
|
|
3791
3915
|
}
|
|
3792
|
-
case "explain":
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
for (const rootId of rootIds) {
|
|
3797
|
-
const expanded = cql.execute({
|
|
3798
|
-
rootNodeIds: [rootId],
|
|
3799
|
-
maxDepth: 2
|
|
3800
|
-
});
|
|
3801
|
-
const matchingResult = searchResults.find((r) => r.nodeId === rootId);
|
|
3802
|
-
contextBlocks.push({
|
|
3803
|
-
rootNode: rootId,
|
|
3804
|
-
score: matchingResult?.score ?? 1,
|
|
3805
|
-
nodes: expanded.nodes,
|
|
3806
|
-
edges: expanded.edges
|
|
3807
|
-
});
|
|
3808
|
-
}
|
|
3809
|
-
return { searchResults, context: contextBlocks };
|
|
3810
|
-
}
|
|
3811
|
-
case "anomaly": {
|
|
3812
|
-
const adapter = new GraphAnomalyAdapter(store);
|
|
3813
|
-
return adapter.detect();
|
|
3814
|
-
}
|
|
3916
|
+
case "explain":
|
|
3917
|
+
return executeExplain(cql, entities, question, fusion);
|
|
3918
|
+
case "anomaly":
|
|
3919
|
+
return new GraphAnomalyAdapter(store).detect();
|
|
3815
3920
|
default:
|
|
3816
3921
|
return null;
|
|
3817
3922
|
}
|
|
@@ -3832,12 +3937,14 @@ var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
|
|
|
3832
3937
|
"method",
|
|
3833
3938
|
"variable"
|
|
3834
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
|
+
}
|
|
3835
3946
|
function estimateNodeTokens(node) {
|
|
3836
|
-
|
|
3837
|
-
if (node.metadata) {
|
|
3838
|
-
chars += JSON.stringify(node.metadata).length;
|
|
3839
|
-
}
|
|
3840
|
-
return Math.ceil(chars / 4);
|
|
3947
|
+
return Math.ceil((countBaseChars(node) + countMetadataChars(node)) / 4);
|
|
3841
3948
|
}
|
|
3842
3949
|
var Assembler = class {
|
|
3843
3950
|
store;
|
|
@@ -3918,47 +4025,55 @@ var Assembler = class {
|
|
|
3918
4025
|
}
|
|
3919
4026
|
return { keptNodes, tokenEstimate, truncated };
|
|
3920
4027
|
}
|
|
3921
|
-
|
|
3922
|
-
* Compute a token budget allocation across node types.
|
|
3923
|
-
*/
|
|
3924
|
-
computeBudget(totalTokens, phase) {
|
|
3925
|
-
const allNodes = this.store.findNodes({});
|
|
4028
|
+
countNodesByType() {
|
|
3926
4029
|
const typeCounts = {};
|
|
3927
|
-
for (const node of
|
|
4030
|
+
for (const node of this.store.findNodes({})) {
|
|
3928
4031
|
typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;
|
|
3929
4032
|
}
|
|
4033
|
+
return typeCounts;
|
|
4034
|
+
}
|
|
4035
|
+
computeModuleDensity() {
|
|
3930
4036
|
const density = {};
|
|
3931
|
-
const
|
|
3932
|
-
|
|
3933
|
-
const
|
|
3934
|
-
|
|
3935
|
-
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;
|
|
3936
4041
|
}
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
4042
|
+
return density;
|
|
4043
|
+
}
|
|
4044
|
+
computeTypeWeights(typeCounts, boostTypes) {
|
|
3940
4045
|
const weights = {};
|
|
4046
|
+
let weightedTotal = 0;
|
|
3941
4047
|
for (const [type, count] of Object.entries(typeCounts)) {
|
|
3942
|
-
const
|
|
3943
|
-
const weight = count * (isBoosted ? boostFactor : 1);
|
|
4048
|
+
const weight = count * (boostTypes?.includes(type) ? 2 : 1);
|
|
3944
4049
|
weights[type] = weight;
|
|
3945
4050
|
weightedTotal += weight;
|
|
3946
4051
|
}
|
|
4052
|
+
return { weights, weightedTotal };
|
|
4053
|
+
}
|
|
4054
|
+
allocateProportionally(weights, weightedTotal, totalTokens) {
|
|
3947
4055
|
const allocations = {};
|
|
3948
|
-
if (weightedTotal
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
}
|
|
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;
|
|
3960
4067
|
}
|
|
3961
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);
|
|
3962
4077
|
return { total: totalTokens, allocations, density };
|
|
3963
4078
|
}
|
|
3964
4079
|
/**
|
|
@@ -3989,49 +4104,43 @@ var Assembler = class {
|
|
|
3989
4104
|
filePaths: Array.from(filePathSet)
|
|
3990
4105
|
};
|
|
3991
4106
|
}
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
const moduleNodes = this.store.findNodes({ type: "module" });
|
|
3997
|
-
const modulesWithEdgeCount = moduleNodes.map((mod) => {
|
|
3998
|
-
const outEdges = this.store.getEdges({ from: mod.id });
|
|
3999
|
-
const inEdges = this.store.getEdges({ to: mod.id });
|
|
4000
|
-
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 };
|
|
4001
4111
|
});
|
|
4002
4112
|
modulesWithEdgeCount.sort((a, b) => b.edgeCount - a.edgeCount);
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
if (fileNode && fileNode.type === "file") {
|
|
4013
|
-
const symbolEdges = this.store.getEdges({ from: fileNode.id, type: "contains" });
|
|
4014
|
-
lines.push(`- ${fileNode.path ?? fileNode.name} (${symbolEdges.length} symbols)`);
|
|
4015
|
-
}
|
|
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)`);
|
|
4016
4122
|
}
|
|
4017
|
-
lines.push("");
|
|
4018
4123
|
}
|
|
4124
|
+
lines.push("");
|
|
4019
4125
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
return { file: f, outDegree: outEdges.length };
|
|
4025
|
-
});
|
|
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 }));
|
|
4026
4130
|
filesWithOutDegree.sort((a, b) => b.outDegree - a.outDegree);
|
|
4027
4131
|
const entryPoints = filesWithOutDegree.filter((f) => f.outDegree > 0).slice(0, 5);
|
|
4028
|
-
if (entryPoints.length
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}
|
|
4033
|
-
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)`);
|
|
4034
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());
|
|
4035
4144
|
return lines.join("\n");
|
|
4036
4145
|
}
|
|
4037
4146
|
/**
|
|
@@ -4165,10 +4274,15 @@ var GraphConstraintAdapter = class {
|
|
|
4165
4274
|
}
|
|
4166
4275
|
store;
|
|
4167
4276
|
computeDependencyGraph() {
|
|
4168
|
-
const
|
|
4169
|
-
const
|
|
4170
|
-
|
|
4171
|
-
|
|
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) => {
|
|
4172
4286
|
const fromNode = this.store.getNode(e.from);
|
|
4173
4287
|
const toNode = this.store.getNode(e.to);
|
|
4174
4288
|
const fromPath = fromNode?.path ?? e.from;
|
|
@@ -4177,7 +4291,6 @@ var GraphConstraintAdapter = class {
|
|
|
4177
4291
|
const line = e.metadata?.line ?? 0;
|
|
4178
4292
|
return { from: fromPath, to: toPath, importType, line };
|
|
4179
4293
|
});
|
|
4180
|
-
return { nodes, edges };
|
|
4181
4294
|
}
|
|
4182
4295
|
computeLayerViolations(layers, rootDir) {
|
|
4183
4296
|
const { edges } = this.computeDependencyGraph();
|
|
@@ -4471,65 +4584,53 @@ var GraphFeedbackAdapter = class {
|
|
|
4471
4584
|
const affectedDocs = [];
|
|
4472
4585
|
let impactScope = 0;
|
|
4473
4586
|
for (const filePath of changedFiles) {
|
|
4474
|
-
const
|
|
4475
|
-
if (
|
|
4476
|
-
const
|
|
4477
|
-
|
|
4478
|
-
for (const edge of inboundImports) {
|
|
4479
|
-
const importerNode = this.store.getNode(edge.from);
|
|
4480
|
-
if (importerNode?.path && /test/i.test(importerNode.path)) {
|
|
4481
|
-
affectedTests.push({
|
|
4482
|
-
testFile: importerNode.path,
|
|
4483
|
-
coversFile: filePath
|
|
4484
|
-
});
|
|
4485
|
-
}
|
|
4486
|
-
impactScope++;
|
|
4487
|
-
}
|
|
4488
|
-
const docsEdges = this.store.getEdges({ to: fileNode.id, type: "documents" });
|
|
4489
|
-
for (const edge of docsEdges) {
|
|
4490
|
-
const docNode = this.store.getNode(edge.from);
|
|
4491
|
-
if (docNode) {
|
|
4492
|
-
affectedDocs.push({
|
|
4493
|
-
docFile: docNode.path ?? docNode.name,
|
|
4494
|
-
documentsFile: filePath
|
|
4495
|
-
});
|
|
4496
|
-
}
|
|
4497
|
-
}
|
|
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;
|
|
4498
4591
|
}
|
|
4499
4592
|
return { affectedTests, affectedDocs, impactScope };
|
|
4500
4593
|
}
|
|
4501
|
-
|
|
4502
|
-
const
|
|
4503
|
-
const
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
let undocumentedFiles = 0;
|
|
4508
|
-
for (const node of fileNodes) {
|
|
4509
|
-
const docsEdges = this.store.getEdges({ to: node.id, type: "documents" });
|
|
4510
|
-
if (docsEdges.length === 0) {
|
|
4511
|
-
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 });
|
|
4512
4600
|
}
|
|
4513
4601
|
}
|
|
4514
|
-
|
|
4515
|
-
for (const
|
|
4516
|
-
const
|
|
4517
|
-
if (
|
|
4518
|
-
|
|
4519
|
-
if (!isEntryPoint) {
|
|
4520
|
-
unreachableNodes++;
|
|
4521
|
-
}
|
|
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 });
|
|
4522
4607
|
}
|
|
4523
4608
|
}
|
|
4609
|
+
return { impactScope: inboundImports.length };
|
|
4610
|
+
}
|
|
4611
|
+
computeHarnessCheckData() {
|
|
4612
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
4524
4613
|
return {
|
|
4525
4614
|
graphExists: true,
|
|
4526
|
-
nodeCount,
|
|
4527
|
-
edgeCount,
|
|
4528
|
-
constraintViolations,
|
|
4529
|
-
undocumentedFiles,
|
|
4530
|
-
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)
|
|
4531
4620
|
};
|
|
4532
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
|
+
}
|
|
4533
4634
|
};
|
|
4534
4635
|
|
|
4535
4636
|
// src/independence/TaskIndependenceAnalyzer.ts
|
|
@@ -4546,47 +4647,46 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4546
4647
|
this.validate(tasks);
|
|
4547
4648
|
const useGraph = this.store != null && depth > 0;
|
|
4548
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) {
|
|
4549
4659
|
const originalFiles = /* @__PURE__ */ new Map();
|
|
4550
4660
|
const expandedFiles = /* @__PURE__ */ new Map();
|
|
4551
4661
|
for (const task of tasks) {
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
} else {
|
|
4558
|
-
expandedFiles.set(task.id, /* @__PURE__ */ new Map());
|
|
4559
|
-
}
|
|
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
|
+
);
|
|
4560
4667
|
}
|
|
4561
|
-
|
|
4668
|
+
return { originalFiles, expandedFiles };
|
|
4669
|
+
}
|
|
4670
|
+
computeAllPairs(taskIds, originalFiles, expandedFiles) {
|
|
4562
4671
|
const pairs = [];
|
|
4563
4672
|
for (let i = 0; i < taskIds.length; i++) {
|
|
4564
4673
|
for (let j = i + 1; j < taskIds.length; j++) {
|
|
4565
4674
|
const idA = taskIds[i];
|
|
4566
4675
|
const idB = taskIds[j];
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
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
|
+
)
|
|
4574
4685
|
);
|
|
4575
|
-
pairs.push(pair);
|
|
4576
4686
|
}
|
|
4577
4687
|
}
|
|
4578
|
-
|
|
4579
|
-
const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
|
|
4580
|
-
return {
|
|
4581
|
-
tasks: taskIds,
|
|
4582
|
-
analysisLevel,
|
|
4583
|
-
depth,
|
|
4584
|
-
pairs,
|
|
4585
|
-
groups,
|
|
4586
|
-
verdict
|
|
4587
|
-
};
|
|
4688
|
+
return pairs;
|
|
4588
4689
|
}
|
|
4589
|
-
// --- Private methods ---
|
|
4590
4690
|
validate(tasks) {
|
|
4591
4691
|
if (tasks.length < 2) {
|
|
4592
4692
|
throw new Error("At least 2 tasks are required for independence analysis");
|
|
@@ -4739,27 +4839,62 @@ var ConflictPredictor = class {
|
|
|
4739
4839
|
predict(params) {
|
|
4740
4840
|
const analyzer = new TaskIndependenceAnalyzer(this.store);
|
|
4741
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() {
|
|
4742
4875
|
const churnMap = /* @__PURE__ */ new Map();
|
|
4743
4876
|
const couplingMap = /* @__PURE__ */ new Map();
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
}
|
|
4753
|
-
}
|
|
4754
|
-
const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
|
|
4755
|
-
for (const fileData of couplingResult.files) {
|
|
4756
|
-
couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
|
|
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);
|
|
4757
4885
|
}
|
|
4758
|
-
churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
|
|
4759
|
-
couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
|
|
4760
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) {
|
|
4761
4896
|
const conflicts = [];
|
|
4762
|
-
for (const pair of
|
|
4897
|
+
for (const pair of pairs) {
|
|
4763
4898
|
if (pair.independent) continue;
|
|
4764
4899
|
const { severity, reason, mitigation } = this.classifyPair(
|
|
4765
4900
|
pair.taskA,
|
|
@@ -4779,9 +4914,9 @@ var ConflictPredictor = class {
|
|
|
4779
4914
|
overlaps: pair.overlaps
|
|
4780
4915
|
});
|
|
4781
4916
|
}
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4917
|
+
return conflicts;
|
|
4918
|
+
}
|
|
4919
|
+
countBySeverity(conflicts) {
|
|
4785
4920
|
let highCount = 0;
|
|
4786
4921
|
let mediumCount = 0;
|
|
4787
4922
|
let lowCount = 0;
|
|
@@ -4790,68 +4925,57 @@ var ConflictPredictor = class {
|
|
|
4790
4925
|
else if (c.severity === "medium") mediumCount++;
|
|
4791
4926
|
else lowCount++;
|
|
4792
4927
|
}
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
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
|
+
}
|
|
4802
4948
|
return {
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
conflicts,
|
|
4807
|
-
groups,
|
|
4808
|
-
summary: {
|
|
4809
|
-
high: highCount,
|
|
4810
|
-
medium: mediumCount,
|
|
4811
|
-
low: lowCount,
|
|
4812
|
-
regrouped
|
|
4813
|
-
},
|
|
4814
|
-
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`
|
|
4815
4952
|
};
|
|
4816
4953
|
}
|
|
4817
|
-
// --- Private helpers ---
|
|
4818
4954
|
classifyPair(taskA, taskB, overlaps, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
4819
4955
|
let maxSeverity = "low";
|
|
4820
4956
|
let primaryReason = "";
|
|
4821
4957
|
let primaryMitigation = "";
|
|
4822
4958
|
for (const overlap of overlaps) {
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
reason = `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`;
|
|
4841
|
-
mitigation = `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`;
|
|
4842
|
-
} else {
|
|
4843
|
-
overlapSeverity = "low";
|
|
4844
|
-
reason = `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`;
|
|
4845
|
-
mitigation = `Info: transitive overlap unlikely to cause conflicts`;
|
|
4846
|
-
}
|
|
4847
|
-
}
|
|
4848
|
-
if (this.severityRank(overlapSeverity) > this.severityRank(maxSeverity)) {
|
|
4849
|
-
maxSeverity = overlapSeverity;
|
|
4850
|
-
primaryReason = reason;
|
|
4851
|
-
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;
|
|
4852
4976
|
} else if (primaryReason === "") {
|
|
4853
|
-
primaryReason = reason;
|
|
4854
|
-
primaryMitigation = mitigation;
|
|
4977
|
+
primaryReason = classified.reason;
|
|
4978
|
+
primaryMitigation = classified.mitigation;
|
|
4855
4979
|
}
|
|
4856
4980
|
}
|
|
4857
4981
|
return { severity: maxSeverity, reason: primaryReason, mitigation: primaryMitigation };
|
|
@@ -4974,7 +5098,7 @@ var ConflictPredictor = class {
|
|
|
4974
5098
|
};
|
|
4975
5099
|
|
|
4976
5100
|
// src/index.ts
|
|
4977
|
-
var VERSION = "0.4.
|
|
5101
|
+
var VERSION = "0.4.1";
|
|
4978
5102
|
export {
|
|
4979
5103
|
Assembler,
|
|
4980
5104
|
CIConnector,
|