roast-my-design-system 5.8.0 → 5.9.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
@@ -8,9 +8,9 @@
8
8
 
9
9
  A free CLI tool (and Claude Code skill) that roasts your repo's design system with real data, then generates the rules that keep your AI agent on-system.
10
10
 
11
- > **New in 5.7: it reads web components, and it admits what it cannot read.** Stencil, Lit and plain custom elements are detected by their tag registrations (validated on telekom/scale, Shoelace, Ionic, Material Web, Adobe Spectrum, Siemens iX, Baloise and Lion); component libraries get composition maps and library language instead of adoption accusations; an earned token namespace like `--telekom-*` is named in the header. And when a repo's component pattern is beyond the scan, the tiles say "not measured" and the score takes no credit, because zeros the scanner never earned are not discipline.
11
+ > **New in 5.9: the agent card knows which doors your rules actually reach.** The report now states, as plain fact, which tools can read the rules files you have: Claude Code reads CLAUDE.md, Codex reads AGENTS.md, Cursor reads AGENTS.md and `.cursor/rules`. Where a door is missing, the card shows the one-line fix (Claude Code skips AGENTS.md; a CLAUDE.md containing the single line `@AGENTS.md` closes the gap). The scan also finds rules files nested in subfolders now, which is how monorepos really do it (twenty carries 35 AGENTS.md files; a root-only look reported 1), and it recognises GEMINI.md, `.windsurf/rules` and `.github/instructions`.
12
12
 
13
- > **New in 5.6: every fix comes with its prompt.** Each move in the report's Where to start now carries a copy button holding a ready-made fix prompt: the finding, the file paths, the expected score payoff, and the calm rules for fixing without steamrolling craft. Paste it into your agent, fix, re-run the scan, press the next button. View it first if you like: the prompt unfolds right in the report.
13
+ > **New in 5.8: every finding can explain itself.** A "why this matters" toggle under each finding unfolds the calm story behind the brutal number: how the mess arrives innocently, what it costs later, how an agent multiplies it, and why the ideal sits where it sits, benchmark medians included. Written to a plain-language standard, reviewed word by word.
14
14
 
