roast-my-design-system 7.3.3 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/package.json +1 -1
- package/skills/roast-my-design-system/scripts/benchmark/benchmark.json +79 -79
- package/skills/roast-my-design-system/scripts/diagnose/index.mjs +138 -30
- package/skills/roast-my-design-system/scripts/diagnose/score.mjs +28 -6
- package/skills/roast-my-design-system/scripts/diagnose/why.mjs +2 -2
- package/skills/roast-my-design-system/scripts/harvest/index.mjs +18 -9
- package/skills/roast-my-design-system/scripts/harvest/paint.mjs +28 -14
- package/skills/roast-my-design-system/scripts/lib/version.mjs +1 -1
- package/skills/roast-my-design-system/scripts/profiles/index.mjs +38 -0
- package/skills/roast-my-design-system/scripts/profiles/shadcn.mjs +18 -3
- package/skills/roast-my-design-system/scripts/rules/build.mjs +6 -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.
|
|
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">
|
|
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.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
|
+
|
|
13
15
|
> **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
16
|
|
|
15
17
|
> **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.
|
|
@@ -20,7 +22,7 @@ A free CLI tool (and Claude Code skill) that roasts your repo's design system wi
|
|
|
20
22
|
|
|
21
23
|
Run it on your codebase and get, in about a second:
|
|
22
24
|
|
|
23
|
-
- **A health score you can defend in a meeting.** 0 to 100, deterministic, and benchmarked against 3 yardsticks: Ideal Design System norms, 34 scanned public repos and 10 reputable design systems (Primer, Polaris, Carbon, shadcn/ui…).
|
|
25
|
+
- **A health score you can defend in a meeting.** It measures how safely an AI agent can build new UI on this repo without going off-system. Everything else on the report is receipts for humans. 0 to 100, deterministic, and benchmarked against 3 yardsticks: Ideal Design System norms, 34 scanned public repos and 10 reputable design systems (Primer, Polaris, Carbon, shadcn/ui…).
|
|
24
26
|
- **Per-package scores for monorepos.** One blended number hides which package is the problem: `packages/ui` scores 80 while `apps/web` scores 40, and now you can see it.
|
|
25
27
|
- **Reads React and web components alike.** Stencil, Lit and custom elements are detected by tag registration and counted by kebab tag. Libraries get composition maps, not adoption accusations. Earned token namespaces are named in the header. Anything the scan can not read is declared "not measured" instead of scored.
|
|
26
28
|
- **The receipts behind it.** Every colour and its near-identical twin. Every spacing value, typeface, duplicated or never-imported component, inline style and !important. All with real file paths, in one self-contained HTML report you can open, Slack or email.
|
|
@@ -77,11 +79,11 @@ Not to be confused with each other: **"Why this matters"** is generic, ships wit
|
|
|
77
79
|
|
|
78
80
|
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
81
|
|
|
80
|
-

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

