roast-my-design-system 7.4.0 → 7.6.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
@@ -1,4 +1,4 @@
1
- <img src="https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/roaster_logo_300px.png?v=7.4.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.6.0" width="72" alt="roast-my-design-system">
2
2
 
3
3
  # roast-my-design-system
4
4
 
@@ -10,6 +10,10 @@
10
10
 
11
11
  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.
12
12
 
13
+ > **New in 7.6: a fourth repo profile, `registry`.** A repo that publishes a shadcn registry is read as a registry, not as an app that installed shadcn. Recognition: a `registry.json` with typed items (`registry:ui`, `registry:style`, ...) or a route that builds one from a packages folder. Covers shadcn's own source, magicui, kibo-ui and tweakcn. The header states what the repo publishes (components, blocks, styles, demos) and how many variants of the same components it keeps. Scoring is unchanged in this release: a registry is measured with the shadcn tiles against the shadcn slice, and the registry counting rules follow in the next one. `summary.json` gains `kind: "registry"` and a `registry` block. Also in 7.6: the theme file is checked for the variables tweakcn adds to every export, shown as a `tweakcn theme` pill on a match; typefaces declared in a single file are reported as a picker rather than as N typefaces.
14
+
15
+ > **New in 7.5: the kit is read as it is built, and every file left out of a count is named.** A fresh `npx shadcn create` install is told apart from a repo built on one: "The score is the kit's, not yours." A rules file a framework wrote for itself is named as such, and so is the shadcn skill when it is installed. The theme file is found where it is, even when `components.json` points at a file that moved; `button/index.tsx` is a Button. The repaint advice checks whether the theme file holds the variables before telling you to swap to them. And the side panel now lists everything a check leaves out, with the reason and one honest line: your agent reads it anyway. New on that list: the Next.js crash page, an embedded widget's `!important`, and `!important` aimed at a library's own class names.
16
+
13
17
  > **New in 7.4: the score says what it measures, and the report has a new shape.** Under the number: "How safely an AI agent can build on this repo without going off-system." A fixed side panel holds the score, the stack, how the repo was read, an index of every section, and what is not yours and not counted. Installed code splits by one test, does it teach an agent a wrong lesson: shadcn's own bracket values are out of the count and named; palette colours inside an installed registry stay in the score and are never prompted; catalogue stock is not a trap. A factory install scores 100. Leftover theme variables become a fix.
14
18
 
15
19
  > **New in 7.3: shadcn repos are compared with shadcn repos.** The benchmark carries a shadcn slice, 17 of the 34 fleet repos, and on a shadcn repo every fleet line reads against it: "Avg shadcn repo", "cleaner than 60% of shadcn repos". Ideals and the reputable-systems line stay the same for everyone.
@@ -79,11 +83,11 @@ Not to be confused with each other: **"Why this matters"** is generic, ships wit
79
83
 
80
84
  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:
81
85
 
82
- ![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 the summary, 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.4.0)
86
+ ![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 the summary, 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.6.0)
83
87
 
84
88
  The same report in light mode (one file, built-in toggle):
85
89
 
86
- ![The diagnosis report in light mode](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-light-hero.png?v=7.4.0)
90
+ ![The diagnosis report in light mode](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-light-hero.png?v=7.6.0)
87
91
 
88
92
  ## What makes the numbers trustworthy
89
93
 
@@ -233,12 +237,13 @@ After the roast, the skill also offers to write `design-system-rules.md` to disk
233
237
 
234
238
  ## Live examples
235
239
 
236
- 5 real roasts of public repos, hosted as-is (the same self-contained HTML the skill generates), spanning React, Stencil and Lit:
240
+ 7 real roasts of public repos, hosted as-is (the same self-contained HTML the skill generates), spanning React, Stencil and Lit:
237
241
 
