@harness-engineering/graph 0.3.4 → 0.4.0

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,6 +59,7 @@ __export(index_exports, {
59
59
  KnowledgeIngestor: () => KnowledgeIngestor,
60
60
  NODE_TYPES: () => NODE_TYPES,
61
61
  OBSERVABILITY_TYPES: () => OBSERVABILITY_TYPES,
62
+ RequirementIngestor: () => RequirementIngestor,
62
63
  ResponseFormatter: () => ResponseFormatter,
63
64
  SlackConnector: () => SlackConnector,
64
65
  SyncManager: () => SyncManager,
@@ -71,6 +72,7 @@ __export(index_exports, {
71
72
  linkToCode: () => linkToCode,
72
73
  loadGraph: () => loadGraph,
73
74
  project: () => project,
75
+ queryTraceability: () => queryTraceability,
74
76
  saveGraph: () => saveGraph
75
77
  });
76
78
  module.exports = __toCommonJS(index_exports);
@@ -112,7 +114,9 @@ var NODE_TYPES = [
112
114
  // Design
113
115
  "design_token",
114
116
  "aesthetic_intent",
115
- "design_constraint"
117
+ "design_constraint",
118
+ // Traceability
119
+ "requirement"
116
120
  ];
117
121
  var EDGE_TYPES = [
118
122
  // Code relationships
@@ -141,7 +145,11 @@ var EDGE_TYPES = [
141
145
  "uses_token",
142
146
  "declares_intent",
143
147
  "violates_design",
144
- "platform_binding"
148
+ "platform_binding",
149
+ // Traceability relationships
150
+ "requires",
151
+ "verified_by",
152
+ "tested_by"
145
153
  ];
146
154
  var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
147
155
  var CURRENT_SCHEMA_VERSION = 1;
@@ -171,9 +179,6 @@ var GraphEdgeSchema = import_zod.z.object({
171
179
  metadata: import_zod.z.record(import_zod.z.unknown()).optional()
172
180
  });
173
181
 
174
- // src/store/GraphStore.ts
175
- var import_lokijs = __toESM(require("lokijs"));
176
-
177
182
  // src/store/Serializer.ts
178
183
  var import_promises = require("fs/promises");
179
184
  var import_node_path = require("path");
@@ -218,28 +223,38 @@ function safeMerge(target, source) {
218
223
  }
219
224
  }
220
225
  }
221
- var GraphStore = class {
222
- db;
223
- nodes;
224
- edges;
225
- constructor() {
226
- this.db = new import_lokijs.default("graph.db");
227
- this.nodes = this.db.addCollection("nodes", {
228
- unique: ["id"],
229
- indices: ["type", "name"]
230
- });
231
- this.edges = this.db.addCollection("edges", {
232
- indices: ["from", "to", "type"]
233
- });
226
+ function edgeKey(from, to, type) {
227
+ return `${from}\0${to}\0${type}`;
228
+ }
229
+ function addToIndex(index, key, edge) {
230
+ const list = index.get(key);
231
+ if (list) {
232
+ list.push(edge);
233
+ } else {
234
+ index.set(key, [edge]);
234
235
  }
236
+ }
237
+ function removeFromIndex(index, key, edge) {
238
+ const list = index.get(key);
239
+ if (!list) return;
240
+ const idx = list.indexOf(edge);
241
+ if (idx !== -1) list.splice(idx, 1);
242
+ if (list.length === 0) index.delete(key);
243
+ }
244
+ var GraphStore = class {
245
+ nodeMap = /* @__PURE__ */ new Map();
246
+ edgeMap = /* @__PURE__ */ new Map();
247
+ // keyed by from\0to\0type
248
+ edgesByFrom = /* @__PURE__ */ new Map();
249
+ edgesByTo = /* @__PURE__ */ new Map();
250
+ edgesByType = /* @__PURE__ */ new Map();
235
251
  // --- Node operations ---
236
252
  addNode(node) {
237
- const existing = this.nodes.by("id", node.id);
253
+ const existing = this.nodeMap.get(node.id);
238
254
  if (existing) {
239
255
  safeMerge(existing, node);
240
- this.nodes.update(existing);
241
256
  } else {
242
- this.nodes.insert({ ...node });
257
+ this.nodeMap.set(node.id, { ...node });
243
258
  }
244
259
  }
245
260
  batchAddNodes(nodes) {
@@ -248,44 +263,44 @@ var GraphStore = class {
248
263
  }
249
264
  }
250
265
  getNode(id) {
251
- const doc = this.nodes.by("id", id);
252
- if (!doc) return null;
253
- return this.stripLokiMeta(doc);
266
+ const node = this.nodeMap.get(id);
267
+ if (!node) return null;
268
+ return { ...node };
254
269
  }
255
270
  findNodes(query) {
256
- const lokiQuery = {};
257
- if (query.type !== void 0) lokiQuery["type"] = query.type;
258
- if (query.name !== void 0) lokiQuery["name"] = query.name;
259
- if (query.path !== void 0) lokiQuery["path"] = query.path;
260
- return this.nodes.find(lokiQuery).map((doc) => this.stripLokiMeta(doc));
271
+ const results = [];
272
+ for (const node of this.nodeMap.values()) {
273
+ if (query.type !== void 0 && node.type !== query.type) continue;
274
+ if (query.name !== void 0 && node.name !== query.name) continue;
275
+ if (query.path !== void 0 && node.path !== query.path) continue;
276
+ results.push({ ...node });
277
+ }
278
+ return results;
261
279
  }
262
280
  removeNode(id) {
263
- const doc = this.nodes.by("id", id);
264
- if (doc) {
265
- this.nodes.remove(doc);
266
- }
267
- const edgesToRemove = this.edges.find({
268
- $or: [{ from: id }, { to: id }]
269
- });
281
+ this.nodeMap.delete(id);
282
+ const fromEdges = this.edgesByFrom.get(id) ?? [];
283
+ const toEdges = this.edgesByTo.get(id) ?? [];
284
+ const edgesToRemove = /* @__PURE__ */ new Set([...fromEdges, ...toEdges]);
270
285
  for (const edge of edgesToRemove) {
271
- this.edges.remove(edge);
286
+ this.removeEdgeInternal(edge);
272
287
  }
273
288
  }
274
289
  // --- Edge operations ---
275
290
  addEdge(edge) {
276
- const existing = this.edges.findOne({
277
- from: edge.from,
278
- to: edge.to,
279
- type: edge.type
280
- });
291
+ const key = edgeKey(edge.from, edge.to, edge.type);
292
+ const existing = this.edgeMap.get(key);
281
293
  if (existing) {
282
294
  if (edge.metadata) {
283
295
  safeMerge(existing, edge);
284
- this.edges.update(existing);
285
296
  }
286
297
  return;
287
298
  }
288
- this.edges.insert({ ...edge });
299
+ const copy = { ...edge };
300
+ this.edgeMap.set(key, copy);
301
+ addToIndex(this.edgesByFrom, edge.from, copy);
302
+ addToIndex(this.edgesByTo, edge.to, copy);
303
+ addToIndex(this.edgesByType, edge.type, copy);
289
304
  }
290
305
  batchAddEdges(edges) {
291
306
  for (const edge of edges) {
@@ -293,22 +308,38 @@ var GraphStore = class {
293
308
  }
294
309
  }
295
310
  getEdges(query) {
296
- const lokiQuery = {};
297
- if (query.from !== void 0) lokiQuery["from"] = query.from;
298
- if (query.to !== void 0) lokiQuery["to"] = query.to;
299
- if (query.type !== void 0) lokiQuery["type"] = query.type;
300
- return this.edges.find(lokiQuery).map((doc) => this.stripLokiMeta(doc));
311
+ let candidates;
312
+ if (query.from !== void 0 && query.to !== void 0 && query.type !== void 0) {
313
+ const edge = this.edgeMap.get(edgeKey(query.from, query.to, query.type));
314
+ return edge ? [{ ...edge }] : [];
315
+ } else if (query.from !== void 0) {
316
+ candidates = this.edgesByFrom.get(query.from) ?? [];
317
+ } else if (query.to !== void 0) {
318
+ candidates = this.edgesByTo.get(query.to) ?? [];
319
+ } else if (query.type !== void 0) {
320
+ candidates = this.edgesByType.get(query.type) ?? [];
321
+ } else {
322
+ candidates = this.edgeMap.values();
323
+ }
324
+ const results = [];
325
+ for (const edge of candidates) {
326
+ if (query.from !== void 0 && edge.from !== query.from) continue;
327
+ if (query.to !== void 0 && edge.to !== query.to) continue;
328
+ if (query.type !== void 0 && edge.type !== query.type) continue;
329
+ results.push({ ...edge });
330
+ }
331
+ return results;
301
332
  }
302
333
  getNeighbors(nodeId, direction = "both") {
303
334
  const neighborIds = /* @__PURE__ */ new Set();
304
335
  if (direction === "outbound" || direction === "both") {
305
- const outEdges = this.edges.find({ from: nodeId });
336
+ const outEdges = this.edgesByFrom.get(nodeId) ?? [];
306
337
  for (const edge of outEdges) {
307
338
  neighborIds.add(edge.to);
308
339
  }
309
340
  }
310
341
  if (direction === "inbound" || direction === "both") {
311
- const inEdges = this.edges.find({ to: nodeId });
342
+ const inEdges = this.edgesByTo.get(nodeId) ?? [];
312
343
  for (const edge of inEdges) {
313
344
  neighborIds.add(edge.from);
314
345
  }
@@ -322,20 +353,23 @@ var GraphStore = class {
322
353
  }
323
354
  // --- Counts ---
324
355
  get nodeCount() {
325
- return this.nodes.count();
356
+ return this.nodeMap.size;
326
357
  }
327
358
  get edgeCount() {
328
- return this.edges.count();
359
+ return this.edgeMap.size;
329
360
  }
330
361
  // --- Clear ---
331
362
  clear() {
332
- this.nodes.clear();
333
- this.edges.clear();
363
+ this.nodeMap.clear();
364
+ this.edgeMap.clear();
365
+ this.edgesByFrom.clear();
366
+ this.edgesByTo.clear();
367
+ this.edgesByType.clear();
334
368
  }
335
369
  // --- Persistence ---
336
370
  async save(dirPath) {
337
- const allNodes = this.nodes.find().map((doc) => this.stripLokiMeta(doc));
338
- const allEdges = this.edges.find().map((doc) => this.stripLokiMeta(doc));
371
+ const allNodes = Array.from(this.nodeMap.values()).map((n) => ({ ...n }));
372
+ const allEdges = Array.from(this.edgeMap.values()).map((e) => ({ ...e }));
339
373
  await saveGraph(dirPath, allNodes, allEdges);
340
374
  }
341
375
  async load(dirPath) {
@@ -343,17 +377,25 @@ var GraphStore = class {
343
377
  if (!data) return false;
344
378
  this.clear();
345
379
  for (const node of data.nodes) {
346
- this.nodes.insert({ ...node });
380
+ this.nodeMap.set(node.id, { ...node });
347
381
  }
348
382
  for (const edge of data.edges) {
349
- this.edges.insert({ ...edge });
383
+ const copy = { ...edge };
384
+ const key = edgeKey(edge.from, edge.to, edge.type);
385
+ this.edgeMap.set(key, copy);
386
+ addToIndex(this.edgesByFrom, edge.from, copy);
387
+ addToIndex(this.edgesByTo, edge.to, copy);
388
+ addToIndex(this.edgesByType, edge.type, copy);
350
389
  }
351
390
  return true;
352
391
  }
353
392
  // --- Internal ---
354
- stripLokiMeta(doc) {
355
- const { $loki: _, meta: _meta, ...rest } = doc;
356
- return rest;
393
+ removeEdgeInternal(edge) {
394
+ const key = edgeKey(edge.from, edge.to, edge.type);
395
+ this.edgeMap.delete(key);
396
+ removeFromIndex(this.edgesByFrom, edge.from, edge);
397
+ removeFromIndex(this.edgesByTo, edge.to, edge);
398
+ removeFromIndex(this.edgesByType, edge.type, edge);
357
399
  }
358
400
  };
359
401
 
@@ -434,11 +476,11 @@ var VectorStore = class _VectorStore {
434
476
  };
435
477
 
436
478
  // src/query/ContextQL.ts
437
- function edgeKey(e) {
479
+ function edgeKey2(e) {
438
480
  return `${e.from}|${e.to}|${e.type}`;
439
481
  }
440
482
  function addEdge(state, edge) {
441
- const key = edgeKey(edge);
483
+ const key = edgeKey2(edge);
442
484
  if (!state.edgeSet.has(key)) {
443
485
  state.edgeSet.add(key);
444
486
  state.resultEdges.push(edge);
@@ -618,6 +660,7 @@ var CodeIngestor = class {
618
660
  constructor(store) {
619
661
  this.store = store;
620
662
  }
663
+ store;
621
664
  async ingest(rootDir) {
622
665
  const start = Date.now();
623
666
  const errors = [];
@@ -640,6 +683,7 @@ var CodeIngestor = class {
640
683
  this.store.addEdge(edge);
641
684
  edgesAdded++;
642
685
  }
686
+ edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
643
687
  return {
644
688
  nodesAdded,
645
689
  nodesUpdated: 0,
@@ -998,6 +1042,48 @@ var CodeIngestor = class {
998
1042
  if (/\.jsx?$/.test(filePath)) return "javascript";
999
1043
  return "unknown";
1000
1044
  }
1045
+ /**
1046
+ * Scan file contents for @req annotations and create verified_by edges
1047
+ * linking requirement nodes to the annotated files.
1048
+ * Format: // @req <feature-name>#<index>
1049
+ */
1050
+ extractReqAnnotations(fileContents, rootDir) {
1051
+ const REQ_TAG = /\/\/\s*@req\s+([\w-]+)#(\d+)/g;
1052
+ const reqNodes = this.store.findNodes({ type: "requirement" });
1053
+ let edgesAdded = 0;
1054
+ for (const [filePath, content] of fileContents) {
1055
+ let match;
1056
+ REQ_TAG.lastIndex = 0;
1057
+ while ((match = REQ_TAG.exec(content)) !== null) {
1058
+ const featureName = match[1];
1059
+ const reqIndex = parseInt(match[2], 10);
1060
+ const reqNode = reqNodes.find(
1061
+ (n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
1062
+ );
1063
+ if (!reqNode) {
1064
+ console.warn(
1065
+ `@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
1066
+ );
1067
+ continue;
1068
+ }
1069
+ const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
1070
+ const fileNodeId = `file:${relPath}`;
1071
+ this.store.addEdge({
1072
+ from: reqNode.id,
1073
+ to: fileNodeId,
1074
+ type: "verified_by",
1075
+ confidence: 1,
1076
+ metadata: {
1077
+ method: "annotation",
1078
+ tag: `@req ${featureName}#${reqIndex}`,
1079
+ confidence: 1
1080
+ }
1081
+ });
1082
+ edgesAdded++;
1083
+ }
1084
+ }
1085
+ return edgesAdded;
1086
+ }
1001
1087
  };
1002
1088
 
1003
1089
  // src/ingest/GitIngestor.ts
@@ -1009,6 +1095,8 @@ var GitIngestor = class {
1009
1095
  this.store = store;
1010
1096
  this.gitRunner = gitRunner;
1011
1097
  }
1098
+ store;
1099
+ gitRunner;
1012
1100
  async ingest(rootDir) {
1013
1101
  const start = Date.now();
1014
1102
  const errors = [];
@@ -1188,6 +1276,7 @@ var TopologicalLinker = class {
1188
1276
  constructor(store) {
1189
1277
  this.store = store;
1190
1278
  }
1279
+ store;
1191
1280
  link() {
1192
1281
  let edgesAdded = 0;
1193
1282
  const files = this.store.findNodes({ type: "file" });
@@ -1285,6 +1374,7 @@ var KnowledgeIngestor = class {
1285
1374
  constructor(store) {
1286
1375
  this.store = store;
1287
1376
  }
1377
+ store;
1288
1378
  async ingestADRs(adrDir) {
1289
1379
  const start = Date.now();
1290
1380
  const errors = [];
@@ -1470,8 +1560,218 @@ var KnowledgeIngestor = class {
1470
1560
  }
1471
1561
  };
1472
1562
 
1473
- // src/ingest/connectors/ConnectorUtils.ts
1563
+ // src/ingest/RequirementIngestor.ts
1564
+ var fs3 = __toESM(require("fs/promises"));
1565
+ var path4 = __toESM(require("path"));
1566
+ var REQUIREMENT_SECTIONS = [
1567
+ "Observable Truths",
1568
+ "Success Criteria",
1569
+ "Acceptance Criteria"
1570
+ ];
1571
+ var SECTION_HEADING_RE = /^#{2,3}\s+(.+)$/;
1572
+ var NUMBERED_ITEM_RE = /^\s*(\d+)\.\s+(.+)$/;
1573
+ function detectEarsPattern(text) {
1574
+ const lower = text.toLowerCase();
1575
+ if (/^if\b.+\bthen\b.+\bshall not\b/.test(lower)) return "unwanted";
1576
+ if (/^when\b/.test(lower)) return "event-driven";
1577
+ if (/^while\b/.test(lower)) return "state-driven";
1578
+ if (/^where\b/.test(lower)) return "optional";
1579
+ if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
1580
+ return void 0;
1581
+ }
1474
1582
  var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
1583
+ var RequirementIngestor = class {
1584
+ constructor(store) {
1585
+ this.store = store;
1586
+ }
1587
+ store;
1588
+ /**
1589
+ * Scan a specs directory for `<feature>/proposal.md` files,
1590
+ * extract numbered requirements from recognized sections,
1591
+ * and create requirement nodes with convention-based edges.
1592
+ */
1593
+ async ingestSpecs(specsDir) {
1594
+ const start = Date.now();
1595
+ const errors = [];
1596
+ let nodesAdded = 0;
1597
+ let edgesAdded = 0;
1598
+ let featureDirs;
1599
+ try {
1600
+ const entries = await fs3.readdir(specsDir, { withFileTypes: true });
1601
+ featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path4.join(specsDir, e.name));
1602
+ } catch {
1603
+ return emptyResult(Date.now() - start);
1604
+ }
1605
+ for (const featureDir of featureDirs) {
1606
+ const featureName = path4.basename(featureDir);
1607
+ const specPath = path4.join(featureDir, "proposal.md");
1608
+ let content;
1609
+ try {
1610
+ content = await fs3.readFile(specPath, "utf-8");
1611
+ } catch {
1612
+ continue;
1613
+ }
1614
+ try {
1615
+ const specHash = hash(specPath);
1616
+ const specNodeId = `file:${specPath}`;
1617
+ this.store.addNode({
1618
+ id: specNodeId,
1619
+ type: "document",
1620
+ name: path4.basename(specPath),
1621
+ path: specPath,
1622
+ metadata: { featureName }
1623
+ });
1624
+ const requirements = this.extractRequirements(content, specPath, specHash, featureName);
1625
+ for (const req of requirements) {
1626
+ this.store.addNode(req.node);
1627
+ nodesAdded++;
1628
+ this.store.addEdge({
1629
+ from: req.node.id,
1630
+ to: specNodeId,
1631
+ type: "specifies"
1632
+ });
1633
+ edgesAdded++;
1634
+ edgesAdded += this.linkByPathPattern(req.node.id, featureName);
1635
+ edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
1636
+ }
1637
+ } catch (err) {
1638
+ errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
1639
+ }
1640
+ }
1641
+ return {
1642
+ nodesAdded,
1643
+ nodesUpdated: 0,
1644
+ edgesAdded,
1645
+ edgesUpdated: 0,
1646
+ errors,
1647
+ durationMs: Date.now() - start
1648
+ };
1649
+ }
1650
+ /**
1651
+ * Parse markdown content and extract numbered items from recognized sections.
1652
+ */
1653
+ extractRequirements(content, specPath, specHash, featureName) {
1654
+ const lines = content.split("\n");
1655
+ const results = [];
1656
+ let currentSection;
1657
+ let inRequirementSection = false;
1658
+ let globalIndex = 0;
1659
+ for (let i = 0; i < lines.length; i++) {
1660
+ const line = lines[i];
1661
+ const headingMatch = line.match(SECTION_HEADING_RE);
1662
+ if (headingMatch) {
1663
+ const heading = headingMatch[1].trim();
1664
+ const isReqSection = REQUIREMENT_SECTIONS.some(
1665
+ (s) => heading.toLowerCase() === s.toLowerCase()
1666
+ );
1667
+ if (isReqSection) {
1668
+ currentSection = heading;
1669
+ inRequirementSection = true;
1670
+ } else {
1671
+ inRequirementSection = false;
1672
+ }
1673
+ continue;
1674
+ }
1675
+ if (!inRequirementSection) continue;
1676
+ const itemMatch = line.match(NUMBERED_ITEM_RE);
1677
+ if (!itemMatch) continue;
1678
+ const index = parseInt(itemMatch[1], 10);
1679
+ const text = itemMatch[2].trim();
1680
+ const rawText = line.trim();
1681
+ const lineNumber = i + 1;
1682
+ globalIndex++;
1683
+ const nodeId = `req:${specHash}:${globalIndex}`;
1684
+ const earsPattern = detectEarsPattern(text);
1685
+ results.push({
1686
+ node: {
1687
+ id: nodeId,
1688
+ type: "requirement",
1689
+ name: text,
1690
+ path: specPath,
1691
+ location: {
1692
+ fileId: `file:${specPath}`,
1693
+ startLine: lineNumber,
1694
+ endLine: lineNumber
1695
+ },
1696
+ metadata: {
1697
+ specPath,
1698
+ index,
1699
+ section: currentSection,
1700
+ rawText,
1701
+ earsPattern,
1702
+ featureName
1703
+ }
1704
+ }
1705
+ });
1706
+ }
1707
+ return results;
1708
+ }
1709
+ /**
1710
+ * Convention-based linking: match requirement to code/test files
1711
+ * by feature name in their path.
1712
+ */
1713
+ linkByPathPattern(reqId, featureName) {
1714
+ let count = 0;
1715
+ const fileNodes = this.store.findNodes({ type: "file" });
1716
+ for (const node of fileNodes) {
1717
+ if (!node.path) continue;
1718
+ const normalizedPath = node.path.replace(/\\/g, "/");
1719
+ const isCodeMatch = normalizedPath.includes("packages/") && path4.basename(normalizedPath).includes(featureName);
1720
+ const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
1721
+ path4.basename(normalizedPath).includes(featureName);
1722
+ if (isCodeMatch && !isTestMatch) {
1723
+ this.store.addEdge({
1724
+ from: reqId,
1725
+ to: node.id,
1726
+ type: "requires",
1727
+ confidence: 0.5,
1728
+ metadata: { method: "convention", matchReason: "path-pattern" }
1729
+ });
1730
+ count++;
1731
+ } else if (isTestMatch) {
1732
+ this.store.addEdge({
1733
+ from: reqId,
1734
+ to: node.id,
1735
+ type: "verified_by",
1736
+ confidence: 0.5,
1737
+ metadata: { method: "convention", matchReason: "path-pattern" }
1738
+ });
1739
+ count++;
1740
+ }
1741
+ }
1742
+ return count;
1743
+ }
1744
+ /**
1745
+ * Convention-based linking: match requirement text to code nodes
1746
+ * by keyword overlap (function/class names appearing in requirement text).
1747
+ */
1748
+ linkByKeywordOverlap(reqId, reqText) {
1749
+ let count = 0;
1750
+ for (const nodeType of CODE_NODE_TYPES2) {
1751
+ const codeNodes = this.store.findNodes({ type: nodeType });
1752
+ for (const node of codeNodes) {
1753
+ if (node.name.length < 3) continue;
1754
+ const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1755
+ const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
1756
+ if (namePattern.test(reqText)) {
1757
+ const edgeType = node.path && node.path.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
1758
+ this.store.addEdge({
1759
+ from: reqId,
1760
+ to: node.id,
1761
+ type: edgeType,
1762
+ confidence: 0.6,
1763
+ metadata: { method: "convention", matchReason: "keyword-overlap" }
1764
+ });
1765
+ count++;
1766
+ }
1767
+ }
1768
+ }
1769
+ return count;
1770
+ }
1771
+ };
1772
+
1773
+ // src/ingest/connectors/ConnectorUtils.ts
1774
+ var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
1475
1775
  var SANITIZE_RULES = [
1476
1776
  // Strip XML/HTML-like instruction tags that could be interpreted as system prompts
1477
1777
  {
@@ -1506,7 +1806,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
1506
1806
  }
1507
1807
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
1508
1808
  let edgesCreated = 0;
1509
- for (const type of CODE_NODE_TYPES2) {
1809
+ for (const type of CODE_NODE_TYPES3) {
1510
1810
  const nodes = store.findNodes({ type });
1511
1811
  for (const node of nodes) {
1512
1812
  if (node.name.length < 3) continue;
@@ -1526,13 +1826,14 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
1526
1826
  }
1527
1827
 
1528
1828
  // src/ingest/connectors/SyncManager.ts
1529
- var fs3 = __toESM(require("fs/promises"));
1530
- var path4 = __toESM(require("path"));
1829
+ var fs4 = __toESM(require("fs/promises"));
1830
+ var path5 = __toESM(require("path"));
1531
1831
  var SyncManager = class {
1532
1832
  constructor(store, graphDir) {
1533
1833
  this.store = store;
1534
- this.metadataPath = path4.join(graphDir, "sync-metadata.json");
1834
+ this.metadataPath = path5.join(graphDir, "sync-metadata.json");
1535
1835
  }
1836
+ store;
1536
1837
  registrations = /* @__PURE__ */ new Map();
1537
1838
  metadataPath;
1538
1839
  registerConnector(connector, config) {
@@ -1585,15 +1886,15 @@ var SyncManager = class {
1585
1886
  }
1586
1887
  async loadMetadata() {
1587
1888
  try {
1588
- const raw = await fs3.readFile(this.metadataPath, "utf-8");
1889
+ const raw = await fs4.readFile(this.metadataPath, "utf-8");
1589
1890
  return JSON.parse(raw);
1590
1891
  } catch {
1591
1892
  return { connectors: {} };
1592
1893
  }
1593
1894
  }
1594
1895
  async saveMetadata(metadata) {
1595
- await fs3.mkdir(path4.dirname(this.metadataPath), { recursive: true });
1596
- await fs3.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
1896
+ await fs4.mkdir(path5.dirname(this.metadataPath), { recursive: true });
1897
+ await fs4.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
1597
1898
  }
1598
1899
  };
1599
1900
 
@@ -2122,11 +2423,12 @@ var FusionLayer = class {
2122
2423
  };
2123
2424
 
2124
2425
  // src/entropy/GraphEntropyAdapter.ts
2125
- var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
2426
+ var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
2126
2427
  var GraphEntropyAdapter = class {
2127
2428
  constructor(store) {
2128
2429
  this.store = store;
2129
2430
  }
2431
+ store;
2130
2432
  /**
2131
2433
  * Find all `documents` edges and classify them as stale or missing-target.
2132
2434
  *
@@ -2188,7 +2490,7 @@ var GraphEntropyAdapter = class {
2188
2490
  }
2189
2491
  findEntryPoints() {
2190
2492
  const entryPoints = [];
2191
- for (const nodeType of CODE_NODE_TYPES3) {
2493
+ for (const nodeType of CODE_NODE_TYPES4) {
2192
2494
  const nodes = this.store.findNodes({ type: nodeType });
2193
2495
  for (const node of nodes) {
2194
2496
  const isIndexFile = nodeType === "file" && node.name === "index.ts";
@@ -2224,7 +2526,7 @@ var GraphEntropyAdapter = class {
2224
2526
  }
2225
2527
  collectUnreachableNodes(visited) {
2226
2528
  const unreachableNodes = [];
2227
- for (const nodeType of CODE_NODE_TYPES3) {
2529
+ for (const nodeType of CODE_NODE_TYPES4) {
2228
2530
  const nodes = this.store.findNodes({ type: nodeType });
2229
2531
  for (const node of nodes) {
2230
2532
  if (!visited.has(node.id)) {
@@ -2267,6 +2569,7 @@ var GraphComplexityAdapter = class {
2267
2569
  constructor(store) {
2268
2570
  this.store = store;
2269
2571
  }
2572
+ store;
2270
2573
  /**
2271
2574
  * Compute complexity hotspots by combining cyclomatic complexity with change frequency.
2272
2575
  *
@@ -2354,6 +2657,7 @@ var GraphCouplingAdapter = class {
2354
2657
  constructor(store) {
2355
2658
  this.store = store;
2356
2659
  }
2660
+ store;
2357
2661
  /**
2358
2662
  * Compute coupling data for all file nodes in the graph.
2359
2663
  *
@@ -2423,6 +2727,7 @@ var GraphAnomalyAdapter = class {
2423
2727
  constructor(store) {
2424
2728
  this.store = store;
2425
2729
  }
2730
+ store;
2426
2731
  detect(options) {
2427
2732
  const threshold = options?.threshold != null && options.threshold > 0 ? options.threshold : DEFAULT_THRESHOLD;
2428
2733
  const requestedMetrics = options?.metrics ?? [...DEFAULT_METRICS];
@@ -3041,9 +3346,9 @@ var EntityExtractor = class {
3041
3346
  }
3042
3347
  const pathConsumed = /* @__PURE__ */ new Set();
3043
3348
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
3044
- const path6 = match[0];
3045
- add(path6);
3046
- pathConsumed.add(path6);
3349
+ const path7 = match[0];
3350
+ add(path7);
3351
+ pathConsumed.add(path7);
3047
3352
  }
3048
3353
  const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
3049
3354
  const words = trimmed.split(/\s+/);
@@ -3114,8 +3419,8 @@ var EntityResolver = class {
3114
3419
  if (isPathLike && node.path.includes(raw)) {
3115
3420
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
3116
3421
  }
3117
- const basename4 = node.path.split("/").pop() ?? "";
3118
- if (basename4.includes(raw)) {
3422
+ const basename5 = node.path.split("/").pop() ?? "";
3423
+ if (basename5.includes(raw)) {
3119
3424
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
3120
3425
  }
3121
3426
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -3190,13 +3495,13 @@ var ResponseFormatter = class {
3190
3495
  const context = Array.isArray(d?.context) ? d.context : [];
3191
3496
  const firstEntity = entities[0];
3192
3497
  const nodeType = firstEntity?.node.type ?? "node";
3193
- const path6 = firstEntity?.node.path ?? "unknown";
3498
+ const path7 = firstEntity?.node.path ?? "unknown";
3194
3499
  let neighborCount = 0;
3195
3500
  const firstContext = context[0];
3196
3501
  if (firstContext && Array.isArray(firstContext.nodes)) {
3197
3502
  neighborCount = firstContext.nodes.length;
3198
3503
  }
3199
- return `**${entityName}** is a ${nodeType} at \`${path6}\`. Connected to ${neighborCount} nodes.`;
3504
+ return `**${entityName}** is a ${nodeType} at \`${path7}\`. Connected to ${neighborCount} nodes.`;
3200
3505
  }
3201
3506
  formatAnomaly(data) {
3202
3507
  const d = data;
@@ -3337,7 +3642,7 @@ var PHASE_NODE_TYPES = {
3337
3642
  debug: ["failure", "learning", "function", "method"],
3338
3643
  plan: ["adr", "document", "module", "layer"]
3339
3644
  };
3340
- var CODE_NODE_TYPES4 = /* @__PURE__ */ new Set([
3645
+ var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
3341
3646
  "file",
3342
3647
  "function",
3343
3648
  "class",
@@ -3552,7 +3857,7 @@ var Assembler = class {
3552
3857
  */
3553
3858
  checkCoverage() {
3554
3859
  const codeNodes = [];
3555
- for (const type of CODE_NODE_TYPES4) {
3860
+ for (const type of CODE_NODE_TYPES5) {
3556
3861
  codeNodes.push(...this.store.findNodes({ type }));
3557
3862
  }
3558
3863
  const documented = [];
@@ -3576,6 +3881,89 @@ var Assembler = class {
3576
3881
  }
3577
3882
  };
3578
3883
 
3884
+ // src/query/Traceability.ts
3885
+ function queryTraceability(store, options) {
3886
+ const allRequirements = store.findNodes({ type: "requirement" });
3887
+ const filtered = allRequirements.filter((node) => {
3888
+ if (options?.specPath && node.metadata?.specPath !== options.specPath) return false;
3889
+ if (options?.featureName && node.metadata?.featureName !== options.featureName) return false;
3890
+ return true;
3891
+ });
3892
+ if (filtered.length === 0) return [];
3893
+ const groups = /* @__PURE__ */ new Map();
3894
+ for (const req of filtered) {
3895
+ const meta = req.metadata;
3896
+ const specPath = meta?.specPath ?? "";
3897
+ const featureName = meta?.featureName ?? "";
3898
+ const key = `${specPath}\0${featureName}`;
3899
+ const list = groups.get(key);
3900
+ if (list) {
3901
+ list.push(req);
3902
+ } else {
3903
+ groups.set(key, [req]);
3904
+ }
3905
+ }
3906
+ const results = [];
3907
+ for (const [, reqs] of groups) {
3908
+ const firstReq = reqs[0];
3909
+ const firstMeta = firstReq.metadata;
3910
+ const specPath = firstMeta?.specPath ?? "";
3911
+ const featureName = firstMeta?.featureName ?? "";
3912
+ const requirements = [];
3913
+ for (const req of reqs) {
3914
+ const requiresEdges = store.getEdges({ from: req.id, type: "requires" });
3915
+ const codeFiles = requiresEdges.map((edge) => {
3916
+ const targetNode = store.getNode(edge.to);
3917
+ return {
3918
+ path: targetNode?.path ?? edge.to,
3919
+ confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
3920
+ method: edge.metadata?.method ?? "convention"
3921
+ };
3922
+ });
3923
+ const verifiedByEdges = store.getEdges({ from: req.id, type: "verified_by" });
3924
+ const testFiles = verifiedByEdges.map((edge) => {
3925
+ const targetNode = store.getNode(edge.to);
3926
+ return {
3927
+ path: targetNode?.path ?? edge.to,
3928
+ confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
3929
+ method: edge.metadata?.method ?? "convention"
3930
+ };
3931
+ });
3932
+ const hasCode = codeFiles.length > 0;
3933
+ const hasTests = testFiles.length > 0;
3934
+ const status = hasCode && hasTests ? "full" : hasCode ? "code-only" : hasTests ? "test-only" : "none";
3935
+ const allConfidences = [
3936
+ ...codeFiles.map((f) => f.confidence),
3937
+ ...testFiles.map((f) => f.confidence)
3938
+ ];
3939
+ const maxConfidence = allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
3940
+ requirements.push({
3941
+ requirementId: req.id,
3942
+ requirementName: req.name,
3943
+ index: req.metadata?.index ?? 0,
3944
+ codeFiles,
3945
+ testFiles,
3946
+ status,
3947
+ maxConfidence
3948
+ });
3949
+ }
3950
+ requirements.sort((a, b) => a.index - b.index);
3951
+ const total = requirements.length;
3952
+ const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
3953
+ const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
3954
+ const fullyTraced = requirements.filter((r) => r.status === "full").length;
3955
+ const untraceable = requirements.filter((r) => r.status === "none").length;
3956
+ const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
3957
+ results.push({
3958
+ specPath,
3959
+ featureName,
3960
+ requirements,
3961
+ summary: { total, withCode, withTests, fullyTraced, untraceable, coveragePercent }
3962
+ });
3963
+ }
3964
+ return results;
3965
+ }
3966
+
3579
3967
  // src/constraints/GraphConstraintAdapter.ts
3580
3968
  var import_minimatch = require("minimatch");
3581
3969
  var import_node_path2 = require("path");
@@ -3583,6 +3971,7 @@ var GraphConstraintAdapter = class {
3583
3971
  constructor(store) {
3584
3972
  this.store = store;
3585
3973
  }
3974
+ store;
3586
3975
  computeDependencyGraph() {
3587
3976
  const fileNodes = this.store.findNodes({ type: "file" });
3588
3977
  const nodes = fileNodes.map((n) => n.path ?? n.id);
@@ -3634,14 +4023,14 @@ var GraphConstraintAdapter = class {
3634
4023
  };
3635
4024
 
3636
4025
  // src/ingest/DesignIngestor.ts
3637
- var fs4 = __toESM(require("fs/promises"));
3638
- var path5 = __toESM(require("path"));
4026
+ var fs5 = __toESM(require("fs/promises"));
4027
+ var path6 = __toESM(require("path"));
3639
4028
  function isDTCGToken(obj) {
3640
4029
  return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
3641
4030
  }
3642
4031
  async function readFileOrNull(filePath) {
3643
4032
  try {
3644
- return await fs4.readFile(filePath, "utf-8");
4033
+ return await fs5.readFile(filePath, "utf-8");
3645
4034
  } catch {
3646
4035
  return null;
3647
4036
  }
@@ -3727,6 +4116,7 @@ var DesignIngestor = class {
3727
4116
  constructor(store) {
3728
4117
  this.store = store;
3729
4118
  }
4119
+ store;
3730
4120
  async ingestTokens(tokensPath) {
3731
4121
  const start = Date.now();
3732
4122
  const content = await readFileOrNull(tokensPath);
@@ -3786,8 +4176,8 @@ var DesignIngestor = class {
3786
4176
  async ingestAll(designDir) {
3787
4177
  const start = Date.now();
3788
4178
  const [tokensResult, intentResult] = await Promise.all([
3789
- this.ingestTokens(path5.join(designDir, "tokens.json")),
3790
- this.ingestDesignIntent(path5.join(designDir, "DESIGN.md"))
4179
+ this.ingestTokens(path6.join(designDir, "tokens.json")),
4180
+ this.ingestDesignIntent(path6.join(designDir, "DESIGN.md"))
3791
4181
  ]);
3792
4182
  const merged = mergeResults(tokensResult, intentResult);
3793
4183
  return { ...merged, durationMs: Date.now() - start };
@@ -3799,6 +4189,7 @@ var DesignConstraintAdapter = class {
3799
4189
  constructor(store) {
3800
4190
  this.store = store;
3801
4191
  }
4192
+ store;
3802
4193
  checkForHardcodedColors(source, file, strictness) {
3803
4194
  const severity = this.mapSeverity(strictness);
3804
4195
  const tokenNodes = this.store.findNodes({ type: "design_token" });
@@ -3882,6 +4273,7 @@ var GraphFeedbackAdapter = class {
3882
4273
  constructor(store) {
3883
4274
  this.store = store;
3884
4275
  }
4276
+ store;
3885
4277
  computeImpactData(changedFiles) {
3886
4278
  const affectedTests = [];
3887
4279
  const affectedDocs = [];
@@ -4034,10 +4426,10 @@ var TaskIndependenceAnalyzer = class {
4034
4426
  includeTypes: ["file"]
4035
4427
  });
4036
4428
  for (const n of queryResult.nodes) {
4037
- const path6 = n.path ?? n.id.replace(/^file:/, "");
4038
- if (!fileSet.has(path6)) {
4039
- if (!result.has(path6)) {
4040
- result.set(path6, file);
4429
+ const path7 = n.path ?? n.id.replace(/^file:/, "");
4430
+ if (!fileSet.has(path7)) {
4431
+ if (!result.has(path7)) {
4432
+ result.set(path7, file);
4041
4433
  }
4042
4434
  }
4043
4435
  }
@@ -4422,6 +4814,7 @@ var VERSION = "0.2.0";
4422
4814
  KnowledgeIngestor,
4423
4815
  NODE_TYPES,
4424
4816
  OBSERVABILITY_TYPES,
4817
+ RequirementIngestor,
4425
4818
  ResponseFormatter,
4426
4819
  SlackConnector,
4427
4820
  SyncManager,
@@ -4434,5 +4827,6 @@ var VERSION = "0.2.0";
4434
4827
  linkToCode,
4435
4828
  loadGraph,
4436
4829
  project,
4830
+ queryTraceability,
4437
4831
  saveGraph
4438
4832
  });