@harness-engineering/graph 0.2.1 → 0.2.3

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,6 +36,8 @@ __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,
@@ -96,7 +98,11 @@ var NODE_TYPES = [
96
98
  "layer",
97
99
  "pattern",
98
100
  "constraint",
99
- "violation"
101
+ "violation",
102
+ // Design
103
+ "design_token",
104
+ "aesthetic_intent",
105
+ "design_constraint"
100
106
  ];
101
107
  var EDGE_TYPES = [
102
108
  // Code relationships
@@ -120,7 +126,12 @@ var EDGE_TYPES = [
120
126
  "failed_in",
121
127
  // Execution relationships (future)
122
128
  "executed_by",
123
- "measured_by"
129
+ "measured_by",
130
+ // Design relationships
131
+ "uses_token",
132
+ "declares_intent",
133
+ "violates_design",
134
+ "platform_binding"
124
135
  ];
125
136
  var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
126
137
  var CURRENT_SCHEMA_VERSION = 1;
@@ -189,6 +200,14 @@ async function loadGraph(dirPath) {
189
200
  }
190
201
 
191
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
+ }
192
211
  var GraphStore = class {
193
212
  db;
194
213
  nodes;
@@ -207,7 +226,7 @@ var GraphStore = class {
207
226
  addNode(node) {
208
227
  const existing = this.nodes.by("id", node.id);
209
228
  if (existing) {
210
- Object.assign(existing, node);
229
+ safeMerge(existing, node);
211
230
  this.nodes.update(existing);
212
231
  } else {
213
232
  this.nodes.insert({ ...node });
@@ -251,7 +270,7 @@ var GraphStore = class {
251
270
  });
252
271
  if (existing) {
253
272
  if (edge.metadata) {
254
- Object.assign(existing, edge);
273
+ safeMerge(existing, edge);
255
274
  this.edges.update(existing);
256
275
  }
257
276
  return;
@@ -531,7 +550,7 @@ var CodeIngestor = class {
531
550
  const fileContents = /* @__PURE__ */ new Map();
532
551
  for (const filePath of files) {
533
552
  try {
534
- const relativePath = path.relative(rootDir, filePath);
553
+ const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
535
554
  const content = await fs.readFile(filePath, "utf-8");
536
555
  const stat2 = await fs.stat(filePath);
537
556
  const fileId = `file:${relativePath}`;
@@ -821,7 +840,7 @@ var CodeIngestor = class {
821
840
  }
822
841
  async resolveImportPath(fromFile, importPath, rootDir) {
823
842
  const fromDir = path.dirname(fromFile);
824
- const resolved = path.normalize(path.join(fromDir, importPath));
843
+ const resolved = path.normalize(path.join(fromDir, importPath)).replace(/\\/g, "/");
825
844
  const extensions = [".ts", ".tsx", ".js", ".jsx"];
826
845
  for (const ext of extensions) {
827
846
  const candidate = resolved.replace(/\.js$/, "") + ext;
@@ -833,7 +852,7 @@ var CodeIngestor = class {
833
852
  }
834
853
  }
835
854
  for (const ext of extensions) {
836
- const candidate = path.join(resolved, `index${ext}`);
855
+ const candidate = path.join(resolved, `index${ext}`).replace(/\\/g, "/");
837
856
  const fullPath = path.join(rootDir, candidate);
838
857
  try {
839
858
  await fs.access(fullPath);
@@ -1149,10 +1168,11 @@ var TopologicalLinker = class {
1149
1168
  // src/ingest/KnowledgeIngestor.ts
1150
1169
  var fs2 = __toESM(require("fs/promises"));
1151
1170
  var path3 = __toESM(require("path"));
1171
+
1172
+ // src/ingest/ingestUtils.ts
1152
1173
  var crypto = __toESM(require("crypto"));
1153
- var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
1154
1174
  function hash(text) {
1155
- return crypto.createHash("md5").update(text).digest("hex").slice(0, 8);
1175
+ return crypto.createHash("sha256").update(text).digest("hex").slice(0, 8);
1156
1176
  }
1157
1177
  function mergeResults(...results) {
1158
1178
  return {
@@ -1167,6 +1187,9 @@ function mergeResults(...results) {
1167
1187
  function emptyResult(durationMs = 0) {
1168
1188
  return { nodesAdded: 0, nodesUpdated: 0, edgesAdded: 0, edgesUpdated: 0, errors: [], durationMs };
1169
1189
  }
1190
+
1191
+ // src/ingest/KnowledgeIngestor.ts
1192
+ var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
1170
1193
  var KnowledgeIngestor = class {
1171
1194
  constructor(store) {
1172
1195
  this.store = store;
@@ -1358,6 +1381,22 @@ var KnowledgeIngestor = class {
1358
1381
 
1359
1382
  // src/ingest/connectors/ConnectorUtils.ts
1360
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
+ }
1361
1400
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
1362
1401
  let edgesCreated = 0;
1363
1402
  for (const type of CODE_NODE_TYPES2) {
@@ -1525,7 +1564,7 @@ var JiraConnector = class {
1525
1564
  store.addNode({
1526
1565
  id: nodeId,
1527
1566
  type: "issue",
1528
- name: issue.fields.summary,
1567
+ name: sanitizeExternalText(issue.fields.summary, 500),
1529
1568
  metadata: {
1530
1569
  key: issue.key,
1531
1570
  status: issue.fields.status?.name,
@@ -1535,7 +1574,9 @@ var JiraConnector = class {
1535
1574
  }
1536
1575
  });
1537
1576
  nodesAdded++;
1538
- const searchText = [issue.fields.summary, issue.fields.description ?? ""].join(" ");
1577
+ const searchText = sanitizeExternalText(
1578
+ [issue.fields.summary, issue.fields.description ?? ""].join(" ")
1579
+ );
1539
1580
  edgesAdded += linkToCode(store, searchText, nodeId, "applies_to");
1540
1581
  }
1541
1582
  startAt += maxResults;
@@ -1611,7 +1652,8 @@ var SlackConnector = class {
1611
1652
  }
1612
1653
  for (const message of data.messages) {
1613
1654
  const nodeId = `conversation:slack:${channel}:${message.ts}`;
1614
- 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;
1615
1657
  store.addNode({
1616
1658
  id: nodeId,
1617
1659
  type: "conversation",
@@ -1623,7 +1665,9 @@ var SlackConnector = class {
1623
1665
  }
1624
1666
  });
1625
1667
  nodesAdded++;
1626
- edgesAdded += linkToCode(store, message.text, nodeId, "references", { checkPaths: true });
1668
+ edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
1669
+ checkPaths: true
1670
+ });
1627
1671
  }
1628
1672
  } catch (err) {
1629
1673
  errors.push(
@@ -1686,7 +1730,7 @@ var ConfluenceConnector = class {
1686
1730
  store.addNode({
1687
1731
  id: nodeId,
1688
1732
  type: "document",
1689
- name: page.title,
1733
+ name: sanitizeExternalText(page.title, 500),
1690
1734
  metadata: {
1691
1735
  source: "confluence",
1692
1736
  spaceKey,
@@ -1696,7 +1740,7 @@ var ConfluenceConnector = class {
1696
1740
  }
1697
1741
  });
1698
1742
  nodesAdded++;
1699
- const text = `${page.title} ${page.body?.storage?.value ?? ""}`;
1743
+ const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
1700
1744
  edgesAdded += linkToCode(store, text, nodeId, "documents");
1701
1745
  }
1702
1746
  nextUrl = data._links?.next ? `${baseUrl}${data._links.next}` : null;
@@ -1761,10 +1805,11 @@ var CIConnector = class {
1761
1805
  const data = await response.json();
1762
1806
  for (const run of data.workflow_runs) {
1763
1807
  const buildId = `build:${run.id}`;
1808
+ const safeName = sanitizeExternalText(run.name, 200);
1764
1809
  store.addNode({
1765
1810
  id: buildId,
1766
1811
  type: "build",
1767
- name: `${run.name} #${run.id}`,
1812
+ name: `${safeName} #${run.id}`,
1768
1813
  metadata: {
1769
1814
  source: "github-actions",
1770
1815
  status: run.status,
@@ -1786,7 +1831,7 @@ var CIConnector = class {
1786
1831
  store.addNode({
1787
1832
  id: testResultId,
1788
1833
  type: "test_result",
1789
- name: `Failed: ${run.name} #${run.id}`,
1834
+ name: `Failed: ${safeName} #${run.id}`,
1790
1835
  metadata: {
1791
1836
  source: "github-actions",
1792
1837
  buildId: String(run.id),
@@ -2534,6 +2579,250 @@ var GraphConstraintAdapter = class {
2534
2579
  }
2535
2580
  };
2536
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
+
2537
2826
  // src/feedback/GraphFeedbackAdapter.ts
2538
2827
  var GraphFeedbackAdapter = class {
2539
2828
  constructor(store) {
@@ -2615,6 +2904,8 @@ var VERSION = "0.2.0";
2615
2904
  CodeIngestor,
2616
2905
  ConfluenceConnector,
2617
2906
  ContextQL,
2907
+ DesignConstraintAdapter,
2908
+ DesignIngestor,
2618
2909
  EDGE_TYPES,
2619
2910
  FusionLayer,
2620
2911
  GitIngestor,