@harness-engineering/graph 0.3.5 → 0.4.1

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
@@ -33,7 +33,9 @@ __export(index_exports, {
33
33
  Assembler: () => Assembler,
34
34
  CIConnector: () => CIConnector,
35
35
  CURRENT_SCHEMA_VERSION: () => CURRENT_SCHEMA_VERSION,
36
+ CascadeSimulator: () => CascadeSimulator,
36
37
  CodeIngestor: () => CodeIngestor,
38
+ CompositeProbabilityStrategy: () => CompositeProbabilityStrategy,
37
39
  ConflictPredictor: () => ConflictPredictor,
38
40
  ConfluenceConnector: () => ConfluenceConnector,
39
41
  ContextQL: () => ContextQL,
@@ -59,6 +61,7 @@ __export(index_exports, {
59
61
  KnowledgeIngestor: () => KnowledgeIngestor,
60
62
  NODE_TYPES: () => NODE_TYPES,
61
63
  OBSERVABILITY_TYPES: () => OBSERVABILITY_TYPES,
64
+ RequirementIngestor: () => RequirementIngestor,
62
65
  ResponseFormatter: () => ResponseFormatter,
63
66
  SlackConnector: () => SlackConnector,
64
67
  SyncManager: () => SyncManager,
@@ -67,10 +70,12 @@ __export(index_exports, {
67
70
  VERSION: () => VERSION,
68
71
  VectorStore: () => VectorStore,
69
72
  askGraph: () => askGraph,
73
+ classifyNodeCategory: () => classifyNodeCategory,
70
74
  groupNodesByImpact: () => groupNodesByImpact,
71
75
  linkToCode: () => linkToCode,
72
76
  loadGraph: () => loadGraph,
73
77
  project: () => project,
78
+ queryTraceability: () => queryTraceability,
74
79
  saveGraph: () => saveGraph
75
80
  });
76
81
  module.exports = __toCommonJS(index_exports);
@@ -112,7 +117,9 @@ var NODE_TYPES = [
112
117
  // Design
113
118
  "design_token",
114
119
  "aesthetic_intent",
115
- "design_constraint"
120
+ "design_constraint",
121
+ // Traceability
122
+ "requirement"
116
123
  ];
117
124
  var EDGE_TYPES = [
118
125
  // Code relationships
@@ -141,7 +148,11 @@ var EDGE_TYPES = [
141
148
  "uses_token",
142
149
  "declares_intent",
143
150
  "violates_design",
144
- "platform_binding"
151
+ "platform_binding",
152
+ // Traceability relationships
153
+ "requires",
154
+ "verified_by",
155
+ "tested_by"
145
156
  ];
146
157
  var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
147
158
  var CURRENT_SCHEMA_VERSION = 1;
@@ -233,6 +244,16 @@ function removeFromIndex(index, key, edge) {
233
244
  if (idx !== -1) list.splice(idx, 1);
234
245
  if (list.length === 0) index.delete(key);
235
246
  }
247
+ function filterEdges(candidates, query) {
248
+ const results = [];
249
+ for (const edge of candidates) {
250
+ if (query.from !== void 0 && edge.from !== query.from) continue;
251
+ if (query.to !== void 0 && edge.to !== query.to) continue;
252
+ if (query.type !== void 0 && edge.type !== query.type) continue;
253
+ results.push({ ...edge });
254
+ }
255
+ return results;
256
+ }
236
257
  var GraphStore = class {
237
258
  nodeMap = /* @__PURE__ */ new Map();
238
259
  edgeMap = /* @__PURE__ */ new Map();
@@ -300,27 +321,25 @@ var GraphStore = class {
300
321
  }
301
322
  }
302
323
  getEdges(query) {
303
- let candidates;
304
324
  if (query.from !== void 0 && query.to !== void 0 && query.type !== void 0) {
305
325
  const edge = this.edgeMap.get(edgeKey(query.from, query.to, query.type));
306
326
  return edge ? [{ ...edge }] : [];
307
- } else if (query.from !== void 0) {
308
- candidates = this.edgesByFrom.get(query.from) ?? [];
309
- } else if (query.to !== void 0) {
310
- candidates = this.edgesByTo.get(query.to) ?? [];
311
- } else if (query.type !== void 0) {
312
- candidates = this.edgesByType.get(query.type) ?? [];
313
- } else {
314
- candidates = this.edgeMap.values();
315
327
  }
316
- const results = [];
317
- for (const edge of candidates) {
318
- if (query.from !== void 0 && edge.from !== query.from) continue;
319
- if (query.to !== void 0 && edge.to !== query.to) continue;
320
- if (query.type !== void 0 && edge.type !== query.type) continue;
321
- results.push({ ...edge });
328
+ const candidates = this.selectCandidates(query);
329
+ return filterEdges(candidates, query);
330
+ }
331
+ /** Pick the most selective index to start from. */
332
+ selectCandidates(query) {
333
+ if (query.from !== void 0) {
334
+ return this.edgesByFrom.get(query.from) ?? [];
322
335
  }
323
- return results;
336
+ if (query.to !== void 0) {
337
+ return this.edgesByTo.get(query.to) ?? [];
338
+ }
339
+ if (query.type !== void 0) {
340
+ return this.edgesByType.get(query.type) ?? [];
341
+ }
342
+ return this.edgeMap.values();
324
343
  }
325
344
  getNeighbors(nodeId, direction = "both") {
326
345
  const neighborIds = /* @__PURE__ */ new Set();
@@ -616,6 +635,12 @@ var CODE_TYPES = /* @__PURE__ */ new Set([
616
635
  "method",
617
636
  "variable"
618
637
  ]);
638
+ function classifyNodeCategory(node) {
639
+ if (TEST_TYPES.has(node.type)) return "tests";
640
+ if (DOC_TYPES.has(node.type)) return "docs";
641
+ if (CODE_TYPES.has(node.type)) return "code";
642
+ return "other";
643
+ }
619
644
  function groupNodesByImpact(nodes, excludeId) {
620
645
  const tests = [];
621
646
  const docs = [];
@@ -623,15 +648,11 @@ function groupNodesByImpact(nodes, excludeId) {
623
648
  const other = [];
624
649
  for (const node of nodes) {
625
650
  if (excludeId && node.id === excludeId) continue;
626
- if (TEST_TYPES.has(node.type)) {
627
- tests.push(node);
628
- } else if (DOC_TYPES.has(node.type)) {
629
- docs.push(node);
630
- } else if (CODE_TYPES.has(node.type)) {
631
- code.push(node);
632
- } else {
633
- other.push(node);
634
- }
651
+ const category = classifyNodeCategory(node);
652
+ if (category === "tests") tests.push(node);
653
+ else if (category === "docs") docs.push(node);
654
+ else if (category === "code") code.push(node);
655
+ else other.push(node);
635
656
  }
636
657
  return { tests, docs, code, other };
637
658
  }
@@ -675,6 +696,7 @@ var CodeIngestor = class {
675
696
  this.store.addEdge(edge);
676
697
  edgesAdded++;
677
698
  }
699
+ edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
678
700
  return {
679
701
  nodesAdded,
680
702
  nodesUpdated: 0,
@@ -1033,6 +1055,48 @@ var CodeIngestor = class {
1033
1055
  if (/\.jsx?$/.test(filePath)) return "javascript";
1034
1056
  return "unknown";
1035
1057
  }
1058
+ /**
1059
+ * Scan file contents for @req annotations and create verified_by edges
1060
+ * linking requirement nodes to the annotated files.
1061
+ * Format: // @req <feature-name>#<index>
1062
+ */
1063
+ extractReqAnnotations(fileContents, rootDir) {
1064
+ const REQ_TAG = /\/\/\s*@req\s+([\w-]+)#(\d+)/g;
1065
+ const reqNodes = this.store.findNodes({ type: "requirement" });
1066
+ let edgesAdded = 0;
1067
+ for (const [filePath, content] of fileContents) {
1068
+ let match;
1069
+ REQ_TAG.lastIndex = 0;
1070
+ while ((match = REQ_TAG.exec(content)) !== null) {
1071
+ const featureName = match[1];
1072
+ const reqIndex = parseInt(match[2], 10);
1073
+ const reqNode = reqNodes.find(
1074
+ (n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
1075
+ );
1076
+ if (!reqNode) {
1077
+ console.warn(
1078
+ `@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
1079
+ );
1080
+ continue;
1081
+ }
1082
+ const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
1083
+ const fileNodeId = `file:${relPath}`;
1084
+ this.store.addEdge({
1085
+ from: reqNode.id,
1086
+ to: fileNodeId,
1087
+ type: "verified_by",
1088
+ confidence: 1,
1089
+ metadata: {
1090
+ method: "annotation",
1091
+ tag: `@req ${featureName}#${reqIndex}`,
1092
+ confidence: 1
1093
+ }
1094
+ });
1095
+ edgesAdded++;
1096
+ }
1097
+ }
1098
+ return edgesAdded;
1099
+ }
1036
1100
  };
1037
1101
 
1038
1102
  // src/ingest/GitIngestor.ts
@@ -1509,8 +1573,218 @@ var KnowledgeIngestor = class {
1509
1573
  }
1510
1574
  };
1511
1575
 
1512
- // src/ingest/connectors/ConnectorUtils.ts
1576
+ // src/ingest/RequirementIngestor.ts
1577
+ var fs3 = __toESM(require("fs/promises"));
1578
+ var path4 = __toESM(require("path"));
1579
+ var REQUIREMENT_SECTIONS = [
1580
+ "Observable Truths",
1581
+ "Success Criteria",
1582
+ "Acceptance Criteria"
1583
+ ];
1584
+ var SECTION_HEADING_RE = /^#{2,3}\s+(.+)$/;
1585
+ var NUMBERED_ITEM_RE = /^\s*(\d+)\.\s+(.+)$/;
1586
+ function detectEarsPattern(text) {
1587
+ const lower = text.toLowerCase();
1588
+ if (/^if\b.+\bthen\b.+\bshall not\b/.test(lower)) return "unwanted";
1589
+ if (/^when\b/.test(lower)) return "event-driven";
1590
+ if (/^while\b/.test(lower)) return "state-driven";
1591
+ if (/^where\b/.test(lower)) return "optional";
1592
+ if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
1593
+ return void 0;
1594
+ }
1513
1595
  var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
1596
+ var RequirementIngestor = class {
1597
+ constructor(store) {
1598
+ this.store = store;
1599
+ }
1600
+ store;
1601
+ /**
1602
+ * Scan a specs directory for `<feature>/proposal.md` files,
1603
+ * extract numbered requirements from recognized sections,
1604
+ * and create requirement nodes with convention-based edges.
1605
+ */
1606
+ async ingestSpecs(specsDir) {
1607
+ const start = Date.now();
1608
+ const errors = [];
1609
+ let nodesAdded = 0;
1610
+ let edgesAdded = 0;
1611
+ let featureDirs;
1612
+ try {
1613
+ const entries = await fs3.readdir(specsDir, { withFileTypes: true });
1614
+ featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path4.join(specsDir, e.name));
1615
+ } catch {
1616
+ return emptyResult(Date.now() - start);
1617
+ }
1618
+ 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
+ }
1653
+ }
1654
+ return {
1655
+ nodesAdded,
1656
+ nodesUpdated: 0,
1657
+ edgesAdded,
1658
+ edgesUpdated: 0,
1659
+ errors,
1660
+ durationMs: Date.now() - start
1661
+ };
1662
+ }
1663
+ /**
1664
+ * Parse markdown content and extract numbered items from recognized sections.
1665
+ */
1666
+ extractRequirements(content, specPath, specHash, featureName) {
1667
+ const lines = content.split("\n");
1668
+ const results = [];
1669
+ let currentSection;
1670
+ let inRequirementSection = false;
1671
+ let globalIndex = 0;
1672
+ for (let i = 0; i < lines.length; i++) {
1673
+ 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;
1685
+ }
1686
+ continue;
1687
+ }
1688
+ if (!inRequirementSection) continue;
1689
+ const itemMatch = line.match(NUMBERED_ITEM_RE);
1690
+ 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
+ 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
+ });
1719
+ }
1720
+ return results;
1721
+ }
1722
+ /**
1723
+ * Convention-based linking: match requirement to code/test files
1724
+ * by feature name in their path.
1725
+ */
1726
+ linkByPathPattern(reqId, featureName) {
1727
+ let count = 0;
1728
+ const fileNodes = this.store.findNodes({ type: "file" });
1729
+ for (const node of fileNodes) {
1730
+ if (!node.path) continue;
1731
+ const normalizedPath = node.path.replace(/\\/g, "/");
1732
+ const isCodeMatch = normalizedPath.includes("packages/") && path4.basename(normalizedPath).includes(featureName);
1733
+ const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
1734
+ path4.basename(normalizedPath).includes(featureName);
1735
+ if (isCodeMatch && !isTestMatch) {
1736
+ this.store.addEdge({
1737
+ from: reqId,
1738
+ to: node.id,
1739
+ type: "requires",
1740
+ confidence: 0.5,
1741
+ metadata: { method: "convention", matchReason: "path-pattern" }
1742
+ });
1743
+ count++;
1744
+ } else if (isTestMatch) {
1745
+ this.store.addEdge({
1746
+ from: reqId,
1747
+ to: node.id,
1748
+ type: "verified_by",
1749
+ confidence: 0.5,
1750
+ metadata: { method: "convention", matchReason: "path-pattern" }
1751
+ });
1752
+ count++;
1753
+ }
1754
+ }
1755
+ return count;
1756
+ }
1757
+ /**
1758
+ * Convention-based linking: match requirement text to code nodes
1759
+ * by keyword overlap (function/class names appearing in requirement text).
1760
+ */
1761
+ linkByKeywordOverlap(reqId, reqText) {
1762
+ let count = 0;
1763
+ for (const nodeType of CODE_NODE_TYPES2) {
1764
+ const codeNodes = this.store.findNodes({ type: nodeType });
1765
+ for (const node of codeNodes) {
1766
+ if (node.name.length < 3) continue;
1767
+ const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1768
+ const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
1769
+ if (namePattern.test(reqText)) {
1770
+ const edgeType = node.path?.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
1771
+ this.store.addEdge({
1772
+ from: reqId,
1773
+ to: node.id,
1774
+ type: edgeType,
1775
+ confidence: 0.6,
1776
+ metadata: { method: "convention", matchReason: "keyword-overlap" }
1777
+ });
1778
+ count++;
1779
+ }
1780
+ }
1781
+ }
1782
+ return count;
1783
+ }
1784
+ };
1785
+
1786
+ // src/ingest/connectors/ConnectorUtils.ts
1787
+ var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
1514
1788
  var SANITIZE_RULES = [
1515
1789
  // Strip XML/HTML-like instruction tags that could be interpreted as system prompts
1516
1790
  {
@@ -1545,7 +1819,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
1545
1819
  }
1546
1820
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
1547
1821
  let edgesCreated = 0;
1548
- for (const type of CODE_NODE_TYPES2) {
1822
+ for (const type of CODE_NODE_TYPES3) {
1549
1823
  const nodes = store.findNodes({ type });
1550
1824
  for (const node of nodes) {
1551
1825
  if (node.name.length < 3) continue;
@@ -1565,12 +1839,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
1565
1839
  }
1566
1840
 
1567
1841
  // src/ingest/connectors/SyncManager.ts
1568
- var fs3 = __toESM(require("fs/promises"));
1569
- var path4 = __toESM(require("path"));
1842
+ var fs4 = __toESM(require("fs/promises"));
1843
+ var path5 = __toESM(require("path"));
1570
1844
  var SyncManager = class {
1571
1845
  constructor(store, graphDir) {
1572
1846
  this.store = store;
1573
- this.metadataPath = path4.join(graphDir, "sync-metadata.json");
1847
+ this.metadataPath = path5.join(graphDir, "sync-metadata.json");
1574
1848
  }
1575
1849
  store;
1576
1850
  registrations = /* @__PURE__ */ new Map();
@@ -1625,15 +1899,15 @@ var SyncManager = class {
1625
1899
  }
1626
1900
  async loadMetadata() {
1627
1901
  try {
1628
- const raw = await fs3.readFile(this.metadataPath, "utf-8");
1902
+ const raw = await fs4.readFile(this.metadataPath, "utf-8");
1629
1903
  return JSON.parse(raw);
1630
1904
  } catch {
1631
1905
  return { connectors: {} };
1632
1906
  }
1633
1907
  }
1634
1908
  async saveMetadata(metadata) {
1635
- await fs3.mkdir(path4.dirname(this.metadataPath), { recursive: true });
1636
- await fs3.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
1909
+ await fs4.mkdir(path5.dirname(this.metadataPath), { recursive: true });
1910
+ await fs4.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
1637
1911
  }
1638
1912
  };
1639
1913
 
@@ -2162,7 +2436,7 @@ var FusionLayer = class {
2162
2436
  };
2163
2437
 
2164
2438
  // src/entropy/GraphEntropyAdapter.ts
2165
- var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
2439
+ var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
2166
2440
  var GraphEntropyAdapter = class {
2167
2441
  constructor(store) {
2168
2442
  this.store = store;
@@ -2229,7 +2503,7 @@ var GraphEntropyAdapter = class {
2229
2503
  }
2230
2504
  findEntryPoints() {
2231
2505
  const entryPoints = [];
2232
- for (const nodeType of CODE_NODE_TYPES3) {
2506
+ for (const nodeType of CODE_NODE_TYPES4) {
2233
2507
  const nodes = this.store.findNodes({ type: nodeType });
2234
2508
  for (const node of nodes) {
2235
2509
  const isIndexFile = nodeType === "file" && node.name === "index.ts";
@@ -2265,7 +2539,7 @@ var GraphEntropyAdapter = class {
2265
2539
  }
2266
2540
  collectUnreachableNodes(visited) {
2267
2541
  const unreachableNodes = [];
2268
- for (const nodeType of CODE_NODE_TYPES3) {
2542
+ for (const nodeType of CODE_NODE_TYPES4) {
2269
2543
  const nodes = this.store.findNodes({ type: nodeType });
2270
2544
  for (const node of nodes) {
2271
2545
  if (!visited.has(node.id)) {
@@ -2710,6 +2984,7 @@ var INTENT_SIGNALS = {
2710
2984
  "depend",
2711
2985
  "blast",
2712
2986
  "radius",
2987
+ "cascade",
2713
2988
  "risk",
2714
2989
  "delete",
2715
2990
  "remove"
@@ -2719,6 +2994,7 @@ var INTENT_SIGNALS = {
2719
2994
  /what\s+(breaks|happens|is affected)/,
2720
2995
  /if\s+i\s+(change|modify|remove|delete)/,
2721
2996
  /blast\s+radius/,
2997
+ /cascad/,
2722
2998
  /what\s+(depend|relies)/
2723
2999
  ]
2724
3000
  },
@@ -3085,9 +3361,9 @@ var EntityExtractor = class {
3085
3361
  }
3086
3362
  const pathConsumed = /* @__PURE__ */ new Set();
3087
3363
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
3088
- const path6 = match[0];
3089
- add(path6);
3090
- pathConsumed.add(path6);
3364
+ const path7 = match[0];
3365
+ add(path7);
3366
+ pathConsumed.add(path7);
3091
3367
  }
3092
3368
  const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
3093
3369
  const words = trimmed.split(/\s+/);
@@ -3158,8 +3434,8 @@ var EntityResolver = class {
3158
3434
  if (isPathLike && node.path.includes(raw)) {
3159
3435
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
3160
3436
  }
3161
- const basename4 = node.path.split("/").pop() ?? "";
3162
- if (basename4.includes(raw)) {
3437
+ const basename5 = node.path.split("/").pop() ?? "";
3438
+ if (basename5.includes(raw)) {
3163
3439
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
3164
3440
  }
3165
3441
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -3204,6 +3480,10 @@ var ResponseFormatter = class {
3204
3480
  }
3205
3481
  formatImpact(entityName, data) {
3206
3482
  const d = data;
3483
+ if ("sourceNodeId" in d && "summary" in d) {
3484
+ const summary = d.summary;
3485
+ return `Blast radius of **${entityName}**: ${summary.totalAffected} affected nodes (${summary.highRisk} high risk, ${summary.mediumRisk} medium, ${summary.lowRisk} low).`;
3486
+ }
3207
3487
  const code = this.safeArrayLength(d?.code);
3208
3488
  const tests = this.safeArrayLength(d?.tests);
3209
3489
  const docs = this.safeArrayLength(d?.docs);
@@ -3234,13 +3514,13 @@ var ResponseFormatter = class {
3234
3514
  const context = Array.isArray(d?.context) ? d.context : [];
3235
3515
  const firstEntity = entities[0];
3236
3516
  const nodeType = firstEntity?.node.type ?? "node";
3237
- const path6 = firstEntity?.node.path ?? "unknown";
3517
+ const path7 = firstEntity?.node.path ?? "unknown";
3238
3518
  let neighborCount = 0;
3239
3519
  const firstContext = context[0];
3240
3520
  if (firstContext && Array.isArray(firstContext.nodes)) {
3241
3521
  neighborCount = firstContext.nodes.length;
3242
3522
  }
3243
- return `**${entityName}** is a ${nodeType} at \`${path6}\`. Connected to ${neighborCount} nodes.`;
3523
+ return `**${entityName}** is a ${nodeType} at \`${path7}\`. Connected to ${neighborCount} nodes.`;
3244
3524
  }
3245
3525
  formatAnomaly(data) {
3246
3526
  const d = data;
@@ -3265,6 +3545,246 @@ var ResponseFormatter = class {
3265
3545
  }
3266
3546
  };
3267
3547
 
3548
+ // src/blast-radius/CompositeProbabilityStrategy.ts
3549
+ var CompositeProbabilityStrategy = class _CompositeProbabilityStrategy {
3550
+ constructor(changeFreqMap, couplingMap) {
3551
+ this.changeFreqMap = changeFreqMap;
3552
+ this.couplingMap = couplingMap;
3553
+ }
3554
+ changeFreqMap;
3555
+ couplingMap;
3556
+ static BASE_WEIGHTS = {
3557
+ imports: 0.7,
3558
+ calls: 0.5,
3559
+ implements: 0.6,
3560
+ inherits: 0.6,
3561
+ co_changes_with: 0.4,
3562
+ references: 0.2,
3563
+ contains: 0.3
3564
+ };
3565
+ static FALLBACK_WEIGHT = 0.1;
3566
+ static EDGE_TYPE_BLEND = 0.5;
3567
+ static CHANGE_FREQ_BLEND = 0.3;
3568
+ static COUPLING_BLEND = 0.2;
3569
+ getEdgeProbability(edge, _fromNode, toNode) {
3570
+ const base = _CompositeProbabilityStrategy.BASE_WEIGHTS[edge.type] ?? _CompositeProbabilityStrategy.FALLBACK_WEIGHT;
3571
+ const changeFreq = this.changeFreqMap.get(toNode.id) ?? 0;
3572
+ const coupling = this.couplingMap.get(toNode.id) ?? 0;
3573
+ return Math.min(
3574
+ 1,
3575
+ base * _CompositeProbabilityStrategy.EDGE_TYPE_BLEND + changeFreq * _CompositeProbabilityStrategy.CHANGE_FREQ_BLEND + coupling * _CompositeProbabilityStrategy.COUPLING_BLEND
3576
+ );
3577
+ }
3578
+ };
3579
+
3580
+ // src/blast-radius/CascadeSimulator.ts
3581
+ var DEFAULT_PROBABILITY_FLOOR = 0.05;
3582
+ var DEFAULT_MAX_DEPTH = 10;
3583
+ var CascadeSimulator = class {
3584
+ constructor(store) {
3585
+ this.store = store;
3586
+ }
3587
+ store;
3588
+ simulate(sourceNodeId, options = {}) {
3589
+ const sourceNode = this.store.getNode(sourceNodeId);
3590
+ if (!sourceNode) {
3591
+ throw new Error(`Node not found: ${sourceNodeId}. Ensure the file has been ingested.`);
3592
+ }
3593
+ const probabilityFloor = options.probabilityFloor ?? DEFAULT_PROBABILITY_FLOOR;
3594
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
3595
+ const edgeTypeFilter = options.edgeTypes ? new Set(options.edgeTypes) : null;
3596
+ const strategy = options.strategy ?? this.buildDefaultStrategy();
3597
+ const visited = /* @__PURE__ */ new Map();
3598
+ const queue = [];
3599
+ const fanOutCount = /* @__PURE__ */ new Map();
3600
+ this.seedQueue(
3601
+ sourceNodeId,
3602
+ sourceNode,
3603
+ strategy,
3604
+ edgeTypeFilter,
3605
+ probabilityFloor,
3606
+ queue,
3607
+ fanOutCount
3608
+ );
3609
+ const truncated = this.runBfs(
3610
+ queue,
3611
+ visited,
3612
+ fanOutCount,
3613
+ sourceNodeId,
3614
+ strategy,
3615
+ edgeTypeFilter,
3616
+ probabilityFloor,
3617
+ maxDepth
3618
+ );
3619
+ return this.buildResult(sourceNodeId, sourceNode.name, visited, fanOutCount, truncated);
3620
+ }
3621
+ seedQueue(sourceNodeId, sourceNode, strategy, edgeTypeFilter, probabilityFloor, queue, fanOutCount) {
3622
+ const sourceEdges = this.store.getEdges({ from: sourceNodeId });
3623
+ for (const edge of sourceEdges) {
3624
+ if (edge.to === sourceNodeId) continue;
3625
+ if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
3626
+ const targetNode = this.store.getNode(edge.to);
3627
+ if (!targetNode) continue;
3628
+ const cumProb = strategy.getEdgeProbability(edge, sourceNode, targetNode);
3629
+ if (cumProb < probabilityFloor) continue;
3630
+ queue.push({
3631
+ nodeId: edge.to,
3632
+ cumProb,
3633
+ depth: 1,
3634
+ parentId: sourceNodeId,
3635
+ incomingEdge: edge.type
3636
+ });
3637
+ }
3638
+ fanOutCount.set(
3639
+ sourceNodeId,
3640
+ sourceEdges.filter(
3641
+ (e) => e.to !== sourceNodeId && (!edgeTypeFilter || edgeTypeFilter.has(e.type))
3642
+ ).length
3643
+ );
3644
+ }
3645
+ runBfs(queue, visited, fanOutCount, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, maxDepth) {
3646
+ const MAX_QUEUE_SIZE = 1e4;
3647
+ let head = 0;
3648
+ while (head < queue.length) {
3649
+ if (queue.length > MAX_QUEUE_SIZE) return true;
3650
+ const entry = queue[head++];
3651
+ const existing = visited.get(entry.nodeId);
3652
+ if (existing && existing.cumulativeProbability >= entry.cumProb) continue;
3653
+ const targetNode = this.store.getNode(entry.nodeId);
3654
+ if (!targetNode) continue;
3655
+ visited.set(entry.nodeId, {
3656
+ nodeId: entry.nodeId,
3657
+ name: targetNode.name,
3658
+ ...targetNode.path !== void 0 && { path: targetNode.path },
3659
+ type: targetNode.type,
3660
+ cumulativeProbability: entry.cumProb,
3661
+ depth: entry.depth,
3662
+ incomingEdge: entry.incomingEdge,
3663
+ parentId: entry.parentId
3664
+ });
3665
+ if (entry.depth < maxDepth) {
3666
+ const childCount = this.expandNode(
3667
+ entry,
3668
+ targetNode,
3669
+ sourceNodeId,
3670
+ strategy,
3671
+ edgeTypeFilter,
3672
+ probabilityFloor,
3673
+ queue
3674
+ );
3675
+ fanOutCount.set(entry.nodeId, (fanOutCount.get(entry.nodeId) ?? 0) + childCount);
3676
+ }
3677
+ }
3678
+ return false;
3679
+ }
3680
+ expandNode(entry, fromNode, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, queue) {
3681
+ const outEdges = this.store.getEdges({ from: entry.nodeId });
3682
+ let childCount = 0;
3683
+ for (const edge of outEdges) {
3684
+ if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
3685
+ if (edge.to === sourceNodeId) continue;
3686
+ const childNode = this.store.getNode(edge.to);
3687
+ if (!childNode) continue;
3688
+ const newCumProb = entry.cumProb * strategy.getEdgeProbability(edge, fromNode, childNode);
3689
+ if (newCumProb < probabilityFloor) continue;
3690
+ childCount++;
3691
+ queue.push({
3692
+ nodeId: edge.to,
3693
+ cumProb: newCumProb,
3694
+ depth: entry.depth + 1,
3695
+ parentId: entry.nodeId,
3696
+ incomingEdge: edge.type
3697
+ });
3698
+ }
3699
+ return childCount;
3700
+ }
3701
+ buildDefaultStrategy() {
3702
+ return new CompositeProbabilityStrategy(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
3703
+ }
3704
+ buildResult(sourceNodeId, sourceName, visited, fanOutCount, truncated = false) {
3705
+ if (visited.size === 0) {
3706
+ return {
3707
+ sourceNodeId,
3708
+ sourceName,
3709
+ layers: [],
3710
+ flatSummary: [],
3711
+ summary: {
3712
+ totalAffected: 0,
3713
+ maxDepthReached: 0,
3714
+ highRisk: 0,
3715
+ mediumRisk: 0,
3716
+ lowRisk: 0,
3717
+ categoryBreakdown: { code: 0, tests: 0, docs: 0, other: 0 },
3718
+ amplificationPoints: [],
3719
+ truncated
3720
+ }
3721
+ };
3722
+ }
3723
+ const allNodes = Array.from(visited.values());
3724
+ const flatSummary = [...allNodes].sort(
3725
+ (a, b) => b.cumulativeProbability - a.cumulativeProbability
3726
+ );
3727
+ const depthMap = /* @__PURE__ */ new Map();
3728
+ for (const node of allNodes) {
3729
+ let list = depthMap.get(node.depth);
3730
+ if (!list) {
3731
+ list = [];
3732
+ depthMap.set(node.depth, list);
3733
+ }
3734
+ list.push(node);
3735
+ }
3736
+ const layers = [];
3737
+ const depths = Array.from(depthMap.keys()).sort((a, b) => a - b);
3738
+ for (const depth of depths) {
3739
+ const nodes = depthMap.get(depth);
3740
+ const breakdown = { code: 0, tests: 0, docs: 0, other: 0 };
3741
+ for (const n of nodes) {
3742
+ const graphNode = this.store.getNode(n.nodeId);
3743
+ if (graphNode) {
3744
+ breakdown[classifyNodeCategory(graphNode)]++;
3745
+ }
3746
+ }
3747
+ layers.push({ depth, nodes, categoryBreakdown: breakdown });
3748
+ }
3749
+ let highRisk = 0;
3750
+ let mediumRisk = 0;
3751
+ let lowRisk = 0;
3752
+ const catBreakdown = { code: 0, tests: 0, docs: 0, other: 0 };
3753
+ for (const node of allNodes) {
3754
+ if (node.cumulativeProbability >= 0.5) highRisk++;
3755
+ else if (node.cumulativeProbability >= 0.2) mediumRisk++;
3756
+ else lowRisk++;
3757
+ const graphNode = this.store.getNode(node.nodeId);
3758
+ if (graphNode) {
3759
+ catBreakdown[classifyNodeCategory(graphNode)]++;
3760
+ }
3761
+ }
3762
+ const amplificationPoints = [];
3763
+ for (const [nodeId, count] of fanOutCount) {
3764
+ if (count > 3) {
3765
+ amplificationPoints.push(nodeId);
3766
+ }
3767
+ }
3768
+ const maxDepthReached = allNodes.reduce((max, n) => Math.max(max, n.depth), 0);
3769
+ return {
3770
+ sourceNodeId,
3771
+ sourceName,
3772
+ layers,
3773
+ flatSummary,
3774
+ summary: {
3775
+ totalAffected: allNodes.length,
3776
+ maxDepthReached,
3777
+ highRisk,
3778
+ mediumRisk,
3779
+ lowRisk,
3780
+ categoryBreakdown: catBreakdown,
3781
+ amplificationPoints,
3782
+ truncated
3783
+ }
3784
+ };
3785
+ }
3786
+ };
3787
+
3268
3788
  // src/nlq/index.ts
3269
3789
  var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships", "explain"]);
3270
3790
  var classifier = new IntentClassifier();
@@ -3327,6 +3847,11 @@ function executeOperation(store, intent, entities, question, fusion) {
3327
3847
  switch (intent) {
3328
3848
  case "impact": {
3329
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
+ }
3330
3855
  const result = cql.execute({
3331
3856
  rootNodeIds: [rootId],
3332
3857
  bidirectional: true,
@@ -3381,7 +3906,7 @@ var PHASE_NODE_TYPES = {
3381
3906
  debug: ["failure", "learning", "function", "method"],
3382
3907
  plan: ["adr", "document", "module", "layer"]
3383
3908
  };
3384
- var CODE_NODE_TYPES4 = /* @__PURE__ */ new Set([
3909
+ var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
3385
3910
  "file",
3386
3911
  "function",
3387
3912
  "class",
@@ -3596,7 +4121,7 @@ var Assembler = class {
3596
4121
  */
3597
4122
  checkCoverage() {
3598
4123
  const codeNodes = [];
3599
- for (const type of CODE_NODE_TYPES4) {
4124
+ for (const type of CODE_NODE_TYPES5) {
3600
4125
  codeNodes.push(...this.store.findNodes({ type }));
3601
4126
  }
3602
4127
  const documented = [];
@@ -3620,6 +4145,99 @@ var Assembler = class {
3620
4145
  }
3621
4146
  };
3622
4147
 
4148
+ // src/query/Traceability.ts
4149
+ function extractConfidence(edge) {
4150
+ return edge.confidence ?? edge.metadata?.confidence ?? 0;
4151
+ }
4152
+ function extractMethod(edge) {
4153
+ return edge.metadata?.method ?? "convention";
4154
+ }
4155
+ function edgesToTracedFiles(store, edges) {
4156
+ return edges.map((edge) => ({
4157
+ path: store.getNode(edge.to)?.path ?? edge.to,
4158
+ confidence: extractConfidence(edge),
4159
+ method: extractMethod(edge)
4160
+ }));
4161
+ }
4162
+ function determineCoverageStatus(hasCode, hasTests) {
4163
+ if (hasCode && hasTests) return "full";
4164
+ if (hasCode) return "code-only";
4165
+ if (hasTests) return "test-only";
4166
+ return "none";
4167
+ }
4168
+ function computeMaxConfidence(codeFiles, testFiles) {
4169
+ const allConfidences = [
4170
+ ...codeFiles.map((f) => f.confidence),
4171
+ ...testFiles.map((f) => f.confidence)
4172
+ ];
4173
+ return allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
4174
+ }
4175
+ function buildRequirementCoverage(store, req) {
4176
+ const codeFiles = edgesToTracedFiles(store, store.getEdges({ from: req.id, type: "requires" }));
4177
+ const testFiles = edgesToTracedFiles(
4178
+ store,
4179
+ store.getEdges({ from: req.id, type: "verified_by" })
4180
+ );
4181
+ const hasCode = codeFiles.length > 0;
4182
+ const hasTests = testFiles.length > 0;
4183
+ return {
4184
+ requirementId: req.id,
4185
+ requirementName: req.name,
4186
+ index: req.metadata?.index ?? 0,
4187
+ codeFiles,
4188
+ testFiles,
4189
+ status: determineCoverageStatus(hasCode, hasTests),
4190
+ maxConfidence: computeMaxConfidence(codeFiles, testFiles)
4191
+ };
4192
+ }
4193
+ function computeSummary(requirements) {
4194
+ const total = requirements.length;
4195
+ const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
4196
+ const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
4197
+ const fullyTraced = requirements.filter((r) => r.status === "full").length;
4198
+ const untraceable = requirements.filter((r) => r.status === "none").length;
4199
+ const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
4200
+ return { total, withCode, withTests, fullyTraced, untraceable, coveragePercent };
4201
+ }
4202
+ function queryTraceability(store, options) {
4203
+ const allRequirements = store.findNodes({ type: "requirement" });
4204
+ const filtered = allRequirements.filter((node) => {
4205
+ if (options?.specPath && node.metadata?.specPath !== options.specPath) return false;
4206
+ if (options?.featureName && node.metadata?.featureName !== options.featureName) return false;
4207
+ return true;
4208
+ });
4209
+ if (filtered.length === 0) return [];
4210
+ const groups = /* @__PURE__ */ new Map();
4211
+ for (const req of filtered) {
4212
+ const meta = req.metadata;
4213
+ const specPath = meta?.specPath ?? "";
4214
+ const featureName = meta?.featureName ?? "";
4215
+ const key = `${specPath}\0${featureName}`;
4216
+ const list = groups.get(key);
4217
+ if (list) {
4218
+ list.push(req);
4219
+ } else {
4220
+ groups.set(key, [req]);
4221
+ }
4222
+ }
4223
+ const results = [];
4224
+ for (const [, reqs] of groups) {
4225
+ const firstReq = reqs[0];
4226
+ const firstMeta = firstReq.metadata;
4227
+ const specPath = firstMeta?.specPath ?? "";
4228
+ const featureName = firstMeta?.featureName ?? "";
4229
+ const requirements = reqs.map((req) => buildRequirementCoverage(store, req));
4230
+ requirements.sort((a, b) => a.index - b.index);
4231
+ results.push({
4232
+ specPath,
4233
+ featureName,
4234
+ requirements,
4235
+ summary: computeSummary(requirements)
4236
+ });
4237
+ }
4238
+ return results;
4239
+ }
4240
+
3623
4241
  // src/constraints/GraphConstraintAdapter.ts
3624
4242
  var import_minimatch = require("minimatch");
3625
4243
  var import_node_path2 = require("path");
@@ -3679,14 +4297,14 @@ var GraphConstraintAdapter = class {
3679
4297
  };
3680
4298
 
3681
4299
  // src/ingest/DesignIngestor.ts
3682
- var fs4 = __toESM(require("fs/promises"));
3683
- var path5 = __toESM(require("path"));
4300
+ var fs5 = __toESM(require("fs/promises"));
4301
+ var path6 = __toESM(require("path"));
3684
4302
  function isDTCGToken(obj) {
3685
4303
  return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
3686
4304
  }
3687
4305
  async function readFileOrNull(filePath) {
3688
4306
  try {
3689
- return await fs4.readFile(filePath, "utf-8");
4307
+ return await fs5.readFile(filePath, "utf-8");
3690
4308
  } catch {
3691
4309
  return null;
3692
4310
  }
@@ -3832,8 +4450,8 @@ var DesignIngestor = class {
3832
4450
  async ingestAll(designDir) {
3833
4451
  const start = Date.now();
3834
4452
  const [tokensResult, intentResult] = await Promise.all([
3835
- this.ingestTokens(path5.join(designDir, "tokens.json")),
3836
- this.ingestDesignIntent(path5.join(designDir, "DESIGN.md"))
4453
+ this.ingestTokens(path6.join(designDir, "tokens.json")),
4454
+ this.ingestDesignIntent(path6.join(designDir, "DESIGN.md"))
3837
4455
  ]);
3838
4456
  const merged = mergeResults(tokensResult, intentResult);
3839
4457
  return { ...merged, durationMs: Date.now() - start };
@@ -4082,10 +4700,10 @@ var TaskIndependenceAnalyzer = class {
4082
4700
  includeTypes: ["file"]
4083
4701
  });
4084
4702
  for (const n of queryResult.nodes) {
4085
- const path6 = n.path ?? n.id.replace(/^file:/, "");
4086
- if (!fileSet.has(path6)) {
4087
- if (!result.has(path6)) {
4088
- result.set(path6, file);
4703
+ const path7 = n.path ?? n.id.replace(/^file:/, "");
4704
+ if (!fileSet.has(path7)) {
4705
+ if (!result.has(path7)) {
4706
+ result.set(path7, file);
4089
4707
  }
4090
4708
  }
4091
4709
  }
@@ -4438,13 +5056,15 @@ var ConflictPredictor = class {
4438
5056
  };
4439
5057
 
4440
5058
  // src/index.ts
4441
- var VERSION = "0.2.0";
5059
+ var VERSION = "0.4.0";
4442
5060
  // Annotate the CommonJS export names for ESM import in node:
4443
5061
  0 && (module.exports = {
4444
5062
  Assembler,
4445
5063
  CIConnector,
4446
5064
  CURRENT_SCHEMA_VERSION,
5065
+ CascadeSimulator,
4447
5066
  CodeIngestor,
5067
+ CompositeProbabilityStrategy,
4448
5068
  ConflictPredictor,
4449
5069
  ConfluenceConnector,
4450
5070
  ContextQL,
@@ -4470,6 +5090,7 @@ var VERSION = "0.2.0";
4470
5090
  KnowledgeIngestor,
4471
5091
  NODE_TYPES,
4472
5092
  OBSERVABILITY_TYPES,
5093
+ RequirementIngestor,
4473
5094
  ResponseFormatter,
4474
5095
  SlackConnector,
4475
5096
  SyncManager,
@@ -4478,9 +5099,11 @@ var VERSION = "0.2.0";
4478
5099
  VERSION,
4479
5100
  VectorStore,
4480
5101
  askGraph,
5102
+ classifyNodeCategory,
4481
5103
  groupNodesByImpact,
4482
5104
  linkToCode,
4483
5105
  loadGraph,
4484
5106
  project,
5107
+ queryTraceability,
4485
5108
  saveGraph
4486
5109
  });