roast-my-design-system 8.4.0 → 8.4.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
@@ -119,6 +119,21 @@ The same report in light mode (one file, built-in toggle):
119
119
 
120
120
  ![The diagnosis report in light mode](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-light-hero.png?v=8.2.2)
121
121
 
122
+ ## What it works on
123
+
124
+ **Supported**
125
+
126
+ - React repos: Next, Remix, Vite and plain React.
127
+ - Web-component repos: Stencil and Lit.
128
+ - 4 kinds of repo: product, library, shadcn, registry.
129
+ - 5 kit profiles: Tailwind theme, MUI, Mantine, Chakra, Ant Design.
130
+ - Any styling on top: Tailwind, styled-components, Emotion, Sass, Less, vanilla-extract, Stitches, CVA, CSS Modules.
131
+
132
+ **Recognised, not supported yet but on the roadmap**
133
+
134
+ - Vue, Angular and Svelte: named in the header, colours and spacing still counted, but components are not measured and the report says so.
135
+ - HeroUI, NextUI, Radix Themes, Fluent UI, React Bootstrap and Grommet: named in the header, no kit rules.
136
+
122
137
  ## What makes the numbers trustworthy
123
138
 
124
139
  - **Deterministic scanner, not AI sampling.** A zero-dependency Node script reads *every* file (about a second on a normal repo, a few on a large monorepo) and returns the same numbers every run. Claude narrates; it never counts.
@@ -164,7 +179,7 @@ The report and the rules file describe the repo as it was at scan time. `--mcp`
164
179
 
165
180
  The loop: context before building, find while building, validate before saving, review before finishing.
166
181
 
167
- 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.
182
+ 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 or a shadcn repo they flag a palette class such as `text-gray-500` or `ring-green-500` where the theme names a colour of that kind.
168
183
 
169
184
  ### What a session looks like
170
185
 
@@ -220,7 +235,7 @@ To use it in Claude Code, type `/mcp__roast__roast-fix` in the chat. MCP prompts
220
235
  Otherwise, add it to Claude Code by hand:
221
236
 
222
237
  ```bash
223
- claude mcp add roast -- npx roast-my-design-system --mcp
238
+ claude mcp add roast -- npx roast-my-design-system@latest --mcp
224
239
  ```
225
240
 
226
241
  **Verified in Claude Code, Cursor, and Windsurf (now Devin Desktop).** Each was tested end to end: server connected, all 5 tools listed, real answers in the editor's own chat. Same promise as the scan: local, read-only, one scan at startup, no port, no account, nothing about your code leaves your machine. A clean answer reads "no measured violations found" with the list of checks attached, because a scanner can only certify what it can count.
package/cli/roast.mjs CHANGED
@@ -106,7 +106,7 @@ For your agent (a plain terminal has no agent to write these)
106
106
  --mcp run as a local MCP server (stdio) so your agent can query
107
107
  the design system live: context, canonical components,
108
108
  tokens, validation. Add to your client, e.g. Claude Code:
109
- claude mcp add roast -- npx roast-my-design-system --mcp
109
+ claude mcp add roast -- npx roast-my-design-system@latest --mcp
110
110
 
111
111
  Read-only scan (--apply and --rules write only the files they name).
112
112
  No network, no telemetry, nothing leaves your machine.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "8.4.0",
