@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.mjs CHANGED
@@ -31,7 +31,11 @@ var NODE_TYPES = [
31
31
  "layer",
32
32
  "pattern",
33
33
  "constraint",
34
- "violation"
34
+ "violation",
35
+ // Design
36
+ "design_token",
37
+ "aesthetic_intent",
38
+ "design_constraint"
35
39
  ];
36
40
  var EDGE_TYPES = [
37
41
  // Code relationships
@@ -55,7 +59,12 @@ var EDGE_TYPES = [
55
59
  "failed_in",
56
60
  // Execution relationships (future)
57
61
  "executed_by",
58
- "measured_by"
62
+ "measured_by",
63
+ // Design relationships
64
+ "uses_token",
65
+ "declares_intent",
66
+ "violates_design",
67
+ "platform_binding"
59
68
  ];
60
69
  var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
61
70
  var CURRENT_SCHEMA_VERSION = 1;
@@ -124,6 +133,14 @@ async function loadGraph(dirPath) {
124
133
  }
125
134
 
126
135
  // src/store/GraphStore.ts
136
+ var POISONED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
137
+ function safeMerge(target, source) {
138
+ for (const key of Object.keys(source)) {
139
+ if (!POISONED_KEYS.has(key)) {
140
+ target[key] = source[key];
141
+ }
142
+ }
143
+ }
127
144
  var GraphStore = class {
128
145
  db;
129
146
  nodes;
@@ -142,7 +159,7 @@ var GraphStore = class {
142
159
  addNode(node) {
143
160
  const existing = this.nodes.by("id", node.id);
144
161
  if (existing) {
145
- Object.assign(existing, node);
162
+ safeMerge(existing, node);
146
163
  this.nodes.update(existing);
147
164
  } else {
148
165
  this.nodes.insert({ ...node });
@@ -186,7 +203,7 @@ var GraphStore = class {
186
203
  });
187
204
  if (existing) {
188
205
  if (edge.metadata) {
189
- Object.assign(existing, edge);
206
+ safeMerge(existing, edge);
190
207
  this.edges.update(existing);
191
208
  }
192
209
  return;
@@ -462,7 +479,7 @@ var CodeIngestor = class {
462
479
  let nodesAdded = 0;
463
480
  let edgesAdded = 0;
464
481
  const files = await this.findSourceFiles(rootDir);
465
- const callableNodes = [];
482
+ const nameToFiles = /* @__PURE__ */ new Map();
466
483
  const fileContents = /* @__PURE__ */ new Map();
467
484
  for (const filePath of files) {
468
485
  try {
@@ -488,7 +505,12 @@ var CodeIngestor = class {
488
505
  nodesAdded++;
489
506
  edgesAdded++;
490
507
  if (node.type === "function" || node.type === "method") {
491
- callableNodes.push({ id: node.id, name: node.name, filePath: relativePath });
508
+ let files2 = nameToFiles.get(node.name);
509
+ if (!files2) {
510
+ files2 = /* @__PURE__ */ new Set();
511
+ nameToFiles.set(node.name, files2);
512
+ }
513
+ files2.add(relativePath);
492
514
  }
493
515
  }
494
516
  const imports = await this.extractImports(content, fileId, relativePath, rootDir);
@@ -500,7 +522,7 @@ var CodeIngestor = class {
500
522
  errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
501
523
  }
502
524
  }
503
- const callsEdges = this.extractCallsEdges(callableNodes, fileContents);
525
+ const callsEdges = this.extractCallsEdges(nameToFiles, fileContents);
504
526
  for (const edge of callsEdges) {
505
527
  this.store.addEdge(edge);
506
528
  edgesAdded++;
@@ -548,7 +570,13 @@ var CodeIngestor = class {
548
570
  name,
549
571
  path: relativePath,
550
572
  location: { fileId, startLine: i + 1, endLine },
551
- metadata: { exported: line.includes("export") }
573
+ metadata: {
574
+ exported: line.includes("export"),
575
+ cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
576
+ nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
577
+ lineCount: endLine - i,
578
+ parameterCount: this.countParameters(line)
579
+ }
552
580
  },
553
581
  edge: { from: fileId, to: id, type: "contains" }
554
582
  });
@@ -634,7 +662,14 @@ var CodeIngestor = class {
634
662
  name: methodName,
635
663
  path: relativePath,
636
664
  location: { fileId, startLine: i + 1, endLine },
637
- metadata: { className: currentClassName, exported: false }
665
+ metadata: {
666
+ className: currentClassName,
667
+ exported: false,
668
+ cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
669
+ nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
670
+ lineCount: endLine - i,
671
+ parameterCount: this.countParameters(line)
672
+ }
638
673
  },
639
674
  edge: { from: currentClassId, to: id, type: "contains" }
640
675
  });
@@ -684,43 +719,37 @@ var CodeIngestor = class {
684
719
  return startIndex + 1;
685
720
  }
686
721
  /**
687
- * Second pass: for each function/method, check if its file content calls any other
688
- * known function/method. Creates approximate "calls" edges via regex matching.
722
+ * Second pass: scan each file for identifiers matching known callable names,
723
+ * then create file-to-file "calls" edges. Uses regex heuristic (not AST).
689
724
  */
690
- extractCallsEdges(callableNodes, fileContents) {
725
+ extractCallsEdges(nameToFiles, fileContents) {
691
726
  const edges = [];
692
- const nameToNodes = /* @__PURE__ */ new Map();
693
- for (const callable of callableNodes) {
694
- let arr = nameToNodes.get(callable.name);
695
- if (!arr) {
696
- arr = [];
697
- nameToNodes.set(callable.name, arr);
698
- }
699
- arr.push({ id: callable.id, filePath: callable.filePath });
700
- }
701
- for (const caller of callableNodes) {
702
- const content = fileContents.get(caller.filePath);
703
- if (!content) continue;
704
- for (const [name, targets] of nameToNodes) {
705
- if (name === caller.name) continue;
706
- const callPattern = new RegExp(`\\b${this.escapeRegex(name)}\\s*\\(`, "g");
707
- if (callPattern.test(content)) {
708
- for (const target of targets) {
709
- edges.push({
710
- from: caller.id,
711
- to: target.id,
712
- type: "calls",
713
- metadata: { confidence: "regex" }
714
- });
715
- }
727
+ const seen = /* @__PURE__ */ new Set();
728
+ for (const [filePath, content] of fileContents) {
729
+ const callerFileId = `file:${filePath}`;
730
+ const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
731
+ let match;
732
+ while ((match = callPattern.exec(content)) !== null) {
733
+ const name = match[1];
734
+ const targetFiles = nameToFiles.get(name);
735
+ if (!targetFiles) continue;
736
+ for (const targetFile of targetFiles) {
737
+ if (targetFile === filePath) continue;
738
+ const targetFileId = `file:${targetFile}`;
739
+ const key = `${callerFileId}|${targetFileId}`;
740
+ if (seen.has(key)) continue;
741
+ seen.add(key);
742
+ edges.push({
743
+ from: callerFileId,
744
+ to: targetFileId,
745
+ type: "calls",
746
+ metadata: { confidence: "regex" }
747
+ });
716
748
  }
717
749
  }
718
750
  }
719
751
  return edges;
720
752
  }
721
- escapeRegex(str) {
722
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
723
- }
724
753
  async extractImports(content, fileId, relativePath, rootDir) {
725
754
  const edges = [];
726
755
  const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+)\s+from\s+['"]([^'"]+)['"]/g;
@@ -766,6 +795,46 @@ var CodeIngestor = class {
766
795
  }
767
796
  return null;
768
797
  }
798
+ computeCyclomaticComplexity(lines) {
799
+ let complexity = 1;
800
+ const decisionPattern = /\b(if|else\s+if|while|for|case)\b|\?\s*[^:?]|&&|\|\||catch\b/g;
801
+ for (const line of lines) {
802
+ const trimmed = line.trim();
803
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
804
+ const matches = trimmed.match(decisionPattern);
805
+ if (matches) complexity += matches.length;
806
+ }
807
+ return complexity;
808
+ }
809
+ computeMaxNesting(lines) {
810
+ let maxDepth = 0;
811
+ let currentDepth = 0;
812
+ for (const line of lines) {
813
+ const trimmed = line.trim();
814
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
815
+ for (const ch of trimmed) {
816
+ if (ch === "{") {
817
+ currentDepth++;
818
+ if (currentDepth > maxDepth) maxDepth = currentDepth;
819
+ } else if (ch === "}") {
820
+ currentDepth--;
821
+ }
822
+ }
823
+ }
824
+ return Math.max(0, maxDepth - 1);
825
+ }
826
+ countParameters(declarationLine) {
827
+ const parenMatch = declarationLine.match(/\(([^)]*)\)/);
828
+ if (!parenMatch || !parenMatch[1].trim()) return 0;
829
+ let depth = 0;
830
+ let count = 1;
831
+ for (const ch of parenMatch[1]) {
832
+ if (ch === "<" || ch === "(") depth++;
833
+ else if (ch === ">" || ch === ")") depth--;
834
+ else if (ch === "," && depth === 0) count++;
835
+ }
836
+ return count;
837
+ }
769
838
  detectLanguage(filePath) {
770
839
  if (/\.tsx?$/.test(filePath)) return "typescript";
771
840
  if (/\.jsx?$/.test(filePath)) return "javascript";
@@ -1032,8 +1101,9 @@ var TopologicalLinker = class {
1032
1101
  // src/ingest/KnowledgeIngestor.ts
1033
1102
  import * as fs2 from "fs/promises";
1034
1103
  import * as path3 from "path";
1104
+
1105
+ // src/ingest/ingestUtils.ts
1035
1106
  import * as crypto from "crypto";
1036
- var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
1037
1107
  function hash(text) {
1038
1108
  return crypto.createHash("md5").update(text).digest("hex").slice(0, 8);
1039
1109
  }
@@ -1050,6 +1120,9 @@ function mergeResults(...results) {
1050
1120
  function emptyResult(durationMs = 0) {
1051
1121
  return { nodesAdded: 0, nodesUpdated: 0, edgesAdded: 0, edgesUpdated: 0, errors: [], durationMs };
1052
1122
  }
1123
+
1124
+ // src/ingest/KnowledgeIngestor.ts
1125
+ var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
1053
1126
  var KnowledgeIngestor = class {
1054
1127
  constructor(store) {
1055
1128
  this.store = store;
@@ -1241,6 +1314,22 @@ var KnowledgeIngestor = class {
1241
1314
 
1242
1315
  // src/ingest/connectors/ConnectorUtils.ts
1243
1316
  var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
1317
+ function sanitizeExternalText(text, maxLength = 2e3) {
1318
+ let sanitized = text.replace(
1319
+ /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
1320
+ ""
1321
+ ).replace(/^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim, "").replace(
1322
+ /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
1323
+ "[filtered]"
1324
+ ).replace(
1325
+ /you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
1326
+ "[filtered]"
1327
+ );
1328
+ if (sanitized.length > maxLength) {
1329
+ sanitized = sanitized.slice(0, maxLength) + "\u2026";
1330
+ }
1331
+ return sanitized;
1332
+ }
1244
1333
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
1245
1334
  let edgesCreated = 0;
1246
1335
  for (const type of CODE_NODE_TYPES2) {
@@ -1408,7 +1497,7 @@ var JiraConnector = class {
1408
1497
  store.addNode({
1409
1498
  id: nodeId,
1410
1499
  type: "issue",
1411
- name: issue.fields.summary,
1500
+ name: sanitizeExternalText(issue.fields.summary, 500),
1412
1501
  metadata: {
1413
1502
  key: issue.key,
1414
1503
  status: issue.fields.status?.name,
@@ -1418,7 +1507,9 @@ var JiraConnector = class {
1418
1507
  }
1419
1508
  });
1420
1509
  nodesAdded++;
1421
- const searchText = [issue.fields.summary, issue.fields.description ?? ""].join(" ");
1510
+ const searchText = sanitizeExternalText(
1511
+ [issue.fields.summary, issue.fields.description ?? ""].join(" ")
1512
+ );
1422
1513
  edgesAdded += linkToCode(store, searchText, nodeId, "applies_to");
1423
1514
  }
1424
1515
  startAt += maxResults;
@@ -1494,7 +1585,8 @@ var SlackConnector = class {
1494
1585
  }
1495
1586
  for (const message of data.messages) {
1496
1587
  const nodeId = `conversation:slack:${channel}:${message.ts}`;
1497
- const snippet = message.text.length > 100 ? message.text.slice(0, 100) : message.text;
1588
+ const sanitizedText = sanitizeExternalText(message.text);
1589
+ const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
1498
1590
  store.addNode({
1499
1591
  id: nodeId,
1500
1592
  type: "conversation",
@@ -1506,7 +1598,9 @@ var SlackConnector = class {
1506
1598
  }
1507
1599
  });
1508
1600
  nodesAdded++;
1509
- edgesAdded += linkToCode(store, message.text, nodeId, "references", { checkPaths: true });
1601
+ edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
1602
+ checkPaths: true
1603
+ });
1510
1604
  }
1511
1605
  } catch (err) {
1512
1606
  errors.push(
@@ -1569,7 +1663,7 @@ var ConfluenceConnector = class {
1569
1663
  store.addNode({
1570
1664
  id: nodeId,
1571
1665
  type: "document",
1572
- name: page.title,
1666
+ name: sanitizeExternalText(page.title, 500),
1573
1667
  metadata: {
1574
1668
  source: "confluence",
1575
1669
  spaceKey,
@@ -1579,7 +1673,7 @@ var ConfluenceConnector = class {
1579
1673
  }
1580
1674
  });
1581
1675
  nodesAdded++;
1582
- const text = `${page.title} ${page.body?.storage?.value ?? ""}`;
1676
+ const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
1583
1677
  edgesAdded += linkToCode(store, text, nodeId, "documents");
1584
1678
  }
1585
1679
  nextUrl = data._links?.next ? `${baseUrl}${data._links.next}` : null;
@@ -1644,10 +1738,11 @@ var CIConnector = class {
1644
1738
  const data = await response.json();
1645
1739
  for (const run of data.workflow_runs) {
1646
1740
  const buildId = `build:${run.id}`;
1741
+ const safeName = sanitizeExternalText(run.name, 200);
1647
1742
  store.addNode({
1648
1743
  id: buildId,
1649
1744
  type: "build",
1650
- name: `${run.name} #${run.id}`,
1745
+ name: `${safeName} #${run.id}`,
1651
1746
  metadata: {
1652
1747
  source: "github-actions",
1653
1748
  status: run.status,
@@ -1669,7 +1764,7 @@ var CIConnector = class {
1669
1764
  store.addNode({
1670
1765
  id: testResultId,
1671
1766
  type: "test_result",
1672
- name: `Failed: ${run.name} #${run.id}`,
1767
+ name: `Failed: ${safeName} #${run.id}`,
1673
1768
  metadata: {
1674
1769
  source: "github-actions",
1675
1770
  buildId: String(run.id),
@@ -1972,6 +2067,144 @@ var GraphEntropyAdapter = class {
1972
2067
  }
1973
2068
  };
1974
2069
 
2070
+ // src/entropy/GraphComplexityAdapter.ts
2071
+ var GraphComplexityAdapter = class {
2072
+ constructor(store) {
2073
+ this.store = store;
2074
+ }
2075
+ /**
2076
+ * Compute complexity hotspots by combining cyclomatic complexity with change frequency.
2077
+ *
2078
+ * 1. Find all function and method nodes
2079
+ * 2. For each, find the containing file and count commit nodes referencing that file
2080
+ * 3. Compute hotspotScore = changeFrequency * cyclomaticComplexity
2081
+ * 4. Sort descending by score
2082
+ * 5. Compute 95th percentile
2083
+ */
2084
+ computeComplexityHotspots() {
2085
+ const functionNodes = [
2086
+ ...this.store.findNodes({ type: "function" }),
2087
+ ...this.store.findNodes({ type: "method" })
2088
+ ];
2089
+ if (functionNodes.length === 0) {
2090
+ return { hotspots: [], percentile95Score: 0 };
2091
+ }
2092
+ const fileChangeFrequency = /* @__PURE__ */ new Map();
2093
+ const hotspots = [];
2094
+ for (const fnNode of functionNodes) {
2095
+ const complexity = fnNode.metadata?.cyclomaticComplexity ?? 1;
2096
+ const containsEdges = this.store.getEdges({ to: fnNode.id, type: "contains" });
2097
+ let fileId;
2098
+ for (const edge of containsEdges) {
2099
+ const sourceNode = this.store.getNode(edge.from);
2100
+ if (sourceNode?.type === "file") {
2101
+ fileId = sourceNode.id;
2102
+ break;
2103
+ }
2104
+ if (sourceNode?.type === "class") {
2105
+ const classContainsEdges = this.store.getEdges({ to: sourceNode.id, type: "contains" });
2106
+ for (const classEdge of classContainsEdges) {
2107
+ const parentNode = this.store.getNode(classEdge.from);
2108
+ if (parentNode?.type === "file") {
2109
+ fileId = parentNode.id;
2110
+ break;
2111
+ }
2112
+ }
2113
+ if (fileId) break;
2114
+ }
2115
+ }
2116
+ if (!fileId) continue;
2117
+ let changeFrequency = fileChangeFrequency.get(fileId);
2118
+ if (changeFrequency === void 0) {
2119
+ const referencesEdges = this.store.getEdges({ to: fileId, type: "references" });
2120
+ changeFrequency = referencesEdges.length;
2121
+ fileChangeFrequency.set(fileId, changeFrequency);
2122
+ }
2123
+ const hotspotScore = changeFrequency * complexity;
2124
+ const filePath = fnNode.path ?? fileId.replace(/^file:/, "");
2125
+ hotspots.push({
2126
+ file: filePath,
2127
+ function: fnNode.name,
2128
+ changeFrequency,
2129
+ complexity,
2130
+ hotspotScore
2131
+ });
2132
+ }
2133
+ hotspots.sort((a, b) => b.hotspotScore - a.hotspotScore);
2134
+ const percentile95Score = this.computePercentile(
2135
+ hotspots.map((h) => h.hotspotScore),
2136
+ 95
2137
+ );
2138
+ return { hotspots, percentile95Score };
2139
+ }
2140
+ computePercentile(descendingScores, percentile) {
2141
+ if (descendingScores.length === 0) return 0;
2142
+ const ascending = [...descendingScores].sort((a, b) => a - b);
2143
+ const index = Math.ceil(percentile / 100 * ascending.length) - 1;
2144
+ return ascending[Math.min(index, ascending.length - 1)];
2145
+ }
2146
+ };
2147
+
2148
+ // src/entropy/GraphCouplingAdapter.ts
2149
+ var GraphCouplingAdapter = class {
2150
+ constructor(store) {
2151
+ this.store = store;
2152
+ }
2153
+ /**
2154
+ * Compute coupling data for all file nodes in the graph.
2155
+ *
2156
+ * For each file:
2157
+ * - fanOut: number of outbound 'imports' edges
2158
+ * - fanIn: number of inbound 'imports' edges from other files
2159
+ * - couplingRatio: fanOut / (fanIn + fanOut), rounded to 2 decimals (0 if both are 0)
2160
+ * - transitiveDepth: longest chain of outbound 'imports' edges via BFS
2161
+ */
2162
+ computeCouplingData() {
2163
+ const fileNodes = this.store.findNodes({ type: "file" });
2164
+ if (fileNodes.length === 0) {
2165
+ return { files: [] };
2166
+ }
2167
+ const files = [];
2168
+ for (const node of fileNodes) {
2169
+ const fileId = node.id;
2170
+ const filePath = node.path ?? node.name;
2171
+ const outEdges = this.store.getEdges({ from: fileId, type: "imports" });
2172
+ const fanOut = outEdges.length;
2173
+ const inEdges = this.store.getEdges({ to: fileId, type: "imports" });
2174
+ const fanIn = inEdges.length;
2175
+ const total = fanIn + fanOut;
2176
+ const couplingRatio = total === 0 ? 0 : Math.round(fanOut / total * 100) / 100;
2177
+ const transitiveDepth = this.computeTransitiveDepth(fileId);
2178
+ files.push({ file: filePath, fanIn, fanOut, couplingRatio, transitiveDepth });
2179
+ }
2180
+ return { files };
2181
+ }
2182
+ /**
2183
+ * BFS from a node following outbound 'imports' edges to find the maximum depth.
2184
+ */
2185
+ computeTransitiveDepth(startId) {
2186
+ const visited = /* @__PURE__ */ new Set();
2187
+ const queue = [[startId, 0]];
2188
+ visited.add(startId);
2189
+ let maxDepth = 0;
2190
+ let head = 0;
2191
+ while (head < queue.length) {
2192
+ const [nodeId, depth] = queue[head++];
2193
+ if (depth > maxDepth) {
2194
+ maxDepth = depth;
2195
+ }
2196
+ const outEdges = this.store.getEdges({ from: nodeId, type: "imports" });
2197
+ for (const edge of outEdges) {
2198
+ if (!visited.has(edge.to)) {
2199
+ visited.add(edge.to);
2200
+ queue.push([edge.to, depth + 1]);
2201
+ }
2202
+ }
2203
+ }
2204
+ return maxDepth;
2205
+ }
2206
+ };
2207
+
1975
2208
  // src/context/Assembler.ts
1976
2209
  var PHASE_NODE_TYPES = {
1977
2210
  implement: ["file", "function", "class", "method", "interface", "variable"],
@@ -2279,6 +2512,250 @@ var GraphConstraintAdapter = class {
2279
2512
  }
2280
2513
  };
2281
2514
 
2515
+ // src/ingest/DesignIngestor.ts
2516
+ import * as fs4 from "fs/promises";
2517
+ import * as path5 from "path";
2518
+ function isDTCGToken(obj) {
2519
+ return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
2520
+ }
2521
+ async function readFileOrNull(filePath) {
2522
+ try {
2523
+ return await fs4.readFile(filePath, "utf-8");
2524
+ } catch {
2525
+ return null;
2526
+ }
2527
+ }
2528
+ function parseJsonOrError(content, filePath) {
2529
+ try {
2530
+ return { data: JSON.parse(content) };
2531
+ } catch (err) {
2532
+ return {
2533
+ error: `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
2534
+ };
2535
+ }
2536
+ }
2537
+ function walkDTCGTokens(store, obj, groupPath, topGroup, tokensPath) {
2538
+ let count = 0;
2539
+ for (const [key, value] of Object.entries(obj)) {
2540
+ if (key.startsWith("$")) continue;
2541
+ if (isDTCGToken(value)) {
2542
+ const tokenPath = [...groupPath, key].join(".");
2543
+ store.addNode({
2544
+ id: `design_token:${tokenPath}`,
2545
+ type: "design_token",
2546
+ name: tokenPath,
2547
+ path: tokensPath,
2548
+ metadata: {
2549
+ tokenType: value.$type,
2550
+ value: value.$value,
2551
+ group: topGroup || groupPath[0] || key,
2552
+ ...value.$description ? { description: value.$description } : {}
2553
+ }
2554
+ });
2555
+ count++;
2556
+ } else if (typeof value === "object" && value !== null) {
2557
+ count += walkDTCGTokens(
2558
+ store,
2559
+ value,
2560
+ [...groupPath, key],
2561
+ topGroup || key,
2562
+ tokensPath
2563
+ );
2564
+ }
2565
+ }
2566
+ return count;
2567
+ }
2568
+ function parseAestheticDirection(content) {
2569
+ const extract = (pattern) => {
2570
+ const m = content.match(pattern);
2571
+ return m ? m[1].trim() : void 0;
2572
+ };
2573
+ const result = {};
2574
+ const style = extract(/\*\*Style:\*\*\s*(.+)/);
2575
+ if (style !== void 0) result.style = style;
2576
+ const tone = extract(/\*\*Tone:\*\*\s*(.+)/);
2577
+ if (tone !== void 0) result.tone = tone;
2578
+ const differentiator = extract(/\*\*Differentiator:\*\*\s*(.+)/);
2579
+ if (differentiator !== void 0) result.differentiator = differentiator;
2580
+ const strictness = extract(/^level:\s*(strict|standard|permissive)\s*$/m);
2581
+ if (strictness !== void 0) result.strictness = strictness;
2582
+ return result;
2583
+ }
2584
+ function parseAntiPatterns(content) {
2585
+ const lines = content.split("\n");
2586
+ const patterns = [];
2587
+ let inSection = false;
2588
+ for (const line of lines) {
2589
+ if (/^##\s+Anti-Patterns/i.test(line)) {
2590
+ inSection = true;
2591
+ continue;
2592
+ }
2593
+ if (inSection && /^##\s+/.test(line)) {
2594
+ break;
2595
+ }
2596
+ if (inSection) {
2597
+ const bulletMatch = line.match(/^-\s+(.+)/);
2598
+ if (bulletMatch) {
2599
+ patterns.push(bulletMatch[1].trim());
2600
+ }
2601
+ }
2602
+ }
2603
+ return patterns;
2604
+ }
2605
+ var DesignIngestor = class {
2606
+ constructor(store) {
2607
+ this.store = store;
2608
+ }
2609
+ async ingestTokens(tokensPath) {
2610
+ const start = Date.now();
2611
+ const content = await readFileOrNull(tokensPath);
2612
+ if (content === null) return emptyResult(Date.now() - start);
2613
+ const parsed = parseJsonOrError(content, tokensPath);
2614
+ if ("error" in parsed) {
2615
+ return { ...emptyResult(Date.now() - start), errors: [parsed.error] };
2616
+ }
2617
+ const nodesAdded = walkDTCGTokens(this.store, parsed.data, [], "", tokensPath);
2618
+ return {
2619
+ nodesAdded,
2620
+ nodesUpdated: 0,
2621
+ edgesAdded: 0,
2622
+ edgesUpdated: 0,
2623
+ errors: [],
2624
+ durationMs: Date.now() - start
2625
+ };
2626
+ }
2627
+ async ingestDesignIntent(designPath) {
2628
+ const start = Date.now();
2629
+ const content = await readFileOrNull(designPath);
2630
+ if (content === null) return emptyResult(Date.now() - start);
2631
+ let nodesAdded = 0;
2632
+ const direction = parseAestheticDirection(content);
2633
+ const metadata = {};
2634
+ if (direction.style) metadata.style = direction.style;
2635
+ if (direction.tone) metadata.tone = direction.tone;
2636
+ if (direction.differentiator) metadata.differentiator = direction.differentiator;
2637
+ if (direction.strictness) metadata.strictness = direction.strictness;
2638
+ this.store.addNode({
2639
+ id: "aesthetic_intent:project",
2640
+ type: "aesthetic_intent",
2641
+ name: "project",
2642
+ path: designPath,
2643
+ metadata
2644
+ });
2645
+ nodesAdded++;
2646
+ for (const text of parseAntiPatterns(content)) {
2647
+ this.store.addNode({
2648
+ id: `design_constraint:${hash(text)}`,
2649
+ type: "design_constraint",
2650
+ name: text,
2651
+ path: designPath,
2652
+ metadata: { rule: text, severity: "warn", scope: "project" }
2653
+ });
2654
+ nodesAdded++;
2655
+ }
2656
+ return {
2657
+ nodesAdded,
2658
+ nodesUpdated: 0,
2659
+ edgesAdded: 0,
2660
+ edgesUpdated: 0,
2661
+ errors: [],
2662
+ durationMs: Date.now() - start
2663
+ };
2664
+ }
2665
+ async ingestAll(designDir) {
2666
+ const start = Date.now();
2667
+ const [tokensResult, intentResult] = await Promise.all([
2668
+ this.ingestTokens(path5.join(designDir, "tokens.json")),
2669
+ this.ingestDesignIntent(path5.join(designDir, "DESIGN.md"))
2670
+ ]);
2671
+ const merged = mergeResults(tokensResult, intentResult);
2672
+ return { ...merged, durationMs: Date.now() - start };
2673
+ }
2674
+ };
2675
+
2676
+ // src/constraints/DesignConstraintAdapter.ts
2677
+ var DesignConstraintAdapter = class {
2678
+ constructor(store) {
2679
+ this.store = store;
2680
+ }
2681
+ checkForHardcodedColors(source, file, strictness) {
2682
+ const severity = this.mapSeverity(strictness);
2683
+ const tokenNodes = this.store.findNodes({ type: "design_token" });
2684
+ const colorValues = /* @__PURE__ */ new Set();
2685
+ for (const node of tokenNodes) {
2686
+ if (node.metadata.tokenType === "color" && typeof node.metadata.value === "string") {
2687
+ colorValues.add(node.metadata.value.toLowerCase());
2688
+ }
2689
+ }
2690
+ const hexPattern = /#[0-9a-fA-F]{3,8}\b/g;
2691
+ const violations = [];
2692
+ let match;
2693
+ while ((match = hexPattern.exec(source)) !== null) {
2694
+ const hexValue = match[0];
2695
+ if (!colorValues.has(hexValue.toLowerCase())) {
2696
+ violations.push({
2697
+ code: "DESIGN-001",
2698
+ file,
2699
+ message: `Hardcoded color ${hexValue} is not in the design token set`,
2700
+ severity,
2701
+ value: hexValue
2702
+ });
2703
+ }
2704
+ }
2705
+ return violations;
2706
+ }
2707
+ checkForHardcodedFonts(source, file, strictness) {
2708
+ const severity = this.mapSeverity(strictness);
2709
+ const tokenNodes = this.store.findNodes({ type: "design_token" });
2710
+ const fontFamilies = /* @__PURE__ */ new Set();
2711
+ for (const node of tokenNodes) {
2712
+ if (node.metadata.tokenType === "typography") {
2713
+ const value = node.metadata.value;
2714
+ if (typeof value === "object" && value !== null && "fontFamily" in value) {
2715
+ fontFamilies.add(value.fontFamily.toLowerCase());
2716
+ }
2717
+ }
2718
+ }
2719
+ const fontPatterns = [/fontFamily:\s*['"]([^'"]+)['"]/g, /font-family:\s*['"]([^'"]+)['"]/g];
2720
+ const violations = [];
2721
+ const seen = /* @__PURE__ */ new Set();
2722
+ for (const pattern of fontPatterns) {
2723
+ let match;
2724
+ while ((match = pattern.exec(source)) !== null) {
2725
+ const fontName = match[1];
2726
+ if (seen.has(fontName.toLowerCase())) continue;
2727
+ seen.add(fontName.toLowerCase());
2728
+ if (!fontFamilies.has(fontName.toLowerCase())) {
2729
+ violations.push({
2730
+ code: "DESIGN-002",
2731
+ file,
2732
+ message: `Hardcoded font family "${fontName}" is not in the design token set`,
2733
+ severity,
2734
+ value: fontName
2735
+ });
2736
+ }
2737
+ }
2738
+ }
2739
+ return violations;
2740
+ }
2741
+ checkAll(source, file, strictness) {
2742
+ return [
2743
+ ...this.checkForHardcodedColors(source, file, strictness),
2744
+ ...this.checkForHardcodedFonts(source, file, strictness)
2745
+ ];
2746
+ }
2747
+ mapSeverity(strictness = "standard") {
2748
+ switch (strictness) {
2749
+ case "permissive":
2750
+ return "info";
2751
+ case "standard":
2752
+ return "warn";
2753
+ case "strict":
2754
+ return "error";
2755
+ }
2756
+ }
2757
+ };
2758
+
2282
2759
  // src/feedback/GraphFeedbackAdapter.ts
2283
2760
  var GraphFeedbackAdapter = class {
2284
2761
  constructor(store) {
@@ -2351,7 +2828,7 @@ var GraphFeedbackAdapter = class {
2351
2828
  };
2352
2829
 
2353
2830
  // src/index.ts
2354
- var VERSION = "0.1.0";
2831
+ var VERSION = "0.2.0";
2355
2832
  export {
2356
2833
  Assembler,
2357
2834
  CIConnector,
@@ -2359,10 +2836,14 @@ export {
2359
2836
  CodeIngestor,
2360
2837
  ConfluenceConnector,
2361
2838
  ContextQL,
2839
+ DesignConstraintAdapter,
2840
+ DesignIngestor,
2362
2841
  EDGE_TYPES,
2363
2842
  FusionLayer,
2364
2843
  GitIngestor,
2844
+ GraphComplexityAdapter,
2365
2845
  GraphConstraintAdapter,
2846
+ GraphCouplingAdapter,
2366
2847
  GraphEdgeSchema,
2367
2848
  GraphEntropyAdapter,
2368
2849
  GraphFeedbackAdapter,