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