roast-my-design-system 4.3.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "4.3.1",
3
+ "version": "4.4.0",
4
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",
@@ -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.1';
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