roast-my-design-system 7.3.0 → 7.3.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
@@ -1,4 +1,4 @@
1
- <img src="https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/roaster_logo_300px.png?v=7.3.0" width="72" alt="roast-my-design-system">
1
+ <img src="https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/roaster_logo_300px.png?v=7.3.2" width="72" alt="roast-my-design-system">
2
2
 
3
3
  # roast-my-design-system
4
4
 
@@ -77,11 +77,11 @@ Not to be confused with each other: **"Why this matters"** is generic, ships wit
77
77
 
78
78
  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:
79
79
 
80
- ![The full diagnosis report for vercel/ai-chatbot in dark mode: health score, the line saying how the repo was read as a shadcn install, the What the numbers mean analysis written by Claude, priced Where to start moves each with 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](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-full-dark.png?v=7.3.0)
80
+ ![The full diagnosis report for vercel/ai-chatbot in dark mode: health score, the line saying how the repo was read as a shadcn install, the What the numbers mean analysis written by Claude, priced Where to start moves each with 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](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-full-dark.png?v=7.3.2)
81
81
 
82
82
  The same report in light mode (one file, built-in toggle):
83
83
 
84
- ![The diagnosis report in light mode](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-light-hero.png?v=7.3.0)
84
+ ![The diagnosis report in light mode](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-light-hero.png?v=7.3.2)
85
85
 
86
86
  ## What makes the numbers trustworthy
87
87
 
@@ -235,6 +235,7 @@ After the roast, the skill also offers to write `design-system-rules.md` to disk
235
235
 
