roast-my-design-system 8.1.0 → 8.2.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <img src="https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/roaster_logo_300px.png?v=8.0.0" width="72" alt="roast-my-design-system">
1
+ <img src="https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/roster_logo_v1.png?v=8.2.0" width="100" alt="roast-my-design-system">
2
2
 
3
3
  # roast-my-design-system
4
4
 
@@ -149,6 +149,51 @@ The loop: context before building, find while building, validate before saving,
149
149
 
150
150
  The server reads the repo the way the report does. On a product built on MUI, Mantine, Chakra UI or Ant Design, the context names the theme file and the kit's own way of reading it, `roast_find_token` answers in spacing steps (`12px` is `p: 3` on a 4px MUI theme), and `roast_validate` and `roast_review` flag a colour or a pixel size written onto a kit component where the theme has a value. On a Tailwind theme they flag a palette class such as `text-gray-500` where the theme names a colour of that kind.
151
151
 
152
+ ### What a session looks like
153
+
154
+ A real exchange against [Unleash](https://github.com/Unleash/unleash), an MUI product that scores 64/100. The agent was asked to add a small usage hint to a feature page. Every answer is the server's own text, unedited.
155
+
156
+ Before writing anything, the agent calls `roast_get_context`:
157
+
158
+ ```
159
+ KIT: MUI (1109 files import it, the theme is read 6162x). A colour, a spacing step or a radius is decided in frontend/src/themes/dark-theme.ts. On a component, read it: sx paths (color: 'text.secondary', p: 2) or theme.palette / theme.spacing() in styled().
160
+ 43 colours are already written onto components (#6c65e5, #f1f0fc, #302e42); do not add one.
161
+ USE THESE, DO NOT REBUILD THEM:
162
+ <Dialogue> from frontend/src/component/common/Dialogue/Dialogue.tsx (97x)
163
+ <TextCell> from frontend/src/component/common/Table/cells/TextCell/TextCell.tsx (96x)
164
+ <PageContent> from frontend/src/component/common/PageContent/PageContent.tsx (84x)
165
+ DUPLICATES, DO NOT MAKE IT WORSE:
166
+ <StyledSuccessIcon> exists in 6 places; match what the surrounding code imports, never create another.
167
+ SPACING: Use spacing steps (p: 2), not pixels. 17 pixel sizes are already written onto components; do not add one.
168
+ TYPE: Sen. No new typefaces, no hand-declared font stacks.
169
+ Before finishing: call roast_validate on what you wrote, then roast_review.
170
+ ```
171
+
172
+ It has a grey in hand and a padding in mind, so `roast_find_token` twice:
173
+
174
+ ```
175
+ #6b7280 → Nearest token: #607d8b, 11 channel steps from #6b7280. Unless the difference is a deliberate decision, use the token.
176
+ 12px → 12px is between steps 1 (8px) and 2 (16px) on the default theme. If the design needs 12px exactly, keep it with a comment; otherwise use the nearest step in sx.
177
+ ```
178
+
179
+ It writes a first draft anyway and runs `roast_validate` before saving:
180
+
181
+ ```
182
+ 2 findings:
183
+ ✕ L5 Colour #6b7280 written onto an MUI component, and the theme has no such colour.
184
+ Fix: Add it to the theme once (frontend/src/themes/dark-theme.ts), then read it there. On an MUI component, point at it: color: 'text.secondary' in sx, or theme.palette.primary.main in a styled() call.
185
+ ✕ L5 Pixel size p: 12px on an MUI component.
186
+ Fix: 12px is between steps 1 (8px) and 2 (16px) on the default theme. If the design needs 12px exactly, keep it with a comment; otherwise use the nearest step in sx.
187
+ ```
188
+
189
+ Second draft: `color: 'text.secondary'`, `p: 1.5`, the radius read from the theme. `roast_validate` again:
190
+
191
+ ```
192
+ No measured violations found. Checked: hardcoded colours vs the token set, near-identical colour twins, off-scale spacing, off-scale radii, font sizes and shadows, typefaces outside the system, arbitrary bracket values, static inline style blocks, !important, duplicate component definitions, colours and pixel sizes written onto kit components where the theme has a value.
193
+ ```
194
+
195
+ Four calls, under 800 tokens in total, and the new component reads the theme instead of adding colour number 44. `roast_review` then checks the whole diff the same way before the agent says it is done.
196
+
152
197
  When the goal is fixing the system rather than building on it, the `roast-fix` prompt serves the top Where-to-start move from a fresh scan. It is a ready-made fix prompt, byte-identical to the report's copy buttons. Fix it, ask again, and the next move has risen to the top: the scan is the progress bar. Pass `move: 2` to jump the queue.
153
198
 
154
199
  To use it in Claude Code, type `/mcp__roast__roast-fix` in the chat. MCP prompts appear as slash commands, named after whatever you registered the server as, and the `/` autocomplete menu lists them too. Add the move number to jump the queue: `/mcp__roast__roast-fix 2`. Other clients list server prompts in their own prompt picker; wherever `roast-build-ui` and `roast-review-ui` show up, `roast-fix` sits beside them.
@@ -250,6 +295,36 @@ You get the roast in chat plus `design-system-roast.html` at your repo root: a s
250
295
 
251
296
  After the roast, the skill also offers to write `design-system-rules.md` to disk and merge it into your CLAUDE.md, `.cursor/rules` or AGENTS.md.
252
297
 
298
+ ### Three prompts to try
299
+
300
+ Type any of these in Claude Code, inside the repo:
301
+
302
+ ```
303
+ Roast my design system.
304
+ ```
305
+
306
+ ```
307
+ How bad is my CSS? Scan this repo and show me the receipts.
308
+ ```
309
+
310
+ With the MCP server connected (see [Live answers over MCP](#live-answers-over-mcp)):
311
+
312
+ ```
313
+ Is there already a Button component in this repo, and which one should I use?
314
+ ```
315
+
316
+ ## Troubleshooting
317
+
318
+ - **"Command not found" or the plugin will not install.** Update Claude Code; the plugin marketplace needs a recent version. The manual install above works on any version.
319
+ - **"Nothing to roast" or a near-empty report.** The scan found almost no colours or spacing. The styling probably lives in another repo, a CDN or an installed package. Run it from the repo that holds the styles.
320
+ - **A score that looks wrong.** Check the header of the report: it names how the repo was read (product, library, shadcn, a kit, a Tailwind theme) and every folder that was left out. Scope the scan with a `.roastignore` file or `--exclude` if a playground or an old app is blurring the numbers.
321
+ - **The report did not open.** It is written to `design-system-roast.html` at the repo root. Open it in any browser; it needs no server and makes no requests.
322
+ - **The MCP server does not appear.** Restart the client after adding it. In Claude Code, `claude mcp list` shows whether it connected. Everything it needs is Node 18 or newer.
323
+
324
+ ## Support
325
+
326
+ Bugs and questions go to [GitHub Issues](https://github.com/gregkozakiewicz/roast-my-design-system/issues). Everything else reaches Greg through [gregkozakiewicz.com](https://gregkozakiewicz.com).
327
+
253
328
  ## Live examples
254
329
 
255
330
  7 real roasts of public repos, hosted as-is (the same self-contained HTML the skill generates), spanning React, Stencil and Lit:
@@ -288,7 +363,7 @@ Yes, the median repo is already a mess. That's the point.
288
363
 
289
364
  MIT. The code is yours to fork, modify and redistribute; the copyright notice travels with it.
290
365
 
291
- Building your own report, summary or audit from this tool's scores, counts or benchmark comparisons? Keep one line in it: *Built with [roast-my-design-system](https://github.com/gregkozakiewicz/roast-my-design-system) by Greg Kozakiewicz*. The scan data asks the same of AI agents that consume it.
366
+ Building your own report, summary or audit from this tool's scores, counts or benchmark comparisons? Keep one line in it: *Built with [roast-my-design-system](https://github.com/gregkozakiewicz/roast-my-design-system) by Greg Kozakiewicz*. The skill asks the same of an AI agent that writes such a document from the scan.
292
367
 
293
368
  **roast-my-design-system**™ and the GK mark are trademarks of Greg Kozakiewicz. Forking is welcome, republishing under this name is not: see [brand and attribution](https://gregkozakiewicz.github.io/roast-my-design-system/brand.html).
294
369
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "8.1.0",
3
+ "version": "8.2.0",
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 112 public repos, reads React and web components (Stencil, Lit), hands you a copy-paste fix prompt for each top finding, writes rules for Claude, Cursor, Copilot and Windsurf with --apply, and runs as a local MCP server with --mcp.",
6
6
  "keywords": [
@@ -51,9 +51,18 @@ const FUNC_COLOR_RE = /\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(\s*[^)]{1,80}\)|\b
51
51
  // scanned as 0 tokens until 5.10.0. Recognised here and normalised to hsl()
52
52
  // so it flows through greys, twins and the rest like any other colour.
53
53
  const HSL_TRIPLET_RE = /^\s*(-?\d+(?:\.\d+)?)(?:deg)?\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%(?:\s*\/\s*(\d+(?:\.\d+)?%?))?\s*$/;
54
+ // The same convention with RGB channels: --bg-default: 255 255 255, wrapped
55
+ // later as rgb(var(--bg-default) / <alpha-value>). dub keeps its whole theme
56
+ // this way, and until 8.2.0 it scanned as a repo without a token file, so an
57
+ // avatar colour list took the title (2026-09-18). Three integers 0-255, and
58
+ // nothing else, so a bare "16px" or "1 2" never reads as a colour.
59
+ const RGB_TRIPLET_RE = /^\s*(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})(?:\s*\/\s*(\d+(?:\.\d+)?%?))?\s*$/;
54
60
  export const tripletToHsl = (v) => {
55
61
  const m = HSL_TRIPLET_RE.exec(v);
56
- return m ? `hsl(${m[1]} ${m[2]}% ${m[3]}%${m[4] ? ` / ${m[4]}` : ''})` : null;
62
+ if (m) return `hsl(${m[1]} ${m[2]}% ${m[3]}%${m[4] ? ` / ${m[4]}` : ''})`;
63
+ const r = RGB_TRIPLET_RE.exec(v);
64
+ if (r && [r[1], r[2], r[3]].every((n) => Number(n) <= 255)) return `rgb(${r[1]} ${r[2]} ${r[3]}${r[4] ? ` / ${r[4]}` : ''})`;
65
+ return null;
57
66
  };
58
67
  // Loose hex in CODE (not in a style block, a class string or a stylesheet) is
59
68
  // ambiguous: #RGBA shorthand is legal CSS but vanishingly rare in code, while
@@ -284,6 +293,16 @@ export function extractStyling(src, { css = false } = {}) {
284
293
  * Returns { colors, greys, spacing, radii, fontSizes, fontFamilies, shadows,
285
294
  * tailwind: {...same buckets from tw classes...}, inlineStyleCount }
286
295
  */
296
+ /** Share of a code file's quoted colours that sit inside an array holding 8 or more of them. */
297
+ export function colourListShare(src) {
298
+ const QUOTED = /(['"`])(?:#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgba?|hsla?)\([^)'"`]*\))\1/g;
299
+ const all = (src.match(QUOTED) ?? []).length;
300
+ if (!all) return 0;
301
+ let inLists = 0;
302
+ for (const a of src.match(/\[[^[\]]*\]/g) ?? []) { const n = (a.match(QUOTED) ?? []).length; if (n >= 8) inLists += n; }
303
+ return inLists / all;
304
+ }
305
+
287
306
  export function harvestTokens(root, styleFiles, codeFiles) {
288
307
  const colors = new Tally(), spacing = new Tally(), radii = new Tally(),
289
308
  fontSizes = new Tally(), fontFamilies = new Tally(), shadows = new Tally();
@@ -349,6 +368,7 @@ export function harvestTokens(root, styleFiles, codeFiles) {
349
368
  return owner;
350
369
  };
351
370
  const paletteFiles = new Set();
371
+ const listShare = new Map(); // code file → share of its colour literals inside 8+ colour arrays
352
372
  const nsDefs = new Map(); // --telekom-x: value → 'telekom' (definitions)
353
373
  const nsRefs = new Map(); // var(--telekom-x) → 'telekom' (references)
354
374
 
@@ -468,6 +488,7 @@ export function harvestTokens(root, styleFiles, codeFiles) {
468
488
  let src = readSource(join(root, f));
469
489
  if (src === null) continue;
470
490
  for (const m of src.matchAll(/[A-Za-z_][\w-]+/g)) ownClasses.add(m[0]);
491
+ listShare.set(f, colourListShare(src));
471
492
 
472
493
  // CSS-in-tagged-templates (Lit css``, styled-components css``) IS the
473
494
  // stylesheet in those worlds: Shoelace keeps its entire component styling
@@ -639,11 +660,19 @@ export function harvestTokens(root, styleFiles, codeFiles) {
639
660
  else straysIn.set(f, (straysIn.get(f) ?? 0) + 1);
640
661
  }
641
662
  }
663
+ // A code palette whose colours sit mostly inside arrays is a list handed to
664
+ // something (dub's avatar pairs, plane's chart series), not the vocabulary
665
+ // an agent should add a colour to. It keeps its palette status for the
666
+ // counts, but it does not take the token-file title while any other
667
+ // candidate qualifies. Probe, 72 repos with a code token file (2026-09-18):
668
+ // 10 switch, 4 have nothing else and keep it. A Tailwind config or a
669
+ // stylesheet is never a list, whatever shape its values take.
670
+ const isList = (f) => /\.[jt]sx?$/.test(f) && !/(^|\/)tailwind\.config\.[mc]?[jt]s$/.test(f) && (listShare.get(f) ?? 0) >= 0.5;
642
671
  const candidates = new Set([...tokenColorDefsPerFile.keys(), ...paletteFiles]);
643
672
  const tokenFile = [...candidates]
644
- .map((f) => ({ f, tokens: tokenColoursIn.get(f) ?? 0, strays: straysIn.get(f) ?? 0 }))
673
+ .map((f) => ({ f, tokens: tokenColoursIn.get(f) ?? 0, strays: straysIn.get(f) ?? 0, list: isList(f) ? 1 : 0 }))
645
674
  .filter((c) => c.tokens >= 3)
646
- .sort((a, b) => b.tokens - a.tokens || a.strays - b.strays)[0]?.f
675
+ .sort((a, b) => a.list - b.list || b.tokens - a.tokens || a.strays - b.strays)[0]?.f
647
676
  ?? [...tokenDefsPerFile.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
648
677
 
649
678
  // Token namespace: the brand stem the repo's custom properties answer to
@@ -184,10 +184,15 @@ export function canonical(raw) {
184
184
  const v = String(raw).trim();
185
185
  if (!v || /var\(|\$\{|calc\(/.test(v)) return null;
186
186
  const t = HSL_TRIPLET.exec(v);
187
- const c = parseColor(t ? `hsl(${t[1]} ${t[2]}% ${t[3]}%)` : v.replace(/\s+/g, ' ').toLowerCase());
187
+ const r = t ? null : RGB_TRIPLET.exec(v);
188
+ const c = parseColor(t ? `hsl(${t[1]} ${t[2]}% ${t[3]}%)`
189
+ : r && [r[1], r[2], r[3]].every((n) => Number(n) <= 255) ? `rgb(${r[1]} ${r[2]} ${r[3]})`
190
+ : v.replace(/\s+/g, ' ').toLowerCase());
188
191
  if (!c) return null;
189
192
  return [c.r, c.g, c.b].map((x) => Math.round(x)).join(',') + ',' + Math.round(c.a * 100);
190
193
  }
191
194
 
192
195
  const HSL_TRIPLET =
193
196
  /^\s*(-?\d+(?:\.\d+)?)(?:deg)?\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%(?:\s*\/\s*(\d+(?:\.\d+)?%?))?\s*$/;
197
+ // bare RGB channels (dub: --bg-default: 255 255 255), see harvest/tokens.mjs
198
+ const RGB_TRIPLET = /^\s*(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})(?:\s*\/\s*\d+(?:\.\d+)?%?)?\s*$/;
@@ -206,8 +206,13 @@ export function countKitPaint(root, codeFiles, { importRe: kitImportRe, themeRe,
206
206
  top: b.top.sort((a, c) => c.count - a.count).slice(0, 10),
207
207
  samples: [...b.samples.entries()].sort((a, c) => c[1] - a[1]).slice(0, 12).map(([value, count]) => ({ value, count })),
208
208
  });
209
- // the theme a reader should open first: a theme-named path, then the most colours
210
- const ranked = themeFiles.sort((a, b) => (THEME_NAME_RE.test(b.f) - THEME_NAME_RE.test(a.f)) || b.n - a.n).map((t) => t.f);
209
+ // the theme a reader should open first: not a component's own theme or a
210
+ // provider (Open-Assistant's Theme/components/Badge.ts and Metabase's
211
+ // ThemeProvider.tsx both outranked the root theme, 2026-09-18), then a
212
+ // theme-named path, then the most colours
213
+ const componentLevel = (f) => /(^|\/)(components?|providers?)\//i.test(f);
214
+ const ranked = themeFiles.sort((a, b) => (componentLevel(a.f) - componentLevel(b.f))
215
+ || (THEME_NAME_RE.test(b.f) - THEME_NAME_RE.test(a.f)) || b.n - a.n).map((t) => t.f);
211
216
  const colourOut = finish(colour);
212
217
  // a written colour the theme already holds: the move can say where it lives
213
218
  for (const s of colourOut.samples) if (themeValues.has(s.value)) s.inTheme = true;
@@ -1,7 +1,7 @@
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 = '8.1.0';
4
+ export const VERSION = '8.2.0';
5
5
  // The shape of harvest.json and summary.json. Bumped only when a field is
6
6
  // renamed, removed or changes meaning; a new field is not a new schema. A
7
7
  // tool comparing two scans compares like with like by this number, not by VERSION.
@@ -60,10 +60,13 @@ export function loadKnowledge(root) {
60
60
  const themeSet = new Set(kit.themeValues ?? []);
61
61
  tokens = {
62
62
  ...tokens,
63
- tokenFile: tokens.tokenFile ?? kit.themeFiles[0] ?? null,
63
+ // the theme first: on a kit repo it is where a colour is decided, whatever
64
+ // stylesheet or palette file holds the most literals
65
+ tokenFile: kit.themeFiles[0] ?? tokens.tokenFile ?? null,
64
66
  colors: tokens.colors.map((c) => (themeSet.has(c.value) ? { ...c, isToken: true } : c)),
65
67
  };
66
- for (const v of themeSet) if (!tokens.colors.some((c) => c.value === v)) tokens.colors.push({ value: v, count: 1, isToken: true });
68
+ // same shape as a harvested colour, so every reader of `files` keeps working
69
+ for (const v of themeSet) if (!tokens.colors.some((c) => c.value === v)) tokens.colors.push({ value: v, count: 1, files: [{ file: kit.themeFiles[0] ?? '', count: 1 }], isToken: true });
67
70
  }
68
71
  const duplicates = findDuplicates(components, profile.uiDir, root);
69
72
  const context = harvestContext(root);
@@ -28,29 +28,41 @@ const PROTOCOL = '2025-06-18';
28
28
  // ---------- tool + resource + prompt catalogue ----------
29
29
  // Descriptions are budgeted: every client loads them into every session, so
30
30
  // each one carries only what changes an agent's tool choice (5.0.1 trim).
31
+ // Every tool is read-only and local: the annotations say so, so a client can
32
+ // run them without a per-call prompt (directory policy, 2026-09-18).
31
33
  const TOOLS = [
32
34
  {
33
35
  name: 'roast_get_context',
36
+ title: 'Design system context',
37
+ annotations: { title: 'Design system context', readOnlyHint: true, destructiveHint: false, openWorldHint: false },
34
38
  description: 'Design-system context before writing UI in this repo: tokens or the kit theme (MUI, Mantine, Chakra, Ant Design, shadcn, a Tailwind theme), canonical components, duplicates, spacing and type rules, from a real scan. Optional path ("packages/ui") narrows the slice.',
35
39
  inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Repo-relative folder (optional)' } } },
36
40
  },
37
41
  {
38
42
  name: 'roast_find_component',
43
+ title: 'Find the canonical component',
44
+ annotations: { title: 'Find the canonical component', readOnlyHint: true, destructiveHint: false, openWorldHint: false },
39
45
  description: 'Find the canonical component for a name or intent ("icon button"). Returns import path, usage count and a real usage example, or an honest zero. Ties are reported, never guessed.',
40
46
  inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Component name or intent' } }, required: ['query'] },
41
47
  },
42
48
  {
43
49
  name: 'roast_find_token',
50
+ title: 'Snap a value to a token',
51
+ annotations: { title: 'Snap a value to a token', readOnlyHint: true, destructiveHint: false, openWorldHint: false },
44
52
  description: 'Snap a raw value (#111111, 13px) to this repo\'s nearest token, theme colour or spacing step, in the kit\'s own terms on a kit repo. Says so when no scale exists.',
45
53
  inputSchema: { type: 'object', properties: { value: { type: 'string', description: 'Colour or length value' } }, required: ['value'] },
46
54
  },
47
55
  {
48
56
  name: 'roast_validate',
57
+ title: 'Validate code before saving',
58
+ annotations: { title: 'Validate code before saving', readOnlyHint: true, destructiveHint: false, openWorldHint: false },
49
59
  description: 'Check code before saving: hardcoded colours, near-token twins, off-scale spacing, arbitrary brackets, inline styles, !important, duplicate components; on a kit repo, colours and pixel sizes written onto kit components where the theme has a value; on a Tailwind theme, palette classes. Findings name the fix.',
50
60
  inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'The code to check' }, file: { type: 'string', description: 'Intended file path (optional)' } }, required: ['code'] },
51
61
  },
52
62
  {
53
63
  name: 'roast_review',
64
+ title: 'Review changed files',
65
+ annotations: { title: 'Review changed files', readOnlyHint: true, destructiveHint: false, openWorldHint: false },
54
66
  description: 'Review the working tree\'s changed files (git diff + untracked) against the design system. Reads the diff itself; send no code. Call before finishing UI work.',
55
67
  inputSchema: { type: 'object', properties: {} },
56
68
  },
@@ -96,8 +96,8 @@ export function getContext(k, { path = null } = {}) {
96
96
  text = assemble(nCanon, nDupes) + shortened();
97
97
  }
98
98
  if (text.length > CONTEXT_BUDGET) text = `${text.slice(0, text.lastIndexOf('\n', CONTEXT_BUDGET))}\n(trimmed to budget; ask roast_find_component / roast_find_token for specifics)`;
99
- // Appended after the trim so it survives on every repo size.
100
- text += '\nCREDIT: if you produce a report or document from this scan, include: Built with roast-my-design-system (https://github.com/gregkozakiewicz/roast-my-design-system) by Greg Kozakiewicz.';
99
+ // No credit line here (8.1.1): a tool answers the question and nothing
100
+ // else. Attribution lives on the report and in the skill's citation rule.
101
101
  return text;
102
102
  }
103
103