roast-my-design-system 7.2.0 → 7.3.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.2.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.0" width="72" alt="roast-my-design-system">
2
2
 
3
3
  # roast-my-design-system
4
4
 
@@ -10,6 +10,8 @@
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.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.
14
+
13
15
  > **New in 7.2: shadcn repos are read as shadcn repos.** The scanner finds the installed catalogue where it actually lives, reads the theme file `components.json` names, and prints how it decided under the report header. 2 new tiles appear on shadcn repos only, both from shadcn's own rules for agents: palette colours in your own code where a theme row exists, and kit components repainted through `className`. Counted per 100 of your own files, never inside the catalogue. The rules file gains a shadcn section. Scores on other repos do not move.
14
16
 
15
17
  > **New in 7.0.** The scoring code is now a separate file that other tools can import, so a CI check gets the same score as the report. Every `summary.json` now records its schema version and which benchmark it was scored against. Five counting bugs are fixed: a bracket spacing class like `p-[13px]` was counted twice, a hex colour inside a CSS comment was counted, and three smaller ones. Most repos lose a few counts. shadcn/ui goes from 325 colours to 323 and keeps its score of 65. Large monorepos scan about ten times faster. If you use the score as a CI threshold, run a fresh scan before comparing. If you read the JSON, a tile's `value` is now a number and the printed text is in `display`.
@@ -75,11 +77,11 @@ Not to be confused with each other: **"Why this matters"** is generic, ships wit
75
77
 
76
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:
77
79
 
78
- ![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.2.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.0)
79
81
 
80
82
  The same report in light mode (one file, built-in toggle):
81
83
 
82
- ![The diagnosis report in light mode](https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/report-light-hero.png?v=7.2.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.0)
83
85
 
84
86
  ## What makes the numbers trustworthy
85
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "7.2.0",
3
+ "version": "7.3.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": [
@@ -85,6 +85,14 @@
85
85
  "opacities": {
86
86
  "value": 5,
87
87
  "note": "disabled, overlay, hover tints"
88
+ },
89
+ "paintTin": {
90
+ "value": 25,
91
+ "note": "palette colours in own code where a theme variable exists, per 100 files"
92
+ },
93
+ "doorOverrides": {
94
+ "value": 15,
95
+ "note": "shadcn components given a colour or font through className, per 100 files"
88
96
  }
89
97
  },
90
98
  "stats": {
@@ -1841,5 +1849,875 @@
1841
1849
  }
1842
1850
  }
1843
1851
  ]