15
15
  > **New in 5.0: it runs as a local MCP server.** One command, and your agent asks the design system before writing UI, then gets the work checked after: which Button is canonical, which token holds that colour, review my changes. Local, deterministic, nothing leaves your machine. See [Live answers over MCP](#live-answers-over-mcp).
16
16
 
@@ -21,7 +21,7 @@ Run it on your codebase and get, in about a second:
21
21
  - **Reads React and web components alike.** Stencil, Lit and custom elements detected by tag registration, counted by kebab tag; libraries get composition maps, not adoption accusations; earned token namespaces named in the header; and anything the scan cannot read is declared "not measured" instead of scored.
22
22
  - **The receipts behind it.** Every colour and its near-identical twin, every spacing value, typeface, duplicated or never-imported component, inline style and !important, with real file paths, in one self-contained HTML report you can open, Slack or email.
23
23
  - **The first fixes ranked by payoff, each with its prompt.** A "Where to start" list derived from your own numbers, and every move carries a copy button with a ready-made fix prompt for your agent: the finding, the files, the expected payoff, and rules that respect deliberate craft. Fix, re-run the scan, press the next button.
24
- - **Rules that stop the mess coming back.** A generated `design-system-rules.md` with canonical components, your token file, and known duplicates to avoid, so your AI agent follows your system instead of guessing at it. `--apply` injects them into every agent file you have: Claude, Cursor, GitHub Copilot, and Windsurf. Every scan also checks the rules you already have for stale references: paths that no longer exist, components named canonical that nothing imports anymore.
24
+ - **Rules that stop the mess coming back.** A generated `design-system-rules.md` with canonical components, your token file, and known duplicates to avoid, so your AI agent follows your system instead of guessing at it. `--apply` injects them into every agent file you have: Claude, Cursor, GitHub Copilot, and Windsurf. Every scan also checks the rules you already have for stale references: paths that no longer exist, components named canonical that nothing imports anymore. And the agent card states which tools can actually read the rules you have (Claude Code, Codex, Cursor), with the one-line fix where a door is missing.
25
25
 
26
26
  ## Why this exists
27
27
 
@@ -60,11 +60,11 @@ One scan powers all of it; the flags decide what lands on disk. Combine freely.
60
60
 
61
61
  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:
62
62
 
63
- ![The full diagnosis report for vercel/ai-chatbot in dark mode: health score, 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, three-yardstick tiles, the adoption map treemap, palette forensics, 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=5.8.0)
63
+ ![The full diagnosis report for vercel/ai-chatbot in dark mode: health score, 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, three-yardstick tiles, the adoption map treemap, palette forensics, 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=5.9.0)
64
64
 
65
65
  The same report in light mode (one file, built-in toggle):
66
66
 
67
- ![The diagnosis report in light mode](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-light-hero.png?v=5.8.0)
67
+ ![The diagnosis report in light mode](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-light-hero.png?v=5.9.0)
68
68
 
69
69
  ## What makes the numbers trustworthy
70
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "5.8.0",
3
+ "version": "5.9.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 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": [
@@ -1168,10 +1168,29 @@ function packagesSection() {
1168
1168
  }
1169
1169
 
1170
1170
  function agentSection() {
1171
- const have = agentFiles.map((c) => `<span class="chip chip-agent">${esc(c.file)} · ${esc(c.tool ?? c.kind)}</span>`).join('');
1171
+ // Root files get chips; nested ones (monorepo packages) fold into a single
1172
+ // count chip, because 35 chips is wallpaper and one number is a finding.
1173
+ const rootAgent = agentFiles.filter((c) => !c.nested);
1174
+ const nestedAgent = agentFiles.filter((c) => c.nested);
1175
+ const overflow = nestedAgent.find((c) => c.file.startsWith('(+'));
1176
+ const nestedCount = nestedAgent.filter((c) => !c.file.startsWith('(+')).length
1177
+ + (overflow ? parseInt(overflow.file.slice(2), 10) || 0 : 0);
1178
+ const have = rootAgent.map((c) => `<span class="chip chip-agent">${esc(c.file)} · ${esc(c.tool ?? c.kind)}</span>`).join('')
1179
+ + (nestedCount ? `<span class="chip chip-agent">+${nestedCount} nested in subfolders</span>` : '');
1180
+ const readsList = (rootAgent.length ? rootAgent : nestedAgent.slice(0, 3)).map((c) => `<span class="mono">${esc(c.file)}</span>`).join(', ');
1172
1181
  const msg = agentFiles.length
1173
- ? `<p class="sub">Your agent reads ${agentFiles.map((c) => `<span class="mono">${esc(c.file)}</span>`).join(', ')}. But none of it points at a single source of truth for components and tokens, because there isn't one yet. The numbers below are what your agent actually works from.</p>`
1182
+ ? `<p class="sub">Your agent reads ${readsList}${nestedCount && rootAgent.length ? `, plus ${nestedCount} rules file${nestedCount === 1 ? '' : 's'} nested in subfolders` : ''}. But none of it points at a single source of truth for components and tokens, because there isn't one yet. The numbers below are what your agent actually works from.</p>`
1174
1183
  : `<p class="sub">No <span class="mono">CLAUDE.md</span>, no <span class="mono">AGENTS.md</span>, no <span class="mono">.cursorrules</span>. Every time your AI builds UI here, it guesses, from everything below. This is why its output looks almost-but-not-quite right.</p>`;
1184
+ // Door coverage, stated as fact: which tools can read what exists. Claude
1185
+ // Code reads CLAUDE.md only; Codex reads AGENTS.md; Cursor reads AGENTS.md
1186
+ // and .cursor/rules (its legacy .cursorrules is labelled, not judged).
1187
+ const hasFile = (name) => agentFiles.some((c) => c.file === name || c.file.endsWith(`/${name}`));
1188
+ const claudeOk = hasFile('CLAUDE.md');
1189
+ const agentsOk = hasFile('AGENTS.md');
1190
+ const cursorOk = agentsOk || agentFiles.some((c) => c.file.endsWith('.cursor/rules') || c.file === '.cursor/rules');
1191
+ const cursorLegacyOnly = !cursorOk && hasFile('.cursorrules');
1192
+ const mark = (ok) => (ok ? '<b class="cov-yes">✓</b>' : '<b class="cov-no">—</b>');
1193
+ const coverage = agentFiles.length ? `<p class="sub coverage">Readable by: Claude Code ${mark(claudeOk)} · Codex ${mark(agentsOk)} · Cursor ${cursorLegacyOnly ? '<b class="cov-no">legacy file only</b>' : mark(cursorOk)}${claudeOk && !agentsOk ? ' · <span class="dim">copy the same rules into an AGENTS.md and the other tools see them too</span>' : ''}${agentsOk && !claudeOk ? ' · <span class="dim">Claude Code skips AGENTS.md: add a CLAUDE.md containing the single line <span class="mono">@AGENTS.md</span> and it reads the same rules</span>' : ''}</p>` : '';
1175
1194
  const stale = h.staleRules ?? [];
1176
1195
  const staleRows = stale.map((s) => `
1177
1196
  <div class="ledger-row">
@@ -1186,7 +1205,7 @@ function agentSection() {
1186
1205
  </div>` : '';
1187
1206
  return `<section class="glass pad agent">
1188
1207
  ${sectionHead('What your AI agent sees today', '')}
1189
- ${msg}${have ? `<div class="chips">${have}</div>` : ''}${staleBlock}</section>`;
1208
+ ${msg}${have ? `<div class="chips">${have}</div>` : ''}${coverage}${staleBlock}</section>`;
1190
1209
  }
1191
1210
 
1192
1211
  // ---------- page ----------
@@ -1294,6 +1313,9 @@ const html = `<!doctype html>
1294
1313
  .chip { border:1px solid var(--line); background:var(--card); border-radius:99px;
1295
1314
  padding:4px 12px; font:500 11.5px/1.5 var(--sans); color:var(--text); }
1296
1315
  .chip-agent { background:var(--text); color:var(--bg); border-color:var(--text); font-weight:600; }
1316
+ .coverage { margin-top:12px; }
1317
+ .cov-yes { color:var(--ok); font-weight:700; }
1318
+ .cov-no { color:var(--coral); font-weight:700; }
1297
1319
  .excl { margin-top:10px; font:500 12px/1.6 var(--sans); color:var(--dim); }
1298
1320
  .excl .mono { font-size:11.5px; color:var(--text); }
1299
1321
  .stale { margin-top:18px; }
@@ -1537,6 +1559,11 @@ const html = `<!doctype html>
1537
1559
  .foot-sub .pulse polyline { animation:none; stroke-dashoffset:0; }
1538
1560
  }
1539
1561
  .scan-id { font-family:var(--mono); font-size:11px; letter-spacing:.04em; }
1562
+ .copy-pill { font:700 9.5px/1 var(--sans); letter-spacing:.06em; text-transform:uppercase; padding:3px 8px;
1563
+ border-radius:99px; border:1px solid var(--line); background:transparent; color:var(--dim);
1564
+ cursor:pointer; vertical-align:1px; }
1565
+ .copy-pill:hover { color:var(--accent); border-color:var(--accent); }
1566
+ @media print { .copy-pill { display:none; } }
1540
1567
  footer .brand { color:var(--accent); font-weight:700; text-decoration:none; }
1541
1568
  footer .brand:hover { text-decoration:underline; }
1542
1569
  footer .creds { display:flex; gap:16px; font:700 9.5px/1.6 var(--sans); letter-spacing:.16em; text-transform:uppercase; color:var(--dim2); }
@@ -1646,6 +1673,8 @@ ${extraSectionsHtml()}
1646
1673
 
1647
1674
  ${whereToStartSection()}
1648
1675
 
1676
+ ${agentSection()}
1677
+
1649
1678
  ${giftSection()}
1650
1679
 
1651
1680
  ${trapsBlock()}
@@ -1654,8 +1683,6 @@ ${trapsBlock()}
1654
1683
 
1655
1684
  ${packagesSection()}
1656
1685
 
1657
- ${agentSection()}
1658
-
1659
1686
  <section style="margin-top:16px">${paletteSection()}</section>
1660
1687
 
1661
1688
  ${spacingBars()}
@@ -1669,7 +1696,7 @@ ${componentsSection()}
1669
1696
 
1670
1697
  <footer>
1671
1698
  <div class="foot-left">
1672
- <div>Generated by <a class="brand" href="https://github.com/gregkozakiewicz/roast-my-design-system">roast-my-design-system</a> <span class="ver">ver. ${VERSION}</span>, free on npm and as a Claude Code + Codex skill</div>
1699
+ <div>Generated by <a class="brand" href="https://github.com/gregkozakiewicz/roast-my-design-system">roast-my-design-system</a> <span class="ver">ver. ${VERSION}</span> · free to run: <a class="brand" href="https://www.npmjs.com/package/roast-my-design-system"><span class="mono" id="npx-cmd">npx roast-my-design-system</span></a> <button type="button" class="copy-pill" id="npx-copy">copy</button> · also a Claude Code + Codex skill and a local MCP server</div>
1673
1700
  <div class="foot-sub">Designed and built by <a class="author" href="https://gregkozakiewicz.com/?utm_source=roast-report"><span class="gk-mark"></span>Greg Kozakiewicz</a> · <span class="scan-id" title="scan id">${scanId}</span> · ${FEEDBACK_ASK} <a class="feedback" href="${feedbackUrl(VERSION).replace(/&/g, '&amp;')}">${PULSE_ICON}${FEEDBACK_CTA}</a></div>
1674
1701
  </div>
1675
1702
  <span class="creds"><span>Non-destructive scan</span><span>Read-only</span><span>Paths are real</span></span>
@@ -1728,6 +1755,17 @@ ${componentsSection()}
1728
1755
  v.textContent=show?'hide it':'view it first';
1729
1756
  });
1730
1757
  });
1758
+ var npxCopy=document.getElementById('npx-copy');
1759
+ if(npxCopy){
1760
+ npxCopy.addEventListener('click', function(){
1761
+ var text=document.getElementById('npx-cmd').textContent;
1762
+ var done=function(){ npxCopy.textContent='copied'; setTimeout(function(){ npxCopy.textContent='copy'; },1600); };
1763
+ var manual=function(){ var r=document.createRange(); r.selectNodeContents(document.getElementById('npx-cmd'));
1764
+ var s=getSelection(); s.removeAllRanges(); s.addRange(r); npxCopy.textContent='select + copy'; setTimeout(function(){ npxCopy.textContent='copy'; },2600); };
1765
+ if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(done, manual); }
1766
+ else { manual(); }
1767
+ });
1768
+ }
1731
1769
  document.querySelectorAll('.whybtn').forEach(function(b){
1732
1770
  b.addEventListener('click', function(){
1733
1771
  var holder=document.getElementById('why-'+b.getAttribute('data-why'));
@@ -2,17 +2,26 @@
2
2
  * Context-file harvest — the agent-facing and design-facing docs the repo
3
3
  * already has. Step 5 (wire up their agent) needs to know what exists so it
4
4
  * extends rather than clobbers; the diagnosis mentions what's missing.
5
+ *
6
+ * Two passes: the root checklist below (known doors at known paths), then a
7
+ * nested sweep for rules files living inside subfolders, because that is how
8
+ * monorepos actually do it (twenty carries 35 AGENTS.md files; a root-only
9
+ * look reported 1 and called it the whole story). Nested entries carry
10
+ * nested:true so the report can summarise them instead of flooding chips.
5
11
  */
6
- import { existsSync, readFileSync, statSync } from 'node:fs';
12
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
7
13
  import { join } from 'node:path';
8
14
 
9
15
  const CONTEXT_FILES = [
10
16
  { file: 'CLAUDE.md', kind: 'agent-rules', tool: 'Claude Code' },
11
17
  { file: 'AGENTS.md', kind: 'agent-rules', tool: 'generic (AGENTS.md standard)' },
18
+ { file: 'GEMINI.md', kind: 'agent-rules', tool: 'Gemini CLI' },
12
19
  { file: '.cursorrules', kind: 'agent-rules', tool: 'Cursor (legacy format)' },
13
20
  { file: '.cursor/rules', kind: 'agent-rules', tool: 'Cursor', dir: true },
14
21
  { file: '.github/copilot-instructions.md', kind: 'agent-rules', tool: 'GitHub Copilot' },
15
- { file: '.windsurfrules', kind: 'agent-rules', tool: 'Windsurf' },
22
+ { file: '.github/instructions', kind: 'agent-rules', tool: 'GitHub Copilot (scoped)', dir: true },
23
+ { file: '.windsurfrules', kind: 'agent-rules', tool: 'Windsurf (legacy format)' },
24
+ { file: '.windsurf/rules', kind: 'agent-rules', tool: 'Windsurf', dir: true },
16
25
  { file: 'components.json', kind: 'design-config', tool: 'shadcn/ui' },
17
26
  { file: 'tailwind.config.js', kind: 'design-config', tool: 'Tailwind' },
18
27
  { file: 'tailwind.config.ts', kind: 'design-config', tool: 'Tailwind' },
@@ -21,6 +30,63 @@ const CONTEXT_FILES = [
21
30
  { file: 'STYLEGUIDE.md', kind: 'design-doc' },
22
31
  ];
23
32
 
33
+ // Nested sweep targets: the doors that tools officially read from
34
+ // subdirectories (closer file wins, per the AGENTS.md standard and Cursor's
35
+ // docs). Only these — a nested tailwind.config is build detail, not a door.
36
+ const NESTED_FILES = {
37
+ 'CLAUDE.md': 'Claude Code',
38
+ 'AGENTS.md': 'generic (AGENTS.md standard)',
39
+ 'GEMINI.md': 'Gemini CLI',
40
+ };
41
+ const NESTED_DIRS = {
42
+ '.cursor': { child: 'rules', tool: 'Cursor' },
43
+ '.windsurf': { child: 'rules', tool: 'Windsurf' },
44
+ };
45
+
46
+ const SKIP_DIRS = new Set([
47
+ 'node_modules', '.next', '.git', 'dist', 'build', 'out', 'coverage',
48
+ '.turbo', '.cache', 'vendor', 'tmp',
49
+ ]);
50
+ const MAX_DEPTH = 6;
51
+ const MAX_NESTED = 60; // entries recorded; the true count keeps counting
52
+
53
+ function sweepNested(root) {
54
+ const found = [];
55
+ let total = 0;
56
+ const walk = (dir, rel, depth) => {
57
+ if (depth > MAX_DEPTH) return;
58
+ let entries;
59
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
60
+ for (const e of entries) {
61
+ const relPath = rel ? `${rel}/${e.name}` : e.name;
62
+ if (e.isDirectory()) {
63
+ if (SKIP_DIRS.has(e.name)) continue;
64
+ const nd = NESTED_DIRS[e.name];
65
+ if (nd) {
66
+ // rel is empty at the root, where the checklist above already owns
67
+ // these paths; the sweep only reports the nested ones.
68
+ if (rel && existsSync(join(dir, e.name, nd.child))) {
69
+ total++;
70
+ if (found.length < MAX_NESTED) found.push({ file: `${relPath}/${nd.child}`, kind: 'agent-rules', tool: nd.tool, nested: true, size: null, mentionsDesign: null });
71
+ }
72
+ continue; // never descend into tool dot-dirs
73
+ }
74
+ if (e.name.startsWith('.')) continue;
75
+ walk(join(dir, e.name), relPath, depth + 1);
76
+ } else if (depth > 0 && NESTED_FILES[e.name]) {
77
+ total++;
78
+ if (found.length < MAX_NESTED) {
79
+ let size = 0;
80
+ try { size = statSync(join(dir, e.name)).size; } catch { /* unreadable */ }
81
+ found.push({ file: relPath, kind: 'agent-rules', tool: NESTED_FILES[e.name], nested: true, size, mentionsDesign: null });
82
+ }
83
+ }
84
+ }
85
+ };
86
+ walk(root, '', 0);
87
+ return { found, total };
88
+ }
89
+
24
90
  export function harvestContext(root) {
25
91
  const found = [];
26
92
  for (const c of CONTEXT_FILES) {
@@ -38,5 +104,10 @@ export function harvestContext(root) {
38
104
  } catch { /* unreadable */ }
39
105
  found.push({ file: c.file, kind: c.kind, tool: c.tool ?? null, size, mentionsDesign });
40
106
  }
107
+ const nested = sweepNested(root);
108
+ found.push(...nested.found);
109
+ if (nested.total > nested.found.length) {
110
+ found.push({ file: `(+${nested.total - nested.found.length} more nested rules files)`, kind: 'agent-rules', tool: null, nested: true, size: null, mentionsDesign: null });
111
+ }
41
112
  return found;
42
113
  }
@@ -1,4 +1,4 @@
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 = '5.8.0';
4
+ export const VERSION = '5.9.0';
@@ -30,7 +30,9 @@ const repoName = h.profile?.name ?? 'this repo';
30
30
  if (!compact) {
31
31
  lines.push(`<!-- Generated by roast-my-design-system from a scan of ${repoName} on ${scanDate}.`);
32
32
  lines.push(' Paste into CLAUDE.md, .cursor/rules or AGENTS.md. Regenerate after big refactors:');
33
- lines.push(' npx roast-my-design-system@latest --rules -->');
33
+ lines.push(' npx roast-my-design-system@latest --rules');
34
+ lines.push(' Door note: Codex and Cursor read AGENTS.md; Claude Code reads CLAUDE.md only.');
35
+ lines.push(' If this lives in AGENTS.md, add the line "@AGENTS.md" to CLAUDE.md so Claude sees it too. -->');
34
36
  lines.push('');
35
37
  }
36
38
  // Mirror of the report's "no design system" banner: with almost no colour or