238
242
  - **[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
239
243
  - **[adobe/spectrum-web-components](https://gregkozakiewicz.github.io/roast-my-design-system/examples/adobe-spectrum.html)**: Lit, the `--spectrum-*` namespace named in the header
240
- - **[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
241
- - **[vercel/ai-chatbot](https://gregkozakiewicz.github.io/roast-my-design-system/examples/vercel-ai-chatbot.html)**: React, with Claude's notes embedded
244
+ - **[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 and read as fresh
245
+ - **[vercel/ai-chatbot](https://gregkozakiewicz.github.io/roast-my-design-system/examples/vercel-ai-chatbot.html)**: React on shadcn, with Claude's notes embedded
246
+ - **[magicuidesign/magicui](https://gregkozakiewicz.github.io/roast-my-design-system/examples/magicui.html)**: read as a shadcn registry, what it publishes named under the score
242
247
  - **[excalidraw/excalidraw](https://gregkozakiewicz.github.io/roast-my-design-system/examples/excalidraw-excalidraw.html)**
243
248
  - **[dubinc/dub](https://gregkozakiewicz.github.io/roast-my-design-system/examples/dubinc-dub.html)**
244
249
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "7.4.0",
3
+ "version": "7.6.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": [
@@ -1852,7 +1852,7 @@
1852
1852
  },
1853
1853
  "slices": {
1854
1854
  "shadcn": {
1855
- "builtAt": "2026-09-12T20:07:59.374Z",
1855
+ "builtAt": "2026-09-12T23:43:34.615Z",
1856
1856
  "repoCount": 16,
1857
1857
  "note": "every repo in the fleet the scanner reads as shadcn; the \"cleaner than\" line on a shadcn repo reads against these",
1858
1858
  "stats": {
@@ -2055,7 +2055,7 @@
2055
2055
  5,
2056
2056
  7,
2057
2057
  11,
2058
- 22,
2058
+ 21,
2059
2059
  24,
2060
2060
  42,
2061
2061
  47,
@@ -2129,20 +2129,20 @@
2129
2129
  0,
2130
2130
  0,
2131
2131
  1,
2132
+ 1,
2132
2133
  2,
2134
+ 4,
2133
2135
  9,
2134
- 9,
2135
- 12,
2136
- 42,
2137
- 42,
2138
- 55,
2139
- 93,
2140
- 157
2136
+ 11,
2137
+ 14,
2138
+ 34,
2139
+ 65,
2140
+ 126
2141
2141
  ],
2142
2142
  "p25": 0,
2143
- "median": 6,
2144
- "p75": 42,
2145
- "p90": 74
2143
+ "median": 2,
2144
+ "p75": 12,
2145
+ "p90": 50
2146
2146
  },
2147
2147
  "neverImported": {
2148
2148
  "values": [
@@ -2151,7 +2151,7 @@
2151
2151
  0,
2152
2152
  0,
2153
2153
  1,
2154
- 2,
2154
+ 3,
2155
2155
  5,
2156
2156
  13,
2157
2157
  19,
@@ -2208,7 +2208,7 @@
2208
2208
  66,
2209
2209
  100,
2210
2210
  179,
2211
- 296,
2211
+ 297,
2212
2212
  488
2213
2213
  ],
2214
2214
  "p25": 22,
@@ -2249,7 +2249,7 @@
2249
2249
  "baseColor": "zinc",
2250
2250
  "tailwind": null,
2251
2251
  "codeFiles": 4391,
2252
- "ownFiles": 811,
2252
+ "ownFiles": 810,
2253
2253
  "metrics": {
2254
2254
  "colors": 310,
2255
2255
  "greys": 66,
@@ -2262,7 +2262,7 @@
2262
2262
  "inlineStyles": 79,
2263
2263
  "arbitrary": 217,
2264
2264
  "nearPairs": 18,
2265
- "important": 157,
2265
+ "important": 126,
2266
2266
  "neverImported": 99,
2267
2267
  "components": 1453,
2268
2268
  "paintTin": 22,
@@ -2316,7 +2316,7 @@
2316
2316
  "inlineStyles": 2,
2317
2317
  "arbitrary": 5,
2318
2318
  "nearPairs": 0,
2319
- "important": 2,
2319
+ "important": 1,
2320
2320
  "neverImported": 0,
2321
2321
  "components": 60,
2322
2322
  "paintTin": 57,
@@ -2343,7 +2343,7 @@
2343
2343
  "inlineStyles": 7,
2344
2344
  "arbitrary": 74,
2345
2345
  "nearPairs": 0,
2346
- "important": 9,
2346
+ "important": 2,
2347
2347
  "neverImported": 0,
2348
2348
  "components": 316,
2349
2349
  "paintTin": 66,
@@ -2411,7 +2411,7 @@
2411
2411
  "baseColor": "slate",
2412
2412
  "tailwind": null,
2413
2413
  "codeFiles": 2604,
2414
- "ownFiles": 788,
2414
+ "ownFiles": 787,
2415
2415
  "metrics": {
2416
2416
  "colors": 132,
2417
2417
  "greys": 16,
@@ -2424,10 +2424,10 @@
2424
2424
  "inlineStyles": 24,
2425
2425
  "arbitrary": 113,
2426
2426
  "nearPairs": 8,
2427
- "important": 55,
2428
- "neverImported": 2,
2427
+ "important": 34,
2428
+ "neverImported": 3,
2429
2429
  "components": 1085,
2430
- "paintTin": 296,
2430
+ "paintTin": 297,
2431
2431
  "doorOverrides": 26
2432
2432
  }
2433
2433
  },
@@ -2438,7 +2438,7 @@
2438
2438
  "baseColor": "neutral",
2439
2439
  "tailwind": null,
2440
2440
  "codeFiles": 2141,
2441
- "ownFiles": 775,
2441
+ "ownFiles": 772,
2442
2442
  "metrics": {
2443
2443
  "colors": 174,
2444
2444
  "greys": 38,
@@ -2465,7 +2465,7 @@
2465
2465
  "baseColor": "gray",
2466
2466
  "tailwind": null,
2467
2467
  "codeFiles": 875,
2468
- "ownFiles": 410,
2468
+ "ownFiles": 409,
2469
2469
  "metrics": {
2470
2470
  "colors": 17,
2471
2471
  "greys": 3,
@@ -2475,7 +2475,7 @@
2475
2475
  "radii": 14,
2476
2476
  "shadows": 0,
2477
2477
  "exactDuplicates": 17,
2478
- "inlineStyles": 22,
2478
+ "inlineStyles": 21,
2479
2479
  "arbitrary": 25,
2480
2480
  "nearPairs": 1,
2481
2481
  "important": 0,
@@ -2505,7 +2505,7 @@
2505
2505
  "inlineStyles": 47,
2506
2506
  "arbitrary": 360,
2507
2507
  "nearPairs": 7,
2508
- "important": 42,
2508
+ "important": 14,
2509
2509
  "neverImported": 13,
2510
2510
  "components": 777,
2511
2511
  "paintTin": 488,
@@ -2519,7 +2519,7 @@
2519
2519
  "baseColor": "slate",
2520
2520
  "tailwind": "3",
2521
2521
  "codeFiles": 2120,
2522
- "ownFiles": 350,
2522
+ "ownFiles": 349,
2523
2523
  "metrics": {
2524
2524
  "colors": 254,
2525
2525
  "greys": 52,
@@ -2586,7 +2586,7 @@
2586
2586
  "inlineStyles": 47,
2587
2587
  "arbitrary": 366,
2588
2588
  "nearPairs": 10,
2589
- "important": 12,
2589
+ "important": 4,
2590
2590
  "neverImported": 0,
2591
2591
  "components": 1204,
2592
2592
  "paintTin": 21,
@@ -2613,7 +2613,7 @@
2613
2613
  "inlineStyles": 58,
2614
2614
  "arbitrary": 569,
2615
2615
  "nearPairs": 16,
2616
- "important": 42,
2616
+ "important": 11,
2617
2617
  "neverImported": 30,
2618
2618
  "components": 1494,
2619
2619
  "paintTin": 58,
@@ -2627,7 +2627,7 @@
2627
2627
  "baseColor": "neutral",
2628
2628
  "tailwind": "4",
2629
2629
  "codeFiles": 1775,
2630
- "ownFiles": 605,
2630
+ "ownFiles": 604,
2631
2631
  "metrics": {
2632
2632
  "colors": 187,
2633
2633
  "greys": 51,
@@ -2667,7 +2667,7 @@
2667
2667
  "inlineStyles": 263,
2668
2668
  "arbitrary": 1602,
2669
2669
  "nearPairs": 59,
2670
- "important": 93,
2670
+ "important": 65,
2671
2671
  "neverImported": 23,
2672
2672
  "components": 3630,
2673
2673
  "paintTin": 8,
@@ -32,7 +32,8 @@ import { fixPrompt } from '../lib/fixprompt.mjs';
32
32
  import { WHY } from './why.mjs';
33
33
  import { parseColor, luminance, isGrey } from '../lib/color.mjs';
34
34
  import { loadBenchmark, benchHelpers, makeHealthOf, coreMetrics, tileHealths, scoreOfTiles, scoreBreakdown, scorePackage as scorePackageOf, ZERO_IDEAL, WARN_TOLERANCE, SCORE_OF, SCHEMA_VERSION } from './score.mjs';
35
- import { profileOf, installedDirs, splitArbitrary } from '../profiles/index.mjs';
35
+ import { ownSpacing, profileOf, installedDirs, splitArbitrary } from '../profiles/index.mjs';
36
+ import { publishesLine } from '../profiles/registry.mjs';
36
37
 
37
38
  // The benchmark and every judgement made against it live in score.mjs; this
38
39
  // file only draws. The same numbers reach summary.json through scoreHarvest.
@@ -207,9 +208,12 @@ const greys = greysSorted(bgLumOf(THEMES.dark.pageBgHex));
207
208
  const greysLight = greysSorted(bgLumOf(THEMES.light.pageBgHex));
208
209
  const nonGreys = colors.filter((c) => !isGrey(c.value));
209
210
  const twColorUtils = (h.tokens.tailwind?.colors ?? []).length;
210
- const spacing = h.tokens.spacing ?? [];
211
- const twSpacing = h.tokens.tailwind?.spacing ?? [];
212
- // Off-scale spacing only (formula in score.mjs, shared with the packages pass).
211
+ // On a kit the off-scale values inside installed folders are shadcn's own:
212
+ // kept out of the tile and the bars (formula in score.mjs), named at the top.
213
+ const P_isShadcn = (x) => profileOf(x).isShadcn;
214
+ const spacingOwn = ownSpacing(h);
215
+ const spacing = spacingOwn.css;
216
+ const twSpacing = P_isShadcn(h) ? [...(h.tokens.tailwind?.spacing ?? []).filter((v) => !v.value.startsWith('[')), ...spacingOwn.tw] : (h.tokens.tailwind?.spacing ?? []);
213
217
  const spacingTotal = M.spacing;
214
218
  const exactDupes = h.duplicates.exactDuplicates ?? [];
215
219
  // A wrapped pair (one file imports the name from the other) is composition,
@@ -248,6 +252,7 @@ const nearPairs = nearColorPairs(colors);
248
252
  const collisions = h.tokens.tokenCollisions ?? [];
249
253
 
250
254
  const P = profileOf(h);
255
+ const fresh = P.isShadcn && P.shadcn?.fresh?.fresh === true;
251
256
  // On a shadcn repo the bracket list is the team's own; installed brackets are
252
257
  // named in the side panel instead (see coreMetrics for the count).
253
258
  if (P.isShadcn) arbitrary = splitArbitrary(arbitrary, installedDirs(P)).own;
@@ -281,7 +286,12 @@ const offenders = h.tokens.offenders ?? [];
281
286
  const candidates = [];
282
287
  const effColors = tokenLed ? colorStrays : colors.length;
283
288
  const effGreys = tokenLed ? greyStrays : greys.length;
284
- if (typefaces.length > 3) candidates.push({ ratio: typefaces.length / 3, text: `${typefaces.length} typefaces. Most products use 2 or 3` });
289
+ // Every typeface declared in one file is a picker (a theme editor's font
290
+ // list, shadcn's docs site): one choice offered, one in use at a time.
291
+ const fontFiles = new Set(fontFamilies.flatMap((f) => (f.files ?? []).map((x) => x.file)));
292
+ const fontPicker = typefaces.length > 3 && fontFiles.size === 1 ? [...fontFiles][0] : null;
293
+ if (fontPicker) candidates.push({ ratio: 1.05, text: `${typefaces.length} typefaces offered by a picker in one file (${basename(fontPicker)}), one in use at a time` });
294
+ else if (typefaces.length > 3) candidates.push({ ratio: typefaces.length / 3, text: `${typefaces.length} typefaces. Most products use 2 or 3` });
285
295
  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` });
286
296
  if (effColors > 24 * 1.25) candidates.push({ ratio: effColors / 24, text: tokenLed
287
297
  ? `${n(colorStrays)} hardcoded colours outside the token set, your agent will happily copy them at random`
@@ -297,15 +307,18 @@ if (componentsMeasured && hardDupes.length > 0) candidates.push({ ratio: 1 + har
297
307
  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` });
298
308
  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` });
299
309
  if (agentFiles.length === 0) candidates.push({ ratio: 1.3, text: `No agent rules file, so your AI agent has nothing to follow` });
300
- 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` });
310
+ const staleOwn = (h.staleRules ?? []).filter((s) => !s.tool);
311
+ if (staleOwn.length >= 2) candidates.push({ ratio: 1.2 + staleOwn.length / 10, text: `${staleOwn.length} lines in your agent rules refer to files that no longer exist` });
301
312
  const findings = candidates.sort((a, b) => b.ratio - a.ratio).map((c) => c.text);
302
313
 
303
- let verdict = findings.length === 0
314
+ let verdict = fresh
315
+ ? 'A fresh shadcn install with nothing built on it yet. Everything below describes the kit you are starting from.'
316
+ : findings.length === 0
304
317
  ? (agentFiles.length > 0
305
318
  ? 'This repo is in good shape, and your agent has rules to read. Keep them in step with the code.'
306
319
  : 'This repo is in good shape. The gap is documentation: your agent still can\'t see the system.')
307
320
  : findings.slice(0, 3).join('. ') + '.';
308
- if (bench && findings.length) {
321
+ if (bench && findings.length && !fresh) {
309
322
  const core = [['colors', tokenLed ? colorStrays : colors.length], ['greys', tokenLed ? greyStrays : greys.length],
310
323
  ['spacing', spacingTotal], ['typefaces', typefaces.length], ['exactDuplicates', hardDupes.length], ['inlineStyles', inline.count]];
311
324
  const worse = core.filter(([m, v]) => median(m) !== null && v > median(m)).length;
@@ -595,12 +608,12 @@ function sheetSection() {
595
608
  const sheet = sc.sheet, paint = sc.paint;
596
609
  const parts = [];
597
610
  if (sheet?.found) {
598
- const bits = [`${n(sheet.lightRows)} variables for light, ${n(sheet.darkRows)} for dark in ${esc(sheet.file)}`];
611
+ const bits = [`${n(sheet.lightRows)} variables for light, ${n(sheet.darkRows)} for dark in ${esc(sheet.file)}${sheet.scope ? ` under ${esc(sheet.scope)}` : ''}${sheet.configured && sheet.configured !== sheet.file ? ` (components.json names ${esc(sheet.configured)}, ${sheet.configuredFound ? 'which holds no theme rows' : 'which does not exist'})` : ''}`];
599
612
  if (sheet.hslEra) bits.push('written in the Tailwind 3 form (hsl triplets)');
600
613
  if (sheet.shadcnMissing?.length) bits.push(`${sheet.shadcnMissing.length} of the ${SHADCN_ROW_COUNT} current shadcn variables not defined (${sheet.shadcnMissing.slice(0, 4).map((r) => `--${esc(r)}`).join(', ')}${sheet.shadcnMissing.length > 4 ? '…' : ''})${sheet.hslEra ? ', normal for an install from before the chart and sidebar rows existed' : ''}`);
601
614
  if (sheet.missingDark?.length) bits.push(`${sheet.missingDark.length} variable${sheet.missingDark.length === 1 ? '' : 's'} with no dark value (${sheet.missingDark.slice(0, 4).map((r) => `--${esc(r)}`).join(', ')})`);
602
615
  if (sheet.custom?.length) bits.push(`${sheet.custom.length} custom variable${sheet.custom.length === 1 ? '' : 's'} of your own (${sheet.custom.slice(0, 5).map((r) => `--${esc(r)}`).join(', ')}${sheet.custom.length > 5 ? '…' : ''})${sheet.customMissingDark?.length ? `, ${sheet.customMissingDark.length} of them light only` : ''}${sheet.customUnregistered?.length ? `, ${sheet.customUnregistered.length} never mapped in @theme inline` : ''}`);
603
- if (sheet.tweakcnPresent >= 10) bits.push('tweakcn variables present: shadows and letter-spacing are themed');
616
+ if (sheet.tweakcnPresent >= 10) bits.push(`a tweakcn theme: ${sheet.tweakcnPresent} of the rows tweakcn adds to every theme it exports are here (shadows, letter-spacing, spacing), so the theme came from a theme editor or copied its shape, and updates will come from there too`);
604
617
  if (sheet.spacingChanged) bits.push(`<b>--spacing is ${esc(sheet.spacing)}</b>, not the default 0.25rem: this resizes every gap in the app at once, which shadcn\'s own changelog says never to do`);
605
618
  parts.push(`<div class="receipts">${eyebrow('the theme file, variable by variable')}<p class="sub">${bits.join(' · ')}.</p></div>`);
606
619
  } else if (sheet) {
@@ -635,7 +648,7 @@ function duplicatesSection() {
635
648
  </div>` : '';
636
649
  const dupeCards = exactDupes.slice(0, 8).map((d) => `
637
650
  <div class="fam">
638
- ${eyebrow(`&lt;${esc(d.name)}&gt; · ${d.wrapped ? 'defined twice, one wraps the other' : `${d.files.length} implementations`}`)}
651
+ ${eyebrow(`&lt;${esc(d.name)}&gt; · ${d.upstream ? 'two shadcn components define it: upstream\'s overlap, not counted' : d.wrapped ? 'defined twice, one wraps the other' : `${d.files.length} implementations`}`)}
639
652
  <div class="fam-rows">
640
653
  ${d.files.slice(0, 6).map((f) => `<div class="mini-card">${fileLink(f)}</div>`).join('')}
641
654
  ${d.files.length > 6 ? `<div class="mini-card dim">…and ${d.files.length - 6} more</div>` : ''}
@@ -742,7 +755,7 @@ function typographySection() {
742
755
  ${typeSpecimens || receipts('the font sizes, by use', sizeChips)}
743
756
  ${radiiSpecimens || receipts('the radii, by use', radiiChips)}
744
757
  ${shadowSpecimens}
745
- ${fontFamilies.length > 1 ? `<div class="glass pad" style="margin-top:16px">${sectionHead(`${typefaces.length} typeface${typefaces.length === 1 ? '' : 's'}, declared ${fontFamilies.length} different ways`, 'every distinct declaration is a chance for the next one to be wrong')}<div class="fam-rows">${fams}</div></div>` : ''}
758
+ ${fontFamilies.length > 1 && fontFamilies.length > typefaces.length ? `<div class="glass pad" style="margin-top:16px">${sectionHead(`${typefaces.length} typeface${typefaces.length === 1 ? '' : 's'}, declared ${fontFamilies.length} different ways`, 'every distinct declaration is a chance for the next one to be wrong')}<div class="fam-rows">${fams}</div></div>` : ''}
746
759
  </section>`;
747
760
  }
748
761
 
@@ -882,7 +895,8 @@ function componentsSection() {
882
895
  ${sectionHead(isLibrary ? 'The composition map' : 'The adoption map', `${n(reusable.length)} components defined · tile area is ${isLibrary ? 'internal use: how the system builds from itself, downstream consumers invisible from here' : 'import count'} · the real system, drawn to scale`)}
883
896
  ${mapped}
884
897
  ${overflow > 0 ? `<p class="sub">${overflow} more adopted component${overflow === 1 ? '' : 's'} below the top 24, not drawn.</p>` : ''}
885
- ${onceUsed.length ? `<p class="sub">${n(onceUsed.length)} component${onceUsed.length === 1 ? ' is' : 's are'} imported exactly once: ${onceUsed.slice(0, 6).map((c) => `&lt;${esc(c.name)}&gt;`).join(', ')}${onceUsed.length > 6 ? ` and ${onceUsed.length - 6} more` : ''}. Quiet corners, not yet a system.</p>` : ''}
898
+ ${fresh ? '<p class="sub">No code of your own uses the kit yet. The counts below are the components using each other.</p>' : ''}
899
+ ${onceUsed.length && !fresh ? `<p class="sub">${n(onceUsed.length)} component${onceUsed.length === 1 ? ' is' : 's are'} imported exactly once: ${onceUsed.slice(0, 6).map((c) => `&lt;${esc(c.name)}&gt;`).join(', ')}${onceUsed.length > 6 ? ` and ${onceUsed.length - 6} more` : ''}. Quiet corners, not yet a system.</p>` : ''}
886
900
  ${top.length ? `<div class="tbl-wrap"><table><thead><tr><th>component</th><th>used</th><th>defined in</th><th>props</th></tr></thead><tbody>${rows}</tbody></table></div>` : ''}
887
901
  ${neverImported.length >= 2 ? `
888
902
  <div class="receipts">${eyebrow(isLibrary ? `${n(neverImported.length)} components unused internally · showroom stock to review, not dead weight: consumers in other repos are invisible from here` : vendoredUi ? `${n(neverImported.length)} catalogue components not used yet · installed by the shadcn CLI and waiting to be used, not written by this team` : `${n(neverImported.length)} components defined but never imported · they sit in the system as wrong answers waiting to be picked`)}
@@ -1081,9 +1095,18 @@ function whereToStartSection() {
1081
1095
  const pt = P.shadcn.paint;
1082
1096
  if (pt.tin.uses >= 10) {
1083
1097
  const s0 = pt.tin.samples[0], f0 = pt.tin.top[0];
1084
- c.push({ score: 20 + pt.tin.per100 / 4, metric: 'paintTin', after: 0,
1085
- title: `Repaint the ${n(pt.tin.uses)} colours from outside the theme`,
1086
- sub: `${esc(s0.value)} appears ${s0.count} times${f0 ? `, ${esc(basename(f0.file))} alone carries ${f0.count}` : ''}. The theme file already has a variable for each (a grey is <code>text-muted-foreground</code>, a status colour is a Badge variant or a variable you add). Swap the class, not the value.` });
1098
+ // The swap only works when the theme file holds the variables. A
1099
+ // product whose sheet defines none of them (formbricks: brand rows
1100
+ // only, slate as the palette) needs a decision first, not a prompt:
1101
+ // swapping classes there leaves text with no colour.
1102
+ const sheetHasRows = (P.shadcn?.sheet?.shadcnPresent ?? 0) >= 5 || P.shadcn?.kit?.cssVariables === false;
1103
+ c.push(sheetHasRows
1104
+ ? { score: 20 + pt.tin.per100 / 4, metric: 'paintTin', after: 0,
1105
+ title: `Repaint the ${n(pt.tin.uses)} colours from outside the theme`,
1106
+ sub: `${esc(s0.value)} appears ${s0.count} times${f0 ? `, ${esc(basename(f0.file))} alone carries ${f0.count}` : ''}. The theme file already has a variable for each (a grey is <code>text-muted-foreground</code>, a status colour is a Badge variant or a variable you add). Swap the class, not the value.` }
1107
+ : { score: 20 + pt.tin.per100 / 4, metric: 'paintTin', after: 0,
1108
+ title: `Decide what the product paints from`,
1109
+ sub: `${esc(s0.value)} appears ${s0.count} times${f0 ? `, ${esc(basename(f0.file))} alone carries ${f0.count}` : ''}, and there is no theme file to point them at: ${P.shadcn?.sheet?.file ? `${esc(P.shadcn.sheet.file)} defines` : 'the theme file defines'} none of shadcn's colour variables, so the palette is the system today. The first move is a decision, not a swap: adopt the theme variables (define them, map the palette onto them), then repaint file by file. Swapping <code>text-slate-500</code> for <code>text-muted-foreground</code> before that leaves the text with no colour.` });
1087
1110
  }
1088
1111
  if (pt.doors.uses >= 5) {
1089
1112
  const s0 = pt.doors.samples[0];
@@ -1130,6 +1153,7 @@ function whereToStartSection() {
1130
1153
  // reads at click time, so a forwarded report keeps working offline.
1131
1154
  const unesc = (s) => s.replace(/<[^>]+>/g, '').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&rarr;/g, '->').replace(/&amp;/g, '&');
1132
1155
  const promptFor = (item) => fixPrompt({
1156
+ metric: item.metric,
1133
1157
  title: unesc(item.title),
1134
1158
  sub: unesc(item.sub),
1135
1159
  deltaText: item.delta > 0 ? `about +${item.delta} points` : (item.target ? `about +${item.target.gain} points once ${item.target.target === 0 ? 'they are all cleared' : `the count is under ${n(item.target.target)}`}` : ''),
@@ -1205,7 +1229,20 @@ function agentSection() {
1205
1229
  const have = rootAgent.map((c) => `<span class="chip chip-agent">${esc(c.file)} · ${esc(c.tool ?? c.kind)}</span>`).join('')
1206
1230
  + (nestedCount ? `<span class="chip chip-agent">+${nestedCount} nested in subfolders</span>` : '');
1207
1231
  const readsList = (rootAgent.length ? rootAgent : nestedAgent.slice(0, 3)).map((c) => `<span class="mono">${esc(c.file)}</span>`).join(', ');
1208
- const msg = agentFiles.length
1232
+ // A file a framework wrote for itself (Next.js re-adds its block on every
1233
+ // dev run) is not guidance about the design system, and says so.
1234
+ const managedOnly = agentFiles.length > 0 && agentFiles.every((c) => c.managedOnly === true);
1235
+ const managedBy = managedOnly ? agentFiles[0].managedBy : null;
1236
+ const skill = (h.context ?? []).find((c) => c.kind === 'agent-skill');
1237
+ const skillLine = P.isShadcn ? (skill ? ` The shadcn skill is installed (<span class="mono">${esc(skill.file)}</span>), so the agent can read the kit's configuration.` : ' shadcn offers a skill for that and it is not installed here.') : '';
1238
+ // the files name the kit (or the skill is installed): the gap is the
1239
+ // repo's own habits, which no hand-written file carries
1240
+ const namesKit = P.isShadcn && (skill || agentFiles.some((c) => c.mentionsDesign === true && c.managedOnly !== true));
1241
+ const msg = managedOnly
1242
+ ? `<p class="sub">Your agent reads ${readsList}. ${esc(managedBy)} wrote it, and it is only about ${esc(managedBy)}: nothing in it mentions the theme file or the component folder, so the agent does not know the ${P.isShadcn ? 'kit' : 'design system'} is there.${skillLine} Add the rules file below and the agent knows the theme, the components and the styling rules.</p>`
1243
+ : namesKit
1244
+ ? `<p class="sub">Your agent reads ${readsList}${nestedCount && rootAgent.length ? `, plus ${nestedCount} rules file${nestedCount === 1 ? '' : 's'} nested in subfolders` : ''}${skill ? `, and the shadcn skill is installed (<span class="mono">${esc(skill.file)}</span>)` : ''}. ${skill && !agentFiles.some((c) => c.mentionsDesign) ? 'The skill tells it' : 'They tell it'} the kit is there. What ${skill && !agentFiles.some((c) => c.mentionsDesign) ? 'it does' : 'they do'} not carry is how this repo actually uses it: the numbers below, which the rules file turns into rules with receipts.</p>`
1245
+ : agentFiles.length
1209
1246
  ? `<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>`
1210
1247
  : `<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>`;
1211
1248
  // Door coverage, stated as fact: which tools can read what exists. Claude
@@ -1222,13 +1259,13 @@ function agentSection() {
1222
1259
  const staleRows = stale.map((s) => `
1223
1260
  <div class="ledger-row">
1224
1261
  <span class="mono strong">${esc(s.ref)}</span>
1225
- <span class="dim">${s.problem === 'missing' ? 'named in' : 'named canonical in'} ${esc(s.file)}, ${s.problem === 'missing' ? 'but the path no longer exists' : 'but nothing imports it this scan'}</span>
1262
+ <span class="dim">${s.problem === 'missing' ? 'named in' : 'named canonical in'} ${esc(s.file)}${s.tool ? ` (written by ${esc(s.tool)}, not by this team)` : ''}, ${s.problem === 'missing' ? 'but the path no longer exists' : 'but nothing imports it this scan'}</span>
1226
1263
  </div>`).join('');
1227
1264
  const staleBlock = stale.length ? `
1228
1265
  <div class="stale">
1229
1266
  ${eyebrow(`${stale.length} rule reference${stale.length === 1 ? '' : 's'} gone stale · a rule your agent obeys is worse than no rule when the repo has moved on`)}
1230
1267
  <div class="ledger">${staleRows}</div>
1231
- <p class="sub" style="margin-top:10px">Rules rot quietly: nobody edits them when a component is renamed or a file moves. Regenerate them from a fresh scan, or fix the lines by hand.</p>
1268
+ <p class="sub" style="margin-top:10px">${stale.every((s) => s.tool) ? 'These lines were written by a tool, not by this team: a rule pack that speaks about files this app does not have. Not yours to fix; worth knowing your agent reads them.' : 'Rules rot quietly: nobody edits them when a component is renamed or a file moves. Regenerate them from a fresh scan, or fix the lines by hand.'}</p>
1232
1269
  </div>` : '';
1233
1270
  return `<section class="glass pad agent">
1234
1271
  ${sectionHead('What your AI agent sees today', '')}
@@ -1279,7 +1316,20 @@ function shadcnReceipt() {
1279
1316
  ].filter(Boolean);
1280
1317
  const unmatched = (k?.fellBack ?? []).filter((f) => f === 'theme' || f === 'chartColor');
1281
1318
  const fell = unmatched.length ? ` · ${unmatched.length === 2 ? 'accent and chart colour' : unmatched[0] === 'theme' ? 'accent' : 'chart colour'} not matched to a named shadcn theme` : '';
1282
- return `<div class="excl">Read as a shadcn install (${esc(P.confidence ?? 'medium')} confidence): ${esc(P.evidence.join(' · '))}${facts.length ? ` · ${esc(facts.join(', '))}` : ''}${esc(fell)}</div>`;
1319
+ const receipt = `${esc(P.evidence.join(' · '))}${facts.length ? ` · ${esc(facts.join(', '))}` : ''}${esc(fell)}`;
1320
+ if (fresh) {
1321
+ const f = P.shadcn.fresh;
1322
+ const doors = f.catalogueCount;
1323
+ const pages = comps.filter((c) => c.isPage && f.ownFiles.includes(c.file)).length;
1324
+ return `<div class="excl fresh">It is a fresh shadcn install${k?.style ? ` (style ${esc(k.style)}${k.baseColor ? `, base colour ${esc(k.baseColor)}` : ''}${k.tailwind ? `, Tailwind ${esc(k.tailwind)}` : ''})` : ''}. Nothing of your own yet: ${n(doors)} component${doors === 1 ? '' : 's'} installed, the theme file untouched, ${pages === 1 ? 'one demo page' : `${pages} pages`}. <b>The score is the kit's, not yours.</b> Run this again once you have built a few screens.</div><div class="excl">Evidence: ${receipt}</div>`;
1325
+ }
1326
+ if (P.isRegistry && P.registry) {
1327
+ const r = P.registry;
1328
+ const what = r.builtFrom ? `publishes ${r.items} component${r.items === 1 ? '' : 's'}, built from its packages folder` : `publishes ${esc(publishesLine(r))}`;
1329
+ const pages = comps.filter((c) => c.isPage).length;
1330
+ return `<div class="excl fresh">Read as a shadcn registry: ${what}${r.variants.length ? `, the same components kept in ${r.variants.length} variants` : ''}.${pages ? ` Its own site: ${n(pages)} page${pages === 1 ? '' : 's'}.` : ''} Scored as a shadcn install for now; the registry rules come next.</div><div class="excl">Evidence: ${receipt}</div>`;
1331
+ }
1332
+ return `<div class="excl">Read as a shadcn install (${esc(P.confidence ?? 'medium')} confidence): ${receipt}</div>`;
1283
1333
  }
1284
1334
 
1285
1335
  // User exclusions are printed in the header, never hidden: a scoped scan must
@@ -1312,7 +1362,7 @@ function sidePanel() {
1312
1362
  ? `<div class="bd">Of which <b>${breakdown.installedPoints} point${breakdown.installedPoints === 1 ? '' : 's'}</b> come from installed code you did not write: ${esc(rp.dirs.map((d) => basename(d)).join(', '))} (${n(rp.files)} file${rp.files === 1 ? '' : 's'}, ${n(rp.tinUses)} palette colour${rp.tinUses === 1 ? '' : 's'}). Your own code alone would score <b>${breakdown.ownScore}</b>. Kept in the score because your agent reads those files like everything else; left out of the fixes because they are not yours to edit.</div>` : '';
1313
1363
  const scoreBlock = healthScore !== null
1314
1364
  ? `<div class="score${noSystemLikely ? ' muted' : ''}">${eyebrow('Health score')}<div class="val">${healthScore}<span class="slash">/</span><span class="of">100</span></div>${noSystemLikely ? '<div class="note">little here to score · see the note</div>' : ''}<div class="def">${def}${lift}</div>${bd}</div>` : '';
1315
- const chips = `<div class="chips">${stack.map((c) => `<span class="chip">${esc(c)}</span>`).join('')}${dsUnrecognised ? '<span class="chip chip-dim">design system: unrecognised</span>' : ''}${legacyChip ? `<span class="chip chip-dim">${esc(legacyChip)}</span>` : ''}${agentFiles.map((c) => `<span class="chip chip-agent">${esc(c.file)}</span>`).join('')}</div>`;
1365
+ const chips = `<div class="chips">${stack.map((c) => `<span class="chip">${esc(c)}</span>`).join('')}${(P.shadcn?.sheet?.tweakcnPresent ?? 0) >= 10 ? '<span class="chip">tweakcn theme</span>' : ''}${dsUnrecognised ? '<span class="chip chip-dim">design system: unrecognised</span>' : ''}${legacyChip ? `<span class="chip chip-dim">${esc(legacyChip)}</span>` : ''}${agentFiles.map((c) => `<span class="chip chip-agent">${esc(c.file)}</span>`).join('')}</div>`;
1316
1366
  const facts = [
1317
1367
  shadcnReceipt(),
1318
1368
  exclusionsLine(),
@@ -1339,12 +1389,38 @@ function exceptionsBlock() {
1339
1389
  if (P.isShadcn) {
1340
1390
  const ai = P.shadcn?.arbitraryInstalled;
1341
1391
  if (ai?.uses) lines.push(`${n(ai.uses)} bracket value${ai.uses === 1 ? '' : 's'} inside ${esc(P.uiDirs.map((d) => basename(d)).join(', '))} are shadcn's own (${ai.values.slice(0, 3).map((v) => esc(v.value)).join(', ')}) and are not counted. shadcn's docs allow brackets for one-off values, so your agent will treat them as normal; the rules file tells it otherwise for your own code.`);
1392
+ const si = spacingOwn.installed;
1393
+ const sv = si.values.length;
1394
+ if (sv) lines.push(`${n(sv)} off-scale spacing value${sv === 1 ? '' : 's'} inside ${esc(P.uiDirs.map((d) => basename(d)).join(', '))} ${sv === 1 ? 'is' : 'are'} shadcn's own (${si.values.slice(0, 3).map((v) => esc(v.value)).join(', ')}) and ${sv === 1 ? 'is' : 'are'} not counted, for the same reason as the bracket values.`);
1342
1395
  if (vendoredUi && neverImported.length) lines.push(`${n(neverImported.length)} catalogue component${neverImported.length === 1 ? '' : 's'} not used yet: stock on the shelf, not scored.`);
1343
1396
  const rp = P.shadcn?.registryPaint;
1344
1397
  if (rp) lines.push(`Installed registr${rp.dirs.length === 1 ? 'y' : 'ies'} ${esc(rp.dirs.map((d) => basename(d)).join(', '))}: ${n(rp.files)} file${rp.files === 1 ? '' : 's'}${rp.tinUses ? `, ${n(rp.tinUses)} palette colour${rp.tinUses === 1 ? '' : 's'}` : ''}. Kept in the score, left out of the fixes. Your agent reads them like everything else.`);
1345
1398
  } else if (isLibrary) {
1346
1399
  lines.push('A library: unused components are internal-only, and downstream consumers are invisible from here.');
1347
1400
  }
1401
+ const li = h.tokens?.important?.library;
1402
+ if (li?.count) lines.push(`${n(li.count)} !important declaration${li.count === 1 ? '' : 's'} aimed at a library's own class names (${li.classes.slice(0, 3).map((c) => esc(c.value)).join(', ')}) ${li.count === 1 ? 'is' : 'are'} not counted: the library ships its CSS, and this is the only way through it. Your agent reads ${li.count === 1 ? 'it' : 'them'} like everything else.`);
1403
+ // Files no checker judges (email, print, artwork, render-to-image, the
1404
+ // crash page): named here with the reason, because the agent reads them
1405
+ // like everything else and can copy what is in them.
1406
+ const ex = h.tokens?.exemptFiles ?? [];
1407
+ if (ex.length) {
1408
+ const byReason = new Map();
1409
+ for (const e of ex) { if (!byReason.has(e.reason)) byReason.set(e.reason, []); byReason.get(e.reason).push(e.file); }
1410
+ for (const [reason, files] of byReason) {
1411
+ // two files with one name (a spare-part copy) are told apart by their folder
1412
+ const label = (f) => {
1413
+ const parts = f.split('/');
1414
+ for (let k = 1; k <= parts.length; k++) {
1415
+ const tail = parts.slice(-k).join('/');
1416
+ if (files.filter((o) => o === tail || o.endsWith(`/${tail}`)).length === 1) return tail;
1417
+ }
1418
+ return f;
1419
+ };
1420
+ const names = files.slice(0, 3).map((f) => esc(label(f))).join(', ') + (files.length > 3 ? ` and ${files.length - 3} more` : '');
1421
+ lines.push(`${n(files.length)} file${files.length === 1 ? '' : 's'} not judged (${names}): ${esc(reason)}. Not counted, but your agent reads ${files.length === 1 ? 'it' : 'them'} like everything else, so what is in there is still an example it can copy.`);
1422
+ }
1423
+ }
1348
1424
  if (!lines.length) return '';
1349
1425
  return `<div class="excs"><b>Not yours, and not counted</b>${lines.map((l) => `<p>${l}</p>`).join('')}</div>`;
1350
1426
  }
@@ -1409,6 +1485,8 @@ const html = `<!doctype html>
1409
1485
  .side .chips { margin:0 0 14px; gap:5px; }
1410
1486
  .side .facts { font:500 11.5px/1.55 var(--sans); color:var(--dim); margin-bottom:16px; }
1411
1487
  .side .facts .excl { margin-top:6px; font-size:11.5px; }
1488
+ .side .facts .excl.fresh { color:var(--text); }
1489
+ .side .facts .excl.fresh b { color:var(--text); }
1412
1490
  .side .facts b { color:var(--text); font-weight:600; }
1413
1491
  .side .idx-head { margin-bottom:4px; }
1414
1492
  .side .idx a { display:block; font:500 13px/1.3 var(--sans); color:var(--dim); text-decoration:none; padding:6px 0 6px 12px; border-left:2px solid var(--line); }
@@ -1961,7 +2039,8 @@ if (summaryPath) {
1961
2039
  verdict,
1962
2040
  role: P.role,
1963
2041
  kind: P.kind,
1964
- ...(P.isShadcn && P.shadcn ? { shadcn: { confidence: P.confidence, evidence: P.evidence, style: P.shadcn.kit?.style ?? null, baseColor: P.shadcn.kit?.baseColor ?? null, tailwind: P.shadcn.kit?.tailwind ?? null, catalogues: P.uiDirs, registries: P.shadcn.registryDirs ?? [], ...(P.shadcn.registryPaint ? { registryFiles: P.shadcn.registryPaint.files, registryPaletteColours: P.shadcn.registryPaint.tinUses } : {}), ownFiles: P.shadcn.paint?.ownFiles ?? null } } : {}),
2042
+ ...(P.isRegistry && P.registry ? { registry: { source: P.registry.source, builtFrom: P.registry.builtFrom, items: P.registry.items, publishes: P.registry.publishes, variants: P.registry.variants } } : {}),
2043
+ ...(P.isShadcn && P.shadcn ? { shadcn: { confidence: P.confidence, evidence: P.evidence, style: P.shadcn.kit?.style ?? null, baseColor: P.shadcn.kit?.baseColor ?? null, tailwind: P.shadcn.kit?.tailwind ?? null, catalogues: P.uiDirs, registries: P.shadcn.registryDirs ?? [], ...(P.shadcn.registryPaint ? { registryFiles: P.shadcn.registryPaint.files, registryPaletteColours: P.shadcn.registryPaint.tinUses } : {}), ownFiles: P.shadcn.paint?.ownFiles ?? null, fresh: P.shadcn.fresh?.fresh === true } } : {}),
1965
2044
  componentsMeasured,
1966
2045
  metrics: (({ colors, colorTokens, colorStrays, greys, greyStrays, spacing, exactDuplicates, inlineStyles, nearPairs, important, neverImported, arbitrary, tokenLed }) =>
1967
2046
  ({ colors, colorTokens, colorStrays, greys, greyStrays, spacing, exactDuplicates, inlineStyles, nearPairs, important, neverImported, arbitrary, tokenLed }))(M),
@@ -26,7 +26,7 @@ import { isGrey } from '../lib/color.mjs';
26
26
  import { nearColorPairs } from '../lib/nearpairs.mjs';
27
27
  import { neverImportedComponents } from '../lib/neverimported.mjs';
28
28
  import { SCHEMA_VERSION } from '../lib/version.mjs';
29
- import { profileOf, installedDirs, splitArbitrary } from '../profiles/index.mjs';
29
+ import { profileOf, installedDirs, splitArbitrary, ownSpacing } from '../profiles/index.mjs';
30
30
 
31
31
  export { SCHEMA_VERSION };
32
32
 
@@ -45,7 +45,8 @@ export function loadBenchmark() {
45
45
  * ideals and the reputable-systems line are the same for every kind.
46
46
  */
47
47
  export function benchHelpers(bench, kind = 'product') {
48
- const slice = bench?.slices?.[kind] ?? null;
48
+ // registries are compared with the shadcn repos, labelled as such (Greg, 2026-09-13)
49
+ const slice = bench?.slices?.[kind === 'registry' ? 'shadcn' : kind] ?? null;
49
50
  if (slice?.stats) bench = { ...bench, stats: { ...(bench?.stats ?? {}), ...slice.stats } };
50
51
  const sliceInfo = slice ? { kind, repoCount: slice.repoCount, builtAt: slice.builtAt } : null;
51
52
  // where does this value sit among the scanned fleet? ("more colours than 90%")
@@ -105,6 +106,8 @@ export const PROFILE_TILES = {
105
106
  ['doorOverrides', 'components recoloured from outside per 100 files'],
106
107
  ],
107
108
  };
109
+ // a registry measures what a shadcn repo measures (release (a), 2026-09-13)
110
+ PROFILE_TILES.registry = PROFILE_TILES.shadcn;
108
111
  export const tilesFor = (kind) => [...TILES, ...(PROFILE_TILES[kind] ?? [])];
109
112
 
110
113
  /** The tiles, in report order: metric key, the label the report prints. */
@@ -136,8 +139,9 @@ export function coreMetrics(h, opts = {}) {
136
139
  const greys = colors.filter((c) => isGrey(c.value));
137
140
  const colorTokens = colors.filter((c) => c.isToken).length;
138
141
  const colorStrays = colors.length - colorTokens;
139
- const spacing = h.tokens?.spacing ?? [];
140
- const twSpacing = h.tokens?.tailwind?.spacing ?? [];
142
+ // Off-scale spacing the team wrote: on a kit the values inside installed
143
+ // folders are shadcn's own, kept out like its bracket values and named.
144
+ const sp = ownSpacing(h);
141
145
  const hardDupes = (h.duplicates?.exactDuplicates ?? []).filter((d) => !d.wrapped);
142
146
  return {
143
147
  colors: colors.length,
@@ -150,7 +154,7 @@ export function coreMetrics(h, opts = {}) {
150
154
  greyStrays: greys.filter((c) => !c.isToken).length,
151
155
  // Off-scale spacing only: CSS-declared values plus bracket utilities.
152
156
  // Many steps of the sanctioned Tailwind scale is health, not sprawl.
153
- spacing: spacing.length + twSpacing.filter((v) => v.value.startsWith('[')).length,
157
+ spacing: sp.css.length + sp.tw.length,
154
158
  // A wrapped pair (one file imports the name from the other) is
155
159
  // composition, not competition: listed with a badge, never counted.
156
160
  exactDuplicates: hardDupes.length,