@harness-engineering/graph 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +115 -16
- package/dist/index.d.ts +115 -16
- package/dist/index.js +964 -733
- package/dist/index.mjs +961 -733
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -37,7 +37,9 @@ var NODE_TYPES = [
|
|
|
37
37
|
"aesthetic_intent",
|
|
38
38
|
"design_constraint",
|
|
39
39
|
// Traceability
|
|
40
|
-
"requirement"
|
|
40
|
+
"requirement",
|
|
41
|
+
// Cache
|
|
42
|
+
"packed_summary"
|
|
41
43
|
];
|
|
42
44
|
var EDGE_TYPES = [
|
|
43
45
|
// Code relationships
|
|
@@ -70,10 +72,21 @@ var EDGE_TYPES = [
|
|
|
70
72
|
// Traceability relationships
|
|
71
73
|
"requires",
|
|
72
74
|
"verified_by",
|
|
73
|
-
"tested_by"
|
|
75
|
+
"tested_by",
|
|
76
|
+
// Cache relationships
|
|
77
|
+
"caches"
|
|
74
78
|
];
|
|
75
79
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
76
80
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
81
|
+
var NODE_STABILITY = {
|
|
82
|
+
File: "session",
|
|
83
|
+
Function: "session",
|
|
84
|
+
Class: "session",
|
|
85
|
+
Constraint: "session",
|
|
86
|
+
PackedSummary: "session",
|
|
87
|
+
SkillDefinition: "static",
|
|
88
|
+
ToolDefinition: "static"
|
|
89
|
+
};
|
|
77
90
|
var GraphNodeSchema = z.object({
|
|
78
91
|
id: z.string(),
|
|
79
92
|
type: z.enum(NODE_TYPES),
|
|
@@ -260,21 +273,26 @@ var GraphStore = class {
|
|
|
260
273
|
return this.edgeMap.values();
|
|
261
274
|
}
|
|
262
275
|
getNeighbors(nodeId, direction = "both") {
|
|
263
|
-
const neighborIds =
|
|
276
|
+
const neighborIds = this.collectNeighborIds(nodeId, direction);
|
|
277
|
+
return this.resolveNodes(neighborIds);
|
|
278
|
+
}
|
|
279
|
+
collectNeighborIds(nodeId, direction) {
|
|
280
|
+
const ids = /* @__PURE__ */ new Set();
|
|
264
281
|
if (direction === "outbound" || direction === "both") {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
neighborIds.add(edge.to);
|
|
282
|
+
for (const edge of this.edgesByFrom.get(nodeId) ?? []) {
|
|
283
|
+
ids.add(edge.to);
|
|
268
284
|
}
|
|
269
285
|
}
|
|
270
286
|
if (direction === "inbound" || direction === "both") {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
neighborIds.add(edge.from);
|
|
287
|
+
for (const edge of this.edgesByTo.get(nodeId) ?? []) {
|
|
288
|
+
ids.add(edge.from);
|
|
274
289
|
}
|
|
275
290
|
}
|
|
291
|
+
return ids;
|
|
292
|
+
}
|
|
293
|
+
resolveNodes(ids) {
|
|
276
294
|
const results = [];
|
|
277
|
-
for (const nid of
|
|
295
|
+
for (const nid of ids) {
|
|
278
296
|
const node = this.getNode(nid);
|
|
279
297
|
if (node) results.push(node);
|
|
280
298
|
}
|
|
@@ -404,6 +422,94 @@ var VectorStore = class _VectorStore {
|
|
|
404
422
|
}
|
|
405
423
|
};
|
|
406
424
|
|
|
425
|
+
// src/store/PackedSummaryCache.ts
|
|
426
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
427
|
+
function normalizeIntent(intent) {
|
|
428
|
+
return intent.trim().toLowerCase().replace(/\s+/g, " ");
|
|
429
|
+
}
|
|
430
|
+
function cacheNodeId(normalizedIntent) {
|
|
431
|
+
return `packed_summary:${normalizedIntent}`;
|
|
432
|
+
}
|
|
433
|
+
var PackedSummaryCache = class {
|
|
434
|
+
constructor(store, ttlMs = DEFAULT_TTL_MS) {
|
|
435
|
+
this.store = store;
|
|
436
|
+
this.ttlMs = ttlMs;
|
|
437
|
+
}
|
|
438
|
+
store;
|
|
439
|
+
ttlMs;
|
|
440
|
+
/** Returns cached envelope with `cached: true` if valid, or null if miss/stale. */
|
|
441
|
+
get(intent) {
|
|
442
|
+
const normalized = normalizeIntent(intent);
|
|
443
|
+
const nodeId = cacheNodeId(normalized);
|
|
444
|
+
const node = this.store.getNode(nodeId);
|
|
445
|
+
if (!node) return null;
|
|
446
|
+
const createdMs = this.parseCreatedMs(node.metadata["createdAt"]);
|
|
447
|
+
if (createdMs === null) return null;
|
|
448
|
+
if (Date.now() - createdMs > this.ttlMs) return null;
|
|
449
|
+
if (!this.areSourcesFresh(nodeId, node, createdMs)) return null;
|
|
450
|
+
return this.parseEnvelope(node.metadata["envelope"]);
|
|
451
|
+
}
|
|
452
|
+
/** Parse and validate createdAt. Returns epoch ms or null if missing/malformed (GC-002). */
|
|
453
|
+
parseCreatedMs(createdAt) {
|
|
454
|
+
if (!createdAt) return null;
|
|
455
|
+
const ms = new Date(createdAt).getTime();
|
|
456
|
+
return Number.isNaN(ms) ? null : ms;
|
|
457
|
+
}
|
|
458
|
+
/** GC-001: Checks source nodes exist and are unmodified since cache creation. */
|
|
459
|
+
areSourcesFresh(nodeId, node, createdMs) {
|
|
460
|
+
const sourceNodeIds = node.metadata["sourceNodeIds"];
|
|
461
|
+
const edges = this.store.getEdges({ from: nodeId, type: "caches" });
|
|
462
|
+
if (sourceNodeIds && edges.length < sourceNodeIds.length) return false;
|
|
463
|
+
for (const edge of edges) {
|
|
464
|
+
const sourceNode = this.store.getNode(edge.to);
|
|
465
|
+
if (!sourceNode) return false;
|
|
466
|
+
if (sourceNode.lastModified) {
|
|
467
|
+
const sourceModMs = new Date(sourceNode.lastModified).getTime();
|
|
468
|
+
if (sourceModMs > createdMs) return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
/** Parse envelope JSON and set cached: true. Returns null on invalid JSON. */
|
|
474
|
+
parseEnvelope(raw) {
|
|
475
|
+
try {
|
|
476
|
+
const envelope = JSON.parse(raw);
|
|
477
|
+
return { ...envelope, meta: { ...envelope.meta, cached: true } };
|
|
478
|
+
} catch {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/** Write a PackedSummary node with caches edges to source nodes. */
|
|
483
|
+
set(intent, envelope, sourceNodeIds) {
|
|
484
|
+
const normalized = normalizeIntent(intent);
|
|
485
|
+
const nodeId = cacheNodeId(normalized);
|
|
486
|
+
this.store.removeNode(nodeId);
|
|
487
|
+
this.store.addNode({
|
|
488
|
+
id: nodeId,
|
|
489
|
+
type: "packed_summary",
|
|
490
|
+
name: normalized,
|
|
491
|
+
metadata: {
|
|
492
|
+
envelope: JSON.stringify(envelope),
|
|
493
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
494
|
+
sourceNodeIds
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
for (const sourceId of sourceNodeIds) {
|
|
498
|
+
this.store.addEdge({
|
|
499
|
+
from: nodeId,
|
|
500
|
+
to: sourceId,
|
|
501
|
+
type: "caches"
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/** Explicitly invalidate a cached packed summary. */
|
|
506
|
+
invalidate(intent) {
|
|
507
|
+
const normalized = normalizeIntent(intent);
|
|
508
|
+
const nodeId = cacheNodeId(normalized);
|
|
509
|
+
this.store.removeNode(nodeId);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
407
513
|
// src/query/ContextQL.ts
|
|
408
514
|
function edgeKey2(e) {
|
|
409
515
|
return `${e.from}|${e.to}|${e.type}`;
|
|
@@ -1021,6 +1127,17 @@ var CodeIngestor = class {
|
|
|
1021
1127
|
import { execFile } from "child_process";
|
|
1022
1128
|
import { promisify } from "util";
|
|
1023
1129
|
var execFileAsync = promisify(execFile);
|
|
1130
|
+
function finalizeCommit(current) {
|
|
1131
|
+
return {
|
|
1132
|
+
hash: current.hash,
|
|
1133
|
+
shortHash: current.shortHash,
|
|
1134
|
+
author: current.author,
|
|
1135
|
+
email: current.email,
|
|
1136
|
+
date: current.date,
|
|
1137
|
+
message: current.message,
|
|
1138
|
+
files: current.files
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1024
1141
|
var GitIngestor = class {
|
|
1025
1142
|
constructor(store, gitRunner) {
|
|
1026
1143
|
this.store = store;
|
|
@@ -1057,39 +1174,49 @@ var GitIngestor = class {
|
|
|
1057
1174
|
}
|
|
1058
1175
|
const commits = this.parseGitLog(output);
|
|
1059
1176
|
for (const commit of commits) {
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1177
|
+
const counts = this.ingestCommit(commit);
|
|
1178
|
+
nodesAdded += counts.nodesAdded;
|
|
1179
|
+
edgesAdded += counts.edgesAdded;
|
|
1180
|
+
}
|
|
1181
|
+
edgesAdded += this.ingestCoChanges(commits);
|
|
1182
|
+
return {
|
|
1183
|
+
nodesAdded,
|
|
1184
|
+
nodesUpdated,
|
|
1185
|
+
edgesAdded,
|
|
1186
|
+
edgesUpdated,
|
|
1187
|
+
errors,
|
|
1188
|
+
durationMs: Date.now() - start
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
ingestCommit(commit) {
|
|
1192
|
+
const nodeId = `commit:${commit.shortHash}`;
|
|
1193
|
+
this.store.addNode({
|
|
1194
|
+
id: nodeId,
|
|
1195
|
+
type: "commit",
|
|
1196
|
+
name: commit.message,
|
|
1197
|
+
metadata: {
|
|
1198
|
+
author: commit.author,
|
|
1199
|
+
email: commit.email,
|
|
1200
|
+
date: commit.date,
|
|
1201
|
+
hash: commit.hash
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
let edgesAdded = 0;
|
|
1205
|
+
for (const file of commit.files) {
|
|
1206
|
+
const fileNodeId = `file:${file}`;
|
|
1207
|
+
if (this.store.getNode(fileNodeId)) {
|
|
1208
|
+
this.store.addEdge({ from: fileNodeId, to: nodeId, type: "triggered_by" });
|
|
1209
|
+
edgesAdded++;
|
|
1084
1210
|
}
|
|
1085
1211
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1212
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1213
|
+
}
|
|
1214
|
+
ingestCoChanges(commits) {
|
|
1215
|
+
let edgesAdded = 0;
|
|
1216
|
+
for (const { fileA, fileB, count } of this.computeCoChanges(commits)) {
|
|
1088
1217
|
const fileAId = `file:${fileA}`;
|
|
1089
1218
|
const fileBId = `file:${fileB}`;
|
|
1090
|
-
|
|
1091
|
-
const nodeB = this.store.getNode(fileBId);
|
|
1092
|
-
if (nodeA && nodeB) {
|
|
1219
|
+
if (this.store.getNode(fileAId) && this.store.getNode(fileBId)) {
|
|
1093
1220
|
this.store.addEdge({
|
|
1094
1221
|
from: fileAId,
|
|
1095
1222
|
to: fileBId,
|
|
@@ -1099,14 +1226,7 @@ var GitIngestor = class {
|
|
|
1099
1226
|
edgesAdded++;
|
|
1100
1227
|
}
|
|
1101
1228
|
}
|
|
1102
|
-
return
|
|
1103
|
-
nodesAdded,
|
|
1104
|
-
nodesUpdated,
|
|
1105
|
-
edgesAdded,
|
|
1106
|
-
edgesUpdated,
|
|
1107
|
-
errors,
|
|
1108
|
-
durationMs: Date.now() - start
|
|
1109
|
-
};
|
|
1229
|
+
return edgesAdded;
|
|
1110
1230
|
}
|
|
1111
1231
|
async runGit(rootDir, args) {
|
|
1112
1232
|
if (this.gitRunner) {
|
|
@@ -1121,63 +1241,49 @@ var GitIngestor = class {
|
|
|
1121
1241
|
const lines = output.split("\n");
|
|
1122
1242
|
let current = null;
|
|
1123
1243
|
for (const line of lines) {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1244
|
+
current = this.processLogLine(line, current, commits);
|
|
1245
|
+
}
|
|
1246
|
+
if (current) {
|
|
1247
|
+
commits.push(finalizeCommit(current));
|
|
1248
|
+
}
|
|
1249
|
+
return commits;
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Process one line from git log output, updating the in-progress commit builder
|
|
1253
|
+
* and flushing completed commits into the accumulator.
|
|
1254
|
+
* Returns the updated current builder (null if flushed and not replaced).
|
|
1255
|
+
*/
|
|
1256
|
+
processLogLine(line, current, commits) {
|
|
1257
|
+
const trimmed = line.trim();
|
|
1258
|
+
if (!trimmed) {
|
|
1259
|
+
if (current?.hasFiles) {
|
|
1260
|
+
commits.push(finalizeCommit(current));
|
|
1261
|
+
return null;
|
|
1139
1262
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
author: current.author,
|
|
1147
|
-
email: current.email,
|
|
1148
|
-
date: current.date,
|
|
1149
|
-
message: current.message,
|
|
1150
|
-
files: current.files
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
current = {
|
|
1154
|
-
hash: parts[0],
|
|
1155
|
-
shortHash: parts[0].substring(0, 7),
|
|
1156
|
-
author: parts[1],
|
|
1157
|
-
email: parts[2],
|
|
1158
|
-
date: parts[3],
|
|
1159
|
-
message: parts.slice(4).join("|"),
|
|
1160
|
-
// message may contain |
|
|
1161
|
-
files: [],
|
|
1162
|
-
hasFiles: false
|
|
1163
|
-
};
|
|
1164
|
-
} else if (current) {
|
|
1165
|
-
current.files.push(trimmed);
|
|
1166
|
-
current.hasFiles = true;
|
|
1263
|
+
return current;
|
|
1264
|
+
}
|
|
1265
|
+
const parts = trimmed.split("|");
|
|
1266
|
+
if (parts.length >= 5 && /^[0-9a-f]{7,40}$/.test(parts[0])) {
|
|
1267
|
+
if (current) {
|
|
1268
|
+
commits.push(finalizeCommit(current));
|
|
1167
1269
|
}
|
|
1270
|
+
return {
|
|
1271
|
+
hash: parts[0],
|
|
1272
|
+
shortHash: parts[0].substring(0, 7),
|
|
1273
|
+
author: parts[1],
|
|
1274
|
+
email: parts[2],
|
|
1275
|
+
date: parts[3],
|
|
1276
|
+
message: parts.slice(4).join("|"),
|
|
1277
|
+
// message may contain |
|
|
1278
|
+
files: [],
|
|
1279
|
+
hasFiles: false
|
|
1280
|
+
};
|
|
1168
1281
|
}
|
|
1169
1282
|
if (current) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
shortHash: current.shortHash,
|
|
1173
|
-
author: current.author,
|
|
1174
|
-
email: current.email,
|
|
1175
|
-
date: current.date,
|
|
1176
|
-
message: current.message,
|
|
1177
|
-
files: current.files
|
|
1178
|
-
});
|
|
1283
|
+
current.files.push(trimmed);
|
|
1284
|
+
current.hasFiles = true;
|
|
1179
1285
|
}
|
|
1180
|
-
return
|
|
1286
|
+
return current;
|
|
1181
1287
|
}
|
|
1182
1288
|
computeCoChanges(commits) {
|
|
1183
1289
|
const pairCounts = /* @__PURE__ */ new Map();
|
|
@@ -1321,50 +1427,25 @@ var KnowledgeIngestor = class {
|
|
|
1321
1427
|
try {
|
|
1322
1428
|
const content = await fs2.readFile(filePath, "utf-8");
|
|
1323
1429
|
const filename = path3.basename(filePath, ".md");
|
|
1324
|
-
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1325
|
-
const title = titleMatch ? titleMatch[1].trim() : filename;
|
|
1326
|
-
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
1327
|
-
const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
|
|
1328
|
-
const date = dateMatch ? dateMatch[1].trim() : void 0;
|
|
1329
|
-
const status = statusMatch ? statusMatch[1].trim() : void 0;
|
|
1330
1430
|
const nodeId = `adr:${filename}`;
|
|
1331
|
-
this.store.addNode(
|
|
1332
|
-
id: nodeId,
|
|
1333
|
-
type: "adr",
|
|
1334
|
-
name: title,
|
|
1335
|
-
path: filePath,
|
|
1336
|
-
metadata: { date, status }
|
|
1337
|
-
});
|
|
1431
|
+
this.store.addNode(parseADRNode(nodeId, filePath, filename, content));
|
|
1338
1432
|
nodesAdded++;
|
|
1339
1433
|
edgesAdded += this.linkToCode(content, nodeId, "documents");
|
|
1340
1434
|
} catch (err) {
|
|
1341
1435
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1342
1436
|
}
|
|
1343
1437
|
}
|
|
1344
|
-
return
|
|
1345
|
-
nodesAdded,
|
|
1346
|
-
nodesUpdated: 0,
|
|
1347
|
-
edgesAdded,
|
|
1348
|
-
edgesUpdated: 0,
|
|
1349
|
-
errors,
|
|
1350
|
-
durationMs: Date.now() - start
|
|
1351
|
-
};
|
|
1438
|
+
return buildResult(nodesAdded, edgesAdded, errors, start);
|
|
1352
1439
|
}
|
|
1353
1440
|
async ingestLearnings(projectPath) {
|
|
1354
1441
|
const start = Date.now();
|
|
1355
1442
|
const filePath = path3.join(projectPath, ".harness", "learnings.md");
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
content = await fs2.readFile(filePath, "utf-8");
|
|
1359
|
-
} catch {
|
|
1360
|
-
return emptyResult(Date.now() - start);
|
|
1361
|
-
}
|
|
1362
|
-
const errors = [];
|
|
1443
|
+
const content = await readFileOrEmpty(filePath);
|
|
1444
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
1363
1445
|
let nodesAdded = 0;
|
|
1364
1446
|
let edgesAdded = 0;
|
|
1365
|
-
const lines = content.split("\n");
|
|
1366
1447
|
let currentDate;
|
|
1367
|
-
for (const line of
|
|
1448
|
+
for (const line of content.split("\n")) {
|
|
1368
1449
|
const headingMatch = line.match(/^##\s+(\S+)/);
|
|
1369
1450
|
if (headingMatch) {
|
|
1370
1451
|
currentDate = headingMatch[1];
|
|
@@ -1373,70 +1454,29 @@ var KnowledgeIngestor = class {
|
|
|
1373
1454
|
const bulletMatch = line.match(/^-\s+(.+)/);
|
|
1374
1455
|
if (!bulletMatch) continue;
|
|
1375
1456
|
const text = bulletMatch[1];
|
|
1376
|
-
const skillMatch = text.match(/\[skill:([^\]]+)\]/);
|
|
1377
|
-
const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
|
|
1378
|
-
const skill = skillMatch ? skillMatch[1] : void 0;
|
|
1379
|
-
const outcome = outcomeMatch ? outcomeMatch[1] : void 0;
|
|
1380
1457
|
const nodeId = `learning:${hash(text)}`;
|
|
1381
|
-
this.store.addNode(
|
|
1382
|
-
id: nodeId,
|
|
1383
|
-
type: "learning",
|
|
1384
|
-
name: text,
|
|
1385
|
-
metadata: { skill, outcome, date: currentDate }
|
|
1386
|
-
});
|
|
1458
|
+
this.store.addNode(parseLearningNode(nodeId, text, currentDate));
|
|
1387
1459
|
nodesAdded++;
|
|
1388
1460
|
edgesAdded += this.linkToCode(text, nodeId, "applies_to");
|
|
1389
1461
|
}
|
|
1390
|
-
return
|
|
1391
|
-
nodesAdded,
|
|
1392
|
-
nodesUpdated: 0,
|
|
1393
|
-
edgesAdded,
|
|
1394
|
-
edgesUpdated: 0,
|
|
1395
|
-
errors,
|
|
1396
|
-
durationMs: Date.now() - start
|
|
1397
|
-
};
|
|
1462
|
+
return buildResult(nodesAdded, edgesAdded, [], start);
|
|
1398
1463
|
}
|
|
1399
1464
|
async ingestFailures(projectPath) {
|
|
1400
1465
|
const start = Date.now();
|
|
1401
1466
|
const filePath = path3.join(projectPath, ".harness", "failures.md");
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
content = await fs2.readFile(filePath, "utf-8");
|
|
1405
|
-
} catch {
|
|
1406
|
-
return emptyResult(Date.now() - start);
|
|
1407
|
-
}
|
|
1408
|
-
const errors = [];
|
|
1467
|
+
const content = await readFileOrEmpty(filePath);
|
|
1468
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
1409
1469
|
let nodesAdded = 0;
|
|
1410
1470
|
let edgesAdded = 0;
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
|
|
1417
|
-
const date = dateMatch ? dateMatch[1].trim() : void 0;
|
|
1418
|
-
const skill = skillMatch ? skillMatch[1].trim() : void 0;
|
|
1419
|
-
const failureType = typeMatch ? typeMatch[1].trim() : void 0;
|
|
1420
|
-
const description = descMatch ? descMatch[1].trim() : void 0;
|
|
1421
|
-
if (!description) continue;
|
|
1422
|
-
const nodeId = `failure:${hash(description)}`;
|
|
1423
|
-
this.store.addNode({
|
|
1424
|
-
id: nodeId,
|
|
1425
|
-
type: "failure",
|
|
1426
|
-
name: description,
|
|
1427
|
-
metadata: { date, skill, type: failureType }
|
|
1428
|
-
});
|
|
1471
|
+
for (const section of content.split(/^##\s+/m).filter((s) => s.trim())) {
|
|
1472
|
+
const parsed = parseFailureSection(section);
|
|
1473
|
+
if (!parsed) continue;
|
|
1474
|
+
const { description, node } = parsed;
|
|
1475
|
+
this.store.addNode(node);
|
|
1429
1476
|
nodesAdded++;
|
|
1430
|
-
edgesAdded += this.linkToCode(description,
|
|
1477
|
+
edgesAdded += this.linkToCode(description, node.id, "caused_by");
|
|
1431
1478
|
}
|
|
1432
|
-
return
|
|
1433
|
-
nodesAdded,
|
|
1434
|
-
nodesUpdated: 0,
|
|
1435
|
-
edgesAdded,
|
|
1436
|
-
edgesUpdated: 0,
|
|
1437
|
-
errors,
|
|
1438
|
-
durationMs: Date.now() - start
|
|
1439
|
-
};
|
|
1479
|
+
return buildResult(nodesAdded, edgesAdded, [], start);
|
|
1440
1480
|
}
|
|
1441
1481
|
async ingestAll(projectPath, opts) {
|
|
1442
1482
|
const start = Date.now();
|
|
@@ -1490,6 +1530,74 @@ var KnowledgeIngestor = class {
|
|
|
1490
1530
|
return results;
|
|
1491
1531
|
}
|
|
1492
1532
|
};
|
|
1533
|
+
async function readFileOrEmpty(filePath) {
|
|
1534
|
+
try {
|
|
1535
|
+
return await fs2.readFile(filePath, "utf-8");
|
|
1536
|
+
} catch {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function buildResult(nodesAdded, edgesAdded, errors, start) {
|
|
1541
|
+
return {
|
|
1542
|
+
nodesAdded,
|
|
1543
|
+
nodesUpdated: 0,
|
|
1544
|
+
edgesAdded,
|
|
1545
|
+
edgesUpdated: 0,
|
|
1546
|
+
errors,
|
|
1547
|
+
durationMs: Date.now() - start
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function parseADRNode(nodeId, filePath, filename, content) {
|
|
1551
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1552
|
+
const title = titleMatch ? titleMatch[1].trim() : filename;
|
|
1553
|
+
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
1554
|
+
const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
|
|
1555
|
+
return {
|
|
1556
|
+
id: nodeId,
|
|
1557
|
+
type: "adr",
|
|
1558
|
+
name: title,
|
|
1559
|
+
path: filePath,
|
|
1560
|
+
metadata: {
|
|
1561
|
+
date: dateMatch ? dateMatch[1].trim() : void 0,
|
|
1562
|
+
status: statusMatch ? statusMatch[1].trim() : void 0
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function parseLearningNode(nodeId, text, currentDate) {
|
|
1567
|
+
const skillMatch = text.match(/\[skill:([^\]]+)\]/);
|
|
1568
|
+
const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
|
|
1569
|
+
return {
|
|
1570
|
+
id: nodeId,
|
|
1571
|
+
type: "learning",
|
|
1572
|
+
name: text,
|
|
1573
|
+
metadata: {
|
|
1574
|
+
skill: skillMatch ? skillMatch[1] : void 0,
|
|
1575
|
+
outcome: outcomeMatch ? outcomeMatch[1] : void 0,
|
|
1576
|
+
date: currentDate
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function parseFailureSection(section) {
|
|
1581
|
+
const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
|
|
1582
|
+
const description = descMatch ? descMatch[1].trim() : void 0;
|
|
1583
|
+
if (!description) return null;
|
|
1584
|
+
const dateMatch = section.match(/\*\*Date:\*\*\s*(.+)/);
|
|
1585
|
+
const skillMatch = section.match(/\*\*Skill:\*\*\s*(.+)/);
|
|
1586
|
+
const typeMatch = section.match(/\*\*Type:\*\*\s*(.+)/);
|
|
1587
|
+
return {
|
|
1588
|
+
description,
|
|
1589
|
+
node: {
|
|
1590
|
+
id: `failure:${hash(description)}`,
|
|
1591
|
+
type: "failure",
|
|
1592
|
+
name: description,
|
|
1593
|
+
metadata: {
|
|
1594
|
+
date: dateMatch ? dateMatch[1].trim() : void 0,
|
|
1595
|
+
skill: skillMatch ? skillMatch[1].trim() : void 0,
|
|
1596
|
+
type: typeMatch ? typeMatch[1].trim() : void 0
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1493
1601
|
|
|
1494
1602
|
// src/ingest/RequirementIngestor.ts
|
|
1495
1603
|
import * as fs3 from "fs/promises";
|
|
@@ -1534,40 +1642,9 @@ var RequirementIngestor = class {
|
|
|
1534
1642
|
return emptyResult(Date.now() - start);
|
|
1535
1643
|
}
|
|
1536
1644
|
for (const featureDir of featureDirs) {
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
try {
|
|
1541
|
-
content = await fs3.readFile(specPath, "utf-8");
|
|
1542
|
-
} catch {
|
|
1543
|
-
continue;
|
|
1544
|
-
}
|
|
1545
|
-
try {
|
|
1546
|
-
const specHash = hash(specPath);
|
|
1547
|
-
const specNodeId = `file:${specPath}`;
|
|
1548
|
-
this.store.addNode({
|
|
1549
|
-
id: specNodeId,
|
|
1550
|
-
type: "document",
|
|
1551
|
-
name: path4.basename(specPath),
|
|
1552
|
-
path: specPath,
|
|
1553
|
-
metadata: { featureName }
|
|
1554
|
-
});
|
|
1555
|
-
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1556
|
-
for (const req of requirements) {
|
|
1557
|
-
this.store.addNode(req.node);
|
|
1558
|
-
nodesAdded++;
|
|
1559
|
-
this.store.addEdge({
|
|
1560
|
-
from: req.node.id,
|
|
1561
|
-
to: specNodeId,
|
|
1562
|
-
type: "specifies"
|
|
1563
|
-
});
|
|
1564
|
-
edgesAdded++;
|
|
1565
|
-
edgesAdded += this.linkByPathPattern(req.node.id, featureName);
|
|
1566
|
-
edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
|
|
1567
|
-
}
|
|
1568
|
-
} catch (err) {
|
|
1569
|
-
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1570
|
-
}
|
|
1645
|
+
const counts = await this.ingestFeatureDir(featureDir, errors);
|
|
1646
|
+
nodesAdded += counts.nodesAdded;
|
|
1647
|
+
edgesAdded += counts.edgesAdded;
|
|
1571
1648
|
}
|
|
1572
1649
|
return {
|
|
1573
1650
|
nodesAdded,
|
|
@@ -1578,6 +1655,48 @@ var RequirementIngestor = class {
|
|
|
1578
1655
|
durationMs: Date.now() - start
|
|
1579
1656
|
};
|
|
1580
1657
|
}
|
|
1658
|
+
async ingestFeatureDir(featureDir, errors) {
|
|
1659
|
+
const featureName = path4.basename(featureDir);
|
|
1660
|
+
const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1661
|
+
let content;
|
|
1662
|
+
try {
|
|
1663
|
+
content = await fs3.readFile(specPath, "utf-8");
|
|
1664
|
+
} catch {
|
|
1665
|
+
return { nodesAdded: 0, edgesAdded: 0 };
|
|
1666
|
+
}
|
|
1667
|
+
try {
|
|
1668
|
+
return this.ingestSpec(specPath, content, featureName);
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1671
|
+
return { nodesAdded: 0, edgesAdded: 0 };
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
ingestSpec(specPath, content, featureName) {
|
|
1675
|
+
const specHash = hash(specPath);
|
|
1676
|
+
const specNodeId = `file:${specPath}`;
|
|
1677
|
+
this.store.addNode({
|
|
1678
|
+
id: specNodeId,
|
|
1679
|
+
type: "document",
|
|
1680
|
+
name: path4.basename(specPath),
|
|
1681
|
+
path: specPath,
|
|
1682
|
+
metadata: { featureName }
|
|
1683
|
+
});
|
|
1684
|
+
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1685
|
+
let nodesAdded = 0;
|
|
1686
|
+
let edgesAdded = 0;
|
|
1687
|
+
for (const req of requirements) {
|
|
1688
|
+
const counts = this.ingestRequirement(req.node, specNodeId, featureName);
|
|
1689
|
+
nodesAdded += counts.nodesAdded;
|
|
1690
|
+
edgesAdded += counts.edgesAdded;
|
|
1691
|
+
}
|
|
1692
|
+
return { nodesAdded, edgesAdded };
|
|
1693
|
+
}
|
|
1694
|
+
ingestRequirement(node, specNodeId, featureName) {
|
|
1695
|
+
this.store.addNode(node);
|
|
1696
|
+
this.store.addEdge({ from: node.id, to: specNodeId, type: "specifies" });
|
|
1697
|
+
const edgesAdded = 1 + this.linkByPathPattern(node.id, featureName) + this.linkByKeywordOverlap(node.id, node.name);
|
|
1698
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1699
|
+
}
|
|
1581
1700
|
/**
|
|
1582
1701
|
* Parse markdown content and extract numbered items from recognized sections.
|
|
1583
1702
|
*/
|
|
@@ -1589,54 +1708,80 @@ var RequirementIngestor = class {
|
|
|
1589
1708
|
let globalIndex = 0;
|
|
1590
1709
|
for (let i = 0; i < lines.length; i++) {
|
|
1591
1710
|
const line = lines[i];
|
|
1592
|
-
const
|
|
1593
|
-
if (
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
);
|
|
1598
|
-
if (isReqSection) {
|
|
1599
|
-
currentSection = heading;
|
|
1600
|
-
inRequirementSection = true;
|
|
1601
|
-
} else {
|
|
1602
|
-
inRequirementSection = false;
|
|
1711
|
+
const sectionResult = this.processHeadingLine(line, inRequirementSection);
|
|
1712
|
+
if (sectionResult !== null) {
|
|
1713
|
+
inRequirementSection = sectionResult.inRequirementSection;
|
|
1714
|
+
if (sectionResult.currentSection !== void 0) {
|
|
1715
|
+
currentSection = sectionResult.currentSection;
|
|
1603
1716
|
}
|
|
1604
1717
|
continue;
|
|
1605
1718
|
}
|
|
1606
1719
|
if (!inRequirementSection) continue;
|
|
1607
1720
|
const itemMatch = line.match(NUMBERED_ITEM_RE);
|
|
1608
1721
|
if (!itemMatch) continue;
|
|
1609
|
-
const index = parseInt(itemMatch[1], 10);
|
|
1610
|
-
const text = itemMatch[2].trim();
|
|
1611
|
-
const rawText = line.trim();
|
|
1612
|
-
const lineNumber = i + 1;
|
|
1613
1722
|
globalIndex++;
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
},
|
|
1627
|
-
metadata: {
|
|
1628
|
-
specPath,
|
|
1629
|
-
index,
|
|
1630
|
-
section: currentSection,
|
|
1631
|
-
rawText,
|
|
1632
|
-
earsPattern,
|
|
1633
|
-
featureName
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
});
|
|
1723
|
+
results.push(
|
|
1724
|
+
this.buildRequirementNode(
|
|
1725
|
+
line,
|
|
1726
|
+
itemMatch,
|
|
1727
|
+
i + 1,
|
|
1728
|
+
specPath,
|
|
1729
|
+
specHash,
|
|
1730
|
+
globalIndex,
|
|
1731
|
+
featureName,
|
|
1732
|
+
currentSection
|
|
1733
|
+
)
|
|
1734
|
+
);
|
|
1637
1735
|
}
|
|
1638
1736
|
return results;
|
|
1639
1737
|
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Check if a line is a section heading and return updated section state,
|
|
1740
|
+
* or return null if the line is not a heading.
|
|
1741
|
+
*/
|
|
1742
|
+
processHeadingLine(line, _inRequirementSection) {
|
|
1743
|
+
const headingMatch = line.match(SECTION_HEADING_RE);
|
|
1744
|
+
if (!headingMatch) return null;
|
|
1745
|
+
const heading = headingMatch[1].trim();
|
|
1746
|
+
const isReqSection = REQUIREMENT_SECTIONS.some(
|
|
1747
|
+
(s) => heading.toLowerCase() === s.toLowerCase()
|
|
1748
|
+
);
|
|
1749
|
+
if (isReqSection) {
|
|
1750
|
+
return { inRequirementSection: true, currentSection: heading };
|
|
1751
|
+
}
|
|
1752
|
+
return { inRequirementSection: false };
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Build a requirement GraphNode from a matched numbered-item line.
|
|
1756
|
+
*/
|
|
1757
|
+
buildRequirementNode(line, itemMatch, lineNumber, specPath, specHash, globalIndex, featureName, currentSection) {
|
|
1758
|
+
const index = parseInt(itemMatch[1], 10);
|
|
1759
|
+
const text = itemMatch[2].trim();
|
|
1760
|
+
const rawText = line.trim();
|
|
1761
|
+
const nodeId = `req:${specHash}:${globalIndex}`;
|
|
1762
|
+
const earsPattern = detectEarsPattern(text);
|
|
1763
|
+
return {
|
|
1764
|
+
node: {
|
|
1765
|
+
id: nodeId,
|
|
1766
|
+
type: "requirement",
|
|
1767
|
+
name: text,
|
|
1768
|
+
path: specPath,
|
|
1769
|
+
location: {
|
|
1770
|
+
fileId: `file:${specPath}`,
|
|
1771
|
+
startLine: lineNumber,
|
|
1772
|
+
endLine: lineNumber
|
|
1773
|
+
},
|
|
1774
|
+
metadata: {
|
|
1775
|
+
specPath,
|
|
1776
|
+
index,
|
|
1777
|
+
section: currentSection,
|
|
1778
|
+
rawText,
|
|
1779
|
+
earsPattern,
|
|
1780
|
+
featureName
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1640
1785
|
/**
|
|
1641
1786
|
* Convention-based linking: match requirement to code/test files
|
|
1642
1787
|
* by feature name in their path.
|
|
@@ -1840,15 +1985,18 @@ function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
|
|
|
1840
1985
|
durationMs: Date.now() - start
|
|
1841
1986
|
};
|
|
1842
1987
|
}
|
|
1988
|
+
function appendJqlClause(jql, clause) {
|
|
1989
|
+
return jql ? `${jql} AND ${clause}` : clause;
|
|
1990
|
+
}
|
|
1843
1991
|
function buildJql(config) {
|
|
1844
1992
|
const project2 = config.project;
|
|
1845
1993
|
let jql = project2 ? `project=${project2}` : "";
|
|
1846
1994
|
const filters = config.filters;
|
|
1847
1995
|
if (filters?.status?.length) {
|
|
1848
|
-
jql
|
|
1996
|
+
jql = appendJqlClause(jql, `status IN (${filters.status.map((s) => `"${s}"`).join(",")})`);
|
|
1849
1997
|
}
|
|
1850
1998
|
if (filters?.labels?.length) {
|
|
1851
|
-
jql
|
|
1999
|
+
jql = appendJqlClause(jql, `labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`);
|
|
1852
2000
|
}
|
|
1853
2001
|
return jql;
|
|
1854
2002
|
}
|
|
@@ -1861,8 +2009,6 @@ var JiraConnector = class {
|
|
|
1861
2009
|
}
|
|
1862
2010
|
async ingest(store, config) {
|
|
1863
2011
|
const start = Date.now();
|
|
1864
|
-
let nodesAdded = 0;
|
|
1865
|
-
let edgesAdded = 0;
|
|
1866
2012
|
const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
|
|
1867
2013
|
const apiKey = process.env[apiKeyEnv];
|
|
1868
2014
|
if (!apiKey) {
|
|
@@ -1884,38 +2030,39 @@ var JiraConnector = class {
|
|
|
1884
2030
|
);
|
|
1885
2031
|
}
|
|
1886
2032
|
const jql = buildJql(config);
|
|
1887
|
-
const headers = {
|
|
1888
|
-
Authorization: `Basic ${apiKey}`,
|
|
1889
|
-
"Content-Type": "application/json"
|
|
1890
|
-
};
|
|
2033
|
+
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
1891
2034
|
try {
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
let total = Infinity;
|
|
1895
|
-
while (startAt < total) {
|
|
1896
|
-
const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
|
|
1897
|
-
const response = await this.httpClient(url, { headers });
|
|
1898
|
-
if (!response.ok) {
|
|
1899
|
-
return buildIngestResult(nodesAdded, edgesAdded, ["Jira API request failed"], start);
|
|
1900
|
-
}
|
|
1901
|
-
const data = await response.json();
|
|
1902
|
-
total = data.total;
|
|
1903
|
-
for (const issue of data.issues) {
|
|
1904
|
-
const counts = this.processIssue(store, issue);
|
|
1905
|
-
nodesAdded += counts.nodesAdded;
|
|
1906
|
-
edgesAdded += counts.edgesAdded;
|
|
1907
|
-
}
|
|
1908
|
-
startAt += maxResults;
|
|
1909
|
-
}
|
|
2035
|
+
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
|
|
2036
|
+
return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
|
|
1910
2037
|
} catch (err) {
|
|
1911
2038
|
return buildIngestResult(
|
|
1912
|
-
|
|
1913
|
-
|
|
2039
|
+
0,
|
|
2040
|
+
0,
|
|
1914
2041
|
[`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
|
|
1915
2042
|
start
|
|
1916
2043
|
);
|
|
1917
2044
|
}
|
|
1918
|
-
|
|
2045
|
+
}
|
|
2046
|
+
async fetchAllIssues(store, baseUrl, jql, headers) {
|
|
2047
|
+
let nodesAdded = 0;
|
|
2048
|
+
let edgesAdded = 0;
|
|
2049
|
+
let startAt = 0;
|
|
2050
|
+
const maxResults = 50;
|
|
2051
|
+
let total = Infinity;
|
|
2052
|
+
while (startAt < total) {
|
|
2053
|
+
const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
|
|
2054
|
+
const response = await this.httpClient(url, { headers });
|
|
2055
|
+
if (!response.ok) throw new Error("Jira API request failed");
|
|
2056
|
+
const data = await response.json();
|
|
2057
|
+
total = data.total;
|
|
2058
|
+
for (const issue of data.issues) {
|
|
2059
|
+
const counts = this.processIssue(store, issue);
|
|
2060
|
+
nodesAdded += counts.nodesAdded;
|
|
2061
|
+
edgesAdded += counts.edgesAdded;
|
|
2062
|
+
}
|
|
2063
|
+
startAt += maxResults;
|
|
2064
|
+
}
|
|
2065
|
+
return { nodesAdded, edgesAdded };
|
|
1919
2066
|
}
|
|
1920
2067
|
processIssue(store, issue) {
|
|
1921
2068
|
const nodeId = `issue:jira:${issue.key}`;
|
|
@@ -2036,6 +2183,16 @@ var SlackConnector = class {
|
|
|
2036
2183
|
};
|
|
2037
2184
|
|
|
2038
2185
|
// src/ingest/connectors/ConfluenceConnector.ts
|
|
2186
|
+
function missingApiKeyResult(envVar, start) {
|
|
2187
|
+
return {
|
|
2188
|
+
nodesAdded: 0,
|
|
2189
|
+
nodesUpdated: 0,
|
|
2190
|
+
edgesAdded: 0,
|
|
2191
|
+
edgesUpdated: 0,
|
|
2192
|
+
errors: [`Missing API key: environment variable "${envVar}" is not set`],
|
|
2193
|
+
durationMs: Date.now() - start
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2039
2196
|
var ConfluenceConnector = class {
|
|
2040
2197
|
name = "confluence";
|
|
2041
2198
|
source = "confluence";
|
|
@@ -2046,40 +2203,34 @@ var ConfluenceConnector = class {
|
|
|
2046
2203
|
async ingest(store, config) {
|
|
2047
2204
|
const start = Date.now();
|
|
2048
2205
|
const errors = [];
|
|
2049
|
-
let nodesAdded = 0;
|
|
2050
|
-
let edgesAdded = 0;
|
|
2051
2206
|
const apiKeyEnv = config.apiKeyEnv ?? "CONFLUENCE_API_KEY";
|
|
2052
2207
|
const apiKey = process.env[apiKeyEnv];
|
|
2053
2208
|
if (!apiKey) {
|
|
2054
|
-
return
|
|
2055
|
-
nodesAdded: 0,
|
|
2056
|
-
nodesUpdated: 0,
|
|
2057
|
-
edgesAdded: 0,
|
|
2058
|
-
edgesUpdated: 0,
|
|
2059
|
-
errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2060
|
-
durationMs: Date.now() - start
|
|
2061
|
-
};
|
|
2209
|
+
return missingApiKeyResult(apiKeyEnv, start);
|
|
2062
2210
|
}
|
|
2063
2211
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
2064
2212
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
2065
2213
|
const spaceKey = config.spaceKey ?? "";
|
|
2066
|
-
|
|
2067
|
-
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
2068
|
-
nodesAdded = result.nodesAdded;
|
|
2069
|
-
edgesAdded = result.edgesAdded;
|
|
2070
|
-
errors.push(...result.errors);
|
|
2071
|
-
} catch (err) {
|
|
2072
|
-
errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2073
|
-
}
|
|
2214
|
+
const counts = await this.fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors);
|
|
2074
2215
|
return {
|
|
2075
|
-
nodesAdded,
|
|
2216
|
+
nodesAdded: counts.nodesAdded,
|
|
2076
2217
|
nodesUpdated: 0,
|
|
2077
|
-
edgesAdded,
|
|
2218
|
+
edgesAdded: counts.edgesAdded,
|
|
2078
2219
|
edgesUpdated: 0,
|
|
2079
2220
|
errors,
|
|
2080
2221
|
durationMs: Date.now() - start
|
|
2081
2222
|
};
|
|
2082
2223
|
}
|
|
2224
|
+
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
|
|
2225
|
+
try {
|
|
2226
|
+
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
2227
|
+
errors.push(...result.errors);
|
|
2228
|
+
return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
|
|
2229
|
+
} catch (err) {
|
|
2230
|
+
errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2231
|
+
return { nodesAdded: 0, edgesAdded: 0 };
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2083
2234
|
async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
|
|
2084
2235
|
const errors = [];
|
|
2085
2236
|
let nodesAdded = 0;
|
|
@@ -2124,6 +2275,61 @@ var ConfluenceConnector = class {
|
|
|
2124
2275
|
};
|
|
2125
2276
|
|
|
2126
2277
|
// src/ingest/connectors/CIConnector.ts
|
|
2278
|
+
function emptyResult2(errors, start) {
|
|
2279
|
+
return {
|
|
2280
|
+
nodesAdded: 0,
|
|
2281
|
+
nodesUpdated: 0,
|
|
2282
|
+
edgesAdded: 0,
|
|
2283
|
+
edgesUpdated: 0,
|
|
2284
|
+
errors,
|
|
2285
|
+
durationMs: Date.now() - start
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
function ingestRun(store, run) {
|
|
2289
|
+
const buildId = `build:${run.id}`;
|
|
2290
|
+
const safeName = sanitizeExternalText(run.name, 200);
|
|
2291
|
+
let nodesAdded = 0;
|
|
2292
|
+
let edgesAdded = 0;
|
|
2293
|
+
store.addNode({
|
|
2294
|
+
id: buildId,
|
|
2295
|
+
type: "build",
|
|
2296
|
+
name: `${safeName} #${run.id}`,
|
|
2297
|
+
metadata: {
|
|
2298
|
+
source: "github-actions",
|
|
2299
|
+
status: run.status,
|
|
2300
|
+
conclusion: run.conclusion,
|
|
2301
|
+
branch: run.head_branch,
|
|
2302
|
+
sha: run.head_sha,
|
|
2303
|
+
url: run.html_url,
|
|
2304
|
+
createdAt: run.created_at
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
2307
|
+
nodesAdded++;
|
|
2308
|
+
const commitNode = store.getNode(`commit:${run.head_sha}`);
|
|
2309
|
+
if (commitNode) {
|
|
2310
|
+
store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
|
|
2311
|
+
edgesAdded++;
|
|
2312
|
+
}
|
|
2313
|
+
if (run.conclusion === "failure") {
|
|
2314
|
+
const testResultId = `test_result:${run.id}`;
|
|
2315
|
+
store.addNode({
|
|
2316
|
+
id: testResultId,
|
|
2317
|
+
type: "test_result",
|
|
2318
|
+
name: `Failed: ${safeName} #${run.id}`,
|
|
2319
|
+
metadata: {
|
|
2320
|
+
source: "github-actions",
|
|
2321
|
+
buildId: String(run.id),
|
|
2322
|
+
conclusion: "failure",
|
|
2323
|
+
branch: run.head_branch,
|
|
2324
|
+
sha: run.head_sha
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
nodesAdded++;
|
|
2328
|
+
store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
|
|
2329
|
+
edgesAdded++;
|
|
2330
|
+
}
|
|
2331
|
+
return { nodesAdded, edgesAdded };
|
|
2332
|
+
}
|
|
2127
2333
|
var CIConnector = class {
|
|
2128
2334
|
name = "ci";
|
|
2129
2335
|
source = "github-actions";
|
|
@@ -2134,22 +2340,29 @@ var CIConnector = class {
|
|
|
2134
2340
|
async ingest(store, config) {
|
|
2135
2341
|
const start = Date.now();
|
|
2136
2342
|
const errors = [];
|
|
2137
|
-
let nodesAdded = 0;
|
|
2138
|
-
let edgesAdded = 0;
|
|
2139
2343
|
const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
|
|
2140
2344
|
const apiKey = process.env[apiKeyEnv];
|
|
2141
2345
|
if (!apiKey) {
|
|
2142
|
-
return
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
edgesUpdated: 0,
|
|
2147
|
-
errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2148
|
-
durationMs: Date.now() - start
|
|
2149
|
-
};
|
|
2346
|
+
return emptyResult2(
|
|
2347
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2348
|
+
start
|
|
2349
|
+
);
|
|
2150
2350
|
}
|
|
2151
2351
|
const repo = config.repo ?? "";
|
|
2152
2352
|
const maxRuns = config.maxRuns ?? 10;
|
|
2353
|
+
const counts = await this.fetchAndIngestRuns(store, repo, maxRuns, apiKey, errors);
|
|
2354
|
+
return {
|
|
2355
|
+
nodesAdded: counts.nodesAdded,
|
|
2356
|
+
nodesUpdated: 0,
|
|
2357
|
+
edgesAdded: counts.edgesAdded,
|
|
2358
|
+
edgesUpdated: 0,
|
|
2359
|
+
errors,
|
|
2360
|
+
durationMs: Date.now() - start
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
async fetchAndIngestRuns(store, repo, maxRuns, apiKey, errors) {
|
|
2364
|
+
let nodesAdded = 0;
|
|
2365
|
+
let edgesAdded = 0;
|
|
2153
2366
|
try {
|
|
2154
2367
|
const url = `https://api.github.com/repos/${repo}/actions/runs?per_page=${maxRuns}`;
|
|
2155
2368
|
const response = await this.httpClient(url, {
|
|
@@ -2157,71 +2370,20 @@ var CIConnector = class {
|
|
|
2157
2370
|
});
|
|
2158
2371
|
if (!response.ok) {
|
|
2159
2372
|
errors.push(`GitHub Actions API error: status ${response.status}`);
|
|
2160
|
-
return {
|
|
2161
|
-
nodesAdded: 0,
|
|
2162
|
-
nodesUpdated: 0,
|
|
2163
|
-
edgesAdded: 0,
|
|
2164
|
-
edgesUpdated: 0,
|
|
2165
|
-
errors,
|
|
2166
|
-
durationMs: Date.now() - start
|
|
2167
|
-
};
|
|
2373
|
+
return { nodesAdded, edgesAdded };
|
|
2168
2374
|
}
|
|
2169
2375
|
const data = await response.json();
|
|
2170
2376
|
for (const run of data.workflow_runs) {
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
branch: run.head_branch,
|
|
2182
|
-
sha: run.head_sha,
|
|
2183
|
-
url: run.html_url,
|
|
2184
|
-
createdAt: run.created_at
|
|
2185
|
-
}
|
|
2186
|
-
});
|
|
2187
|
-
nodesAdded++;
|
|
2188
|
-
const commitNode = store.getNode(`commit:${run.head_sha}`);
|
|
2189
|
-
if (commitNode) {
|
|
2190
|
-
store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
|
|
2191
|
-
edgesAdded++;
|
|
2192
|
-
}
|
|
2193
|
-
if (run.conclusion === "failure") {
|
|
2194
|
-
const testResultId = `test_result:${run.id}`;
|
|
2195
|
-
store.addNode({
|
|
2196
|
-
id: testResultId,
|
|
2197
|
-
type: "test_result",
|
|
2198
|
-
name: `Failed: ${safeName} #${run.id}`,
|
|
2199
|
-
metadata: {
|
|
2200
|
-
source: "github-actions",
|
|
2201
|
-
buildId: String(run.id),
|
|
2202
|
-
conclusion: "failure",
|
|
2203
|
-
branch: run.head_branch,
|
|
2204
|
-
sha: run.head_sha
|
|
2205
|
-
}
|
|
2206
|
-
});
|
|
2207
|
-
nodesAdded++;
|
|
2208
|
-
store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
|
|
2209
|
-
edgesAdded++;
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
} catch (err) {
|
|
2213
|
-
errors.push(
|
|
2214
|
-
`GitHub Actions fetch error: ${err instanceof Error ? err.message : String(err)}`
|
|
2215
|
-
);
|
|
2216
|
-
}
|
|
2217
|
-
return {
|
|
2218
|
-
nodesAdded,
|
|
2219
|
-
nodesUpdated: 0,
|
|
2220
|
-
edgesAdded,
|
|
2221
|
-
edgesUpdated: 0,
|
|
2222
|
-
errors,
|
|
2223
|
-
durationMs: Date.now() - start
|
|
2224
|
-
};
|
|
2377
|
+
const counts = ingestRun(store, run);
|
|
2378
|
+
nodesAdded += counts.nodesAdded;
|
|
2379
|
+
edgesAdded += counts.edgesAdded;
|
|
2380
|
+
}
|
|
2381
|
+
} catch (err) {
|
|
2382
|
+
errors.push(
|
|
2383
|
+
`GitHub Actions fetch error: ${err instanceof Error ? err.message : String(err)}`
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
return { nodesAdded, edgesAdded };
|
|
2225
2387
|
}
|
|
2226
2388
|
};
|
|
2227
2389
|
|
|
@@ -2291,16 +2453,29 @@ var FusionLayer = class {
|
|
|
2291
2453
|
return [];
|
|
2292
2454
|
}
|
|
2293
2455
|
const allNodes = this.store.findNodes({});
|
|
2456
|
+
const semanticScores = this.buildSemanticScores(queryEmbedding, allNodes.length);
|
|
2457
|
+
const { kwWeight, semWeight } = this.resolveWeights(semanticScores.size > 0);
|
|
2458
|
+
const results = this.scoreNodes(allNodes, keywords, semanticScores, kwWeight, semWeight);
|
|
2459
|
+
results.sort((a, b) => b.score - a.score);
|
|
2460
|
+
return results.slice(0, topK);
|
|
2461
|
+
}
|
|
2462
|
+
buildSemanticScores(queryEmbedding, nodeCount) {
|
|
2294
2463
|
const semanticScores = /* @__PURE__ */ new Map();
|
|
2295
2464
|
if (queryEmbedding && this.vectorStore) {
|
|
2296
|
-
const vectorResults = this.vectorStore.search(queryEmbedding,
|
|
2465
|
+
const vectorResults = this.vectorStore.search(queryEmbedding, nodeCount);
|
|
2297
2466
|
for (const vr of vectorResults) {
|
|
2298
2467
|
semanticScores.set(vr.id, vr.score);
|
|
2299
2468
|
}
|
|
2300
2469
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2470
|
+
return semanticScores;
|
|
2471
|
+
}
|
|
2472
|
+
resolveWeights(hasSemanticScores) {
|
|
2473
|
+
return {
|
|
2474
|
+
kwWeight: hasSemanticScores ? this.keywordWeight : 1,
|
|
2475
|
+
semWeight: hasSemanticScores ? this.semanticWeight : 0
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
scoreNodes(allNodes, keywords, semanticScores, kwWeight, semWeight) {
|
|
2304
2479
|
const results = [];
|
|
2305
2480
|
for (const node of allNodes) {
|
|
2306
2481
|
const kwScore = this.keywordScore(keywords, node);
|
|
@@ -2311,15 +2486,11 @@ var FusionLayer = class {
|
|
|
2311
2486
|
nodeId: node.id,
|
|
2312
2487
|
node,
|
|
2313
2488
|
score: fusedScore,
|
|
2314
|
-
signals: {
|
|
2315
|
-
keyword: kwScore,
|
|
2316
|
-
semantic: semScore
|
|
2317
|
-
}
|
|
2489
|
+
signals: { keyword: kwScore, semantic: semScore }
|
|
2318
2490
|
});
|
|
2319
2491
|
}
|
|
2320
2492
|
}
|
|
2321
|
-
results
|
|
2322
|
-
return results.slice(0, topK);
|
|
2493
|
+
return results;
|
|
2323
2494
|
}
|
|
2324
2495
|
extractKeywords(query) {
|
|
2325
2496
|
const tokens = query.toLowerCase().split(/[\s\-_.,:;!?()[\]{}"'`/\\|@#$%^&*+=<>~]+/).filter((t) => t.length >= 2).filter((t) => !STOP_WORDS.has(t));
|
|
@@ -2374,37 +2545,50 @@ var GraphEntropyAdapter = class {
|
|
|
2374
2545
|
const missingTargets = [];
|
|
2375
2546
|
let freshEdges = 0;
|
|
2376
2547
|
for (const edge of documentsEdges) {
|
|
2377
|
-
const
|
|
2378
|
-
if (
|
|
2548
|
+
const result = this.classifyDocEdge(edge);
|
|
2549
|
+
if (result.kind === "missing") {
|
|
2379
2550
|
missingTargets.push(edge.to);
|
|
2380
|
-
|
|
2551
|
+
} else if (result.kind === "fresh") {
|
|
2552
|
+
freshEdges++;
|
|
2553
|
+
} else {
|
|
2554
|
+
staleEdges.push(result.entry);
|
|
2381
2555
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2556
|
+
}
|
|
2557
|
+
return { staleEdges, missingTargets, freshEdges };
|
|
2558
|
+
}
|
|
2559
|
+
classifyDocEdge(edge) {
|
|
2560
|
+
const codeNode = this.store.getNode(edge.to);
|
|
2561
|
+
if (!codeNode) {
|
|
2562
|
+
return { kind: "missing" };
|
|
2563
|
+
}
|
|
2564
|
+
const docNode = this.store.getNode(edge.from);
|
|
2565
|
+
const codeLastModified = codeNode.lastModified;
|
|
2566
|
+
const docLastModified = docNode?.lastModified;
|
|
2567
|
+
if (codeLastModified && docLastModified) {
|
|
2568
|
+
if (codeLastModified > docLastModified) {
|
|
2569
|
+
return {
|
|
2570
|
+
kind: "stale",
|
|
2571
|
+
entry: {
|
|
2388
2572
|
docNodeId: edge.from,
|
|
2389
2573
|
codeNodeId: edge.to,
|
|
2390
2574
|
edgeType: edge.type,
|
|
2391
2575
|
codeLastModified,
|
|
2392
2576
|
docLastModified
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
freshEdges++;
|
|
2396
|
-
}
|
|
2397
|
-
} else {
|
|
2398
|
-
staleEdges.push({
|
|
2399
|
-
docNodeId: edge.from,
|
|
2400
|
-
codeNodeId: edge.to,
|
|
2401
|
-
edgeType: edge.type,
|
|
2402
|
-
codeLastModified,
|
|
2403
|
-
docLastModified
|
|
2404
|
-
});
|
|
2577
|
+
}
|
|
2578
|
+
};
|
|
2405
2579
|
}
|
|
2580
|
+
return { kind: "fresh" };
|
|
2406
2581
|
}
|
|
2407
|
-
return {
|
|
2582
|
+
return {
|
|
2583
|
+
kind: "stale",
|
|
2584
|
+
entry: {
|
|
2585
|
+
docNodeId: edge.from,
|
|
2586
|
+
codeNodeId: edge.to,
|
|
2587
|
+
edgeType: edge.type,
|
|
2588
|
+
codeLastModified,
|
|
2589
|
+
docLastModified
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2408
2592
|
}
|
|
2409
2593
|
/**
|
|
2410
2594
|
* BFS from entry points to find reachable vs unreachable code nodes.
|
|
@@ -2661,36 +2845,12 @@ var GraphAnomalyAdapter = class {
|
|
|
2661
2845
|
store;
|
|
2662
2846
|
detect(options) {
|
|
2663
2847
|
const threshold = options?.threshold != null && options.threshold > 0 ? options.threshold : DEFAULT_THRESHOLD;
|
|
2664
|
-
const
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
if (RECOGNIZED_METRICS.has(m)) {
|
|
2669
|
-
metricsToAnalyze.push(m);
|
|
2670
|
-
} else {
|
|
2671
|
-
warnings.push(m);
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
const allOutliers = [];
|
|
2675
|
-
const analyzedNodeIds = /* @__PURE__ */ new Set();
|
|
2676
|
-
const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
|
|
2677
|
-
const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
|
|
2678
|
-
const needsComplexity = metricsToAnalyze.includes("hotspotScore");
|
|
2679
|
-
const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
|
|
2680
|
-
const cachedHotspotData = needsComplexity ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
|
|
2681
|
-
for (const metric of metricsToAnalyze) {
|
|
2682
|
-
const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
|
|
2683
|
-
for (const e of entries) {
|
|
2684
|
-
analyzedNodeIds.add(e.nodeId);
|
|
2685
|
-
}
|
|
2686
|
-
const outliers = this.computeZScoreOutliers(entries, metric, threshold);
|
|
2687
|
-
allOutliers.push(...outliers);
|
|
2688
|
-
}
|
|
2689
|
-
allOutliers.sort((a, b) => b.zScore - a.zScore);
|
|
2848
|
+
const { metricsToAnalyze, warnings } = this.filterMetrics(
|
|
2849
|
+
options?.metrics ?? [...DEFAULT_METRICS]
|
|
2850
|
+
);
|
|
2851
|
+
const { allOutliers, analyzedNodeIds } = this.computeAllOutliers(metricsToAnalyze, threshold);
|
|
2690
2852
|
const articulationPoints = this.findArticulationPoints();
|
|
2691
|
-
const
|
|
2692
|
-
const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
|
|
2693
|
-
const overlapping = [...outlierNodeIds].filter((id) => apNodeIds.has(id));
|
|
2853
|
+
const overlapping = this.computeOverlap(allOutliers, articulationPoints);
|
|
2694
2854
|
return {
|
|
2695
2855
|
statisticalOutliers: allOutliers,
|
|
2696
2856
|
articulationPoints,
|
|
@@ -2706,6 +2866,38 @@ var GraphAnomalyAdapter = class {
|
|
|
2706
2866
|
}
|
|
2707
2867
|
};
|
|
2708
2868
|
}
|
|
2869
|
+
filterMetrics(requested) {
|
|
2870
|
+
const metricsToAnalyze = [];
|
|
2871
|
+
const warnings = [];
|
|
2872
|
+
for (const m of requested) {
|
|
2873
|
+
if (RECOGNIZED_METRICS.has(m)) {
|
|
2874
|
+
metricsToAnalyze.push(m);
|
|
2875
|
+
} else {
|
|
2876
|
+
warnings.push(m);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
return { metricsToAnalyze, warnings };
|
|
2880
|
+
}
|
|
2881
|
+
computeAllOutliers(metricsToAnalyze, threshold) {
|
|
2882
|
+
const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
|
|
2883
|
+
const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
|
|
2884
|
+
const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
|
|
2885
|
+
const cachedHotspotData = metricsToAnalyze.includes("hotspotScore") ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
|
|
2886
|
+
const allOutliers = [];
|
|
2887
|
+
const analyzedNodeIds = /* @__PURE__ */ new Set();
|
|
2888
|
+
for (const metric of metricsToAnalyze) {
|
|
2889
|
+
const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
|
|
2890
|
+
for (const e of entries) analyzedNodeIds.add(e.nodeId);
|
|
2891
|
+
allOutliers.push(...this.computeZScoreOutliers(entries, metric, threshold));
|
|
2892
|
+
}
|
|
2893
|
+
allOutliers.sort((a, b) => b.zScore - a.zScore);
|
|
2894
|
+
return { allOutliers, analyzedNodeIds };
|
|
2895
|
+
}
|
|
2896
|
+
computeOverlap(outliers, articulationPoints) {
|
|
2897
|
+
const outlierNodeIds = new Set(outliers.map((o) => o.nodeId));
|
|
2898
|
+
const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
|
|
2899
|
+
return [...outlierNodeIds].filter((id) => apNodeIds.has(id));
|
|
2900
|
+
}
|
|
2709
2901
|
collectMetricValues(metric, cachedCouplingData, cachedHotspotData) {
|
|
2710
2902
|
const entries = [];
|
|
2711
2903
|
if (metric === "cyclomaticComplexity") {
|
|
@@ -3261,37 +3453,54 @@ var EntityExtractor = class {
|
|
|
3261
3453
|
result.push(entity);
|
|
3262
3454
|
}
|
|
3263
3455
|
};
|
|
3264
|
-
const quotedConsumed =
|
|
3456
|
+
const quotedConsumed = this.extractQuoted(trimmed, add);
|
|
3457
|
+
const casingConsumed = this.extractCasing(trimmed, quotedConsumed, add);
|
|
3458
|
+
const pathConsumed = this.extractPaths(trimmed, add);
|
|
3459
|
+
this.extractNouns(trimmed, buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed), add);
|
|
3460
|
+
return result;
|
|
3461
|
+
}
|
|
3462
|
+
/** Strategy 1: Quoted strings. Returns the set of consumed tokens. */
|
|
3463
|
+
extractQuoted(trimmed, add) {
|
|
3464
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
3265
3465
|
for (const match of trimmed.matchAll(QUOTED_RE)) {
|
|
3266
3466
|
const inner = match[1].trim();
|
|
3267
3467
|
if (inner.length > 0) {
|
|
3268
3468
|
add(inner);
|
|
3269
|
-
|
|
3469
|
+
consumed.add(inner);
|
|
3270
3470
|
}
|
|
3271
3471
|
}
|
|
3272
|
-
|
|
3472
|
+
return consumed;
|
|
3473
|
+
}
|
|
3474
|
+
/** Strategy 2: PascalCase/camelCase tokens. Returns the set of consumed tokens. */
|
|
3475
|
+
extractCasing(trimmed, quotedConsumed, add) {
|
|
3476
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
3273
3477
|
for (const match of trimmed.matchAll(PASCAL_OR_CAMEL_RE)) {
|
|
3274
3478
|
const token = match[0];
|
|
3275
3479
|
if (!quotedConsumed.has(token)) {
|
|
3276
3480
|
add(token);
|
|
3277
|
-
|
|
3481
|
+
consumed.add(token);
|
|
3278
3482
|
}
|
|
3279
3483
|
}
|
|
3280
|
-
|
|
3484
|
+
return consumed;
|
|
3485
|
+
}
|
|
3486
|
+
/** Strategy 3: File paths. Returns the set of consumed tokens. */
|
|
3487
|
+
extractPaths(trimmed, add) {
|
|
3488
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
3281
3489
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3282
3490
|
const path7 = match[0];
|
|
3283
3491
|
add(path7);
|
|
3284
|
-
|
|
3492
|
+
consumed.add(path7);
|
|
3285
3493
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3494
|
+
return consumed;
|
|
3495
|
+
}
|
|
3496
|
+
/** Strategy 4: Remaining significant nouns after stop-word and intent-keyword removal. */
|
|
3497
|
+
extractNouns(trimmed, allConsumed, add) {
|
|
3498
|
+
for (const raw of trimmed.split(/\s+/)) {
|
|
3289
3499
|
const cleaned = raw.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "");
|
|
3290
3500
|
if (cleaned.length === 0) continue;
|
|
3291
3501
|
if (isSkippableWord(cleaned, allConsumed)) continue;
|
|
3292
3502
|
add(cleaned);
|
|
3293
3503
|
}
|
|
3294
|
-
return result;
|
|
3295
3504
|
}
|
|
3296
3505
|
};
|
|
3297
3506
|
|
|
@@ -3708,36 +3917,41 @@ var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships"
|
|
|
3708
3917
|
var classifier = new IntentClassifier();
|
|
3709
3918
|
var extractor = new EntityExtractor();
|
|
3710
3919
|
var formatter = new ResponseFormatter();
|
|
3920
|
+
function lowConfidenceResult(intent, confidence) {
|
|
3921
|
+
return {
|
|
3922
|
+
intent,
|
|
3923
|
+
intentConfidence: confidence,
|
|
3924
|
+
entities: [],
|
|
3925
|
+
summary: "I'm not sure what you're asking. Try rephrasing your question.",
|
|
3926
|
+
data: null,
|
|
3927
|
+
suggestions: [
|
|
3928
|
+
'Try "what breaks if I change <name>?" for impact analysis',
|
|
3929
|
+
'Try "where is <name>?" to find entities',
|
|
3930
|
+
'Try "what calls <name>?" for relationships',
|
|
3931
|
+
'Try "what is <name>?" for explanations',
|
|
3932
|
+
'Try "what looks wrong?" for anomaly detection'
|
|
3933
|
+
]
|
|
3934
|
+
};
|
|
3935
|
+
}
|
|
3936
|
+
function noEntityResult(intent, confidence) {
|
|
3937
|
+
return {
|
|
3938
|
+
intent,
|
|
3939
|
+
intentConfidence: confidence,
|
|
3940
|
+
entities: [],
|
|
3941
|
+
summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
|
|
3942
|
+
data: null
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3711
3945
|
async function askGraph(store, question) {
|
|
3712
3946
|
const fusion = new FusionLayer(store);
|
|
3713
3947
|
const resolver = new EntityResolver(store, fusion);
|
|
3714
3948
|
const classification = classifier.classify(question);
|
|
3715
3949
|
if (classification.confidence < 0.3) {
|
|
3716
|
-
return
|
|
3717
|
-
intent: classification.intent,
|
|
3718
|
-
intentConfidence: classification.confidence,
|
|
3719
|
-
entities: [],
|
|
3720
|
-
summary: "I'm not sure what you're asking. Try rephrasing your question.",
|
|
3721
|
-
data: null,
|
|
3722
|
-
suggestions: [
|
|
3723
|
-
'Try "what breaks if I change <name>?" for impact analysis',
|
|
3724
|
-
'Try "where is <name>?" to find entities',
|
|
3725
|
-
'Try "what calls <name>?" for relationships',
|
|
3726
|
-
'Try "what is <name>?" for explanations',
|
|
3727
|
-
'Try "what looks wrong?" for anomaly detection'
|
|
3728
|
-
]
|
|
3729
|
-
};
|
|
3950
|
+
return lowConfidenceResult(classification.intent, classification.confidence);
|
|
3730
3951
|
}
|
|
3731
|
-
const
|
|
3732
|
-
const entities = resolver.resolve(rawEntities);
|
|
3952
|
+
const entities = resolver.resolve(extractor.extract(question));
|
|
3733
3953
|
if (ENTITY_REQUIRED_INTENTS.has(classification.intent) && entities.length === 0) {
|
|
3734
|
-
return
|
|
3735
|
-
intent: classification.intent,
|
|
3736
|
-
intentConfidence: classification.confidence,
|
|
3737
|
-
entities: [],
|
|
3738
|
-
summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
|
|
3739
|
-
data: null
|
|
3740
|
-
};
|
|
3954
|
+
return noEntityResult(classification.intent, classification.confidence);
|
|
3741
3955
|
}
|
|
3742
3956
|
let data;
|
|
3743
3957
|
try {
|
|
@@ -3751,67 +3965,59 @@ async function askGraph(store, question) {
|
|
|
3751
3965
|
data: null
|
|
3752
3966
|
};
|
|
3753
3967
|
}
|
|
3754
|
-
const summary = formatter.format(classification.intent, entities, data, question);
|
|
3755
3968
|
return {
|
|
3756
3969
|
intent: classification.intent,
|
|
3757
3970
|
intentConfidence: classification.confidence,
|
|
3758
3971
|
entities,
|
|
3759
|
-
summary,
|
|
3972
|
+
summary: formatter.format(classification.intent, entities, data, question),
|
|
3760
3973
|
data
|
|
3761
3974
|
};
|
|
3762
3975
|
}
|
|
3976
|
+
function buildContextBlocks(cql, rootIds, searchResults) {
|
|
3977
|
+
return rootIds.map((rootId) => {
|
|
3978
|
+
const expanded = cql.execute({ rootNodeIds: [rootId], maxDepth: 2 });
|
|
3979
|
+
const match = searchResults.find((r) => r.nodeId === rootId);
|
|
3980
|
+
return {
|
|
3981
|
+
rootNode: rootId,
|
|
3982
|
+
score: match?.score ?? 1,
|
|
3983
|
+
nodes: expanded.nodes,
|
|
3984
|
+
edges: expanded.edges
|
|
3985
|
+
};
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3988
|
+
function executeImpact(store, cql, entities, question) {
|
|
3989
|
+
const rootId = entities[0].nodeId;
|
|
3990
|
+
const lower = question.toLowerCase();
|
|
3991
|
+
if (lower.includes("blast radius") || lower.includes("cascade")) {
|
|
3992
|
+
return new CascadeSimulator(store).simulate(rootId);
|
|
3993
|
+
}
|
|
3994
|
+
const result = cql.execute({ rootNodeIds: [rootId], bidirectional: true, maxDepth: 3 });
|
|
3995
|
+
return groupNodesByImpact(result.nodes, rootId);
|
|
3996
|
+
}
|
|
3997
|
+
function executeExplain(cql, entities, question, fusion) {
|
|
3998
|
+
const searchResults = fusion.search(question, 10);
|
|
3999
|
+
const rootIds = entities.length > 0 ? [entities[0].nodeId] : searchResults.slice(0, 3).map((r) => r.nodeId);
|
|
4000
|
+
return { searchResults, context: buildContextBlocks(cql, rootIds, searchResults) };
|
|
4001
|
+
}
|
|
3763
4002
|
function executeOperation(store, intent, entities, question, fusion) {
|
|
3764
4003
|
const cql = new ContextQL(store);
|
|
3765
4004
|
switch (intent) {
|
|
3766
|
-
case "impact":
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
if (lowerQuestion.includes("blast radius") || lowerQuestion.includes("cascade")) {
|
|
3770
|
-
const simulator = new CascadeSimulator(store);
|
|
3771
|
-
return simulator.simulate(rootId);
|
|
3772
|
-
}
|
|
3773
|
-
const result = cql.execute({
|
|
3774
|
-
rootNodeIds: [rootId],
|
|
3775
|
-
bidirectional: true,
|
|
3776
|
-
maxDepth: 3
|
|
3777
|
-
});
|
|
3778
|
-
return groupNodesByImpact(result.nodes, rootId);
|
|
3779
|
-
}
|
|
3780
|
-
case "find": {
|
|
4005
|
+
case "impact":
|
|
4006
|
+
return executeImpact(store, cql, entities, question);
|
|
4007
|
+
case "find":
|
|
3781
4008
|
return fusion.search(question, 10);
|
|
3782
|
-
}
|
|
3783
4009
|
case "relationships": {
|
|
3784
|
-
const rootId = entities[0].nodeId;
|
|
3785
4010
|
const result = cql.execute({
|
|
3786
|
-
rootNodeIds: [
|
|
4011
|
+
rootNodeIds: [entities[0].nodeId],
|
|
3787
4012
|
bidirectional: true,
|
|
3788
4013
|
maxDepth: 1
|
|
3789
4014
|
});
|
|
3790
4015
|
return { nodes: result.nodes, edges: result.edges };
|
|
3791
4016
|
}
|
|
3792
|
-
case "explain":
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
for (const rootId of rootIds) {
|
|
3797
|
-
const expanded = cql.execute({
|
|
3798
|
-
rootNodeIds: [rootId],
|
|
3799
|
-
maxDepth: 2
|
|
3800
|
-
});
|
|
3801
|
-
const matchingResult = searchResults.find((r) => r.nodeId === rootId);
|
|
3802
|
-
contextBlocks.push({
|
|
3803
|
-
rootNode: rootId,
|
|
3804
|
-
score: matchingResult?.score ?? 1,
|
|
3805
|
-
nodes: expanded.nodes,
|
|
3806
|
-
edges: expanded.edges
|
|
3807
|
-
});
|
|
3808
|
-
}
|
|
3809
|
-
return { searchResults, context: contextBlocks };
|
|
3810
|
-
}
|
|
3811
|
-
case "anomaly": {
|
|
3812
|
-
const adapter = new GraphAnomalyAdapter(store);
|
|
3813
|
-
return adapter.detect();
|
|
3814
|
-
}
|
|
4017
|
+
case "explain":
|
|
4018
|
+
return executeExplain(cql, entities, question, fusion);
|
|
4019
|
+
case "anomaly":
|
|
4020
|
+
return new GraphAnomalyAdapter(store).detect();
|
|
3815
4021
|
default:
|
|
3816
4022
|
return null;
|
|
3817
4023
|
}
|
|
@@ -3832,12 +4038,14 @@ var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
|
|
|
3832
4038
|
"method",
|
|
3833
4039
|
"variable"
|
|
3834
4040
|
]);
|
|
4041
|
+
function countMetadataChars(node) {
|
|
4042
|
+
return node.metadata ? JSON.stringify(node.metadata).length : 0;
|
|
4043
|
+
}
|
|
4044
|
+
function countBaseChars(node) {
|
|
4045
|
+
return (node.name?.length ?? 0) + (node.path?.length ?? 0) + (node.type?.length ?? 0);
|
|
4046
|
+
}
|
|
3835
4047
|
function estimateNodeTokens(node) {
|
|
3836
|
-
|
|
3837
|
-
if (node.metadata) {
|
|
3838
|
-
chars += JSON.stringify(node.metadata).length;
|
|
3839
|
-
}
|
|
3840
|
-
return Math.ceil(chars / 4);
|
|
4048
|
+
return Math.ceil((countBaseChars(node) + countMetadataChars(node)) / 4);
|
|
3841
4049
|
}
|
|
3842
4050
|
var Assembler = class {
|
|
3843
4051
|
store;
|
|
@@ -3918,47 +4126,55 @@ var Assembler = class {
|
|
|
3918
4126
|
}
|
|
3919
4127
|
return { keptNodes, tokenEstimate, truncated };
|
|
3920
4128
|
}
|
|
3921
|
-
|
|
3922
|
-
* Compute a token budget allocation across node types.
|
|
3923
|
-
*/
|
|
3924
|
-
computeBudget(totalTokens, phase) {
|
|
3925
|
-
const allNodes = this.store.findNodes({});
|
|
4129
|
+
countNodesByType() {
|
|
3926
4130
|
const typeCounts = {};
|
|
3927
|
-
for (const node of
|
|
4131
|
+
for (const node of this.store.findNodes({})) {
|
|
3928
4132
|
typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;
|
|
3929
4133
|
}
|
|
4134
|
+
return typeCounts;
|
|
4135
|
+
}
|
|
4136
|
+
computeModuleDensity() {
|
|
3930
4137
|
const density = {};
|
|
3931
|
-
const
|
|
3932
|
-
|
|
3933
|
-
const
|
|
3934
|
-
|
|
3935
|
-
density[mod.name] = outEdges.length + inEdges.length;
|
|
4138
|
+
for (const mod of this.store.findNodes({ type: "module" })) {
|
|
4139
|
+
const out = this.store.getEdges({ from: mod.id }).length;
|
|
4140
|
+
const inn = this.store.getEdges({ to: mod.id }).length;
|
|
4141
|
+
density[mod.name] = out + inn;
|
|
3936
4142
|
}
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
4143
|
+
return density;
|
|
4144
|
+
}
|
|
4145
|
+
computeTypeWeights(typeCounts, boostTypes) {
|
|
3940
4146
|
const weights = {};
|
|
4147
|
+
let weightedTotal = 0;
|
|
3941
4148
|
for (const [type, count] of Object.entries(typeCounts)) {
|
|
3942
|
-
const
|
|
3943
|
-
const weight = count * (isBoosted ? boostFactor : 1);
|
|
4149
|
+
const weight = count * (boostTypes?.includes(type) ? 2 : 1);
|
|
3944
4150
|
weights[type] = weight;
|
|
3945
4151
|
weightedTotal += weight;
|
|
3946
4152
|
}
|
|
4153
|
+
return { weights, weightedTotal };
|
|
4154
|
+
}
|
|
4155
|
+
allocateProportionally(weights, weightedTotal, totalTokens) {
|
|
3947
4156
|
const allocations = {};
|
|
3948
|
-
if (weightedTotal
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
}
|
|
4157
|
+
if (weightedTotal === 0) return allocations;
|
|
4158
|
+
let allocated = 0;
|
|
4159
|
+
const types = Object.keys(weights);
|
|
4160
|
+
for (let i = 0; i < types.length; i++) {
|
|
4161
|
+
const type = types[i];
|
|
4162
|
+
if (i === types.length - 1) {
|
|
4163
|
+
allocations[type] = totalTokens - allocated;
|
|
4164
|
+
} else {
|
|
4165
|
+
const share = Math.round(weights[type] / weightedTotal * totalTokens);
|
|
4166
|
+
allocations[type] = share;
|
|
4167
|
+
allocated += share;
|
|
3960
4168
|
}
|
|
3961
4169
|
}
|
|
4170
|
+
return allocations;
|
|
4171
|
+
}
|
|
4172
|
+
computeBudget(totalTokens, phase) {
|
|
4173
|
+
const typeCounts = this.countNodesByType();
|
|
4174
|
+
const density = this.computeModuleDensity();
|
|
4175
|
+
const boostTypes = phase ? PHASE_NODE_TYPES[phase] : void 0;
|
|
4176
|
+
const { weights, weightedTotal } = this.computeTypeWeights(typeCounts, boostTypes);
|
|
4177
|
+
const allocations = this.allocateProportionally(weights, weightedTotal, totalTokens);
|
|
3962
4178
|
return { total: totalTokens, allocations, density };
|
|
3963
4179
|
}
|
|
3964
4180
|
/**
|
|
@@ -3989,49 +4205,43 @@ var Assembler = class {
|
|
|
3989
4205
|
filePaths: Array.from(filePathSet)
|
|
3990
4206
|
};
|
|
3991
4207
|
}
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
const moduleNodes = this.store.findNodes({ type: "module" });
|
|
3997
|
-
const modulesWithEdgeCount = moduleNodes.map((mod) => {
|
|
3998
|
-
const outEdges = this.store.getEdges({ from: mod.id });
|
|
3999
|
-
const inEdges = this.store.getEdges({ to: mod.id });
|
|
4000
|
-
return { module: mod, edgeCount: outEdges.length + inEdges.length };
|
|
4208
|
+
buildModuleLines() {
|
|
4209
|
+
const modulesWithEdgeCount = this.store.findNodes({ type: "module" }).map((mod) => {
|
|
4210
|
+
const edgeCount = this.store.getEdges({ from: mod.id }).length + this.store.getEdges({ to: mod.id }).length;
|
|
4211
|
+
return { module: mod, edgeCount };
|
|
4001
4212
|
});
|
|
4002
4213
|
modulesWithEdgeCount.sort((a, b) => b.edgeCount - a.edgeCount);
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
if (fileNode && fileNode.type === "file") {
|
|
4013
|
-
const symbolEdges = this.store.getEdges({ from: fileNode.id, type: "contains" });
|
|
4014
|
-
lines.push(`- ${fileNode.path ?? fileNode.name} (${symbolEdges.length} symbols)`);
|
|
4015
|
-
}
|
|
4214
|
+
if (modulesWithEdgeCount.length === 0) return [];
|
|
4215
|
+
const lines = ["## Modules", ""];
|
|
4216
|
+
for (const { module: mod, edgeCount } of modulesWithEdgeCount) {
|
|
4217
|
+
lines.push(`### ${mod.name} (${edgeCount} connections)`, "");
|
|
4218
|
+
for (const edge of this.store.getEdges({ from: mod.id, type: "contains" })) {
|
|
4219
|
+
const fileNode = this.store.getNode(edge.to);
|
|
4220
|
+
if (fileNode?.type === "file") {
|
|
4221
|
+
const symbols = this.store.getEdges({ from: fileNode.id, type: "contains" }).length;
|
|
4222
|
+
lines.push(`- ${fileNode.path ?? fileNode.name} (${symbols} symbols)`);
|
|
4016
4223
|
}
|
|
4017
|
-
lines.push("");
|
|
4018
4224
|
}
|
|
4225
|
+
lines.push("");
|
|
4019
4226
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
return { file: f, outDegree: outEdges.length };
|
|
4025
|
-
});
|
|
4227
|
+
return lines;
|
|
4228
|
+
}
|
|
4229
|
+
buildEntryPointLines() {
|
|
4230
|
+
const filesWithOutDegree = this.store.findNodes({ type: "file" }).filter((n) => !n.name.startsWith("index.")).map((f) => ({ file: f, outDegree: this.store.getEdges({ from: f.id }).length }));
|
|
4026
4231
|
filesWithOutDegree.sort((a, b) => b.outDegree - a.outDegree);
|
|
4027
4232
|
const entryPoints = filesWithOutDegree.filter((f) => f.outDegree > 0).slice(0, 5);
|
|
4028
|
-
if (entryPoints.length
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}
|
|
4033
|
-
lines.push("");
|
|
4233
|
+
if (entryPoints.length === 0) return [];
|
|
4234
|
+
const lines = ["## Entry Points", ""];
|
|
4235
|
+
for (const { file, outDegree } of entryPoints) {
|
|
4236
|
+
lines.push(`- ${file.path ?? file.name} (${outDegree} outbound edges)`);
|
|
4034
4237
|
}
|
|
4238
|
+
lines.push("");
|
|
4239
|
+
return lines;
|
|
4240
|
+
}
|
|
4241
|
+
generateMap() {
|
|
4242
|
+
const lines = ["# Repository Structure", ""];
|
|
4243
|
+
lines.push(...this.buildModuleLines());
|
|
4244
|
+
lines.push(...this.buildEntryPointLines());
|
|
4035
4245
|
return lines.join("\n");
|
|
4036
4246
|
}
|
|
4037
4247
|
/**
|
|
@@ -4165,10 +4375,15 @@ var GraphConstraintAdapter = class {
|
|
|
4165
4375
|
}
|
|
4166
4376
|
store;
|
|
4167
4377
|
computeDependencyGraph() {
|
|
4168
|
-
const
|
|
4169
|
-
const
|
|
4170
|
-
|
|
4171
|
-
|
|
4378
|
+
const nodes = this.collectFileNodePaths();
|
|
4379
|
+
const edges = this.collectImportEdges();
|
|
4380
|
+
return { nodes, edges };
|
|
4381
|
+
}
|
|
4382
|
+
collectFileNodePaths() {
|
|
4383
|
+
return this.store.findNodes({ type: "file" }).map((n) => n.path ?? n.id);
|
|
4384
|
+
}
|
|
4385
|
+
collectImportEdges() {
|
|
4386
|
+
return this.store.getEdges({ type: "imports" }).map((e) => {
|
|
4172
4387
|
const fromNode = this.store.getNode(e.from);
|
|
4173
4388
|
const toNode = this.store.getNode(e.to);
|
|
4174
4389
|
const fromPath = fromNode?.path ?? e.from;
|
|
@@ -4177,7 +4392,6 @@ var GraphConstraintAdapter = class {
|
|
|
4177
4392
|
const line = e.metadata?.line ?? 0;
|
|
4178
4393
|
return { from: fromPath, to: toPath, importType, line };
|
|
4179
4394
|
});
|
|
4180
|
-
return { nodes, edges };
|
|
4181
4395
|
}
|
|
4182
4396
|
computeLayerViolations(layers, rootDir) {
|
|
4183
4397
|
const { edges } = this.computeDependencyGraph();
|
|
@@ -4471,65 +4685,53 @@ var GraphFeedbackAdapter = class {
|
|
|
4471
4685
|
const affectedDocs = [];
|
|
4472
4686
|
let impactScope = 0;
|
|
4473
4687
|
for (const filePath of changedFiles) {
|
|
4474
|
-
const
|
|
4475
|
-
if (
|
|
4476
|
-
const
|
|
4477
|
-
|
|
4478
|
-
for (const edge of inboundImports) {
|
|
4479
|
-
const importerNode = this.store.getNode(edge.from);
|
|
4480
|
-
if (importerNode?.path && /test/i.test(importerNode.path)) {
|
|
4481
|
-
affectedTests.push({
|
|
4482
|
-
testFile: importerNode.path,
|
|
4483
|
-
coversFile: filePath
|
|
4484
|
-
});
|
|
4485
|
-
}
|
|
4486
|
-
impactScope++;
|
|
4487
|
-
}
|
|
4488
|
-
const docsEdges = this.store.getEdges({ to: fileNode.id, type: "documents" });
|
|
4489
|
-
for (const edge of docsEdges) {
|
|
4490
|
-
const docNode = this.store.getNode(edge.from);
|
|
4491
|
-
if (docNode) {
|
|
4492
|
-
affectedDocs.push({
|
|
4493
|
-
docFile: docNode.path ?? docNode.name,
|
|
4494
|
-
documentsFile: filePath
|
|
4495
|
-
});
|
|
4496
|
-
}
|
|
4497
|
-
}
|
|
4688
|
+
const fileNode = this.store.findNodes({ path: filePath })[0];
|
|
4689
|
+
if (!fileNode) continue;
|
|
4690
|
+
const counts = this.collectFileImpact(fileNode.id, filePath, affectedTests, affectedDocs);
|
|
4691
|
+
impactScope += counts.impactScope;
|
|
4498
4692
|
}
|
|
4499
4693
|
return { affectedTests, affectedDocs, impactScope };
|
|
4500
4694
|
}
|
|
4501
|
-
|
|
4502
|
-
const
|
|
4503
|
-
const
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
let undocumentedFiles = 0;
|
|
4508
|
-
for (const node of fileNodes) {
|
|
4509
|
-
const docsEdges = this.store.getEdges({ to: node.id, type: "documents" });
|
|
4510
|
-
if (docsEdges.length === 0) {
|
|
4511
|
-
undocumentedFiles++;
|
|
4695
|
+
collectFileImpact(fileNodeId, filePath, affectedTests, affectedDocs) {
|
|
4696
|
+
const inboundImports = this.store.getEdges({ to: fileNodeId, type: "imports" });
|
|
4697
|
+
for (const edge of inboundImports) {
|
|
4698
|
+
const importerNode = this.store.getNode(edge.from);
|
|
4699
|
+
if (importerNode?.path && /test/i.test(importerNode.path)) {
|
|
4700
|
+
affectedTests.push({ testFile: importerNode.path, coversFile: filePath });
|
|
4512
4701
|
}
|
|
4513
4702
|
}
|
|
4514
|
-
|
|
4515
|
-
for (const
|
|
4516
|
-
const
|
|
4517
|
-
if (
|
|
4518
|
-
|
|
4519
|
-
if (!isEntryPoint) {
|
|
4520
|
-
unreachableNodes++;
|
|
4521
|
-
}
|
|
4703
|
+
const docsEdges = this.store.getEdges({ to: fileNodeId, type: "documents" });
|
|
4704
|
+
for (const edge of docsEdges) {
|
|
4705
|
+
const docNode = this.store.getNode(edge.from);
|
|
4706
|
+
if (docNode) {
|
|
4707
|
+
affectedDocs.push({ docFile: docNode.path ?? docNode.name, documentsFile: filePath });
|
|
4522
4708
|
}
|
|
4523
4709
|
}
|
|
4710
|
+
return { impactScope: inboundImports.length };
|
|
4711
|
+
}
|
|
4712
|
+
computeHarnessCheckData() {
|
|
4713
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
4524
4714
|
return {
|
|
4525
4715
|
graphExists: true,
|
|
4526
|
-
nodeCount,
|
|
4527
|
-
edgeCount,
|
|
4528
|
-
constraintViolations,
|
|
4529
|
-
undocumentedFiles,
|
|
4530
|
-
unreachableNodes
|
|
4716
|
+
nodeCount: this.store.nodeCount,
|
|
4717
|
+
edgeCount: this.store.edgeCount,
|
|
4718
|
+
constraintViolations: this.store.getEdges({ type: "violates" }).length,
|
|
4719
|
+
undocumentedFiles: this.countUndocumentedFiles(fileNodes),
|
|
4720
|
+
unreachableNodes: this.countUnreachableNodes(fileNodes)
|
|
4531
4721
|
};
|
|
4532
4722
|
}
|
|
4723
|
+
countUndocumentedFiles(fileNodes) {
|
|
4724
|
+
return fileNodes.filter(
|
|
4725
|
+
(node) => this.store.getEdges({ to: node.id, type: "documents" }).length === 0
|
|
4726
|
+
).length;
|
|
4727
|
+
}
|
|
4728
|
+
countUnreachableNodes(fileNodes) {
|
|
4729
|
+
return fileNodes.filter((node) => {
|
|
4730
|
+
if (this.store.getEdges({ to: node.id, type: "imports" }).length > 0) return false;
|
|
4731
|
+
const isEntryPoint = node.name === "index.ts" || node.path !== void 0 && node.path.endsWith("/index.ts") || node.metadata?.entryPoint === true;
|
|
4732
|
+
return !isEntryPoint;
|
|
4733
|
+
}).length;
|
|
4734
|
+
}
|
|
4533
4735
|
};
|
|
4534
4736
|
|
|
4535
4737
|
// src/independence/TaskIndependenceAnalyzer.ts
|
|
@@ -4546,47 +4748,46 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4546
4748
|
this.validate(tasks);
|
|
4547
4749
|
const useGraph = this.store != null && depth > 0;
|
|
4548
4750
|
const analysisLevel = useGraph ? "graph-expanded" : "file-only";
|
|
4751
|
+
const { originalFiles, expandedFiles } = this.buildFileSets(tasks, useGraph, depth, edgeTypes);
|
|
4752
|
+
const taskIds = tasks.map((t) => t.id);
|
|
4753
|
+
const pairs = this.computeAllPairs(taskIds, originalFiles, expandedFiles);
|
|
4754
|
+
const groups = this.buildGroups(taskIds, pairs);
|
|
4755
|
+
const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
|
|
4756
|
+
return { tasks: taskIds, analysisLevel, depth, pairs, groups, verdict };
|
|
4757
|
+
}
|
|
4758
|
+
// --- Private methods ---
|
|
4759
|
+
buildFileSets(tasks, useGraph, depth, edgeTypes) {
|
|
4549
4760
|
const originalFiles = /* @__PURE__ */ new Map();
|
|
4550
4761
|
const expandedFiles = /* @__PURE__ */ new Map();
|
|
4551
4762
|
for (const task of tasks) {
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
} else {
|
|
4558
|
-
expandedFiles.set(task.id, /* @__PURE__ */ new Map());
|
|
4559
|
-
}
|
|
4763
|
+
originalFiles.set(task.id, new Set(task.files));
|
|
4764
|
+
expandedFiles.set(
|
|
4765
|
+
task.id,
|
|
4766
|
+
useGraph ? this.expandViaGraph(task.files, depth, edgeTypes) : /* @__PURE__ */ new Map()
|
|
4767
|
+
);
|
|
4560
4768
|
}
|
|
4561
|
-
|
|
4769
|
+
return { originalFiles, expandedFiles };
|
|
4770
|
+
}
|
|
4771
|
+
computeAllPairs(taskIds, originalFiles, expandedFiles) {
|
|
4562
4772
|
const pairs = [];
|
|
4563
4773
|
for (let i = 0; i < taskIds.length; i++) {
|
|
4564
4774
|
for (let j = i + 1; j < taskIds.length; j++) {
|
|
4565
4775
|
const idA = taskIds[i];
|
|
4566
4776
|
const idB = taskIds[j];
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4777
|
+
pairs.push(
|
|
4778
|
+
this.computePairOverlap(
|
|
4779
|
+
idA,
|
|
4780
|
+
idB,
|
|
4781
|
+
originalFiles.get(idA),
|
|
4782
|
+
originalFiles.get(idB),
|
|
4783
|
+
expandedFiles.get(idA),
|
|
4784
|
+
expandedFiles.get(idB)
|
|
4785
|
+
)
|
|
4574
4786
|
);
|
|
4575
|
-
pairs.push(pair);
|
|
4576
4787
|
}
|
|
4577
4788
|
}
|
|
4578
|
-
|
|
4579
|
-
const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
|
|
4580
|
-
return {
|
|
4581
|
-
tasks: taskIds,
|
|
4582
|
-
analysisLevel,
|
|
4583
|
-
depth,
|
|
4584
|
-
pairs,
|
|
4585
|
-
groups,
|
|
4586
|
-
verdict
|
|
4587
|
-
};
|
|
4789
|
+
return pairs;
|
|
4588
4790
|
}
|
|
4589
|
-
// --- Private methods ---
|
|
4590
4791
|
validate(tasks) {
|
|
4591
4792
|
if (tasks.length < 2) {
|
|
4592
4793
|
throw new Error("At least 2 tasks are required for independence analysis");
|
|
@@ -4739,27 +4940,62 @@ var ConflictPredictor = class {
|
|
|
4739
4940
|
predict(params) {
|
|
4740
4941
|
const analyzer = new TaskIndependenceAnalyzer(this.store);
|
|
4741
4942
|
const result = analyzer.analyze(params);
|
|
4943
|
+
const { churnMap, couplingMap, churnThreshold, couplingThreshold } = this.buildMetricMaps();
|
|
4944
|
+
const conflicts = this.classifyConflicts(
|
|
4945
|
+
result.pairs,
|
|
4946
|
+
churnMap,
|
|
4947
|
+
couplingMap,
|
|
4948
|
+
churnThreshold,
|
|
4949
|
+
couplingThreshold
|
|
4950
|
+
);
|
|
4951
|
+
const taskIds = result.tasks;
|
|
4952
|
+
const groups = this.buildHighSeverityGroups(taskIds, conflicts);
|
|
4953
|
+
const regrouped = !this.groupsEqual(result.groups, groups);
|
|
4954
|
+
const { highCount, mediumCount, lowCount } = this.countBySeverity(conflicts);
|
|
4955
|
+
const verdict = this.generateVerdict(
|
|
4956
|
+
taskIds,
|
|
4957
|
+
groups,
|
|
4958
|
+
result.analysisLevel,
|
|
4959
|
+
highCount,
|
|
4960
|
+
mediumCount,
|
|
4961
|
+
lowCount,
|
|
4962
|
+
regrouped
|
|
4963
|
+
);
|
|
4964
|
+
return {
|
|
4965
|
+
tasks: taskIds,
|
|
4966
|
+
analysisLevel: result.analysisLevel,
|
|
4967
|
+
depth: result.depth,
|
|
4968
|
+
conflicts,
|
|
4969
|
+
groups,
|
|
4970
|
+
summary: { high: highCount, medium: mediumCount, low: lowCount, regrouped },
|
|
4971
|
+
verdict
|
|
4972
|
+
};
|
|
4973
|
+
}
|
|
4974
|
+
// --- Private helpers ---
|
|
4975
|
+
buildMetricMaps() {
|
|
4742
4976
|
const churnMap = /* @__PURE__ */ new Map();
|
|
4743
4977
|
const couplingMap = /* @__PURE__ */ new Map();
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
}
|
|
4753
|
-
}
|
|
4754
|
-
const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
|
|
4755
|
-
for (const fileData of couplingResult.files) {
|
|
4756
|
-
couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
|
|
4978
|
+
if (this.store == null) {
|
|
4979
|
+
return { churnMap, couplingMap, churnThreshold: Infinity, couplingThreshold: Infinity };
|
|
4980
|
+
}
|
|
4981
|
+
const complexityResult = new GraphComplexityAdapter(this.store).computeComplexityHotspots();
|
|
4982
|
+
for (const hotspot of complexityResult.hotspots) {
|
|
4983
|
+
const existing = churnMap.get(hotspot.file);
|
|
4984
|
+
if (existing === void 0 || hotspot.changeFrequency > existing) {
|
|
4985
|
+
churnMap.set(hotspot.file, hotspot.changeFrequency);
|
|
4757
4986
|
}
|
|
4758
|
-
churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
|
|
4759
|
-
couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
|
|
4760
4987
|
}
|
|
4988
|
+
const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
|
|
4989
|
+
for (const fileData of couplingResult.files) {
|
|
4990
|
+
couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
|
|
4991
|
+
}
|
|
4992
|
+
const churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
|
|
4993
|
+
const couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
|
|
4994
|
+
return { churnMap, couplingMap, churnThreshold, couplingThreshold };
|
|
4995
|
+
}
|
|
4996
|
+
classifyConflicts(pairs, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
4761
4997
|
const conflicts = [];
|
|
4762
|
-
for (const pair of
|
|
4998
|
+
for (const pair of pairs) {
|
|
4763
4999
|
if (pair.independent) continue;
|
|
4764
5000
|
const { severity, reason, mitigation } = this.classifyPair(
|
|
4765
5001
|
pair.taskA,
|
|
@@ -4779,9 +5015,9 @@ var ConflictPredictor = class {
|
|
|
4779
5015
|
overlaps: pair.overlaps
|
|
4780
5016
|
});
|
|
4781
5017
|
}
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
5018
|
+
return conflicts;
|
|
5019
|
+
}
|
|
5020
|
+
countBySeverity(conflicts) {
|
|
4785
5021
|
let highCount = 0;
|
|
4786
5022
|
let mediumCount = 0;
|
|
4787
5023
|
let lowCount = 0;
|
|
@@ -4790,68 +5026,57 @@ var ConflictPredictor = class {
|
|
|
4790
5026
|
else if (c.severity === "medium") mediumCount++;
|
|
4791
5027
|
else lowCount++;
|
|
4792
5028
|
}
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
5029
|
+
return { highCount, mediumCount, lowCount };
|
|
5030
|
+
}
|
|
5031
|
+
classifyTransitiveOverlap(taskA, taskB, overlap, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
5032
|
+
const churn = churnMap.get(overlap.file);
|
|
5033
|
+
const coupling = couplingMap.get(overlap.file);
|
|
5034
|
+
const via = overlap.via ?? "unknown";
|
|
5035
|
+
if (churn !== void 0 && churn >= churnThreshold && churnThreshold !== Infinity) {
|
|
5036
|
+
return {
|
|
5037
|
+
severity: "medium",
|
|
5038
|
+
reason: `Transitive overlap on high-churn file ${overlap.file} (via ${via})`,
|
|
5039
|
+
mitigation: `Review: ${overlap.file} changes frequently \u2014 coordinate edits between ${taskA} and ${taskB}`
|
|
5040
|
+
};
|
|
5041
|
+
}
|
|
5042
|
+
if (coupling !== void 0 && coupling >= couplingThreshold && couplingThreshold !== Infinity) {
|
|
5043
|
+
return {
|
|
5044
|
+
severity: "medium",
|
|
5045
|
+
reason: `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`,
|
|
5046
|
+
mitigation: `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`
|
|
5047
|
+
};
|
|
5048
|
+
}
|
|
4802
5049
|
return {
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
conflicts,
|
|
4807
|
-
groups,
|
|
4808
|
-
summary: {
|
|
4809
|
-
high: highCount,
|
|
4810
|
-
medium: mediumCount,
|
|
4811
|
-
low: lowCount,
|
|
4812
|
-
regrouped
|
|
4813
|
-
},
|
|
4814
|
-
verdict
|
|
5050
|
+
severity: "low",
|
|
5051
|
+
reason: `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`,
|
|
5052
|
+
mitigation: `Info: transitive overlap unlikely to cause conflicts`
|
|
4815
5053
|
};
|
|
4816
5054
|
}
|
|
4817
|
-
// --- Private helpers ---
|
|
4818
5055
|
classifyPair(taskA, taskB, overlaps, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
4819
5056
|
let maxSeverity = "low";
|
|
4820
5057
|
let primaryReason = "";
|
|
4821
5058
|
let primaryMitigation = "";
|
|
4822
5059
|
for (const overlap of overlaps) {
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
reason = `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`;
|
|
4841
|
-
mitigation = `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`;
|
|
4842
|
-
} else {
|
|
4843
|
-
overlapSeverity = "low";
|
|
4844
|
-
reason = `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`;
|
|
4845
|
-
mitigation = `Info: transitive overlap unlikely to cause conflicts`;
|
|
4846
|
-
}
|
|
4847
|
-
}
|
|
4848
|
-
if (this.severityRank(overlapSeverity) > this.severityRank(maxSeverity)) {
|
|
4849
|
-
maxSeverity = overlapSeverity;
|
|
4850
|
-
primaryReason = reason;
|
|
4851
|
-
primaryMitigation = mitigation;
|
|
5060
|
+
const classified = overlap.type === "direct" ? {
|
|
5061
|
+
severity: "high",
|
|
5062
|
+
reason: `Both tasks write to ${overlap.file}`,
|
|
5063
|
+
mitigation: `Serialize: run ${taskA} before ${taskB}`
|
|
5064
|
+
} : this.classifyTransitiveOverlap(
|
|
5065
|
+
taskA,
|
|
5066
|
+
taskB,
|
|
5067
|
+
overlap,
|
|
5068
|
+
churnMap,
|
|
5069
|
+
couplingMap,
|
|
5070
|
+
churnThreshold,
|
|
5071
|
+
couplingThreshold
|
|
5072
|
+
);
|
|
5073
|
+
if (this.severityRank(classified.severity) > this.severityRank(maxSeverity)) {
|
|
5074
|
+
maxSeverity = classified.severity;
|
|
5075
|
+
primaryReason = classified.reason;
|
|
5076
|
+
primaryMitigation = classified.mitigation;
|
|
4852
5077
|
} else if (primaryReason === "") {
|
|
4853
|
-
primaryReason = reason;
|
|
4854
|
-
primaryMitigation = mitigation;
|
|
5078
|
+
primaryReason = classified.reason;
|
|
5079
|
+
primaryMitigation = classified.mitigation;
|
|
4855
5080
|
}
|
|
4856
5081
|
}
|
|
4857
5082
|
return { severity: maxSeverity, reason: primaryReason, mitigation: primaryMitigation };
|
|
@@ -4974,7 +5199,7 @@ var ConflictPredictor = class {
|
|
|
4974
5199
|
};
|
|
4975
5200
|
|
|
4976
5201
|
// src/index.ts
|
|
4977
|
-
var VERSION = "0.4.
|
|
5202
|
+
var VERSION = "0.4.3";
|
|
4978
5203
|
export {
|
|
4979
5204
|
Assembler,
|
|
4980
5205
|
CIConnector,
|
|
@@ -5005,8 +5230,10 @@ export {
|
|
|
5005
5230
|
IntentClassifier,
|
|
5006
5231
|
JiraConnector,
|
|
5007
5232
|
KnowledgeIngestor,
|
|
5233
|
+
NODE_STABILITY,
|
|
5008
5234
|
NODE_TYPES,
|
|
5009
5235
|
OBSERVABILITY_TYPES,
|
|
5236
|
+
PackedSummaryCache,
|
|
5010
5237
|
RequirementIngestor,
|
|
5011
5238
|
ResponseFormatter,
|
|
5012
5239
|
SlackConnector,
|
|
@@ -5020,6 +5247,7 @@ export {
|
|
|
5020
5247
|
groupNodesByImpact,
|
|
5021
5248
|
linkToCode,
|
|
5022
5249
|
loadGraph,
|
|
5250
|
+
normalizeIntent,
|
|
5023
5251
|
project,
|
|
5024
5252
|
queryTraceability,
|
|
5025
5253
|
saveGraph
|