@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.js
CHANGED
|
@@ -59,8 +59,10 @@ __export(index_exports, {
|
|
|
59
59
|
IntentClassifier: () => IntentClassifier,
|
|
60
60
|
JiraConnector: () => JiraConnector,
|
|
61
61
|
KnowledgeIngestor: () => KnowledgeIngestor,
|
|
62
|
+
NODE_STABILITY: () => NODE_STABILITY,
|
|
62
63
|
NODE_TYPES: () => NODE_TYPES,
|
|
63
64
|
OBSERVABILITY_TYPES: () => OBSERVABILITY_TYPES,
|
|
65
|
+
PackedSummaryCache: () => PackedSummaryCache,
|
|
64
66
|
RequirementIngestor: () => RequirementIngestor,
|
|
65
67
|
ResponseFormatter: () => ResponseFormatter,
|
|
66
68
|
SlackConnector: () => SlackConnector,
|
|
@@ -74,6 +76,7 @@ __export(index_exports, {
|
|
|
74
76
|
groupNodesByImpact: () => groupNodesByImpact,
|
|
75
77
|
linkToCode: () => linkToCode,
|
|
76
78
|
loadGraph: () => loadGraph,
|
|
79
|
+
normalizeIntent: () => normalizeIntent,
|
|
77
80
|
project: () => project,
|
|
78
81
|
queryTraceability: () => queryTraceability,
|
|
79
82
|
saveGraph: () => saveGraph
|
|
@@ -119,7 +122,9 @@ var NODE_TYPES = [
|
|
|
119
122
|
"aesthetic_intent",
|
|
120
123
|
"design_constraint",
|
|
121
124
|
// Traceability
|
|
122
|
-
"requirement"
|
|
125
|
+
"requirement",
|
|
126
|
+
// Cache
|
|
127
|
+
"packed_summary"
|
|
123
128
|
];
|
|
124
129
|
var EDGE_TYPES = [
|
|
125
130
|
// Code relationships
|
|
@@ -152,10 +157,21 @@ var EDGE_TYPES = [
|
|
|
152
157
|
// Traceability relationships
|
|
153
158
|
"requires",
|
|
154
159
|
"verified_by",
|
|
155
|
-
"tested_by"
|
|
160
|
+
"tested_by",
|
|
161
|
+
// Cache relationships
|
|
162
|
+
"caches"
|
|
156
163
|
];
|
|
157
164
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
158
165
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
166
|
+
var NODE_STABILITY = {
|
|
167
|
+
File: "session",
|
|
168
|
+
Function: "session",
|
|
169
|
+
Class: "session",
|
|
170
|
+
Constraint: "session",
|
|
171
|
+
PackedSummary: "session",
|
|
172
|
+
SkillDefinition: "static",
|
|
173
|
+
ToolDefinition: "static"
|
|
174
|
+
};
|
|
159
175
|
var GraphNodeSchema = import_zod.z.object({
|
|
160
176
|
id: import_zod.z.string(),
|
|
161
177
|
type: import_zod.z.enum(NODE_TYPES),
|
|
@@ -342,21 +358,26 @@ var GraphStore = class {
|
|
|
342
358
|
return this.edgeMap.values();
|
|
343
359
|
}
|
|
344
360
|
getNeighbors(nodeId, direction = "both") {
|
|
345
|
-
const neighborIds =
|
|
361
|
+
const neighborIds = this.collectNeighborIds(nodeId, direction);
|
|
362
|
+
return this.resolveNodes(neighborIds);
|
|
363
|
+
}
|
|
364
|
+
collectNeighborIds(nodeId, direction) {
|
|
365
|
+
const ids = /* @__PURE__ */ new Set();
|
|
346
366
|
if (direction === "outbound" || direction === "both") {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
neighborIds.add(edge.to);
|
|
367
|
+
for (const edge of this.edgesByFrom.get(nodeId) ?? []) {
|
|
368
|
+
ids.add(edge.to);
|
|
350
369
|
}
|
|
351
370
|
}
|
|
352
371
|
if (direction === "inbound" || direction === "both") {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
neighborIds.add(edge.from);
|
|
372
|
+
for (const edge of this.edgesByTo.get(nodeId) ?? []) {
|
|
373
|
+
ids.add(edge.from);
|
|
356
374
|
}
|
|
357
375
|
}
|
|
376
|
+
return ids;
|
|
377
|
+
}
|
|
378
|
+
resolveNodes(ids) {
|
|
358
379
|
const results = [];
|
|
359
|
-
for (const nid of
|
|
380
|
+
for (const nid of ids) {
|
|
360
381
|
const node = this.getNode(nid);
|
|
361
382
|
if (node) results.push(node);
|
|
362
383
|
}
|
|
@@ -486,6 +507,94 @@ var VectorStore = class _VectorStore {
|
|
|
486
507
|
}
|
|
487
508
|
};
|
|
488
509
|
|
|
510
|
+
// src/store/PackedSummaryCache.ts
|
|
511
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
512
|
+
function normalizeIntent(intent) {
|
|
513
|
+
return intent.trim().toLowerCase().replace(/\s+/g, " ");
|
|
514
|
+
}
|
|
515
|
+
function cacheNodeId(normalizedIntent) {
|
|
516
|
+
return `packed_summary:${normalizedIntent}`;
|
|
517
|
+
}
|
|
518
|
+
var PackedSummaryCache = class {
|
|
519
|
+
constructor(store, ttlMs = DEFAULT_TTL_MS) {
|
|
520
|
+
this.store = store;
|
|
521
|
+
this.ttlMs = ttlMs;
|
|
522
|
+
}
|
|
523
|
+
store;
|
|
524
|
+
ttlMs;
|
|
525
|
+
/** Returns cached envelope with `cached: true` if valid, or null if miss/stale. */
|
|
526
|
+
get(intent) {
|
|
527
|
+
const normalized = normalizeIntent(intent);
|
|
528
|
+
const nodeId = cacheNodeId(normalized);
|
|
529
|
+
const node = this.store.getNode(nodeId);
|
|
530
|
+
if (!node) return null;
|
|
531
|
+
const createdMs = this.parseCreatedMs(node.metadata["createdAt"]);
|
|
532
|
+
if (createdMs === null) return null;
|
|
533
|
+
if (Date.now() - createdMs > this.ttlMs) return null;
|
|
534
|
+
if (!this.areSourcesFresh(nodeId, node, createdMs)) return null;
|
|
535
|
+
return this.parseEnvelope(node.metadata["envelope"]);
|
|
536
|
+
}
|
|
537
|
+
/** Parse and validate createdAt. Returns epoch ms or null if missing/malformed (GC-002). */
|
|
538
|
+
parseCreatedMs(createdAt) {
|
|
539
|
+
if (!createdAt) return null;
|
|
540
|
+
const ms = new Date(createdAt).getTime();
|
|
541
|
+
return Number.isNaN(ms) ? null : ms;
|
|
542
|
+
}
|
|
543
|
+
/** GC-001: Checks source nodes exist and are unmodified since cache creation. */
|
|
544
|
+
areSourcesFresh(nodeId, node, createdMs) {
|
|
545
|
+
const sourceNodeIds = node.metadata["sourceNodeIds"];
|
|
546
|
+
const edges = this.store.getEdges({ from: nodeId, type: "caches" });
|
|
547
|
+
if (sourceNodeIds && edges.length < sourceNodeIds.length) return false;
|
|
548
|
+
for (const edge of edges) {
|
|
549
|
+
const sourceNode = this.store.getNode(edge.to);
|
|
550
|
+
if (!sourceNode) return false;
|
|
551
|
+
if (sourceNode.lastModified) {
|
|
552
|
+
const sourceModMs = new Date(sourceNode.lastModified).getTime();
|
|
553
|
+
if (sourceModMs > createdMs) return false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
/** Parse envelope JSON and set cached: true. Returns null on invalid JSON. */
|
|
559
|
+
parseEnvelope(raw) {
|
|
560
|
+
try {
|
|
561
|
+
const envelope = JSON.parse(raw);
|
|
562
|
+
return { ...envelope, meta: { ...envelope.meta, cached: true } };
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/** Write a PackedSummary node with caches edges to source nodes. */
|
|
568
|
+
set(intent, envelope, sourceNodeIds) {
|
|
569
|
+
const normalized = normalizeIntent(intent);
|
|
570
|
+
const nodeId = cacheNodeId(normalized);
|
|
571
|
+
this.store.removeNode(nodeId);
|
|
572
|
+
this.store.addNode({
|
|
573
|
+
id: nodeId,
|
|
574
|
+
type: "packed_summary",
|
|
575
|
+
name: normalized,
|
|
576
|
+
metadata: {
|
|
577
|
+
envelope: JSON.stringify(envelope),
|
|
578
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
579
|
+
sourceNodeIds
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
for (const sourceId of sourceNodeIds) {
|
|
583
|
+
this.store.addEdge({
|
|
584
|
+
from: nodeId,
|
|
585
|
+
to: sourceId,
|
|
586
|
+
type: "caches"
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/** Explicitly invalidate a cached packed summary. */
|
|
591
|
+
invalidate(intent) {
|
|
592
|
+
const normalized = normalizeIntent(intent);
|
|
593
|
+
const nodeId = cacheNodeId(normalized);
|
|
594
|
+
this.store.removeNode(nodeId);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
489
598
|
// src/query/ContextQL.ts
|
|
490
599
|
function edgeKey2(e) {
|
|
491
600
|
return `${e.from}|${e.to}|${e.type}`;
|
|
@@ -1103,6 +1212,17 @@ var CodeIngestor = class {
|
|
|
1103
1212
|
var import_node_child_process = require("child_process");
|
|
1104
1213
|
var import_node_util = require("util");
|
|
1105
1214
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
1215
|
+
function finalizeCommit(current) {
|
|
1216
|
+
return {
|
|
1217
|
+
hash: current.hash,
|
|
1218
|
+
shortHash: current.shortHash,
|
|
1219
|
+
author: current.author,
|
|
1220
|
+
email: current.email,
|
|
1221
|
+
date: current.date,
|
|
1222
|
+
message: current.message,
|
|
1223
|
+
files: current.files
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1106
1226
|
var GitIngestor = class {
|
|
1107
1227
|
constructor(store, gitRunner) {
|
|
1108
1228
|
this.store = store;
|
|
@@ -1139,39 +1259,49 @@ var GitIngestor = class {
|
|
|
1139
1259
|
}
|
|
1140
1260
|
const commits = this.parseGitLog(output);
|
|
1141
1261
|
for (const commit of commits) {
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1262
|
+
const counts = this.ingestCommit(commit);
|
|
1263
|
+
nodesAdded += counts.nodesAdded;
|
|
1264
|
+
edgesAdded += counts.edgesAdded;
|
|
1265
|
+
}
|
|
1266
|
+
edgesAdded += this.ingestCoChanges(commits);
|
|
1267
|
+
return {
|
|
1268
|
+
nodesAdded,
|
|
1269
|
+
nodesUpdated,
|
|
1270
|
+
edgesAdded,
|
|
1271
|
+
edgesUpdated,
|
|
1272
|
+
errors,
|
|
1273
|
+
durationMs: Date.now() - start
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
ingestCommit(commit) {
|
|
1277
|
+
const nodeId = `commit:${commit.shortHash}`;
|
|
1278
|
+
this.store.addNode({
|
|
1279
|
+
id: nodeId,
|
|
1280
|
+
type: "commit",
|
|
1281
|
+
name: commit.message,
|
|
1282
|
+
metadata: {
|
|
1283
|
+
author: commit.author,
|
|
1284
|
+
email: commit.email,
|
|
1285
|
+
date: commit.date,
|
|
1286
|
+
hash: commit.hash
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
let edgesAdded = 0;
|
|
1290
|
+
for (const file of commit.files) {
|
|
1291
|
+
const fileNodeId = `file:${file}`;
|
|
1292
|
+
if (this.store.getNode(fileNodeId)) {
|
|
1293
|
+
this.store.addEdge({ from: fileNodeId, to: nodeId, type: "triggered_by" });
|
|
1294
|
+
edgesAdded++;
|
|
1166
1295
|
}
|
|
1167
1296
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1297
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1298
|
+
}
|
|
1299
|
+
ingestCoChanges(commits) {
|
|
1300
|
+
let edgesAdded = 0;
|
|
1301
|
+
for (const { fileA, fileB, count } of this.computeCoChanges(commits)) {
|
|
1170
1302
|
const fileAId = `file:${fileA}`;
|
|
1171
1303
|
const fileBId = `file:${fileB}`;
|
|
1172
|
-
|
|
1173
|
-
const nodeB = this.store.getNode(fileBId);
|
|
1174
|
-
if (nodeA && nodeB) {
|
|
1304
|
+
if (this.store.getNode(fileAId) && this.store.getNode(fileBId)) {
|
|
1175
1305
|
this.store.addEdge({
|
|
1176
1306
|
from: fileAId,
|
|
1177
1307
|
to: fileBId,
|
|
@@ -1181,14 +1311,7 @@ var GitIngestor = class {
|
|
|
1181
1311
|
edgesAdded++;
|
|
1182
1312
|
}
|
|
1183
1313
|
}
|
|
1184
|
-
return
|
|
1185
|
-
nodesAdded,
|
|
1186
|
-
nodesUpdated,
|
|
1187
|
-
edgesAdded,
|
|
1188
|
-
edgesUpdated,
|
|
1189
|
-
errors,
|
|
1190
|
-
durationMs: Date.now() - start
|
|
1191
|
-
};
|
|
1314
|
+
return edgesAdded;
|
|
1192
1315
|
}
|
|
1193
1316
|
async runGit(rootDir, args) {
|
|
1194
1317
|
if (this.gitRunner) {
|
|
@@ -1203,63 +1326,49 @@ var GitIngestor = class {
|
|
|
1203
1326
|
const lines = output.split("\n");
|
|
1204
1327
|
let current = null;
|
|
1205
1328
|
for (const line of lines) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1329
|
+
current = this.processLogLine(line, current, commits);
|
|
1330
|
+
}
|
|
1331
|
+
if (current) {
|
|
1332
|
+
commits.push(finalizeCommit(current));
|
|
1333
|
+
}
|
|
1334
|
+
return commits;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Process one line from git log output, updating the in-progress commit builder
|
|
1338
|
+
* and flushing completed commits into the accumulator.
|
|
1339
|
+
* Returns the updated current builder (null if flushed and not replaced).
|
|
1340
|
+
*/
|
|
1341
|
+
processLogLine(line, current, commits) {
|
|
1342
|
+
const trimmed = line.trim();
|
|
1343
|
+
if (!trimmed) {
|
|
1344
|
+
if (current?.hasFiles) {
|
|
1345
|
+
commits.push(finalizeCommit(current));
|
|
1346
|
+
return null;
|
|
1221
1347
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
author: current.author,
|
|
1229
|
-
email: current.email,
|
|
1230
|
-
date: current.date,
|
|
1231
|
-
message: current.message,
|
|
1232
|
-
files: current.files
|
|
1233
|
-
});
|
|
1234
|
-
}
|
|
1235
|
-
current = {
|
|
1236
|
-
hash: parts[0],
|
|
1237
|
-
shortHash: parts[0].substring(0, 7),
|
|
1238
|
-
author: parts[1],
|
|
1239
|
-
email: parts[2],
|
|
1240
|
-
date: parts[3],
|
|
1241
|
-
message: parts.slice(4).join("|"),
|
|
1242
|
-
// message may contain |
|
|
1243
|
-
files: [],
|
|
1244
|
-
hasFiles: false
|
|
1245
|
-
};
|
|
1246
|
-
} else if (current) {
|
|
1247
|
-
current.files.push(trimmed);
|
|
1248
|
-
current.hasFiles = true;
|
|
1348
|
+
return current;
|
|
1349
|
+
}
|
|
1350
|
+
const parts = trimmed.split("|");
|
|
1351
|
+
if (parts.length >= 5 && /^[0-9a-f]{7,40}$/.test(parts[0])) {
|
|
1352
|
+
if (current) {
|
|
1353
|
+
commits.push(finalizeCommit(current));
|
|
1249
1354
|
}
|
|
1355
|
+
return {
|
|
1356
|
+
hash: parts[0],
|
|
1357
|
+
shortHash: parts[0].substring(0, 7),
|
|
1358
|
+
author: parts[1],
|
|
1359
|
+
email: parts[2],
|
|
1360
|
+
date: parts[3],
|
|
1361
|
+
message: parts.slice(4).join("|"),
|
|
1362
|
+
// message may contain |
|
|
1363
|
+
files: [],
|
|
1364
|
+
hasFiles: false
|
|
1365
|
+
};
|
|
1250
1366
|
}
|
|
1251
1367
|
if (current) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
shortHash: current.shortHash,
|
|
1255
|
-
author: current.author,
|
|
1256
|
-
email: current.email,
|
|
1257
|
-
date: current.date,
|
|
1258
|
-
message: current.message,
|
|
1259
|
-
files: current.files
|
|
1260
|
-
});
|
|
1368
|
+
current.files.push(trimmed);
|
|
1369
|
+
current.hasFiles = true;
|
|
1261
1370
|
}
|
|
1262
|
-
return
|
|
1371
|
+
return current;
|
|
1263
1372
|
}
|
|
1264
1373
|
computeCoChanges(commits) {
|
|
1265
1374
|
const pairCounts = /* @__PURE__ */ new Map();
|
|
@@ -1403,50 +1512,25 @@ var KnowledgeIngestor = class {
|
|
|
1403
1512
|
try {
|
|
1404
1513
|
const content = await fs2.readFile(filePath, "utf-8");
|
|
1405
1514
|
const filename = path3.basename(filePath, ".md");
|
|
1406
|
-
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1407
|
-
const title = titleMatch ? titleMatch[1].trim() : filename;
|
|
1408
|
-
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
1409
|
-
const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
|
|
1410
|
-
const date = dateMatch ? dateMatch[1].trim() : void 0;
|
|
1411
|
-
const status = statusMatch ? statusMatch[1].trim() : void 0;
|
|
1412
1515
|
const nodeId = `adr:${filename}`;
|
|
1413
|
-
this.store.addNode(
|
|
1414
|
-
id: nodeId,
|
|
1415
|
-
type: "adr",
|
|
1416
|
-
name: title,
|
|
1417
|
-
path: filePath,
|
|
1418
|
-
metadata: { date, status }
|
|
1419
|
-
});
|
|
1516
|
+
this.store.addNode(parseADRNode(nodeId, filePath, filename, content));
|
|
1420
1517
|
nodesAdded++;
|
|
1421
1518
|
edgesAdded += this.linkToCode(content, nodeId, "documents");
|
|
1422
1519
|
} catch (err) {
|
|
1423
1520
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1424
1521
|
}
|
|
1425
1522
|
}
|
|
1426
|
-
return
|
|
1427
|
-
nodesAdded,
|
|
1428
|
-
nodesUpdated: 0,
|
|
1429
|
-
edgesAdded,
|
|
1430
|
-
edgesUpdated: 0,
|
|
1431
|
-
errors,
|
|
1432
|
-
durationMs: Date.now() - start
|
|
1433
|
-
};
|
|
1523
|
+
return buildResult(nodesAdded, edgesAdded, errors, start);
|
|
1434
1524
|
}
|
|
1435
1525
|
async ingestLearnings(projectPath) {
|
|
1436
1526
|
const start = Date.now();
|
|
1437
1527
|
const filePath = path3.join(projectPath, ".harness", "learnings.md");
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
content = await fs2.readFile(filePath, "utf-8");
|
|
1441
|
-
} catch {
|
|
1442
|
-
return emptyResult(Date.now() - start);
|
|
1443
|
-
}
|
|
1444
|
-
const errors = [];
|
|
1528
|
+
const content = await readFileOrEmpty(filePath);
|
|
1529
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
1445
1530
|
let nodesAdded = 0;
|
|
1446
1531
|
let edgesAdded = 0;
|
|
1447
|
-
const lines = content.split("\n");
|
|
1448
1532
|
let currentDate;
|
|
1449
|
-
for (const line of
|
|
1533
|
+
for (const line of content.split("\n")) {
|
|
1450
1534
|
const headingMatch = line.match(/^##\s+(\S+)/);
|
|
1451
1535
|
if (headingMatch) {
|
|
1452
1536
|
currentDate = headingMatch[1];
|
|
@@ -1455,70 +1539,29 @@ var KnowledgeIngestor = class {
|
|
|
1455
1539
|
const bulletMatch = line.match(/^-\s+(.+)/);
|
|
1456
1540
|
if (!bulletMatch) continue;
|
|
1457
1541
|
const text = bulletMatch[1];
|
|
1458
|
-
const skillMatch = text.match(/\[skill:([^\]]+)\]/);
|
|
1459
|
-
const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
|
|
1460
|
-
const skill = skillMatch ? skillMatch[1] : void 0;
|
|
1461
|
-
const outcome = outcomeMatch ? outcomeMatch[1] : void 0;
|
|
1462
1542
|
const nodeId = `learning:${hash(text)}`;
|
|
1463
|
-
this.store.addNode(
|
|
1464
|
-
id: nodeId,
|
|
1465
|
-
type: "learning",
|
|
1466
|
-
name: text,
|
|
1467
|
-
metadata: { skill, outcome, date: currentDate }
|
|
1468
|
-
});
|
|
1543
|
+
this.store.addNode(parseLearningNode(nodeId, text, currentDate));
|
|
1469
1544
|
nodesAdded++;
|
|
1470
1545
|
edgesAdded += this.linkToCode(text, nodeId, "applies_to");
|
|
1471
1546
|
}
|
|
1472
|
-
return
|
|
1473
|
-
nodesAdded,
|
|
1474
|
-
nodesUpdated: 0,
|
|
1475
|
-
edgesAdded,
|
|
1476
|
-
edgesUpdated: 0,
|
|
1477
|
-
errors,
|
|
1478
|
-
durationMs: Date.now() - start
|
|
1479
|
-
};
|
|
1547
|
+
return buildResult(nodesAdded, edgesAdded, [], start);
|
|
1480
1548
|
}
|
|
1481
1549
|
async ingestFailures(projectPath) {
|
|
1482
1550
|
const start = Date.now();
|
|
1483
1551
|
const filePath = path3.join(projectPath, ".harness", "failures.md");
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
content = await fs2.readFile(filePath, "utf-8");
|
|
1487
|
-
} catch {
|
|
1488
|
-
return emptyResult(Date.now() - start);
|
|
1489
|
-
}
|
|
1490
|
-
const errors = [];
|
|
1552
|
+
const content = await readFileOrEmpty(filePath);
|
|
1553
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
1491
1554
|
let nodesAdded = 0;
|
|
1492
1555
|
let edgesAdded = 0;
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
|
|
1499
|
-
const date = dateMatch ? dateMatch[1].trim() : void 0;
|
|
1500
|
-
const skill = skillMatch ? skillMatch[1].trim() : void 0;
|
|
1501
|
-
const failureType = typeMatch ? typeMatch[1].trim() : void 0;
|
|
1502
|
-
const description = descMatch ? descMatch[1].trim() : void 0;
|
|
1503
|
-
if (!description) continue;
|
|
1504
|
-
const nodeId = `failure:${hash(description)}`;
|
|
1505
|
-
this.store.addNode({
|
|
1506
|
-
id: nodeId,
|
|
1507
|
-
type: "failure",
|
|
1508
|
-
name: description,
|
|
1509
|
-
metadata: { date, skill, type: failureType }
|
|
1510
|
-
});
|
|
1556
|
+
for (const section of content.split(/^##\s+/m).filter((s) => s.trim())) {
|
|
1557
|
+
const parsed = parseFailureSection(section);
|
|
1558
|
+
if (!parsed) continue;
|
|
1559
|
+
const { description, node } = parsed;
|
|
1560
|
+
this.store.addNode(node);
|
|
1511
1561
|
nodesAdded++;
|
|
1512
|
-
edgesAdded += this.linkToCode(description,
|
|
1562
|
+
edgesAdded += this.linkToCode(description, node.id, "caused_by");
|
|
1513
1563
|
}
|
|
1514
|
-
return
|
|
1515
|
-
nodesAdded,
|
|
1516
|
-
nodesUpdated: 0,
|
|
1517
|
-
edgesAdded,
|
|
1518
|
-
edgesUpdated: 0,
|
|
1519
|
-
errors,
|
|
1520
|
-
durationMs: Date.now() - start
|
|
1521
|
-
};
|
|
1564
|
+
return buildResult(nodesAdded, edgesAdded, [], start);
|
|
1522
1565
|
}
|
|
1523
1566
|
async ingestAll(projectPath, opts) {
|
|
1524
1567
|
const start = Date.now();
|
|
@@ -1572,6 +1615,74 @@ var KnowledgeIngestor = class {
|
|
|
1572
1615
|
return results;
|
|
1573
1616
|
}
|
|
1574
1617
|
};
|
|
1618
|
+
async function readFileOrEmpty(filePath) {
|
|
1619
|
+
try {
|
|
1620
|
+
return await fs2.readFile(filePath, "utf-8");
|
|
1621
|
+
} catch {
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
function buildResult(nodesAdded, edgesAdded, errors, start) {
|
|
1626
|
+
return {
|
|
1627
|
+
nodesAdded,
|
|
1628
|
+
nodesUpdated: 0,
|
|
1629
|
+
edgesAdded,
|
|
1630
|
+
edgesUpdated: 0,
|
|
1631
|
+
errors,
|
|
1632
|
+
durationMs: Date.now() - start
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function parseADRNode(nodeId, filePath, filename, content) {
|
|
1636
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1637
|
+
const title = titleMatch ? titleMatch[1].trim() : filename;
|
|
1638
|
+
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
1639
|
+
const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/);
|
|
1640
|
+
return {
|
|
1641
|
+
id: nodeId,
|
|
1642
|
+
type: "adr",
|
|
1643
|
+
name: title,
|
|
1644
|
+
path: filePath,
|
|
1645
|
+
metadata: {
|
|
1646
|
+
date: dateMatch ? dateMatch[1].trim() : void 0,
|
|
1647
|
+
status: statusMatch ? statusMatch[1].trim() : void 0
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
function parseLearningNode(nodeId, text, currentDate) {
|
|
1652
|
+
const skillMatch = text.match(/\[skill:([^\]]+)\]/);
|
|
1653
|
+
const outcomeMatch = text.match(/\[outcome:([^\]]+)\]/);
|
|
1654
|
+
return {
|
|
1655
|
+
id: nodeId,
|
|
1656
|
+
type: "learning",
|
|
1657
|
+
name: text,
|
|
1658
|
+
metadata: {
|
|
1659
|
+
skill: skillMatch ? skillMatch[1] : void 0,
|
|
1660
|
+
outcome: outcomeMatch ? outcomeMatch[1] : void 0,
|
|
1661
|
+
date: currentDate
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
function parseFailureSection(section) {
|
|
1666
|
+
const descMatch = section.match(/\*\*Description:\*\*\s*(.+)/);
|
|
1667
|
+
const description = descMatch ? descMatch[1].trim() : void 0;
|
|
1668
|
+
if (!description) return null;
|
|
1669
|
+
const dateMatch = section.match(/\*\*Date:\*\*\s*(.+)/);
|
|
1670
|
+
const skillMatch = section.match(/\*\*Skill:\*\*\s*(.+)/);
|
|
1671
|
+
const typeMatch = section.match(/\*\*Type:\*\*\s*(.+)/);
|
|
1672
|
+
return {
|
|
1673
|
+
description,
|
|
1674
|
+
node: {
|
|
1675
|
+
id: `failure:${hash(description)}`,
|
|
1676
|
+
type: "failure",
|
|
1677
|
+
name: description,
|
|
1678
|
+
metadata: {
|
|
1679
|
+
date: dateMatch ? dateMatch[1].trim() : void 0,
|
|
1680
|
+
skill: skillMatch ? skillMatch[1].trim() : void 0,
|
|
1681
|
+
type: typeMatch ? typeMatch[1].trim() : void 0
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1575
1686
|
|
|
1576
1687
|
// src/ingest/RequirementIngestor.ts
|
|
1577
1688
|
var fs3 = __toESM(require("fs/promises"));
|
|
@@ -1616,40 +1727,9 @@ var RequirementIngestor = class {
|
|
|
1616
1727
|
return emptyResult(Date.now() - start);
|
|
1617
1728
|
}
|
|
1618
1729
|
for (const featureDir of featureDirs) {
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
try {
|
|
1623
|
-
content = await fs3.readFile(specPath, "utf-8");
|
|
1624
|
-
} catch {
|
|
1625
|
-
continue;
|
|
1626
|
-
}
|
|
1627
|
-
try {
|
|
1628
|
-
const specHash = hash(specPath);
|
|
1629
|
-
const specNodeId = `file:${specPath}`;
|
|
1630
|
-
this.store.addNode({
|
|
1631
|
-
id: specNodeId,
|
|
1632
|
-
type: "document",
|
|
1633
|
-
name: path4.basename(specPath),
|
|
1634
|
-
path: specPath,
|
|
1635
|
-
metadata: { featureName }
|
|
1636
|
-
});
|
|
1637
|
-
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1638
|
-
for (const req of requirements) {
|
|
1639
|
-
this.store.addNode(req.node);
|
|
1640
|
-
nodesAdded++;
|
|
1641
|
-
this.store.addEdge({
|
|
1642
|
-
from: req.node.id,
|
|
1643
|
-
to: specNodeId,
|
|
1644
|
-
type: "specifies"
|
|
1645
|
-
});
|
|
1646
|
-
edgesAdded++;
|
|
1647
|
-
edgesAdded += this.linkByPathPattern(req.node.id, featureName);
|
|
1648
|
-
edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
|
|
1649
|
-
}
|
|
1650
|
-
} catch (err) {
|
|
1651
|
-
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1652
|
-
}
|
|
1730
|
+
const counts = await this.ingestFeatureDir(featureDir, errors);
|
|
1731
|
+
nodesAdded += counts.nodesAdded;
|
|
1732
|
+
edgesAdded += counts.edgesAdded;
|
|
1653
1733
|
}
|
|
1654
1734
|
return {
|
|
1655
1735
|
nodesAdded,
|
|
@@ -1660,6 +1740,48 @@ var RequirementIngestor = class {
|
|
|
1660
1740
|
durationMs: Date.now() - start
|
|
1661
1741
|
};
|
|
1662
1742
|
}
|
|
1743
|
+
async ingestFeatureDir(featureDir, errors) {
|
|
1744
|
+
const featureName = path4.basename(featureDir);
|
|
1745
|
+
const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1746
|
+
let content;
|
|
1747
|
+
try {
|
|
1748
|
+
content = await fs3.readFile(specPath, "utf-8");
|
|
1749
|
+
} catch {
|
|
1750
|
+
return { nodesAdded: 0, edgesAdded: 0 };
|
|
1751
|
+
}
|
|
1752
|
+
try {
|
|
1753
|
+
return this.ingestSpec(specPath, content, featureName);
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1756
|
+
return { nodesAdded: 0, edgesAdded: 0 };
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
ingestSpec(specPath, content, featureName) {
|
|
1760
|
+
const specHash = hash(specPath);
|
|
1761
|
+
const specNodeId = `file:${specPath}`;
|
|
1762
|
+
this.store.addNode({
|
|
1763
|
+
id: specNodeId,
|
|
1764
|
+
type: "document",
|
|
1765
|
+
name: path4.basename(specPath),
|
|
1766
|
+
path: specPath,
|
|
1767
|
+
metadata: { featureName }
|
|
1768
|
+
});
|
|
1769
|
+
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1770
|
+
let nodesAdded = 0;
|
|
1771
|
+
let edgesAdded = 0;
|
|
1772
|
+
for (const req of requirements) {
|
|
1773
|
+
const counts = this.ingestRequirement(req.node, specNodeId, featureName);
|
|
1774
|
+
nodesAdded += counts.nodesAdded;
|
|
1775
|
+
edgesAdded += counts.edgesAdded;
|
|
1776
|
+
}
|
|
1777
|
+
return { nodesAdded, edgesAdded };
|
|
1778
|
+
}
|
|
1779
|
+
ingestRequirement(node, specNodeId, featureName) {
|
|
1780
|
+
this.store.addNode(node);
|
|
1781
|
+
this.store.addEdge({ from: node.id, to: specNodeId, type: "specifies" });
|
|
1782
|
+
const edgesAdded = 1 + this.linkByPathPattern(node.id, featureName) + this.linkByKeywordOverlap(node.id, node.name);
|
|
1783
|
+
return { nodesAdded: 1, edgesAdded };
|
|
1784
|
+
}
|
|
1663
1785
|
/**
|
|
1664
1786
|
* Parse markdown content and extract numbered items from recognized sections.
|
|
1665
1787
|
*/
|
|
@@ -1671,54 +1793,80 @@ var RequirementIngestor = class {
|
|
|
1671
1793
|
let globalIndex = 0;
|
|
1672
1794
|
for (let i = 0; i < lines.length; i++) {
|
|
1673
1795
|
const line = lines[i];
|
|
1674
|
-
const
|
|
1675
|
-
if (
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
);
|
|
1680
|
-
if (isReqSection) {
|
|
1681
|
-
currentSection = heading;
|
|
1682
|
-
inRequirementSection = true;
|
|
1683
|
-
} else {
|
|
1684
|
-
inRequirementSection = false;
|
|
1796
|
+
const sectionResult = this.processHeadingLine(line, inRequirementSection);
|
|
1797
|
+
if (sectionResult !== null) {
|
|
1798
|
+
inRequirementSection = sectionResult.inRequirementSection;
|
|
1799
|
+
if (sectionResult.currentSection !== void 0) {
|
|
1800
|
+
currentSection = sectionResult.currentSection;
|
|
1685
1801
|
}
|
|
1686
1802
|
continue;
|
|
1687
1803
|
}
|
|
1688
1804
|
if (!inRequirementSection) continue;
|
|
1689
1805
|
const itemMatch = line.match(NUMBERED_ITEM_RE);
|
|
1690
1806
|
if (!itemMatch) continue;
|
|
1691
|
-
const index = parseInt(itemMatch[1], 10);
|
|
1692
|
-
const text = itemMatch[2].trim();
|
|
1693
|
-
const rawText = line.trim();
|
|
1694
|
-
const lineNumber = i + 1;
|
|
1695
1807
|
globalIndex++;
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
},
|
|
1709
|
-
metadata: {
|
|
1710
|
-
specPath,
|
|
1711
|
-
index,
|
|
1712
|
-
section: currentSection,
|
|
1713
|
-
rawText,
|
|
1714
|
-
earsPattern,
|
|
1715
|
-
featureName
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
});
|
|
1808
|
+
results.push(
|
|
1809
|
+
this.buildRequirementNode(
|
|
1810
|
+
line,
|
|
1811
|
+
itemMatch,
|
|
1812
|
+
i + 1,
|
|
1813
|
+
specPath,
|
|
1814
|
+
specHash,
|
|
1815
|
+
globalIndex,
|
|
1816
|
+
featureName,
|
|
1817
|
+
currentSection
|
|
1818
|
+
)
|
|
1819
|
+
);
|
|
1719
1820
|
}
|
|
1720
1821
|
return results;
|
|
1721
1822
|
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Check if a line is a section heading and return updated section state,
|
|
1825
|
+
* or return null if the line is not a heading.
|
|
1826
|
+
*/
|
|
1827
|
+
processHeadingLine(line, _inRequirementSection) {
|
|
1828
|
+
const headingMatch = line.match(SECTION_HEADING_RE);
|
|
1829
|
+
if (!headingMatch) return null;
|
|
1830
|
+
const heading = headingMatch[1].trim();
|
|
1831
|
+
const isReqSection = REQUIREMENT_SECTIONS.some(
|
|
1832
|
+
(s) => heading.toLowerCase() === s.toLowerCase()
|
|
1833
|
+
);
|
|
1834
|
+
if (isReqSection) {
|
|
1835
|
+
return { inRequirementSection: true, currentSection: heading };
|
|
1836
|
+
}
|
|
1837
|
+
return { inRequirementSection: false };
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Build a requirement GraphNode from a matched numbered-item line.
|
|
1841
|
+
*/
|
|
1842
|
+
buildRequirementNode(line, itemMatch, lineNumber, specPath, specHash, globalIndex, featureName, currentSection) {
|
|
1843
|
+
const index = parseInt(itemMatch[1], 10);
|
|
1844
|
+
const text = itemMatch[2].trim();
|
|
1845
|
+
const rawText = line.trim();
|
|
1846
|
+
const nodeId = `req:${specHash}:${globalIndex}`;
|
|
1847
|
+
const earsPattern = detectEarsPattern(text);
|
|
1848
|
+
return {
|
|
1849
|
+
node: {
|
|
1850
|
+
id: nodeId,
|
|
1851
|
+
type: "requirement",
|
|
1852
|
+
name: text,
|
|
1853
|
+
path: specPath,
|
|
1854
|
+
location: {
|
|
1855
|
+
fileId: `file:${specPath}`,
|
|
1856
|
+
startLine: lineNumber,
|
|
1857
|
+
endLine: lineNumber
|
|
1858
|
+
},
|
|
1859
|
+
metadata: {
|
|
1860
|
+
specPath,
|
|
1861
|
+
index,
|
|
1862
|
+
section: currentSection,
|
|
1863
|
+
rawText,
|
|
1864
|
+
earsPattern,
|
|
1865
|
+
featureName
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1722
1870
|
/**
|
|
1723
1871
|
* Convention-based linking: match requirement to code/test files
|
|
1724
1872
|
* by feature name in their path.
|
|
@@ -1922,15 +2070,18 @@ function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
|
|
|
1922
2070
|
durationMs: Date.now() - start
|
|
1923
2071
|
};
|
|
1924
2072
|
}
|
|
2073
|
+
function appendJqlClause(jql, clause) {
|
|
2074
|
+
return jql ? `${jql} AND ${clause}` : clause;
|
|
2075
|
+
}
|
|
1925
2076
|
function buildJql(config) {
|
|
1926
2077
|
const project2 = config.project;
|
|
1927
2078
|
let jql = project2 ? `project=${project2}` : "";
|
|
1928
2079
|
const filters = config.filters;
|
|
1929
2080
|
if (filters?.status?.length) {
|
|
1930
|
-
jql
|
|
2081
|
+
jql = appendJqlClause(jql, `status IN (${filters.status.map((s) => `"${s}"`).join(",")})`);
|
|
1931
2082
|
}
|
|
1932
2083
|
if (filters?.labels?.length) {
|
|
1933
|
-
jql
|
|
2084
|
+
jql = appendJqlClause(jql, `labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`);
|
|
1934
2085
|
}
|
|
1935
2086
|
return jql;
|
|
1936
2087
|
}
|
|
@@ -1943,8 +2094,6 @@ var JiraConnector = class {
|
|
|
1943
2094
|
}
|
|
1944
2095
|
async ingest(store, config) {
|
|
1945
2096
|
const start = Date.now();
|
|
1946
|
-
let nodesAdded = 0;
|
|
1947
|
-
let edgesAdded = 0;
|
|
1948
2097
|
const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
|
|
1949
2098
|
const apiKey = process.env[apiKeyEnv];
|
|
1950
2099
|
if (!apiKey) {
|
|
@@ -1966,38 +2115,39 @@ var JiraConnector = class {
|
|
|
1966
2115
|
);
|
|
1967
2116
|
}
|
|
1968
2117
|
const jql = buildJql(config);
|
|
1969
|
-
const headers = {
|
|
1970
|
-
Authorization: `Basic ${apiKey}`,
|
|
1971
|
-
"Content-Type": "application/json"
|
|
1972
|
-
};
|
|
2118
|
+
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
1973
2119
|
try {
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
let total = Infinity;
|
|
1977
|
-
while (startAt < total) {
|
|
1978
|
-
const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
|
|
1979
|
-
const response = await this.httpClient(url, { headers });
|
|
1980
|
-
if (!response.ok) {
|
|
1981
|
-
return buildIngestResult(nodesAdded, edgesAdded, ["Jira API request failed"], start);
|
|
1982
|
-
}
|
|
1983
|
-
const data = await response.json();
|
|
1984
|
-
total = data.total;
|
|
1985
|
-
for (const issue of data.issues) {
|
|
1986
|
-
const counts = this.processIssue(store, issue);
|
|
1987
|
-
nodesAdded += counts.nodesAdded;
|
|
1988
|
-
edgesAdded += counts.edgesAdded;
|
|
1989
|
-
}
|
|
1990
|
-
startAt += maxResults;
|
|
1991
|
-
}
|
|
2120
|
+
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
|
|
2121
|
+
return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
|
|
1992
2122
|
} catch (err) {
|
|
1993
2123
|
return buildIngestResult(
|
|
1994
|
-
|
|
1995
|
-
|
|
2124
|
+
0,
|
|
2125
|
+
0,
|
|
1996
2126
|
[`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
|
|
1997
2127
|
start
|
|
1998
2128
|
);
|
|
1999
2129
|
}
|
|
2000
|
-
|
|
2130
|
+
}
|
|
2131
|
+
async fetchAllIssues(store, baseUrl, jql, headers) {
|
|
2132
|
+
let nodesAdded = 0;
|
|
2133
|
+
let edgesAdded = 0;
|
|
2134
|
+
let startAt = 0;
|
|
2135
|
+
const maxResults = 50;
|
|
2136
|
+
let total = Infinity;
|
|
2137
|
+
while (startAt < total) {
|
|
2138
|
+
const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
|
|
2139
|
+
const response = await this.httpClient(url, { headers });
|
|
2140
|
+
if (!response.ok) throw new Error("Jira API request failed");
|
|
2141
|
+
const data = await response.json();
|
|
2142
|
+
total = data.total;
|
|
2143
|
+
for (const issue of data.issues) {
|
|
2144
|
+
const counts = this.processIssue(store, issue);
|
|
2145
|
+
nodesAdded += counts.nodesAdded;
|
|
2146
|
+
edgesAdded += counts.edgesAdded;
|
|
2147
|
+
}
|
|
2148
|
+
startAt += maxResults;
|
|
2149
|
+
}
|
|
2150
|
+
return { nodesAdded, edgesAdded };
|
|
2001
2151
|
}
|
|
2002
2152
|
processIssue(store, issue) {
|
|
2003
2153
|
const nodeId = `issue:jira:${issue.key}`;
|
|
@@ -2118,6 +2268,16 @@ var SlackConnector = class {
|
|
|
2118
2268
|
};
|
|
2119
2269
|
|
|
2120
2270
|
// src/ingest/connectors/ConfluenceConnector.ts
|
|
2271
|
+
function missingApiKeyResult(envVar, start) {
|
|
2272
|
+
return {
|
|
2273
|
+
nodesAdded: 0,
|
|
2274
|
+
nodesUpdated: 0,
|
|
2275
|
+
edgesAdded: 0,
|
|
2276
|
+
edgesUpdated: 0,
|
|
2277
|
+
errors: [`Missing API key: environment variable "${envVar}" is not set`],
|
|
2278
|
+
durationMs: Date.now() - start
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2121
2281
|
var ConfluenceConnector = class {
|
|
2122
2282
|
name = "confluence";
|
|
2123
2283
|
source = "confluence";
|
|
@@ -2128,40 +2288,34 @@ var ConfluenceConnector = class {
|
|
|
2128
2288
|
async ingest(store, config) {
|
|
2129
2289
|
const start = Date.now();
|
|
2130
2290
|
const errors = [];
|
|
2131
|
-
let nodesAdded = 0;
|
|
2132
|
-
let edgesAdded = 0;
|
|
2133
2291
|
const apiKeyEnv = config.apiKeyEnv ?? "CONFLUENCE_API_KEY";
|
|
2134
2292
|
const apiKey = process.env[apiKeyEnv];
|
|
2135
2293
|
if (!apiKey) {
|
|
2136
|
-
return
|
|
2137
|
-
nodesAdded: 0,
|
|
2138
|
-
nodesUpdated: 0,
|
|
2139
|
-
edgesAdded: 0,
|
|
2140
|
-
edgesUpdated: 0,
|
|
2141
|
-
errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2142
|
-
durationMs: Date.now() - start
|
|
2143
|
-
};
|
|
2294
|
+
return missingApiKeyResult(apiKeyEnv, start);
|
|
2144
2295
|
}
|
|
2145
2296
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
2146
2297
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
2147
2298
|
const spaceKey = config.spaceKey ?? "";
|
|
2148
|
-
|
|
2149
|
-
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
2150
|
-
nodesAdded = result.nodesAdded;
|
|
2151
|
-
edgesAdded = result.edgesAdded;
|
|
2152
|
-
errors.push(...result.errors);
|
|
2153
|
-
} catch (err) {
|
|
2154
|
-
errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2155
|
-
}
|
|
2299
|
+
const counts = await this.fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors);
|
|
2156
2300
|
return {
|
|
2157
|
-
nodesAdded,
|
|
2301
|
+
nodesAdded: counts.nodesAdded,
|
|
2158
2302
|
nodesUpdated: 0,
|
|
2159
|
-
edgesAdded,
|
|
2303
|
+
edgesAdded: counts.edgesAdded,
|
|
2160
2304
|
edgesUpdated: 0,
|
|
2161
2305
|
errors,
|
|
2162
2306
|
durationMs: Date.now() - start
|
|
2163
2307
|
};
|
|
2164
2308
|
}
|
|
2309
|
+
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
|
|
2310
|
+
try {
|
|
2311
|
+
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
2312
|
+
errors.push(...result.errors);
|
|
2313
|
+
return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
|
|
2314
|
+
} catch (err) {
|
|
2315
|
+
errors.push(`Confluence fetch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2316
|
+
return { nodesAdded: 0, edgesAdded: 0 };
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2165
2319
|
async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
|
|
2166
2320
|
const errors = [];
|
|
2167
2321
|
let nodesAdded = 0;
|
|
@@ -2206,6 +2360,61 @@ var ConfluenceConnector = class {
|
|
|
2206
2360
|
};
|
|
2207
2361
|
|
|
2208
2362
|
// src/ingest/connectors/CIConnector.ts
|
|
2363
|
+
function emptyResult2(errors, start) {
|
|
2364
|
+
return {
|
|
2365
|
+
nodesAdded: 0,
|
|
2366
|
+
nodesUpdated: 0,
|
|
2367
|
+
edgesAdded: 0,
|
|
2368
|
+
edgesUpdated: 0,
|
|
2369
|
+
errors,
|
|
2370
|
+
durationMs: Date.now() - start
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
function ingestRun(store, run) {
|
|
2374
|
+
const buildId = `build:${run.id}`;
|
|
2375
|
+
const safeName = sanitizeExternalText(run.name, 200);
|
|
2376
|
+
let nodesAdded = 0;
|
|
2377
|
+
let edgesAdded = 0;
|
|
2378
|
+
store.addNode({
|
|
2379
|
+
id: buildId,
|
|
2380
|
+
type: "build",
|
|
2381
|
+
name: `${safeName} #${run.id}`,
|
|
2382
|
+
metadata: {
|
|
2383
|
+
source: "github-actions",
|
|
2384
|
+
status: run.status,
|
|
2385
|
+
conclusion: run.conclusion,
|
|
2386
|
+
branch: run.head_branch,
|
|
2387
|
+
sha: run.head_sha,
|
|
2388
|
+
url: run.html_url,
|
|
2389
|
+
createdAt: run.created_at
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2392
|
+
nodesAdded++;
|
|
2393
|
+
const commitNode = store.getNode(`commit:${run.head_sha}`);
|
|
2394
|
+
if (commitNode) {
|
|
2395
|
+
store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
|
|
2396
|
+
edgesAdded++;
|
|
2397
|
+
}
|
|
2398
|
+
if (run.conclusion === "failure") {
|
|
2399
|
+
const testResultId = `test_result:${run.id}`;
|
|
2400
|
+
store.addNode({
|
|
2401
|
+
id: testResultId,
|
|
2402
|
+
type: "test_result",
|
|
2403
|
+
name: `Failed: ${safeName} #${run.id}`,
|
|
2404
|
+
metadata: {
|
|
2405
|
+
source: "github-actions",
|
|
2406
|
+
buildId: String(run.id),
|
|
2407
|
+
conclusion: "failure",
|
|
2408
|
+
branch: run.head_branch,
|
|
2409
|
+
sha: run.head_sha
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
nodesAdded++;
|
|
2413
|
+
store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
|
|
2414
|
+
edgesAdded++;
|
|
2415
|
+
}
|
|
2416
|
+
return { nodesAdded, edgesAdded };
|
|
2417
|
+
}
|
|
2209
2418
|
var CIConnector = class {
|
|
2210
2419
|
name = "ci";
|
|
2211
2420
|
source = "github-actions";
|
|
@@ -2216,22 +2425,29 @@ var CIConnector = class {
|
|
|
2216
2425
|
async ingest(store, config) {
|
|
2217
2426
|
const start = Date.now();
|
|
2218
2427
|
const errors = [];
|
|
2219
|
-
let nodesAdded = 0;
|
|
2220
|
-
let edgesAdded = 0;
|
|
2221
2428
|
const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
|
|
2222
2429
|
const apiKey = process.env[apiKeyEnv];
|
|
2223
2430
|
if (!apiKey) {
|
|
2224
|
-
return
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
edgesUpdated: 0,
|
|
2229
|
-
errors: [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2230
|
-
durationMs: Date.now() - start
|
|
2231
|
-
};
|
|
2431
|
+
return emptyResult2(
|
|
2432
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
2433
|
+
start
|
|
2434
|
+
);
|
|
2232
2435
|
}
|
|
2233
2436
|
const repo = config.repo ?? "";
|
|
2234
2437
|
const maxRuns = config.maxRuns ?? 10;
|
|
2438
|
+
const counts = await this.fetchAndIngestRuns(store, repo, maxRuns, apiKey, errors);
|
|
2439
|
+
return {
|
|
2440
|
+
nodesAdded: counts.nodesAdded,
|
|
2441
|
+
nodesUpdated: 0,
|
|
2442
|
+
edgesAdded: counts.edgesAdded,
|
|
2443
|
+
edgesUpdated: 0,
|
|
2444
|
+
errors,
|
|
2445
|
+
durationMs: Date.now() - start
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
async fetchAndIngestRuns(store, repo, maxRuns, apiKey, errors) {
|
|
2449
|
+
let nodesAdded = 0;
|
|
2450
|
+
let edgesAdded = 0;
|
|
2235
2451
|
try {
|
|
2236
2452
|
const url = `https://api.github.com/repos/${repo}/actions/runs?per_page=${maxRuns}`;
|
|
2237
2453
|
const response = await this.httpClient(url, {
|
|
@@ -2239,71 +2455,20 @@ var CIConnector = class {
|
|
|
2239
2455
|
});
|
|
2240
2456
|
if (!response.ok) {
|
|
2241
2457
|
errors.push(`GitHub Actions API error: status ${response.status}`);
|
|
2242
|
-
return {
|
|
2243
|
-
nodesAdded: 0,
|
|
2244
|
-
nodesUpdated: 0,
|
|
2245
|
-
edgesAdded: 0,
|
|
2246
|
-
edgesUpdated: 0,
|
|
2247
|
-
errors,
|
|
2248
|
-
durationMs: Date.now() - start
|
|
2249
|
-
};
|
|
2458
|
+
return { nodesAdded, edgesAdded };
|
|
2250
2459
|
}
|
|
2251
2460
|
const data = await response.json();
|
|
2252
2461
|
for (const run of data.workflow_runs) {
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
branch: run.head_branch,
|
|
2264
|
-
sha: run.head_sha,
|
|
2265
|
-
url: run.html_url,
|
|
2266
|
-
createdAt: run.created_at
|
|
2267
|
-
}
|
|
2268
|
-
});
|
|
2269
|
-
nodesAdded++;
|
|
2270
|
-
const commitNode = store.getNode(`commit:${run.head_sha}`);
|
|
2271
|
-
if (commitNode) {
|
|
2272
|
-
store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
|
|
2273
|
-
edgesAdded++;
|
|
2274
|
-
}
|
|
2275
|
-
if (run.conclusion === "failure") {
|
|
2276
|
-
const testResultId = `test_result:${run.id}`;
|
|
2277
|
-
store.addNode({
|
|
2278
|
-
id: testResultId,
|
|
2279
|
-
type: "test_result",
|
|
2280
|
-
name: `Failed: ${safeName} #${run.id}`,
|
|
2281
|
-
metadata: {
|
|
2282
|
-
source: "github-actions",
|
|
2283
|
-
buildId: String(run.id),
|
|
2284
|
-
conclusion: "failure",
|
|
2285
|
-
branch: run.head_branch,
|
|
2286
|
-
sha: run.head_sha
|
|
2287
|
-
}
|
|
2288
|
-
});
|
|
2289
|
-
nodesAdded++;
|
|
2290
|
-
store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
|
|
2291
|
-
edgesAdded++;
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
} catch (err) {
|
|
2295
|
-
errors.push(
|
|
2296
|
-
`GitHub Actions fetch error: ${err instanceof Error ? err.message : String(err)}`
|
|
2297
|
-
);
|
|
2298
|
-
}
|
|
2299
|
-
return {
|
|
2300
|
-
nodesAdded,
|
|
2301
|
-
nodesUpdated: 0,
|
|
2302
|
-
edgesAdded,
|
|
2303
|
-
edgesUpdated: 0,
|
|
2304
|
-
errors,
|
|
2305
|
-
durationMs: Date.now() - start
|
|
2306
|
-
};
|
|
2462
|
+
const counts = ingestRun(store, run);
|
|
2463
|
+
nodesAdded += counts.nodesAdded;
|
|
2464
|
+
edgesAdded += counts.edgesAdded;
|
|
2465
|
+
}
|
|
2466
|
+
} catch (err) {
|
|
2467
|
+
errors.push(
|
|
2468
|
+
`GitHub Actions fetch error: ${err instanceof Error ? err.message : String(err)}`
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
return { nodesAdded, edgesAdded };
|
|
2307
2472
|
}
|
|
2308
2473
|
};
|
|
2309
2474
|
|
|
@@ -2373,16 +2538,29 @@ var FusionLayer = class {
|
|
|
2373
2538
|
return [];
|
|
2374
2539
|
}
|
|
2375
2540
|
const allNodes = this.store.findNodes({});
|
|
2541
|
+
const semanticScores = this.buildSemanticScores(queryEmbedding, allNodes.length);
|
|
2542
|
+
const { kwWeight, semWeight } = this.resolveWeights(semanticScores.size > 0);
|
|
2543
|
+
const results = this.scoreNodes(allNodes, keywords, semanticScores, kwWeight, semWeight);
|
|
2544
|
+
results.sort((a, b) => b.score - a.score);
|
|
2545
|
+
return results.slice(0, topK);
|
|
2546
|
+
}
|
|
2547
|
+
buildSemanticScores(queryEmbedding, nodeCount) {
|
|
2376
2548
|
const semanticScores = /* @__PURE__ */ new Map();
|
|
2377
2549
|
if (queryEmbedding && this.vectorStore) {
|
|
2378
|
-
const vectorResults = this.vectorStore.search(queryEmbedding,
|
|
2550
|
+
const vectorResults = this.vectorStore.search(queryEmbedding, nodeCount);
|
|
2379
2551
|
for (const vr of vectorResults) {
|
|
2380
2552
|
semanticScores.set(vr.id, vr.score);
|
|
2381
2553
|
}
|
|
2382
2554
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2555
|
+
return semanticScores;
|
|
2556
|
+
}
|
|
2557
|
+
resolveWeights(hasSemanticScores) {
|
|
2558
|
+
return {
|
|
2559
|
+
kwWeight: hasSemanticScores ? this.keywordWeight : 1,
|
|
2560
|
+
semWeight: hasSemanticScores ? this.semanticWeight : 0
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
scoreNodes(allNodes, keywords, semanticScores, kwWeight, semWeight) {
|
|
2386
2564
|
const results = [];
|
|
2387
2565
|
for (const node of allNodes) {
|
|
2388
2566
|
const kwScore = this.keywordScore(keywords, node);
|
|
@@ -2393,15 +2571,11 @@ var FusionLayer = class {
|
|
|
2393
2571
|
nodeId: node.id,
|
|
2394
2572
|
node,
|
|
2395
2573
|
score: fusedScore,
|
|
2396
|
-
signals: {
|
|
2397
|
-
keyword: kwScore,
|
|
2398
|
-
semantic: semScore
|
|
2399
|
-
}
|
|
2574
|
+
signals: { keyword: kwScore, semantic: semScore }
|
|
2400
2575
|
});
|
|
2401
2576
|
}
|
|
2402
2577
|
}
|
|
2403
|
-
results
|
|
2404
|
-
return results.slice(0, topK);
|
|
2578
|
+
return results;
|
|
2405
2579
|
}
|
|
2406
2580
|
extractKeywords(query) {
|
|
2407
2581
|
const tokens = query.toLowerCase().split(/[\s\-_.,:;!?()[\]{}"'`/\\|@#$%^&*+=<>~]+/).filter((t) => t.length >= 2).filter((t) => !STOP_WORDS.has(t));
|
|
@@ -2456,37 +2630,50 @@ var GraphEntropyAdapter = class {
|
|
|
2456
2630
|
const missingTargets = [];
|
|
2457
2631
|
let freshEdges = 0;
|
|
2458
2632
|
for (const edge of documentsEdges) {
|
|
2459
|
-
const
|
|
2460
|
-
if (
|
|
2633
|
+
const result = this.classifyDocEdge(edge);
|
|
2634
|
+
if (result.kind === "missing") {
|
|
2461
2635
|
missingTargets.push(edge.to);
|
|
2462
|
-
|
|
2636
|
+
} else if (result.kind === "fresh") {
|
|
2637
|
+
freshEdges++;
|
|
2638
|
+
} else {
|
|
2639
|
+
staleEdges.push(result.entry);
|
|
2463
2640
|
}
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2641
|
+
}
|
|
2642
|
+
return { staleEdges, missingTargets, freshEdges };
|
|
2643
|
+
}
|
|
2644
|
+
classifyDocEdge(edge) {
|
|
2645
|
+
const codeNode = this.store.getNode(edge.to);
|
|
2646
|
+
if (!codeNode) {
|
|
2647
|
+
return { kind: "missing" };
|
|
2648
|
+
}
|
|
2649
|
+
const docNode = this.store.getNode(edge.from);
|
|
2650
|
+
const codeLastModified = codeNode.lastModified;
|
|
2651
|
+
const docLastModified = docNode?.lastModified;
|
|
2652
|
+
if (codeLastModified && docLastModified) {
|
|
2653
|
+
if (codeLastModified > docLastModified) {
|
|
2654
|
+
return {
|
|
2655
|
+
kind: "stale",
|
|
2656
|
+
entry: {
|
|
2470
2657
|
docNodeId: edge.from,
|
|
2471
2658
|
codeNodeId: edge.to,
|
|
2472
2659
|
edgeType: edge.type,
|
|
2473
2660
|
codeLastModified,
|
|
2474
2661
|
docLastModified
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
freshEdges++;
|
|
2478
|
-
}
|
|
2479
|
-
} else {
|
|
2480
|
-
staleEdges.push({
|
|
2481
|
-
docNodeId: edge.from,
|
|
2482
|
-
codeNodeId: edge.to,
|
|
2483
|
-
edgeType: edge.type,
|
|
2484
|
-
codeLastModified,
|
|
2485
|
-
docLastModified
|
|
2486
|
-
});
|
|
2662
|
+
}
|
|
2663
|
+
};
|
|
2487
2664
|
}
|
|
2665
|
+
return { kind: "fresh" };
|
|
2488
2666
|
}
|
|
2489
|
-
return {
|
|
2667
|
+
return {
|
|
2668
|
+
kind: "stale",
|
|
2669
|
+
entry: {
|
|
2670
|
+
docNodeId: edge.from,
|
|
2671
|
+
codeNodeId: edge.to,
|
|
2672
|
+
edgeType: edge.type,
|
|
2673
|
+
codeLastModified,
|
|
2674
|
+
docLastModified
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2490
2677
|
}
|
|
2491
2678
|
/**
|
|
2492
2679
|
* BFS from entry points to find reachable vs unreachable code nodes.
|
|
@@ -2743,36 +2930,12 @@ var GraphAnomalyAdapter = class {
|
|
|
2743
2930
|
store;
|
|
2744
2931
|
detect(options) {
|
|
2745
2932
|
const threshold = options?.threshold != null && options.threshold > 0 ? options.threshold : DEFAULT_THRESHOLD;
|
|
2746
|
-
const
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
if (RECOGNIZED_METRICS.has(m)) {
|
|
2751
|
-
metricsToAnalyze.push(m);
|
|
2752
|
-
} else {
|
|
2753
|
-
warnings.push(m);
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
const allOutliers = [];
|
|
2757
|
-
const analyzedNodeIds = /* @__PURE__ */ new Set();
|
|
2758
|
-
const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
|
|
2759
|
-
const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
|
|
2760
|
-
const needsComplexity = metricsToAnalyze.includes("hotspotScore");
|
|
2761
|
-
const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
|
|
2762
|
-
const cachedHotspotData = needsComplexity ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
|
|
2763
|
-
for (const metric of metricsToAnalyze) {
|
|
2764
|
-
const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
|
|
2765
|
-
for (const e of entries) {
|
|
2766
|
-
analyzedNodeIds.add(e.nodeId);
|
|
2767
|
-
}
|
|
2768
|
-
const outliers = this.computeZScoreOutliers(entries, metric, threshold);
|
|
2769
|
-
allOutliers.push(...outliers);
|
|
2770
|
-
}
|
|
2771
|
-
allOutliers.sort((a, b) => b.zScore - a.zScore);
|
|
2933
|
+
const { metricsToAnalyze, warnings } = this.filterMetrics(
|
|
2934
|
+
options?.metrics ?? [...DEFAULT_METRICS]
|
|
2935
|
+
);
|
|
2936
|
+
const { allOutliers, analyzedNodeIds } = this.computeAllOutliers(metricsToAnalyze, threshold);
|
|
2772
2937
|
const articulationPoints = this.findArticulationPoints();
|
|
2773
|
-
const
|
|
2774
|
-
const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
|
|
2775
|
-
const overlapping = [...outlierNodeIds].filter((id) => apNodeIds.has(id));
|
|
2938
|
+
const overlapping = this.computeOverlap(allOutliers, articulationPoints);
|
|
2776
2939
|
return {
|
|
2777
2940
|
statisticalOutliers: allOutliers,
|
|
2778
2941
|
articulationPoints,
|
|
@@ -2788,6 +2951,38 @@ var GraphAnomalyAdapter = class {
|
|
|
2788
2951
|
}
|
|
2789
2952
|
};
|
|
2790
2953
|
}
|
|
2954
|
+
filterMetrics(requested) {
|
|
2955
|
+
const metricsToAnalyze = [];
|
|
2956
|
+
const warnings = [];
|
|
2957
|
+
for (const m of requested) {
|
|
2958
|
+
if (RECOGNIZED_METRICS.has(m)) {
|
|
2959
|
+
metricsToAnalyze.push(m);
|
|
2960
|
+
} else {
|
|
2961
|
+
warnings.push(m);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
return { metricsToAnalyze, warnings };
|
|
2965
|
+
}
|
|
2966
|
+
computeAllOutliers(metricsToAnalyze, threshold) {
|
|
2967
|
+
const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
|
|
2968
|
+
const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
|
|
2969
|
+
const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
|
|
2970
|
+
const cachedHotspotData = metricsToAnalyze.includes("hotspotScore") ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
|
|
2971
|
+
const allOutliers = [];
|
|
2972
|
+
const analyzedNodeIds = /* @__PURE__ */ new Set();
|
|
2973
|
+
for (const metric of metricsToAnalyze) {
|
|
2974
|
+
const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
|
|
2975
|
+
for (const e of entries) analyzedNodeIds.add(e.nodeId);
|
|
2976
|
+
allOutliers.push(...this.computeZScoreOutliers(entries, metric, threshold));
|
|
2977
|
+
}
|
|
2978
|
+
allOutliers.sort((a, b) => b.zScore - a.zScore);
|
|
2979
|
+
return { allOutliers, analyzedNodeIds };
|
|
2980
|
+
}
|
|
2981
|
+
computeOverlap(outliers, articulationPoints) {
|
|
2982
|
+
const outlierNodeIds = new Set(outliers.map((o) => o.nodeId));
|
|
2983
|
+
const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
|
|
2984
|
+
return [...outlierNodeIds].filter((id) => apNodeIds.has(id));
|
|
2985
|
+
}
|
|
2791
2986
|
collectMetricValues(metric, cachedCouplingData, cachedHotspotData) {
|
|
2792
2987
|
const entries = [];
|
|
2793
2988
|
if (metric === "cyclomaticComplexity") {
|
|
@@ -3343,37 +3538,54 @@ var EntityExtractor = class {
|
|
|
3343
3538
|
result.push(entity);
|
|
3344
3539
|
}
|
|
3345
3540
|
};
|
|
3346
|
-
const quotedConsumed =
|
|
3541
|
+
const quotedConsumed = this.extractQuoted(trimmed, add);
|
|
3542
|
+
const casingConsumed = this.extractCasing(trimmed, quotedConsumed, add);
|
|
3543
|
+
const pathConsumed = this.extractPaths(trimmed, add);
|
|
3544
|
+
this.extractNouns(trimmed, buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed), add);
|
|
3545
|
+
return result;
|
|
3546
|
+
}
|
|
3547
|
+
/** Strategy 1: Quoted strings. Returns the set of consumed tokens. */
|
|
3548
|
+
extractQuoted(trimmed, add) {
|
|
3549
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
3347
3550
|
for (const match of trimmed.matchAll(QUOTED_RE)) {
|
|
3348
3551
|
const inner = match[1].trim();
|
|
3349
3552
|
if (inner.length > 0) {
|
|
3350
3553
|
add(inner);
|
|
3351
|
-
|
|
3554
|
+
consumed.add(inner);
|
|
3352
3555
|
}
|
|
3353
3556
|
}
|
|
3354
|
-
|
|
3557
|
+
return consumed;
|
|
3558
|
+
}
|
|
3559
|
+
/** Strategy 2: PascalCase/camelCase tokens. Returns the set of consumed tokens. */
|
|
3560
|
+
extractCasing(trimmed, quotedConsumed, add) {
|
|
3561
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
3355
3562
|
for (const match of trimmed.matchAll(PASCAL_OR_CAMEL_RE)) {
|
|
3356
3563
|
const token = match[0];
|
|
3357
3564
|
if (!quotedConsumed.has(token)) {
|
|
3358
3565
|
add(token);
|
|
3359
|
-
|
|
3566
|
+
consumed.add(token);
|
|
3360
3567
|
}
|
|
3361
3568
|
}
|
|
3362
|
-
|
|
3569
|
+
return consumed;
|
|
3570
|
+
}
|
|
3571
|
+
/** Strategy 3: File paths. Returns the set of consumed tokens. */
|
|
3572
|
+
extractPaths(trimmed, add) {
|
|
3573
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
3363
3574
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3364
3575
|
const path7 = match[0];
|
|
3365
3576
|
add(path7);
|
|
3366
|
-
|
|
3577
|
+
consumed.add(path7);
|
|
3367
3578
|
}
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3579
|
+
return consumed;
|
|
3580
|
+
}
|
|
3581
|
+
/** Strategy 4: Remaining significant nouns after stop-word and intent-keyword removal. */
|
|
3582
|
+
extractNouns(trimmed, allConsumed, add) {
|
|
3583
|
+
for (const raw of trimmed.split(/\s+/)) {
|
|
3371
3584
|
const cleaned = raw.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "");
|
|
3372
3585
|
if (cleaned.length === 0) continue;
|
|
3373
3586
|
if (isSkippableWord(cleaned, allConsumed)) continue;
|
|
3374
3587
|
add(cleaned);
|
|
3375
3588
|
}
|
|
3376
|
-
return result;
|
|
3377
3589
|
}
|
|
3378
3590
|
};
|
|
3379
3591
|
|
|
@@ -3790,36 +4002,41 @@ var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships"
|
|
|
3790
4002
|
var classifier = new IntentClassifier();
|
|
3791
4003
|
var extractor = new EntityExtractor();
|
|
3792
4004
|
var formatter = new ResponseFormatter();
|
|
4005
|
+
function lowConfidenceResult(intent, confidence) {
|
|
4006
|
+
return {
|
|
4007
|
+
intent,
|
|
4008
|
+
intentConfidence: confidence,
|
|
4009
|
+
entities: [],
|
|
4010
|
+
summary: "I'm not sure what you're asking. Try rephrasing your question.",
|
|
4011
|
+
data: null,
|
|
4012
|
+
suggestions: [
|
|
4013
|
+
'Try "what breaks if I change <name>?" for impact analysis',
|
|
4014
|
+
'Try "where is <name>?" to find entities',
|
|
4015
|
+
'Try "what calls <name>?" for relationships',
|
|
4016
|
+
'Try "what is <name>?" for explanations',
|
|
4017
|
+
'Try "what looks wrong?" for anomaly detection'
|
|
4018
|
+
]
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
function noEntityResult(intent, confidence) {
|
|
4022
|
+
return {
|
|
4023
|
+
intent,
|
|
4024
|
+
intentConfidence: confidence,
|
|
4025
|
+
entities: [],
|
|
4026
|
+
summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
|
|
4027
|
+
data: null
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
3793
4030
|
async function askGraph(store, question) {
|
|
3794
4031
|
const fusion = new FusionLayer(store);
|
|
3795
4032
|
const resolver = new EntityResolver(store, fusion);
|
|
3796
4033
|
const classification = classifier.classify(question);
|
|
3797
4034
|
if (classification.confidence < 0.3) {
|
|
3798
|
-
return
|
|
3799
|
-
intent: classification.intent,
|
|
3800
|
-
intentConfidence: classification.confidence,
|
|
3801
|
-
entities: [],
|
|
3802
|
-
summary: "I'm not sure what you're asking. Try rephrasing your question.",
|
|
3803
|
-
data: null,
|
|
3804
|
-
suggestions: [
|
|
3805
|
-
'Try "what breaks if I change <name>?" for impact analysis',
|
|
3806
|
-
'Try "where is <name>?" to find entities',
|
|
3807
|
-
'Try "what calls <name>?" for relationships',
|
|
3808
|
-
'Try "what is <name>?" for explanations',
|
|
3809
|
-
'Try "what looks wrong?" for anomaly detection'
|
|
3810
|
-
]
|
|
3811
|
-
};
|
|
4035
|
+
return lowConfidenceResult(classification.intent, classification.confidence);
|
|
3812
4036
|
}
|
|
3813
|
-
const
|
|
3814
|
-
const entities = resolver.resolve(rawEntities);
|
|
4037
|
+
const entities = resolver.resolve(extractor.extract(question));
|
|
3815
4038
|
if (ENTITY_REQUIRED_INTENTS.has(classification.intent) && entities.length === 0) {
|
|
3816
|
-
return
|
|
3817
|
-
intent: classification.intent,
|
|
3818
|
-
intentConfidence: classification.confidence,
|
|
3819
|
-
entities: [],
|
|
3820
|
-
summary: "Could not find any matching nodes in the graph for your query. Try using exact class names, function names, or file paths.",
|
|
3821
|
-
data: null
|
|
3822
|
-
};
|
|
4039
|
+
return noEntityResult(classification.intent, classification.confidence);
|
|
3823
4040
|
}
|
|
3824
4041
|
let data;
|
|
3825
4042
|
try {
|
|
@@ -3833,67 +4050,59 @@ async function askGraph(store, question) {
|
|
|
3833
4050
|
data: null
|
|
3834
4051
|
};
|
|
3835
4052
|
}
|
|
3836
|
-
const summary = formatter.format(classification.intent, entities, data, question);
|
|
3837
4053
|
return {
|
|
3838
4054
|
intent: classification.intent,
|
|
3839
4055
|
intentConfidence: classification.confidence,
|
|
3840
4056
|
entities,
|
|
3841
|
-
summary,
|
|
4057
|
+
summary: formatter.format(classification.intent, entities, data, question),
|
|
3842
4058
|
data
|
|
3843
4059
|
};
|
|
3844
4060
|
}
|
|
4061
|
+
function buildContextBlocks(cql, rootIds, searchResults) {
|
|
4062
|
+
return rootIds.map((rootId) => {
|
|
4063
|
+
const expanded = cql.execute({ rootNodeIds: [rootId], maxDepth: 2 });
|
|
4064
|
+
const match = searchResults.find((r) => r.nodeId === rootId);
|
|
4065
|
+
return {
|
|
4066
|
+
rootNode: rootId,
|
|
4067
|
+
score: match?.score ?? 1,
|
|
4068
|
+
nodes: expanded.nodes,
|
|
4069
|
+
edges: expanded.edges
|
|
4070
|
+
};
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
function executeImpact(store, cql, entities, question) {
|
|
4074
|
+
const rootId = entities[0].nodeId;
|
|
4075
|
+
const lower = question.toLowerCase();
|
|
4076
|
+
if (lower.includes("blast radius") || lower.includes("cascade")) {
|
|
4077
|
+
return new CascadeSimulator(store).simulate(rootId);
|
|
4078
|
+
}
|
|
4079
|
+
const result = cql.execute({ rootNodeIds: [rootId], bidirectional: true, maxDepth: 3 });
|
|
4080
|
+
return groupNodesByImpact(result.nodes, rootId);
|
|
4081
|
+
}
|
|
4082
|
+
function executeExplain(cql, entities, question, fusion) {
|
|
4083
|
+
const searchResults = fusion.search(question, 10);
|
|
4084
|
+
const rootIds = entities.length > 0 ? [entities[0].nodeId] : searchResults.slice(0, 3).map((r) => r.nodeId);
|
|
4085
|
+
return { searchResults, context: buildContextBlocks(cql, rootIds, searchResults) };
|
|
4086
|
+
}
|
|
3845
4087
|
function executeOperation(store, intent, entities, question, fusion) {
|
|
3846
4088
|
const cql = new ContextQL(store);
|
|
3847
4089
|
switch (intent) {
|
|
3848
|
-
case "impact":
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
if (lowerQuestion.includes("blast radius") || lowerQuestion.includes("cascade")) {
|
|
3852
|
-
const simulator = new CascadeSimulator(store);
|
|
3853
|
-
return simulator.simulate(rootId);
|
|
3854
|
-
}
|
|
3855
|
-
const result = cql.execute({
|
|
3856
|
-
rootNodeIds: [rootId],
|
|
3857
|
-
bidirectional: true,
|
|
3858
|
-
maxDepth: 3
|
|
3859
|
-
});
|
|
3860
|
-
return groupNodesByImpact(result.nodes, rootId);
|
|
3861
|
-
}
|
|
3862
|
-
case "find": {
|
|
4090
|
+
case "impact":
|
|
4091
|
+
return executeImpact(store, cql, entities, question);
|
|
4092
|
+
case "find":
|
|
3863
4093
|
return fusion.search(question, 10);
|
|
3864
|
-
}
|
|
3865
4094
|
case "relationships": {
|
|
3866
|
-
const rootId = entities[0].nodeId;
|
|
3867
4095
|
const result = cql.execute({
|
|
3868
|
-
rootNodeIds: [
|
|
4096
|
+
rootNodeIds: [entities[0].nodeId],
|
|
3869
4097
|
bidirectional: true,
|
|
3870
4098
|
maxDepth: 1
|
|
3871
4099
|
});
|
|
3872
4100
|
return { nodes: result.nodes, edges: result.edges };
|
|
3873
4101
|
}
|
|
3874
|
-
case "explain":
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
for (const rootId of rootIds) {
|
|
3879
|
-
const expanded = cql.execute({
|
|
3880
|
-
rootNodeIds: [rootId],
|
|
3881
|
-
maxDepth: 2
|
|
3882
|
-
});
|
|
3883
|
-
const matchingResult = searchResults.find((r) => r.nodeId === rootId);
|
|
3884
|
-
contextBlocks.push({
|
|
3885
|
-
rootNode: rootId,
|
|
3886
|
-
score: matchingResult?.score ?? 1,
|
|
3887
|
-
nodes: expanded.nodes,
|
|
3888
|
-
edges: expanded.edges
|
|
3889
|
-
});
|
|
3890
|
-
}
|
|
3891
|
-
return { searchResults, context: contextBlocks };
|
|
3892
|
-
}
|
|
3893
|
-
case "anomaly": {
|
|
3894
|
-
const adapter = new GraphAnomalyAdapter(store);
|
|
3895
|
-
return adapter.detect();
|
|
3896
|
-
}
|
|
4102
|
+
case "explain":
|
|
4103
|
+
return executeExplain(cql, entities, question, fusion);
|
|
4104
|
+
case "anomaly":
|
|
4105
|
+
return new GraphAnomalyAdapter(store).detect();
|
|
3897
4106
|
default:
|
|
3898
4107
|
return null;
|
|
3899
4108
|
}
|
|
@@ -3914,12 +4123,14 @@ var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
|
|
|
3914
4123
|
"method",
|
|
3915
4124
|
"variable"
|
|
3916
4125
|
]);
|
|
4126
|
+
function countMetadataChars(node) {
|
|
4127
|
+
return node.metadata ? JSON.stringify(node.metadata).length : 0;
|
|
4128
|
+
}
|
|
4129
|
+
function countBaseChars(node) {
|
|
4130
|
+
return (node.name?.length ?? 0) + (node.path?.length ?? 0) + (node.type?.length ?? 0);
|
|
4131
|
+
}
|
|
3917
4132
|
function estimateNodeTokens(node) {
|
|
3918
|
-
|
|
3919
|
-
if (node.metadata) {
|
|
3920
|
-
chars += JSON.stringify(node.metadata).length;
|
|
3921
|
-
}
|
|
3922
|
-
return Math.ceil(chars / 4);
|
|
4133
|
+
return Math.ceil((countBaseChars(node) + countMetadataChars(node)) / 4);
|
|
3923
4134
|
}
|
|
3924
4135
|
var Assembler = class {
|
|
3925
4136
|
store;
|
|
@@ -4000,47 +4211,55 @@ var Assembler = class {
|
|
|
4000
4211
|
}
|
|
4001
4212
|
return { keptNodes, tokenEstimate, truncated };
|
|
4002
4213
|
}
|
|
4003
|
-
|
|
4004
|
-
* Compute a token budget allocation across node types.
|
|
4005
|
-
*/
|
|
4006
|
-
computeBudget(totalTokens, phase) {
|
|
4007
|
-
const allNodes = this.store.findNodes({});
|
|
4214
|
+
countNodesByType() {
|
|
4008
4215
|
const typeCounts = {};
|
|
4009
|
-
for (const node of
|
|
4216
|
+
for (const node of this.store.findNodes({})) {
|
|
4010
4217
|
typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;
|
|
4011
4218
|
}
|
|
4219
|
+
return typeCounts;
|
|
4220
|
+
}
|
|
4221
|
+
computeModuleDensity() {
|
|
4012
4222
|
const density = {};
|
|
4013
|
-
const
|
|
4014
|
-
|
|
4015
|
-
const
|
|
4016
|
-
|
|
4017
|
-
density[mod.name] = outEdges.length + inEdges.length;
|
|
4223
|
+
for (const mod of this.store.findNodes({ type: "module" })) {
|
|
4224
|
+
const out = this.store.getEdges({ from: mod.id }).length;
|
|
4225
|
+
const inn = this.store.getEdges({ to: mod.id }).length;
|
|
4226
|
+
density[mod.name] = out + inn;
|
|
4018
4227
|
}
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4228
|
+
return density;
|
|
4229
|
+
}
|
|
4230
|
+
computeTypeWeights(typeCounts, boostTypes) {
|
|
4022
4231
|
const weights = {};
|
|
4232
|
+
let weightedTotal = 0;
|
|
4023
4233
|
for (const [type, count] of Object.entries(typeCounts)) {
|
|
4024
|
-
const
|
|
4025
|
-
const weight = count * (isBoosted ? boostFactor : 1);
|
|
4234
|
+
const weight = count * (boostTypes?.includes(type) ? 2 : 1);
|
|
4026
4235
|
weights[type] = weight;
|
|
4027
4236
|
weightedTotal += weight;
|
|
4028
4237
|
}
|
|
4238
|
+
return { weights, weightedTotal };
|
|
4239
|
+
}
|
|
4240
|
+
allocateProportionally(weights, weightedTotal, totalTokens) {
|
|
4029
4241
|
const allocations = {};
|
|
4030
|
-
if (weightedTotal
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
}
|
|
4242
|
+
if (weightedTotal === 0) return allocations;
|
|
4243
|
+
let allocated = 0;
|
|
4244
|
+
const types = Object.keys(weights);
|
|
4245
|
+
for (let i = 0; i < types.length; i++) {
|
|
4246
|
+
const type = types[i];
|
|
4247
|
+
if (i === types.length - 1) {
|
|
4248
|
+
allocations[type] = totalTokens - allocated;
|
|
4249
|
+
} else {
|
|
4250
|
+
const share = Math.round(weights[type] / weightedTotal * totalTokens);
|
|
4251
|
+
allocations[type] = share;
|
|
4252
|
+
allocated += share;
|
|
4042
4253
|
}
|
|
4043
4254
|
}
|
|
4255
|
+
return allocations;
|
|
4256
|
+
}
|
|
4257
|
+
computeBudget(totalTokens, phase) {
|
|
4258
|
+
const typeCounts = this.countNodesByType();
|
|
4259
|
+
const density = this.computeModuleDensity();
|
|
4260
|
+
const boostTypes = phase ? PHASE_NODE_TYPES[phase] : void 0;
|
|
4261
|
+
const { weights, weightedTotal } = this.computeTypeWeights(typeCounts, boostTypes);
|
|
4262
|
+
const allocations = this.allocateProportionally(weights, weightedTotal, totalTokens);
|
|
4044
4263
|
return { total: totalTokens, allocations, density };
|
|
4045
4264
|
}
|
|
4046
4265
|
/**
|
|
@@ -4071,49 +4290,43 @@ var Assembler = class {
|
|
|
4071
4290
|
filePaths: Array.from(filePathSet)
|
|
4072
4291
|
};
|
|
4073
4292
|
}
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
const moduleNodes = this.store.findNodes({ type: "module" });
|
|
4079
|
-
const modulesWithEdgeCount = moduleNodes.map((mod) => {
|
|
4080
|
-
const outEdges = this.store.getEdges({ from: mod.id });
|
|
4081
|
-
const inEdges = this.store.getEdges({ to: mod.id });
|
|
4082
|
-
return { module: mod, edgeCount: outEdges.length + inEdges.length };
|
|
4293
|
+
buildModuleLines() {
|
|
4294
|
+
const modulesWithEdgeCount = this.store.findNodes({ type: "module" }).map((mod) => {
|
|
4295
|
+
const edgeCount = this.store.getEdges({ from: mod.id }).length + this.store.getEdges({ to: mod.id }).length;
|
|
4296
|
+
return { module: mod, edgeCount };
|
|
4083
4297
|
});
|
|
4084
4298
|
modulesWithEdgeCount.sort((a, b) => b.edgeCount - a.edgeCount);
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
if (fileNode && fileNode.type === "file") {
|
|
4095
|
-
const symbolEdges = this.store.getEdges({ from: fileNode.id, type: "contains" });
|
|
4096
|
-
lines.push(`- ${fileNode.path ?? fileNode.name} (${symbolEdges.length} symbols)`);
|
|
4097
|
-
}
|
|
4299
|
+
if (modulesWithEdgeCount.length === 0) return [];
|
|
4300
|
+
const lines = ["## Modules", ""];
|
|
4301
|
+
for (const { module: mod, edgeCount } of modulesWithEdgeCount) {
|
|
4302
|
+
lines.push(`### ${mod.name} (${edgeCount} connections)`, "");
|
|
4303
|
+
for (const edge of this.store.getEdges({ from: mod.id, type: "contains" })) {
|
|
4304
|
+
const fileNode = this.store.getNode(edge.to);
|
|
4305
|
+
if (fileNode?.type === "file") {
|
|
4306
|
+
const symbols = this.store.getEdges({ from: fileNode.id, type: "contains" }).length;
|
|
4307
|
+
lines.push(`- ${fileNode.path ?? fileNode.name} (${symbols} symbols)`);
|
|
4098
4308
|
}
|
|
4099
|
-
lines.push("");
|
|
4100
4309
|
}
|
|
4310
|
+
lines.push("");
|
|
4101
4311
|
}
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
return { file: f, outDegree: outEdges.length };
|
|
4107
|
-
});
|
|
4312
|
+
return lines;
|
|
4313
|
+
}
|
|
4314
|
+
buildEntryPointLines() {
|
|
4315
|
+
const filesWithOutDegree = this.store.findNodes({ type: "file" }).filter((n) => !n.name.startsWith("index.")).map((f) => ({ file: f, outDegree: this.store.getEdges({ from: f.id }).length }));
|
|
4108
4316
|
filesWithOutDegree.sort((a, b) => b.outDegree - a.outDegree);
|
|
4109
4317
|
const entryPoints = filesWithOutDegree.filter((f) => f.outDegree > 0).slice(0, 5);
|
|
4110
|
-
if (entryPoints.length
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
}
|
|
4115
|
-
lines.push("");
|
|
4318
|
+
if (entryPoints.length === 0) return [];
|
|
4319
|
+
const lines = ["## Entry Points", ""];
|
|
4320
|
+
for (const { file, outDegree } of entryPoints) {
|
|
4321
|
+
lines.push(`- ${file.path ?? file.name} (${outDegree} outbound edges)`);
|
|
4116
4322
|
}
|
|
4323
|
+
lines.push("");
|
|
4324
|
+
return lines;
|
|
4325
|
+
}
|
|
4326
|
+
generateMap() {
|
|
4327
|
+
const lines = ["# Repository Structure", ""];
|
|
4328
|
+
lines.push(...this.buildModuleLines());
|
|
4329
|
+
lines.push(...this.buildEntryPointLines());
|
|
4117
4330
|
return lines.join("\n");
|
|
4118
4331
|
}
|
|
4119
4332
|
/**
|
|
@@ -4247,10 +4460,15 @@ var GraphConstraintAdapter = class {
|
|
|
4247
4460
|
}
|
|
4248
4461
|
store;
|
|
4249
4462
|
computeDependencyGraph() {
|
|
4250
|
-
const
|
|
4251
|
-
const
|
|
4252
|
-
|
|
4253
|
-
|
|
4463
|
+
const nodes = this.collectFileNodePaths();
|
|
4464
|
+
const edges = this.collectImportEdges();
|
|
4465
|
+
return { nodes, edges };
|
|
4466
|
+
}
|
|
4467
|
+
collectFileNodePaths() {
|
|
4468
|
+
return this.store.findNodes({ type: "file" }).map((n) => n.path ?? n.id);
|
|
4469
|
+
}
|
|
4470
|
+
collectImportEdges() {
|
|
4471
|
+
return this.store.getEdges({ type: "imports" }).map((e) => {
|
|
4254
4472
|
const fromNode = this.store.getNode(e.from);
|
|
4255
4473
|
const toNode = this.store.getNode(e.to);
|
|
4256
4474
|
const fromPath = fromNode?.path ?? e.from;
|
|
@@ -4259,7 +4477,6 @@ var GraphConstraintAdapter = class {
|
|
|
4259
4477
|
const line = e.metadata?.line ?? 0;
|
|
4260
4478
|
return { from: fromPath, to: toPath, importType, line };
|
|
4261
4479
|
});
|
|
4262
|
-
return { nodes, edges };
|
|
4263
4480
|
}
|
|
4264
4481
|
computeLayerViolations(layers, rootDir) {
|
|
4265
4482
|
const { edges } = this.computeDependencyGraph();
|
|
@@ -4553,65 +4770,53 @@ var GraphFeedbackAdapter = class {
|
|
|
4553
4770
|
const affectedDocs = [];
|
|
4554
4771
|
let impactScope = 0;
|
|
4555
4772
|
for (const filePath of changedFiles) {
|
|
4556
|
-
const
|
|
4557
|
-
if (
|
|
4558
|
-
const
|
|
4559
|
-
|
|
4560
|
-
for (const edge of inboundImports) {
|
|
4561
|
-
const importerNode = this.store.getNode(edge.from);
|
|
4562
|
-
if (importerNode?.path && /test/i.test(importerNode.path)) {
|
|
4563
|
-
affectedTests.push({
|
|
4564
|
-
testFile: importerNode.path,
|
|
4565
|
-
coversFile: filePath
|
|
4566
|
-
});
|
|
4567
|
-
}
|
|
4568
|
-
impactScope++;
|
|
4569
|
-
}
|
|
4570
|
-
const docsEdges = this.store.getEdges({ to: fileNode.id, type: "documents" });
|
|
4571
|
-
for (const edge of docsEdges) {
|
|
4572
|
-
const docNode = this.store.getNode(edge.from);
|
|
4573
|
-
if (docNode) {
|
|
4574
|
-
affectedDocs.push({
|
|
4575
|
-
docFile: docNode.path ?? docNode.name,
|
|
4576
|
-
documentsFile: filePath
|
|
4577
|
-
});
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4773
|
+
const fileNode = this.store.findNodes({ path: filePath })[0];
|
|
4774
|
+
if (!fileNode) continue;
|
|
4775
|
+
const counts = this.collectFileImpact(fileNode.id, filePath, affectedTests, affectedDocs);
|
|
4776
|
+
impactScope += counts.impactScope;
|
|
4580
4777
|
}
|
|
4581
4778
|
return { affectedTests, affectedDocs, impactScope };
|
|
4582
4779
|
}
|
|
4583
|
-
|
|
4584
|
-
const
|
|
4585
|
-
const
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
let undocumentedFiles = 0;
|
|
4590
|
-
for (const node of fileNodes) {
|
|
4591
|
-
const docsEdges = this.store.getEdges({ to: node.id, type: "documents" });
|
|
4592
|
-
if (docsEdges.length === 0) {
|
|
4593
|
-
undocumentedFiles++;
|
|
4780
|
+
collectFileImpact(fileNodeId, filePath, affectedTests, affectedDocs) {
|
|
4781
|
+
const inboundImports = this.store.getEdges({ to: fileNodeId, type: "imports" });
|
|
4782
|
+
for (const edge of inboundImports) {
|
|
4783
|
+
const importerNode = this.store.getNode(edge.from);
|
|
4784
|
+
if (importerNode?.path && /test/i.test(importerNode.path)) {
|
|
4785
|
+
affectedTests.push({ testFile: importerNode.path, coversFile: filePath });
|
|
4594
4786
|
}
|
|
4595
4787
|
}
|
|
4596
|
-
|
|
4597
|
-
for (const
|
|
4598
|
-
const
|
|
4599
|
-
if (
|
|
4600
|
-
|
|
4601
|
-
if (!isEntryPoint) {
|
|
4602
|
-
unreachableNodes++;
|
|
4603
|
-
}
|
|
4788
|
+
const docsEdges = this.store.getEdges({ to: fileNodeId, type: "documents" });
|
|
4789
|
+
for (const edge of docsEdges) {
|
|
4790
|
+
const docNode = this.store.getNode(edge.from);
|
|
4791
|
+
if (docNode) {
|
|
4792
|
+
affectedDocs.push({ docFile: docNode.path ?? docNode.name, documentsFile: filePath });
|
|
4604
4793
|
}
|
|
4605
4794
|
}
|
|
4795
|
+
return { impactScope: inboundImports.length };
|
|
4796
|
+
}
|
|
4797
|
+
computeHarnessCheckData() {
|
|
4798
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
4606
4799
|
return {
|
|
4607
4800
|
graphExists: true,
|
|
4608
|
-
nodeCount,
|
|
4609
|
-
edgeCount,
|
|
4610
|
-
constraintViolations,
|
|
4611
|
-
undocumentedFiles,
|
|
4612
|
-
unreachableNodes
|
|
4801
|
+
nodeCount: this.store.nodeCount,
|
|
4802
|
+
edgeCount: this.store.edgeCount,
|
|
4803
|
+
constraintViolations: this.store.getEdges({ type: "violates" }).length,
|
|
4804
|
+
undocumentedFiles: this.countUndocumentedFiles(fileNodes),
|
|
4805
|
+
unreachableNodes: this.countUnreachableNodes(fileNodes)
|
|
4613
4806
|
};
|
|
4614
4807
|
}
|
|
4808
|
+
countUndocumentedFiles(fileNodes) {
|
|
4809
|
+
return fileNodes.filter(
|
|
4810
|
+
(node) => this.store.getEdges({ to: node.id, type: "documents" }).length === 0
|
|
4811
|
+
).length;
|
|
4812
|
+
}
|
|
4813
|
+
countUnreachableNodes(fileNodes) {
|
|
4814
|
+
return fileNodes.filter((node) => {
|
|
4815
|
+
if (this.store.getEdges({ to: node.id, type: "imports" }).length > 0) return false;
|
|
4816
|
+
const isEntryPoint = node.name === "index.ts" || node.path !== void 0 && node.path.endsWith("/index.ts") || node.metadata?.entryPoint === true;
|
|
4817
|
+
return !isEntryPoint;
|
|
4818
|
+
}).length;
|
|
4819
|
+
}
|
|
4615
4820
|
};
|
|
4616
4821
|
|
|
4617
4822
|
// src/independence/TaskIndependenceAnalyzer.ts
|
|
@@ -4628,47 +4833,46 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4628
4833
|
this.validate(tasks);
|
|
4629
4834
|
const useGraph = this.store != null && depth > 0;
|
|
4630
4835
|
const analysisLevel = useGraph ? "graph-expanded" : "file-only";
|
|
4836
|
+
const { originalFiles, expandedFiles } = this.buildFileSets(tasks, useGraph, depth, edgeTypes);
|
|
4837
|
+
const taskIds = tasks.map((t) => t.id);
|
|
4838
|
+
const pairs = this.computeAllPairs(taskIds, originalFiles, expandedFiles);
|
|
4839
|
+
const groups = this.buildGroups(taskIds, pairs);
|
|
4840
|
+
const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
|
|
4841
|
+
return { tasks: taskIds, analysisLevel, depth, pairs, groups, verdict };
|
|
4842
|
+
}
|
|
4843
|
+
// --- Private methods ---
|
|
4844
|
+
buildFileSets(tasks, useGraph, depth, edgeTypes) {
|
|
4631
4845
|
const originalFiles = /* @__PURE__ */ new Map();
|
|
4632
4846
|
const expandedFiles = /* @__PURE__ */ new Map();
|
|
4633
4847
|
for (const task of tasks) {
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
} else {
|
|
4640
|
-
expandedFiles.set(task.id, /* @__PURE__ */ new Map());
|
|
4641
|
-
}
|
|
4848
|
+
originalFiles.set(task.id, new Set(task.files));
|
|
4849
|
+
expandedFiles.set(
|
|
4850
|
+
task.id,
|
|
4851
|
+
useGraph ? this.expandViaGraph(task.files, depth, edgeTypes) : /* @__PURE__ */ new Map()
|
|
4852
|
+
);
|
|
4642
4853
|
}
|
|
4643
|
-
|
|
4854
|
+
return { originalFiles, expandedFiles };
|
|
4855
|
+
}
|
|
4856
|
+
computeAllPairs(taskIds, originalFiles, expandedFiles) {
|
|
4644
4857
|
const pairs = [];
|
|
4645
4858
|
for (let i = 0; i < taskIds.length; i++) {
|
|
4646
4859
|
for (let j = i + 1; j < taskIds.length; j++) {
|
|
4647
4860
|
const idA = taskIds[i];
|
|
4648
4861
|
const idB = taskIds[j];
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4862
|
+
pairs.push(
|
|
4863
|
+
this.computePairOverlap(
|
|
4864
|
+
idA,
|
|
4865
|
+
idB,
|
|
4866
|
+
originalFiles.get(idA),
|
|
4867
|
+
originalFiles.get(idB),
|
|
4868
|
+
expandedFiles.get(idA),
|
|
4869
|
+
expandedFiles.get(idB)
|
|
4870
|
+
)
|
|
4656
4871
|
);
|
|
4657
|
-
pairs.push(pair);
|
|
4658
4872
|
}
|
|
4659
4873
|
}
|
|
4660
|
-
|
|
4661
|
-
const verdict = this.generateVerdict(taskIds, groups, analysisLevel);
|
|
4662
|
-
return {
|
|
4663
|
-
tasks: taskIds,
|
|
4664
|
-
analysisLevel,
|
|
4665
|
-
depth,
|
|
4666
|
-
pairs,
|
|
4667
|
-
groups,
|
|
4668
|
-
verdict
|
|
4669
|
-
};
|
|
4874
|
+
return pairs;
|
|
4670
4875
|
}
|
|
4671
|
-
// --- Private methods ---
|
|
4672
4876
|
validate(tasks) {
|
|
4673
4877
|
if (tasks.length < 2) {
|
|
4674
4878
|
throw new Error("At least 2 tasks are required for independence analysis");
|
|
@@ -4821,27 +5025,62 @@ var ConflictPredictor = class {
|
|
|
4821
5025
|
predict(params) {
|
|
4822
5026
|
const analyzer = new TaskIndependenceAnalyzer(this.store);
|
|
4823
5027
|
const result = analyzer.analyze(params);
|
|
5028
|
+
const { churnMap, couplingMap, churnThreshold, couplingThreshold } = this.buildMetricMaps();
|
|
5029
|
+
const conflicts = this.classifyConflicts(
|
|
5030
|
+
result.pairs,
|
|
5031
|
+
churnMap,
|
|
5032
|
+
couplingMap,
|
|
5033
|
+
churnThreshold,
|
|
5034
|
+
couplingThreshold
|
|
5035
|
+
);
|
|
5036
|
+
const taskIds = result.tasks;
|
|
5037
|
+
const groups = this.buildHighSeverityGroups(taskIds, conflicts);
|
|
5038
|
+
const regrouped = !this.groupsEqual(result.groups, groups);
|
|
5039
|
+
const { highCount, mediumCount, lowCount } = this.countBySeverity(conflicts);
|
|
5040
|
+
const verdict = this.generateVerdict(
|
|
5041
|
+
taskIds,
|
|
5042
|
+
groups,
|
|
5043
|
+
result.analysisLevel,
|
|
5044
|
+
highCount,
|
|
5045
|
+
mediumCount,
|
|
5046
|
+
lowCount,
|
|
5047
|
+
regrouped
|
|
5048
|
+
);
|
|
5049
|
+
return {
|
|
5050
|
+
tasks: taskIds,
|
|
5051
|
+
analysisLevel: result.analysisLevel,
|
|
5052
|
+
depth: result.depth,
|
|
5053
|
+
conflicts,
|
|
5054
|
+
groups,
|
|
5055
|
+
summary: { high: highCount, medium: mediumCount, low: lowCount, regrouped },
|
|
5056
|
+
verdict
|
|
5057
|
+
};
|
|
5058
|
+
}
|
|
5059
|
+
// --- Private helpers ---
|
|
5060
|
+
buildMetricMaps() {
|
|
4824
5061
|
const churnMap = /* @__PURE__ */ new Map();
|
|
4825
5062
|
const couplingMap = /* @__PURE__ */ new Map();
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
}
|
|
4835
|
-
}
|
|
4836
|
-
const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
|
|
4837
|
-
for (const fileData of couplingResult.files) {
|
|
4838
|
-
couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
|
|
5063
|
+
if (this.store == null) {
|
|
5064
|
+
return { churnMap, couplingMap, churnThreshold: Infinity, couplingThreshold: Infinity };
|
|
5065
|
+
}
|
|
5066
|
+
const complexityResult = new GraphComplexityAdapter(this.store).computeComplexityHotspots();
|
|
5067
|
+
for (const hotspot of complexityResult.hotspots) {
|
|
5068
|
+
const existing = churnMap.get(hotspot.file);
|
|
5069
|
+
if (existing === void 0 || hotspot.changeFrequency > existing) {
|
|
5070
|
+
churnMap.set(hotspot.file, hotspot.changeFrequency);
|
|
4839
5071
|
}
|
|
4840
|
-
churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
|
|
4841
|
-
couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
|
|
4842
5072
|
}
|
|
5073
|
+
const couplingResult = new GraphCouplingAdapter(this.store).computeCouplingData();
|
|
5074
|
+
for (const fileData of couplingResult.files) {
|
|
5075
|
+
couplingMap.set(fileData.file, fileData.fanIn + fileData.fanOut);
|
|
5076
|
+
}
|
|
5077
|
+
const churnThreshold = this.computePercentile(Array.from(churnMap.values()), 80);
|
|
5078
|
+
const couplingThreshold = this.computePercentile(Array.from(couplingMap.values()), 80);
|
|
5079
|
+
return { churnMap, couplingMap, churnThreshold, couplingThreshold };
|
|
5080
|
+
}
|
|
5081
|
+
classifyConflicts(pairs, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
4843
5082
|
const conflicts = [];
|
|
4844
|
-
for (const pair of
|
|
5083
|
+
for (const pair of pairs) {
|
|
4845
5084
|
if (pair.independent) continue;
|
|
4846
5085
|
const { severity, reason, mitigation } = this.classifyPair(
|
|
4847
5086
|
pair.taskA,
|
|
@@ -4861,9 +5100,9 @@ var ConflictPredictor = class {
|
|
|
4861
5100
|
overlaps: pair.overlaps
|
|
4862
5101
|
});
|
|
4863
5102
|
}
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
5103
|
+
return conflicts;
|
|
5104
|
+
}
|
|
5105
|
+
countBySeverity(conflicts) {
|
|
4867
5106
|
let highCount = 0;
|
|
4868
5107
|
let mediumCount = 0;
|
|
4869
5108
|
let lowCount = 0;
|
|
@@ -4872,68 +5111,57 @@ var ConflictPredictor = class {
|
|
|
4872
5111
|
else if (c.severity === "medium") mediumCount++;
|
|
4873
5112
|
else lowCount++;
|
|
4874
5113
|
}
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5114
|
+
return { highCount, mediumCount, lowCount };
|
|
5115
|
+
}
|
|
5116
|
+
classifyTransitiveOverlap(taskA, taskB, overlap, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
5117
|
+
const churn = churnMap.get(overlap.file);
|
|
5118
|
+
const coupling = couplingMap.get(overlap.file);
|
|
5119
|
+
const via = overlap.via ?? "unknown";
|
|
5120
|
+
if (churn !== void 0 && churn >= churnThreshold && churnThreshold !== Infinity) {
|
|
5121
|
+
return {
|
|
5122
|
+
severity: "medium",
|
|
5123
|
+
reason: `Transitive overlap on high-churn file ${overlap.file} (via ${via})`,
|
|
5124
|
+
mitigation: `Review: ${overlap.file} changes frequently \u2014 coordinate edits between ${taskA} and ${taskB}`
|
|
5125
|
+
};
|
|
5126
|
+
}
|
|
5127
|
+
if (coupling !== void 0 && coupling >= couplingThreshold && couplingThreshold !== Infinity) {
|
|
5128
|
+
return {
|
|
5129
|
+
severity: "medium",
|
|
5130
|
+
reason: `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`,
|
|
5131
|
+
mitigation: `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`
|
|
5132
|
+
};
|
|
5133
|
+
}
|
|
4884
5134
|
return {
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
conflicts,
|
|
4889
|
-
groups,
|
|
4890
|
-
summary: {
|
|
4891
|
-
high: highCount,
|
|
4892
|
-
medium: mediumCount,
|
|
4893
|
-
low: lowCount,
|
|
4894
|
-
regrouped
|
|
4895
|
-
},
|
|
4896
|
-
verdict
|
|
5135
|
+
severity: "low",
|
|
5136
|
+
reason: `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`,
|
|
5137
|
+
mitigation: `Info: transitive overlap unlikely to cause conflicts`
|
|
4897
5138
|
};
|
|
4898
5139
|
}
|
|
4899
|
-
// --- Private helpers ---
|
|
4900
5140
|
classifyPair(taskA, taskB, overlaps, churnMap, couplingMap, churnThreshold, couplingThreshold) {
|
|
4901
5141
|
let maxSeverity = "low";
|
|
4902
5142
|
let primaryReason = "";
|
|
4903
5143
|
let primaryMitigation = "";
|
|
4904
5144
|
for (const overlap of overlaps) {
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
reason = `Transitive overlap on highly-coupled file ${overlap.file} (via ${via})`;
|
|
4923
|
-
mitigation = `Review: ${overlap.file} has high coupling \u2014 coordinate edits between ${taskA} and ${taskB}`;
|
|
4924
|
-
} else {
|
|
4925
|
-
overlapSeverity = "low";
|
|
4926
|
-
reason = `Transitive overlap on ${overlap.file} (via ${via}) \u2014 low risk`;
|
|
4927
|
-
mitigation = `Info: transitive overlap unlikely to cause conflicts`;
|
|
4928
|
-
}
|
|
4929
|
-
}
|
|
4930
|
-
if (this.severityRank(overlapSeverity) > this.severityRank(maxSeverity)) {
|
|
4931
|
-
maxSeverity = overlapSeverity;
|
|
4932
|
-
primaryReason = reason;
|
|
4933
|
-
primaryMitigation = mitigation;
|
|
5145
|
+
const classified = overlap.type === "direct" ? {
|
|
5146
|
+
severity: "high",
|
|
5147
|
+
reason: `Both tasks write to ${overlap.file}`,
|
|
5148
|
+
mitigation: `Serialize: run ${taskA} before ${taskB}`
|
|
5149
|
+
} : this.classifyTransitiveOverlap(
|
|
5150
|
+
taskA,
|
|
5151
|
+
taskB,
|
|
5152
|
+
overlap,
|
|
5153
|
+
churnMap,
|
|
5154
|
+
couplingMap,
|
|
5155
|
+
churnThreshold,
|
|
5156
|
+
couplingThreshold
|
|
5157
|
+
);
|
|
5158
|
+
if (this.severityRank(classified.severity) > this.severityRank(maxSeverity)) {
|
|
5159
|
+
maxSeverity = classified.severity;
|
|
5160
|
+
primaryReason = classified.reason;
|
|
5161
|
+
primaryMitigation = classified.mitigation;
|
|
4934
5162
|
} else if (primaryReason === "") {
|
|
4935
|
-
primaryReason = reason;
|
|
4936
|
-
primaryMitigation = mitigation;
|
|
5163
|
+
primaryReason = classified.reason;
|
|
5164
|
+
primaryMitigation = classified.mitigation;
|
|
4937
5165
|
}
|
|
4938
5166
|
}
|
|
4939
5167
|
return { severity: maxSeverity, reason: primaryReason, mitigation: primaryMitigation };
|
|
@@ -5056,7 +5284,7 @@ var ConflictPredictor = class {
|
|
|
5056
5284
|
};
|
|
5057
5285
|
|
|
5058
5286
|
// src/index.ts
|
|
5059
|
-
var VERSION = "0.4.
|
|
5287
|
+
var VERSION = "0.4.3";
|
|
5060
5288
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5061
5289
|
0 && (module.exports = {
|
|
5062
5290
|
Assembler,
|
|
@@ -5088,8 +5316,10 @@ var VERSION = "0.4.0";
|
|
|
5088
5316
|
IntentClassifier,
|
|
5089
5317
|
JiraConnector,
|
|
5090
5318
|
KnowledgeIngestor,
|
|
5319
|
+
NODE_STABILITY,
|
|
5091
5320
|
NODE_TYPES,
|
|
5092
5321
|
OBSERVABILITY_TYPES,
|
|
5322
|
+
PackedSummaryCache,
|
|
5093
5323
|
RequirementIngestor,
|
|
5094
5324
|
ResponseFormatter,
|
|
5095
5325
|
SlackConnector,
|
|
@@ -5103,6 +5333,7 @@ var VERSION = "0.4.0";
|
|
|
5103
5333
|
groupNodesByImpact,
|
|
5104
5334
|
linkToCode,
|
|
5105
5335
|
loadGraph,
|
|
5336
|
+
normalizeIntent,
|
|
5106
5337
|
project,
|
|
5107
5338
|
queryTraceability,
|
|
5108
5339
|
saveGraph
|