@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.mjs CHANGED
@@ -35,7 +35,9 @@ var NODE_TYPES = [
35
35
  // Design
36
36
  "design_token",
37
37
  "aesthetic_intent",
38
- "design_constraint"
38
+ "design_constraint",
39
+ // Traceability
40
+ "requirement"
39
41
  ];
40
42
  var EDGE_TYPES = [
41
43
  // Code relationships
@@ -64,7 +66,11 @@ var EDGE_TYPES = [
64
66
  "uses_token",
65
67
  "declares_intent",
66
68
  "violates_design",
67
- "platform_binding"
69
+ "platform_binding",
70
+ // Traceability relationships
71
+ "requires",
72
+ "verified_by",
73
+ "tested_by"
68
74
  ];
69
75
  var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
70
76
  var CURRENT_SCHEMA_VERSION = 1;
@@ -156,6 +162,16 @@ function removeFromIndex(index, key, edge) {
156
162
  if (idx !== -1) list.splice(idx, 1);
157
163
  if (list.length === 0) index.delete(key);
158
164
  }
165
+ function filterEdges(candidates, query) {
166
+ const results = [];
167
+ for (const edge of candidates) {
168
+ if (query.from !== void 0 && edge.from !== query.from) continue;
169
+ if (query.to !== void 0 && edge.to !== query.to) continue;
170
+ if (query.type !== void 0 && edge.type !== query.type) continue;
171
+ results.push({ ...edge });
172
+ }
173
+ return results;
174
+ }
159
175
  var GraphStore = class {
160
176
  nodeMap = /* @__PURE__ */ new Map();
161
177
  edgeMap = /* @__PURE__ */ new Map();
@@ -223,27 +239,25 @@ var GraphStore = class {
223
239
  }
224
240
  }
225
241
  getEdges(query) {
226
- let candidates;
227
242
  if (query.from !== void 0 && query.to !== void 0 && query.type !== void 0) {
228
243
  const edge = this.edgeMap.get(edgeKey(query.from, query.to, query.type));
229
244
  return edge ? [{ ...edge }] : [];
230
- } else if (query.from !== void 0) {
231
- candidates = this.edgesByFrom.get(query.from) ?? [];
232
- } else if (query.to !== void 0) {
233
- candidates = this.edgesByTo.get(query.to) ?? [];
234
- } else if (query.type !== void 0) {
235
- candidates = this.edgesByType.get(query.type) ?? [];
236
- } else {
237
- candidates = this.edgeMap.values();
238
245
  }
239
- const results = [];
240
- for (const edge of candidates) {
241
- if (query.from !== void 0 && edge.from !== query.from) continue;
242
- if (query.to !== void 0 && edge.to !== query.to) continue;
243
- if (query.type !== void 0 && edge.type !== query.type) continue;
244
- results.push({ ...edge });
246
+ const candidates = this.selectCandidates(query);
247
+ return filterEdges(candidates, query);
248
+ }
249
+ /** Pick the most selective index to start from. */
250
+ selectCandidates(query) {
251
+ if (query.from !== void 0) {
252
+ return this.edgesByFrom.get(query.from) ?? [];
245
253
  }
246
- return results;
254
+ if (query.to !== void 0) {
255
+ return this.edgesByTo.get(query.to) ?? [];
256
+ }
257
+ if (query.type !== void 0) {
258
+ return this.edgesByType.get(query.type) ?? [];
259
+ }
260
+ return this.edgeMap.values();
247
261
  }
248
262
  getNeighbors(nodeId, direction = "both") {
249
263
  const neighborIds = /* @__PURE__ */ new Set();
@@ -539,6 +553,12 @@ var CODE_TYPES = /* @__PURE__ */ new Set([
539
553
  "method",
540
554
  "variable"
541
555
  ]);
556
+ function classifyNodeCategory(node) {
557
+ if (TEST_TYPES.has(node.type)) return "tests";
558
+ if (DOC_TYPES.has(node.type)) return "docs";
559
+ if (CODE_TYPES.has(node.type)) return "code";
560
+ return "other";
561
+ }
542
562
  function groupNodesByImpact(nodes, excludeId) {
543
563
  const tests = [];
544
564
  const docs = [];
@@ -546,15 +566,11 @@ function groupNodesByImpact(nodes, excludeId) {
546
566
  const other = [];
547
567
  for (const node of nodes) {
548
568
  if (excludeId && node.id === excludeId) continue;
549
- if (TEST_TYPES.has(node.type)) {
550
- tests.push(node);
551
- } else if (DOC_TYPES.has(node.type)) {
552
- docs.push(node);
553
- } else if (CODE_TYPES.has(node.type)) {
554
- code.push(node);
555
- } else {
556
- other.push(node);
557
- }
569
+ const category = classifyNodeCategory(node);
570
+ if (category === "tests") tests.push(node);
571
+ else if (category === "docs") docs.push(node);
572
+ else if (category === "code") code.push(node);
573
+ else other.push(node);
558
574
  }
559
575
  return { tests, docs, code, other };
560
576
  }
@@ -598,6 +614,7 @@ var CodeIngestor = class {
598
614
  this.store.addEdge(edge);
599
615
  edgesAdded++;
600
616
  }
617
+ edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
601
618
  return {
602
619
  nodesAdded,
603
620
  nodesUpdated: 0,
@@ -956,6 +973,48 @@ var CodeIngestor = class {
956
973
  if (/\.jsx?$/.test(filePath)) return "javascript";
957
974
  return "unknown";
958
975
  }
976
+ /**
977
+ * Scan file contents for @req annotations and create verified_by edges
978
+ * linking requirement nodes to the annotated files.
979
+ * Format: // @req <feature-name>#<index>
980
+ */
981
+ extractReqAnnotations(fileContents, rootDir) {
982
+ const REQ_TAG = /\/\/\s*@req\s+([\w-]+)#(\d+)/g;
983
+ const reqNodes = this.store.findNodes({ type: "requirement" });
984
+ let edgesAdded = 0;
985
+ for (const [filePath, content] of fileContents) {
986
+ let match;
987
+ REQ_TAG.lastIndex = 0;
988
+ while ((match = REQ_TAG.exec(content)) !== null) {
989
+ const featureName = match[1];
990
+ const reqIndex = parseInt(match[2], 10);
991
+ const reqNode = reqNodes.find(
992
+ (n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
993
+ );
994
+ if (!reqNode) {
995
+ console.warn(
996
+ `@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
997
+ );
998
+ continue;
999
+ }
1000
+ const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
1001
+ const fileNodeId = `file:${relPath}`;
1002
+ this.store.addEdge({
1003
+ from: reqNode.id,
1004
+ to: fileNodeId,
1005
+ type: "verified_by",
1006
+ confidence: 1,
1007
+ metadata: {
1008
+ method: "annotation",
1009
+ tag: `@req ${featureName}#${reqIndex}`,
1010
+ confidence: 1
1011
+ }
1012
+ });
1013
+ edgesAdded++;
1014
+ }
1015
+ }
1016
+ return edgesAdded;
1017
+ }
959
1018
  };
960
1019
 
961
1020
  // src/ingest/GitIngestor.ts
@@ -1432,8 +1491,218 @@ var KnowledgeIngestor = class {
1432
1491
  }
1433
1492
  };
1434
1493
 
1435
- // src/ingest/connectors/ConnectorUtils.ts
1494
+ // src/ingest/RequirementIngestor.ts
1495
+ import * as fs3 from "fs/promises";
1496
+ import * as path4 from "path";
1497
+ var REQUIREMENT_SECTIONS = [
1498
+ "Observable Truths",
1499
+ "Success Criteria",
1500
+ "Acceptance Criteria"
1501
+ ];
1502
+ var SECTION_HEADING_RE = /^#{2,3}\s+(.+)$/;
1503
+ var NUMBERED_ITEM_RE = /^\s*(\d+)\.\s+(.+)$/;
1504
+ function detectEarsPattern(text) {
1505
+ const lower = text.toLowerCase();
1506
+ if (/^if\b.+\bthen\b.+\bshall not\b/.test(lower)) return "unwanted";
1507
+ if (/^when\b/.test(lower)) return "event-driven";
1508
+ if (/^while\b/.test(lower)) return "state-driven";
1509
+ if (/^where\b/.test(lower)) return "optional";
1510
+ if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
1511
+ return void 0;
1512
+ }
1436
1513
  var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
1514
+ var RequirementIngestor = class {
1515
+ constructor(store) {
1516
+ this.store = store;
1517
+ }
1518
+ store;
1519
+ /**
1520
+ * Scan a specs directory for `<feature>/proposal.md` files,
1521
+ * extract numbered requirements from recognized sections,
1522
+ * and create requirement nodes with convention-based edges.
1523
+ */
1524
+ async ingestSpecs(specsDir) {
1525
+ const start = Date.now();
1526
+ const errors = [];
1527
+ let nodesAdded = 0;
1528
+ let edgesAdded = 0;
1529
+ let featureDirs;
1530
+ try {
1531
+ const entries = await fs3.readdir(specsDir, { withFileTypes: true });
1532
+ featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path4.join(specsDir, e.name));
1533
+ } catch {
1534
+ return emptyResult(Date.now() - start);
1535
+ }
1536
+ for (const featureDir of featureDirs) {
1537
+ const featureName = path4.basename(featureDir);
1538
+ const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
1539
+ let content;
1540
+ try {
1541
+ content = await fs3.readFile(specPath, "utf-8");
1542
+ } catch {
1543
+ continue;
1544
+ }
1545
+ try {
1546
+ const specHash = hash(specPath);
1547
+ const specNodeId = `file:${specPath}`;
1548
+ this.store.addNode({
1549
+ id: specNodeId,
1550
+ type: "document",
1551
+ name: path4.basename(specPath),
1552
+ path: specPath,
1553
+ metadata: { featureName }
1554
+ });
1555
+ const requirements = this.extractRequirements(content, specPath, specHash, featureName);
1556
+ for (const req of requirements) {
1557
+ this.store.addNode(req.node);
1558
+ nodesAdded++;
1559
+ this.store.addEdge({
1560
+ from: req.node.id,
1561
+ to: specNodeId,
1562
+ type: "specifies"
1563
+ });
1564
+ edgesAdded++;
1565
+ edgesAdded += this.linkByPathPattern(req.node.id, featureName);
1566
+ edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
1567
+ }
1568
+ } catch (err) {
1569
+ errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
1570
+ }
1571
+ }
1572
+ return {
1573
+ nodesAdded,
1574
+ nodesUpdated: 0,
1575
+ edgesAdded,
1576
+ edgesUpdated: 0,
1577
+ errors,
1578
+ durationMs: Date.now() - start
1579
+ };
1580
+ }
1581
+ /**
1582
+ * Parse markdown content and extract numbered items from recognized sections.
1583
+ */
1584
+ extractRequirements(content, specPath, specHash, featureName) {
1585
+ const lines = content.split("\n");
1586
+ const results = [];
1587
+ let currentSection;
1588
+ let inRequirementSection = false;
1589
+ let globalIndex = 0;
1590
+ for (let i = 0; i < lines.length; i++) {
1591
+ const line = lines[i];
1592
+ const headingMatch = line.match(SECTION_HEADING_RE);
1593
+ if (headingMatch) {
1594
+ const heading = headingMatch[1].trim();
1595
+ const isReqSection = REQUIREMENT_SECTIONS.some(
1596
+ (s) => heading.toLowerCase() === s.toLowerCase()
1597
+ );
1598
+ if (isReqSection) {
1599
+ currentSection = heading;
1600
+ inRequirementSection = true;
1601
+ } else {
1602
+ inRequirementSection = false;
1603
+ }
1604
+ continue;
1605
+ }
1606
+ if (!inRequirementSection) continue;
1607
+ const itemMatch = line.match(NUMBERED_ITEM_RE);
1608
+ if (!itemMatch) continue;
1609
+ const index = parseInt(itemMatch[1], 10);
1610
+ const text = itemMatch[2].trim();
1611
+ const rawText = line.trim();
1612
+ const lineNumber = i + 1;
1613
+ globalIndex++;
1614
+ const nodeId = `req:${specHash}:${globalIndex}`;
1615
+ const earsPattern = detectEarsPattern(text);
1616
+ results.push({
1617
+ node: {
1618
+ id: nodeId,
1619
+ type: "requirement",
1620
+ name: text,
1621
+ path: specPath,
1622
+ location: {
1623
+ fileId: `file:${specPath}`,
1624
+ startLine: lineNumber,
1625
+ endLine: lineNumber
1626
+ },
1627
+ metadata: {
1628
+ specPath,
1629
+ index,
1630
+ section: currentSection,
1631
+ rawText,
1632
+ earsPattern,
1633
+ featureName
1634
+ }
1635
+ }
1636
+ });
1637
+ }
1638
+ return results;
1639
+ }
1640
+ /**
1641
+ * Convention-based linking: match requirement to code/test files
1642
+ * by feature name in their path.
1643
+ */
1644
+ linkByPathPattern(reqId, featureName) {
1645
+ let count = 0;
1646
+ const fileNodes = this.store.findNodes({ type: "file" });
1647
+ for (const node of fileNodes) {
1648
+ if (!node.path) continue;
1649
+ const normalizedPath = node.path.replace(/\\/g, "/");
1650
+ const isCodeMatch = normalizedPath.includes("packages/") && path4.basename(normalizedPath).includes(featureName);
1651
+ const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
1652
+ path4.basename(normalizedPath).includes(featureName);
1653
+ if (isCodeMatch && !isTestMatch) {
1654
+ this.store.addEdge({
1655
+ from: reqId,
1656
+ to: node.id,
1657
+ type: "requires",
1658
+ confidence: 0.5,
1659
+ metadata: { method: "convention", matchReason: "path-pattern" }
1660
+ });
1661
+ count++;
1662
+ } else if (isTestMatch) {
1663
+ this.store.addEdge({
1664
+ from: reqId,
1665
+ to: node.id,
1666
+ type: "verified_by",
1667
+ confidence: 0.5,
1668
+ metadata: { method: "convention", matchReason: "path-pattern" }
1669
+ });
1670
+ count++;
1671
+ }
1672
+ }
1673
+ return count;
1674
+ }
1675
+ /**
1676
+ * Convention-based linking: match requirement text to code nodes
1677
+ * by keyword overlap (function/class names appearing in requirement text).
1678
+ */
1679
+ linkByKeywordOverlap(reqId, reqText) {
1680
+ let count = 0;
1681
+ for (const nodeType of CODE_NODE_TYPES2) {
1682
+ const codeNodes = this.store.findNodes({ type: nodeType });
1683
+ for (const node of codeNodes) {
1684
+ if (node.name.length < 3) continue;
1685
+ const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1686
+ const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
1687
+ if (namePattern.test(reqText)) {
1688
+ const edgeType = node.path?.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
1689
+ this.store.addEdge({
1690
+ from: reqId,
1691
+ to: node.id,
1692
+ type: edgeType,
1693
+ confidence: 0.6,
1694
+ metadata: { method: "convention", matchReason: "keyword-overlap" }
1695
+ });
1696
+ count++;
1697
+ }
1698
+ }
1699
+ }
1700
+ return count;
1701
+ }
1702
+ };
1703
+
1704
+ // src/ingest/connectors/ConnectorUtils.ts
1705
+ var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
1437
1706
  var SANITIZE_RULES = [
1438
1707
  // Strip XML/HTML-like instruction tags that could be interpreted as system prompts
1439
1708
  {
@@ -1468,7 +1737,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
1468
1737
  }
1469
1738
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
1470
1739
  let edgesCreated = 0;
1471
- for (const type of CODE_NODE_TYPES2) {
1740
+ for (const type of CODE_NODE_TYPES3) {
1472
1741
  const nodes = store.findNodes({ type });
1473
1742
  for (const node of nodes) {
1474
1743
  if (node.name.length < 3) continue;
@@ -1488,12 +1757,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
1488
1757
  }
1489
1758
 
1490
1759
  // src/ingest/connectors/SyncManager.ts
1491
- import * as fs3 from "fs/promises";
1492
- import * as path4 from "path";
1760
+ import * as fs4 from "fs/promises";
1761
+ import * as path5 from "path";
1493
1762
  var SyncManager = class {
1494
1763
  constructor(store, graphDir) {
1495
1764
  this.store = store;
1496
- this.metadataPath = path4.join(graphDir, "sync-metadata.json");
1765
+ this.metadataPath = path5.join(graphDir, "sync-metadata.json");
1497
1766
  }
1498
1767
  store;
1499
1768
  registrations = /* @__PURE__ */ new Map();
@@ -1548,15 +1817,15 @@ var SyncManager = class {
1548
1817
  }
1549
1818
  async loadMetadata() {
1550
1819
  try {
1551
- const raw = await fs3.readFile(this.metadataPath, "utf-8");
1820
+ const raw = await fs4.readFile(this.metadataPath, "utf-8");
1552
1821
  return JSON.parse(raw);
1553
1822
  } catch {
1554
1823
  return { connectors: {} };
1555
1824
  }
1556
1825
  }
1557
1826
  async saveMetadata(metadata) {
1558
- await fs3.mkdir(path4.dirname(this.metadataPath), { recursive: true });
1559
- await fs3.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
1827
+ await fs4.mkdir(path5.dirname(this.metadataPath), { recursive: true });
1828
+ await fs4.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
1560
1829
  }
1561
1830
  };
1562
1831
 
@@ -2085,7 +2354,7 @@ var FusionLayer = class {
2085
2354
  };
2086
2355
 
2087
2356
  // src/entropy/GraphEntropyAdapter.ts
2088
- var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
2357
+ var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
2089
2358
  var GraphEntropyAdapter = class {
2090
2359
  constructor(store) {
2091
2360
  this.store = store;
@@ -2152,7 +2421,7 @@ var GraphEntropyAdapter = class {
2152
2421
  }
2153
2422
  findEntryPoints() {
2154
2423
  const entryPoints = [];
2155
- for (const nodeType of CODE_NODE_TYPES3) {
2424
+ for (const nodeType of CODE_NODE_TYPES4) {
2156
2425
  const nodes = this.store.findNodes({ type: nodeType });
2157
2426
  for (const node of nodes) {
2158
2427
  const isIndexFile = nodeType === "file" && node.name === "index.ts";
@@ -2188,7 +2457,7 @@ var GraphEntropyAdapter = class {
2188
2457
  }
2189
2458
  collectUnreachableNodes(visited) {
2190
2459
  const unreachableNodes = [];
2191
- for (const nodeType of CODE_NODE_TYPES3) {
2460
+ for (const nodeType of CODE_NODE_TYPES4) {
2192
2461
  const nodes = this.store.findNodes({ type: nodeType });
2193
2462
  for (const node of nodes) {
2194
2463
  if (!visited.has(node.id)) {
@@ -2633,6 +2902,7 @@ var INTENT_SIGNALS = {
2633
2902
  "depend",
2634
2903
  "blast",
2635
2904
  "radius",
2905
+ "cascade",
2636
2906
  "risk",
2637
2907
  "delete",
2638
2908
  "remove"
@@ -2642,6 +2912,7 @@ var INTENT_SIGNALS = {
2642
2912
  /what\s+(breaks|happens|is affected)/,
2643
2913
  /if\s+i\s+(change|modify|remove|delete)/,
2644
2914
  /blast\s+radius/,
2915
+ /cascad/,
2645
2916
  /what\s+(depend|relies)/
2646
2917
  ]
2647
2918
  },
@@ -3008,9 +3279,9 @@ var EntityExtractor = class {
3008
3279
  }
3009
3280
  const pathConsumed = /* @__PURE__ */ new Set();
3010
3281
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
3011
- const path6 = match[0];
3012
- add(path6);
3013
- pathConsumed.add(path6);
3282
+ const path7 = match[0];
3283
+ add(path7);
3284
+ pathConsumed.add(path7);
3014
3285
  }
3015
3286
  const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
3016
3287
  const words = trimmed.split(/\s+/);
@@ -3081,8 +3352,8 @@ var EntityResolver = class {
3081
3352
  if (isPathLike && node.path.includes(raw)) {
3082
3353
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
3083
3354
  }
3084
- const basename4 = node.path.split("/").pop() ?? "";
3085
- if (basename4.includes(raw)) {
3355
+ const basename5 = node.path.split("/").pop() ?? "";
3356
+ if (basename5.includes(raw)) {
3086
3357
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
3087
3358
  }
3088
3359
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -3127,6 +3398,10 @@ var ResponseFormatter = class {
3127
3398
  }
3128
3399
  formatImpact(entityName, data) {
3129
3400
  const d = data;
3401
+ if ("sourceNodeId" in d && "summary" in d) {
3402
+ const summary = d.summary;
3403
+ return `Blast radius of **${entityName}**: ${summary.totalAffected} affected nodes (${summary.highRisk} high risk, ${summary.mediumRisk} medium, ${summary.lowRisk} low).`;
3404
+ }
3130
3405
  const code = this.safeArrayLength(d?.code);
3131
3406
  const tests = this.safeArrayLength(d?.tests);
3132
3407
  const docs = this.safeArrayLength(d?.docs);
@@ -3157,13 +3432,13 @@ var ResponseFormatter = class {
3157
3432
  const context = Array.isArray(d?.context) ? d.context : [];
3158
3433
  const firstEntity = entities[0];
3159
3434
  const nodeType = firstEntity?.node.type ?? "node";
3160
- const path6 = firstEntity?.node.path ?? "unknown";
3435
+ const path7 = firstEntity?.node.path ?? "unknown";
3161
3436
  let neighborCount = 0;
3162
3437
  const firstContext = context[0];
3163
3438
  if (firstContext && Array.isArray(firstContext.nodes)) {
3164
3439
  neighborCount = firstContext.nodes.length;
3165
3440
  }
3166
- return `**${entityName}** is a ${nodeType} at \`${path6}\`. Connected to ${neighborCount} nodes.`;
3441
+ return `**${entityName}** is a ${nodeType} at \`${path7}\`. Connected to ${neighborCount} nodes.`;
3167
3442
  }
3168
3443
  formatAnomaly(data) {
3169
3444
  const d = data;
@@ -3188,6 +3463,246 @@ var ResponseFormatter = class {
3188
3463
  }
3189
3464
  };
3190
3465
 
3466
+ // src/blast-radius/CompositeProbabilityStrategy.ts
3467
+ var CompositeProbabilityStrategy = class _CompositeProbabilityStrategy {
3468
+ constructor(changeFreqMap, couplingMap) {
3469
+ this.changeFreqMap = changeFreqMap;
3470
+ this.couplingMap = couplingMap;
3471
+ }
3472
+ changeFreqMap;
3473
+ couplingMap;
3474
+ static BASE_WEIGHTS = {
3475
+ imports: 0.7,
3476
+ calls: 0.5,
3477
+ implements: 0.6,
3478
+ inherits: 0.6,
3479
+ co_changes_with: 0.4,
3480
+ references: 0.2,
3481
+ contains: 0.3
3482
+ };
3483
+ static FALLBACK_WEIGHT = 0.1;
3484
+ static EDGE_TYPE_BLEND = 0.5;
3485
+ static CHANGE_FREQ_BLEND = 0.3;
3486
+ static COUPLING_BLEND = 0.2;
3487
+ getEdgeProbability(edge, _fromNode, toNode) {
3488
+ const base = _CompositeProbabilityStrategy.BASE_WEIGHTS[edge.type] ?? _CompositeProbabilityStrategy.FALLBACK_WEIGHT;
3489
+ const changeFreq = this.changeFreqMap.get(toNode.id) ?? 0;
3490
+ const coupling = this.couplingMap.get(toNode.id) ?? 0;
3491
+ return Math.min(
3492
+ 1,
3493
+ base * _CompositeProbabilityStrategy.EDGE_TYPE_BLEND + changeFreq * _CompositeProbabilityStrategy.CHANGE_FREQ_BLEND + coupling * _CompositeProbabilityStrategy.COUPLING_BLEND
3494
+ );
3495
+ }
3496
+ };
3497
+
3498
+ // src/blast-radius/CascadeSimulator.ts
3499
+ var DEFAULT_PROBABILITY_FLOOR = 0.05;
3500
+ var DEFAULT_MAX_DEPTH = 10;
3501
+ var CascadeSimulator = class {
3502
+ constructor(store) {
3503
+ this.store = store;
3504
+ }
3505
+ store;
3506
+ simulate(sourceNodeId, options = {}) {
3507
+ const sourceNode = this.store.getNode(sourceNodeId);
3508
+ if (!sourceNode) {
3509
+ throw new Error(`Node not found: ${sourceNodeId}. Ensure the file has been ingested.`);
3510
+ }
3511
+ const probabilityFloor = options.probabilityFloor ?? DEFAULT_PROBABILITY_FLOOR;
3512
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
3513
+ const edgeTypeFilter = options.edgeTypes ? new Set(options.edgeTypes) : null;
3514
+ const strategy = options.strategy ?? this.buildDefaultStrategy();
3515
+ const visited = /* @__PURE__ */ new Map();
3516
+ const queue = [];
3517
+ const fanOutCount = /* @__PURE__ */ new Map();
3518
+ this.seedQueue(
3519
+ sourceNodeId,
3520
+ sourceNode,
3521
+ strategy,
3522
+ edgeTypeFilter,
3523
+ probabilityFloor,
3524
+ queue,
3525
+ fanOutCount
3526
+ );
3527
+ const truncated = this.runBfs(
3528
+ queue,
3529
+ visited,
3530
+ fanOutCount,
3531
+ sourceNodeId,
3532
+ strategy,
3533
+ edgeTypeFilter,
3534
+ probabilityFloor,
3535
+ maxDepth
3536
+ );
3537
+ return this.buildResult(sourceNodeId, sourceNode.name, visited, fanOutCount, truncated);
3538
+ }
3539
+ seedQueue(sourceNodeId, sourceNode, strategy, edgeTypeFilter, probabilityFloor, queue, fanOutCount) {
3540
+ const sourceEdges = this.store.getEdges({ from: sourceNodeId });
3541
+ for (const edge of sourceEdges) {
3542
+ if (edge.to === sourceNodeId) continue;
3543
+ if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
3544
+ const targetNode = this.store.getNode(edge.to);
3545
+ if (!targetNode) continue;
3546
+ const cumProb = strategy.getEdgeProbability(edge, sourceNode, targetNode);
3547
+ if (cumProb < probabilityFloor) continue;
3548
+ queue.push({
3549
+ nodeId: edge.to,
3550
+ cumProb,
3551
+ depth: 1,
3552
+ parentId: sourceNodeId,
3553
+ incomingEdge: edge.type
3554
+ });
3555
+ }
3556
+ fanOutCount.set(
3557
+ sourceNodeId,
3558
+ sourceEdges.filter(
3559
+ (e) => e.to !== sourceNodeId && (!edgeTypeFilter || edgeTypeFilter.has(e.type))
3560
+ ).length
3561
+ );
3562
+ }
3563
+ runBfs(queue, visited, fanOutCount, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, maxDepth) {
3564
+ const MAX_QUEUE_SIZE = 1e4;
3565
+ let head = 0;
3566
+ while (head < queue.length) {
3567
+ if (queue.length > MAX_QUEUE_SIZE) return true;
3568
+ const entry = queue[head++];
3569
+ const existing = visited.get(entry.nodeId);
3570
+ if (existing && existing.cumulativeProbability >= entry.cumProb) continue;
3571
+ const targetNode = this.store.getNode(entry.nodeId);
3572
+ if (!targetNode) continue;
3573
+ visited.set(entry.nodeId, {
3574
+ nodeId: entry.nodeId,
3575
+ name: targetNode.name,
3576
+ ...targetNode.path !== void 0 && { path: targetNode.path },
3577
+ type: targetNode.type,
3578
+ cumulativeProbability: entry.cumProb,
3579
+ depth: entry.depth,
3580
+ incomingEdge: entry.incomingEdge,
3581
+ parentId: entry.parentId
3582
+ });
3583
+ if (entry.depth < maxDepth) {
3584
+ const childCount = this.expandNode(
3585
+ entry,
3586
+ targetNode,
3587
+ sourceNodeId,
3588
+ strategy,
3589
+ edgeTypeFilter,
3590
+ probabilityFloor,
3591
+ queue
3592
+ );
3593
+ fanOutCount.set(entry.nodeId, (fanOutCount.get(entry.nodeId) ?? 0) + childCount);
3594
+ }
3595
+ }
3596
+ return false;
3597
+ }
3598
+ expandNode(entry, fromNode, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, queue) {
3599
+ const outEdges = this.store.getEdges({ from: entry.nodeId });
3600
+ let childCount = 0;
3601
+ for (const edge of outEdges) {
3602
+ if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
3603
+ if (edge.to === sourceNodeId) continue;
3604
+ const childNode = this.store.getNode(edge.to);
3605
+ if (!childNode) continue;
3606
+ const newCumProb = entry.cumProb * strategy.getEdgeProbability(edge, fromNode, childNode);
3607
+ if (newCumProb < probabilityFloor) continue;
3608
+ childCount++;
3609
+ queue.push({
3610
+ nodeId: edge.to,
3611
+ cumProb: newCumProb,
3612
+ depth: entry.depth + 1,
3613
+ parentId: entry.nodeId,
3614
+ incomingEdge: edge.type
3615
+ });
3616
+ }
3617
+ return childCount;
3618
+ }
3619
+ buildDefaultStrategy() {
3620
+ return new CompositeProbabilityStrategy(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
3621
+ }
3622
+ buildResult(sourceNodeId, sourceName, visited, fanOutCount, truncated = false) {
3623
+ if (visited.size === 0) {
3624
+ return {
3625
+ sourceNodeId,
3626
+ sourceName,
3627
+ layers: [],
3628
+ flatSummary: [],
3629
+ summary: {
3630
+ totalAffected: 0,
3631
+ maxDepthReached: 0,
3632
+ highRisk: 0,
3633
+ mediumRisk: 0,
3634
+ lowRisk: 0,
3635
+ categoryBreakdown: { code: 0, tests: 0, docs: 0, other: 0 },
3636
+ amplificationPoints: [],
3637
+ truncated
3638
+ }
3639
+ };
3640
+ }
3641
+ const allNodes = Array.from(visited.values());
3642
+ const flatSummary = [...allNodes].sort(
3643
+ (a, b) => b.cumulativeProbability - a.cumulativeProbability
3644
+ );
3645
+ const depthMap = /* @__PURE__ */ new Map();
3646
+ for (const node of allNodes) {
3647
+ let list = depthMap.get(node.depth);
3648
+ if (!list) {
3649
+ list = [];
3650
+ depthMap.set(node.depth, list);
3651
+ }
3652
+ list.push(node);
3653
+ }
3654
+ const layers = [];
3655
+ const depths = Array.from(depthMap.keys()).sort((a, b) => a - b);
3656
+ for (const depth of depths) {
3657
+ const nodes = depthMap.get(depth);
3658
+ const breakdown = { code: 0, tests: 0, docs: 0, other: 0 };
3659
+ for (const n of nodes) {
3660
+ const graphNode = this.store.getNode(n.nodeId);
3661
+ if (graphNode) {
3662
+ breakdown[classifyNodeCategory(graphNode)]++;
3663
+ }
3664
+ }
3665
+ layers.push({ depth, nodes, categoryBreakdown: breakdown });
3666
+ }
3667
+ let highRisk = 0;
3668
+ let mediumRisk = 0;
3669
+ let lowRisk = 0;
3670
+ const catBreakdown = { code: 0, tests: 0, docs: 0, other: 0 };
3671
+ for (const node of allNodes) {
3672
+ if (node.cumulativeProbability >= 0.5) highRisk++;
3673
+ else if (node.cumulativeProbability >= 0.2) mediumRisk++;
3674
+ else lowRisk++;
3675
+ const graphNode = this.store.getNode(node.nodeId);
3676
+ if (graphNode) {
3677
+ catBreakdown[classifyNodeCategory(graphNode)]++;
3678
+ }
3679
+ }
3680
+ const amplificationPoints = [];
3681
+ for (const [nodeId, count] of fanOutCount) {
3682
+ if (count > 3) {
3683
+ amplificationPoints.push(nodeId);
3684
+ }
3685
+ }
3686
+ const maxDepthReached = allNodes.reduce((max, n) => Math.max(max, n.depth), 0);
3687
+ return {
3688
+ sourceNodeId,
3689
+ sourceName,
3690
+ layers,
3691
+ flatSummary,
3692
+ summary: {
3693
+ totalAffected: allNodes.length,
3694
+ maxDepthReached,
3695
+ highRisk,
3696
+ mediumRisk,
3697
+ lowRisk,
3698
+ categoryBreakdown: catBreakdown,
3699
+ amplificationPoints,
3700
+ truncated
3701
+ }
3702
+ };
3703
+ }
3704
+ };
3705
+
3191
3706
  // src/nlq/index.ts
3192
3707
  var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships", "explain"]);
3193
3708
  var classifier = new IntentClassifier();
@@ -3250,6 +3765,11 @@ function executeOperation(store, intent, entities, question, fusion) {
3250
3765
  switch (intent) {
3251
3766
  case "impact": {
3252
3767
  const rootId = entities[0].nodeId;
3768
+ const lowerQuestion = question.toLowerCase();
3769
+ if (lowerQuestion.includes("blast radius") || lowerQuestion.includes("cascade")) {
3770
+ const simulator = new CascadeSimulator(store);
3771
+ return simulator.simulate(rootId);
3772
+ }
3253
3773
  const result = cql.execute({
3254
3774
  rootNodeIds: [rootId],
3255
3775
  bidirectional: true,
@@ -3304,7 +3824,7 @@ var PHASE_NODE_TYPES = {
3304
3824
  debug: ["failure", "learning", "function", "method"],
3305
3825
  plan: ["adr", "document", "module", "layer"]
3306
3826
  };
3307
- var CODE_NODE_TYPES4 = /* @__PURE__ */ new Set([
3827
+ var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
3308
3828
  "file",
3309
3829
  "function",
3310
3830
  "class",
@@ -3519,7 +4039,7 @@ var Assembler = class {
3519
4039
  */
3520
4040
  checkCoverage() {
3521
4041
  const codeNodes = [];
3522
- for (const type of CODE_NODE_TYPES4) {
4042
+ for (const type of CODE_NODE_TYPES5) {
3523
4043
  codeNodes.push(...this.store.findNodes({ type }));
3524
4044
  }
3525
4045
  const documented = [];
@@ -3543,6 +4063,99 @@ var Assembler = class {
3543
4063
  }
3544
4064
  };
3545
4065
 
4066
+ // src/query/Traceability.ts
4067
+ function extractConfidence(edge) {
4068
+ return edge.confidence ?? edge.metadata?.confidence ?? 0;
4069
+ }
4070
+ function extractMethod(edge) {
4071
+ return edge.metadata?.method ?? "convention";
4072
+ }
4073
+ function edgesToTracedFiles(store, edges) {
4074
+ return edges.map((edge) => ({
4075
+ path: store.getNode(edge.to)?.path ?? edge.to,
4076
+ confidence: extractConfidence(edge),
4077
+ method: extractMethod(edge)
4078
+ }));
4079
+ }
4080
+ function determineCoverageStatus(hasCode, hasTests) {
4081
+ if (hasCode && hasTests) return "full";
4082
+ if (hasCode) return "code-only";
4083
+ if (hasTests) return "test-only";
4084
+ return "none";
4085
+ }
4086
+ function computeMaxConfidence(codeFiles, testFiles) {
4087
+ const allConfidences = [
4088
+ ...codeFiles.map((f) => f.confidence),
4089
+ ...testFiles.map((f) => f.confidence)
4090
+ ];
4091
+ return allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
4092
+ }
4093
+ function buildRequirementCoverage(store, req) {
4094
+ const codeFiles = edgesToTracedFiles(store, store.getEdges({ from: req.id, type: "requires" }));
4095
+ const testFiles = edgesToTracedFiles(
4096
+ store,
4097
+ store.getEdges({ from: req.id, type: "verified_by" })
4098
+ );
4099
+ const hasCode = codeFiles.length > 0;
4100
+ const hasTests = testFiles.length > 0;
4101
+ return {
4102
+ requirementId: req.id,
4103
+ requirementName: req.name,
4104
+ index: req.metadata?.index ?? 0,
4105
+ codeFiles,
4106
+ testFiles,
4107
+ status: determineCoverageStatus(hasCode, hasTests),
4108
+ maxConfidence: computeMaxConfidence(codeFiles, testFiles)
4109
+ };
4110
+ }
4111
+ function computeSummary(requirements) {
4112
+ const total = requirements.length;
4113
+ const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
4114
+ const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
4115
+ const fullyTraced = requirements.filter((r) => r.status === "full").length;
4116
+ const untraceable = requirements.filter((r) => r.status === "none").length;
4117
+ const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
4118
+ return { total, withCode, withTests, fullyTraced, untraceable, coveragePercent };
4119
+ }
4120
+ function queryTraceability(store, options) {
4121
+ const allRequirements = store.findNodes({ type: "requirement" });
4122
+ const filtered = allRequirements.filter((node) => {
4123
+ if (options?.specPath && node.metadata?.specPath !== options.specPath) return false;
4124
+ if (options?.featureName && node.metadata?.featureName !== options.featureName) return false;
4125
+ return true;
4126
+ });
4127
+ if (filtered.length === 0) return [];
4128
+ const groups = /* @__PURE__ */ new Map();
4129
+ for (const req of filtered) {
4130
+ const meta = req.metadata;
4131
+ const specPath = meta?.specPath ?? "";
4132
+ const featureName = meta?.featureName ?? "";
4133
+ const key = `${specPath}\0${featureName}`;
4134
+ const list = groups.get(key);
4135
+ if (list) {
4136
+ list.push(req);
4137
+ } else {
4138
+ groups.set(key, [req]);
4139
+ }
4140
+ }
4141
+ const results = [];
4142
+ for (const [, reqs] of groups) {
4143
+ const firstReq = reqs[0];
4144
+ const firstMeta = firstReq.metadata;
4145
+ const specPath = firstMeta?.specPath ?? "";
4146
+ const featureName = firstMeta?.featureName ?? "";
4147
+ const requirements = reqs.map((req) => buildRequirementCoverage(store, req));
4148
+ requirements.sort((a, b) => a.index - b.index);
4149
+ results.push({
4150
+ specPath,
4151
+ featureName,
4152
+ requirements,
4153
+ summary: computeSummary(requirements)
4154
+ });
4155
+ }
4156
+ return results;
4157
+ }
4158
+
3546
4159
  // src/constraints/GraphConstraintAdapter.ts
3547
4160
  import { minimatch } from "minimatch";
3548
4161
  import { relative as relative2 } from "path";
@@ -3602,14 +4215,14 @@ var GraphConstraintAdapter = class {
3602
4215
  };
3603
4216
 
3604
4217
  // src/ingest/DesignIngestor.ts
3605
- import * as fs4 from "fs/promises";
3606
- import * as path5 from "path";
4218
+ import * as fs5 from "fs/promises";
4219
+ import * as path6 from "path";
3607
4220
  function isDTCGToken(obj) {
3608
4221
  return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
3609
4222
  }
3610
4223
  async function readFileOrNull(filePath) {
3611
4224
  try {
3612
- return await fs4.readFile(filePath, "utf-8");
4225
+ return await fs5.readFile(filePath, "utf-8");
3613
4226
  } catch {
3614
4227
  return null;
3615
4228
  }
@@ -3755,8 +4368,8 @@ var DesignIngestor = class {
3755
4368
  async ingestAll(designDir) {
3756
4369
  const start = Date.now();
3757
4370
  const [tokensResult, intentResult] = await Promise.all([
3758
- this.ingestTokens(path5.join(designDir, "tokens.json")),
3759
- this.ingestDesignIntent(path5.join(designDir, "DESIGN.md"))
4371
+ this.ingestTokens(path6.join(designDir, "tokens.json")),
4372
+ this.ingestDesignIntent(path6.join(designDir, "DESIGN.md"))
3760
4373
  ]);
3761
4374
  const merged = mergeResults(tokensResult, intentResult);
3762
4375
  return { ...merged, durationMs: Date.now() - start };
@@ -4005,10 +4618,10 @@ var TaskIndependenceAnalyzer = class {
4005
4618
  includeTypes: ["file"]
4006
4619
  });
4007
4620
  for (const n of queryResult.nodes) {
4008
- const path6 = n.path ?? n.id.replace(/^file:/, "");
4009
- if (!fileSet.has(path6)) {
4010
- if (!result.has(path6)) {
4011
- result.set(path6, file);
4621
+ const path7 = n.path ?? n.id.replace(/^file:/, "");
4622
+ if (!fileSet.has(path7)) {
4623
+ if (!result.has(path7)) {
4624
+ result.set(path7, file);
4012
4625
  }
4013
4626
  }
4014
4627
  }
@@ -4361,12 +4974,14 @@ var ConflictPredictor = class {
4361
4974
  };
4362
4975
 
4363
4976
  // src/index.ts
4364
- var VERSION = "0.2.0";
4977
+ var VERSION = "0.4.0";
4365
4978
  export {
4366
4979
  Assembler,
4367
4980
  CIConnector,
4368
4981
  CURRENT_SCHEMA_VERSION,
4982
+ CascadeSimulator,
4369
4983
  CodeIngestor,
4984
+ CompositeProbabilityStrategy,
4370
4985
  ConflictPredictor,
4371
4986
  ConfluenceConnector,
4372
4987
  ContextQL,
@@ -4392,6 +5007,7 @@ export {
4392
5007
  KnowledgeIngestor,
4393
5008
  NODE_TYPES,
4394
5009
  OBSERVABILITY_TYPES,
5010
+ RequirementIngestor,
4395
5011
  ResponseFormatter,
4396
5012
  SlackConnector,
4397
5013
  SyncManager,
@@ -4400,9 +5016,11 @@ export {
4400
5016
  VERSION,
4401
5017
  VectorStore,
4402
5018
  askGraph,
5019
+ classifyNodeCategory,
4403
5020
  groupNodesByImpact,
4404
5021
  linkToCode,
4405
5022
  loadGraph,
4406
5023
  project,
5024
+ queryTraceability,
4407
5025
  saveGraph
4408
5026
  };