@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.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;
@@ -94,9 +100,6 @@ var GraphEdgeSchema = z.object({
94
100
  metadata: z.record(z.unknown()).optional()
95
101
  });
96
102
 
97
- // src/store/GraphStore.ts
98
- import loki from "lokijs";
99
-
100
103
  // src/store/Serializer.ts
101
104
  import { readFile, writeFile, mkdir, access } from "fs/promises";
102
105
  import { join } from "path";
@@ -141,28 +144,38 @@ function safeMerge(target, source) {
141
144
  }
142
145
  }
143
146
  }
144
- var GraphStore = class {
145
- db;
146
- nodes;
147
- edges;
148
- constructor() {
149
- this.db = new loki("graph.db");
150
- this.nodes = this.db.addCollection("nodes", {
151
- unique: ["id"],
152
- indices: ["type", "name"]
153
- });
154
- this.edges = this.db.addCollection("edges", {
155
- indices: ["from", "to", "type"]
156
- });
147
+ function edgeKey(from, to, type) {
148
+ return `${from}\0${to}\0${type}`;
149
+ }
150
+ function addToIndex(index, key, edge) {
151
+ const list = index.get(key);
152
+ if (list) {
153
+ list.push(edge);
154
+ } else {
155
+ index.set(key, [edge]);
157
156
  }
157
+ }
158
+ function removeFromIndex(index, key, edge) {
159
+ const list = index.get(key);
160
+ if (!list) return;
161
+ const idx = list.indexOf(edge);
162
+ if (idx !== -1) list.splice(idx, 1);
163
+ if (list.length === 0) index.delete(key);
164
+ }
165
+ var GraphStore = class {
166
+ nodeMap = /* @__PURE__ */ new Map();
167
+ edgeMap = /* @__PURE__ */ new Map();
168
+ // keyed by from\0to\0type
169
+ edgesByFrom = /* @__PURE__ */ new Map();
170
+ edgesByTo = /* @__PURE__ */ new Map();
171
+ edgesByType = /* @__PURE__ */ new Map();
158
172
  // --- Node operations ---
159
173
  addNode(node) {
160
- const existing = this.nodes.by("id", node.id);
174
+ const existing = this.nodeMap.get(node.id);
161
175
  if (existing) {
162
176
  safeMerge(existing, node);
163
- this.nodes.update(existing);
164
177
  } else {
165
- this.nodes.insert({ ...node });
178
+ this.nodeMap.set(node.id, { ...node });
166
179
  }
167
180
  }
168
181
  batchAddNodes(nodes) {
@@ -171,44 +184,44 @@ var GraphStore = class {
171
184
  }
172
185
  }
173
186
  getNode(id) {
174
- const doc = this.nodes.by("id", id);
175
- if (!doc) return null;
176
- return this.stripLokiMeta(doc);
187
+ const node = this.nodeMap.get(id);
188
+ if (!node) return null;
189
+ return { ...node };
177
190
  }
178
191
  findNodes(query) {
179
- const lokiQuery = {};
180
- if (query.type !== void 0) lokiQuery["type"] = query.type;
181
- if (query.name !== void 0) lokiQuery["name"] = query.name;
182
- if (query.path !== void 0) lokiQuery["path"] = query.path;
183
- return this.nodes.find(lokiQuery).map((doc) => this.stripLokiMeta(doc));
192
+ const results = [];
193
+ for (const node of this.nodeMap.values()) {
194
+ if (query.type !== void 0 && node.type !== query.type) continue;
195
+ if (query.name !== void 0 && node.name !== query.name) continue;
196
+ if (query.path !== void 0 && node.path !== query.path) continue;
197
+ results.push({ ...node });
198
+ }
199
+ return results;
184
200
  }
185
201
  removeNode(id) {
186
- const doc = this.nodes.by("id", id);
187
- if (doc) {
188
- this.nodes.remove(doc);
189
- }
190
- const edgesToRemove = this.edges.find({
191
- $or: [{ from: id }, { to: id }]
192
- });
202
+ this.nodeMap.delete(id);
203
+ const fromEdges = this.edgesByFrom.get(id) ?? [];
204
+ const toEdges = this.edgesByTo.get(id) ?? [];
205
+ const edgesToRemove = /* @__PURE__ */ new Set([...fromEdges, ...toEdges]);
193
206
  for (const edge of edgesToRemove) {
194
- this.edges.remove(edge);
207
+ this.removeEdgeInternal(edge);
195
208
  }
196
209
  }
197
210
  // --- Edge operations ---
198
211
  addEdge(edge) {
199
- const existing = this.edges.findOne({
200
- from: edge.from,
201
- to: edge.to,
202
- type: edge.type
203
- });
212
+ const key = edgeKey(edge.from, edge.to, edge.type);
213
+ const existing = this.edgeMap.get(key);
204
214
  if (existing) {
205
215
  if (edge.metadata) {
206
216
  safeMerge(existing, edge);
207
- this.edges.update(existing);
208
217
  }
209
218
  return;
210
219
  }
211
- this.edges.insert({ ...edge });
220
+ const copy = { ...edge };
221
+ this.edgeMap.set(key, copy);
222
+ addToIndex(this.edgesByFrom, edge.from, copy);
223
+ addToIndex(this.edgesByTo, edge.to, copy);
224
+ addToIndex(this.edgesByType, edge.type, copy);
212
225
  }
213
226
  batchAddEdges(edges) {
214
227
  for (const edge of edges) {
@@ -216,22 +229,38 @@ var GraphStore = class {
216
229
  }
217
230
  }
218
231
  getEdges(query) {
219
- const lokiQuery = {};
220
- if (query.from !== void 0) lokiQuery["from"] = query.from;
221
- if (query.to !== void 0) lokiQuery["to"] = query.to;
222
- if (query.type !== void 0) lokiQuery["type"] = query.type;
223
- return this.edges.find(lokiQuery).map((doc) => this.stripLokiMeta(doc));
232
+ let candidates;
233
+ if (query.from !== void 0 && query.to !== void 0 && query.type !== void 0) {
234
+ const edge = this.edgeMap.get(edgeKey(query.from, query.to, query.type));
235
+ return edge ? [{ ...edge }] : [];
236
+ } else if (query.from !== void 0) {
237
+ candidates = this.edgesByFrom.get(query.from) ?? [];
238
+ } else if (query.to !== void 0) {
239
+ candidates = this.edgesByTo.get(query.to) ?? [];
240
+ } else if (query.type !== void 0) {
241
+ candidates = this.edgesByType.get(query.type) ?? [];
242
+ } else {
243
+ candidates = this.edgeMap.values();
244
+ }
245
+ const results = [];
246
+ for (const edge of candidates) {
247
+ if (query.from !== void 0 && edge.from !== query.from) continue;
248
+ if (query.to !== void 0 && edge.to !== query.to) continue;
249
+ if (query.type !== void 0 && edge.type !== query.type) continue;
250
+ results.push({ ...edge });
251
+ }
252
+ return results;
224
253
  }
225
254
  getNeighbors(nodeId, direction = "both") {
226
255
  const neighborIds = /* @__PURE__ */ new Set();
227
256
  if (direction === "outbound" || direction === "both") {
228
- const outEdges = this.edges.find({ from: nodeId });
257
+ const outEdges = this.edgesByFrom.get(nodeId) ?? [];
229
258
  for (const edge of outEdges) {
230
259
  neighborIds.add(edge.to);
231
260
  }
232
261
  }
233
262
  if (direction === "inbound" || direction === "both") {
234
- const inEdges = this.edges.find({ to: nodeId });
263
+ const inEdges = this.edgesByTo.get(nodeId) ?? [];
235
264
  for (const edge of inEdges) {
236
265
  neighborIds.add(edge.from);
237
266
  }
@@ -245,20 +274,23 @@ var GraphStore = class {
245
274
  }
246
275
  // --- Counts ---
247
276
  get nodeCount() {
248
- return this.nodes.count();
277
+ return this.nodeMap.size;
249
278
  }
250
279
  get edgeCount() {
251
- return this.edges.count();
280
+ return this.edgeMap.size;
252
281
  }
253
282
  // --- Clear ---
254
283
  clear() {
255
- this.nodes.clear();
256
- this.edges.clear();
284
+ this.nodeMap.clear();
285
+ this.edgeMap.clear();
286
+ this.edgesByFrom.clear();
287
+ this.edgesByTo.clear();
288
+ this.edgesByType.clear();
257
289
  }
258
290
  // --- Persistence ---
259
291
  async save(dirPath) {
260
- const allNodes = this.nodes.find().map((doc) => this.stripLokiMeta(doc));
261
- const allEdges = this.edges.find().map((doc) => this.stripLokiMeta(doc));
292
+ const allNodes = Array.from(this.nodeMap.values()).map((n) => ({ ...n }));
293
+ const allEdges = Array.from(this.edgeMap.values()).map((e) => ({ ...e }));
262
294
  await saveGraph(dirPath, allNodes, allEdges);
263
295
  }
264
296
  async load(dirPath) {
@@ -266,17 +298,25 @@ var GraphStore = class {
266
298
  if (!data) return false;
267
299
  this.clear();
268
300
  for (const node of data.nodes) {
269
- this.nodes.insert({ ...node });
301
+ this.nodeMap.set(node.id, { ...node });
270
302
  }
271
303
  for (const edge of data.edges) {
272
- this.edges.insert({ ...edge });
304
+ const copy = { ...edge };
305
+ const key = edgeKey(edge.from, edge.to, edge.type);
306
+ this.edgeMap.set(key, copy);
307
+ addToIndex(this.edgesByFrom, edge.from, copy);
308
+ addToIndex(this.edgesByTo, edge.to, copy);
309
+ addToIndex(this.edgesByType, edge.type, copy);
273
310
  }
274
311
  return true;
275
312
  }
276
313
  // --- Internal ---
277
- stripLokiMeta(doc) {
278
- const { $loki: _, meta: _meta, ...rest } = doc;
279
- return rest;
314
+ removeEdgeInternal(edge) {
315
+ const key = edgeKey(edge.from, edge.to, edge.type);
316
+ this.edgeMap.delete(key);
317
+ removeFromIndex(this.edgesByFrom, edge.from, edge);
318
+ removeFromIndex(this.edgesByTo, edge.to, edge);
319
+ removeFromIndex(this.edgesByType, edge.type, edge);
280
320
  }
281
321
  };
282
322
 
@@ -357,11 +397,11 @@ var VectorStore = class _VectorStore {
357
397
  };
358
398
 
359
399
  // src/query/ContextQL.ts
360
- function edgeKey(e) {
400
+ function edgeKey2(e) {
361
401
  return `${e.from}|${e.to}|${e.type}`;
362
402
  }
363
403
  function addEdge(state, edge) {
364
- const key = edgeKey(edge);
404
+ const key = edgeKey2(edge);
365
405
  if (!state.edgeSet.has(key)) {
366
406
  state.edgeSet.add(key);
367
407
  state.resultEdges.push(edge);
@@ -541,6 +581,7 @@ var CodeIngestor = class {
541
581
  constructor(store) {
542
582
  this.store = store;
543
583
  }
584
+ store;
544
585
  async ingest(rootDir) {
545
586
  const start = Date.now();
546
587
  const errors = [];
@@ -563,6 +604,7 @@ var CodeIngestor = class {
563
604
  this.store.addEdge(edge);
564
605
  edgesAdded++;
565
606
  }
607
+ edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
566
608
  return {
567
609
  nodesAdded,
568
610
  nodesUpdated: 0,
@@ -921,6 +963,48 @@ var CodeIngestor = class {
921
963
  if (/\.jsx?$/.test(filePath)) return "javascript";
922
964
  return "unknown";
923
965
  }
966
+ /**
967
+ * Scan file contents for @req annotations and create verified_by edges
968
+ * linking requirement nodes to the annotated files.
969
+ * Format: // @req <feature-name>#<index>
970
+ */
971
+ extractReqAnnotations(fileContents, rootDir) {
972
+ const REQ_TAG = /\/\/\s*@req\s+([\w-]+)#(\d+)/g;
973
+ const reqNodes = this.store.findNodes({ type: "requirement" });
974
+ let edgesAdded = 0;
975
+ for (const [filePath, content] of fileContents) {
976
+ let match;
977
+ REQ_TAG.lastIndex = 0;
978
+ while ((match = REQ_TAG.exec(content)) !== null) {
979
+ const featureName = match[1];
980
+ const reqIndex = parseInt(match[2], 10);
981
+ const reqNode = reqNodes.find(
982
+ (n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
983
+ );
984
+ if (!reqNode) {
985
+ console.warn(
986
+ `@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
987
+ );
988
+ continue;
989
+ }
990
+ const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
991
+ const fileNodeId = `file:${relPath}`;
992
+ this.store.addEdge({
993
+ from: reqNode.id,
994
+ to: fileNodeId,
995
+ type: "verified_by",
996
+ confidence: 1,
997
+ metadata: {
998
+ method: "annotation",
999
+ tag: `@req ${featureName}#${reqIndex}`,
1000
+ confidence: 1
1001
+ }
1002
+ });
1003
+ edgesAdded++;
1004
+ }
1005
+ }
1006
+ return edgesAdded;
1007
+ }
924
1008
  };
925
1009
 
926
1010
  // src/ingest/GitIngestor.ts
@@ -932,6 +1016,8 @@ var GitIngestor = class {
932
1016
  this.store = store;
933
1017
  this.gitRunner = gitRunner;
934
1018
  }
1019
+ store;
1020
+ gitRunner;
935
1021
  async ingest(rootDir) {
936
1022
  const start = Date.now();
937
1023
  const errors = [];
@@ -1111,6 +1197,7 @@ var TopologicalLinker = class {
1111
1197
  constructor(store) {
1112
1198
  this.store = store;
1113
1199
  }
1200
+ store;
1114
1201
  link() {
1115
1202
  let edgesAdded = 0;
1116
1203
  const files = this.store.findNodes({ type: "file" });
@@ -1208,6 +1295,7 @@ var KnowledgeIngestor = class {
1208
1295
  constructor(store) {
1209
1296
  this.store = store;
1210
1297
  }
1298
+ store;
1211
1299
  async ingestADRs(adrDir) {
1212
1300
  const start = Date.now();
1213
1301
  const errors = [];
@@ -1393,8 +1481,218 @@ var KnowledgeIngestor = class {
1393
1481
  }
1394
1482
  };
1395
1483
 
1396
- // src/ingest/connectors/ConnectorUtils.ts
1484
+ // src/ingest/RequirementIngestor.ts
1485
+ import * as fs3 from "fs/promises";
1486
+ import * as path4 from "path";
1487
+ var REQUIREMENT_SECTIONS = [
1488
+ "Observable Truths",
1489
+ "Success Criteria",
1490
+ "Acceptance Criteria"
1491
+ ];
1492
+ var SECTION_HEADING_RE = /^#{2,3}\s+(.+)$/;
1493
+ var NUMBERED_ITEM_RE = /^\s*(\d+)\.\s+(.+)$/;
1494
+ function detectEarsPattern(text) {
1495
+ const lower = text.toLowerCase();
1496
+ if (/^if\b.+\bthen\b.+\bshall not\b/.test(lower)) return "unwanted";
1497
+ if (/^when\b/.test(lower)) return "event-driven";
1498
+ if (/^while\b/.test(lower)) return "state-driven";
1499
+ if (/^where\b/.test(lower)) return "optional";
1500
+ if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
1501
+ return void 0;
1502
+ }
1397
1503
  var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
1504
+ var RequirementIngestor = class {
1505
+ constructor(store) {
1506
+ this.store = store;
1507
+ }
1508
+ store;
1509
+ /**
1510
+ * Scan a specs directory for `<feature>/proposal.md` files,
1511
+ * extract numbered requirements from recognized sections,
1512
+ * and create requirement nodes with convention-based edges.
1513
+ */
1514
+ async ingestSpecs(specsDir) {
1515
+ const start = Date.now();
1516
+ const errors = [];
1517
+ let nodesAdded = 0;
1518
+ let edgesAdded = 0;
1519
+ let featureDirs;
1520
+ try {
1521
+ const entries = await fs3.readdir(specsDir, { withFileTypes: true });
1522
+ featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path4.join(specsDir, e.name));
1523
+ } catch {
1524
+ return emptyResult(Date.now() - start);
1525
+ }
1526
+ for (const featureDir of featureDirs) {
1527
+ const featureName = path4.basename(featureDir);
1528
+ const specPath = path4.join(featureDir, "proposal.md");
1529
+ let content;
1530
+ try {
1531
+ content = await fs3.readFile(specPath, "utf-8");
1532
+ } catch {
1533
+ continue;
1534
+ }
1535
+ try {
1536
+ const specHash = hash(specPath);
1537
+ const specNodeId = `file:${specPath}`;
1538
+ this.store.addNode({
1539
+ id: specNodeId,
1540
+ type: "document",
1541
+ name: path4.basename(specPath),
1542
+ path: specPath,
1543
+ metadata: { featureName }
1544
+ });
1545
+ const requirements = this.extractRequirements(content, specPath, specHash, featureName);
1546
+ for (const req of requirements) {
1547
+ this.store.addNode(req.node);
1548
+ nodesAdded++;
1549
+ this.store.addEdge({
1550
+ from: req.node.id,
1551
+ to: specNodeId,
1552
+ type: "specifies"
1553
+ });
1554
+ edgesAdded++;
1555
+ edgesAdded += this.linkByPathPattern(req.node.id, featureName);
1556
+ edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
1557
+ }
1558
+ } catch (err) {
1559
+ errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
1560
+ }
1561
+ }
1562
+ return {
1563
+ nodesAdded,
1564
+ nodesUpdated: 0,
1565
+ edgesAdded,
1566
+ edgesUpdated: 0,
1567
+ errors,
1568
+ durationMs: Date.now() - start
1569
+ };
1570
+ }
1571
+ /**
1572
+ * Parse markdown content and extract numbered items from recognized sections.
1573
+ */
1574
+ extractRequirements(content, specPath, specHash, featureName) {
1575
+ const lines = content.split("\n");
1576
+ const results = [];
1577
+ let currentSection;
1578
+ let inRequirementSection = false;
1579
+ let globalIndex = 0;
1580
+ for (let i = 0; i < lines.length; i++) {
1581
+ const line = lines[i];
1582
+ const headingMatch = line.match(SECTION_HEADING_RE);
1583
+ if (headingMatch) {
1584
+ const heading = headingMatch[1].trim();
1585
+ const isReqSection = REQUIREMENT_SECTIONS.some(
1586
+ (s) => heading.toLowerCase() === s.toLowerCase()
1587
+ );
1588
+ if (isReqSection) {
1589
+ currentSection = heading;
1590
+ inRequirementSection = true;
1591
+ } else {
1592
+ inRequirementSection = false;
1593
+ }
1594
+ continue;
1595
+ }
1596
+ if (!inRequirementSection) continue;
1597
+ const itemMatch = line.match(NUMBERED_ITEM_RE);
1598
+ if (!itemMatch) continue;
1599
+ const index = parseInt(itemMatch[1], 10);
1600
+ const text = itemMatch[2].trim();
1601
+ const rawText = line.trim();
1602
+ const lineNumber = i + 1;
1603
+ globalIndex++;
1604
+ const nodeId = `req:${specHash}:${globalIndex}`;
1605
+ const earsPattern = detectEarsPattern(text);
1606
+ results.push({
1607
+ node: {
1608
+ id: nodeId,
1609
+ type: "requirement",
1610
+ name: text,
1611
+ path: specPath,
1612
+ location: {
1613
+ fileId: `file:${specPath}`,
1614
+ startLine: lineNumber,
1615
+ endLine: lineNumber
1616
+ },
1617
+ metadata: {
1618
+ specPath,
1619
+ index,
1620
+ section: currentSection,
1621
+ rawText,
1622
+ earsPattern,
1623
+ featureName
1624
+ }
1625
+ }
1626
+ });
1627
+ }
1628
+ return results;
1629
+ }
1630
+ /**
1631
+ * Convention-based linking: match requirement to code/test files
1632
+ * by feature name in their path.
1633
+ */
1634
+ linkByPathPattern(reqId, featureName) {
1635
+ let count = 0;
1636
+ const fileNodes = this.store.findNodes({ type: "file" });
1637
+ for (const node of fileNodes) {
1638
+ if (!node.path) continue;
1639
+ const normalizedPath = node.path.replace(/\\/g, "/");
1640
+ const isCodeMatch = normalizedPath.includes("packages/") && path4.basename(normalizedPath).includes(featureName);
1641
+ const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
1642
+ path4.basename(normalizedPath).includes(featureName);
1643
+ if (isCodeMatch && !isTestMatch) {
1644
+ this.store.addEdge({
1645
+ from: reqId,
1646
+ to: node.id,
1647
+ type: "requires",
1648
+ confidence: 0.5,
1649
+ metadata: { method: "convention", matchReason: "path-pattern" }
1650
+ });
1651
+ count++;
1652
+ } else if (isTestMatch) {
1653
+ this.store.addEdge({
1654
+ from: reqId,
1655
+ to: node.id,
1656
+ type: "verified_by",
1657
+ confidence: 0.5,
1658
+ metadata: { method: "convention", matchReason: "path-pattern" }
1659
+ });
1660
+ count++;
1661
+ }
1662
+ }
1663
+ return count;
1664
+ }
1665
+ /**
1666
+ * Convention-based linking: match requirement text to code nodes
1667
+ * by keyword overlap (function/class names appearing in requirement text).
1668
+ */
1669
+ linkByKeywordOverlap(reqId, reqText) {
1670
+ let count = 0;
1671
+ for (const nodeType of CODE_NODE_TYPES2) {
1672
+ const codeNodes = this.store.findNodes({ type: nodeType });
1673
+ for (const node of codeNodes) {
1674
+ if (node.name.length < 3) continue;
1675
+ const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1676
+ const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
1677
+ if (namePattern.test(reqText)) {
1678
+ const edgeType = node.path && node.path.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
1679
+ this.store.addEdge({
1680
+ from: reqId,
1681
+ to: node.id,
1682
+ type: edgeType,
1683
+ confidence: 0.6,
1684
+ metadata: { method: "convention", matchReason: "keyword-overlap" }
1685
+ });
1686
+ count++;
1687
+ }
1688
+ }
1689
+ }
1690
+ return count;
1691
+ }
1692
+ };
1693
+
1694
+ // src/ingest/connectors/ConnectorUtils.ts
1695
+ var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
1398
1696
  var SANITIZE_RULES = [
1399
1697
  // Strip XML/HTML-like instruction tags that could be interpreted as system prompts
1400
1698
  {
@@ -1429,7 +1727,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
1429
1727
  }
1430
1728
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
1431
1729
  let edgesCreated = 0;
1432
- for (const type of CODE_NODE_TYPES2) {
1730
+ for (const type of CODE_NODE_TYPES3) {
1433
1731
  const nodes = store.findNodes({ type });
1434
1732
  for (const node of nodes) {
1435
1733
  if (node.name.length < 3) continue;
@@ -1449,13 +1747,14 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
1449
1747
  }
1450
1748
 
1451
1749
  // src/ingest/connectors/SyncManager.ts
1452
- import * as fs3 from "fs/promises";
1453
- import * as path4 from "path";
1750
+ import * as fs4 from "fs/promises";
1751
+ import * as path5 from "path";
1454
1752
  var SyncManager = class {
1455
1753
  constructor(store, graphDir) {
1456
1754
  this.store = store;
1457
- this.metadataPath = path4.join(graphDir, "sync-metadata.json");
1755
+ this.metadataPath = path5.join(graphDir, "sync-metadata.json");
1458
1756
  }
1757
+ store;
1459
1758
  registrations = /* @__PURE__ */ new Map();
1460
1759
  metadataPath;
1461
1760
  registerConnector(connector, config) {
@@ -1508,15 +1807,15 @@ var SyncManager = class {
1508
1807
  }
1509
1808
  async loadMetadata() {
1510
1809
  try {
1511
- const raw = await fs3.readFile(this.metadataPath, "utf-8");
1810
+ const raw = await fs4.readFile(this.metadataPath, "utf-8");
1512
1811
  return JSON.parse(raw);
1513
1812
  } catch {
1514
1813
  return { connectors: {} };
1515
1814
  }
1516
1815
  }
1517
1816
  async saveMetadata(metadata) {
1518
- await fs3.mkdir(path4.dirname(this.metadataPath), { recursive: true });
1519
- await fs3.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
1817
+ await fs4.mkdir(path5.dirname(this.metadataPath), { recursive: true });
1818
+ await fs4.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
1520
1819
  }
1521
1820
  };
1522
1821
 
@@ -2045,11 +2344,12 @@ var FusionLayer = class {
2045
2344
  };
2046
2345
 
2047
2346
  // src/entropy/GraphEntropyAdapter.ts
2048
- var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
2347
+ var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
2049
2348
  var GraphEntropyAdapter = class {
2050
2349
  constructor(store) {
2051
2350
  this.store = store;
2052
2351
  }
2352
+ store;
2053
2353
  /**
2054
2354
  * Find all `documents` edges and classify them as stale or missing-target.
2055
2355
  *
@@ -2111,7 +2411,7 @@ var GraphEntropyAdapter = class {
2111
2411
  }
2112
2412
  findEntryPoints() {
2113
2413
  const entryPoints = [];
2114
- for (const nodeType of CODE_NODE_TYPES3) {
2414
+ for (const nodeType of CODE_NODE_TYPES4) {
2115
2415
  const nodes = this.store.findNodes({ type: nodeType });
2116
2416
  for (const node of nodes) {
2117
2417
  const isIndexFile = nodeType === "file" && node.name === "index.ts";
@@ -2147,7 +2447,7 @@ var GraphEntropyAdapter = class {
2147
2447
  }
2148
2448
  collectUnreachableNodes(visited) {
2149
2449
  const unreachableNodes = [];
2150
- for (const nodeType of CODE_NODE_TYPES3) {
2450
+ for (const nodeType of CODE_NODE_TYPES4) {
2151
2451
  const nodes = this.store.findNodes({ type: nodeType });
2152
2452
  for (const node of nodes) {
2153
2453
  if (!visited.has(node.id)) {
@@ -2190,6 +2490,7 @@ var GraphComplexityAdapter = class {
2190
2490
  constructor(store) {
2191
2491
  this.store = store;
2192
2492
  }
2493
+ store;
2193
2494
  /**
2194
2495
  * Compute complexity hotspots by combining cyclomatic complexity with change frequency.
2195
2496
  *
@@ -2277,6 +2578,7 @@ var GraphCouplingAdapter = class {
2277
2578
  constructor(store) {
2278
2579
  this.store = store;
2279
2580
  }
2581
+ store;
2280
2582
  /**
2281
2583
  * Compute coupling data for all file nodes in the graph.
2282
2584
  *
@@ -2346,6 +2648,7 @@ var GraphAnomalyAdapter = class {
2346
2648
  constructor(store) {
2347
2649
  this.store = store;
2348
2650
  }
2651
+ store;
2349
2652
  detect(options) {
2350
2653
  const threshold = options?.threshold != null && options.threshold > 0 ? options.threshold : DEFAULT_THRESHOLD;
2351
2654
  const requestedMetrics = options?.metrics ?? [...DEFAULT_METRICS];
@@ -2964,9 +3267,9 @@ var EntityExtractor = class {
2964
3267
  }
2965
3268
  const pathConsumed = /* @__PURE__ */ new Set();
2966
3269
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
2967
- const path6 = match[0];
2968
- add(path6);
2969
- pathConsumed.add(path6);
3270
+ const path7 = match[0];
3271
+ add(path7);
3272
+ pathConsumed.add(path7);
2970
3273
  }
2971
3274
  const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
2972
3275
  const words = trimmed.split(/\s+/);
@@ -3037,8 +3340,8 @@ var EntityResolver = class {
3037
3340
  if (isPathLike && node.path.includes(raw)) {
3038
3341
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
3039
3342
  }
3040
- const basename4 = node.path.split("/").pop() ?? "";
3041
- if (basename4.includes(raw)) {
3343
+ const basename5 = node.path.split("/").pop() ?? "";
3344
+ if (basename5.includes(raw)) {
3042
3345
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
3043
3346
  }
3044
3347
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -3113,13 +3416,13 @@ var ResponseFormatter = class {
3113
3416
  const context = Array.isArray(d?.context) ? d.context : [];
3114
3417
  const firstEntity = entities[0];
3115
3418
  const nodeType = firstEntity?.node.type ?? "node";
3116
- const path6 = firstEntity?.node.path ?? "unknown";
3419
+ const path7 = firstEntity?.node.path ?? "unknown";
3117
3420
  let neighborCount = 0;
3118
3421
  const firstContext = context[0];
3119
3422
  if (firstContext && Array.isArray(firstContext.nodes)) {
3120
3423
  neighborCount = firstContext.nodes.length;
3121
3424
  }
3122
- return `**${entityName}** is a ${nodeType} at \`${path6}\`. Connected to ${neighborCount} nodes.`;
3425
+ return `**${entityName}** is a ${nodeType} at \`${path7}\`. Connected to ${neighborCount} nodes.`;
3123
3426
  }
3124
3427
  formatAnomaly(data) {
3125
3428
  const d = data;
@@ -3260,7 +3563,7 @@ var PHASE_NODE_TYPES = {
3260
3563
  debug: ["failure", "learning", "function", "method"],
3261
3564
  plan: ["adr", "document", "module", "layer"]
3262
3565
  };
3263
- var CODE_NODE_TYPES4 = /* @__PURE__ */ new Set([
3566
+ var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
3264
3567
  "file",
3265
3568
  "function",
3266
3569
  "class",
@@ -3475,7 +3778,7 @@ var Assembler = class {
3475
3778
  */
3476
3779
  checkCoverage() {
3477
3780
  const codeNodes = [];
3478
- for (const type of CODE_NODE_TYPES4) {
3781
+ for (const type of CODE_NODE_TYPES5) {
3479
3782
  codeNodes.push(...this.store.findNodes({ type }));
3480
3783
  }
3481
3784
  const documented = [];
@@ -3499,6 +3802,89 @@ var Assembler = class {
3499
3802
  }
3500
3803
  };
3501
3804
 
3805
+ // src/query/Traceability.ts
3806
+ function queryTraceability(store, options) {
3807
+ const allRequirements = store.findNodes({ type: "requirement" });
3808
+ const filtered = allRequirements.filter((node) => {
3809
+ if (options?.specPath && node.metadata?.specPath !== options.specPath) return false;
3810
+ if (options?.featureName && node.metadata?.featureName !== options.featureName) return false;
3811
+ return true;
3812
+ });
3813
+ if (filtered.length === 0) return [];
3814
+ const groups = /* @__PURE__ */ new Map();
3815
+ for (const req of filtered) {
3816
+ const meta = req.metadata;
3817
+ const specPath = meta?.specPath ?? "";
3818
+ const featureName = meta?.featureName ?? "";
3819
+ const key = `${specPath}\0${featureName}`;
3820
+ const list = groups.get(key);
3821
+ if (list) {
3822
+ list.push(req);
3823
+ } else {
3824
+ groups.set(key, [req]);
3825
+ }
3826
+ }
3827
+ const results = [];
3828
+ for (const [, reqs] of groups) {
3829
+ const firstReq = reqs[0];
3830
+ const firstMeta = firstReq.metadata;
3831
+ const specPath = firstMeta?.specPath ?? "";
3832
+ const featureName = firstMeta?.featureName ?? "";
3833
+ const requirements = [];
3834
+ for (const req of reqs) {
3835
+ const requiresEdges = store.getEdges({ from: req.id, type: "requires" });
3836
+ const codeFiles = requiresEdges.map((edge) => {
3837
+ const targetNode = store.getNode(edge.to);
3838
+ return {
3839
+ path: targetNode?.path ?? edge.to,
3840
+ confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
3841
+ method: edge.metadata?.method ?? "convention"
3842
+ };
3843
+ });
3844
+ const verifiedByEdges = store.getEdges({ from: req.id, type: "verified_by" });
3845
+ const testFiles = verifiedByEdges.map((edge) => {
3846
+ const targetNode = store.getNode(edge.to);
3847
+ return {
3848
+ path: targetNode?.path ?? edge.to,
3849
+ confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
3850
+ method: edge.metadata?.method ?? "convention"
3851
+ };
3852
+ });
3853
+ const hasCode = codeFiles.length > 0;
3854
+ const hasTests = testFiles.length > 0;
3855
+ const status = hasCode && hasTests ? "full" : hasCode ? "code-only" : hasTests ? "test-only" : "none";
3856
+ const allConfidences = [
3857
+ ...codeFiles.map((f) => f.confidence),
3858
+ ...testFiles.map((f) => f.confidence)
3859
+ ];
3860
+ const maxConfidence = allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
3861
+ requirements.push({
3862
+ requirementId: req.id,
3863
+ requirementName: req.name,
3864
+ index: req.metadata?.index ?? 0,
3865
+ codeFiles,
3866
+ testFiles,
3867
+ status,
3868
+ maxConfidence
3869
+ });
3870
+ }
3871
+ requirements.sort((a, b) => a.index - b.index);
3872
+ const total = requirements.length;
3873
+ const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
3874
+ const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
3875
+ const fullyTraced = requirements.filter((r) => r.status === "full").length;
3876
+ const untraceable = requirements.filter((r) => r.status === "none").length;
3877
+ const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
3878
+ results.push({
3879
+ specPath,
3880
+ featureName,
3881
+ requirements,
3882
+ summary: { total, withCode, withTests, fullyTraced, untraceable, coveragePercent }
3883
+ });
3884
+ }
3885
+ return results;
3886
+ }
3887
+
3502
3888
  // src/constraints/GraphConstraintAdapter.ts
3503
3889
  import { minimatch } from "minimatch";
3504
3890
  import { relative as relative2 } from "path";
@@ -3506,6 +3892,7 @@ var GraphConstraintAdapter = class {
3506
3892
  constructor(store) {
3507
3893
  this.store = store;
3508
3894
  }
3895
+ store;
3509
3896
  computeDependencyGraph() {
3510
3897
  const fileNodes = this.store.findNodes({ type: "file" });
3511
3898
  const nodes = fileNodes.map((n) => n.path ?? n.id);
@@ -3557,14 +3944,14 @@ var GraphConstraintAdapter = class {
3557
3944
  };
3558
3945
 
3559
3946
  // src/ingest/DesignIngestor.ts
3560
- import * as fs4 from "fs/promises";
3561
- import * as path5 from "path";
3947
+ import * as fs5 from "fs/promises";
3948
+ import * as path6 from "path";
3562
3949
  function isDTCGToken(obj) {
3563
3950
  return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
3564
3951
  }
3565
3952
  async function readFileOrNull(filePath) {
3566
3953
  try {
3567
- return await fs4.readFile(filePath, "utf-8");
3954
+ return await fs5.readFile(filePath, "utf-8");
3568
3955
  } catch {
3569
3956
  return null;
3570
3957
  }
@@ -3650,6 +4037,7 @@ var DesignIngestor = class {
3650
4037
  constructor(store) {
3651
4038
  this.store = store;
3652
4039
  }
4040
+ store;
3653
4041
  async ingestTokens(tokensPath) {
3654
4042
  const start = Date.now();
3655
4043
  const content = await readFileOrNull(tokensPath);
@@ -3709,8 +4097,8 @@ var DesignIngestor = class {
3709
4097
  async ingestAll(designDir) {
3710
4098
  const start = Date.now();
3711
4099
  const [tokensResult, intentResult] = await Promise.all([
3712
- this.ingestTokens(path5.join(designDir, "tokens.json")),
3713
- this.ingestDesignIntent(path5.join(designDir, "DESIGN.md"))
4100
+ this.ingestTokens(path6.join(designDir, "tokens.json")),
4101
+ this.ingestDesignIntent(path6.join(designDir, "DESIGN.md"))
3714
4102
  ]);
3715
4103
  const merged = mergeResults(tokensResult, intentResult);
3716
4104
  return { ...merged, durationMs: Date.now() - start };
@@ -3722,6 +4110,7 @@ var DesignConstraintAdapter = class {
3722
4110
  constructor(store) {
3723
4111
  this.store = store;
3724
4112
  }
4113
+ store;
3725
4114
  checkForHardcodedColors(source, file, strictness) {
3726
4115
  const severity = this.mapSeverity(strictness);
3727
4116
  const tokenNodes = this.store.findNodes({ type: "design_token" });
@@ -3805,6 +4194,7 @@ var GraphFeedbackAdapter = class {
3805
4194
  constructor(store) {
3806
4195
  this.store = store;
3807
4196
  }
4197
+ store;
3808
4198
  computeImpactData(changedFiles) {
3809
4199
  const affectedTests = [];
3810
4200
  const affectedDocs = [];
@@ -3957,10 +4347,10 @@ var TaskIndependenceAnalyzer = class {
3957
4347
  includeTypes: ["file"]
3958
4348
  });
3959
4349
  for (const n of queryResult.nodes) {
3960
- const path6 = n.path ?? n.id.replace(/^file:/, "");
3961
- if (!fileSet.has(path6)) {
3962
- if (!result.has(path6)) {
3963
- result.set(path6, file);
4350
+ const path7 = n.path ?? n.id.replace(/^file:/, "");
4351
+ if (!fileSet.has(path7)) {
4352
+ if (!result.has(path7)) {
4353
+ result.set(path7, file);
3964
4354
  }
3965
4355
  }
3966
4356
  }
@@ -4344,6 +4734,7 @@ export {
4344
4734
  KnowledgeIngestor,
4345
4735
  NODE_TYPES,
4346
4736
  OBSERVABILITY_TYPES,
4737
+ RequirementIngestor,
4347
4738
  ResponseFormatter,
4348
4739
  SlackConnector,
4349
4740
  SyncManager,
@@ -4356,5 +4747,6 @@ export {
4356
4747
  linkToCode,
4357
4748
  loadGraph,
4358
4749
  project,
4750
+ queryTraceability,
4359
4751
  saveGraph
4360
4752
  };