236
236
  - **[telekom/scale](https://gregkozakiewicz.github.io/roast-my-design-system/examples/telekom-scale.html)**: Stencil, 95 components read by tag, with Claude's notes embedded
237
237
  - **[adobe/spectrum-web-components](https://gregkozakiewicz.github.io/roast-my-design-system/examples/adobe-spectrum.html)**: Lit, the `--spectrum-*` namespace named in the header
238
+ - **[npx shadcn create, fresh](https://gregkozakiewicz.github.io/roast-my-design-system/examples/shadcn-create-fresh.html)**: a factory install with all 61 components, scanned as is
238
239
  - **[vercel/ai-chatbot](https://gregkozakiewicz.github.io/roast-my-design-system/examples/vercel-ai-chatbot.html)**: React, with Claude's notes embedded
239
240
  - **[excalidraw/excalidraw](https://gregkozakiewicz.github.io/roast-my-design-system/examples/excalidraw-excalidraw.html)**
240
241
  - **[dubinc/dub](https://gregkozakiewicz.github.io/roast-my-design-system/examples/dubinc-dub.html)**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "7.3.0",
3
+ "version": "7.3.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, 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": [
@@ -277,7 +277,7 @@ const offenders = h.tokens.offenders ?? [];
277
277
  const candidates = [];
278
278
  const effColors = tokenLed ? colorStrays : colors.length;
279
279
  const effGreys = tokenLed ? greyStrays : greys.length;
280
- if (typefaces.length > 3) candidates.push({ ratio: typefaces.length / 3, text: `${typefaces.length} typefaces, brands use 2 or 3` });
280
+ if (typefaces.length > 3) candidates.push({ ratio: typefaces.length / 3, text: `${typefaces.length} typefaces. Most products use 2 or 3` });
281
281
  else if (typefaces.length && fontFamilies.length > 6) candidates.push({ ratio: fontFamilies.length / 6, text: `${typefaces.length} typeface${typefaces.length > 1 ? 's' : ''} declared ${fontFamilies.length} different ways` });
282
282
  if (effColors > 24 * 1.25) candidates.push({ ratio: effColors / 24, text: tokenLed
283
283
  ? `${n(colorStrays)} hardcoded colours outside the token set, your agent will happily copy them at random`
@@ -285,15 +285,15 @@ if (effColors > 24 * 1.25) candidates.push({ ratio: effColors / 24, text: tokenL
285
285
  if (effGreys > 13 * 1.25) candidates.push({ ratio: effGreys / 13, text: tokenLed
286
286
  ? `${greyStrays} hardcoded greys outside the token set`
287
287
  : `${greys.length} shades of grey doing the job of 13` });
288
- if (spacingTotal > 15) candidates.push({ ratio: spacingTotal / 12, text: `${spacingTotal} off-scale spacing values where a dozen would do` });
289
- if (nearPairs.length >= 3) candidates.push({ ratio: 1 + nearPairs.length / 6, text: `${nearPairs.length} colour pairs are nearly identical (${nearPairs[0].a.value} next to ${nearPairs[0].b.value}), copy-paste, not decisions` });
290
- if (important.count >= 10) candidates.push({ ratio: 1 + important.count / 30, text: `${n(important.count)} !important declarations, the cascade admitting defeat` });
288
+ if (spacingTotal > 15) candidates.push({ ratio: spacingTotal / 12, text: `${spacingTotal} spacing values sit outside the scale. A tidy system needs about 12` });
289
+ if (nearPairs.length >= 3) candidates.push({ ratio: 1 + nearPairs.length / 6, text: `${nearPairs.length} pairs of colours are nearly the same (${nearPairs[0].a.value} next to ${nearPairs[0].b.value}). Each pair is 1 colour recorded twice` });
290
+ if (important.count >= 10) candidates.push({ ratio: 1 + important.count / 30, text: `${n(important.count)} !important declarations. Each one forces a style through instead of fixing the rule that blocked it` });
291
291
  if (componentsMeasured && iconCollisions.length >= 5) candidates.push({ ratio: 1 + iconCollisions.length / 8, text: `two icon sets collide on ${iconCollisions.length} names` });
292
- if (componentsMeasured && hardDupes.length > 0) candidates.push({ ratio: 1 + hardDupes.length / 5, text: `${hardDupes.length} component${hardDupes.length > 1 ? 's' : ''} implemented more than once, so an agent asked for a Button has several random options` });
293
- if (inline.count > 20) candidates.push({ ratio: inline.count / 20, text: `${n(inline.count)} inline style blocks bypassing every system, and teaching your agent to do the same` });
294
- if (arbitraryCount >= 20) candidates.push({ ratio: arbitraryCount / 30, text: `${n(arbitraryCount)} arbitrary values like ${arbitrary[0].value} punched through the Tailwind scale, each one a precedent your agent will follow` });
295
- if (agentFiles.length === 0) candidates.push({ ratio: 1.3, text: `no agent rules, so your AI is guessing` });
296
- if ((h.staleRules ?? []).length >= 2) candidates.push({ ratio: 1.2 + h.staleRules.length / 10, text: `${h.staleRules.length} references in your agent rules point at things this scan can no longer find` });
292
+ if (componentsMeasured && hardDupes.length > 0) candidates.push({ ratio: 1 + hardDupes.length / 5, text: `${hardDupes.length} component${hardDupes.length > 1 ? 's' : ''} exist in more than 1 version. An agent asked for a Button can not tell which one to use` });
293
+ if (inline.count > 20) candidates.push({ ratio: inline.count / 20, text: `${n(inline.count)} inline style blocks skip the design system. An agent that reads them learns to do the same` });
294
+ if (arbitraryCount >= 20) candidates.push({ ratio: arbitraryCount / 30, text: `${n(arbitraryCount)} values like ${arbitrary[0].value} are written outside the Tailwind scale. An agent that reads them learns to do the same` });
295
+ if (agentFiles.length === 0) candidates.push({ ratio: 1.3, text: `No agent rules file, so your AI agent has nothing to follow` });
296
+ if ((h.staleRules ?? []).length >= 2) candidates.push({ ratio: 1.2 + h.staleRules.length / 10, text: `${h.staleRules.length} lines in your agent rules refer to files that no longer exist` });
297
297
  const findings = candidates.sort((a, b) => b.ratio - a.ratio).map((c) => c.text);
298
298
 
299
299
  let verdict = findings.length === 0
@@ -337,8 +337,8 @@ function row(label, valText, refVal, value) {
337
337
  }
338
338
  const FALLBACK_TARGET = {
339
339
  colors: 'a system needs ~24', greys: 'a scale has up to 13', spacing: 'a dozen deliberate exceptions',
340
- exactDuplicates: 'should be 0', inlineStyles: 'invisible to any system', nearPairs: 'copy-paste, not decisions',
341
- important: 'the cascade admitting defeat', neverImported: 'the system nobody found', arbitrary: 'a handful of deliberate exceptions',
340
+ exactDuplicates: 'should be 0', inlineStyles: 'invisible to any system', nearPairs: '1 colour recorded twice',
341
+ important: 'forces a style through', neverImported: 'the system nobody found', arbitrary: 'a handful of deliberate exceptions',
342
342
  paintTin: 'the theme file already has a variable for it', doorOverrides: 'use a variant instead',
343
343
  };
344
344
  // Dress one judged tile for the page: formatted number, comparison rows.
@@ -495,7 +495,7 @@ function paletteSection() {
495
495
  <div class="usage-bar">${cells}${restCell}</div>
496
496
  ${ramp}
497
497
  ${nearPairs.length ? `
498
- <div class="receipts">${eyebrow(`${nearPairs.length} nearly identical pair${nearPairs.length === 1 ? '' : 's'} · copy-paste, not decisions`)}
498
+ <div class="receipts">${eyebrow(`${nearPairs.length} nearly identical pair${nearPairs.length === 1 ? '' : 's'} · each pair is 1 colour recorded twice`)}
499
499
  <div class="chips-row">${nearPairs.slice(0, 8).map((pr) => `<span class="vchip" title="every channel within ${pr.d} of its twin"><i class="ndot" style="background:${swatch(pr.a.value)}"></i>${esc(pr.a.value)} ×${pr.a.count} <b class="nsim">≈</b> <i class="ndot" style="background:${swatch(pr.b.value)}"></i>${esc(pr.b.value)} ×${pr.b.count}</span>`).join('')}${nearPairs.length > 8 ? `<span class="vchip dim">+${nearPairs.length - 8} more</span>` : ''}</div>
500
500
  ${whyToggle('nearPairs')}</div>` : ''}
501
501
  </div>
@@ -514,31 +514,31 @@ function dupesTrap() {
514
514
  const hard = exactDupes.filter((d) => !d.wrapped);
515
515
  if (hard.length < 2) return '';
516
516
  const top = hard[0];
517
- return trapBox(`&lt;${esc(top.name)}&gt; alone has ${top.files.length} implementations. An agent asked for one cannot tell which is canonical, so it picks at random or writes another, and every wrong pick becomes the example the next agent copies. This is the finding that multiplies itself.`);
517
+ return trapBox(`&lt;${esc(top.name)}&gt; alone has ${top.files.length} implementations. An agent asked for one can not tell which is the real one, so it picks 1 at random or writes another. Every wrong pick becomes the example the next agent copies.`);
518
518
  }
519
519
  function spacingTrap() {
520
520
  const candidates = [...spacing, ...(arbitrary ?? [])].filter((s) => s.count >= 15)
521
521
  .sort((a, b) => b.count - a.count);
522
522
  if (candidates.length) {
523
523
  const t = candidates[0];
524
- return trapBox(`${esc(t.value)} appears ${t.count} times. An agent looking for how this repo does spacing reads repetition as intent, so it will write occurrence ${t.count + 1}. The most copied pattern here is the one you least want copied.`);
524
+ return trapBox(`${esc(t.value)} appears ${t.count} times. An agent looking for how this repo does spacing reads a repeated value as the rule, so it will write use number ${t.count + 1}. The most copied value here is the one you least want copied.`);
525
525
  }
526
526
  if (twSpacing.length > 0 && spacing.length >= 10) {
527
- return trapBox(`Two spacing dialects coexist here: Tailwind steps and ${spacing.length} raw CSS values. A human knows which is legacy. An agent sees two valid options and matches whichever file it opened last, so every edit is a coin toss between systems.`);
527
+ return trapBox(`Two spacing dialects coexist here: Tailwind steps and ${spacing.length} raw CSS values. A person knows which one is old. An agent sees 2 valid options and follows whichever file it opened last, so each edit lands in one system or the other by chance.`);
528
528
  }
529
529
  return '';
530
530
  }
531
531
  function coloursTrap() {
532
532
  if (nearPairs.length < 6) return '';
533
- return trapBox(`This palette holds ${nearPairs.length} pairs of colours a screen can barely tell apart. An agent asked for the brand grey cannot see the difference either, so it copies whichever twin is nearest, and when unsure it invents a third between them. Twins breed triplets.`);
533
+ return trapBox(`This palette holds ${nearPairs.length} pairs of colours a screen can barely tell apart. An agent asked for the brand grey can not see the difference either, so it copies whichever twin is nearest, and when unsure it writes a third between them. 2 becomes 3.`);
534
534
  }
535
535
  function inlineTrap() {
536
536
  if (inline.count < 50) return '';
537
- return trapBox(`${n(inline.count)} inline styles live where the system should. An agent learns a codebase by example, and every inline style is an example that says the system is optional. Each exception it copies becomes the precedent for the next one.`);
537
+ return trapBox(`${n(inline.count)} inline styles live where the system should. An agent learns a codebase by example, and every inline style is an example that says the system is optional. Each one it copies becomes the example for the next.`);
538
538
  }
539
539
  function importantTrap() {
540
540
  if (important.count < 20) return '';
541
- return trapBox(`${n(important.count)} !important declarations, and each one is a shouting match a previous developer decided to win by force. An agent whose style will not apply does what the repo taught it and shouts louder. The volume in this codebase only goes up.`);
541
+ return trapBox(`${n(important.count)} !important declarations, and each one forces a style through instead of fixing the rule that blocked it. An agent whose style will not apply does what the repo taught it and adds another. The count only goes up.`);
542
542
  }
543
543
  function collisionsTrap() {
544
544
  if (!collisions.length) return '';
@@ -551,7 +551,7 @@ function collisionsTrap() {
551
551
  function orphansTrap() {
552
552
  if (!componentsMeasured || isLibrary) return '';
553
553
  if (neverImported.length < 10) return '';
554
- return trapBox(`${neverImported.length} components are never imported anywhere. An agent searching for a Button finds the abandoned ones alongside the canonical one with nothing to tell them apart, so yesterday's dead end becomes today's example.`);
554
+ return trapBox(`${neverImported.length} components are never imported anywhere. An agent searching for a Button finds the unused ones next to the real one with nothing to tell them apart, so an old dead end becomes today's example.`);
555
555
  }
556
556
  // At most three traps per report, in severity order: scarcity is what makes
557
557
  // the marker readable as a warning rather than wallpaper.
@@ -572,7 +572,7 @@ function spacingBars() {
572
572
  <div class="bar-track"><div class="bar${s.off ? ' bar-off' : ''}" style="width:${Math.max(2, Math.round((s.count / max) * 100))}%"></div></div>
573
573
  <span class="bar-count">${n(s.count)}</span></div>`).join('');
574
574
  const arb = arbitraryCount ? `
575
- <div class="receipts">${eyebrow(`${n(arbitraryCount)} arbitrary values punched through the scale, one bracket at a time`)}
575
+ <div class="receipts">${eyebrow(`${n(arbitraryCount)} bracket values written outside the scale`)}
576
576
  <div class="chips-row">${arbitrary.slice(0, 10).map((a) => `<span class="vchip bad" title="${esc(a.files?.[0]?.file ?? '')}">${esc(a.value)} ×${a.count}</span>`).join('')}${arbitrary.length > 10 ? `<span class="vchip dim">+${arbitrary.length - 10} more</span>` : ''}</div>
577
577
  ${whyToggle('arbitrary')}</div>` : '';
578
578
  return `<section class="glass pad">
@@ -759,7 +759,7 @@ function inlineSection() {
759
759
  if (inline.count < 5 && important.count < 5) return '';
760
760
  const max = Math.max(...inline.files.slice(0, 6).map((f) => f.count), 1);
761
761
  const importantRow = important.count ? `
762
- <div class="receipts">${eyebrow(`${n(important.count)} !important declaration${important.count === 1 ? '' : 's'} · the cascade admitting defeat`)}
762
+ <div class="receipts">${eyebrow(`${n(important.count)} !important declaration${important.count === 1 ? '' : 's'} · each one forces a style through`)}
763
763
  <div class="chips-row">${important.files.map((f) => `<span class="vchip bad" title="${esc(f.file)}">${esc(basename(f.file))} ×${f.count}</span>`).join('')}</div>
764
764
  ${whyToggle('important')}</div>` : '';
765
765
  const blocks = inline.count >= 5 ? `<div class="fam-rows">${inline.files.slice(0, 6).map((f) => `
@@ -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 = '7.3.0';
4
+ export const VERSION = '7.3.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.
@@ -142,7 +142,7 @@ export function validateContent(content, k) {
142
142
  const repoArb = (k.tokens.tailwind?.arbitrary ?? []).find((x) => x.value === a.value);
143
143
  const priorArb = discount(repoArb?.count, localArb, a.value);
144
144
  add('arbitrary-value', 'violation', a.index,
145
- `Arbitrary bracket ${a.value}${priorArb ? `, already punched through the scale ${priorArb}x in this repo` : ''}.`,
145
+ `Bracket value ${a.value}${priorArb ? `, already written outside the scale ${priorArb}x in this repo` : ''}.`,
146
146
  'If the value repeats it is a decision: name it as a token. If it does not, use the nearest scale step.');
147
147
  }
148
148
 
@@ -205,7 +205,7 @@ export function validateContent(content, k) {
205
205
  }
206
206
  for (const imp of got.important) {
207
207
  add('important', 'violation', imp.index,
208
- '!important is the cascade admitting defeat.',
208
+ '!important forces a style through instead of fixing the rule that blocked it.',
209
209
  'Fix the selector or the source of the conflict instead of shouting over it.');
210
210
  }
211
211
 
@@ -64,7 +64,7 @@ export function getContext(k, { path = null } = {}) {
64
64
  ? 'SPACING: Tailwind scale only. No arbitrary brackets (p-[13px]); a repeating value is a token, not a bracket.'
65
65
  : k.spacingSeen.size
66
66
  ? `SPACING: this repo has no scale, just ${k.spacingSeen.size} values in circulation. Reuse one rather than inventing value number ${k.spacingSeen.size + 1}; do not treat them as a system.`
67
- : 'SPACING: nothing on record yet. Whatever you pick becomes a precedent; pick something divisible by 4.');
67
+ : 'SPACING: nothing on record yet. Whatever you pick, the next agent will copy; pick something divisible by 4.');
68
68
 
69
69
  const faces = distinctTypefaces(t.fontFamilies ?? []);
70
70
  if (faces.length) L.push(`TYPE: ${faces.slice(0, 3).join(', ')}. No new typefaces, no hand-declared font stacks.`);
@@ -177,7 +177,7 @@ export function findToken(k, { value } = {}) {
177
177
  }
178
178
  const prior = k.spacingSeen.get(v);
179
179
  if (prior) return `${v} is already in use ${prior}x in this repo. Consistent, so use it, but know it lives off any scale.`;
180
- if (!k.spacingSeen.size) return `This repo has no spacing values on record to compare ${v} against. Whatever you pick becomes a precedent; pick something divisible by 4.`;
180
+ if (!k.spacingSeen.size) return `This repo has no spacing values on record to compare ${v} against. Whatever you pick, the next agent will copy; pick something divisible by 4.`;
181
181
  const near = [...k.spacingSeen.entries()].map(([val, count]) => ({ val, count, px: toPxLocal(val) }))
182
182
  .filter((x) => x.px !== null).sort((a, b) => Math.abs(a.px - px) - Math.abs(b.px - px))[0];
183
183
  return near
@@ -190,7 +190,7 @@ if (neverImported.length >= 3) {
190
190
  section('Styling discipline');
191
191
  rule(`Never write ${reusable.length ? '`style={{ ... }}`' : 'inline `style="..."` attributes'} for static values; styling belongs to classes and tokens where the system can see it.`);
192
192
  if ((t.inlineStyles?.count ?? 0) > 0) lines.push(` (${t.inlineStyles.count} static inline block${t.inlineStyles.count === 1 ? '' : 's'} already exist; do not add to them.)`);
193
- if ((t.important?.count ?? 0) >= 3) rule(`Never write !important; the scan found ${t.important.count} declaration${t.important.count === 1 ? '' : 's'} already. When a style does not apply, fix the selector or the source of the conflict instead of shouting over it.`);
193
+ if ((t.important?.count ?? 0) >= 3) rule(`Never write !important; the scan found ${t.important.count} declaration${t.important.count === 1 ? '' : 's'} already. When a style does not apply, fix the selector or the source of the conflict instead of forcing the style through.`);
194
194
  rule('Before styling anything new, look at a neighbouring component and match how it does it. Consistency with the repo beats personal preference.');
195
195
 
196
196
  lines.push('');
@@ -29,7 +29,7 @@ const RULES = {
29
29
  'duplicate-component': { name: 'Duplicated component', desc: 'A component implemented more than once. An agent asked for it has several plausible answers.' },
30
30
  'never-imported-component': { name: 'Component nobody imports', desc: 'Defined in the system, imported by nothing. It sits there as a wrong answer waiting to be picked.' },
31
31
  'inline-styles': { name: 'Inline style blocks', desc: 'Static values written as style attributes, bypassing the system and teaching the agent to do the same.' },
32
- 'important-styles': { name: '!important declarations', desc: 'The cascade admitting defeat. Fix specificity at the source and these stop being necessary.' },
32
+ 'important-styles': { name: '!important declarations', desc: 'Forces a style through instead of fixing the rule that blocked it. Fix specificity at the source and these stop being necessary.' },
33
33
  'stray-colors': { name: 'Hardcoded colours', desc: 'Colour values no token names. Each one is a value the agent will happily copy.' },
34
34
  'stale-rule-reference': { name: 'Stale agent-rule reference', desc: 'The agent rules reference something this scan can no longer find. A rule the agent obeys is worse than no rule when the repo has moved on.' },
35
35
  };