1852
+ },
1853
+ "slices": {
1854
+ "shadcn": {
1855
+ "builtAt": "2026-09-12T00:06:00.881Z",
1856
+ "repoCount": 17,
1857
+ "note": "every repo in the fleet the scanner reads as shadcn; the \"cleaner than\" line on a shadcn repo reads against these",
1858
+ "stats": {
1859
+ "colors": {
1860
+ "values": [
1861
+ 8,
1862
+ 12,
1863
+ 17,
1864
+ 31,
1865
+ 41,
1866
+ 56,
1867
+ 56,
1868
+ 66,
1869
+ 115,
1870
+ 118,
1871
+ 132,
1872
+ 174,
1873
+ 187,
1874
+ 254,
1875
+ 265,
1876
+ 310,
1877
+ 1030
1878
+ ],
1879
+ "p25": 41,
1880
+ "median": 115,
1881
+ "p75": 187,
1882
+ "p90": 283
1883
+ },
1884
+ "greys": {
1885
+ "values": [
1886
+ 0,
1887
+ 0,
1888
+ 3,
1889
+ 3,
1890
+ 4,
1891
+ 6,
1892
+ 9,
1893
+ 12,
1894
+ 14,
1895
+ 16,
1896
+ 22,
1897
+ 33,
1898
+ 38,
1899
+ 51,
1900
+ 52,
1901
+ 66,
1902
+ 111
1903
+ ],
1904
+ "p25": 4,
1905
+ "median": 14,
1906
+ "p75": 38,
1907
+ "p90": 58
1908
+ },
1909
+ "spacing": {
1910
+ "values": [
1911
+ 5,
1912
+ 6,
1913
+ 10,
1914
+ 10,
1915
+ 11,
1916
+ 14,
1917
+ 21,
1918
+ 25,
1919
+ 32,
1920
+ 34,
1921
+ 40,
1922
+ 45,
1923
+ 49,
1924
+ 59,
1925
+ 66,
1926
+ 120,
1927
+ 220
1928
+ ],
1929
+ "p25": 11,
1930
+ "median": 32,
1931
+ "p75": 49,
1932
+ "p90": 88
1933
+ },
1934
+ "typefaces": {
1935
+ "values": [
1936
+ 0,
1937
+ 1,
1938
+ 1,
1939
+ 2,
1940
+ 2,
1941
+ 2,
1942
+ 3,
1943
+ 3,
1944
+ 3,
1945
+ 3,
1946
+ 3,
1947
+ 3,
1948
+ 3,
1949
+ 5,
1950
+ 6,
1951
+ 8,
1952
+ 19
1953
+ ],
1954
+ "p25": 2,
1955
+ "median": 3,
1956
+ "p75": 3,
1957
+ "p90": 7
1958
+ },
1959
+ "fontSizes": {
1960
+ "values": [
1961
+ 5,
1962
+ 9,
1963
+ 12,
1964
+ 12,
1965
+ 12,
1966
+ 13,
1967
+ 13,
1968
+ 20,
1969
+ 21,
1970
+ 24,
1971
+ 27,
1972
+ 27,
1973
+ 42,
1974
+ 43,
1975
+ 49,
1976
+ 49,
1977
+ 79
1978
+ ],
1979
+ "p25": 12,
1980
+ "median": 21,
1981
+ "p75": 42,
1982
+ "p90": 49
1983
+ },
1984
+ "radii": {
1985
+ "values": [
1986
+ 7,
1987
+ 8,
1988
+ 9,
1989
+ 11,
1990
+ 11,
1991
+ 12,
1992
+ 13,
1993
+ 14,
1994
+ 14,
1995
+ 19,
1996
+ 20,
1997
+ 20,
1998
+ 21,
1999
+ 26,
2000
+ 28,
2001
+ 34,
2002
+ 50
2003
+ ],
2004
+ "p25": 11,
2005
+ "median": 14,
2006
+ "p75": 21,
2007
+ "p90": 30
2008
+ },
2009
+ "shadows": {
2010
+ "values": [
2011
+ 0,
2012
+ 0,
2013
+ 0,
2014
+ 0,
2015
+ 0,
2016
+ 0,
2017
+ 0,
2018
+ 0,
2019
+ 1,
2020
+ 1,
2021
+ 2,
2022
+ 2,
2023
+ 4,
2024
+ 6,
2025
+ 10,
2026
+ 10,
2027
+ 14
2028
+ ],
2029
+ "p25": 0,
2030
+ "median": 1,
2031
+ "p75": 4,
2032
+ "p90": 10
2033
+ },
2034
+ "exactDuplicates": {
2035
+ "values": [
2036
+ 0,
2037
+ 1,
2038
+ 1,
2039
+ 1,
2040
+ 14,
2041
+ 16,
2042
+ 17,
2043
+ 20,
2044
+ 21,
2045
+ 23,
2046
+ 25,
2047
+ 61,
2048
+ 65,
2049
+ 102,
2050
+ 104,
2051
+ 134,
2052
+ 288
2053
+ ],
2054
+ "p25": 14,
2055
+ "median": 21,
2056
+ "p75": 65,
2057
+ "p90": 116
2058
+ },
2059
+ "inlineStyles": {
2060
+ "values": [
2061
+ 0,
2062
+ 2,
2063
+ 5,
2064
+ 7,
2065
+ 11,
2066
+ 22,
2067
+ 24,
2068
+ 42,
2069
+ 47,
2070
+ 47,
2071
+ 49,
2072
+ 58,
2073
+ 74,
2074
+ 79,
2075
+ 117,
2076
+ 120,
2077
+ 263
2078
+ ],
2079
+ "p25": 11,
2080
+ "median": 47,
2081
+ "p75": 74,
2082
+ "p90": 118
2083
+ },
2084
+ "arbitrary": {
2085
+ "values": [
2086
+ 12,
2087
+ 38,
2088
+ 54,
2089
+ 100,
2090
+ 101,
2091
+ 126,
2092
+ 128,
2093
+ 163,
2094
+ 169,
2095
+ 245,
2096
+ 257,
2097
+ 385,
2098
+ 386,
2099
+ 614,
2100
+ 622,
2101
+ 1625,
2102
+ 1673
2103
+ ],
2104
+ "p25": 101,
2105
+ "median": 169,
2106
+ "p75": 386,
2107
+ "p90": 1023
2108
+ },
2109
+ "nearPairs": {
2110
+ "values": [
2111
+ 0,
2112
+ 0,
2113
+ 0,
2114
+ 0,
2115
+ 1,
2116
+ 2,
2117
+ 7,
2118
+ 8,
2119
+ 10,
2120
+ 11,
2121
+ 15,
2122
+ 16,
2123
+ 18,
2124
+ 18,
2125
+ 21,
2126
+ 21,
2127
+ 59
2128
+ ],
2129
+ "p25": 1,
2130
+ "median": 10,
2131
+ "p75": 18,
2132
+ "p90": 21
2133
+ },
2134
+ "important": {
2135
+ "values": [
2136
+ 0,
2137
+ 0,
2138
+ 0,
2139
+ 0,
2140
+ 0,
2141
+ 0,
2142
+ 1,
2143
+ 1,
2144
+ 2,
2145
+ 9,
2146
+ 9,
2147
+ 12,
2148
+ 42,
2149
+ 42,
2150
+ 55,
2151
+ 93,
2152
+ 157
2153
+ ],
2154
+ "p25": 0,
2155
+ "median": 2,
2156
+ "p75": 42,
2157
+ "p90": 70
2158
+ },
2159
+ "neverImported": {
2160
+ "values": [
2161
+ 0,
2162
+ 0,
2163
+ 0,
2164
+ 0,
2165
+ 1,
2166
+ 2,
2167
+ 5,
2168
+ 13,
2169
+ 16,
2170
+ 19,
2171
+ 23,
2172
+ 26,
2173
+ 30,
2174
+ 44,
2175
+ 94,
2176
+ 99,
2177
+ 100
2178
+ ],
2179
+ "p25": 1,
2180
+ "median": 16,
2181
+ "p75": 30,
2182
+ "p90": 96
2183
+ },
2184
+ "components": {
2185
+ "values": [
2186
+ 60,
2187
+ 182,
2188
+ 246,
2189
+ 272,
2190
+ 316,
2191
+ 700,
2192
+ 777,
2193
+ 811,
2194
+ 911,
2195
+ 971,
2196
+ 1085,
2197
+ 1137,
2198
+ 1204,
2199
+ 1453,
2200
+ 1494,
2201
+ 1594,
2202
+ 3630
2203
+ ],
2204
+ "p25": 316,
2205
+ "median": 911,
2206
+ "p75": 1204,
2207
+ "p90": 1534
2208
+ },
2209
+ "paintTin": {
2210
+ "values": [
2211
+ 5,
2212
+ 8,
2213
+ 20,
2214
+ 21,
2215
+ 22,
2216
+ 32,
2217
+ 33,
2218
+ 42,
2219
+ 57,
2220
+ 58,
2221
+ 62,
2222
+ 67,
2223
+ 100,
2224
+ 183,
2225
+ 296,
2226
+ 491,
2227
+ 658
2228
+ ],
2229
+ "p25": 22,
2230
+ "median": 57,
2231
+ "p75": 100,
2232
+ "p90": 374
2233
+ },
2234
+ "doorOverrides": {
2235
+ "values": [
2236
+ 0,
2237
+ 3,
2238
+ 5,
2239
+ 13,
2240
+ 13,
2241
+ 15,
2242
+ 16,
2243
+ 16,
2244
+ 17,
2245
+ 18,
2246
+ 22,
2247
+ 26,
2248
+ 28,
2249
+ 31,
2250
+ 40,
2251
+ 44,
2252
+ 55
2253
+ ],
2254
+ "p25": 13,
2255
+ "median": 17,
2256
+ "p75": 28,
2257
+ "p90": 42
2258
+ }
2259
+ },
2260
+ "repos": [
2261
+ {
2262
+ "repo": "calcom/cal.com",
2263
+ "confidence": "high",
2264
+ "style": "default",
2265
+ "baseColor": "zinc",
2266
+ "tailwind": null,
2267
+ "codeFiles": 4391,
2268
+ "ownFiles": 811,
2269
+ "metrics": {
2270
+ "colors": 310,
2271
+ "greys": 66,
2272
+ "spacing": 120,
2273
+ "typefaces": 8,
2274
+ "fontSizes": 49,
2275
+ "radii": 50,
2276
+ "shadows": 10,
2277
+ "exactDuplicates": 134,
2278
+ "inlineStyles": 79,
2279
+ "arbitrary": 257,
2280
+ "nearPairs": 18,
2281
+ "important": 157,
2282
+ "neverImported": 99,
2283
+ "components": 1453,
2284
+ "paintTin": 22,
2285
+ "doorOverrides": 5
2286
+ }
2287
+ },
2288
+ {
2289
+ "repo": "shadcn-ui/taxonomy",
2290
+ "confidence": "medium",
2291
+ "style": null,
2292
+ "baseColor": null,
2293
+ "tailwind": null,
2294
+ "codeFiles": 129,
2295
+ "ownFiles": 54,
2296
+ "metrics": {
2297
+ "colors": 8,
2298
+ "greys": 0,
2299
+ "spacing": 5,
2300
+ "typefaces": 2,
2301
+ "fontSizes": 13,
2302
+ "radii": 7,
2303
+ "shadows": 1,
2304
+ "exactDuplicates": 0,
2305
+ "inlineStyles": 0,
2306
+ "arbitrary": 54,
2307
+ "nearPairs": 0,
2308
+ "important": 1,
2309
+ "neverImported": 94,
2310
+ "components": 182,
2311
+ "paintTin": 20,
2312
+ "doorOverrides": 0
2313
+ }
2314
+ },
2315
+ {
2316
+ "repo": "steven-tey/novel",
2317
+ "confidence": "medium",
2318
+ "style": "default",
2319
+ "baseColor": "slate",
2320
+ "tailwind": "3",
2321
+ "codeFiles": 61,
2322
+ "ownFiles": 23,
2323
+ "metrics": {
2324
+ "colors": 31,
2325
+ "greys": 6,
2326
+ "spacing": 10,
2327
+ "typefaces": 5,
2328
+ "fontSizes": 5,
2329
+ "radii": 11,
2330
+ "shadows": 2,
2331
+ "exactDuplicates": 1,
2332
+ "inlineStyles": 2,
2333
+ "arbitrary": 12,
2334
+ "nearPairs": 0,
2335
+ "important": 2,
2336
+ "neverImported": 0,
2337
+ "components": 60,
2338
+ "paintTin": 57,
2339
+ "doorOverrides": 13
2340
+ }
2341
+ },
2342
+ {
2343
+ "repo": "vercel/ai-chatbot",
2344
+ "confidence": "high",
2345
+ "style": "radix-maia",
2346
+ "baseColor": "neutral",
2347
+ "tailwind": "4",
2348
+ "codeFiles": 146,
2349
+ "ownFiles": 60,
2350
+ "metrics": {
2351
+ "colors": 41,
2352
+ "greys": 0,
2353
+ "spacing": 6,
2354
+ "typefaces": 3,
2355
+ "fontSizes": 12,
2356
+ "radii": 14,
2357
+ "shadows": 4,
2358
+ "exactDuplicates": 1,
2359
+ "inlineStyles": 7,
2360
+ "arbitrary": 100,
2361
+ "nearPairs": 0,
2362
+ "important": 9,
2363
+ "neverImported": 0,
2364
+ "components": 316,
2365
+ "paintTin": 67,
2366
+ "doorOverrides": 18
2367
+ }
2368
+ },
2369
+ {
2370
+ "repo": "mckaywrigley/chatbot-ui",
2371
+ "confidence": "high",
2372
+ "style": "default",
2373
+ "baseColor": "gray",
2374
+ "tailwind": "3",
2375
+ "codeFiles": 258,
2376
+ "ownFiles": 102,
2377
+ "metrics": {
2378
+ "colors": 12,
2379
+ "greys": 3,
2380
+ "spacing": 25,
2381
+ "typefaces": 1,
2382
+ "fontSizes": 9,
2383
+ "radii": 8,
2384
+ "shadows": 0,
2385
+ "exactDuplicates": 1,
2386
+ "inlineStyles": 11,
2387
+ "arbitrary": 126,
2388
+ "nearPairs": 2,
2389
+ "important": 0,
2390
+ "neverImported": 100,
2391
+ "components": 272,
2392
+ "paintTin": 33,
2393
+ "doorOverrides": 17
2394
+ }
2395
+ },
2396
+ {
2397
+ "repo": "documenso/documenso",
2398
+ "confidence": "medium",
2399
+ "style": null,
2400
+ "baseColor": null,
2401
+ "tailwind": null,
2402
+ "codeFiles": 1934,
2403
+ "ownFiles": 466,
2404
+ "metrics": {
2405
+ "colors": 56,
2406
+ "greys": 12,
2407
+ "spacing": 34,
2408
+ "typefaces": 6,
2409
+ "fontSizes": 24,
2410
+ "radii": 13,
2411
+ "shadows": 0,
2412
+ "exactDuplicates": 16,
2413
+ "inlineStyles": 74,
2414
+ "arbitrary": 245,
2415
+ "nearPairs": 18,
2416
+ "important": 0,
2417
+ "neverImported": 44,
2418
+ "components": 811,
2419
+ "paintTin": 62,
2420
+ "doorOverrides": 22
2421
+ }
2422
+ },
2423
+ {
2424
+ "repo": "dubinc/dub",
2425
+ "confidence": "medium",
2426
+ "style": null,
2427
+ "baseColor": null,
2428
+ "tailwind": null,
2429
+ "codeFiles": 4160,
2430
+ "ownFiles": 1389,
2431
+ "metrics": {
2432
+ "colors": 118,
2433
+ "greys": 22,
2434
+ "spacing": 49,
2435
+ "typefaces": 3,
2436
+ "fontSizes": 43,
2437
+ "radii": 28,
2438
+ "shadows": 0,
2439
+ "exactDuplicates": 23,
2440
+ "inlineStyles": 49,
2441
+ "arbitrary": 622,
2442
+ "nearPairs": 15,
2443
+ "important": 1,
2444
+ "neverImported": 16,
2445
+ "components": 1594,
2446
+ "paintTin": 658,
2447
+ "doorOverrides": 40
2448
+ }
2449
+ },
2450
+ {
2451
+ "repo": "formbricks/formbricks",
2452
+ "confidence": "high",
2453
+ "style": "new-york",
2454
+ "baseColor": "slate",
2455
+ "tailwind": null,
2456
+ "codeFiles": 2604,
2457
+ "ownFiles": 788,
2458
+ "metrics": {
2459
+ "colors": 132,
2460
+ "greys": 16,
2461
+ "spacing": 32,
2462
+ "typefaces": 2,
2463
+ "fontSizes": 21,
2464
+ "radii": 20,
2465
+ "shadows": 6,
2466
+ "exactDuplicates": 61,
2467
+ "inlineStyles": 24,
2468
+ "arbitrary": 169,
2469
+ "nearPairs": 8,
2470
+ "important": 55,
2471
+ "neverImported": 2,
2472
+ "components": 1085,
2473
+ "paintTin": 296,
2474
+ "doorOverrides": 31
2475
+ }
2476
+ },
2477
+ {
2478
+ "repo": "openstatusHQ/openstatus",
2479
+ "confidence": "high",
2480
+ "style": "new-york",
2481
+ "baseColor": "neutral",
2482
+ "tailwind": null,
2483
+ "codeFiles": 2141,
2484
+ "ownFiles": 775,
2485
+ "metrics": {
2486
+ "colors": 174,
2487
+ "greys": 38,
2488
+ "spacing": 11,
2489
+ "typefaces": 3,
2490
+ "fontSizes": 13,
2491
+ "radii": 11,
2492
+ "shadows": 0,
2493
+ "exactDuplicates": 102,
2494
+ "inlineStyles": 120,
2495
+ "arbitrary": 163,
2496
+ "nearPairs": 11,
2497
+ "important": 0,
2498
+ "neverImported": 5,
2499
+ "components": 1137,
2500
+ "paintTin": 5,
2501
+ "doorOverrides": 16
2502
+ }
2503
+ },
2504
+ {
2505
+ "repo": "lukevella/rallly",
2506
+ "confidence": "medium",
2507
+ "style": "base-vega",
2508
+ "baseColor": "gray",
2509
+ "tailwind": null,
2510
+ "codeFiles": 875,
2511
+ "ownFiles": 410,
2512
+ "metrics": {
2513
+ "colors": 17,
2514
+ "greys": 3,
2515
+ "spacing": 14,
2516
+ "typefaces": 3,
2517
+ "fontSizes": 12,
2518
+ "radii": 14,
2519
+ "shadows": 0,
2520
+ "exactDuplicates": 17,
2521
+ "inlineStyles": 22,
2522
+ "arbitrary": 38,
2523
+ "nearPairs": 1,
2524
+ "important": 0,
2525
+ "neverImported": 19,
2526
+ "components": 700,
2527
+ "paintTin": 100,
2528
+ "doorOverrides": 3
2529
+ }
2530
+ },
2531
+ {
2532
+ "repo": "mfts/papermark",
2533
+ "confidence": "high",
2534
+ "style": "default",
2535
+ "baseColor": "gray",
2536
+ "tailwind": "3",
2537
+ "codeFiles": 1379,
2538
+ "ownFiles": 531,
2539
+ "metrics": {
2540
+ "colors": 66,
2541
+ "greys": 14,
2542
+ "spacing": 45,
2543
+ "typefaces": 1,
2544
+ "fontSizes": 27,
2545
+ "radii": 19,
2546
+ "shadows": 1,
2547
+ "exactDuplicates": 20,
2548
+ "inlineStyles": 47,
2549
+ "arbitrary": 386,
2550
+ "nearPairs": 7,
2551
+ "important": 42,
2552
+ "neverImported": 13,
2553
+ "components": 777,
2554
+ "paintTin": 491,
2555
+ "doorOverrides": 55
2556
+ }
2557
+ },
2558
+ {
2559
+ "repo": "elie222/inbox-zero",
2560
+ "confidence": "medium",
2561
+ "style": "default",
2562
+ "baseColor": "slate",
2563
+ "tailwind": "3",
2564
+ "codeFiles": 2120,
2565
+ "ownFiles": 340,
2566
+ "metrics": {
2567
+ "colors": 254,
2568
+ "greys": 52,
2569
+ "spacing": 21,
2570
+ "typefaces": 3,
2571
+ "fontSizes": 27,
2572
+ "radii": 21,
2573
+ "shadows": 0,
2574
+ "exactDuplicates": 21,
2575
+ "inlineStyles": 5,
2576
+ "arbitrary": 128,
2577
+ "nearPairs": 21,
2578
+ "important": 9,
2579
+ "neverImported": 0,
2580
+ "components": 911,
2581
+ "paintTin": 183,
2582
+ "doorOverrides": 15
2583
+ }
2584
+ },
2585
+ {
2586
+ "repo": "linkwarden/linkwarden",
2587
+ "confidence": "medium",
2588
+ "style": "default",
2589
+ "baseColor": "neutral",
2590
+ "tailwind": "3",
2591
+ "codeFiles": 519,
2592
+ "ownFiles": 226,
2593
+ "metrics": {
2594
+ "colors": 56,
2595
+ "greys": 9,
2596
+ "spacing": 10,
2597
+ "typefaces": 2,
2598
+ "fontSizes": 12,
2599
+ "radii": 9,
2600
+ "shadows": 0,
2601
+ "exactDuplicates": 25,
2602
+ "inlineStyles": 42,
2603
+ "arbitrary": 101,
2604
+ "nearPairs": 0,
2605
+ "important": 0,
2606
+ "neverImported": 1,
2607
+ "components": 246,
2608
+ "paintTin": 42,
2609
+ "doorOverrides": 13
2610
+ }
2611
+ },
2612
+ {
2613
+ "repo": "langfuse/langfuse",
2614
+ "confidence": "high",
2615
+ "style": "default",
2616
+ "baseColor": "slate",
2617
+ "tailwind": "4",
2618
+ "codeFiles": 3749,
2619
+ "ownFiles": 1205,
2620
+ "metrics": {
2621
+ "colors": 115,
2622
+ "greys": 4,
2623
+ "spacing": 40,
2624
+ "typefaces": 0,
2625
+ "fontSizes": 20,
2626
+ "radii": 12,
2627
+ "shadows": 0,
2628
+ "exactDuplicates": 14,
2629
+ "inlineStyles": 47,
2630
+ "arbitrary": 385,
2631
+ "nearPairs": 10,
2632
+ "important": 12,
2633
+ "neverImported": 0,
2634
+ "components": 1204,
2635
+ "paintTin": 21,
2636
+ "doorOverrides": 26
2637
+ }
2638
+ },
2639
+ {
2640
+ "repo": "teableio/teable",
2641
+ "confidence": "high",
2642
+ "style": "new-york",
2643
+ "baseColor": "gray",
2644
+ "tailwind": "3",
2645
+ "codeFiles": 5553,
2646
+ "ownFiles": 1234,
2647
+ "metrics": {
2648
+ "colors": 265,
2649
+ "greys": 33,
2650
+ "spacing": 66,
2651
+ "typefaces": 3,
2652
+ "fontSizes": 42,
2653
+ "radii": 26,
2654
+ "shadows": 10,
2655
+ "exactDuplicates": 65,
2656
+ "inlineStyles": 58,
2657
+ "arbitrary": 614,
2658
+ "nearPairs": 16,
2659
+ "important": 42,
2660
+ "neverImported": 30,
2661
+ "components": 1494,
2662
+ "paintTin": 58,
2663
+ "doorOverrides": 28
2664
+ }
2665
+ },
2666
+ {
2667
+ "repo": "midday-ai/midday",
2668
+ "confidence": "medium",
2669
+ "style": "new-york",
2670
+ "baseColor": "neutral",
2671
+ "tailwind": "4",
2672
+ "codeFiles": 1775,
2673
+ "ownFiles": 605,
2674
+ "metrics": {
2675
+ "colors": 187,
2676
+ "greys": 51,
2677
+ "spacing": 59,
2678
+ "typefaces": 3,
2679
+ "fontSizes": 49,
2680
+ "radii": 20,
2681
+ "shadows": 2,
2682
+ "exactDuplicates": 104,
2683
+ "inlineStyles": 117,
2684
+ "arbitrary": 1673,
2685
+ "nearPairs": 21,
2686
+ "important": 0,
2687
+ "neverImported": 26,
2688
+ "components": 971,
2689
+ "paintTin": 32,
2690
+ "doorOverrides": 44
2691
+ }
2692
+ },
2693
+ {
2694
+ "repo": "supabase/supabase",
2695
+ "confidence": "high",
2696
+ "style": "default",
2697
+ "baseColor": "gray",
2698
+ "tailwind": null,
2699
+ "codeFiles": 5954,
2700
+ "ownFiles": 3325,
2701
+ "metrics": {
2702
+ "colors": 1030,
2703
+ "greys": 111,
2704
+ "spacing": 220,
2705
+ "typefaces": 19,
2706
+ "fontSizes": 79,
2707
+ "radii": 34,
2708
+ "shadows": 14,
2709
+ "exactDuplicates": 288,
2710
+ "inlineStyles": 263,
2711
+ "arbitrary": 1625,
2712
+ "nearPairs": 59,
2713
+ "important": 93,
2714
+ "neverImported": 23,
2715
+ "components": 3630,
2716
+ "paintTin": 8,
2717
+ "doorOverrides": 16
2718
+ }
2719
+ }
2720
+ ]
2721
+ }
1844
2722
  }
