@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.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 = /* @__PURE__ */ new Set();
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 outEdges = this.edgesByFrom.get(nodeId) ?? [];
266
- for (const edge of outEdges) {
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 inEdges = this.edgesByTo.get(nodeId) ?? [];
272
- for (const edge of inEdges) {
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 neighborIds) {
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 nodeId = `commit:${commit.shortHash}`;
1061
- this.store.addNode({
1062
- id: nodeId,
1063
- type: "commit",
1064
- name: commit.message,
1065
- metadata: {
1066
- author: commit.author,
1067
- email: commit.email,
1068
- date: commit.date,
1069
- hash: commit.hash
1070
- }
1071
- });
1072
- nodesAdded++;
1073
- for (const file of commit.files) {
1074
- const fileNodeId = `file:${file}`;
1075
- const existingNode = this.store.getNode(fileNodeId);
1076
- if (existingNode) {
1077
- this.store.addEdge({
1078
- from: fileNodeId,
1079
- to: nodeId,
1080
- type: "triggered_by"
1081
- });
1082
- edgesAdded++;
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
- const coChanges = this.computeCoChanges(commits);
1087
- for (const { fileA, fileB, count } of coChanges) {
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
- const nodeA = this.store.getNode(fileAId);
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
- const trimmed = line.trim();
1125
- if (!trimmed) {
1126
- if (current && current.hasFiles) {
1127
- commits.push({
1128
- hash: current.hash,
1129
- shortHash: current.shortHash,
1130
- author: current.author,
1131
- email: current.email,
1132
- date: current.date,
1133
- message: current.message,
1134
- files: current.files
1135
- });
1136
- current = null;
1137
- }
1138
- continue;
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
- const parts = trimmed.split("|");
1141
- if (parts.length >= 5 && /^[0-9a-f]{7,40}$/.test(parts[0])) {
1142
- if (current) {
1143
- commits.push({
1144
- hash: current.hash,
1145
- shortHash: current.shortHash,
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
- commits.push({
1171
- hash: current.hash,
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 commits;
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
- let content;
1357
- try {
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 lines) {
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
- let content;
1403
- try {
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 sections = content.split(/^##\s+/m).filter((s) => s.trim());
1412
- for (const section of sections) {
1413
- const dateMatch = section.match(/\*\*Date:\*\*\s*(.+)/);
1414
- const skillMatch = section.match(/\*\*Skill:\*\*\s*(.+)/);
1415
- const typeMatch = section.match(/\*\*Type:\*\*\s*(.+)/);
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, nodeId, "caused_by");
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 featureName = path4.basename(featureDir);
1538
- const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
1539
- let content;
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 headingMatch = line.match(SECTION_HEADING_RE);
1593
- if (headingMatch) {
1594
- const heading = headingMatch[1].trim();
1595
- const isReqSection = REQUIREMENT_SECTIONS.some(
1596
- (s) => heading.toLowerCase() === s.toLowerCase()
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
- const nodeId = `req:${specHash}:${globalIndex}`;
1615
- const earsPattern = detectEarsPattern(text);
1616
- results.push({
1617
- node: {
1618
- id: nodeId,
1619
- type: "requirement",
1620
- name: text,
1621
- path: specPath,
1622
- location: {
1623
- fileId: `file:${specPath}`,
1624
- startLine: lineNumber,
1625
- endLine: lineNumber
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 += `${jql ? " AND " : ""}status IN (${filters.status.map((s) => `"${s}"`).join(",")})`;
1996
+ jql = appendJqlClause(jql, `status IN (${filters.status.map((s) => `"${s}"`).join(",")})`);
1849
1997
  }
1850
1998
  if (filters?.labels?.length) {
1851
- jql += `${jql ? " AND " : ""}labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`;
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
- let startAt = 0;
1893
- const maxResults = 50;
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
- nodesAdded,
1913
- edgesAdded,
2039
+ 0,
2040
+ 0,
1914
2041
  [`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
1915
2042
  start
1916
2043
  );
1917
2044
  }
1918
- return buildIngestResult(nodesAdded, edgesAdded, [], start);
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
- try {
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
- nodesAdded: 0,
2144
- nodesUpdated: 0,
2145
- edgesAdded: 0,
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 buildId = `build:${run.id}`;
2172
- const safeName = sanitizeExternalText(run.name, 200);
2173
- store.addNode({
2174
- id: buildId,
2175
- type: "build",
2176
- name: `${safeName} #${run.id}`,
2177
- metadata: {
2178
- source: "github-actions",
2179
- status: run.status,
2180
- conclusion: run.conclusion,
2181
- branch: run.head_branch,
2182
- sha: run.head_sha,
2183
- url: run.html_url,
2184
- createdAt: run.created_at
2185
- }
2186
- });
2187
- nodesAdded++;
2188
- const commitNode = store.getNode(`commit:${run.head_sha}`);
2189
- if (commitNode) {
2190
- store.addEdge({ from: buildId, to: commitNode.id, type: "triggered_by" });
2191
- edgesAdded++;
2192
- }
2193
- if (run.conclusion === "failure") {
2194
- const testResultId = `test_result:${run.id}`;
2195
- store.addNode({
2196
- id: testResultId,
2197
- type: "test_result",
2198
- name: `Failed: ${safeName} #${run.id}`,
2199
- metadata: {
2200
- source: "github-actions",
2201
- buildId: String(run.id),
2202
- conclusion: "failure",
2203
- branch: run.head_branch,
2204
- sha: run.head_sha
2205
- }
2206
- });
2207
- nodesAdded++;
2208
- store.addEdge({ from: testResultId, to: buildId, type: "failed_in" });
2209
- edgesAdded++;
2210
- }
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, allNodes.length);
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
- const hasSemanticScores = semanticScores.size > 0;
2302
- const kwWeight = hasSemanticScores ? this.keywordWeight : 1;
2303
- const semWeight = hasSemanticScores ? this.semanticWeight : 0;
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.sort((a, b) => b.score - a.score);
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 codeNode = this.store.getNode(edge.to);
2378
- if (!codeNode) {
2548
+ const result = this.classifyDocEdge(edge);
2549
+ if (result.kind === "missing") {
2379
2550
  missingTargets.push(edge.to);
2380
- continue;
2551
+ } else if (result.kind === "fresh") {
2552
+ freshEdges++;
2553
+ } else {
2554
+ staleEdges.push(result.entry);
2381
2555
  }
2382
- const docNode = this.store.getNode(edge.from);
2383
- const codeLastModified = codeNode.lastModified;
2384
- const docLastModified = docNode?.lastModified;
2385
- if (codeLastModified && docLastModified) {
2386
- if (codeLastModified > docLastModified) {
2387
- staleEdges.push({
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
- } else {
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 { staleEdges, missingTargets, freshEdges };
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 requestedMetrics = options?.metrics ?? [...DEFAULT_METRICS];
2665
- const warnings = [];
2666
- const metricsToAnalyze = [];
2667
- for (const m of requestedMetrics) {
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 outlierNodeIds = new Set(allOutliers.map((o) => o.nodeId));
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 = /* @__PURE__ */ new Set();
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
- quotedConsumed.add(inner);
3469
+ consumed.add(inner);
3270
3470
  }
3271
3471
  }
3272
- const casingConsumed = /* @__PURE__ */ new Set();
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
- casingConsumed.add(token);
3481
+ consumed.add(token);
3278
3482
  }
3279
3483
  }
3280
- const pathConsumed = /* @__PURE__ */ new Set();
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
- pathConsumed.add(path7);
3492
+ consumed.add(path7);
3285
3493
  }
3286
- const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
3287
- const words = trimmed.split(/\s+/);
3288
- for (const raw of words) {
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 rawEntities = extractor.extract(question);
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
- const rootId = entities[0].nodeId;
3768
- const lowerQuestion = question.toLowerCase();
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: [rootId],
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
- const searchResults = fusion.search(question, 10);
3794
- const contextBlocks = [];
3795
- const rootIds = entities.length > 0 ? [entities[0].nodeId] : searchResults.slice(0, 3).map((r) => r.nodeId);
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
- let chars = (node.name?.length ?? 0) + (node.path?.length ?? 0) + (node.type?.length ?? 0);
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 allNodes) {
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 moduleNodes = this.store.findNodes({ type: "module" });
3932
- for (const mod of moduleNodes) {
3933
- const outEdges = this.store.getEdges({ from: mod.id });
3934
- const inEdges = this.store.getEdges({ to: mod.id });
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
- const boostTypes = phase ? PHASE_NODE_TYPES[phase] : void 0;
3938
- const boostFactor = 2;
3939
- let weightedTotal = 0;
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 isBoosted = boostTypes?.includes(type);
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 > 0) {
3949
- let allocated = 0;
3950
- const types = Object.keys(weights);
3951
- for (let i = 0; i < types.length; i++) {
3952
- const type = types[i];
3953
- if (i === types.length - 1) {
3954
- allocations[type] = totalTokens - allocated;
3955
- } else {
3956
- const share = Math.round(weights[type] / weightedTotal * totalTokens);
3957
- allocations[type] = share;
3958
- allocated += share;
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
- * Generate a markdown repository map from graph structure.
3994
- */
3995
- generateMap() {
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
- const lines = ["# Repository Structure", ""];
4004
- if (modulesWithEdgeCount.length > 0) {
4005
- lines.push("## Modules", "");
4006
- for (const { module: mod, edgeCount } of modulesWithEdgeCount) {
4007
- lines.push(`### ${mod.name} (${edgeCount} connections)`);
4008
- lines.push("");
4009
- const containsEdges = this.store.getEdges({ from: mod.id, type: "contains" });
4010
- for (const edge of containsEdges) {
4011
- const fileNode = this.store.getNode(edge.to);
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
- const fileNodes = this.store.findNodes({ type: "file" });
4021
- const nonBarrelFiles = fileNodes.filter((n) => !n.name.startsWith("index."));
4022
- const filesWithOutDegree = nonBarrelFiles.map((f) => {
4023
- const outEdges = this.store.getEdges({ from: f.id });
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 > 0) {
4029
- lines.push("## Entry Points", "");
4030
- for (const { file, outDegree } of entryPoints) {
4031
- lines.push(`- ${file.path ?? file.name} (${outDegree} outbound edges)`);
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 fileNodes = this.store.findNodes({ type: "file" });
4169
- const nodes = fileNodes.map((n) => n.path ?? n.id);
4170
- const importsEdges = this.store.getEdges({ type: "imports" });
4171
- const edges = importsEdges.map((e) => {
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 fileNodes = this.store.findNodes({ path: filePath });
4475
- if (fileNodes.length === 0) continue;
4476
- const fileNode = fileNodes[0];
4477
- const inboundImports = this.store.getEdges({ to: fileNode.id, type: "imports" });
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
- computeHarnessCheckData() {
4502
- const nodeCount = this.store.nodeCount;
4503
- const edgeCount = this.store.edgeCount;
4504
- const violatesEdges = this.store.getEdges({ type: "violates" });
4505
- const constraintViolations = violatesEdges.length;
4506
- const fileNodes = this.store.findNodes({ type: "file" });
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
- let unreachableNodes = 0;
4515
- for (const node of fileNodes) {
4516
- const inboundImports = this.store.getEdges({ to: node.id, type: "imports" });
4517
- if (inboundImports.length === 0) {
4518
- const isEntryPoint = node.name === "index.ts" || node.path !== void 0 && node.path.endsWith("/index.ts") || node.metadata?.entryPoint === true;
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
- const origSet = new Set(task.files);
4553
- originalFiles.set(task.id, origSet);
4554
- if (useGraph) {
4555
- const expanded = this.expandViaGraph(task.files, depth, edgeTypes);
4556
- expandedFiles.set(task.id, expanded);
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
- const taskIds = tasks.map((t) => t.id);
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
- const pair = this.computePairOverlap(
4568
- idA,
4569
- idB,
4570
- originalFiles.get(idA),
4571
- originalFiles.get(idB),
4572
- expandedFiles.get(idA),
4573
- expandedFiles.get(idB)
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
- const groups = this.buildGroups(taskIds, pairs);
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
- let churnThreshold = Infinity;
4745
- let couplingThreshold = Infinity;
4746
- if (this.store != null) {
4747
- const complexityResult = new GraphComplexityAdapter(this.store).computeComplexityHotspots();
4748
- for (const hotspot of complexityResult.hotspots) {
4749
- const existing = churnMap.get(hotspot.file);
4750
- if (existing === void 0 || hotspot.changeFrequency > existing) {
4751
- churnMap.set(hotspot.file, hotspot.changeFrequency);
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 result.pairs) {
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
- const taskIds = result.tasks;
4783
- const groups = this.buildHighSeverityGroups(taskIds, conflicts);
4784
- const regrouped = !this.groupsEqual(result.groups, groups);
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
- const verdict = this.generateVerdict(
4794
- taskIds,
4795
- groups,
4796
- result.analysisLevel,
4797
- highCount,
4798
- mediumCount,
4799
- lowCount,
4800
- regrouped
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
- tasks: taskIds,
4804
- analysisLevel: result.analysisLevel,
4805
- depth: result.depth,
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
- let overlapSeverity;
4824
- let reason;
4825
- let mitigation;
4826
- if (overlap.type === "direct") {
4827
- overlapSeverity = "high";
4828
- reason = `Both tasks write to ${overlap.file}`;
4829
- mitigation = `Serialize: run ${taskA} before ${taskB}`;
4830
- } else {
4831
- const churn = churnMap.get(overlap.file);
4832
- const coupling = couplingMap.get(overlap.file);
4833
- const via = overlap.via ?? "unknown";
4834
- if (churn !== void 0 && churn >= churnThreshold && churnThreshold !== Infinity) {
4835
- overlapSeverity = "medium";
4836
- reason = `Transitive overlap on high-churn file ${overlap.file} (via ${via})`;
4837
- mitigation = `Review: ${overlap.file} changes frequently \u2014 coordinate edits between ${taskA} and ${taskB}`;
4838
- } else if (coupling !== void 0 && coupling >= couplingThreshold && couplingThreshold !== Infinity) {
4839
- overlapSeverity = "medium";
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.0";
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