3
+ "version": "8.4.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 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": [
@@ -35,13 +35,15 @@ export const PALETTE_CLASS_RE = new RegExp(`(?<![\\w-])(?:[\\w-]+:)*(?:bg|text|b
35
35
  // Evening overrides painted by hand with white or black (the palette shades
36
36
  // are already caught above with their dark: prefix).
37
37
  export const GREY_HUE_RE = /-(?:slate|gray|zinc|neutral|stone|mauve|olive|mist|taupe|white|black)(?:-|\/|$)/;
38
- const DARK_WB_RE = /(?<![\w-])dark:(?:bg|text|border)-(?:white|black)(?:\/\d+)?(?![\w-])/g;
38
+ // exported for the checkers: the same override judged on a file under review
39
+ export const DARK_WB_RE = /(?<![\w-])dark:(?:bg|text|border)-(?:white|black)(?:\/\d+)?(?![\w-])/g;
39
40
  // Colour passed into a kit door: palette colour, black, white, any variant prefix.
40
41
  const DOOR_COLOR_RE = new RegExp(`(?<![\\w-])(?:[\\w-]+:)*(?:bg|text|border)-(?:${PALETTE}|white|black)(?:-(?:50|[1-9]00|950))?(?:/\\d+)?(?![\\w-])`, 'g');
41
42
  // Typography passed into a kit door: weight or size. A receipt, not a score.
42
43
  const DOOR_TYPO_RE = /(?<![\w-])(?:[\w-]+:)*(?:font-(?:thin|extralight|light|normal|medium|semibold|bold|extrabold|black)|text-(?:xs|sm|base|lg|xl|[2-9]xl))(?![\w-])/g;
43
44
 
44
- const DEMO_PATH_RE = /(^|\/)(stories|storybook|__stories__|examples?|demos?|templates?|playground|fixtures?|__tests__|__mocks__|e2e|cypress)\//i;
45
+ // exported for the checkers: a demo folder is not own code there either
46
+ export const DEMO_PATH_RE = /(^|\/)(stories|storybook|__stories__|examples?|demos?|templates?|playground|fixtures?|__tests__|__mocks__|e2e|cypress)\//i;
45
47
 
46
48
  /**
47
49
  * @param root repo root
@@ -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.4.0';
4
+ export const VERSION = '8.4.2';
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.
@@ -17,7 +17,7 @@ import { exemptReason } from '../lib/exempt.mjs';
17
17
  import { extraDeclarations, fontDeclarations } from '../lib/declarations.mjs';
18
18
  import { typefaceOf, GENERIC_FONTS } from '../lib/typefaces.mjs';
19
19
  import { kitPaintInSource } from '../lib/kitpaint.mjs';
20
- import { PALETTE_CLASS_RE, GREY_HUE_RE } from '../harvest/paint.mjs';
20
+ import { PALETTE_CLASS_RE, GREY_HUE_RE, DARK_WB_RE, DEMO_PATH_RE } from '../harvest/paint.mjs';
21
21
 
22
22
  // What this engine measures — shipped with every result, clean or not.
23
23
  export const CHECKS = [
@@ -37,7 +37,7 @@ export const KIT_CHECK = 'colours and pixel sizes written onto kit components wh
37
37
  export const PALETTE_CHECK = 'palette classes where the theme names a colour';
38
38
  export function checksFor(k) {
39
39
  if (k?.kit) return [...CHECKS, KIT_CHECK];
40
- if (k?.tailwind) return [...CHECKS, PALETTE_CHECK];
40
+ if (k?.tailwind || k?.shadcn) return [...CHECKS, PALETTE_CHECK];
41
41
  return CHECKS;
42
42
  }
43
43
  // "an MUI component", "an Ant Design component", "a Mantine component"
@@ -142,27 +142,40 @@ export function validateContent(content, k) {
142
142
  }
143
143
  }
144
144
 
145
- // ---------- a Tailwind theme's palette classes ----------
146
- // The theme names its colours (bg-surface, text-ink); a palette class
147
- // (text-gray-500) where the theme has a colour of that kind is paint from
148
- // the tin. Same rule as the report's tile, from harvest/paint.mjs.
149
- if (k.tailwind && !css) {
150
- const tw = k.tailwind;
145
+ // ---------- palette classes where the theme names its colours ----------
146
+ // A Tailwind theme names its colours (bg-surface, text-ink); a shadcn sheet
147
+ // names them too (bg-primary, text-muted-foreground). A palette class
148
+ // (text-gray-500, ring-green-500) where a name of that kind exists is paint
149
+ // from the tin. Same rule as the report's tile, from harvest/paint.mjs.
150
+ // On a shadcn repo the catalogue and kit blocks are the kit's own doors
151
+ // (editing them is the intended use) and a demo folder is not own code,
152
+ // exactly the files the tile leaves out.
153
+ const inShadcnDoors = (f) => !!f && k.shadcn.doors.some((d) => f === d || f.startsWith(`${d}/`));
154
+ const paletteRule = k.tailwind ? 'tailwind'
155
+ : k.shadcn && !(file && (inShadcnDoors(file) || DEMO_PATH_RE.test(file))) ? 'shadcn'
156
+ : null;
157
+ if (paletteRule && !css) {
158
+ const tw = k.tailwind ?? {};
151
159
  const retuned = new Set(tw.retuned ?? []);
152
160
  const fam = tw.families ?? null;
153
161
  const hasFamily = (cls) => !fam || (GREY_HUE_RE.test(cls) ? fam.grey : fam.colour);
162
+ const themeFile = paletteRule === 'tailwind' ? tw.file : (k.shadcn.sheet ?? 'the theme sheet');
154
163
  // the example name follows the utility: a text- class wants an ink or
155
164
  // foreground name, a bg- class a surface or background name
156
- const names = tw.names ?? [];
165
+ const names = paletteRule === 'tailwind' ? (tw.names ?? []) : ['primary', 'foreground', 'muted-foreground', 'background', 'border'];
157
166
  const pick = (re) => names.find((n) => re.test(n)) ?? names[0] ?? 'brand';
158
- for (const m of text.matchAll(PALETTE_CLASS_RE)) {
167
+ const hits = [...text.matchAll(PALETTE_CLASS_RE)];
168
+ // the evening override painted by hand (dark:bg-black) is the same sin
169
+ // on a shadcn sheet, which always has a dark row of its own
170
+ if (paletteRule === 'shadcn') hits.push(...text.matchAll(DARK_WB_RE));
171
+ for (const m of hits.sort((a, b) => a.index - b.index)) {
159
172
  const cls = m[0];
160
173
  const util = cls.replace(/^((?:[\w-]+:)*[a-z]+)-.*$/, '$1');
161
174
  const example = /(^|:)(?:text|placeholder|caret|decoration)$/.test(util) ? pick(/ink|text|fg|foreground/) : /(^|:)bg$/.test(util) ? pick(/surface|bg|background|canvas/) : pick(/border|edge|line|ring/) ;
162
175
  if (retuned.has(cls.replace(/^(?:[\w-]+:)*[a-z]+-/, '').replace(/\/\d+$/, ''))) continue;
163
176
  if (!hasFamily(cls)) continue;
164
177
  add('palette-class', 'violation', m.index,
165
- `Palette class ${cls} where the theme names its colours (${tw.file}).`,
178
+ `Palette class ${cls} where the theme names its colours (${themeFile}).`,
166
179
  `Use a theme name as the class (${util}-${example}); if the colour is missing, add it to the theme once.`);
167
180
  }
168
181
  }
@@ -149,6 +149,17 @@ export function loadKnowledge(root) {
149
149
  // colours in: null when the repo is neither (see profiles/)
150
150
  kit,
151
151
  tailwind: P.isTailwind && profile.tailwind ? profile.tailwind : null,
152
+ // A shadcn kitchen: the sheet names the colours, so a palette class in
153
+ // own code is paint from a tin (shadcn's own rule: semantic colours,
154
+ // never bg-blue-500). The report's tile counts it over own code plus
155
+ // installed registries, never inside the catalogue or a kit block; the
156
+ // checkers judge a file under review by the same line (2026-09-20: the
157
+ // report counted a ring-green-500 that validate, review and --check let
158
+ // through, because the rule was switched on for Tailwind themes only).
159
+ shadcn: P.isShadcn && profile.shadcn ? {
160
+ sheet: profile.shadcn.sheet?.found ? profile.shadcn.sheet.file : null,
161
+ doors: [...P.uiDirs, ...(profile.shadcn.blockFiles ?? [])],
162
+ } : null,
152
163
  agentFiles: (context ?? []).filter((c) => c.kind === 'agent-rules'),
153
164
  };
154
165
  }
@@ -50,7 +50,7 @@ export function getContext(k, { path = null } = {}) {
50
50
  if (kit.colour?.uses) L.push(` ${kit.colour.uses} colours are already written onto components (${kit.colour.samples.slice(0, 3).map((x) => x.value).join(', ')}); do not add one.`);
51
51
  } else if (t.tokenFile) {
52
52
  const strays = t.colors.length - k.tokenColors.length;
53
- L.push(`TOKENS: ${k.tokenColors.length} colour tokens in ${t.tokenFile}. Use them; never hardcode a colour.${strays ? ` (${strays} hardcoded strays already exist; do not add more.)` : ''}`);
53
+ L.push(`TOKENS: ${k.tokenColors.length} colour tokens in ${t.tokenFile}. Use them${k.shadcn ? ' as classes (bg-primary, text-muted-foreground); never a palette class (text-gray-500, ring-green-500) and' : ';'} never hardcode a colour.${strays ? ` (${strays} hardcoded strays already exist; do not add more.)` : ''}`);
54
54
  } else if (t.colors.length) {
55
55
  L.push(`TOKENS: none defined. ${t.colors.length} distinct colours already in play; reuse one, never invent another.`);
56
56
  }
@@ -238,7 +238,7 @@ if (neverImported.length >= 3) {
238
238
 
239
239
  lines.push('');
240
240
  if (compact) {
241
- lines.push(`*Compact rules by roast-my-design-system ver. ${VERSION}; the full set with receipts: npx roast-my-design-system --rules*`);
241
+ lines.push(`*Compact rules by roast-my-design-system ver. ${VERSION}; the full set with receipts: npx roast-my-design-system@latest --rules*`);
242
242
  } else {
243
243
  lines.push('---');
244
244
  lines.push(`*Generated by [roast-my-design-system](https://github.com/gregkozakiewicz/roast-my-design-system) ver. ${VERSION}. Rescan after refactors to keep these rules honest.*`);
@@ -251,7 +251,7 @@ if (neverImported.length >= 3) {
251
251
  if (compact && text.length > MAX_COMPACT) {
252
252
  const cut = text.lastIndexOf('\n### ', MAX_COMPACT);
253
253
  if (cut > 0) {
254
- text = `${text.slice(0, cut).trimEnd()}\n\n*Trimmed to fit this file's limits; the full rules: npx roast-my-design-system --rules*\n`;
254
+ text = `${text.slice(0, cut).trimEnd()}\n\n*Trimmed to fit this file's limits; the full rules: npx roast-my-design-system@latest --rules*\n`;
255
255
  }
256
256
  }
257
257
  const ruleCount = text.split('\n').filter((l) => l.startsWith('- ')).length;