1845
2723
  }
@@ -32,7 +32,7 @@ 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, scorePackage as scorePackageOf, ZERO_IDEAL, WARN_TOLERANCE, SCORE_OF, SCHEMA_VERSION } from './score.mjs';
35
- import { profileOf, profileYardstick } from '../profiles/index.mjs';
35
+ import { profileOf } from '../profiles/index.mjs';
36
36
 
37
37
  // The benchmark and every judgement made against it live in score.mjs; this
38
38
  // file only draws. The same numbers reach summary.json through scoreHarvest.
@@ -47,9 +47,10 @@ if (!inPath) { console.error('Usage: node src/diagnose/index.mjs <harvest.json>
47
47
  const outPath = resolve(arg('out', 'diagnosis.html'));
48
48
 
49
49
  const h = JSON.parse(readFileSync(inPath, 'utf8'));
50
- // The yardstick: the general benchmark, plus whatever the repo's kind brings
51
- // for the tiles only it measures (the shadcn card owns 2).
52
- const B = benchHelpers(bench, profileYardstick(h));
50
+ // The yardstick: the general benchmark, read through the slice for this
51
+ // repo's kind when the benchmark carries one (a shadcn repo is compared with
52
+ // the shadcn repos in the fleet).
53
+ const B = benchHelpers(bench, profileOf(h).kind);
53
54
  const { percentile, cleanerPct, ideal, median, displayAvg, refMedian } = B;
54
55
  const healthOf = makeHealthOf(B);
55
56
  const M = coreMetrics(h);
@@ -361,7 +362,7 @@ function tile(t, pLabel, pMetric, pFallback) {
361
362
  const avgNote = pct !== null && pct >= 60 && health !== 'good' ? ` · messier than ${pct}%`
362
363
  : clean !== null && clean >= 60 ? ` · cleaner than ${clean}%` : '';
363
364
  const av = displayAvg(metric);
364
- if (av !== null) rows.push(row('Avg Design System', `${n(av)}${avgNote}`, av, value));
365
+ if (av !== null) rows.push(row(B.sliceInfo ? `Avg ${B.sliceInfo.kind} repo` : 'Avg Design System', `${n(av)}${avgNote}`, av, value));
365
366
  if (rm !== null && (rm > 0 || ZERO_IDEAL.has(metric) || metric === 'arbitrary')) rows.push(row('Reputable systems', n(rm), rm, value));
366
367
  // A vendored catalogue is never judged for what it left on the shelf, and a
367
368
  // library's internal use says nothing about adoption: shown, not scored.
@@ -602,7 +603,8 @@ function sheetSection() {
602
603
  if (paint) {
603
604
  const tinChips = (paint.tin.samples ?? []).slice(0, 8).map((s) => `<span class="vchip bad">${esc(s.value)} ×${s.count}</span>`).join('');
604
605
  const tinFiles = (paint.tin.top ?? []).slice(0, 5).map((f) => `<span class="vchip" title="${esc(f.file)}">${esc(basename(f.file))} ×${f.count}</span>`).join('');
605
- parts.push(`<div class="receipts">${eyebrow(`${n(paint.tin.uses)} colours from outside the theme in ${n(paint.tin.files)} of ${n(paint.ownFiles)} own files · ${n(paint.tin.per100)} per 100 files`)}${paint.tin.uses ? `<div class="chips-row">${tinChips}</div><div class="chips-row">${tinFiles}</div>` : '<p class="sub">Your own code takes every colour from the theme file. This is what shadcn is designed for.</p>'}</div>`);
606
+ const rp = sc.registryPaint ?? null;
607
+ parts.push(`<div class="receipts">${eyebrow(`${n(paint.tin.uses)} colours from outside the theme in ${n(paint.tin.files)} of ${n(paint.ownFiles)} own files · ${n(paint.tin.per100)} per 100 files`)}${paint.tin.uses ? `<div class="chips-row">${tinChips}</div><div class="chips-row">${tinFiles}</div>` : '<p class="sub">Your own code takes every colour from the theme file. This is what shadcn is designed for.</p>'}${rp?.tinUses ? `<p class="sub">Not counted above: ${n(rp.tinUses)} palette colour${rp.tinUses === 1 ? '' : 's'} inside ${esc(rp.dirs.map((d) => basename(d)).join(', '))}, installed by a registry rather than written here. An agent reading those files will still copy them.</p>` : ''}</div>`);
606
608
  parts.push(whyToggle('paintTin'));
607
609
  const doorChips = (paint.doors.samples ?? []).slice(0, 6).map((s) => `<span class="vchip bad">${esc(s.value)} ×${s.count}</span>`).join('');
608
610
  parts.push(`<div class="receipts">${eyebrow(`${n(paint.doors.uses)} shadcn components restyled through className · ${n(paint.doors.per100)} per 100 files`)}${paint.doors.uses ? `<div class="chips-row">${doorChips}</div>` : '<p class="sub">No shadcn component is given a colour or a font through className. Variants are doing their job.</p>'}</div>`);
@@ -1263,7 +1265,9 @@ function shadcnReceipt() {
1263
1265
  ].filter(Boolean);
1264
1266
  const unmatched = (k?.fellBack ?? []).filter((f) => f === 'theme' || f === 'chartColor');
1265
1267
  const fell = unmatched.length ? ` · ${unmatched.length === 2 ? 'accent and chart colour' : unmatched[0] === 'theme' ? 'accent' : 'chart colour'} not matched to a named shadcn theme` : '';
1266
- 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>`;
1268
+ const rp = P.shadcn?.registryPaint ?? null;
1269
+ const regLine = rp ? `<div class="excl">Installed registries kept out of the own-code counts: ${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'} of their own` : ''}). Installed code, not written here, but your agent reads it like everything else and copies what it finds there.</div>` : '';
1270
+ 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>${regLine}`;
1267
1271
  }
1268
1272
 
1269
1273
  // User exclusions are printed in the header, never hidden: a scoped scan must
@@ -1839,7 +1843,7 @@ if (summaryPath) {
1839
1843
  // The contract two scans are compared by: schemaVersion for the
1840
1844
  // shape, benchmark for the ruler each scan was measured against.
1841
1845
  schemaVersion: SCHEMA_VERSION,
1842
- benchmark: bench ? { builtAt: bench.builtAt, repoCount: bench.repoCount, referenceSystems: bench.referenceSystems?.count ?? 0 } : null,
1846
+ benchmark: bench ? { builtAt: bench.builtAt, repoCount: bench.repoCount, referenceSystems: bench.referenceSystems?.count ?? 0, ...(B.sliceInfo ? { slice: B.sliceInfo } : {}) } : null,
1843
1847
  ...(commissionedBy ? { commissionedBy } : {}),
1844
1848
  ...(notesText ? { notesEmbedded: true } : {}),
1845
1849
  ...(extraSections.length ? { sectionsEmbedded: extraSections.map((s) => s.title) } : {}),
@@ -1849,7 +1853,7 @@ if (summaryPath) {
1849
1853
  verdict,
1850
1854
  role: P.role,
1851
1855
  kind: P.kind,
1852
- ...(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, ownFiles: P.shadcn.paint?.ownFiles ?? null } } : {}),
1856
+ ...(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 } } : {}),
1853
1857
  componentsMeasured,
1854
1858
  metrics: (({ colors, colorTokens, colorStrays, greys, greyStrays, spacing, exactDuplicates, inlineStyles, nearPairs, important, neverImported, arbitrary, tokenLed }) =>
1855
1859
  ({ 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, profileYardstick } from '../profiles/index.mjs';
29
+ import { profileOf } from '../profiles/index.mjs';
30
30
 
31
31
  export { SCHEMA_VERSION };
32
32
 
@@ -38,19 +38,16 @@ export function loadBenchmark() {
38
38
  }
39
39
 
40
40
  /**
41
- * The yardstick readers for one benchmark object (null-safe). A profile may
42
- * bring its own ideals and fleet stats for the metrics only it measures
43
- * (question 6 of the profile plan); they overlay the general benchmark and
44
- * never replace an entry the benchmark already has.
41
+ * The yardstick readers for one benchmark object (null-safe). When the
42
+ * benchmark carries a slice for the repo's kind (benchmark.slices.shadcn,
43
+ * built by tools/benchmark/build-slice.mjs), the fleet lines read against
44
+ * that slice: a shadcn repo is compared with shadcn repos. The curated
45
+ * ideals and the reputable-systems line are the same for every kind.
45
46
  */
46
- export function benchHelpers(bench, yardstick = null) {
47
- if (yardstick && (Object.keys(yardstick.ideals ?? {}).length || Object.keys(yardstick.stats ?? {}).length)) {
48
- bench = {
49
- ...(bench ?? {}),
50
- ideal2026: { ...(yardstick.ideals ?? {}), ...(bench?.ideal2026 ?? {}) },
51
- stats: { ...(yardstick.stats ?? {}), ...(bench?.stats ?? {}) },
52
- };
53
- }
47
+ export function benchHelpers(bench, kind = 'product') {
48
+ const slice = bench?.slices?.[kind] ?? null;
49
+ if (slice?.stats) bench = { ...bench, stats: { ...(bench?.stats ?? {}), ...slice.stats } };
50
+ const sliceInfo = slice ? { kind, repoCount: slice.repoCount, builtAt: slice.builtAt } : null;
54
51
  // where does this value sit among the scanned fleet? ("more colours than 90%")
55
52
  const percentile = (metric, value) => {
56
53
  const vals = bench?.stats?.[metric]?.values;
@@ -77,7 +74,7 @@ export function benchHelpers(bench, yardstick = null) {
77
74
  return mean >= 0.5 ? Math.round(mean) : null;
78
75
  };
79
76
  const refMedian = (metric) => bench?.referenceSystems?.stats?.[metric]?.median ?? null;
80
- return { percentile, cleanerPct, ideal, median, displayAvg, refMedian };
77
+ return { percentile, cleanerPct, ideal, median, displayAvg, refMedian, sliceInfo };
81
78
  }
82
79
 
83
80
  export const ZERO_IDEAL = new Set(['exactDuplicates', 'inlineStyles', 'nearPairs', 'important', 'neverImported']);
@@ -230,7 +227,7 @@ export function scorePackage(m, healthOf, b) {
230
227
 
231
228
  /** The whole judgement of one harvest, as data. */
232
229
  export function scoreHarvest(h, bench = loadBenchmark()) {
233
- const b = benchHelpers(bench, profileYardstick(h));
230
+ const b = benchHelpers(bench, profileOf(h).kind);
234
231
  const healthOf = makeHealthOf(b);
235
232
  const metrics = coreMetrics(h);
236
233
  const tiles = tileHealths(metrics, healthOf);
@@ -238,7 +235,7 @@ export function scoreHarvest(h, bench = loadBenchmark()) {
238
235
  schemaVersion: SCHEMA_VERSION,
239
236
  // A rebuilt ruler moves every score with no change in the repo, so a
240
237
  // history must know which ruler each scan was measured with.
241
- benchmark: bench ? { builtAt: bench.builtAt, repoCount: bench.repoCount, referenceSystems: bench.referenceSystems?.count ?? 0 } : null,
238
+ benchmark: bench ? { builtAt: bench.builtAt, repoCount: bench.repoCount, referenceSystems: bench.referenceSystems?.count ?? 0, ...(b.sliceInfo ? { slice: b.sliceInfo } : {}) } : null,
242
239
  metrics,
243
240
  tiles,
244
241
  score: scoreOfTiles(tiles),
@@ -68,9 +68,18 @@ decideProfile(profile, components, files, target);
68
68
  {
69
69
  const P = profileOf(profile);
70
70
  if (P.isShadcn) {
71
- const doorFiles = new Set([...P.uiDirs.flatMap((d) => files.code.filter((f) => f.startsWith(`${d}/`))), ...(profile.shadcn.blockFiles ?? [])]);
71
+ const installed = [...P.uiDirs, ...(profile.shadcn?.registryDirs ?? [])];
72
+ const doorFiles = new Set([...installed.flatMap((d) => files.code.filter((f) => f.startsWith(`${d}/`))), ...(profile.shadcn?.blockFiles ?? [])]);
72
73
  const kitNames = new Set(components.filter((c) => doorFiles.has(c.file)).map((c) => c.name));
73
- profile.shadcn.paint = countPaint(target, files.code, { uiDirs: [...P.uiDirs, ...(profile.shadcn.blockFiles ?? [])], kitNames });
74
+ profile.shadcn.paint = countPaint(target, files.code, { uiDirs: [...installed, ...(profile.shadcn.blockFiles ?? [])], kitNames });
75
+ // what the installed registries carry themselves: kept out of the own-code
76
+ // counts, shown in the header, because an agent reads them all the same
77
+ const regDirs = profile.shadcn.registryDirs ?? [];
78
+ if (regDirs.length) {
79
+ const regFiles = files.code.filter((f) => regDirs.some((d) => f.startsWith(`${d}/`)));
80
+ const rp = countPaint(target, regFiles, { uiDirs: [], kitNames });
81
+ profile.shadcn.registryPaint = { files: rp.ownFiles, tinUses: rp.tin.uses, dirs: regDirs };
82
+ }
74
83
  }
75
84
  }
76
85
 
@@ -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.2.0';
4
+ export const VERSION = '7.3.0';
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.
@@ -100,13 +100,3 @@ export function profileOf(h) {
100
100
  };
101
101
  }
102
102
 
103
- /**
104
- * Question 6, per kind: ideals and fleet stats a profile owns until the
105
- * benchmark slice for that kind exists. Overlaid on the general benchmark by
106
- * score.mjs; the general profile owns none, so nothing moves for it.
107
- */
108
- export function profileYardstick(h) {
109
- const kind = profileOf(h).kind;
110
- const p = PROFILES.find((x) => x.kind === kind);
111
- return { ideals: p?.ideals ?? {}, stats: p?.stats ?? {} };
112
- }
@@ -28,6 +28,15 @@ export const BLOCK_COMPONENTS = new Set(['app-sidebar', 'calendars', 'chart-area
28
28
  'search-form', 'section-cards', 'settings-dialog', 'sidebar-left', 'sidebar-opt-in-form',
29
29
  'sidebar-right', 'signup-form', 'site-header', 'team-switcher', 'version-switcher']);
30
30
 
31
+ // Third-party registries that install through the shadcn CLI into a folder of
32
+ // their own under the components alias (ai-elements from Vercel, kibo-ui,
33
+ // magicui, motion-primitives...). Installed code the team did not write,
34
+ // judged like the catalogue. Folder names as the CLI creates them; a
35
+ // components.json `registries` key ("@kibo-ui") adds its own name at scan.
36
+ export const REGISTRY_DIRS = new Set(['ai-elements', 'kibo-ui', 'magicui', 'magic-ui', 'aceternity',
37
+ 'aceternity-ui', 'originui', 'origin-ui', 'cult-ui', 'motion-primitives', 'animate-ui', 'tailark',
38
+ 'shadcn-blocks', 'skiper-ui', 'react-bits', 'eldora-ui', 'cuicui', 'spectrum-ui']);
39
+
31
40
  // The sheet: rows shadcn's theming docs define, each in :root and .dark.
32
41
  export const SHADCN_ROWS = ['background', 'foreground', 'card', 'card-foreground', 'popover',
33
42
  'popover-foreground', 'primary', 'primary-foreground', 'secondary', 'secondary-foreground',
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import { existsSync, readFileSync } from 'node:fs';
29
29
  import { join, dirname, basename } from 'node:path';
30
- import { CATALOGUE, BLOCK_COMPONENTS, KNOWN_ROWS, TWEAKCN_ROWS, LIGHT_ONLY_ROWS, FACTORY_SPACING,
30
+ import { CATALOGUE, BLOCK_COMPONENTS, REGISTRY_DIRS, KNOWN_ROWS, TWEAKCN_ROWS, LIGHT_ONLY_ROWS, FACTORY_SPACING,
31
31
  FRONTS, LEGACY_FRONTS, BASES, BASE_COLORS, ICON_LIBRARIES, RADIUS_MAP, THEMES, SHADCN_ROWS } from './shadcn-data.mjs';
32
32
 
33
33
  const readJSON = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; } };
@@ -239,29 +239,29 @@ export default {
239
239
  profile.designSystem = { kind: 'shadcn', name: 'shadcn/ui', confidence: 'high', cssVariables: cssVars };
240
240
  // kit doors installed as blocks into own code
241
241
  const blockFiles = files.code.filter((f) => /\.[jt]sx$/.test(f) && !profile.uiDirs.some((d) => f.startsWith(`${d}/`)) && BLOCK_COMPONENTS.has(stripExt(basename(f))));
242
+ // third-party registries installed beside the catalogue: a folder named
243
+ // as the CLI creates it, or as a `registries` key in components.json
244
+ // names it. Installed code, judged like the catalogue.
245
+ const registryNames = new Set(REGISTRY_DIRS);
246
+ for (const { cfg } of configs) for (const k of Object.keys(cfg.registries ?? {})) registryNames.add(k.replace(/^@/, '').toLowerCase());
247
+ const registryDirs = [...new Set(files.code
248
+ .filter((f) => /\.[jt]sx$/.test(f) && !/(^|\/)(node_modules|public|docs?|content|posts?)\//.test(f))
249
+ .map((f) => dirname(f))
250
+ .filter((d) => registryNames.has(basename(d).toLowerCase()) && !profile.uiDirs.some((u) => d === u || d.startsWith(`${u}/`))))]
251
+ .filter((d) => files.code.filter((f) => f.startsWith(`${d}/`) && /\.[jt]sx$/.test(f)).length >= 2);
252
+ if (registryDirs.length) evidence.push(`${registryDirs.length} installed registr${registryDirs.length === 1 ? 'y' : 'ies'} beside it: ${registryDirs.map((d) => basename(d)).join(', ')}`);
242
253
  profile.shadcn = {
243
254
  installs: installs.map((i) => ({ config: i.config, uiDir: i.uiDir, catalogueNames: i.catalogueNames })),
244
255
  kit,
245
256
  sheet,
246
257
  blockFiles,
258
+ registryDirs,
247
259
  };
248
260
  return { confidence, evidence };
249
261
  },
250
262
 
251
- /**
252
- * Question 6: the ideal for this kind. Per 100 own-code files. The numbers
253
- * come from the 2026-09-11 fleet probe over the 15 shadcn kitchens in the
254
- * benchmark (docs: shadcn-step1-fleet-probe): the ideal is the tidiest third,
255
- * the stats are the whole set so the "cleaner than" line is honest. The
256
- * step 4 slice replaces these with a rebuilt ruler.
257
- */
258
- ideals: {
259
- paintTin: { value: 25, note: 'palette colours per 100 own-code files; the tidiest third of 15 shadcn repos sit under this' },
260
- doorOverrides: { value: 15, note: 'kit components repainted through className, per 100 own-code files; the tidiest third sit under this' },
261
- },
262
- // the 15 shadcn kitchens in the benchmark fleet, 2026-09-11, per 100 own-code files
263
- stats: {
264
- paintTin: { values: [5, 8, 20, 21, 22, 32, 33, 58, 62, 66, 100, 179, 296, 488, 658], median: 62 },
265
- doorOverrides: { values: [0, 3, 5, 14, 16, 16, 16, 17, 22, 26, 28, 31, 40, 44, 55], median: 22 },
266
- },
263
+ // Question 6 and 9 live in benchmark.json: the curated ideals for the 2
264
+ // shadcn tiles sit in ideal2026, and the shadcn slice (built by
265
+ // tools/benchmark/build-slice.mjs from every fleet repo read as shadcn)
266
+ // gives the fleet lines. Nothing here owns a number.
267
267
  };
@@ -177,6 +177,7 @@ if (neverImported.length >= 3) {
177
177
  if (paint?.tin?.uses) lines.push(` (${paint.tin.uses} palette colour${paint.tin.uses === 1 ? '' : 's'} already sit in own code, ${paint.tin.samples.slice(0, 3).map((s) => `\`${s.value}\` ×${s.count}`).join(', ')}; do not add to them.)`);
178
178
  rule('Before adding classes to a shadcn component, use one of its variants (`variant="outline"`, `size="sm"`). `className` on a shadcn component is for layout only: width, margin, position. Never colour, never typography.');
179
179
  if (paint?.doors?.uses) lines.push(` (${paint.doors.uses} shadcn component${paint.doors.uses === 1 ? '' : 's'} already restyled through className, like ${paint.doors.samples.slice(0, 2).map((s) => `\`${s.value}\``).join(' and ')}; do not add to them.)`);
180
+ if (sc.registryDirs?.length) rule(`${sc.registryDirs.map((d) => `\`${d}\``).join(', ')} ${sc.registryDirs.length === 1 ? 'is an installed registry' : 'are installed registries'} (added through the shadcn CLI, not written here). Treat ${sc.registryDirs.length === 1 ? 'it' : 'them'} like \`${P.uiDir ?? 'components/ui'}\`: reach for what is there before building your own${sc.registryPaint?.tinUses ? `, and do not copy the ${sc.registryPaint.tinUses} palette colour${sc.registryPaint.tinUses === 1 ? '' : 's'} inside into own code` : ''}.`);
180
181
  rule(`Edit the component you own in \`${P.uiDir ?? 'components/ui'}\`. Never build a second one beside it under another name. A wrapper that composes shadcn components is fine; a second implementation is not.`);
181
182
  rule('Merge classes with `cn()`. Never concatenate strings and never write a ternary inside a className string.');
182
183
  rule('Add a component with `npx shadcn@latest add <name>`, then edit it. To see what changed upstream, run `npx shadcn@latest add <name> --diff`.');