roast-my-design-system 9.3.2 → 9.3.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/README.md CHANGED
@@ -133,20 +133,20 @@ Eleven reports, hosted exactly as the tool writes them. Every number determinist
133
133
  - **[Metabase](https://gregkozakiewicz.github.io/roast-my-design-system/examples/metabase-mantine.html)** (Mantine): its own wrapper over Mantine counts as the kit, so 2,679 files are read instead of 392; nothing written onto components, 2 spacings per 100 kit files. Score 47.
134
134
  - **[SigNoz](https://gregkozakiewicz.github.io/roast-my-design-system/examples/signoz-antd.html)** (Ant Design): 2 colours and 7 spacings per 100 kit files written in style objects where a token exists. Score 51.
135
135
  - **[Apache Airflow](https://gregkozakiewicz.github.io/roast-my-design-system/examples/airflow-chakra.html)** (Chakra UI): no colours written onto components, 4 spacings per 100 kit files as pixel strings where a space step exists. Score 43.
136
- - **[vercel/ai-chatbot](https://gregkozakiewicz.github.io/roast-my-design-system/examples/vercel-ai-chatbot.html)** (shadcn install): 74 values like [13px] written outside the Tailwind scale, and 66 palette colours per 100 files where a theme variable exists; Claude's notes embedded. Score 80.
136
+ - **[vercel/ai-chatbot](https://gregkozakiewicz.github.io/roast-my-design-system/examples/vercel-ai-chatbot.html)** (shadcn install): 71 values like [13px] written outside the Tailwind scale, and 66 palette colours per 100 files where a theme variable exists; Claude's notes embedded. Score 80.
137
137
  - **[excalidraw/excalidraw](https://gregkozakiewicz.github.io/roast-my-design-system/examples/excalidraw-excalidraw.html)**: 78 off-scale spacing values and 90 !important declarations. Score 55.
138
- - **[dubinc/dub](https://gregkozakiewicz.github.io/roast-my-design-system/examples/dubinc-dub.html)**: 624 arbitrary bracket values, 21 duplicated components, and the chart-palette gap named. Score 20.
138
+ - **[dubinc/dub](https://gregkozakiewicz.github.io/roast-my-design-system/examples/dubinc-dub.html)**: 642 arbitrary bracket values, 21 duplicated components, and the chart-palette gap named. Score 20.
139
139
  - **[telekom/scale](https://gregkozakiewicz.github.io/roast-my-design-system/examples/telekom-scale.html)** (Stencil): 95 Stencil components read by tag; 66 spacing values outside the scale where about 12 would do; Claude's notes embedded. Score 55.
140
140
  - **[magicuidesign/magicui](https://gregkozakiewicz.github.io/roast-my-design-system/examples/magicui.html)** (registry): counted on the components it publishes, 52 off-theme colours per 100 files in the code it ships, its docs site kept out and named. Score 78.
141
141
  - **[adobe/spectrum-web-components](https://gregkozakiewicz.github.io/roast-my-design-system/examples/adobe-spectrum.html)** (Lit): hardcoded colours sitting beside 744 colour tokens, and 37 !important declarations. Score 66.
142
142
 
143
143
  The full report for vercel/ai-chatbot. The verdict answers what the agent will learn here, the gap section comes first, then "What the repo teaches the agent", Claude's read of the scan:
144
144
 
145
- ![The full diagnosis report for vercel/ai-chatbot in dark mode: a fixed side panel with the health score and what it measures, the stack, how the repo was read as a shadcn install, an index of every section and what is not the team's and not counted; then where the agent will have to guess, the fixes you can make right now, each with what it is worth and its copy-the-fix-prompt button, the wrapped present with the agent rules, an agent trap callout, 3-yardstick tiles including the 2 shadcn tiles, the adoption map treemap, palette forensics, the shadcn theme variable by variable, spacing receipts, typography specimens, offenders, duplicates, and the component usage ledger](assets/report-full-dark.png?v=9.2.6)
145
+ ![The full diagnosis report for vercel/ai-chatbot in dark mode: a fixed side panel with the health score and what it measures, the stack, how the repo was read as a shadcn install, an index of every section and what is not the team's and not counted; then where the agent will have to guess, the fixes you can make right now, each with what it is worth and its copy-the-fix-prompt button, the wrapped present with the agent rules, an agent trap callout, 3-yardstick tiles including the 2 shadcn tiles, the adoption map treemap, palette forensics, the shadcn theme variable by variable, spacing receipts, typography specimens, offenders, duplicates, and the component usage ledger](assets/report-full-dark.png?v=9.3.3)
146
146
 
147
147
  The same report in light mode (one file, built-in toggle):
148
148
 
149
- ![The diagnosis report in light mode](assets/report-light-hero.png?v=9.2.6)
149
+ ![The diagnosis report in light mode](assets/report-light-hero.png?v=9.3.3)
150
150
 
151
151
  ## Why the numbers hold
152
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "9.3.2",
3
+ "version": "9.3.3",
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": [
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { existsSync } from 'node:fs';
10
10
  import {
11
- isEmail, EMAIL_KIT_RE, ARTWORK_NAME_RE, foreignStylesheet, RENDER_TO_IMAGE_RE, OG_ROUTE_RE, RENDERER_PATH_RE, CRASH_PAGE_RE, svgHeavy, exemptReason, isLibraryClass, WIDGET_CSS_RE, WIDGET_CONFIG_RE,
11
+ isEmail, foreignStylesheet, CRASH_PAGE_RE, exemptReason, isLibraryClass, WIDGET_CSS_RE, WIDGET_CONFIG_RE,
12
12
  } from '../lib/exempt.mjs';
13
13
  import { join } from 'node:path';
14
14
  import { canonical, parseColor } from '../lib/color.mjs';
@@ -25,7 +25,10 @@ class Tally {
25
25
  e.count++;
26
26
  e.files.set(file, (e.files.get(file) ?? 0) + 1);
27
27
  }
28
- toJSON(limitFiles = 5) {
28
+ // `every` keeps the whole per-file tally beside the capped list shown in
29
+ // the report, for a caller that has to split the count by folder exactly
30
+ // (profiles/splitArbitrary: installed code against the team's own).
31
+ toJSON(limitFiles = 5, { every = false } = {}) {
29
32
  return [...this.map.values()]
30
33
  .sort((a, b) => b.count - a.count)
31
34
  .map((e) => ({
@@ -33,6 +36,7 @@ class Tally {
33
36
  count: e.count,
34
37
  files: [...e.files.entries()].sort((a, b) => b[1] - a[1]).slice(0, limitFiles)
35
38
  .map(([file, count]) => ({ file, count })),
39
+ ...(every ? { every: [...e.files.entries()].map(([file, count]) => ({ file, count })) } : {}),
36
40
  }));
37
41
  }
38
42
  }
@@ -500,7 +504,6 @@ export function harvestTokens(root, styleFiles, codeFiles, { email = null } = {}
500
504
  // reason to discredit the whole report.
501
505
  // Email, artwork, mostly-SVG files and pixel renderers: all imported from
502
506
  // lib/exempt.mjs, the one list every checker reads.
503
- const ARTWORK_RE = ARTWORK_NAME_RE;
504
507
  // every file a check skipped, with the reason, so the report can name them
505
508
  const exemptFiles = [];
506
509
  const skip = (f, src) => { const r = exemptReason(f, src, { email }); if (r) exemptFiles.push({ file: f, reason: r }); return true; };
@@ -566,18 +569,14 @@ export function harvestTokens(root, styleFiles, codeFiles, { email = null } = {}
566
569
  paletteFiles.add(f);
567
570
  for (const m of src.matchAll(HEX_RE)) tokenDefined.add(normalizeHex(m[0]));
568
571
  }
569
- if (ARTWORK_RE.test(f) || RENDERER_PATH_RE.test(f) || svgHeavy(src)) { skip(f, src); continue; }
570
- // A render-to-image surface (OG card, PDF invoice) is artwork drawn with
571
- // code: its colours are the picture's, not the product's palette. Until
572
- // 5.10.0 they were the only "strays" a clean shadcn repo had, and they
573
- // triggered the "every single one is hardcoded" banner on it.
574
- const renderToImage = RENDER_TO_IMAGE_RE.test(src) || OG_ROUTE_RE.test(f);
575
- if (renderToImage) { skip(f, src); continue; }
576
- // a page a headless browser prints to a PDF (rybbit's reports), found by
577
- // the walk from the file that prints it (lib/exempt.mjs printedFilesOf)
578
- if (email?.printed?.includes(f)) { skip(f, src); continue; }
579
- // an email written with an email kit, wherever it lives (react-email)
580
- if (EMAIL_KIT_RE.test(src)) { skip(f, src); continue; }
572
+ // Artwork, renderers, render-to-image surfaces (OG cards, PDF invoices),
573
+ // pages a headless browser prints, emails written with a kit: one rule,
574
+ // lib/exempt.mjs, the same answer the live checks and the guard give.
575
+ // Until 9.3.3 this counter kept its own list, and the two had drifted: a
576
+ // file named Badge or IconButton walked free here on its name alone
577
+ // (2,321 files in 173 repos) while an icon in an icons folder was
578
+ // counted here and exempt everywhere else (likec4's 12,203 icon colours).
579
+ if (exemptReason(f, src, { email })) { skip(f, src); continue; }
581
580
 
582
581
  // Tailwind classes
583
582
  for (const cls of classStrings(src)) {
@@ -603,7 +602,7 @@ export function harvestTokens(root, styleFiles, codeFiles, { email = null } = {}
603
602
  // length inside them is still a hardcoded value — harvest those from every
604
603
  // non-trivial block; count only the all-literal blocks.
605
604
  const allBlocks = inlineStyleBlocks(src).filter((b) => !TRIVIAL_INLINE_RE.test(b.trim()));
606
- const staticBlocks = renderToImage ? [] : allBlocks.filter(isStaticInline);
605
+ const staticBlocks = allBlocks.filter(isStaticInline);
607
606
  if (staticBlocks.length) {
608
607
  inlineStyleCount += staticBlocks.length;
609
608
  inlineStyleFiles.set(f, (inlineStyleFiles.get(f) ?? 0) + staticBlocks.length);
@@ -810,7 +809,7 @@ export function harvestTokens(root, styleFiles, codeFiles, { email = null } = {}
810
809
  spacing: twSpacing.toJSON(),
811
810
  radii: twRadii.toJSON(),
812
811
  textSizes: twTextSizes.toJSON(),
813
- arbitrary: twArbitrary.toJSON(),
812
+ arbitrary: twArbitrary.toJSON(5, { every: true }),
814
813
  },
815
814
  important: {
816
815
  // aimed at a library's own class names: kept out, named at the top
@@ -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 = '9.3.2';
4
+ export const VERSION = '9.3.3';
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.
@@ -197,14 +197,18 @@ export function splitArbitrary(entries, dirs) {
197
197
  const installedValues = new Map();
198
198
  let installedUses = 0;
199
199
  for (const e of entries) {
200
- const ownFiles = (e.files ?? []).filter((f) => !underAny(f.file, dirs));
201
- const instFiles = (e.files ?? []).filter((f) => underAny(f.file, dirs));
202
- const instCount = instFiles.reduce((s, f) => s + f.count, 0);
200
+ // the whole per-file tally when the harvest kept one (every), else the
201
+ // capped list: a use in a file that fell off the list of five was counted
202
+ // as the team's own, so ai-chatbot's installed badge put one of shadcn's
203
+ // [3px] values in the headline (2026-09-30)
204
+ const all = e.every ?? e.files ?? [];
205
+ const ownFiles = all.filter((f) => !underAny(f.file, dirs)).sort((a, b) => b.count - a.count).slice(0, 5);
206
+ const instCount = all.filter((f) => underAny(f.file, dirs)).reduce((s, f) => s + f.count, 0);
203
207
  if (instCount) { installedUses += instCount; installedValues.set(e.value, (installedValues.get(e.value) ?? 0) + instCount); }
204
- // the file list under an entry is capped, so own is the remainder of the
205
- // entry's total, never a sum of the files that happened to be listed
208
+ // own is the remainder of the entry's total, never a sum of the files
209
+ // that happened to be listed
206
210
  const ownCount = e.count - instCount;
207
- if (ownCount > 0) own.push({ ...e, count: ownCount, files: ownFiles });
211
+ if (ownCount > 0) { const { every, ...rest } = e; own.push({ ...rest, count: ownCount, files: ownFiles }); }
208
212
  }
209
213
  own.sort((a, b) => b.count - a.count);
210
214
  const values = [...installedValues.entries()].sort((a, b) => b[1] - a[1]).map(([value, count]) => ({ value, count }));