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 +17 -2
- package/cli/roast.mjs +1 -1
- package/package.json +1 -1
- package/skills/roast-my-design-system/scripts/harvest/paint.mjs +4 -2
- package/skills/roast-my-design-system/scripts/lib/version.mjs +1 -1
- package/skills/roast-my-design-system/scripts/mcp/engine.mjs +24 -11
- package/skills/roast-my-design-system/scripts/mcp/knowledge.mjs +11 -0
- package/skills/roast-my-design-system/scripts/mcp/tools.mjs +1 -1
- package/skills/roast-my-design-system/scripts/rules/build.mjs +2 -2
package/README.md
CHANGED
|
@@ -119,6 +119,21 @@ The same report in light mode (one file, built-in toggle):
|
|
|
119
119
|
|
|
120
120
|

|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
// ----------
|
|
146
|
-
//
|
|
147
|
-
// (text-
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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 (${
|
|
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;
|