@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.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 = /* @__PURE__ */ new Set();
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 outEdges = this.edgesByFrom.get(nodeId) ?? [];
348
- for (const edge of outEdges) {
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 inEdges = this.edgesByTo.get(nodeId) ?? [];
354
- for (const edge of inEdges) {
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 neighborIds) {
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 nodeId = `commit:${commit.shortHash}`;
1143
- this.store.addNode({
1144
- id: nodeId,
1145
- type: "commit",
1146
- name: commit.message,
1147
- metadata: {
1148
- author: commit.author,
1149
- email: commit.email,
1150
- date: commit.date,
1151
- hash: commit.hash
1152
- }
1153
- });
1154
- nodesAdded++;
1155
- for (const file of commit.files) {
1156
- const fileNodeId = `file:${file}`;
1157
- const existingNode = this.store.getNode(fileNodeId);
1158
- if (existingNode) {
1159
- this.store.addEdge({
1160
- from: fileNodeId,
1161
- to: nodeId,
1162
- type: "triggered_by"
1163
- });
1164
- edgesAdded++;
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
- const coChanges = this.computeCoChanges(commits);
1169
- for (const { fileA, fileB, count } of coChanges) {
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
- const nodeA = this.store.getNode(fileAId);
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
- const trimmed = line.trim();
1207
- if (!trimmed) {
1208
- if (current && current.hasFiles) {
1209
- commits.push({
1210
- hash: current.hash,
1211
- shortHash: current.shortHash,
1212
- author: current.author,
1213
- email: current.email,
1214
- date: current.date,
1215
- message: current.message,
1216
- files: current.files
1217
- });
1218
- current = null;
1219
- }
1220
- continue;
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
- const parts = trimmed.split("|");
1223
- if (parts.length >= 5 && /^[0-9a-f]{7,40}$/.test(parts[0])) {
1224
- if (current) {
1225
- commits.push({
1226
- hash: current.hash,
1227
- shortHash: current.shortHash,
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
- commits.push({
1253
- hash: current.hash,
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 commits;
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
- let content;
1439
- try {
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 lines) {
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
- let content;
1485
- try {
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 sections = content.split(/^##\s+/m).filter((s) => s.trim());
1494
- for (const section of sections) {
1495
- const dateMatch = section.match(/\*\*Date:\*\*\s*(.+)/);
1496
- const skillMatch = section.match(/\*\*Skill:\*\*\s*(.+)/);
1497
- const typeMatch = section.match(/\*\*Type:\*\*\s*(.+)/);
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, nodeId, "caused_by");
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 featureName = path4.basename(featureDir);
1620
- const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
1621
- let content;
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 headingMatch = line.match(SECTION_HEADING_RE);
1675
- if (headingMatch) {
1676
- const heading = headingMatch[1].trim();
1677
- const isReqSection = REQUIREMENT_SECTIONS.some(
1678
- (s) => heading.toLowerCase() === s.toLowerCase()
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
- const nodeId = `req:${specHash}:${globalIndex}`;
1697
- const earsPattern = detectEarsPattern(text);
1698
- results.push({
1699
- node: {
1700
- id: nodeId,
1701
- type: "requirement",
1702
- name: text,
1703
- path: specPath,
1704
- location: {
1705
- fileId: `file:${specPath}`,
1706
- startLine: lineNumber,
1707
- endLine: lineNumber
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 += `${jql ? " AND " : ""}status IN (${filters.status.map((s) => `"${s}"`).join(",")})`;
2081
+ jql = appendJqlClause(jql, `status IN (${filters.status.map((s) => `"${s}"`).join(",")})`);
1931
2082
  }
1932
2083
  if (filters?.labels?.length) {
1933
- jql += `${jql ? " AND " : ""}labels IN (${filters.labels.map((l) => `"${l}"`).join(",")})`;
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
- let startAt = 0;
1975
- const maxResults = 50;
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
- nodesAdded,
1995
- edgesAdded,
2124
+ 0,
2125
+ 0,
1996
2126
  [`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
1997
2127
  start
1998
2128
  );
1999
2129
  }
2000
- return buildIngestResult(nodesAdded, edgesAdded, [], start);
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
- try {
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
- nodesAdded: 0,
2226
- nodesUpdated: 0,
2227
- edgesAdded: 0,
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 buildId = `build:${run.id}`;
2254
- const safeName = sanitizeExternalText(run.name, 200);
2255
- store.addNode({
2256
- id: buildId,
2257
- type: "build",
2258
- name: `${safeName} #${run.id}`,
2259
- metadata: {
2260
- source: "github-actions",
2261
- status: run.status,
2262
- conclusion: run.conclusion,
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, allNodes.length);
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
- const hasSemanticScores = semanticScores.size > 0;
2384
- const kwWeight = hasSemanticScores ? this.keywordWeight : 1;
2385
- const semWeight = hasSemanticScores ? this.semanticWeight : 0;
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.sort((a, b) => b.score - a.score);
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 codeNode = this.store.getNode(edge.to);
2460
- if (!codeNode) {
2633
+ const result = this.classifyDocEdge(edge);
2634
+ if (result.kind === "missing") {
2461
2635
  missingTargets.push(edge.to);
2462
- continue;
2636
+ } else if (result.kind === "fresh") {
2637
+ freshEdges++;
2638
+ } else {
2639
+ staleEdges.push(result.entry);
2463
2640
  }
2464
- const docNode = this.store.getNode(edge.from);
2465
- const codeLastModified = codeNode.lastModified;
2466
- const docLastModified = docNode?.lastModified;
2467
- if (codeLastModified && docLastModified) {
2468
- if (codeLastModified > docLastModified) {
2469
- staleEdges.push({
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
- } else {
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 { staleEdges, missingTargets, freshEdges };
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 requestedMetrics = options?.metrics ?? [...DEFAULT_METRICS];
2747
- const warnings = [];
2748
- const metricsToAnalyze = [];
2749
- for (const m of requestedMetrics) {
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 outlierNodeIds = new Set(allOutliers.map((o) => o.nodeId));
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 = /* @__PURE__ */ new Set();
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
- quotedConsumed.add(inner);
3554
+ consumed.add(inner);
3352
3555
  }
3353
3556
  }
3354
- const casingConsumed = /* @__PURE__ */ new Set();
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
- casingConsumed.add(token);
3566
+ consumed.add(token);
3360
3567
  }
3361
3568
  }
3362
- const pathConsumed = /* @__PURE__ */ new Set();
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
- pathConsumed.add(path7);
3577
+ consumed.add(path7);
3367
3578
  }
3368
- const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
3369
- const words = trimmed.split(/\s+/);
3370
- for (const raw of words) {
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 rawEntities = extractor.extract(question);
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
- const rootId = entities[0].nodeId;
3850
- const lowerQuestion = question.toLowerCase();
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: [rootId],
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
- const searchResults = fusion.search(question, 10);
3876
- const contextBlocks = [];
3877
- const rootIds = entities.length > 0 ? [entities[0].nodeId] : searchResults.slice(0, 3).map((r) => r.nodeId);
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
- let chars = (node.name?.length ?? 0) + (node.path?.length ?? 0) + (node.type?.length ?? 0);
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 allNodes) {
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 moduleNodes = this.store.findNodes({ type: "module" });
4014
- for (const mod of moduleNodes) {
4015
- const outEdges = this.store.getEdges({ from: mod.id });
4016
- const inEdges = this.store.getEdges({ to: mod.id });
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
- const boostTypes = phase ? PHASE_NODE_TYPES[phase] : void 0;
4020
- const boostFactor = 2;
4021
- let weightedTotal = 0;
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 isBoosted = boostTypes?.includes(type);
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 > 0) {
4031
- let allocated = 0;
4032
- const types = Object.keys(weights);
4033
- for (let i = 0; i < types.length; i++) {
4034
- const type = types[i];
4035
- if (i === types.length - 1) {
4036
- allocations[type] = totalTokens - allocated;
4037
- } else {
4038
- const share = Math.round(weights[type] / weightedTotal * totalTokens);
4039
- allocations[type] = share;
4040
- allocated += share;
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
- * Generate a markdown repository map from graph structure.
4076
- */
4077
- generateMap() {
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
- const lines = ["# Repository Structure", ""];
4086
- if (modulesWithEdgeCount.length > 0) {
4087
- lines.push("## Modules", "");
4088
- for (const { module: mod, edgeCount } of modulesWithEdgeCount) {
4089
- lines.push(`### ${mod.name} (${edgeCount} connections)`);
4090
- lines.push("");
4091
- const containsEdges = this.store.getEdges({ from: mod.id, type: "contains" });
4092
- for (const edge of containsEdges) {
4093
- const fileNode = this.store.getNode(edge.to);
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
- const fileNodes = this.store.findNodes({ type: "file" });
4103
- const nonBarrelFiles = fileNodes.filter((n) => !n.name.startsWith("index."));
4104
- const filesWithOutDegree = nonBarrelFiles.map((f) => {
4105
- const outEdges = this.store.getEdges({ from: f.id });
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 > 0) {
4111
- lines.push("## Entry Points", "");
4112
- for (const { file, outDegree } of entryPoints) {
4113
- lines.push(`- ${file.path ?? file.name} (${outDegree} outbound edges)`);
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 fileNodes = this.store.findNodes({ type: "file" });
4251
- const nodes = fileNodes.map((n) => n.path ?? n.id);
4252
- const importsEdges = this.store.getEdges({ type: "imports" });
4253
- const edges = importsEdges.map((e) => {
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 fileNodes = this.store.findNodes({ path: filePath });
4557
- if (fileNodes.length === 0) continue;
4558
- const fileNode = fileNodes[0];
4559
- const inboundImports = this.store.getEdges({ to: fileNode.id, type: "imports" });
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
- computeHarnessCheckData() {
4584
- const nodeCount = this.store.nodeCount;
4585
- const edgeCount = this.store.edgeCount;
4586
- const violatesEdges = this.store.getEdges({ type: "violates" });
4587
- const constraintViolations = violatesEdges.length;
4588
- const fileNodes = this.store.findNodes({ type: "file" });
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
- let unreachableNodes = 0;
4597
- for (const node of fileNodes) {
4598
- const inboundImports = this.store.getEdges({ to: node.id, type: "imports" });
4599
- if (inboundImports.length === 0) {
4600
- const isEntryPoint = node.name === "index.ts" || node.path !== void 0 && node.path.endsWith("/index.ts") || node.metadata?.entryPoint === true;
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
- const origSet = new Set(task.files);
4635
- originalFiles.set(task.id, origSet);
4636
- if (useGraph) {
4637
- const expanded = this.expandViaGraph(task.files, depth, edgeTypes);
4638
- expandedFiles.set(task.id, expanded);
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
- const taskIds = tasks.map((t) => t.id);
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
- const pair = this.computePairOverlap(
4650
- idA,
4651
- idB,
4652
- originalFiles.get(idA),
4653
- originalFiles.get(idB),
4654
- expandedFiles.get(idA),
4655
- expandedFiles.get(idB)
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
- const groups = this.buildGroups(taskIds, pairs);
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
- let churnThreshold = Infinity;
4827
- let couplingThreshold = Infinity;
4828
- if (this.store != null) {
4829
- const complexityResult = new GraphComplexityAdapter(this.store).computeComplexityHotspots();
4830
- for (const hotspot of complexityResult.hotspots) {
4831
- const existing = churnMap.get(hotspot.file);
4832
- if (existing === void 0 || hotspot.changeFrequency > existing) {
4833
- churnMap.set(hotspot.file, hotspot.changeFrequency);
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 result.pairs) {
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
- const taskIds = result.tasks;
4865
- const groups = this.buildHighSeverityGroups(taskIds, conflicts);
4866
- const regrouped = !this.groupsEqual(result.groups, groups);
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
- const verdict = this.generateVerdict(
4876
- taskIds,
4877
- groups,
4878
- result.analysisLevel,
4879
- highCount,
4880
- mediumCount,
4881
- lowCount,
4882
- regrouped
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
- tasks: taskIds,
4886
- analysisLevel: result.analysisLevel,
4887
- depth: result.depth,
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
- let overlapSeverity;
4906
- let reason;
4907
- let mitigation;
4908
- if (overlap.type === "direct") {
4909
- overlapSeverity = "high";
4910
- reason = `Both tasks write to ${overlap.file}`;
4911
- mitigation = `Serialize: run ${taskA} before ${taskB}`;
4912
- } else {
4913
- const churn = churnMap.get(overlap.file);
4914
- const coupling = couplingMap.get(overlap.file);
4915
- const via = overlap.via ?? "unknown";
4916
- if (churn !== void 0 && churn >= churnThreshold && churnThreshold !== Infinity) {
4917
- overlapSeverity = "medium";
4918
- reason = `Transitive overlap on high-churn file ${overlap.file} (via ${via})`;
4919
- mitigation = `Review: ${overlap.file} changes frequently \u2014 coordinate edits between ${taskA} and ${taskB}`;
4920
- } else if (coupling !== void 0 && coupling >= couplingThreshold && couplingThreshold !== Infinity) {
4921
- overlapSeverity = "medium";
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.0";
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