roast-my-design-system 4.3.0 → 4.4.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
@@ -12,7 +12,7 @@ Run it on your codebase and get, in about a second:
12
12
  - **Per-package scores for monorepos.** One blended number hides which package is the problem: `packages/ui` scores 80 while `apps/web` scores 40, and now you can see it.
13
13
  - **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.
14
14
  - **The first fixes ranked by payoff.** A "Where to start" list derived from your own numbers: keep the report as the audit, or hand it to Claude as the punch list for the fix.
15
- - **Rules that stop the mess coming back.** A generated `design-system-rules.md` for your CLAUDE.md or .cursor/rules: canonical components, your token file, known duplicates to avoid, so your AI agent follows your system instead of guessing at it. `--apply` injects them for you, and every scan checks the rules you already have for stale references: paths that no longer exist, components named canonical that nothing imports anymore.
15
+ - **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.
16
16
 
17
17
  ## Why this exists
18
18
 
@@ -36,7 +36,7 @@ One scan powers all of it; the flags decide what lands on disk. Combine freely.
36
36
  | `... --theme light` / `--out <file>` / `--no-open` | Light report, custom report path, don't open the browser |
37
37
  | `/roast-my-design-system` (in Claude Code) | The full experience: the roast in chat, the report, the rules offer, and the fix loop with Claude on your own numbers |
38
38
 
39
- Every scan also checks the agent rules you already have and flags stale references, no flag needed.
39
+ **One scan writes rules for every agent: Claude, Cursor, GitHub Copilot, and Windsurf.** Every scan also checks the agent rules you already have and flags stale references, no flag needed.
40
40
 
41
41
  ## Example use cases
42
42
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "4.3.0",
4
- "description": "Your AI can write the UI. This makes sure it writes your UI. A deterministic scanner counts every colour, spacing value and duplicate component, scores you 0-100 against 34 public repos, scopes the scan with .roastignore, injects agent rules with --apply.",
3
+ "version": "4.4.0",
4
+ "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, scopes the scan with .roastignore, and writes rules for Claude, Cursor, GitHub Copilot and Windsurf with --apply.",
5
5
  "keywords": [
6
6
  "design-system",
7
7
  "design-tokens",
@@ -185,5 +185,69 @@ export function harvestComponents(root, codeFiles) {
185
185
  }
186
186
 
187
187
  components.sort((a, b) => b.usageCount - a.usageCount || a.name.localeCompare(b.name));
188
+
189
+ // Pass 3: golden examples — for the most-used components, quote the repo's
190
+ // own most common usage, verbatim, with a receipt. Descriptive, never
191
+ // invented: we photograph the plate that leaves the kitchen most often.
192
+ // A component with no dominant pattern gets no example (printing one would
193
+ // be a lie), and a component used a handful of times is not a pattern yet.
194
+ const uniqueNames = new Set([...byName].filter(([, d]) => d.length === 1).map(([n]) => n));
195
+ const candidates = components
196
+ .filter((c) => !c.isPage && c.usageCount >= 6 && uniqueNames.has(c.name))
197
+ .slice(0, 12);
198
+ for (const c of candidates) {
199
+ const found = []; // { tag, file }
200
+ for (const [file, src] of sources) {
201
+ if (file === c.file) continue;
202
+ let from = 0, open;
203
+ while ((open = src.indexOf(`<${c.name}`, from)) !== -1) {
204
+ from = open + c.name.length + 1;
205
+ const after = src[open + c.name.length + 1];
206
+ if (after && !/[\s/>]/.test(after)) continue;
207
+ const tag = sliceTag(src, open);
208
+ if (tag && tag.length <= 220 && !tag.includes('`')) found.push({ tag, file });
209
+ if (found.length > 400) break;
210
+ }
211
+ }
212
+ if (found.length < 5) continue;
213
+ // Group by prop-name signature; the winner must be a real majority habit.
214
+ const sigOf = (tag) => [...tag.matchAll(/[\s({]([A-Za-z_][\w-]*)=/g)].map((m) => m[1]).sort().join(',');
215
+ const groups = new Map();
216
+ for (const u of found) {
217
+ const s = sigOf(u.tag);
218
+ (groups.get(s) ?? groups.set(s, []).get(s)).push(u);
219
+ }
220
+ const [sig, top] = [...groups.entries()].sort((a, b) => b[1].length - a[1].length)[0];
221
+ if (top.length < 3 || top.length / found.length < 0.4) continue;
222
+ // The shortest real tag with the dominant signature is the cleanest quote.
223
+ const best = [...top].sort((a, b) => a.tag.length - b.tag.length)[0];
224
+ const selfClosed = /\/>\s*$/.test(best.tag);
225
+ c.usageExample = {
226
+ snippet: `${best.tag.replace(/\s+/g, ' ').trim()}${selfClosed ? '' : `…</${c.name}>`}`,
227
+ file: best.file,
228
+ matches: top.length,
229
+ total: found.length,
230
+ props: sig || null,
231
+ };
232
+ }
233
+
188
234
  return { components, totalDefined: components.length };
189
235
  }
236
+
237
+ /**
238
+ * Slice a JSX opening tag from `<` to its matching `>`, string- and
239
+ * brace-aware so `onClick={() => a > b}` cannot end the tag early.
240
+ */
241
+ function sliceTag(src, open) {
242
+ let depth = 0, str = null;
243
+ const limit = Math.min(src.length, open + 400);
244
+ for (let i = open; i < limit; i++) {
245
+ const c = src[i];
246
+ if (str) { if (c === str && src[i - 1] !== '\\') str = null; continue; }
247
+ if (QUOTES.has(c)) { str = c; continue; }
248
+ if (c === '{') depth++;
249
+ else if (c === '}') depth--;
250
+ else if (c === '>' && depth === 0) return src.slice(open, i + 1);
251
+ }
252
+ return null;
253
+ }
@@ -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 = '4.3.0';
4
+ export const VERSION = '4.4.0';
@@ -78,10 +78,18 @@ const repoName = h.profile?.name ?? 'this repo';
78
78
  .sort((a, b) => b.usageCount - a.usageCount).slice(0, CAP.components);
79
79
  if (top.length) {
80
80
  section('Canonical components');
81
- rule('Use these existing components instead of writing new ones:');
81
+ rule(compact
82
+ ? 'Use these existing components instead of writing new ones:'
83
+ : 'Use these existing components instead of writing new ones, the way this repo already uses them:');
82
84
  for (const c of top) {
83
85
  const props = !compact && c.propsHint?.named?.length ? ` · props: ${c.propsHint.named.slice(0, 4).join(', ')}` : '';
84
86
  lines.push(` - \`<${c.name}>\` from \`${c.file}\` (used ${c.usageCount}x${props})`);
87
+ // Golden example: the repo's own most common real usage, quoted with a
88
+ // receipt. Full variant only; compact hosts trade examples for size.
89
+ const ex = c.usageExample;
90
+ if (!compact && ex) {
91
+ lines.push(` - most common usage, as in \`${ex.file}\` (matching ${ex.matches} of ${ex.total} usages): \`${ex.snippet}\``);
92
+ }
85
93
  }
86
94
  }
87
95