@harness-engineering/graph 0.2.0 → 0.2.2

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
@@ -36,10 +36,14 @@ __export(index_exports, {
36
36
  CodeIngestor: () => CodeIngestor,
37
37
  ConfluenceConnector: () => ConfluenceConnector,
38
38
  ContextQL: () => ContextQL,
39
+ DesignConstraintAdapter: () => DesignConstraintAdapter,
40
+ DesignIngestor: () => DesignIngestor,
39
41
  EDGE_TYPES: () => EDGE_TYPES,
40
42
  FusionLayer: () => FusionLayer,
41
43
  GitIngestor: () => GitIngestor,
44
+ GraphComplexityAdapter: () => GraphComplexityAdapter,
42
45
  GraphConstraintAdapter: () => GraphConstraintAdapter,
46
+ GraphCouplingAdapter: () => GraphCouplingAdapter,
43
47
  GraphEdgeSchema: () => GraphEdgeSchema,
44
48
  GraphEntropyAdapter: () => GraphEntropyAdapter,
45
49
  GraphFeedbackAdapter: () => GraphFeedbackAdapter,
@@ -94,7 +98,11 @@ var NODE_TYPES = [
94
98
  "layer",
95
99
  "pattern",
96
100
  "constraint",
97
- "violation"
101
+ "violation",
102
+ // Design
103
+ "design_token",
104
+ "aesthetic_intent",
105
+ "design_constraint"
98
106
  ];
99
107
  var EDGE_TYPES = [
100
108
  // Code relationships
@@ -118,7 +126,12 @@ var EDGE_TYPES = [
118
126
  "failed_in",
119
127
  // Execution relationships (future)
120
128
  "executed_by",
121
- "measured_by"
129
+ "measured_by",
130
+ // Design relationships
131
+ "uses_token",
132
+ "declares_intent",
133
+ "violates_design",
134
+ "platform_binding"
122
135
  ];
123
136
  var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
124
137
  var CURRENT_SCHEMA_VERSION = 1;
@@ -187,6 +200,14 @@ async function loadGraph(dirPath) {
187
200
  }
188
201
 
189
202
  // src/store/GraphStore.ts
203
+ var POISONED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
204
+ function safeMerge(target, source) {
205
+ for (const key of Object.keys(source)) {
206
+ if (!POISONED_KEYS.has(key)) {
207
+ target[key] = source[key];
208
+ }
209
+ }
210
+ }
190
211
  var GraphStore = class {
191
212
  db;
192
213
  nodes;
@@ -205,7 +226,7 @@ var GraphStore = class {
205
226
  addNode(node) {
206
227
  const existing = this.nodes.by("id", node.id);
207
228
  if (existing) {
208
- Object.assign(existing, node);
229
+ safeMerge(existing, node);
209
230
  this.nodes.update(existing);
210
231
  } else {
211
232
  this.nodes.insert({ ...node });
@@ -249,7 +270,7 @@ var GraphStore = class {
249
270
  });
250
271
  if (existing) {
251
272
  if (edge.metadata) {
252
- Object.assign(existing, edge);
273
+ safeMerge(existing, edge);
253
274
  this.edges.update(existing);
254
275
  }
255
276
  return;
@@ -525,7 +546,7 @@ var CodeIngestor = class {
525
546
  let nodesAdded = 0;
526
547
  let edgesAdded = 0;
527
548
  const files = await this.findSourceFiles(rootDir);
528
- const callableNodes = [];
549
+ const nameToFiles = /* @__PURE__ */ new Map();
529
550
  const fileContents = /* @__PURE__ */ new Map();
530
551
  for (const filePath of files) {
531
552
  try {
@@ -551,7 +572,12 @@ var CodeIngestor = class {
551
572
  nodesAdded++;
552
573
  edgesAdded++;
553
574
  if (node.type === "function" || node.type === "method") {
554
- callableNodes.push({ id: node.id, name: node.name, filePath: relativePath });
575
+ let files2 = nameToFiles.get(node.name);
576
+ if (!files2) {
577
+ files2 = /* @__PURE__ */ new Set();
578
+ nameToFiles.set(node.name, files2);
579
+ }
580
+ files2.add(relativePath);
555
581
  }
556
582
  }
557
583
  const imports = await this.extractImports(content, fileId, relativePath, rootDir);
@@ -563,7 +589,7 @@ var CodeIngestor = class {
563
589
  errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
564
590
  }
565
591
  }
566
- const callsEdges = this.extractCallsEdges(callableNodes, fileContents);
592
+ const callsEdges = this.extractCallsEdges(nameToFiles, fileContents);
567
593
  for (const edge of callsEdges) {
568
594
  this.store.addEdge(edge);
569
595
  edgesAdded++;
@@ -611,7 +637,13 @@ var CodeIngestor = class {
611
637
  name,
612
638
  path: relativePath,
613
639
  location: { fileId, startLine: i + 1, endLine },
614
- metadata: { exported: line.includes("export") }
640
+ metadata: {
641
+ exported: line.includes("export"),
642
+ cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
643
+ nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
644
+ lineCount: endLine - i,
645
+ parameterCount: this.countParameters(line)
646
+ }
615
647
  },
616
648
  edge: { from: fileId, to: id, type: "contains" }
617
649
  });
@@ -697,7 +729,14 @@ var CodeIngestor = class {
697
729
  name: methodName,
698
730
  path: relativePath,
699
731
  location: { fileId, startLine: i + 1, endLine },
700
- metadata: { className: currentClassName, exported: false }
732
+ metadata: {
733
+ className: currentClassName,
734
+ exported: false,
735
+ cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
736
+ nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
737
+ lineCount: endLine - i,
738
+ parameterCount: this.countParameters(line)
739
+ }
701
740
  },
702
741
  edge: { from: currentClassId, to: id, type: "contains" }
703
742
  });
@@ -747,43 +786,37 @@ var CodeIngestor = class {
747
786
  return startIndex + 1;
748
787
  }
749
788
  /**
750
- * Second pass: for each function/method, check if its file content calls any other
751
- * known function/method. Creates approximate "calls" edges via regex matching.
789
+ * Second pass: scan each file for identifiers matching known callable names,
790
+ * then create file-to-file "calls" edges. Uses regex heuristic (not AST).
752
791
  */
753
- extractCallsEdges(callableNodes, fileContents) {
792
+ extractCallsEdges(nameToFiles, fileContents) {
754
793
  const edges = [];
755
- const nameToNodes = /* @__PURE__ */ new Map();
756
- for (const callable of callableNodes) {
757
- let arr = nameToNodes.get(callable.name);
758
- if (!arr) {
759
- arr = [];
760
- nameToNodes.set(callable.name, arr);
761
- }
762
- arr.push({ id: callable.id, filePath: callable.filePath });
763
- }
764
- for (const caller of callableNodes) {
765
- const content = fileContents.get(caller.filePath);
766
- if (!content) continue;
767
- for (const [name, targets] of nameToNodes) {
768
- if (name === caller.name) continue;
769
- const callPattern = new RegExp(`\\b${this.escapeRegex(name)}\\s*\\(`, "g");
770
- if (callPattern.test(content)) {
771
- for (const target of targets) {
772
- edges.push({
773
- from: caller.id,
774
- to: target.id,
775
- type: "calls",
776
- metadata: { confidence: "regex" }
777
- });
778
- }
794
+ const seen = /* @__PURE__ */ new Set();
795
+ for (const [filePath, content] of fileContents) {
796
+ const callerFileId = `file:${filePath}`;
797
+ const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
798
+ let match;
799
+ while ((match = callPattern.exec(content)) !== null) {
800
+ const name = match[1];
801
+ const targetFiles = nameToFiles.get(name);
802
+ if (!targetFiles) continue;
803
+ for (const targetFile of targetFiles) {
804
+ if (targetFile === filePath) continue;
805
+ const targetFileId = `file:${targetFile}`;
806
+ const key = `${callerFileId}|${targetFileId}`;
807
+ if (seen.has(key)) continue;
808
+ seen.add(key);
809
+ edges.push({
810
+ from: callerFileId,
811
+ to: targetFileId,
812
+ type: "calls",
813
+ metadata: { confidence: "regex" }
814
+ });
779
815
  }
780
816
  }
781
817
  }
782
818
  return edges;
783
819
  }
784
- escapeRegex(str) {
785
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
786
- }
787
820
  async extractImports(content, fileId, relativePath, rootDir) {
788
821
  const edges = [];
789
822
  const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+)\s+from\s+['"]([^'"]+)['"]/g;
@@ -829,6 +862,46 @@ var CodeIngestor = class {
829
862
  }
830
863
  return null;
831
864
  }
865
+ computeCyclomaticComplexity(lines) {
866
+ let complexity = 1;
867
+ const decisionPattern = /\b(if|else\s+if|while|for|case)\b|\?\s*[^:?]|&&|\|\||catch\b/g;
868
+ for (const line of lines) {
869
+ const trimmed = line.trim();
870
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
871
+ const matches = trimmed.match(decisionPattern);
872
+ if (matches) complexity += matches.length;
873
+ }
874
+ return complexity;
875
+ }
876
+ computeMaxNesting(lines) {
877
+ let maxDepth = 0;
878
+ let currentDepth = 0;
879
+ for (const line of lines) {
880
+ const trimmed = line.trim();
881
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
882
+ for (const ch of trimmed) {
883
+ if (ch === "{") {
884
+ currentDepth++;
885
+ if (currentDepth > maxDepth) maxDepth = currentDepth;
886
+ } else if (ch === "}") {
887
+ currentDepth--;
888
+ }
889
+ }
890
+ }
891
+ return Math.max(0, maxDepth - 1);
892
+ }
893
+ countParameters(declarationLine) {
894
+ const parenMatch = declarationLine.match(/\(([^)]*)\)/);
895
+ if (!parenMatch || !parenMatch[1].trim()) return 0;
896
+ let depth = 0;
897
+ let count = 1;
898
+ for (const ch of parenMatch[1]) {
899
+ if (ch === "<" || ch === "(") depth++;
900
+ else if (ch === ">" || ch === ")") depth--;
901
+ else if (ch === "," && depth === 0) count++;
902
+ }
903
+ return count;
904
+ }
832
905
  detectLanguage(filePath) {
833
906
  if (/\.tsx?$/.test(filePath)) return "typescript";
834
907
  if (/\.jsx?$/.test(filePath)) return "javascript";
@@ -1095,8 +1168,9 @@ var TopologicalLinker = class {
1095
1168
  // src/ingest/KnowledgeIngestor.ts
1096
1169
  var fs2 = __toESM(require("fs/promises"));
1097
1170
  var path3 = __toESM(require("path"));
1171
+
1172
+ // src/ingest/ingestUtils.ts
1098
1173
  var crypto = __toESM(require("crypto"));
1099
- var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
1100
1174
  function hash(text) {
1101
1175
  return crypto.createHash("md5").update(text).digest("hex").slice(0, 8);
1102
1176
  }
@@ -1113,6 +1187,9 @@ function mergeResults(...results) {
1113
1187
  function emptyResult(durationMs = 0) {
1114
1188
  return { nodesAdded: 0, nodesUpdated: 0, edgesAdded: 0, edgesUpdated: 0, errors: [], durationMs };
1115
1189
  }
1190
+
1191
+ // src/ingest/KnowledgeIngestor.ts
1192
+ var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
1116
1193
  var KnowledgeIngestor = class {
1117
1194
  constructor(store) {
1118
1195
  this.store = store;
@@ -1304,6 +1381,22 @@ var KnowledgeIngestor = class {
1304
1381
 
1305
1382
  // src/ingest/connectors/ConnectorUtils.ts
1306
1383
  var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
1384
+ function sanitizeExternalText(text, maxLength = 2e3) {
1385
+ let sanitized = text.replace(
1386
+ /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
1387
+ ""
1388
+ ).replace(/^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim, "").replace(
1389
+ /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
1390
+ "[filtered]"
1391
+ ).replace(
1392
+ /you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
1393
+ "[filtered]"
1394
+ );
1395
+ if (sanitized.length > maxLength) {
1396
+ sanitized = sanitized.slice(0, maxLength) + "\u2026";
1397
+ }
1398
+ return sanitized;
1399
+ }
1307
1400
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
1308
1401
  let edgesCreated = 0;
1309
1402
  for (const type of CODE_NODE_TYPES2) {
@@ -1471,7 +1564,7 @@ var JiraConnector = class {
1471
1564
  store.addNode({
1472
1565
  id: nodeId,
1473
1566
  type: "issue",
1474
- name: issue.fields.summary,
1567
+ name: sanitizeExternalText(issue.fields.summary, 500),
1475
1568
  metadata: {
1476
1569
  key: issue.key,
1477
1570
  status: issue.fields.status?.name,
@@ -1481,7 +1574,9 @@ var JiraConnector = class {
1481
1574
  }
1482
1575
  });
1483
1576
  nodesAdded++;
1484
- const searchText = [issue.fields.summary, issue.fields.description ?? ""].join(" ");
1577
+ const searchText = sanitizeExternalText(
1578
+ [issue.fields.summary, issue.fields.description ?? ""].join(" ")
1579
+ );
1485
1580
  edgesAdded += linkToCode(store, searchText, nodeId, "applies_to");
1486
1581
  }
1487
1582
  startAt += maxResults;
@@ -1557,7 +1652,8 @@ var SlackConnector = class {
1557
1652
  }
1558
1653
  for (const message of data.messages) {
1559
1654
  const nodeId = `conversation:slack:${channel}:${message.ts}`;
1560
- const snippet = message.text.length > 100 ? message.text.slice(0, 100) : message.text;
1655
+ const sanitizedText = sanitizeExternalText(message.text);
1656
+ const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
1561
1657
  store.addNode({
1562
1658
  id: nodeId,
1563
1659
  type: "conversation",
@@ -1569,7 +1665,9 @@ var SlackConnector = class {
1569
1665
  }
1570
1666
  });
1571
1667
  nodesAdded++;
1572
- edgesAdded += linkToCode(store, message.text, nodeId, "references", { checkPaths: true });
1668
+ edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
1669
+ checkPaths: true
1670
+ });
1573
1671
  }
1574
1672
  } catch (err) {
1575
1673
  errors.push(
@@ -1632,7 +1730,7 @@ var ConfluenceConnector = class {
1632
1730
  store.addNode({
1633
1731
  id: nodeId,
1634
1732
  type: "document",
1635
- name: page.title,
1733
+ name: sanitizeExternalText(page.title, 500),
1636
1734
  metadata: {
1637
1735
  source: "confluence",
1638
1736
  spaceKey,
@@ -1642,7 +1740,7 @@ var ConfluenceConnector = class {
1642
1740
  }
1643
1741
  });
1644
1742
  nodesAdded++;
1645
- const text = `${page.title} ${page.body?.storage?.value ?? ""}`;
1743
+ const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
1646
1744
  edgesAdded += linkToCode(store, text, nodeId, "documents");
1647
1745
  }
1648
1746
  nextUrl = data._links?.next ? `${baseUrl}${data._links.next}` : null;
@@ -1707,10 +1805,11 @@ var CIConnector = class {
1707
1805
  const data = await response.json();
1708
1806
  for (const run of data.workflow_runs) {
1709
1807
  const buildId = `build:${run.id}`;
1808
+ const safeName = sanitizeExternalText(run.name, 200);
1710
1809
  store.addNode({
1711
1810
  id: buildId,
1712
1811
  type: "build",
1713
- name: `${run.name} #${run.id}`,
1812
+ name: `${safeName} #${run.id}`,
1714
1813
  metadata: {
1715
1814
  source: "github-actions",
1716
1815
  status: run.status,
@@ -1732,7 +1831,7 @@ var CIConnector = class {
1732
1831
  store.addNode({
1733
1832
  id: testResultId,
1734
1833
  type: "test_result",
1735
- name: `Failed: ${run.name} #${run.id}`,
1834
+ name: `Failed: ${safeName} #${run.id}`,
1736
1835
  metadata: {
1737
1836
  source: "github-actions",
1738
1837
  buildId: String(run.id),
@@ -2035,6 +2134,144 @@ var GraphEntropyAdapter = class {
2035
2134
  }
2036
2135
  };
2037
2136
 
2137
+ // src/entropy/GraphComplexityAdapter.ts
2138
+ var GraphComplexityAdapter = class {
2139
+ constructor(store) {
2140
+ this.store = store;
2141
+ }
2142
+ /**
2143
+ * Compute complexity hotspots by combining cyclomatic complexity with change frequency.
2144
+ *
2145
+ * 1. Find all function and method nodes
2146
+ * 2. For each, find the containing file and count commit nodes referencing that file
2147
+ * 3. Compute hotspotScore = changeFrequency * cyclomaticComplexity
2148
+ * 4. Sort descending by score
2149
+ * 5. Compute 95th percentile
2150
+ */
2151
+ computeComplexityHotspots() {
2152
+ const functionNodes = [
2153
+ ...this.store.findNodes({ type: "function" }),
2154
+ ...this.store.findNodes({ type: "method" })
2155
+ ];
2156
+ if (functionNodes.length === 0) {
2157
+ return { hotspots: [], percentile95Score: 0 };
2158
+ }
2159
+ const fileChangeFrequency = /* @__PURE__ */ new Map();
2160
+ const hotspots = [];
2161
+ for (const fnNode of functionNodes) {
2162
+ const complexity = fnNode.metadata?.cyclomaticComplexity ?? 1;
2163
+ const containsEdges = this.store.getEdges({ to: fnNode.id, type: "contains" });
2164
+ let fileId;
2165
+ for (const edge of containsEdges) {
2166
+ const sourceNode = this.store.getNode(edge.from);
2167
+ if (sourceNode?.type === "file") {
2168
+ fileId = sourceNode.id;
2169
+ break;
2170
+ }
2171
+ if (sourceNode?.type === "class") {
2172
+ const classContainsEdges = this.store.getEdges({ to: sourceNode.id, type: "contains" });
2173
+ for (const classEdge of classContainsEdges) {
2174
+ const parentNode = this.store.getNode(classEdge.from);
2175
+ if (parentNode?.type === "file") {
2176
+ fileId = parentNode.id;
2177
+ break;
2178
+ }
2179
+ }
2180
+ if (fileId) break;
2181
+ }
2182
+ }
2183
+ if (!fileId) continue;
2184
+ let changeFrequency = fileChangeFrequency.get(fileId);
2185
+ if (changeFrequency === void 0) {
2186
+ const referencesEdges = this.store.getEdges({ to: fileId, type: "references" });
2187
+ changeFrequency = referencesEdges.length;
2188
+ fileChangeFrequency.set(fileId, changeFrequency);
2189
+ }
2190
+ const hotspotScore = changeFrequency * complexity;
2191
+ const filePath = fnNode.path ?? fileId.replace(/^file:/, "");
2192
+ hotspots.push({
2193
+ file: filePath,
2194
+ function: fnNode.name,
2195
+ changeFrequency,
2196
+ complexity,
2197
+ hotspotScore
2198
+ });
2199
+ }
2200
+ hotspots.sort((a, b) => b.hotspotScore - a.hotspotScore);
2201
+ const percentile95Score = this.computePercentile(
2202
+ hotspots.map((h) => h.hotspotScore),
2203
+ 95
2204
+ );
2205
+ return { hotspots, percentile95Score };
2206
+ }
2207
+ computePercentile(descendingScores, percentile) {
2208
+ if (descendingScores.length === 0) return 0;
2209
+ const ascending = [...descendingScores].sort((a, b) => a - b);
2210
+ const index = Math.ceil(percentile / 100 * ascending.length) - 1;
2211
+ return ascending[Math.min(index, ascending.length - 1)];
2212
+ }
2213
+ };
2214
+
2215
+ // src/entropy/GraphCouplingAdapter.ts
2216
+ var GraphCouplingAdapter = class {
2217
+ constructor(store) {
2218
+ this.store = store;
2219
+ }
2220
+ /**
2221
+ * Compute coupling data for all file nodes in the graph.
2222
+ *
2223
+ * For each file:
2224
+ * - fanOut: number of outbound 'imports' edges
2225
+ * - fanIn: number of inbound 'imports' edges from other files
2226
+ * - couplingRatio: fanOut / (fanIn + fanOut), rounded to 2 decimals (0 if both are 0)
2227
+ * - transitiveDepth: longest chain of outbound 'imports' edges via BFS
2228
+ */
2229
+ computeCouplingData() {
2230
+ const fileNodes = this.store.findNodes({ type: "file" });
2231
+ if (fileNodes.length === 0) {
2232
+ return { files: [] };
2233
+ }
2234
+ const files = [];
2235
+ for (const node of fileNodes) {
2236
+ const fileId = node.id;
2237
+ const filePath = node.path ?? node.name;
2238
+ const outEdges = this.store.getEdges({ from: fileId, type: "imports" });
2239
+ const fanOut = outEdges.length;
2240
+ const inEdges = this.store.getEdges({ to: fileId, type: "imports" });
2241
+ const fanIn = inEdges.length;
2242
+ const total = fanIn + fanOut;
2243
+ const couplingRatio = total === 0 ? 0 : Math.round(fanOut / total * 100) / 100;
2244
+ const transitiveDepth = this.computeTransitiveDepth(fileId);
2245
+ files.push({ file: filePath, fanIn, fanOut, couplingRatio, transitiveDepth });
2246
+ }
2247
+ return { files };
2248
+ }
2249
+ /**
2250
+ * BFS from a node following outbound 'imports' edges to find the maximum depth.
2251
+ */
2252
+ computeTransitiveDepth(startId) {
2253
+ const visited = /* @__PURE__ */ new Set();
2254
+ const queue = [[startId, 0]];
2255
+ visited.add(startId);
2256
+ let maxDepth = 0;
2257
+ let head = 0;
2258
+ while (head < queue.length) {
2259
+ const [nodeId, depth] = queue[head++];
2260
+ if (depth > maxDepth) {
2261
+ maxDepth = depth;
2262
+ }
2263
+ const outEdges = this.store.getEdges({ from: nodeId, type: "imports" });
2264
+ for (const edge of outEdges) {
2265
+ if (!visited.has(edge.to)) {
2266
+ visited.add(edge.to);
2267
+ queue.push([edge.to, depth + 1]);
2268
+ }
2269
+ }
2270
+ }
2271
+ return maxDepth;
2272
+ }
2273
+ };
2274
+
2038
2275
  // src/context/Assembler.ts
2039
2276
  var PHASE_NODE_TYPES = {
2040
2277
  implement: ["file", "function", "class", "method", "interface", "variable"],
@@ -2342,6 +2579,250 @@ var GraphConstraintAdapter = class {
2342
2579
  }
2343
2580
  };
2344
2581
 
2582
+ // src/ingest/DesignIngestor.ts
2583
+ var fs4 = __toESM(require("fs/promises"));
2584
+ var path5 = __toESM(require("path"));
2585
+ function isDTCGToken(obj) {
2586
+ return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
2587
+ }
2588
+ async function readFileOrNull(filePath) {
2589
+ try {
2590
+ return await fs4.readFile(filePath, "utf-8");
2591
+ } catch {
2592
+ return null;
2593
+ }
2594
+ }
2595
+ function parseJsonOrError(content, filePath) {
2596
+ try {
2597
+ return { data: JSON.parse(content) };
2598
+ } catch (err) {
2599
+ return {
2600
+ error: `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
2601
+ };
2602
+ }
2603
+ }
2604
+ function walkDTCGTokens(store, obj, groupPath, topGroup, tokensPath) {
2605
+ let count = 0;
2606
+ for (const [key, value] of Object.entries(obj)) {
2607
+ if (key.startsWith("$")) continue;
2608
+ if (isDTCGToken(value)) {
2609
+ const tokenPath = [...groupPath, key].join(".");
2610
+ store.addNode({
2611
+ id: `design_token:${tokenPath}`,
2612
+ type: "design_token",
2613
+ name: tokenPath,
2614
+ path: tokensPath,
2615
+ metadata: {
2616
+ tokenType: value.$type,
2617
+ value: value.$value,
2618
+ group: topGroup || groupPath[0] || key,
2619
+ ...value.$description ? { description: value.$description } : {}
2620
+ }
2621
+ });
2622
+ count++;
2623
+ } else if (typeof value === "object" && value !== null) {
2624
+ count += walkDTCGTokens(
2625
+ store,
2626
+ value,
2627
+ [...groupPath, key],
2628
+ topGroup || key,
2629
+ tokensPath
2630
+ );
2631
+ }
2632
+ }
2633
+ return count;
2634
+ }
2635
+ function parseAestheticDirection(content) {
2636
+ const extract = (pattern) => {
2637
+ const m = content.match(pattern);
2638
+ return m ? m[1].trim() : void 0;
2639
+ };
2640
+ const result = {};
2641
+ const style = extract(/\*\*Style:\*\*\s*(.+)/);
2642
+ if (style !== void 0) result.style = style;
2643
+ const tone = extract(/\*\*Tone:\*\*\s*(.+)/);
2644
+ if (tone !== void 0) result.tone = tone;
2645
+ const differentiator = extract(/\*\*Differentiator:\*\*\s*(.+)/);
2646
+ if (differentiator !== void 0) result.differentiator = differentiator;
2647
+ const strictness = extract(/^level:\s*(strict|standard|permissive)\s*$/m);
2648
+ if (strictness !== void 0) result.strictness = strictness;
2649
+ return result;
2650
+ }
2651
+ function parseAntiPatterns(content) {
2652
+ const lines = content.split("\n");
2653
+ const patterns = [];
2654
+ let inSection = false;
2655
+ for (const line of lines) {
2656
+ if (/^##\s+Anti-Patterns/i.test(line)) {
2657
+ inSection = true;
2658
+ continue;
2659
+ }
2660
+ if (inSection && /^##\s+/.test(line)) {
2661
+ break;
2662
+ }
2663
+ if (inSection) {
2664
+ const bulletMatch = line.match(/^-\s+(.+)/);
2665
+ if (bulletMatch) {
2666
+ patterns.push(bulletMatch[1].trim());
2667
+ }
2668
+ }
2669
+ }
2670
+ return patterns;
2671
+ }
2672
+ var DesignIngestor = class {
2673
+ constructor(store) {
2674
+ this.store = store;
2675
+ }
2676
+ async ingestTokens(tokensPath) {
2677
+ const start = Date.now();
2678
+ const content = await readFileOrNull(tokensPath);
2679
+ if (content === null) return emptyResult(Date.now() - start);
2680
+ const parsed = parseJsonOrError(content, tokensPath);
2681
+ if ("error" in parsed) {
2682
+ return { ...emptyResult(Date.now() - start), errors: [parsed.error] };
2683
+ }
2684
+ const nodesAdded = walkDTCGTokens(this.store, parsed.data, [], "", tokensPath);
2685
+ return {
2686
+ nodesAdded,
2687
+ nodesUpdated: 0,
2688
+ edgesAdded: 0,
2689
+ edgesUpdated: 0,
2690
+ errors: [],
2691
+ durationMs: Date.now() - start
2692
+ };
2693
+ }
2694
+ async ingestDesignIntent(designPath) {
2695
+ const start = Date.now();
2696
+ const content = await readFileOrNull(designPath);
2697
+ if (content === null) return emptyResult(Date.now() - start);
2698
+ let nodesAdded = 0;
2699
+ const direction = parseAestheticDirection(content);
2700
+ const metadata = {};
2701
+ if (direction.style) metadata.style = direction.style;
2702
+ if (direction.tone) metadata.tone = direction.tone;
2703
+ if (direction.differentiator) metadata.differentiator = direction.differentiator;
2704
+ if (direction.strictness) metadata.strictness = direction.strictness;
2705
+ this.store.addNode({
2706
+ id: "aesthetic_intent:project",
2707
+ type: "aesthetic_intent",
2708
+ name: "project",
2709
+ path: designPath,
2710
+ metadata
2711
+ });
2712
+ nodesAdded++;
2713
+ for (const text of parseAntiPatterns(content)) {
2714
+ this.store.addNode({
2715
+ id: `design_constraint:${hash(text)}`,
2716
+ type: "design_constraint",
2717
+ name: text,
2718
+ path: designPath,
2719
+ metadata: { rule: text, severity: "warn", scope: "project" }
2720
+ });
2721
+ nodesAdded++;
2722
+ }
2723
+ return {
2724
+ nodesAdded,
2725
+ nodesUpdated: 0,
2726
+ edgesAdded: 0,
2727
+ edgesUpdated: 0,
2728
+ errors: [],
2729
+ durationMs: Date.now() - start
2730
+ };
2731
+ }
2732
+ async ingestAll(designDir) {
2733
+ const start = Date.now();
2734
+ const [tokensResult, intentResult] = await Promise.all([
2735
+ this.ingestTokens(path5.join(designDir, "tokens.json")),
2736
+ this.ingestDesignIntent(path5.join(designDir, "DESIGN.md"))
2737
+ ]);
2738
+ const merged = mergeResults(tokensResult, intentResult);
2739
+ return { ...merged, durationMs: Date.now() - start };
2740
+ }
2741
+ };
2742
+
2743
+ // src/constraints/DesignConstraintAdapter.ts
2744
+ var DesignConstraintAdapter = class {
2745
+ constructor(store) {
2746
+ this.store = store;
2747
+ }
2748
+ checkForHardcodedColors(source, file, strictness) {
2749
+ const severity = this.mapSeverity(strictness);
2750
+ const tokenNodes = this.store.findNodes({ type: "design_token" });
2751
+ const colorValues = /* @__PURE__ */ new Set();
2752
+ for (const node of tokenNodes) {
2753
+ if (node.metadata.tokenType === "color" && typeof node.metadata.value === "string") {
2754
+ colorValues.add(node.metadata.value.toLowerCase());
2755
+ }
2756
+ }
2757
+ const hexPattern = /#[0-9a-fA-F]{3,8}\b/g;
2758
+ const violations = [];
2759
+ let match;
2760
+ while ((match = hexPattern.exec(source)) !== null) {
2761
+ const hexValue = match[0];
2762
+ if (!colorValues.has(hexValue.toLowerCase())) {
2763
+ violations.push({
2764
+ code: "DESIGN-001",
2765
+ file,
2766
+ message: `Hardcoded color ${hexValue} is not in the design token set`,
2767
+ severity,
2768
+ value: hexValue
2769
+ });
2770
+ }
2771
+ }
2772
+ return violations;
2773
+ }
2774
+ checkForHardcodedFonts(source, file, strictness) {
2775
+ const severity = this.mapSeverity(strictness);
2776
+ const tokenNodes = this.store.findNodes({ type: "design_token" });
2777
+ const fontFamilies = /* @__PURE__ */ new Set();
2778
+ for (const node of tokenNodes) {
2779
+ if (node.metadata.tokenType === "typography") {
2780
+ const value = node.metadata.value;
2781
+ if (typeof value === "object" && value !== null && "fontFamily" in value) {
2782
+ fontFamilies.add(value.fontFamily.toLowerCase());
2783
+ }
2784
+ }
2785
+ }
2786
+ const fontPatterns = [/fontFamily:\s*['"]([^'"]+)['"]/g, /font-family:\s*['"]([^'"]+)['"]/g];
2787
+ const violations = [];
2788
+ const seen = /* @__PURE__ */ new Set();
2789
+ for (const pattern of fontPatterns) {
2790
+ let match;
2791
+ while ((match = pattern.exec(source)) !== null) {
2792
+ const fontName = match[1];
2793
+ if (seen.has(fontName.toLowerCase())) continue;
2794
+ seen.add(fontName.toLowerCase());
2795
+ if (!fontFamilies.has(fontName.toLowerCase())) {
2796
+ violations.push({
2797
+ code: "DESIGN-002",
2798
+ file,
2799
+ message: `Hardcoded font family "${fontName}" is not in the design token set`,
2800
+ severity,
2801
+ value: fontName
2802
+ });
2803
+ }
2804
+ }
2805
+ }
2806
+ return violations;
2807
+ }
2808
+ checkAll(source, file, strictness) {
2809
+ return [
2810
+ ...this.checkForHardcodedColors(source, file, strictness),
2811
+ ...this.checkForHardcodedFonts(source, file, strictness)
2812
+ ];
2813
+ }
2814
+ mapSeverity(strictness = "standard") {
2815
+ switch (strictness) {
2816
+ case "permissive":
2817
+ return "info";
2818
+ case "standard":
2819
+ return "warn";
2820
+ case "strict":
2821
+ return "error";
2822
+ }
2823
+ }
2824
+ };
2825
+
2345
2826
  // src/feedback/GraphFeedbackAdapter.ts
2346
2827
  var GraphFeedbackAdapter = class {
2347
2828
  constructor(store) {
@@ -2414,7 +2895,7 @@ var GraphFeedbackAdapter = class {
2414
2895
  };
2415
2896
 
2416
2897
  // src/index.ts
2417
- var VERSION = "0.1.0";
2898
+ var VERSION = "0.2.0";
2418
2899
  // Annotate the CommonJS export names for ESM import in node:
2419
2900
  0 && (module.exports = {
2420
2901
  Assembler,
@@ -2423,10 +2904,14 @@ var VERSION = "0.1.0";
2423
2904
  CodeIngestor,
2424
2905
  ConfluenceConnector,
2425
2906
  ContextQL,
2907
+ DesignConstraintAdapter,
2908
+ DesignIngestor,
2426
2909
  EDGE_TYPES,
2427
2910
  FusionLayer,
2428
2911
  GitIngestor,
2912
+ GraphComplexityAdapter,
2429
2913
  GraphConstraintAdapter,
2914
+ GraphCouplingAdapter,
2430
2915
  GraphEdgeSchema,
2431
2916
  GraphEntropyAdapter,
2432
2917
  GraphFeedbackAdapter,