|
|
85
87
|
|
|
86
88
|
## What makes the numbers trustworthy
|
|
87
89
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.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": [
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"note": "palette colours in own code where a theme variable exists, per 100 files"
|
|
92
92
|
},
|
|
93
93
|
"doorOverrides": {
|
|
94
|
-
"value":
|
|
95
|
-
"note": "shadcn components given a colour
|
|
94
|
+
"value": 2,
|
|
95
|
+
"note": "shadcn components given a colour through className, per 100 files; the tidiest half of 16 shadcn repos sit under this (2026-09-13 audit)"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"stats": {
|
|
@@ -1852,7 +1852,7 @@
|
|
|
1852
1852
|
},
|
|
1853
1853
|
"slices": {
|
|
1854
1854
|
"shadcn": {
|
|
1855
|
-
"builtAt": "2026-09-
|
|
1855
|
+
"builtAt": "2026-09-12T20:07:59.374Z",
|
|
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": {
|
|
@@ -2074,27 +2074,27 @@
|
|
|
2074
2074
|
},
|
|
2075
2075
|
"arbitrary": {
|
|
2076
2076
|
"values": [
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2077
|
+
5,
|
|
2078
|
+
25,
|
|
2079
|
+
41,
|
|
2080
|
+
74,
|
|
2081
|
+
88,
|
|
2082
|
+
99,
|
|
2083
|
+
113,
|
|
2084
|
+
113,
|
|
2085
|
+
148,
|
|
2086
|
+
204,
|
|
2087
|
+
217,
|
|
2088
|
+
360,
|
|
2089
|
+
366,
|
|
2090
|
+
569,
|
|
2091
|
+
1186,
|
|
2092
|
+
1602
|
|
2093
2093
|
],
|
|
2094
|
-
"p25":
|
|
2095
|
-
"median":
|
|
2096
|
-
"p75":
|
|
2097
|
-
"p90":
|
|
2094
|
+
"p25": 85,
|
|
2095
|
+
"median": 131,
|
|
2096
|
+
"p75": 362,
|
|
2097
|
+
"p90": 878
|
|
2098
2098
|
},
|
|
2099
2099
|
"nearPairs": {
|
|
2100
2100
|
"values": [
|
|
@@ -2205,40 +2205,40 @@
|
|
|
2205
2205
|
57,
|
|
2206
2206
|
58,
|
|
2207
2207
|
62,
|
|
2208
|
-
|
|
2208
|
+
66,
|
|
2209
2209
|
100,
|
|
2210
|
-
|
|
2210
|
+
179,
|
|
2211
2211
|
296,
|
|
2212
|
-
|
|
2212
|
+
488
|
|
2213
2213
|
],
|
|
2214
2214
|
"p25": 22,
|
|
2215
2215
|
"median": 50,
|
|
2216
2216
|
"p75": 75,
|
|
2217
|
-
"p90":
|
|
2217
|
+
"p90": 238
|
|
2218
2218
|
},
|
|
2219
2219
|
"doorOverrides": {
|
|
2220
2220
|
"values": [
|
|
2221
2221
|
0,
|
|
2222
|
+
0,
|
|
2223
|
+
0,
|
|
2224
|
+
1,
|
|
2225
|
+
1,
|
|
2226
|
+
1,
|
|
2227
|
+
1,
|
|
2228
|
+
1,
|
|
2229
|
+
2,
|
|
2222
2230
|
3,
|
|
2223
|
-
|
|
2224
|
-
|
|
2231
|
+
4,
|
|
2232
|
+
4,
|
|
2233
|
+
6,
|
|
2225
2234
|
13,
|
|
2226
|
-
15,
|
|
2227
|
-
16,
|
|
2228
|
-
16,
|
|
2229
|
-
17,
|
|
2230
|
-
18,
|
|
2231
|
-
22,
|
|
2232
2235
|
26,
|
|
2233
|
-
|
|
2234
|
-
31,
|
|
2235
|
-
44,
|
|
2236
|
-
55
|
|
2236
|
+
32
|
|
2237
2237
|
],
|
|
2238
|
-
"p25":
|
|
2239
|
-
"median":
|
|
2240
|
-
"p75":
|
|
2241
|
-
"p90":
|
|
2238
|
+
"p25": 1,
|
|
2239
|
+
"median": 2,
|
|
2240
|
+
"p75": 5,
|
|
2241
|
+
"p90": 20
|
|
2242
2242
|
}
|
|
2243
2243
|
},
|
|
2244
2244
|
"repos": [
|
|
@@ -2260,13 +2260,13 @@
|
|
|
2260
2260
|
"shadows": 10,
|
|
2261
2261
|
"exactDuplicates": 134,
|
|
2262
2262
|
"inlineStyles": 79,
|
|
2263
|
-
"arbitrary":
|
|
2263
|
+
"arbitrary": 217,
|
|
2264
2264
|
"nearPairs": 18,
|
|
2265
2265
|
"important": 157,
|
|
2266
2266
|
"neverImported": 99,
|
|
2267
2267
|
"components": 1453,
|
|
2268
2268
|
"paintTin": 22,
|
|
2269
|
-
"doorOverrides":
|
|
2269
|
+
"doorOverrides": 1
|
|
2270
2270
|
}
|
|
2271
2271
|
},
|
|
2272
2272
|
{
|
|
@@ -2287,7 +2287,7 @@
|
|
|
2287
2287
|
"shadows": 1,
|
|
2288
2288
|
"exactDuplicates": 0,
|
|
2289
2289
|
"inlineStyles": 0,
|
|
2290
|
-
"arbitrary":
|
|
2290
|
+
"arbitrary": 41,
|
|
2291
2291
|
"nearPairs": 0,
|
|
2292
2292
|
"important": 1,
|
|
2293
2293
|
"neverImported": 94,
|
|
@@ -2314,7 +2314,7 @@
|
|
|
2314
2314
|
"shadows": 2,
|
|
2315
2315
|
"exactDuplicates": 1,
|
|
2316
2316
|
"inlineStyles": 2,
|
|
2317
|
-
"arbitrary":
|
|
2317
|
+
"arbitrary": 5,
|
|
2318
2318
|
"nearPairs": 0,
|
|
2319
2319
|
"important": 2,
|
|
2320
2320
|
"neverImported": 0,
|
|
@@ -2330,7 +2330,7 @@
|
|
|
2330
2330
|
"baseColor": "neutral",
|
|
2331
2331
|
"tailwind": "4",
|
|
2332
2332
|
"codeFiles": 146,
|
|
2333
|
-
"ownFiles":
|
|
2333
|
+
"ownFiles": 68,
|
|
2334
2334
|
"metrics": {
|
|
2335
2335
|
"colors": 41,
|
|
2336
2336
|
"greys": 0,
|
|
@@ -2341,13 +2341,13 @@
|
|
|
2341
2341
|
"shadows": 4,
|
|
2342
2342
|
"exactDuplicates": 1,
|
|
2343
2343
|
"inlineStyles": 7,
|
|
2344
|
-
"arbitrary":
|
|
2344
|
+
"arbitrary": 74,
|
|
2345
2345
|
"nearPairs": 0,
|
|
2346
2346
|
"important": 9,
|
|
2347
2347
|
"neverImported": 0,
|
|
2348
2348
|
"components": 316,
|
|
2349
|
-
"paintTin":
|
|
2350
|
-
"doorOverrides":
|
|
2349
|
+
"paintTin": 66,
|
|
2350
|
+
"doorOverrides": 1
|
|
2351
2351
|
}
|
|
2352
2352
|
},
|
|
2353
2353
|
{
|
|
@@ -2368,13 +2368,13 @@
|
|
|
2368
2368
|
"shadows": 0,
|
|
2369
2369
|
"exactDuplicates": 1,
|
|
2370
2370
|
"inlineStyles": 11,
|
|
2371
|
-
"arbitrary":
|
|
2371
|
+
"arbitrary": 113,
|
|
2372
2372
|
"nearPairs": 2,
|
|
2373
2373
|
"important": 0,
|
|
2374
2374
|
"neverImported": 100,
|
|
2375
2375
|
"components": 272,
|
|
2376
2376
|
"paintTin": 33,
|
|
2377
|
-
"doorOverrides":
|
|
2377
|
+
"doorOverrides": 4
|
|
2378
2378
|
}
|
|
2379
2379
|
},
|
|
2380
2380
|
{
|
|
@@ -2395,13 +2395,13 @@
|
|
|
2395
2395
|
"shadows": 0,
|
|
2396
2396
|
"exactDuplicates": 16,
|
|
2397
2397
|
"inlineStyles": 74,
|
|
2398
|
-
"arbitrary":
|
|
2398
|
+
"arbitrary": 204,
|
|
2399
2399
|
"nearPairs": 18,
|
|
2400
2400
|
"important": 0,
|
|
2401
2401
|
"neverImported": 44,
|
|
2402
2402
|
"components": 811,
|
|
2403
2403
|
"paintTin": 62,
|
|
2404
|
-
"doorOverrides":
|
|
2404
|
+
"doorOverrides": 6
|
|
2405
2405
|
}
|
|
2406
2406
|
},
|
|
2407
2407
|
{
|
|
@@ -2422,13 +2422,13 @@
|
|
|
2422
2422
|
"shadows": 6,
|
|
2423
2423
|
"exactDuplicates": 61,
|
|
2424
2424
|
"inlineStyles": 24,
|
|
2425
|
-
"arbitrary":
|
|
2425
|
+
"arbitrary": 113,
|
|
2426
2426
|
"nearPairs": 8,
|
|
2427
2427
|
"important": 55,
|
|
2428
2428
|
"neverImported": 2,
|
|
2429
2429
|
"components": 1085,
|
|
2430
2430
|
"paintTin": 296,
|
|
2431
|
-
"doorOverrides":
|
|
2431
|
+
"doorOverrides": 26
|
|
2432
2432
|
}
|
|
2433
2433
|
},
|
|
2434
2434
|
{
|
|
@@ -2449,13 +2449,13 @@
|
|
|
2449
2449
|
"shadows": 0,
|
|
2450
2450
|
"exactDuplicates": 102,
|
|
2451
2451
|
"inlineStyles": 120,
|
|
2452
|
-
"arbitrary":
|
|
2452
|
+
"arbitrary": 148,
|
|
2453
2453
|
"nearPairs": 11,
|
|
2454
2454
|
"important": 0,
|
|
2455
2455
|
"neverImported": 5,
|
|
2456
2456
|
"components": 1137,
|
|
2457
2457
|
"paintTin": 5,
|
|
2458
|
-
"doorOverrides":
|
|
2458
|
+
"doorOverrides": 1
|
|
2459
2459
|
}
|
|
2460
2460
|
},
|
|
2461
2461
|
{
|
|
@@ -2476,13 +2476,13 @@
|
|
|
2476
2476
|
"shadows": 0,
|
|
2477
2477
|
"exactDuplicates": 17,
|
|
2478
2478
|
"inlineStyles": 22,
|
|
2479
|
-
"arbitrary":
|
|
2479
|
+
"arbitrary": 25,
|
|
2480
2480
|
"nearPairs": 1,
|
|
2481
2481
|
"important": 0,
|
|
2482
2482
|
"neverImported": 19,
|
|
2483
2483
|
"components": 700,
|
|
2484
2484
|
"paintTin": 100,
|
|
2485
|
-
"doorOverrides":
|
|
2485
|
+
"doorOverrides": 1
|
|
2486
2486
|
}
|
|
2487
2487
|
},
|
|
2488
2488
|
{
|
|
@@ -2492,7 +2492,7 @@
|
|
|
2492
2492
|
"baseColor": "gray",
|
|
2493
2493
|
"tailwind": "3",
|
|
2494
2494
|
"codeFiles": 1379,
|
|
2495
|
-
"ownFiles":
|
|
2495
|
+
"ownFiles": 534,
|
|
2496
2496
|
"metrics": {
|
|
2497
2497
|
"colors": 66,
|
|
2498
2498
|
"greys": 14,
|
|
@@ -2503,13 +2503,13 @@
|
|
|
2503
2503
|
"shadows": 1,
|
|
2504
2504
|
"exactDuplicates": 20,
|
|
2505
2505
|
"inlineStyles": 47,
|
|
2506
|
-
"arbitrary":
|
|
2506
|
+
"arbitrary": 360,
|
|
2507
2507
|
"nearPairs": 7,
|
|
2508
2508
|
"important": 42,
|
|
2509
2509
|
"neverImported": 13,
|
|
2510
2510
|
"components": 777,
|
|
2511
|
-
"paintTin":
|
|
2512
|
-
"doorOverrides":
|
|
2511
|
+
"paintTin": 488,
|
|
2512
|
+
"doorOverrides": 32
|
|
2513
2513
|
}
|
|
2514
2514
|
},
|
|
2515
2515
|
{
|
|
@@ -2519,7 +2519,7 @@
|
|
|
2519
2519
|
"baseColor": "slate",
|
|
2520
2520
|
"tailwind": "3",
|
|
2521
2521
|
"codeFiles": 2120,
|
|
2522
|
-
"ownFiles":
|
|
2522
|
+
"ownFiles": 350,
|
|
2523
2523
|
"metrics": {
|
|
2524
2524
|
"colors": 254,
|
|
2525
2525
|
"greys": 52,
|
|
@@ -2530,13 +2530,13 @@
|
|
|
2530
2530
|
"shadows": 0,
|
|
2531
2531
|
"exactDuplicates": 21,
|
|
2532
2532
|
"inlineStyles": 5,
|
|
2533
|
-
"arbitrary":
|
|
2533
|
+
"arbitrary": 99,
|
|
2534
2534
|
"nearPairs": 21,
|
|
2535
2535
|
"important": 9,
|
|
2536
2536
|
"neverImported": 0,
|
|
2537
2537
|
"components": 911,
|
|
2538
|
-
"paintTin":
|
|
2539
|
-
"doorOverrides":
|
|
2538
|
+
"paintTin": 179,
|
|
2539
|
+
"doorOverrides": 3
|
|
2540
2540
|
}
|
|
2541
2541
|
},
|
|
2542
2542
|
{
|
|
@@ -2557,13 +2557,13 @@
|
|
|
2557
2557
|
"shadows": 0,
|
|
2558
2558
|
"exactDuplicates": 25,
|
|
2559
2559
|
"inlineStyles": 42,
|
|
2560
|
-
"arbitrary":
|
|
2560
|
+
"arbitrary": 88,
|
|
2561
2561
|
"nearPairs": 0,
|
|
2562
2562
|
"important": 0,
|
|
2563
2563
|
"neverImported": 1,
|
|
2564
2564
|
"components": 246,
|
|
2565
2565
|
"paintTin": 42,
|
|
2566
|
-
"doorOverrides":
|
|
2566
|
+
"doorOverrides": 4
|
|
2567
2567
|
}
|
|
2568
2568
|
},
|
|
2569
2569
|
{
|
|
@@ -2584,13 +2584,13 @@
|
|
|
2584
2584
|
"shadows": 0,
|
|
2585
2585
|
"exactDuplicates": 14,
|
|
2586
2586
|
"inlineStyles": 47,
|
|
2587
|
-
"arbitrary":
|
|
2587
|
+
"arbitrary": 366,
|
|
2588
2588
|
"nearPairs": 10,
|
|
2589
2589
|
"important": 12,
|
|
2590
2590
|
"neverImported": 0,
|
|
2591
2591
|
"components": 1204,
|
|
2592
2592
|
"paintTin": 21,
|
|
2593
|
-
"doorOverrides":
|
|
2593
|
+
"doorOverrides": 0
|
|
2594
2594
|
}
|
|
2595
2595
|
},
|
|
2596
2596
|
{
|
|
@@ -2611,13 +2611,13 @@
|
|
|
2611
2611
|
"shadows": 10,
|
|
2612
2612
|
"exactDuplicates": 65,
|
|
2613
2613
|
"inlineStyles": 58,
|
|
2614
|
-
"arbitrary":
|
|
2614
|
+
"arbitrary": 569,
|
|
2615
2615
|
"nearPairs": 16,
|
|
2616
2616
|
"important": 42,
|
|
2617
2617
|
"neverImported": 30,
|
|
2618
2618
|
"components": 1494,
|
|
2619
2619
|
"paintTin": 58,
|
|
2620
|
-
"doorOverrides":
|
|
2620
|
+
"doorOverrides": 2
|
|
2621
2621
|
}
|
|
2622
2622
|
},
|
|
2623
2623
|
{
|
|
@@ -2638,13 +2638,13 @@
|
|
|
2638
2638
|
"shadows": 2,
|
|
2639
2639
|
"exactDuplicates": 104,
|
|
2640
2640
|
"inlineStyles": 117,
|
|
2641
|
-
"arbitrary":
|
|
2641
|
+
"arbitrary": 1186,
|
|
2642
2642
|
"nearPairs": 21,
|
|
2643
2643
|
"important": 0,
|
|
2644
2644
|
"neverImported": 26,
|
|
2645
2645
|
"components": 971,
|
|
2646
2646
|
"paintTin": 32,
|
|
2647
|
-
"doorOverrides":
|
|
2647
|
+
"doorOverrides": 1
|
|
2648
2648
|
}
|
|
2649
2649
|
},
|
|
2650
2650
|
{
|
|
@@ -2665,13 +2665,13 @@
|
|
|
2665
2665
|
"shadows": 14,
|
|
2666
2666
|
"exactDuplicates": 288,
|
|
2667
2667
|
"inlineStyles": 263,
|
|
2668
|
-
"arbitrary":
|
|
2668
|
+
"arbitrary": 1602,
|
|
2669
2669
|
"nearPairs": 59,
|
|
2670
2670
|
"important": 93,
|
|
2671
2671
|
"neverImported": 23,
|
|
2672
2672
|
"components": 3630,
|
|
2673
2673
|
"paintTin": 8,
|
|
2674
|
-
"doorOverrides":
|
|
2674
|
+
"doorOverrides": 0
|
|
2675
2675
|
}
|
|
2676
2676
|
}
|
|
2677
2677
|
]
|
|
@@ -31,8 +31,8 @@ import { feedbackUrl, FEEDBACK_ASK, FEEDBACK_CTA } from '../lib/feedback.mjs';
|
|
|
31
31
|
import { fixPrompt } from '../lib/fixprompt.mjs';
|
|
32
32
|
import { WHY } from './why.mjs';
|
|
33
33
|
import { parseColor, luminance, isGrey } from '../lib/color.mjs';
|
|
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 } from '../profiles/index.mjs';
|
|
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';
|
|
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.
|
|
@@ -234,7 +234,7 @@ const typefaces = distinctTypefaces(fontFamilies);
|
|
|
234
234
|
const { colorTokens, colorStrays, tokenLed, greyStrays } = M;
|
|
235
235
|
|
|
236
236
|
// Arbitrary bracket values (p-[13px], text-[10px]) — scale erosion, counted.
|
|
237
|
-
|
|
237
|
+
let arbitrary = h.tokens.tailwind?.arbitrary ?? [];
|
|
238
238
|
const arbitraryCount = M.arbitrary;
|
|
239
239
|
|
|
240
240
|
// !important declarations: the cascade admitting defeat.
|
|
@@ -248,6 +248,10 @@ const nearPairs = nearColorPairs(colors);
|
|
|
248
248
|
const collisions = h.tokens.tokenCollisions ?? [];
|
|
249
249
|
|
|
250
250
|
const P = profileOf(h);
|
|
251
|
+
// On a shadcn repo the bracket list is the team's own; installed brackets are
|
|
252
|
+
// named in the side panel instead (see coreMetrics for the count).
|
|
253
|
+
if (P.isShadcn) arbitrary = splitArbitrary(arbitrary, installedDirs(P)).own;
|
|
254
|
+
const breakdown = scoreBreakdown(h, bench);
|
|
251
255
|
const neverImported = neverImportedComponents(h.components, P.uiDir);
|
|
252
256
|
|
|
253
257
|
// Measurability and kind, decided once in profiles/ (telekom/scale, 2026-09-01):
|
|
@@ -339,7 +343,7 @@ const FALLBACK_TARGET = {
|
|
|
339
343
|
colors: 'a system needs ~24', greys: 'a scale has up to 13', spacing: 'a dozen deliberate exceptions',
|
|
340
344
|
exactDuplicates: 'should be 0', inlineStyles: 'invisible to any system', nearPairs: '1 colour recorded twice',
|
|
341
345
|
important: 'forces a style through', neverImported: 'the system nobody found', arbitrary: 'a handful of deliberate exceptions',
|
|
342
|
-
paintTin: 'the theme file already has a variable for it', doorOverrides: 'use a variant
|
|
346
|
+
paintTin: 'the theme file already has a variable for it', doorOverrides: 'use a variant, or add one',
|
|
343
347
|
};
|
|
344
348
|
// Dress one judged tile for the page: formatted number, comparison rows.
|
|
345
349
|
// Accepts a judged tile from score.mjs, or the positional form the
|
|
@@ -549,7 +553,9 @@ function collisionsTrap() {
|
|
|
549
553
|
return trapBox(`--${esc(top.name)} is ${where(a)}, and ${where(b)}. One name, two colours, and nothing in the code says which is the real one. An agent reads whichever package it opened, so the brand depends on where it started.${rest}`);
|
|
550
554
|
}
|
|
551
555
|
function orphansTrap() {
|
|
552
|
-
|
|
556
|
+
// Catalogue stock is protective, not a trap: an agent reaches for it
|
|
557
|
+
// instead of building its own version (Sahaj Jain, 2026-09-09).
|
|
558
|
+
if (!componentsMeasured || isLibrary || vendoredUi) return '';
|
|
553
559
|
if (neverImported.length < 10) return '';
|
|
554
560
|
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
561
|
}
|
|
@@ -607,7 +613,8 @@ function sheetSection() {
|
|
|
607
613
|
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>`);
|
|
608
614
|
parts.push(whyToggle('paintTin'));
|
|
609
615
|
const doorChips = (paint.doors.samples ?? []).slice(0, 6).map((s) => `<span class="vchip bad">${esc(s.value)} ×${s.count}</span>`).join('');
|
|
610
|
-
|
|
616
|
+
const typoChips = (paint.typo?.samples ?? []).slice(0, 6).map((s) => `<span class="vchip">${esc(s.value)} ×${s.count}</span>`).join('');
|
|
617
|
+
parts.push(`<div class="receipts">${eyebrow(`${n(paint.doors.uses)} shadcn components recoloured 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 through className. Variants are doing their job.</p>'}${paint.typo?.uses ? `<p class="sub" style="margin-top:10px">Shown, not scored: ${n(paint.typo.uses)} shadcn components given a text size or weight through className, ${n(paint.typo.per100)} per 100 files. shadcn asks for a variant here too, but every shadcn repo does this at the same rate, so it says nothing about yours.</p><div class="chips-row">${typoChips}</div>` : ''}</div>`);
|
|
611
618
|
parts.push(whyToggle('doorOverrides'));
|
|
612
619
|
}
|
|
613
620
|
if (!parts.length) return '';
|
|
@@ -975,9 +982,15 @@ function extraSectionsHtml() {
|
|
|
975
982
|
// buttons hold, published in --summary so the MCP server's roast-fix prompt
|
|
976
983
|
// hands out byte-identical text. One composer, two doors.
|
|
977
984
|
let startMoves = [];
|
|
985
|
+
let startProjection = null;
|
|
978
986
|
|
|
979
987
|
function whereToStartSection() {
|
|
980
988
|
const c = [];
|
|
989
|
+
if (P.isShadcn && (P.shadcn?.sheet?.customUnused ?? []).length) {
|
|
990
|
+
const dead = P.shadcn.sheet.customUnused;
|
|
991
|
+
c.push({ score: 40, metric: null, title: `Delete the ${dead.length} theme variable${dead.length === 1 ? '' : 's'} nothing uses`,
|
|
992
|
+
sub: `${dead.slice(0, 3).map((r) => `--${esc(r)}`).join(', ')}${dead.length > 3 ? ' and more' : ''} sit in ${esc(P.shadcn.sheet.file)} next to the real theme, and nothing in the repo reads them. An agent opening that file sees 2 colour systems and can not tell which one is dead. ${dead.length * 2} lines to delete; the cheapest fix on this page.` });
|
|
993
|
+
}
|
|
981
994
|
if (agentFiles.length === 0) c.push({ score: 60, metric: null, title: 'Write the agent rules file',
|
|
982
995
|
sub: `No CLAUDE.md, no AGENTS.md. One page naming the canonical components and the tokens file stops your agent guessing on every UI change. Cheapest fix on this list.` });
|
|
983
996
|
if (componentsMeasured && hardDupes.length > 0) {
|
|
@@ -1075,8 +1088,8 @@ function whereToStartSection() {
|
|
|
1075
1088
|
if (pt.doors.uses >= 5) {
|
|
1076
1089
|
const s0 = pt.doors.samples[0];
|
|
1077
1090
|
c.push({ score: 15 + pt.doors.per100 / 4, metric: 'doorOverrides', after: 0,
|
|
1078
|
-
title: `Stop
|
|
1079
|
-
sub: `${n(pt.doors.uses)} shadcn components receive a colour
|
|
1091
|
+
title: `Stop recolouring shadcn components through className`,
|
|
1092
|
+
sub: `${n(pt.doors.uses)} shadcn components receive a colour through className, like ${esc(s0.value)}. Use the variant that does it, or add one to the component you own. className on a shadcn component is for layout only.` });
|
|
1080
1093
|
}
|
|
1081
1094
|
}
|
|
1082
1095
|
|
|
@@ -1100,6 +1113,7 @@ function whereToStartSection() {
|
|
|
1100
1113
|
const applied = new Map();
|
|
1101
1114
|
for (const item of top3) if (item.metric && item.after !== undefined && item.delta > 0) applied.set(item.metric, item.after);
|
|
1102
1115
|
const after = projectedScore(applied);
|
|
1116
|
+
startProjection = { count: top3.length, after };
|
|
1103
1117
|
const words = ['One tweak', 'Two tweaks', 'Three tweaks'][top3.length - 1];
|
|
1104
1118
|
const head = healthScore !== null && after > healthScore
|
|
1105
1119
|
? `${words} · <b class="proj">${healthScore} → ${after}</b>`
|
|
@@ -1265,9 +1279,7 @@ function shadcnReceipt() {
|
|
|
1265
1279
|
].filter(Boolean);
|
|
1266
1280
|
const unmatched = (k?.fellBack ?? []).filter((f) => f === 'theme' || f === 'chartColor');
|
|
1267
1281
|
const fell = unmatched.length ? ` · ${unmatched.length === 2 ? 'accent and chart colour' : unmatched[0] === 'theme' ? 'accent' : 'chart colour'} not matched to a named shadcn theme` : '';
|
|
1268
|
-
|
|
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}`;
|
|
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>`;
|
|
1271
1283
|
}
|
|
1272
1284
|
|
|
1273
1285
|
// User exclusions are printed in the header, never hidden: a scoped scan must
|
|
@@ -1286,6 +1298,85 @@ function exclusionsLine() {
|
|
|
1286
1298
|
return `<div class="excl">${parts.join(' · ')} · ${n(total)} files kept out of this scan</div>`;
|
|
1287
1299
|
}
|
|
1288
1300
|
|
|
1301
|
+
|
|
1302
|
+
// ---------- the side panel (fixed left column, 2026-09 template) ----------
|
|
1303
|
+
// Score, the definition it measures, the stack, how the repo was read, the
|
|
1304
|
+
// index of what rendered, and what is not the team's and not counted.
|
|
1305
|
+
function sidePanel() {
|
|
1306
|
+
const date = esc((h.harvestedAt ?? '').slice(0, 10));
|
|
1307
|
+
const def = 'How safely an AI agent can build on this repo without going off-system.';
|
|
1308
|
+
const lift = startProjection && healthScore !== null && startProjection.after > healthScore
|
|
1309
|
+
? ` <b>${['One fix lifts', 'Two fixes lift', 'Three fixes lift'][startProjection.count - 1] ?? 'The fixes lift'} it to ${startProjection.after}.</b>` : '';
|
|
1310
|
+
const rp = P.shadcn?.registryPaint ?? null;
|
|
1311
|
+
const bd = breakdown.ownScore !== null && breakdown.installedPoints > 0 && rp
|
|
1312
|
+
? `<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
|
+
const scoreBlock = healthScore !== null
|
|
1314
|
+
? `<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>`;
|
|
1316
|
+
const facts = [
|
|
1317
|
+
shadcnReceipt(),
|
|
1318
|
+
exclusionsLine(),
|
|
1319
|
+
commissionedBy ? `<div class="excl">Commissioned by <b>${esc(commissionedBy)}</b></div>` : '',
|
|
1320
|
+
].join('');
|
|
1321
|
+
return `<aside class="side">
|
|
1322
|
+
<div class="eyebrow">Design-system diagnosis</div>
|
|
1323
|
+
<div class="repo"><span class="mono">${esc(repoName)}</span></div>
|
|
1324
|
+
<div class="scanned">Scanned ${date} · ${n(h.files.code)} code files · ${h.tookMs}ms</div>
|
|
1325
|
+
${scoreBlock}
|
|
1326
|
+
${chips}
|
|
1327
|
+
<div class="facts">${facts}</div>
|
|
1328
|
+
<div class="eyebrow idx-head">Index</div>
|
|
1329
|
+
<nav class="idx" id="idx"></nav>
|
|
1330
|
+
${exceptionsBlock()}
|
|
1331
|
+
<div class="side-foot">roast-my-design-system ${VERSION}</div>
|
|
1332
|
+
</aside>`;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// What is not the team's, and how it is treated. Every exclusion is named
|
|
1336
|
+
// here with what it carries and the honest caveat: the agent reads it anyway.
|
|
1337
|
+
function exceptionsBlock() {
|
|
1338
|
+
const lines = [];
|
|
1339
|
+
if (P.isShadcn) {
|
|
1340
|
+
const ai = P.shadcn?.arbitraryInstalled;
|
|
1341
|
+
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.`);
|
|
1342
|
+
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
|
+
const rp = P.shadcn?.registryPaint;
|
|
1344
|
+
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
|
+
} else if (isLibrary) {
|
|
1346
|
+
lines.push('A library: unused components are internal-only, and downstream consumers are invisible from here.');
|
|
1347
|
+
}
|
|
1348
|
+
if (!lines.length) return '';
|
|
1349
|
+
return `<div class="excs"><b>Not yours, and not counted</b>${lines.map((l) => `<p>${l}</p>`).join('')}</div>`;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// The index is built from what rendered: every section with a heading gets
|
|
1353
|
+
// a stable id, in page order, and the side panel lists them. Nothing here is
|
|
1354
|
+
// hand-maintained, so a section that did not render is never listed.
|
|
1355
|
+
function addIndex(page) {
|
|
1356
|
+
// heading text is already HTML-escaped; strip tags and leading counts only
|
|
1357
|
+
const short = (t) => t.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim().replace(/^[\d,]+ /, '')
|
|
1358
|
+
.replace(/^What your AI agent sees today$/, 'What your agent sees').replace(/^You sat through the roast$/, 'Your present')
|
|
1359
|
+
.replace(/^The shadcn theme and the 2 shadcn checks$/, 'shadcn theme and checks').replace(/, declared .*$/, '')
|
|
1360
|
+
.replace(/^off-scale spacing values$/, 'Off-scale spacing').replace(/^inline style blocks?$/, 'Inline styles').replace(/^typefaces?$/, 'Typefaces');
|
|
1361
|
+
const entries = [];
|
|
1362
|
+
let i = 0;
|
|
1363
|
+
const body = page.replace(/<section\b([^>]*)>([\s\S]*?)(?=<section\b|<footer>|<\/main>)/g, (m, attrs, inner) => {
|
|
1364
|
+
const h2 = inner.match(/<h2>([\s\S]*?)<\/h2>/);
|
|
1365
|
+
if (!h2) return m;
|
|
1366
|
+
i += 1;
|
|
1367
|
+
let id = (attrs.match(/\bid="([^"]+)"/) ?? [])[1];
|
|
1368
|
+
let open = attrs;
|
|
1369
|
+
if (!id) { id = `s-${i}`; open = ` id="${id}"${attrs}`; }
|
|
1370
|
+
entries.push({ id, text: short(h2[1]) });
|
|
1371
|
+
return `<section${open}>${inner}`;
|
|
1372
|
+
});
|
|
1373
|
+
const nav = entries.map((e, k) => `<a href="#${e.id}"><i>${String(k + 1).padStart(2, '0')}</i>${e.text}</a>`).join('');
|
|
1374
|
+
return body.replace('<nav class="idx" id="idx"></nav>', `<nav class="idx" id="idx">${nav}</nav>`);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Rendered first: the side panel quotes its projection.
|
|
1378
|
+
const whereToStartHtml = whereToStartSection();
|
|
1379
|
+
|
|
1289
1380
|
const html = `<!doctype html>
|
|
1290
1381
|
<html lang="en" data-theme="${themeName}"><head><meta charset="utf-8">
|
|
1291
1382
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -1302,7 +1393,34 @@ const html = `<!doctype html>
|
|
|
1302
1393
|
/* fixed gradient wash on its own layer: background-attachment:fixed +
|
|
1303
1394
|
backdrop-filter makes Chromium skip repaints on scroll */
|
|
1304
1395
|
body::before { content:''; position:fixed; inset:0; z-index:-1; background:var(--page-grad), var(--bg); }
|
|
1305
|
-
.wrap {
|
|
1396
|
+
.wrap { zoom:0.9; }
|
|
1397
|
+
/* two columns: a fixed side panel and the scrolling main column */
|
|
1398
|
+
.side { position:fixed; left:0; top:0; bottom:0; width:300px; padding:22px 24px; overflow:auto; z-index:5;
|
|
1399
|
+
border-right:1px solid var(--line); background:var(--card-solid, var(--bg)); }
|
|
1400
|
+
.main { margin-left:300px; padding:22px 40px 60px; max-width:1080px; }
|
|
1401
|
+
.side .repo { font:600 20px/1.25 var(--disp); letter-spacing:-.01em; margin:4px 0 4px; word-break:break-all; }
|
|
1402
|
+
.side .repo .mono { font-size:17px; }
|
|
1403
|
+
.side .scanned { font:500 11.5px/1.5 var(--sans); color:var(--dim); margin:0 0 16px; }
|
|
1404
|
+
.side .score { text-align:left; margin:0 0 16px; }
|
|
1405
|
+
.side .score .def { font:500 11.5px/1.5 var(--sans); color:var(--dim); margin-top:8px; }
|
|
1406
|
+
.side .score .def b { color:var(--text); font-weight:700; }
|
|
1407
|
+
.side .score .bd { font:500 11.5px/1.5 var(--sans); color:var(--dim); margin-top:8px; padding-top:8px; border-top:1px solid var(--line); }
|
|
1408
|
+
.side .score .bd b { color:var(--text); }
|
|
1409
|
+
.side .chips { margin:0 0 14px; gap:5px; }
|
|
1410
|
+
.side .facts { font:500 11.5px/1.55 var(--sans); color:var(--dim); margin-bottom:16px; }
|
|
1411
|
+
.side .facts .excl { margin-top:6px; font-size:11.5px; }
|
|
1412
|
+
.side .facts b { color:var(--text); font-weight:600; }
|
|
1413
|
+
.side .idx-head { margin-bottom:4px; }
|
|
1414
|
+
.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); }
|
|
1415
|
+
.side .idx a:hover, .side .idx a.on { color:var(--accent); border-left-color:var(--accent); }
|
|
1416
|
+
.side .idx a i { font-style:normal; font-size:10.5px; margin-right:8px; opacity:.6; }
|
|
1417
|
+
.side .excs { margin-top:16px; padding-top:12px; border-top:1px solid var(--line); font:500 11.5px/1.55 var(--sans); color:var(--dim); }
|
|
1418
|
+
.side .excs b { color:var(--text); display:block; margin-bottom:4px; }
|
|
1419
|
+
.side .excs p { margin:0 0 6px; }
|
|
1420
|
+
.side .side-foot { margin-top:18px; font:500 11px/1.5 var(--sans); color:var(--dim2, var(--dim)); }
|
|
1421
|
+
.main > .verdict-card { margin-top:0; }
|
|
1422
|
+
@media (max-width: 900px) { .side { position:static; width:auto; border-right:0; border-bottom:1px solid var(--line); } .main { margin-left:0; padding:20px; } }
|
|
1423
|
+
@media print { .side { position:static; width:auto; border-right:0; } .main { margin-left:0; } .wrap { zoom:1; } }
|
|
1306
1424
|
|
|
1307
1425
|
.glass { background:var(--card); backdrop-filter:blur(24px) saturate(140%); -webkit-backdrop-filter:blur(24px) saturate(140%);
|
|
1308
1426
|
border:1px solid var(--line); border-radius:16px;
|
|
@@ -1344,8 +1462,8 @@ const html = `<!doctype html>
|
|
|
1344
1462
|
|
|
1345
1463
|
.chips { display:flex; flex-wrap:wrap; gap:8px; margin-top:18px; }
|
|
1346
1464
|
.chip { border:1px solid var(--line); background:var(--card); border-radius:99px;
|
|
1347
|
-
padding:
|
|
1348
|
-
.chip-agent {
|
|
1465
|
+
padding:3px 8px; font:500 10.5px/1.5 var(--sans); color:var(--text); }
|
|
1466
|
+
.chip-agent { font-weight:600; }
|
|
1349
1467
|
.coverage { margin-top:12px; }
|
|
1350
1468
|
.cov-yes { color:var(--ok); font-weight:700; }
|
|
1351
1469
|
.cov-no { color:var(--coral); font-weight:700; }
|
|
@@ -1681,32 +1799,20 @@ const html = `<!doctype html>
|
|
|
1681
1799
|
</button>
|
|
1682
1800
|
<script>try{var t=localStorage.getItem('roast-theme');if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t)}catch(e){}</script>
|
|
1683
1801
|
<div class="wrap">
|
|
1684
|
-
|
|
1685
|
-
<
|
|
1686
|
-
<div class="hero">
|
|
1687
|
-
<div>
|
|
1688
|
-
<div class="kicker"><span class="dot"></span>Scan complete · ${h.tookMs}ms · ${n(h.files.code)} code files</div>
|
|
1689
|
-
<h1>Design System Diagnosis</h1>
|
|
1690
|
-
<div class="meta"><span class="mono">${esc(repoName)}</span> · scanned ${esc((h.harvestedAt ?? '').slice(0, 10))}${commissionedBy ? ` · commissioned by ${esc(commissionedBy)}` : ''}</div>
|
|
1691
|
-
</div>
|
|
1692
|
-
${healthScore !== null ? `<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 below</div>' : ''}</div>` : ''}
|
|
1693
|
-
</div>
|
|
1694
|
-
<div class="chips">${stack.map((s) => `<span class="chip">${esc(s)}</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>
|
|
1695
|
-
${shadcnReceipt()}
|
|
1696
|
-
${exclusionsLine()}
|
|
1802
|
+
${sidePanel()}
|
|
1803
|
+
<main class="main">
|
|
1697
1804
|
${noSystemLikely ? `<div class="nods">${ICONS.warn}<span>There is most likely <b>no design system in this repo</b>: almost no colour or spacing values were found. Styling may live outside this codebase (CDN stylesheets, a parent repo, or generated output).</span></div>` : ''}
|
|
1698
1805
|
<div class="glass verdict-card">
|
|
1699
1806
|
<div class="blob b1"></div><div class="blob b2"></div>
|
|
1700
1807
|
${eyebrow('Summary')}
|
|
1701
1808
|
<div class="verdict">${esc(verdict)}</div>
|
|
1702
1809
|
</div>
|
|
1703
|
-
</header>
|
|
1704
1810
|
|
|
1705
1811
|
${notesSection()}
|
|
1706
1812
|
|
|
1707
1813
|
${extraSectionsHtml()}
|
|
1708
1814
|
|
|
1709
|
-
${
|
|
1815
|
+
${whereToStartHtml}
|
|
1710
1816
|
|
|
1711
1817
|
${agentSection()}
|
|
1712
1818
|
|
|
@@ -1737,6 +1843,7 @@ ${componentsSection()}
|
|
|
1737
1843
|
</div>
|
|
1738
1844
|
<span class="creds"><span>Non-destructive scan</span><span>Read-only</span><span>Paths are real</span></span>
|
|
1739
1845
|
</footer>
|
|
1846
|
+
</main>
|
|
1740
1847
|
<script>
|
|
1741
1848
|
(function(){
|
|
1742
1849
|
var gift=document.getElementById('gift'); if(!gift) return;
|
|
@@ -1831,7 +1938,7 @@ ${componentsSection()}
|
|
|
1831
1938
|
</script>
|
|
1832
1939
|
</div></body></html>`;
|
|
1833
1940
|
|
|
1834
|
-
writeFileSync(outPath, html);
|
|
1941
|
+
writeFileSync(outPath, addIndex(html));
|
|
1835
1942
|
|
|
1836
1943
|
// Machine-readable summary for wrappers (the npx CLI reads this instead of
|
|
1837
1944
|
// parsing the HTML): --summary <path> writes score, verdict and per-tile health.
|
|
@@ -1849,6 +1956,7 @@ if (summaryPath) {
|
|
|
1849
1956
|
...(extraSections.length ? { sectionsEmbedded: extraSections.map((s) => s.title) } : {}),
|
|
1850
1957
|
...(h.exclusions ? { exclusions: h.exclusions } : {}),
|
|
1851
1958
|
score: healthScore,
|
|
1959
|
+
...(breakdown.ownScore !== null ? { ownCodeScore: breakdown.ownScore, installedPoints: breakdown.installedPoints } : {}),
|
|
1852
1960
|
noSystemLikely,
|
|
1853
1961
|
verdict,
|
|
1854
1962
|
role: P.role,
|
|
@@ -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 } from '../profiles/index.mjs';
|
|
29
|
+
import { profileOf, installedDirs, splitArbitrary } from '../profiles/index.mjs';
|
|
30
30
|
|
|
31
31
|
export { SCHEMA_VERSION };
|
|
32
32
|
|
|
@@ -102,7 +102,7 @@ export function makeHealthOf(b) {
|
|
|
102
102
|
export const PROFILE_TILES = {
|
|
103
103
|
shadcn: [
|
|
104
104
|
['paintTin', 'off-theme colours per 100 files'],
|
|
105
|
-
['doorOverrides', 'components
|
|
105
|
+
['doorOverrides', 'components recoloured from outside per 100 files'],
|
|
106
106
|
],
|
|
107
107
|
};
|
|
108
108
|
export const tilesFor = (kind) => [...TILES, ...(PROFILE_TILES[kind] ?? [])];
|
|
@@ -124,7 +124,14 @@ export const TILES = [
|
|
|
124
124
|
* The numbers every judgement rests on, read once from a harvest. Plain
|
|
125
125
|
* integers and flags; the same formulas the per-package pass uses.
|
|
126
126
|
*/
|
|
127
|
-
export function coreMetrics(h) {
|
|
127
|
+
export function coreMetrics(h, opts = {}) {
|
|
128
|
+
const P = profileOf(h);
|
|
129
|
+
// On a shadcn repo the bracket count is the team's own: brackets inside the
|
|
130
|
+
// catalogue, kit blocks and registries are installed choices, not drift.
|
|
131
|
+
const arbitraryEntries = P.isShadcn ? splitArbitrary(h.tokens?.tailwind?.arbitrary ?? [], installedDirs(P)).own : (h.tokens?.tailwind?.arbitrary ?? []);
|
|
132
|
+
// ownCode: the breakdown under the score. Same metrics, paint counted on
|
|
133
|
+
// own code only, so the difference is what installed registries cost.
|
|
134
|
+
const paint = opts.ownCode ? (P.shadcn?.paintOwn ?? P.shadcn?.paint) : P.shadcn?.paint;
|
|
128
135
|
const colors = h.tokens?.colors ?? [];
|
|
129
136
|
const greys = colors.filter((c) => isGrey(c.value));
|
|
130
137
|
const colorTokens = colors.filter((c) => c.isToken).length;
|
|
@@ -151,7 +158,7 @@ export function coreMetrics(h) {
|
|
|
151
158
|
nearPairs: nearColorPairs(colors).length,
|
|
152
159
|
important: h.tokens?.important?.count ?? 0,
|
|
153
160
|
neverImported: neverImportedComponents(h.components, profileOf(h).uiDir).length,
|
|
154
|
-
arbitrary:
|
|
161
|
+
arbitrary: arbitraryEntries.reduce((sum, a) => sum + a.count, 0),
|
|
155
162
|
// Measurability and kind, decided once in profiles/: an unreadable stack
|
|
156
163
|
// takes no score credit; a library's orphans are shown, not judged; a
|
|
157
164
|
// vendored shadcn catalogue is stock on a shelf, not abandonment.
|
|
@@ -163,11 +170,26 @@ export function coreMetrics(h) {
|
|
|
163
170
|
// utility-class mode (components.json cssVariables: false): the palette IS
|
|
164
171
|
// the theme by design, so a palette class is not paint from a tin
|
|
165
172
|
utilityPalette: profileOf(h).designSystem?.cssVariables === false,
|
|
166
|
-
paintTin:
|
|
167
|
-
doorOverrides:
|
|
173
|
+
paintTin: paint?.tin?.per100 ?? 0,
|
|
174
|
+
doorOverrides: paint?.doors?.per100 ?? 0,
|
|
168
175
|
};
|
|
169
176
|
}
|
|
170
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The score as the agent meets the repo, and the score own code alone would
|
|
180
|
+
* earn. The difference is what installed code the team did not write costs.
|
|
181
|
+
* ownScore is null when nothing installed carries any weight (no registries).
|
|
182
|
+
*/
|
|
183
|
+
export function scoreBreakdown(h, bench = loadBenchmark()) {
|
|
184
|
+
const b = benchHelpers(bench, profileOf(h).kind);
|
|
185
|
+
const healthOf = makeHealthOf(b);
|
|
186
|
+
const all = scoreOfTiles(tileHealths(coreMetrics(h), healthOf));
|
|
187
|
+
const P = profileOf(h);
|
|
188
|
+
if (!P.isShadcn || !(P.shadcn?.registryDirs ?? []).length) return { score: all, ownScore: null, installedPoints: 0 };
|
|
189
|
+
const own = scoreOfTiles(tileHealths(coreMetrics(h, { ownCode: true }), healthOf));
|
|
190
|
+
return { score: all, ownScore: own, installedPoints: own === null || all === null ? 0 : own - all };
|
|
191
|
+
}
|
|
192
|
+
|
|
171
193
|
/**
|
|
172
194
|
* Judge the nine tiles from a metric set. Each tile carries the value shown,
|
|
173
195
|
* the value judged (strays for token-led colour tiles), and its health.
|
|
@@ -16,9 +16,9 @@ export const WHY = {
|
|
|
16
16
|
'The ideal is 25 per 100 files. That is where the tidiest third of 15 shadcn repos in the benchmark sit; the median is 62. shadcn\'s own rules for agents say it in 1 line: use semantic colours, never bg-blue-500.',
|
|
17
17
|
],
|
|
18
18
|
doorOverrides: [
|
|
19
|
-
'A shadcn component ships with variants: outline, ghost, destructive, small, large. The variant decides how the component looks, once, in the component file you own. Passing bg-blue-100
|
|
19
|
+
'A shadcn component ships with variants: outline, ghost, destructive, small, large. The variant decides how the component looks, once, in the component file you own. Passing bg-blue-100 through className decides it again at the call site, where the component can not see it.',
|
|
20
20
|
'Do this in 5 places and 1 button has 6 designs. The next person can not tell which one is intended, and an agent copies whichever it finds first. shadcn\'s guidance gives 3 routes: use a variant that exists, add a variant to the component, or add a variable to the theme file. className is for layout: width, margin, position.',
|
|
21
|
-
'The ideal is
|
|
21
|
+
'The ideal is 2 per 100 files, the tidiest half of the 16 shadcn repos in the benchmark; the median is 1. Typography through className, a text-sm on an Input, is shown here and not scored: 12 of the 16 repos do it at the same rate, so it separates nothing, and nowhere else in this report does a text size cost points.',
|
|
22
22
|
],
|
|
23
23
|
colors: [
|
|
24
24
|
'Look, nobody consciously chooses 100 colours to start with. They were added with each change, and the change after that. No one was checking for the drift, because no one expected it. Then come the multipliers: a rebrand, a dark-mode pass, ad hoc files, each one multiplying the values again.',
|
|
@@ -27,7 +27,7 @@ import { ruleStaleness } from '../lib/staleness.mjs';
|
|
|
27
27
|
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
28
28
|
import { lastTouchedDates } from '../lib/lasttouched.mjs';
|
|
29
29
|
import { SCHEMA_VERSION } from '../lib/version.mjs';
|
|
30
|
-
import { decideProfile, profileOf } from '../profiles/index.mjs';
|
|
30
|
+
import { decideProfile, profileOf, installedDirs, splitArbitrary } from '../profiles/index.mjs';
|
|
31
31
|
import { countPaint } from './paint.mjs';
|
|
32
32
|
|
|
33
33
|
function arg(name, fallback) {
|
|
@@ -58,6 +58,7 @@ const exclusions = loadExclusions(target, argAll('exclude'));
|
|
|
58
58
|
const files = walkRepo(target, 14, exclusions);
|
|
59
59
|
const profile = profileRepo(target, files);
|
|
60
60
|
const { components } = harvestComponents(target, files.code);
|
|
61
|
+
const tokens = harvestTokens(target, files.styles, files.code);
|
|
61
62
|
// Repo kind and measurability, decided once in profiles/ and read everywhere
|
|
62
63
|
// (a published components package with no app pages is a LIBRARY; a stack the
|
|
63
64
|
// detector cannot read is NOT MEASURED, never scored as zeros). The decision
|
|
@@ -68,22 +69,30 @@ decideProfile(profile, components, files, target);
|
|
|
68
69
|
{
|
|
69
70
|
const P = profileOf(profile);
|
|
70
71
|
if (P.isShadcn) {
|
|
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 ?? [])]);
|
|
73
|
-
const kitNames = new Set(components.filter((c) => doorFiles.has(c.file)).map((c) => c.name));
|
|
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
72
|
const regDirs = profile.shadcn.registryDirs ?? [];
|
|
73
|
+
const blocks = profile.shadcn.blockFiles ?? [];
|
|
74
|
+
const allInstalled = installedDirs(P);
|
|
75
|
+
const doorFiles = new Set(allInstalled.flatMap((d) => files.code.filter((f) => f === d || f.startsWith(`${d}/`))));
|
|
76
|
+
const kitNames = new Set(components.filter((c) => doorFiles.has(c.file)).map((c) => c.name));
|
|
77
|
+
// The score reads the repo as the agent meets it: own code plus installed
|
|
78
|
+
// registries, because a palette colour in ai-elements teaches the same
|
|
79
|
+
// wrong lesson as one in own code. The catalogue and kit blocks stay out:
|
|
80
|
+
// editing them is the intended use.
|
|
81
|
+
profile.shadcn.paint = countPaint(target, files.code, { uiDirs: [...P.uiDirs, ...blocks], kitNames });
|
|
82
|
+
// Own code alone, for the breakdown line under the score.
|
|
83
|
+
profile.shadcn.paintOwn = regDirs.length ? countPaint(target, files.code, { uiDirs: allInstalled, kitNames }) : profile.shadcn.paint;
|
|
78
84
|
if (regDirs.length) {
|
|
79
85
|
const regFiles = files.code.filter((f) => regDirs.some((d) => f.startsWith(`${d}/`)));
|
|
80
86
|
const rp = countPaint(target, regFiles, { uiDirs: [], kitNames });
|
|
81
|
-
profile.shadcn.registryPaint = { files: rp.ownFiles, tinUses: rp.tin.uses, dirs: regDirs };
|
|
87
|
+
profile.shadcn.registryPaint = { files: rp.ownFiles, tinUses: rp.tin.uses, doorUses: rp.doors.uses, dirs: regDirs };
|
|
82
88
|
}
|
|
89
|
+
// Bracket values inside installed code are shadcn's (or a registry's)
|
|
90
|
+
// choices: a true lesson, badly framed. Kept out of the count, named.
|
|
91
|
+
const split = splitArbitrary(tokens.tailwind?.arbitrary ?? [], allInstalled);
|
|
92
|
+
profile.shadcn.arbitraryInstalled = split.installed;
|
|
83
93
|
}
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
const tokens = harvestTokens(target, files.styles, files.code);
|
|
87
96
|
const duplicates = findDuplicates(components, profile.uiDir, target);
|
|
88
97
|
const context = harvestContext(target);
|
|
89
98
|
const staleRules = ruleStaleness(target, components,
|
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
* row exists: `text-gray-500`, `bg-blue-100`, `text-emerald-600` on a
|
|
7
7
|
* status, and the manual evening override `dark:bg-gray-950`. 1 sin, 3
|
|
8
8
|
* spellings. The rule: "Semantic colors. Never `bg-blue-500`."
|
|
9
|
-
* 2. Doors
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* 2. Doors recoloured from outside. A colour class passed into a kit
|
|
10
|
+
* component through className (`<Card className="bg-blue-100">`) where
|
|
11
|
+
* a variant or a sheet row was the intended route. The rule: "className
|
|
12
|
+
* for layout only." Typography through className (`<Input
|
|
13
|
+
* className="text-sm">`) is counted separately as a receipt, never
|
|
14
|
+
* scored: the 2026-09-13 fleet audit found it on 12 of 16 shadcn repos at
|
|
15
|
+
* a flat rate, so it separates nothing, and nowhere else in the report
|
|
16
|
+
* does a text size cost points.
|
|
13
17
|
*
|
|
14
18
|
* Both are counted over OWN code only: never inside the catalogue (those are
|
|
15
19
|
* kit doors, edited on purpose), never in exempt files (email, print,
|
|
@@ -30,9 +34,10 @@ const TIN_RE = new RegExp(`(?<![\\w-])(?:[\\w-]+:)*(?:bg|text|border|ring|outlin
|
|
|
30
34
|
// Evening overrides painted by hand with white or black (the palette shades
|
|
31
35
|
// are already caught above with their dark: prefix).
|
|
32
36
|
const DARK_WB_RE = /(?<![\w-])dark:(?:bg|text|border)-(?:white|black)(?:\/\d+)?(?![\w-])/g;
|
|
33
|
-
// Colour
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
// Colour passed into a kit door: palette colour, black, white, any variant prefix.
|
|
38
|
+
const DOOR_COLOR_RE = new RegExp(`(?<![\\w-])(?:[\\w-]+:)*(?:bg|text|border)-(?:${PALETTE}|white|black)(?:-(?:50|[1-9]00|950))?(?:/\\d+)?(?![\\w-])`, 'g');
|
|
39
|
+
// Typography passed into a kit door: weight or size. A receipt, not a score.
|
|
40
|
+
const DOOR_TYPO_RE = /(?<![\w-])(?:[\w-]+:)*(?:font-(?:thin|extralight|light|normal|medium|semibold|bold|extrabold|black)|text-(?:xs|sm|base|lg|xl|[2-9]xl))(?![\w-])/g;
|
|
36
41
|
|
|
37
42
|
const DEMO_PATH_RE = /(^|\/)(stories|storybook|__stories__|examples?|demos?|templates?|playground|fixtures?|__tests__|__mocks__|e2e|cypress)\//i;
|
|
38
43
|
|
|
@@ -52,6 +57,7 @@ export function countPaint(root, codeFiles, { uiDirs = [], kitNames = new Set()
|
|
|
52
57
|
let ownFiles = 0;
|
|
53
58
|
const tin = { uses: 0, files: 0, top: [], samples: new Map() };
|
|
54
59
|
const doors = { uses: 0, files: 0, top: [], samples: new Map() };
|
|
60
|
+
const typo = { uses: 0, files: 0, top: [], samples: new Map() };
|
|
55
61
|
const bump = (bucket, file, count, sample) => {
|
|
56
62
|
bucket.uses += count; bucket.files += 1;
|
|
57
63
|
bucket.top.push({ file, count });
|
|
@@ -77,20 +83,28 @@ export function countPaint(root, codeFiles, { uiDirs = [], kitNames = new Set()
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
if (doorOpen) {
|
|
80
|
-
let hits = 0; const seen = new Map();
|
|
86
|
+
let hits = 0, thits = 0; const seen = new Map(), tseen = new Map();
|
|
81
87
|
for (const m of src.matchAll(doorOpen)) {
|
|
82
88
|
const cls = m[2] ?? m[3] ?? '';
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
const colour = cls.match(DOOR_COLOR_RE);
|
|
90
|
+
if (colour) {
|
|
91
|
+
hits += 1;
|
|
92
|
+
const key = `<${m[1]} className="${colour.slice(0, 3).join(' ')}">`;
|
|
93
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
94
|
+
} else {
|
|
95
|
+
const t = cls.match(DOOR_TYPO_RE);
|
|
96
|
+
if (t) { thits += 1; const key = `<${m[1]} className="${t.slice(0, 2).join(' ')}">`; tseen.set(key, (tseen.get(key) ?? 0) + 1); }
|
|
97
|
+
}
|
|
88
98
|
}
|
|
89
99
|
if (hits) {
|
|
90
100
|
bump(doors, f, hits, null);
|
|
91
101
|
for (const [k, v] of seen) doors.samples.set(k, (doors.samples.get(k) ?? 0) + v);
|
|
92
102
|
doors.top.at(-1).sample = [...seen.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
93
103
|
}
|
|
104
|
+
if (thits) {
|
|
105
|
+
bump(typo, f, thits, null);
|
|
106
|
+
for (const [k, v] of tseen) typo.samples.set(k, (typo.samples.get(k) ?? 0) + v);
|
|
107
|
+
}
|
|
94
108
|
}
|
|
95
109
|
}
|
|
96
110
|
|
|
@@ -101,5 +115,5 @@ export function countPaint(root, codeFiles, { uiDirs = [], kitNames = new Set()
|
|
|
101
115
|
top: b.top.sort((a, b2) => b2.count - a.count).slice(0, 8),
|
|
102
116
|
samples: [...b.samples.entries()].sort((a, b2) => b2[1] - a[1]).slice(0, 10).map(([value, count]) => ({ value, count })),
|
|
103
117
|
});
|
|
104
|
-
return { ownFiles, tin: finish(tin), doors: finish(doors) };
|
|
118
|
+
return { ownFiles, tin: finish(tin), doors: finish(doors), typo: finish(typo) };
|
|
105
119
|
}
|
|
@@ -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.
|
|
4
|
+
export const VERSION = '7.4.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,3 +100,41 @@ export function profileOf(h) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Installed code: folders the team did not write. The catalogue, kit blocks
|
|
105
|
+
* installed into own code, and third-party registries. Under the agent-safety
|
|
106
|
+
* definition (2026-09-13) installed code splits in two: what teaches a wrong
|
|
107
|
+
* lesson stays in the score, attributed (registry palette colours); what
|
|
108
|
+
* teaches a true lesson or none is kept out of the count and named at the top
|
|
109
|
+
* (shadcn's own bracket values, unused stock).
|
|
110
|
+
*/
|
|
111
|
+
export function installedDirs(P) {
|
|
112
|
+
const sc = P?.shadcn ?? {};
|
|
113
|
+
return [...(P?.uiDirs ?? []), ...(sc.registryDirs ?? []), ...(sc.blockFiles ?? [])];
|
|
114
|
+
}
|
|
115
|
+
const underAny = (file, dirs) => dirs.some((d) => file === d || file.startsWith(`${d}/`));
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Split bracket-value entries ({value, count, files:[{file,count}]}) into the
|
|
119
|
+
* team's own uses and installed uses. Own entries keep only own files with
|
|
120
|
+
* counts re-summed; installed is a flat count with its top values.
|
|
121
|
+
*/
|
|
122
|
+
export function splitArbitrary(entries, dirs) {
|
|
123
|
+
if (!dirs.length) return { own: entries, installed: { uses: 0, values: [] } };
|
|
124
|
+
const own = [];
|
|
125
|
+
const installedValues = new Map();
|
|
126
|
+
let installedUses = 0;
|
|
127
|
+
for (const e of entries) {
|
|
128
|
+
const ownFiles = (e.files ?? []).filter((f) => !underAny(f.file, dirs));
|
|
129
|
+
const instFiles = (e.files ?? []).filter((f) => underAny(f.file, dirs));
|
|
130
|
+
const instCount = instFiles.reduce((s, f) => s + f.count, 0);
|
|
131
|
+
if (instCount) { installedUses += instCount; installedValues.set(e.value, (installedValues.get(e.value) ?? 0) + instCount); }
|
|
132
|
+
// the file list under an entry is capped, so own is the remainder of the
|
|
133
|
+
// entry's total, never a sum of the files that happened to be listed
|
|
134
|
+
const ownCount = e.count - instCount;
|
|
135
|
+
if (ownCount > 0) own.push({ ...e, count: ownCount, files: ownFiles });
|
|
136
|
+
}
|
|
137
|
+
own.sort((a, b) => b.count - a.count);
|
|
138
|
+
const values = [...installedValues.entries()].sort((a, b) => b[1] - a[1]).map(([value, count]) => ({ value, count }));
|
|
139
|
+
return { own, installed: { uses: installedUses, values } };
|
|
140
|
+
}
|
|
@@ -125,7 +125,7 @@ function rowsUnder(css, selector) {
|
|
|
125
125
|
const norm = (v) => (v ?? '').replace(/\s+/g, ' ').trim();
|
|
126
126
|
|
|
127
127
|
/** Read the sheet: the CSS file the config names. */
|
|
128
|
-
function readSheet(root, wsRoot, cssPath) {
|
|
128
|
+
function readSheet(root, wsRoot, cssPath, files = null) {
|
|
129
129
|
if (!cssPath) return null;
|
|
130
130
|
const file = [join(wsRoot, cssPath), join(wsRoot, 'src', cssPath)].find((p) => existsSync(p));
|
|
131
131
|
if (!file) return { file: cssPath, found: false };
|
|
@@ -146,8 +146,23 @@ function readSheet(root, wsRoot, cssPath) {
|
|
|
146
146
|
const customUnregistered = themeVars.size
|
|
147
147
|
? custom.filter((r) => !/^(font|shadow|tracking|spacing|radius|ease|breakpoint|typeset)/.test(r) && !themeVars.has(`color-${r}`))
|
|
148
148
|
: [];
|
|
149
|
+
// A custom row nothing references anywhere (no var(--x), no class built on
|
|
150
|
+
// it) is a leftover: the Next.js starter's --foreground-rgb next to the real
|
|
151
|
+
// --foreground. An agent can not tell which colour system is dead.
|
|
152
|
+
let customUnused = [];
|
|
153
|
+
if (files && custom.length) {
|
|
154
|
+
const rel = file.slice(root.length + 1);
|
|
155
|
+
const others = [...(files.code ?? []), ...(files.styles ?? [])].filter((f) => f !== rel).slice(0, 4000);
|
|
156
|
+
const pending = new Set(custom.filter((r) => !/^(font|shadow|tracking|spacing|radius|ease|breakpoint|typeset)/.test(r)));
|
|
157
|
+
for (const f of others) {
|
|
158
|
+
if (!pending.size) break;
|
|
159
|
+
const src = read(join(root, f));
|
|
160
|
+
for (const r of [...pending]) if (src.includes(`--${r}`) || new RegExp(`[-:]${r}(?![\\w-])`).test(src)) pending.delete(r);
|
|
161
|
+
}
|
|
162
|
+
customUnused = [...pending];
|
|
163
|
+
}
|
|
149
164
|
return {
|
|
150
|
-
file: file.slice(root.length + 1), found: true, hslEra,
|
|
165
|
+
file: file.slice(root.length + 1), found: true, hslEra, customUnused,
|
|
151
166
|
lightRows: rows.length, darkRows: dark.size, shadcnPresent, shadcnMissing: SHADCN_ROWS.filter((r) => !light.has(r)),
|
|
152
167
|
known: known.length, custom, missingDark, customMissingDark, customUnregistered, tweakcnPresent,
|
|
153
168
|
spacing, spacingChanged: spacing !== null && norm(spacing) !== FACTORY_SPACING,
|
|
@@ -219,7 +234,7 @@ export default {
|
|
|
219
234
|
}
|
|
220
235
|
const pkg = readJSON(join(wsRoot, 'package.json')) ?? readJSON(join(root, 'package.json')) ?? {};
|
|
221
236
|
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
222
|
-
const sheet = readSheet(root, wsRoot, cfg.tailwind?.css);
|
|
237
|
+
const sheet = readSheet(root, wsRoot, cfg.tailwind?.css, files);
|
|
223
238
|
installs.push({ config: file, uiDir: uiAbs, catalogueNames, kit: readKit(cfg, sheet, deps), sheet });
|
|
224
239
|
}
|
|
225
240
|
const swept = catalogueSweep(root, files);
|
|
@@ -176,11 +176,16 @@ if (neverImported.length >= 3) {
|
|
|
176
176
|
rule('Use the semantic classes the theme gives you (`bg-background`, `text-muted-foreground`, `border-border`), never a palette colour like `bg-blue-500` or `text-gray-600`, and never a hand-written `dark:` colour. The variables already carry both modes.');
|
|
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
|
-
if (paint?.
|
|
179
|
+
if (paint?.typo?.uses) lines.push(` (${paint.typo.uses} shadcn component${paint.typo.uses === 1 ? '' : 's'} get a text size or weight through className, like ${paint.typo.samples.slice(0, 2).map((s) => `\`${s.value}\``).join(' and ')}; add a size variant instead.)`);
|
|
180
|
+
if (paint?.doors?.uses) lines.push(` (${paint.doors.uses} shadcn component${paint.doors.uses === 1 ? '' : 's'} already recoloured through className, like ${paint.doors.samples.slice(0, 2).map((s) => `\`${s.value}\``).join(' and ')}; do not add to them.)`);
|
|
180
181
|
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` : ''}.`);
|
|
181
182
|
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.`);
|
|
182
183
|
rule('Merge classes with `cn()`. Never concatenate strings and never write a ternary inside a className string.');
|
|
183
184
|
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`.');
|
|
185
|
+
const ai = sc.arbitraryInstalled ?? null;
|
|
186
|
+
if (ai?.uses) rule(`Bracket values in \`${P.uiDir ?? 'components/ui'}\` are shadcn's, not a pattern to copy. The installed components use a few values Tailwind's scale does not have (${ai.values.slice(0, 3).map((v) => `\`${v.value}\``).join(', ')}), written by shadcn's authors for those components only. In your own code, do not write a bracket value: use a step from the scale, or a variable from the theme file. If a value you need is missing, add it to the theme file once and use it by name.`);
|
|
187
|
+
else rule('In your own code, do not write a bracket value: use a step from the scale, or a variable from the theme file. If a value you need is missing, add it to the theme file once and use it by name.');
|
|
188
|
+
if (sc.sheet?.customUnused?.length) rule(`${sc.sheet.customUnused.slice(0, 3).map((r) => `\`--${r}\``).join(', ')} in \`${sc.sheet.file}\` ${sc.sheet.customUnused.length === 1 ? 'is' : 'are'} defined and used nowhere. Do not use ${sc.sheet.customUnused.length === 1 ? 'it' : 'them'}; ${sc.sheet.customUnused.length === 1 ? 'it is' : 'they are'} a leftover next to the real theme.`);
|
|
184
189
|
rule('Radius comes from the `--radius` variable and the scale derived from it; never a bracket value. Never change `--spacing`: it resizes every gap in the app.');
|
|
185
190
|
if (sc.sheet?.customUnregistered?.length) rule(`Custom variables must be registered where Tailwind can see them: ${sc.sheet.customUnregistered.slice(0, 3).map((r) => `\`--${r}\``).join(', ')} ${sc.sheet.customUnregistered.length === 1 ? 'is' : 'are'} defined but never mapped in \`@theme inline\`, so \`bg-${sc.sheet.customUnregistered[0]}\` does nothing.`);
|
|
186
191
|
rule('Small habits shadcn expects: `gap-*` not `space-x/y-*`; `size-4` not `w-4 h-4`; `truncate` for one-line clipping; no `z-index` on Dialog, Sheet, Popover or Tooltip, they stack themselves; icons from the project\'s own icon library only.');
|