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