roast-my-design-system 5.5.0 → 5.5.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/README.md CHANGED
@@ -59,7 +59,7 @@ One scan powers all of it; the flags decide what lands on disk. Combine freely.
59
59
 
60
60
  The full report for vercel/ai-chatbot, top to bottom — including "What the numbers mean", Claude's read of the scan, embedded right under the verdict:
61
61
 
62
- ![The full diagnosis report for vercel/ai-chatbot in dark mode: health score, the What the numbers mean analysis written by Claude, priced Where to start moves, the wrapped present with the agent rules, an agent trap callout, three-yardstick tiles, palette forensics, spacing receipts, typography specimens, offenders, duplicates, and the component usage ledger](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-full-dark.png?v=5.2.3)
62
+ ![The full diagnosis report for vercel/ai-chatbot in dark mode: health score, the What the numbers mean analysis written by Claude, priced Where to start moves, the wrapped present with the agent rules, an agent trap callout, three-yardstick tiles, the adoption map treemap, palette forensics, spacing receipts, typography specimens, offenders, duplicates, and the component usage ledger](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-full-dark.png?v=5.5.1)
63
63
 
64
64
  The same report in light mode (one file, built-in toggle):
65
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "5.5.0",
3
+ "version": "5.5.2",
4
4
  "mcpName": "io.github.gregkozakiewicz/roast-my-design-system",
5
5
  "description": "Your AI can write the UI. This makes sure it writes your UI. A deterministic scanner scores your design system 0-100 against 34 public repos, writes rules for Claude, Cursor, Copilot and Windsurf with --apply, and runs as a local MCP server with --mcp.",
6
6
  "keywords": [
@@ -30,6 +30,10 @@
30
30
  "license": "MIT",
31
31
  "author": "Greg Kozakiewicz (https://gregkozakiewicz.com)",
32
32
  "type": "module",
33
+ "exports": {
34
+ "./engine": "./skills/roast-my-design-system/scripts/lib/guard-api.mjs",
35
+ "./package.json": "./package.json"
36
+ },
33
37
  "bin": {
34
38
  "roast-my-design-system": "bin/roast.mjs"
35
39
  },
@@ -0,0 +1,98 @@
1
+ /**
2
+ * guard-api — the one official doorway into the engine for OTHER packages.
3
+ *
4
+ * guard-my-design-system imports this file (through the roast npm package's
5
+ * "./engine" export). Everything else in the engine stays private and free to
6
+ * refactor; what is exported here is a public promise — rename or reshape it
7
+ * only with a deliberate version bump and a changelog line.
8
+ *
9
+ * Kept deliberately narrow: learn what the repo's system is, scan a piece of
10
+ * text for styling, and say which on-system value a stray most resembles.
11
+ */
12
+ import { extname } from 'node:path';
13
+ import { walkRepo } from '../harvest/walk.mjs';
14
+ import { harvestTokens, extractStyling, normalizeHex, isGrey } from '../harvest/tokens.mjs';
15
+ import { loadExclusions } from './exclusions.mjs';
16
+ import { hexRgb } from './nearpairs.mjs';
17
+ import { typefaceOf, GENERIC_FONTS } from './typefaces.mjs';
18
+
19
+ export { extractStyling, normalizeHex, isGrey, hexRgb, typefaceOf, GENERIC_FONTS };
20
+
21
+ // Same classification walkRepo uses, exposed so a caller looking at one file
22
+ // (a diff hunk) treats it the way the full scan would.
23
+ const CODE_EXTS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs', '.cjs']);
24
+ const STYLE_EXTS = new Set(['.css', '.scss', '.sass', '.less']);
25
+ export const isCodeFile = (p) => CODE_EXTS.has(extname(p));
26
+ export const isStyleFile = (p) => STYLE_EXTS.has(extname(p));
27
+
28
+ /**
29
+ * Scan the whole repo once and return what its design system IS — the
30
+ * reference a guard judges new lines against. Respects .roastignore.
31
+ *
32
+ * Returns {
33
+ * tokenFile, // the file defining the most --vars, or null
34
+ * colors, // [{ value, count, isToken, files }] every colour seen
35
+ * tokens, // shorthand: just the token colour values
36
+ * spacing, // [{ value, count, files }] every length seen
37
+ * fontFamilies, // [{ value, count, files }] every family declared
38
+ * tailwind, // { colors, spacing, radii, textSizes, arbitrary }
39
+ * files, // { styles: n, code: n } — how much was read
40
+ * }
41
+ */
42
+ export function learnSystem(repoRoot, { exclude = [] } = {}) {
43
+ const exclusions = loadExclusions(repoRoot, exclude);
44
+ const files = walkRepo(repoRoot, 14, exclusions);
45
+ const t = harvestTokens(repoRoot, files.styles, files.code);
46
+ return {
47
+ tokenFile: t.tokenFile,
48
+ colors: t.colors,
49
+ tokens: t.colors.filter((c) => c.isToken).map((c) => c.value),
50
+ spacing: t.spacing,
51
+ fontFamilies: t.fontFamilies,
52
+ tailwind: t.tailwind,
53
+ files: { styles: files.styles.length, code: files.code.length },
54
+ };
55
+ }
56
+
57
+ /**
58
+ * The token a stray colour most resembles. Distance is the largest channel
59
+ * difference (the same yardstick nearColorPairs uses), so "8" here means what
60
+ * "near-identical" means everywhere else in the engine. Hex-parseable values
61
+ * only; returns { value, distance } or null.
62
+ */
63
+ export function nearestColor(value, tokenValues) {
64
+ const target = hexRgb(normalizeHex(value));
65
+ if (!target) return null;
66
+ let best = null;
67
+ for (const t of tokenValues) {
68
+ const rgb = hexRgb(t);
69
+ if (!rgb || rgb.a !== target.a) continue;
70
+ const d = Math.max(Math.abs(rgb.r - target.r), Math.abs(rgb.g - target.g), Math.abs(rgb.b - target.b));
71
+ if (d === 0) continue; // identical to a token: not a stray at all
72
+ if (!best || d < best.distance) best = { value: t, distance: d };
73
+ }
74
+ return best;
75
+ }
76
+
77
+ /**
78
+ * The on-scale length a stray spacing value most resembles. Same-unit
79
+ * comparison only (12px is not "near" 0.75rem here; unit conversion is a
80
+ * judgement the guard should not fake). Returns { value, distance } or null.
81
+ */
82
+ export function nearestLength(value, scaleValues) {
83
+ const parse = (v) => {
84
+ const m = /^(-?\d*\.?\d+)(px|rem|em|%|vh|vw|pt)$/.exec(String(v).trim());
85
+ return m ? { n: parseFloat(m[1]), unit: m[2] } : null;
86
+ };
87
+ const target = parse(value);
88
+ if (!target) return null;
89
+ let best = null;
90
+ for (const s of scaleValues) {
91
+ const p = parse(s);
92
+ if (!p || p.unit !== target.unit) continue;
93
+ const d = Math.abs(p.n - target.n);
94
+ if (d === 0) continue;
95
+ if (!best || d < best.distance) best = { value: s, distance: d };
96
+ }
97
+ return best;
98
+ }
@@ -1,4 +1,4 @@
1
1
  // Single version constant for the engine — imported by diagnose (report
2
2
  // footer) and rules (generated-by line). This is the bump spot that used to
3
3
  // live as a const inside diagnose/index.mjs.
4
- export const VERSION = '5.5.0';
4
+ export const VERSION = '5.5.2';