roast-my-design-system 7.3.1 → 7.3.3
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 +4 -3
- package/package.json +1 -1
- package/skills/roast-my-design-system/scripts/benchmark/benchmark.json +40 -83
- package/skills/roast-my-design-system/scripts/diagnose/index.mjs +21 -21
- package/skills/roast-my-design-system/scripts/lib/version.mjs +1 -1
- package/skills/roast-my-design-system/scripts/mcp/engine.mjs +2 -2
- package/skills/roast-my-design-system/scripts/mcp/tools.mjs +2 -2
- package/skills/roast-my-design-system/scripts/profiles/shadcn.mjs +26 -3
- package/skills/roast-my-design-system/scripts/rules/build.mjs +1 -1
- package/skills/roast-my-design-system/scripts/sarif/index.mjs +1 -1
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.3.
|
|
1
|
+
<img src="https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/roaster_logo_300px.png?v=7.3.2" width="72" alt="roast-my-design-system">
|
|
2
2
|
|
|
3
3
|
# roast-my-design-system
|
|
4
4
|
|
|
@@ -77,11 +77,11 @@ Not to be confused with each other: **"Why this matters"** is generic, ships wit
|
|
|
77
77
|
|
|
78
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:
|
|
79
79
|
|
|
80
|
-

|
|
81
81
|
|
|
82
82
|
The same report in light mode (one file, built-in toggle):
|
|
83
83
|
|
|
84
|
-

|
|
85
85
|
|
|
86
86
|
## What makes the numbers trustworthy
|
|
87
87
|
|
|
@@ -235,6 +235,7 @@ After the roast, the skill also offers to write `design-system-rules.md` to disk
|
|
|
235
235
|
|
|
236
236
|
- **[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
|
|
237
237
|
- **[adobe/spectrum-web-components](https://gregkozakiewicz.github.io/roast-my-design-system/examples/adobe-spectrum.html)**: Lit, the `--spectrum-*` namespace named in the header
|
|
238
|
+
- **[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
|
|
238
239
|
- **[vercel/ai-chatbot](https://gregkozakiewicz.github.io/roast-my-design-system/examples/vercel-ai-chatbot.html)**: React, with Claude's notes embedded
|
|
239
240
|
- **[excalidraw/excalidraw](https://gregkozakiewicz.github.io/roast-my-design-system/examples/excalidraw-excalidraw.html)**
|
|
240
241
|
- **[dubinc/dub](https://gregkozakiewicz.github.io/roast-my-design-system/examples/dubinc-dub.html)**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.3",
|
|
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,8 +1852,8 @@
|
|
|
1852
1852
|
},
|
|
1853
1853
|
"slices": {
|
|
1854
1854
|
"shadcn": {
|
|
1855
|
-
"builtAt": "2026-09-
|
|
1856
|
-
"repoCount":
|
|
1855
|
+
"builtAt": "2026-09-12T19:02:27.297Z",
|
|
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": {
|
|
1859
1859
|
"colors": {
|
|
@@ -1867,7 +1867,6 @@
|
|
|
1867
1867
|
56,
|
|
1868
1868
|
66,
|
|
1869
1869
|
115,
|
|
1870
|
-
118,
|
|
1871
1870
|
132,
|
|
1872
1871
|
174,
|
|
1873
1872
|
187,
|
|
@@ -1876,10 +1875,10 @@
|
|
|
1876
1875
|
310,
|
|
1877
1876
|
1030
|
|
1878
1877
|
],
|
|
1879
|
-
"p25":
|
|
1880
|
-
"median":
|
|
1881
|
-
"p75":
|
|
1882
|
-
"p90":
|
|
1878
|
+
"p25": 39,
|
|
1879
|
+
"median": 91,
|
|
1880
|
+
"p75": 204,
|
|
1881
|
+
"p90": 288
|
|
1883
1882
|
},
|
|
1884
1883
|
"greys": {
|
|
1885
1884
|
"values": [
|
|
@@ -1893,7 +1892,6 @@
|
|
|
1893
1892
|
12,
|
|
1894
1893
|
14,
|
|
1895
1894
|
16,
|
|
1896
|
-
22,
|
|
1897
1895
|
33,
|
|
1898
1896
|
38,
|
|
1899
1897
|
51,
|
|
@@ -1902,9 +1900,9 @@
|
|
|
1902
1900
|
111
|
|
1903
1901
|
],
|
|
1904
1902
|
"p25": 4,
|
|
1905
|
-
"median":
|
|
1906
|
-
"p75":
|
|
1907
|
-
"p90":
|
|
1903
|
+
"median": 13,
|
|
1904
|
+
"p75": 41,
|
|
1905
|
+
"p90": 59
|
|
1908
1906
|
},
|
|
1909
1907
|
"spacing": {
|
|
1910
1908
|
"values": [
|
|
@@ -1920,16 +1918,15 @@
|
|
|
1920
1918
|
34,
|
|
1921
1919
|
40,
|
|
1922
1920
|
45,
|
|
1923
|
-
49,
|
|
1924
1921
|
59,
|
|
1925
1922
|
66,
|
|
1926
1923
|
120,
|
|
1927
1924
|
220
|
|
1928
1925
|
],
|
|
1929
1926
|
"p25": 11,
|
|
1930
|
-
"median":
|
|
1927
|
+
"median": 29,
|
|
1931
1928
|
"p75": 49,
|
|
1932
|
-
"p90":
|
|
1929
|
+
"p90": 93
|
|
1933
1930
|
},
|
|
1934
1931
|
"typefaces": {
|
|
1935
1932
|
"values": [
|
|
@@ -1945,7 +1942,6 @@
|
|
|
1945
1942
|
3,
|
|
1946
1943
|
3,
|
|
1947
1944
|
3,
|
|
1948
|
-
3,
|
|
1949
1945
|
5,
|
|
1950
1946
|
6,
|
|
1951
1947
|
8,
|
|
@@ -1953,7 +1949,7 @@
|
|
|
1953
1949
|
],
|
|
1954
1950
|
"p25": 2,
|
|
1955
1951
|
"median": 3,
|
|
1956
|
-
"p75":
|
|
1952
|
+
"p75": 4,
|
|
1957
1953
|
"p90": 7
|
|
1958
1954
|
},
|
|
1959
1955
|
"fontSizes": {
|
|
@@ -1971,14 +1967,13 @@
|
|
|
1971
1967
|
27,
|
|
1972
1968
|
27,
|
|
1973
1969
|
42,
|
|
1974
|
-
43,
|
|
1975
1970
|
49,
|
|
1976
1971
|
49,
|
|
1977
1972
|
79
|
|
1978
1973
|
],
|
|
1979
1974
|
"p25": 12,
|
|
1980
1975
|
"median": 21,
|
|
1981
|
-
"p75":
|
|
1976
|
+
"p75": 31,
|
|
1982
1977
|
"p90": 49
|
|
1983
1978
|
},
|
|
1984
1979
|
"radii": {
|
|
@@ -1997,13 +1992,12 @@
|
|
|
1997
1992
|
20,
|
|
1998
1993
|
21,
|
|
1999
1994
|
26,
|
|
2000
|
-
28,
|
|
2001
1995
|
34,
|
|
2002
1996
|
50
|
|
2003
1997
|
],
|
|
2004
1998
|
"p25": 11,
|
|
2005
1999
|
"median": 14,
|
|
2006
|
-
"p75":
|
|
2000
|
+
"p75": 20,
|
|
2007
2001
|
"p90": 30
|
|
2008
2002
|
},
|
|
2009
2003
|
"shadows": {
|
|
@@ -2015,7 +2009,6 @@
|
|
|
2015
2009
|
0,
|
|
2016
2010
|
0,
|
|
2017
2011
|
0,
|
|
2018
|
-
0,
|
|
2019
2012
|
1,
|
|
2020
2013
|
1,
|
|
2021
2014
|
2,
|
|
@@ -2028,7 +2021,7 @@
|
|
|
2028
2021
|
],
|
|
2029
2022
|
"p25": 0,
|
|
2030
2023
|
"median": 1,
|
|
2031
|
-
"p75":
|
|
2024
|
+
"p75": 5,
|
|
2032
2025
|
"p90": 10
|
|
2033
2026
|
},
|
|
2034
2027
|
"exactDuplicates": {
|
|
@@ -2042,7 +2035,6 @@
|
|
|
2042
2035
|
17,
|
|
2043
2036
|
20,
|
|
2044
2037
|
21,
|
|
2045
|
-
23,
|
|
2046
2038
|
25,
|
|
2047
2039
|
61,
|
|
2048
2040
|
65,
|
|
@@ -2051,10 +2043,10 @@
|
|
|
2051
2043
|
134,
|
|
2052
2044
|
288
|
|
2053
2045
|
],
|
|
2054
|
-
"p25":
|
|
2046
|
+
"p25": 11,
|
|
2055
2047
|
"median": 21,
|
|
2056
|
-
"p75":
|
|
2057
|
-
"p90":
|
|
2048
|
+
"p75": 74,
|
|
2049
|
+
"p90": 119
|
|
2058
2050
|
},
|
|
2059
2051
|
"inlineStyles": {
|
|
2060
2052
|
"values": [
|
|
@@ -2068,7 +2060,6 @@
|
|
|
2068
2060
|
42,
|
|
2069
2061
|
47,
|
|
2070
2062
|
47,
|
|
2071
|
-
49,
|
|
2072
2063
|
58,
|
|
2073
2064
|
74,
|
|
2074
2065
|
79,
|
|
@@ -2076,10 +2067,10 @@
|
|
|
2076
2067
|
120,
|
|
2077
2068
|
263
|
|
2078
2069
|
],
|
|
2079
|
-
"p25":
|
|
2080
|
-
"median":
|
|
2081
|
-
"p75":
|
|
2082
|
-
"p90":
|
|
2070
|
+
"p25": 10,
|
|
2071
|
+
"median": 45,
|
|
2072
|
+
"p75": 75,
|
|
2073
|
+
"p90": 119
|
|
2083
2074
|
},
|
|
2084
2075
|
"arbitrary": {
|
|
2085
2076
|
"values": [
|
|
@@ -2097,14 +2088,13 @@
|
|
|
2097
2088
|
385,
|
|
2098
2089
|
386,
|
|
2099
2090
|
614,
|
|
2100
|
-
622,
|
|
2101
2091
|
1625,
|
|
2102
2092
|
1673
|
|
2103
2093
|
],
|
|
2104
2094
|
"p25": 101,
|
|
2105
|
-
"median":
|
|
2106
|
-
"p75":
|
|
2107
|
-
"p90":
|
|
2095
|
+
"median": 166,
|
|
2096
|
+
"p75": 385,
|
|
2097
|
+
"p90": 1120
|
|
2108
2098
|
},
|
|
2109
2099
|
"nearPairs": {
|
|
2110
2100
|
"values": [
|
|
@@ -2118,7 +2108,6 @@
|
|
|
2118
2108
|
8,
|
|
2119
2109
|
10,
|
|
2120
2110
|
11,
|
|
2121
|
-
15,
|
|
2122
2111
|
16,
|
|
2123
2112
|
18,
|
|
2124
2113
|
18,
|
|
@@ -2127,7 +2116,7 @@
|
|
|
2127
2116
|
59
|
|
2128
2117
|
],
|
|
2129
2118
|
"p25": 1,
|
|
2130
|
-
"median":
|
|
2119
|
+
"median": 9,
|
|
2131
2120
|
"p75": 18,
|
|
2132
2121
|
"p90": 21
|
|
2133
2122
|
},
|
|
@@ -2140,7 +2129,6 @@
|
|
|
2140
2129
|
0,
|
|
2141
2130
|
0,
|
|
2142
2131
|
1,
|
|
2143
|
-
1,
|
|
2144
2132
|
2,
|
|
2145
2133
|
9,
|
|
2146
2134
|
9,
|
|
@@ -2152,9 +2140,9 @@
|
|
|
2152
2140
|
157
|
|
2153
2141
|
],
|
|
2154
2142
|
"p25": 0,
|
|
2155
|
-
"median":
|
|
2143
|
+
"median": 6,
|
|
2156
2144
|
"p75": 42,
|
|
2157
|
-
"p90":
|
|
2145
|
+
"p90": 74
|
|
2158
2146
|
},
|
|
2159
2147
|
"neverImported": {
|
|
2160
2148
|
"values": [
|
|
@@ -2166,7 +2154,6 @@
|
|
|
2166
2154
|
2,
|
|
2167
2155
|
5,
|
|
2168
2156
|
13,
|
|
2169
|
-
16,
|
|
2170
2157
|
19,
|
|
2171
2158
|
23,
|
|
2172
2159
|
26,
|
|
@@ -2178,8 +2165,8 @@
|
|
|
2178
2165
|
],
|
|
2179
2166
|
"p25": 1,
|
|
2180
2167
|
"median": 16,
|
|
2181
|
-
"p75":
|
|
2182
|
-
"p90":
|
|
2168
|
+
"p75": 34,
|
|
2169
|
+
"p90": 97
|
|
2183
2170
|
},
|
|
2184
2171
|
"components": {
|
|
2185
2172
|
"values": [
|
|
@@ -2198,13 +2185,12 @@
|
|
|
2198
2185
|
1204,
|
|
2199
2186
|
1453,
|
|
2200
2187
|
1494,
|
|
2201
|
-
1594,
|
|
2202
2188
|
3630
|
|
2203
2189
|
],
|
|
2204
|
-
"p25":
|
|
2205
|
-
"median":
|
|
2206
|
-
"p75":
|
|
2207
|
-
"p90":
|
|
2190
|
+
"p25": 305,
|
|
2191
|
+
"median": 861,
|
|
2192
|
+
"p75": 1154,
|
|
2193
|
+
"p90": 1474
|
|
2208
2194
|
},
|
|
2209
2195
|
"paintTin": {
|
|
2210
2196
|
"values": [
|
|
@@ -2223,13 +2209,12 @@
|
|
|
2223
2209
|
100,
|
|
2224
2210
|
183,
|
|
2225
2211
|
296,
|
|
2226
|
-
491
|
|
2227
|
-
658
|
|
2212
|
+
491
|
|
2228
2213
|
],
|
|
2229
2214
|
"p25": 22,
|
|
2230
|
-
"median":
|
|
2231
|
-
"p75":
|
|
2232
|
-
"p90":
|
|
2215
|
+
"median": 50,
|
|
2216
|
+
"p75": 75,
|
|
2217
|
+
"p90": 240
|
|
2233
2218
|
},
|
|
2234
2219
|
"doorOverrides": {
|
|
2235
2220
|
"values": [
|
|
@@ -2247,14 +2232,13 @@
|
|
|
2247
2232
|
26,
|
|
2248
2233
|
28,
|
|
2249
2234
|
31,
|
|
2250
|
-
40,
|
|
2251
2235
|
44,
|
|
2252
2236
|
55
|
|
2253
2237
|
],
|
|
2254
2238
|
"p25": 13,
|
|
2255
2239
|
"median": 17,
|
|
2256
|
-
"p75":
|
|
2257
|
-
"p90":
|
|
2240
|
+
"p75": 27,
|
|
2241
|
+
"p90": 38
|
|
2258
2242
|
}
|
|
2259
2243
|
},
|
|
2260
2244
|
"repos": [
|
|
@@ -2420,33 +2404,6 @@
|
|
|
2420
2404
|
"doorOverrides": 22
|
|
2421
2405
|
}
|
|
2422
2406
|
},
|
|
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
2407
|
{
|
|
2451
2408
|
"repo": "formbricks/formbricks",
|
|
2452
2409
|
"confidence": "high",
|
|
@@ -277,7 +277,7 @@ const offenders = h.tokens.offenders ?? [];
|
|
|
277
277
|
const candidates = [];
|
|
278
278
|
const effColors = tokenLed ? colorStrays : colors.length;
|
|
279
279
|
const effGreys = tokenLed ? greyStrays : greys.length;
|
|
280
|
-
if (typefaces.length > 3) candidates.push({ ratio: typefaces.length / 3, text: `${typefaces.length} typefaces
|
|
280
|
+
if (typefaces.length > 3) candidates.push({ ratio: typefaces.length / 3, text: `${typefaces.length} typefaces. Most products use 2 or 3` });
|
|
281
281
|
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` });
|
|
282
282
|
if (effColors > 24 * 1.25) candidates.push({ ratio: effColors / 24, text: tokenLed
|
|
283
283
|
? `${n(colorStrays)} hardcoded colours outside the token set, your agent will happily copy them at random`
|
|
@@ -285,15 +285,15 @@ if (effColors > 24 * 1.25) candidates.push({ ratio: effColors / 24, text: tokenL
|
|
|
285
285
|
if (effGreys > 13 * 1.25) candidates.push({ ratio: effGreys / 13, text: tokenLed
|
|
286
286
|
? `${greyStrays} hardcoded greys outside the token set`
|
|
287
287
|
: `${greys.length} shades of grey doing the job of 13` });
|
|
288
|
-
if (spacingTotal > 15) candidates.push({ ratio: spacingTotal / 12, text: `${spacingTotal}
|
|
289
|
-
if (nearPairs.length >= 3) candidates.push({ ratio: 1 + nearPairs.length / 6, text: `${nearPairs.length}
|
|
290
|
-
if (important.count >= 10) candidates.push({ ratio: 1 + important.count / 30, text: `${n(important.count)} !important declarations
|
|
288
|
+
if (spacingTotal > 15) candidates.push({ ratio: spacingTotal / 12, text: `${spacingTotal} spacing values sit outside the scale. A tidy system needs about 12` });
|
|
289
|
+
if (nearPairs.length >= 3) candidates.push({ ratio: 1 + nearPairs.length / 6, text: `${nearPairs.length} pairs of colours are nearly the same (${nearPairs[0].a.value} next to ${nearPairs[0].b.value}). Each pair is 1 colour recorded twice` });
|
|
290
|
+
if (important.count >= 10) candidates.push({ ratio: 1 + important.count / 30, text: `${n(important.count)} !important declarations. Each one forces a style through instead of fixing the rule that blocked it` });
|
|
291
291
|
if (componentsMeasured && iconCollisions.length >= 5) candidates.push({ ratio: 1 + iconCollisions.length / 8, text: `two icon sets collide on ${iconCollisions.length} names` });
|
|
292
|
-
if (componentsMeasured && hardDupes.length > 0) candidates.push({ ratio: 1 + hardDupes.length / 5, text: `${hardDupes.length} component${hardDupes.length > 1 ? 's' : ''}
|
|
293
|
-
if (inline.count > 20) candidates.push({ ratio: inline.count / 20, text: `${n(inline.count)} inline style blocks
|
|
294
|
-
if (arbitraryCount >= 20) candidates.push({ ratio: arbitraryCount / 30, text: `${n(arbitraryCount)}
|
|
295
|
-
if (agentFiles.length === 0) candidates.push({ ratio: 1.3, text: `
|
|
296
|
-
if ((h.staleRules ?? []).length >= 2) candidates.push({ ratio: 1.2 + h.staleRules.length / 10, text: `${h.staleRules.length}
|
|
292
|
+
if (componentsMeasured && hardDupes.length > 0) candidates.push({ ratio: 1 + hardDupes.length / 5, text: `${hardDupes.length} component${hardDupes.length > 1 ? 's' : ''} exist in more than 1 version. An agent asked for a Button can not tell which one to use` });
|
|
293
|
+
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` });
|
|
294
|
+
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` });
|
|
295
|
+
if (agentFiles.length === 0) candidates.push({ ratio: 1.3, text: `No agent rules file, so your AI agent has nothing to follow` });
|
|
296
|
+
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` });
|
|
297
297
|
const findings = candidates.sort((a, b) => b.ratio - a.ratio).map((c) => c.text);
|
|
298
298
|
|
|
299
299
|
let verdict = findings.length === 0
|
|
@@ -337,8 +337,8 @@ function row(label, valText, refVal, value) {
|
|
|
337
337
|
}
|
|
338
338
|
const FALLBACK_TARGET = {
|
|
339
339
|
colors: 'a system needs ~24', greys: 'a scale has up to 13', spacing: 'a dozen deliberate exceptions',
|
|
340
|
-
exactDuplicates: 'should be 0', inlineStyles: 'invisible to any system', nearPairs: '
|
|
341
|
-
important: '
|
|
340
|
+
exactDuplicates: 'should be 0', inlineStyles: 'invisible to any system', nearPairs: '1 colour recorded twice',
|
|
341
|
+
important: 'forces a style through', neverImported: 'the system nobody found', arbitrary: 'a handful of deliberate exceptions',
|
|
342
342
|
paintTin: 'the theme file already has a variable for it', doorOverrides: 'use a variant instead',
|
|
343
343
|
};
|
|
344
344
|
// Dress one judged tile for the page: formatted number, comparison rows.
|
|
@@ -495,7 +495,7 @@ function paletteSection() {
|
|
|
495
495
|
<div class="usage-bar">${cells}${restCell}</div>
|
|
496
496
|
${ramp}
|
|
497
497
|
${nearPairs.length ? `
|
|
498
|
-
<div class="receipts">${eyebrow(`${nearPairs.length} nearly identical pair${nearPairs.length === 1 ? '' : 's'} ·
|
|
498
|
+
<div class="receipts">${eyebrow(`${nearPairs.length} nearly identical pair${nearPairs.length === 1 ? '' : 's'} · each pair is 1 colour recorded twice`)}
|
|
499
499
|
<div class="chips-row">${nearPairs.slice(0, 8).map((pr) => `<span class="vchip" title="every channel within ${pr.d} of its twin"><i class="ndot" style="background:${swatch(pr.a.value)}"></i>${esc(pr.a.value)} ×${pr.a.count} <b class="nsim">≈</b> <i class="ndot" style="background:${swatch(pr.b.value)}"></i>${esc(pr.b.value)} ×${pr.b.count}</span>`).join('')}${nearPairs.length > 8 ? `<span class="vchip dim">+${nearPairs.length - 8} more</span>` : ''}</div>
|
|
500
500
|
${whyToggle('nearPairs')}</div>` : ''}
|
|
501
501
|
</div>
|
|
@@ -514,31 +514,31 @@ function dupesTrap() {
|
|
|
514
514
|
const hard = exactDupes.filter((d) => !d.wrapped);
|
|
515
515
|
if (hard.length < 2) return '';
|
|
516
516
|
const top = hard[0];
|
|
517
|
-
return trapBox(`<${esc(top.name)}> alone has ${top.files.length} implementations. An agent asked for one
|
|
517
|
+
return trapBox(`<${esc(top.name)}> alone has ${top.files.length} implementations. An agent asked for one can not tell which is the real one, so it picks 1 at random or writes another. Every wrong pick becomes the example the next agent copies.`);
|
|
518
518
|
}
|
|
519
519
|
function spacingTrap() {
|
|
520
520
|
const candidates = [...spacing, ...(arbitrary ?? [])].filter((s) => s.count >= 15)
|
|
521
521
|
.sort((a, b) => b.count - a.count);
|
|
522
522
|
if (candidates.length) {
|
|
523
523
|
const t = candidates[0];
|
|
524
|
-
return trapBox(`${esc(t.value)} appears ${t.count} times. An agent looking for how this repo does spacing reads
|
|
524
|
+
return trapBox(`${esc(t.value)} appears ${t.count} times. An agent looking for how this repo does spacing reads a repeated value as the rule, so it will write use number ${t.count + 1}. The most copied value here is the one you least want copied.`);
|
|
525
525
|
}
|
|
526
526
|
if (twSpacing.length > 0 && spacing.length >= 10) {
|
|
527
|
-
return trapBox(`Two spacing dialects coexist here: Tailwind steps and ${spacing.length} raw CSS values. A
|
|
527
|
+
return trapBox(`Two spacing dialects coexist here: Tailwind steps and ${spacing.length} raw CSS values. A person knows which one is old. An agent sees 2 valid options and follows whichever file it opened last, so each edit lands in one system or the other by chance.`);
|
|
528
528
|
}
|
|
529
529
|
return '';
|
|
530
530
|
}
|
|
531
531
|
function coloursTrap() {
|
|
532
532
|
if (nearPairs.length < 6) return '';
|
|
533
|
-
return trapBox(`This palette holds ${nearPairs.length} pairs of colours a screen can barely tell apart. An agent asked for the brand grey
|
|
533
|
+
return trapBox(`This palette holds ${nearPairs.length} pairs of colours a screen can barely tell apart. An agent asked for the brand grey can not see the difference either, so it copies whichever twin is nearest, and when unsure it writes a third between them. 2 becomes 3.`);
|
|
534
534
|
}
|
|
535
535
|
function inlineTrap() {
|
|
536
536
|
if (inline.count < 50) return '';
|
|
537
|
-
return trapBox(`${n(inline.count)} inline styles live where the system should. An agent learns a codebase by example, and every inline style is an example that says the system is optional. Each
|
|
537
|
+
return trapBox(`${n(inline.count)} inline styles live where the system should. An agent learns a codebase by example, and every inline style is an example that says the system is optional. Each one it copies becomes the example for the next.`);
|
|
538
538
|
}
|
|
539
539
|
function importantTrap() {
|
|
540
540
|
if (important.count < 20) return '';
|
|
541
|
-
return trapBox(`${n(important.count)} !important declarations, and each one
|
|
541
|
+
return trapBox(`${n(important.count)} !important declarations, and each one forces a style through instead of fixing the rule that blocked it. An agent whose style will not apply does what the repo taught it and adds another. The count only goes up.`);
|
|
542
542
|
}
|
|
543
543
|
function collisionsTrap() {
|
|
544
544
|
if (!collisions.length) return '';
|
|
@@ -551,7 +551,7 @@ function collisionsTrap() {
|
|
|
551
551
|
function orphansTrap() {
|
|
552
552
|
if (!componentsMeasured || isLibrary) return '';
|
|
553
553
|
if (neverImported.length < 10) return '';
|
|
554
|
-
return trapBox(`${neverImported.length} components are never imported anywhere. An agent searching for a Button finds the
|
|
554
|
+
return trapBox(`${neverImported.length} components are never imported anywhere. An agent searching for a Button finds the unused ones next to the real one with nothing to tell them apart, so an old dead end becomes today's example.`);
|
|
555
555
|
}
|
|
556
556
|
// At most three traps per report, in severity order: scarcity is what makes
|
|
557
557
|
// the marker readable as a warning rather than wallpaper.
|
|
@@ -572,7 +572,7 @@ function spacingBars() {
|
|
|
572
572
|
<div class="bar-track"><div class="bar${s.off ? ' bar-off' : ''}" style="width:${Math.max(2, Math.round((s.count / max) * 100))}%"></div></div>
|
|
573
573
|
<span class="bar-count">${n(s.count)}</span></div>`).join('');
|
|
574
574
|
const arb = arbitraryCount ? `
|
|
575
|
-
<div class="receipts">${eyebrow(`${n(arbitraryCount)}
|
|
575
|
+
<div class="receipts">${eyebrow(`${n(arbitraryCount)} bracket values written outside the scale`)}
|
|
576
576
|
<div class="chips-row">${arbitrary.slice(0, 10).map((a) => `<span class="vchip bad" title="${esc(a.files?.[0]?.file ?? '')}">${esc(a.value)} ×${a.count}</span>`).join('')}${arbitrary.length > 10 ? `<span class="vchip dim">+${arbitrary.length - 10} more</span>` : ''}</div>
|
|
577
577
|
${whyToggle('arbitrary')}</div>` : '';
|
|
578
578
|
return `<section class="glass pad">
|
|
@@ -759,7 +759,7 @@ function inlineSection() {
|
|
|
759
759
|
if (inline.count < 5 && important.count < 5) return '';
|
|
760
760
|
const max = Math.max(...inline.files.slice(0, 6).map((f) => f.count), 1);
|
|
761
761
|
const importantRow = important.count ? `
|
|
762
|
-
<div class="receipts">${eyebrow(`${n(important.count)} !important declaration${important.count === 1 ? '' : 's'} ·
|
|
762
|
+
<div class="receipts">${eyebrow(`${n(important.count)} !important declaration${important.count === 1 ? '' : 's'} · each one forces a style through`)}
|
|
763
763
|
<div class="chips-row">${important.files.map((f) => `<span class="vchip bad" title="${esc(f.file)}">${esc(basename(f.file))} ×${f.count}</span>`).join('')}</div>
|
|
764
764
|
${whyToggle('important')}</div>` : '';
|
|
765
765
|
const blocks = inline.count >= 5 ? `<div class="fam-rows">${inline.files.slice(0, 6).map((f) => `
|
|
@@ -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.3.
|
|
4
|
+
export const VERSION = '7.3.3';
|
|
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.
|
|
@@ -142,7 +142,7 @@ export function validateContent(content, k) {
|
|
|
142
142
|
const repoArb = (k.tokens.tailwind?.arbitrary ?? []).find((x) => x.value === a.value);
|
|
143
143
|
const priorArb = discount(repoArb?.count, localArb, a.value);
|
|
144
144
|
add('arbitrary-value', 'violation', a.index,
|
|
145
|
-
`
|
|
145
|
+
`Bracket value ${a.value}${priorArb ? `, already written outside the scale ${priorArb}x in this repo` : ''}.`,
|
|
146
146
|
'If the value repeats it is a decision: name it as a token. If it does not, use the nearest scale step.');
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -205,7 +205,7 @@ export function validateContent(content, k) {
|
|
|
205
205
|
}
|
|
206
206
|
for (const imp of got.important) {
|
|
207
207
|
add('important', 'violation', imp.index,
|
|
208
|
-
'!important
|
|
208
|
+
'!important forces a style through instead of fixing the rule that blocked it.',
|
|
209
209
|
'Fix the selector or the source of the conflict instead of shouting over it.');
|
|
210
210
|
}
|
|
211
211
|
|
|
@@ -64,7 +64,7 @@ export function getContext(k, { path = null } = {}) {
|
|
|
64
64
|
? 'SPACING: Tailwind scale only. No arbitrary brackets (p-[13px]); a repeating value is a token, not a bracket.'
|
|
65
65
|
: k.spacingSeen.size
|
|
66
66
|
? `SPACING: this repo has no scale, just ${k.spacingSeen.size} values in circulation. Reuse one rather than inventing value number ${k.spacingSeen.size + 1}; do not treat them as a system.`
|
|
67
|
-
: 'SPACING: nothing on record yet. Whatever you pick
|
|
67
|
+
: 'SPACING: nothing on record yet. Whatever you pick, the next agent will copy; pick something divisible by 4.');
|
|
68
68
|
|
|
69
69
|
const faces = distinctTypefaces(t.fontFamilies ?? []);
|
|
70
70
|
if (faces.length) L.push(`TYPE: ${faces.slice(0, 3).join(', ')}. No new typefaces, no hand-declared font stacks.`);
|
|
@@ -177,7 +177,7 @@ export function findToken(k, { value } = {}) {
|
|
|
177
177
|
}
|
|
178
178
|
const prior = k.spacingSeen.get(v);
|
|
179
179
|
if (prior) return `${v} is already in use ${prior}x in this repo. Consistent, so use it, but know it lives off any scale.`;
|
|
180
|
-
if (!k.spacingSeen.size) return `This repo has no spacing values on record to compare ${v} against. Whatever you pick
|
|
180
|
+
if (!k.spacingSeen.size) return `This repo has no spacing values on record to compare ${v} against. Whatever you pick, the next agent will copy; pick something divisible by 4.`;
|
|
181
181
|
const near = [...k.spacingSeen.entries()].map(([val, count]) => ({ val, count, px: toPxLocal(val) }))
|
|
182
182
|
.filter((x) => x.px !== null).sort((a, b) => Math.abs(a.px - px) - Math.abs(b.px - px))[0];
|
|
183
183
|
return near
|
|
@@ -93,6 +93,25 @@ function resolveAlias(root, wsRoot, alias) {
|
|
|
93
93
|
return null;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The theme contract is the real signature of a shadcn install: the named
|
|
98
|
+
* variables (background, foreground, primary, muted, border, ring...) defined
|
|
99
|
+
* in a stylesheet. Door names and Radix imports are not enough. dubinc/dub
|
|
100
|
+
* has 12 catalogue-named files on Radix and cva, no components.json and 0 of
|
|
101
|
+
* these variables: its own token system, a shadcn ancestry, not a shadcn
|
|
102
|
+
* install. It was read as one from 7.2.0 to 7.3.2 (2026-09-13).
|
|
103
|
+
*/
|
|
104
|
+
const CONTRACT_ROWS = ['background', 'foreground', 'primary', 'muted', 'border', 'ring', 'accent', 'card', 'popover', 'destructive'];
|
|
105
|
+
function themeContract(root, files) {
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
for (const f of (files.styles ?? []).slice(0, 60)) {
|
|
108
|
+
const css = read(join(root, f));
|
|
109
|
+
for (const r of CONTRACT_ROWS) if (!seen.has(r) && new RegExp(`--${r}\\s*:`).test(css)) seen.add(r);
|
|
110
|
+
if (seen.size === CONTRACT_ROWS.length) break;
|
|
111
|
+
}
|
|
112
|
+
return seen.size;
|
|
113
|
+
}
|
|
114
|
+
|
|
96
115
|
/** Rows declared under a selector in a CSS text: name -> value. */
|
|
97
116
|
function rowsUnder(css, selector) {
|
|
98
117
|
const out = new Map();
|
|
@@ -209,11 +228,14 @@ export default {
|
|
|
209
228
|
else { const i = installs.find((x) => x.uiDir === s.dir); i.catalogueNames = Math.max(i.catalogueNames, s.catalogueNames); }
|
|
210
229
|
}
|
|
211
230
|
|
|
212
|
-
// 3. the decision:
|
|
213
|
-
// catalogue
|
|
231
|
+
// 3. the decision. With components.json: the CLI's own marker plus a
|
|
232
|
+
// catalogue folder. Without it: a catalogue by name AND the theme contract
|
|
233
|
+
// in a stylesheet (5 or more of shadcn's named variables); names and
|
|
234
|
+
// Radix alone describe half the React world.
|
|
235
|
+
const contract = themeContract(root, files);
|
|
214
236
|
const withConfigAndDir = installs.filter((i) => i.config && i.uiDir);
|
|
215
237
|
const byName = installs.filter((i) => i.catalogueNames >= 8);
|
|
216
|
-
if (!withConfigAndDir.length && !byName.length) return null;
|
|
238
|
+
if (!withConfigAndDir.length && !(byName.length && contract >= 5)) return null;
|
|
217
239
|
|
|
218
240
|
installs.sort((a, b) => b.catalogueNames - a.catalogueNames || (b.config ? 1 : 0) - (a.config ? 1 : 0));
|
|
219
241
|
const primary = installs[0];
|
|
@@ -229,6 +251,7 @@ export default {
|
|
|
229
251
|
if (i.config) evidence.push(`components.json in ${dirname(i.config) === '.' ? 'the root' : dirname(i.config)}${i.uiDir ? `, ${i.catalogueNames} catalogue component${i.catalogueNames === 1 ? '' : 's'} in ${i.uiDir}` : ', no catalogue folder found through its alias'}`);
|
|
230
252
|
else evidence.push(`${i.catalogueNames} catalogue components in ${i.uiDir}, no components.json`);
|
|
231
253
|
}
|
|
254
|
+
evidence.push(contract ? `${contract} of shadcn's ${CONTRACT_ROWS.length} theme variables defined` : 'no shadcn theme variables found in any stylesheet');
|
|
232
255
|
if (kit?.style) evidence.push(`style ${kit.style}${kit.baseColor ? `, base colour ${kit.baseColor}` : ''}${kit.tailwind ? `, Tailwind ${kit.tailwind}` : ''}`);
|
|
233
256
|
|
|
234
257
|
// 4. write the facts every consumer reads
|
|
@@ -190,7 +190,7 @@ if (neverImported.length >= 3) {
|
|
|
190
190
|
section('Styling discipline');
|
|
191
191
|
rule(`Never write ${reusable.length ? '`style={{ ... }}`' : 'inline `style="..."` attributes'} for static values; styling belongs to classes and tokens where the system can see it.`);
|
|
192
192
|
if ((t.inlineStyles?.count ?? 0) > 0) lines.push(` (${t.inlineStyles.count} static inline block${t.inlineStyles.count === 1 ? '' : 's'} already exist; do not add to them.)`);
|
|
193
|
-
if ((t.important?.count ?? 0) >= 3) rule(`Never write !important; the scan found ${t.important.count} declaration${t.important.count === 1 ? '' : 's'} already. When a style does not apply, fix the selector or the source of the conflict instead of
|
|
193
|
+
if ((t.important?.count ?? 0) >= 3) rule(`Never write !important; the scan found ${t.important.count} declaration${t.important.count === 1 ? '' : 's'} already. When a style does not apply, fix the selector or the source of the conflict instead of forcing the style through.`);
|
|
194
194
|
rule('Before styling anything new, look at a neighbouring component and match how it does it. Consistency with the repo beats personal preference.');
|
|
195
195
|
|
|
196
196
|
lines.push('');
|
|
@@ -29,7 +29,7 @@ const RULES = {
|
|
|
29
29
|
'duplicate-component': { name: 'Duplicated component', desc: 'A component implemented more than once. An agent asked for it has several plausible answers.' },
|
|
30
30
|
'never-imported-component': { name: 'Component nobody imports', desc: 'Defined in the system, imported by nothing. It sits there as a wrong answer waiting to be picked.' },
|
|
31
31
|
'inline-styles': { name: 'Inline style blocks', desc: 'Static values written as style attributes, bypassing the system and teaching the agent to do the same.' },
|
|
32
|
-
'important-styles': { name: '!important declarations', desc: '
|
|
32
|
+
'important-styles': { name: '!important declarations', desc: 'Forces a style through instead of fixing the rule that blocked it. Fix specificity at the source and these stop being necessary.' },
|
|
33
33
|
'stray-colors': { name: 'Hardcoded colours', desc: 'Colour values no token names. Each one is a value the agent will happily copy.' },
|
|
34
34
|
'stale-rule-reference': { name: 'Stale agent-rule reference', desc: 'The agent rules reference something this scan can no longer find. A rule the agent obeys is worse than no rule when the repo has moved on.' },
|
|
35
35
|
};
|