roast-my-design-system 5.7.2 → 5.9.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 -6
- package/package.json +1 -1
- package/skills/roast-my-design-system/scripts/diagnose/index.mjs +136 -34
- package/skills/roast-my-design-system/scripts/diagnose/why.mjs +61 -0
- package/skills/roast-my-design-system/scripts/harvest/context.mjs +73 -2
- package/skills/roast-my-design-system/scripts/lib/version.mjs +1 -1
- package/skills/roast-my-design-system/scripts/rules/build.mjs +3 -1
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
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.
|
|
10
10
|
|
|
11
|
-
> **New in 5.
|
|
11
|
+
> **New in 5.9: the agent card knows which doors your rules actually reach.** The report now states, as plain fact, which tools can read the rules files you have: Claude Code reads CLAUDE.md, Codex reads AGENTS.md, Cursor reads AGENTS.md and `.cursor/rules`. Where a door is missing, the card shows the one-line fix (Claude Code skips AGENTS.md; a CLAUDE.md containing the single line `@AGENTS.md` closes the gap). The scan also finds rules files nested in subfolders now, which is how monorepos really do it (twenty carries 35 AGENTS.md files; a root-only look reported 1), and it recognises GEMINI.md, `.windsurf/rules` and `.github/instructions`.
|
|
12
12
|
|
|
13
|
-
> **New in 5.
|
|
13
|
+
> **New in 5.8: every finding can explain itself.** A "why this matters" toggle under each finding unfolds the calm story behind the brutal number: how the mess arrives innocently, what it costs later, how an agent multiplies it, and why the ideal sits where it sits, benchmark medians included. Written to a plain-language standard, reviewed word by word.
|
|
14
14
|
|
|
15
15
|
> **New in 5.0: it runs as a local MCP server.** One command, and your agent asks the design system before writing UI, then gets the work checked after: which Button is canonical, which token holds that colour, review my changes. Local, deterministic, nothing leaves your machine. See [Live answers over MCP](#live-answers-over-mcp).
|
|
16
16
|
|
|
@@ -21,7 +21,7 @@ Run it on your codebase and get, in about a second:
|
|
|
21
21
|
- **Reads React and web components alike.** Stencil, Lit and custom elements detected by tag registration, counted by kebab tag; libraries get composition maps, not adoption accusations; earned token namespaces named in the header; and anything the scan cannot read is declared "not measured" instead of scored.
|
|
22
22
|
- **The receipts behind it.** Every colour and its near-identical twin, every spacing value, typeface, duplicated or never-imported component, inline style and !important, with real file paths, in one self-contained HTML report you can open, Slack or email.
|
|
23
23
|
- **The first fixes ranked by payoff, each with its prompt.** A "Where to start" list derived from your own numbers, and every move carries a copy button with a ready-made fix prompt for your agent: the finding, the files, the expected payoff, and rules that respect deliberate craft. Fix, re-run the scan, press the next button.
|
|
24
|
-
- **Rules that stop the mess coming back.** A generated `design-system-rules.md` with canonical components, your token file, and known duplicates to avoid, so your AI agent follows your system instead of guessing at it. `--apply` injects them into every agent file you have: Claude, Cursor, GitHub Copilot, and Windsurf. Every scan also checks the rules you already have for stale references: paths that no longer exist, components named canonical that nothing imports anymore.
|
|
24
|
+
- **Rules that stop the mess coming back.** A generated `design-system-rules.md` with canonical components, your token file, and known duplicates to avoid, so your AI agent follows your system instead of guessing at it. `--apply` injects them into every agent file you have: Claude, Cursor, GitHub Copilot, and Windsurf. Every scan also checks the rules you already have for stale references: paths that no longer exist, components named canonical that nothing imports anymore. And the agent card states which tools can actually read the rules you have (Claude Code, Codex, Cursor), with the one-line fix where a door is missing.
|
|
25
25
|
|
|
26
26
|
## Why this exists
|
|
27
27
|
|
|
@@ -60,17 +60,17 @@ One scan powers all of it; the flags decide what lands on disk. Combine freely.
|
|
|
60
60
|
|
|
61
61
|
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:
|
|
62
62
|
|
|
63
|
-

|
|
64
64
|
|
|
65
65
|
The same report in light mode (one file, built-in toggle):
|
|
66
66
|
|
|
67
|
-

|
|
68
68
|
|
|
69
69
|
## What makes the numbers trustworthy
|
|
70
70
|
|
|
71
71
|
- **Deterministic scanner, not AI sampling.** A zero-dependency Node script reads *every* file (about a second on a normal repo, a few on a large monorepo) and returns the same numbers every run. Claude narrates; it never counts.
|
|
72
72
|
- **Read-only.** Nothing in your repo is modified. The only outputs are a temp JSON and the HTML report.
|
|
73
|
-
- **No network, no telemetry.** Everything runs locally. Nothing about your code leaves your machine.
|
|
73
|
+
- **No network, no telemetry.** Everything runs locally. Nothing about your code leaves your machine. Dependency scanners such as Socket may flag URL strings in this package; they are product links and format identifiers written into generated reports, never fetched.
|
|
74
74
|
- **Zero dependencies, enforced by the test suite.** The package installs nothing but itself, and that is a tested promise rather than a habit: the suite fails if package.json ever declares a dependency, and the exact file list npm ships is a photographed contract, so nothing can stow away in a release.
|
|
75
75
|
- **Honest gaps.** When a repo's components register in a pattern the scan cannot read, the component tiles say "not measured" and drop out of the score. A zero the scanner never earned is presented as blindness, not discipline.
|
|
76
76
|
- **Honest exclusions.** Test files, Storybook stories, docs sites, example apps, SVG artwork, and email templates (which *must* inline styles) are excluded, so you can't discredit the numbers on a technicality. Your own exclusions (`.roastignore`, `--exclude`) are printed in the report header with file counts, so a scoped scan can never pass itself off as the whole repo.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.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": [
|
|
@@ -29,6 +29,7 @@ import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
|
29
29
|
import { VERSION } from '../lib/version.mjs';
|
|
30
30
|
import { feedbackUrl, FEEDBACK_ASK, FEEDBACK_CTA } from '../lib/feedback.mjs';
|
|
31
31
|
import { fixPrompt } from '../lib/fixprompt.mjs';
|
|
32
|
+
import { WHY } from './why.mjs';
|
|
32
33
|
|
|
33
34
|
// The benchmark (Ideal-2026 norms + scanned-repo stats) ships next to the
|
|
34
35
|
// code so the page works offline; degrade gracefully when absent.
|
|
@@ -343,18 +344,22 @@ function healthOf(metric, value) {
|
|
|
343
344
|
if (value <= warnCap) return 'warn';
|
|
344
345
|
return 'bad';
|
|
345
346
|
}
|
|
347
|
+
// Our own marks, drawn by hand for this report: a tick that starts at the
|
|
348
|
+
// left foot, a full-bleed cross, a round exclamation, a quiet dash. Generic
|
|
349
|
+
// glyphs on purpose, and our own coordinates on purpose: nothing here is
|
|
350
|
+
// copied from any icon set.
|
|
346
351
|
const ICONS = {
|
|
347
|
-
good: '<svg class="ic ic-good" viewBox="0 0 24 24"><polyline points="
|
|
348
|
-
warn: '<svg class="ic ic-warn" viewBox="0 0 24 24"><circle cx="12" cy="12" r="
|
|
349
|
-
bad: '<svg class="ic ic-bad" viewBox="0 0 24 24"><line x1="18" y1="
|
|
352
|
+
good: '<svg class="ic ic-good" viewBox="0 0 24 24"><polyline points="4.5 12.5 9.5 17.5 19.5 6.5"/></svg>',
|
|
353
|
+
warn: '<svg class="ic ic-warn" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9.5"/><line x1="12" y1="7.5" x2="12" y2="13"/><line x1="12" y1="16.5" x2="12" y2="16.51"/></svg>',
|
|
354
|
+
bad: '<svg class="ic ic-bad" viewBox="0 0 24 24"><line x1="18.5" y1="5.5" x2="5.5" y2="18.5"/><line x1="5.5" y1="5.5" x2="18.5" y2="18.5"/></svg>',
|
|
350
355
|
info: '',
|
|
351
|
-
na: '<svg class="ic ic-na" viewBox="0 0 24 24"><line x1="
|
|
356
|
+
na: '<svg class="ic ic-na" viewBox="0 0 24 24"><line x1="6.5" y1="12" x2="17.5" y2="12"/></svg>',
|
|
352
357
|
};
|
|
353
358
|
|
|
354
|
-
//
|
|
355
|
-
// right
|
|
356
|
-
//
|
|
357
|
-
const PULSE_ICON = '<svg class="pulse" viewBox="0 0 24 24" aria-hidden="true"><polyline points="2 12 6 12 9
|
|
359
|
+
// The heartbeat line, drawn by hand: flat, one beat, flat. It traces left to
|
|
360
|
+
// right, sits before the footer's feedback link and inherits its colour
|
|
361
|
+
// through currentColor, so it follows the theme.
|
|
362
|
+
const PULSE_ICON = '<svg class="pulse" viewBox="0 0 24 24" aria-hidden="true"><polyline points="2 12.5 6.5 12.5 9.5 4 15 20.5 18 12.5 22 12.5"/></svg>';
|
|
358
363
|
|
|
359
364
|
// Comparison rows inside a tile: your number vs each yardstick.
|
|
360
365
|
// ▼ mint = you sit at or under the reference, ▲ coral = you're over it.
|
|
@@ -468,6 +473,12 @@ const statTile = (s) => `<div class="stat glass st-${s.health}">
|
|
|
468
473
|
const eyebrow = (text) => `<div class="eyebrow">${text}</div>`;
|
|
469
474
|
const sectionHead = (title, subtitle) => `<div class="sec-head"><h2>${title}</h2>${subtitle ? `<p class="sub">${subtitle}</p>` : ''}</div>`;
|
|
470
475
|
|
|
476
|
+
// "Why this matters": the calm explanation folded under a brutal finding,
|
|
477
|
+
// same unfold mechanics as the fix-prompt viewer. One per finding, rendered
|
|
478
|
+
// only where that finding's evidence block rendered.
|
|
479
|
+
const WHY_ICON = '<svg class="why-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9.5"/><path d="M9.3 9.1a2.8 2.8 0 1 1 4 3.1c-.9.5-1.3 1.1-1.3 2.1"/><line x1="12" y1="17.2" x2="12" y2="17.21"/></svg>';
|
|
480
|
+
const whyToggle = (key) => (WHY[key] ? `<div class="whywrap"><button type="button" class="whybtn" data-why="${key}">${WHY_ICON}<span class="why-lab">why this matters</span></button><div class="whytext" hidden id="why-${key}">${WHY[key].map((p) => `<p>${p}</p>`).join('')}</div></div>` : '');
|
|
481
|
+
|
|
471
482
|
function paletteSection() {
|
|
472
483
|
if (!colors.length) return '';
|
|
473
484
|
const tokens = colors.filter((c) => c.isToken).length;
|
|
@@ -491,7 +502,8 @@ function paletteSection() {
|
|
|
491
502
|
const ramp = greys.length >= 4 ? `
|
|
492
503
|
<div class="ramp-head">${eyebrow('Grey ramp · sorted dark → light · how many can you tell apart?')}</div>
|
|
493
504
|
<div class="only-dark">${rampFor(greys)}</div>
|
|
494
|
-
<div class="only-light">${rampFor(greysLight)}</div
|
|
505
|
+
<div class="only-light">${rampFor(greysLight)}</div>
|
|
506
|
+
${whyToggle('greys')}` : '';
|
|
495
507
|
|
|
496
508
|
return `<section class="palette-grid">
|
|
497
509
|
<div class="glass pal-hero grad">
|
|
@@ -502,6 +514,7 @@ function paletteSection() {
|
|
|
502
514
|
<div class="split-track"><div class="split"><span class="sp-tok" style="width:${tokenPct}%"></span><span class="sp-stray" style="width:${100 - tokenPct}%"></span></div></div>
|
|
503
515
|
<div class="split-legend"><span>${n(tokens)} tokens</span><span>${n(strays)} strays</span></div>
|
|
504
516
|
${tokens ? '' : `<p class="sub warn-text">None of these are defined as CSS variables. Every single one is a hardcoded value.</p>`}
|
|
517
|
+
${whyToggle('colors')}
|
|
505
518
|
</div>
|
|
506
519
|
<div class="glass pal-usage">
|
|
507
520
|
<div class="pal-usage-head">
|
|
@@ -512,7 +525,8 @@ function paletteSection() {
|
|
|
512
525
|
${ramp}
|
|
513
526
|
${nearPairs.length ? `
|
|
514
527
|
<div class="receipts">${eyebrow(`${nearPairs.length} nearly identical pair${nearPairs.length === 1 ? '' : 's'} · copy-paste, not decisions`)}
|
|
515
|
-
<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:${esc(pr.a.value)}"></i>${esc(pr.a.value)} ×${pr.a.count} <b class="nsim">≈</b> <i class="ndot" style="background:${esc(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
|
|
528
|
+
<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:${esc(pr.a.value)}"></i>${esc(pr.a.value)} ×${pr.a.count} <b class="nsim">≈</b> <i class="ndot" style="background:${esc(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>
|
|
529
|
+
${whyToggle('nearPairs')}</div>` : ''}
|
|
516
530
|
</div>
|
|
517
531
|
</section>`;
|
|
518
532
|
}
|
|
@@ -580,10 +594,12 @@ function spacingBars() {
|
|
|
580
594
|
<span class="bar-count">${n(s.count)}</span></div>`).join('');
|
|
581
595
|
const arb = arbitraryCount ? `
|
|
582
596
|
<div class="receipts">${eyebrow(`${n(arbitraryCount)} arbitrary values punched through the scale, one bracket at a time`)}
|
|
583
|
-
<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
|
|
597
|
+
<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>
|
|
598
|
+
${whyToggle('arbitrary')}</div>` : '';
|
|
584
599
|
return `<section class="glass pad">
|
|
585
600
|
${sectionHead(`${n(spacingTotal)} off-scale spacing values`, `a disciplined repo keeps these around a dozen · on-scale Tailwind steps (·) shown for context · off-scale in coral`)}
|
|
586
|
-
<div class="bars">${rows}</div
|
|
601
|
+
<div class="bars">${rows}</div>
|
|
602
|
+
${whyToggle('spacing')}${arb}</section>`;
|
|
587
603
|
}
|
|
588
604
|
|
|
589
605
|
function duplicatesSection() {
|
|
@@ -612,7 +628,8 @@ function duplicatesSection() {
|
|
|
612
628
|
</div>`).join('');
|
|
613
629
|
return `<div class="glass pad half">
|
|
614
630
|
${sectionHead('Duplicated components', 'an agent asking which one is canonical gets several plausible answers. Paths open in VS Code')}
|
|
615
|
-
<div class="fams">${iconCard}${dupeCards}${famCards}</div
|
|
631
|
+
<div class="fams">${iconCard}${dupeCards}${famCards}</div>
|
|
632
|
+
${whyToggle('duplicates')}</div>`;
|
|
616
633
|
}
|
|
617
634
|
|
|
618
635
|
// Scanned values get injected into style attributes below (a radius, a shadow,
|
|
@@ -695,6 +712,7 @@ function typographySection() {
|
|
|
695
712
|
return `<section>
|
|
696
713
|
${sectionHead('Typography & shape', '')}
|
|
697
714
|
<div class="stats minis">${mini.map((s) => statTile(s)).join('')}</div>
|
|
715
|
+
${typefaces.length ? whyToggle('typefaces') : ''}
|
|
698
716
|
${typeSpecimens || receipts('the font sizes, by use', sizeChips)}
|
|
699
717
|
${radiiSpecimens || receipts('the radii, by use', radiiChips)}
|
|
700
718
|
${shadowSpecimens}
|
|
@@ -723,7 +741,8 @@ function inlineSection() {
|
|
|
723
741
|
const max = Math.max(...inline.files.slice(0, 6).map((f) => f.count), 1);
|
|
724
742
|
const importantRow = important.count ? `
|
|
725
743
|
<div class="receipts">${eyebrow(`${n(important.count)} !important declaration${important.count === 1 ? '' : 's'} · the cascade admitting defeat`)}
|
|
726
|
-
<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
|
|
744
|
+
<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>
|
|
745
|
+
${whyToggle('important')}</div>` : '';
|
|
727
746
|
const blocks = inline.count >= 5 ? `<div class="fam-rows">${inline.files.slice(0, 6).map((f) => `
|
|
728
747
|
<div class="mini-card col">
|
|
729
748
|
<div class="mc-row">${fileLink(f.file)}<span class="mc-count">×${f.count}</span></div>
|
|
@@ -731,7 +750,7 @@ function inlineSection() {
|
|
|
731
750
|
</div>`).join('')}</div>` : '';
|
|
732
751
|
return `<div class="glass pad half">
|
|
733
752
|
${sectionHead(inline.count >= 5 ? `${n(inline.count)} inline style blocks` : `${n(important.count)} !important declarations`, 'styling no system can see')}
|
|
734
|
-
${blocks}${importantRow}</div>`;
|
|
753
|
+
${blocks}${inline.count >= 5 ? whyToggle('inline') : ''}${importantRow}</div>`;
|
|
735
754
|
}
|
|
736
755
|
|
|
737
756
|
// The adoption map: the real system drawn to scale as a treemap. Every
|
|
@@ -842,7 +861,8 @@ function componentsSection() {
|
|
|
842
861
|
${neverImported.length >= 2 ? `
|
|
843
862
|
<div class="receipts">${eyebrow(isLibrary ? `${n(neverImported.length)} components unused internally · showroom stock to review, not dead weight: consumers in other repos are invisible from here` : `${n(neverImported.length)} components defined but never imported · they sit in the system as wrong answers waiting to be picked`)}
|
|
844
863
|
${orphanRows()}
|
|
845
|
-
<p class="sub" style="margin-top:8px">Routers, dynamic imports and barrel files can hide real usage, so treat this as a shortlist to check, not a demolition order.</p
|
|
864
|
+
<p class="sub" style="margin-top:8px">Routers, dynamic imports and barrel files can hide real usage, so treat this as a shortlist to check, not a demolition order.</p>
|
|
865
|
+
${isLibrary ? '' : whyToggle('neverImported')}` : ''}</section>`;
|
|
846
866
|
}
|
|
847
867
|
|
|
848
868
|
// "Where to start" — at most three moves, every one derived from this repo's
|
|
@@ -1148,10 +1168,29 @@ function packagesSection() {
|
|
|
1148
1168
|
}
|
|
1149
1169
|
|
|
1150
1170
|
function agentSection() {
|
|
1151
|
-
|
|
1171
|
+
// Root files get chips; nested ones (monorepo packages) fold into a single
|
|
1172
|
+
// count chip, because 35 chips is wallpaper and one number is a finding.
|
|
1173
|
+
const rootAgent = agentFiles.filter((c) => !c.nested);
|
|
1174
|
+
const nestedAgent = agentFiles.filter((c) => c.nested);
|
|
1175
|
+
const overflow = nestedAgent.find((c) => c.file.startsWith('(+'));
|
|
1176
|
+
const nestedCount = nestedAgent.filter((c) => !c.file.startsWith('(+')).length
|
|
1177
|
+
+ (overflow ? parseInt(overflow.file.slice(2), 10) || 0 : 0);
|
|
1178
|
+
const have = rootAgent.map((c) => `<span class="chip chip-agent">${esc(c.file)} · ${esc(c.tool ?? c.kind)}</span>`).join('')
|
|
1179
|
+
+ (nestedCount ? `<span class="chip chip-agent">+${nestedCount} nested in subfolders</span>` : '');
|
|
1180
|
+
const readsList = (rootAgent.length ? rootAgent : nestedAgent.slice(0, 3)).map((c) => `<span class="mono">${esc(c.file)}</span>`).join(', ');
|
|
1152
1181
|
const msg = agentFiles.length
|
|
1153
|
-
? `<p class="sub">Your agent reads ${
|
|
1182
|
+
? `<p class="sub">Your agent reads ${readsList}${nestedCount && rootAgent.length ? `, plus ${nestedCount} rules file${nestedCount === 1 ? '' : 's'} nested in subfolders` : ''}. But none of it points at a single source of truth for components and tokens, because there isn't one yet. The numbers below are what your agent actually works from.</p>`
|
|
1154
1183
|
: `<p class="sub">No <span class="mono">CLAUDE.md</span>, no <span class="mono">AGENTS.md</span>, no <span class="mono">.cursorrules</span>. Every time your AI builds UI here, it guesses, from everything below. This is why its output looks almost-but-not-quite right.</p>`;
|
|
1184
|
+
// Door coverage, stated as fact: which tools can read what exists. Claude
|
|
1185
|
+
// Code reads CLAUDE.md only; Codex reads AGENTS.md; Cursor reads AGENTS.md
|
|
1186
|
+
// and .cursor/rules (its legacy .cursorrules is labelled, not judged).
|
|
1187
|
+
const hasFile = (name) => agentFiles.some((c) => c.file === name || c.file.endsWith(`/${name}`));
|
|
1188
|
+
const claudeOk = hasFile('CLAUDE.md');
|
|
1189
|
+
const agentsOk = hasFile('AGENTS.md');
|
|
1190
|
+
const cursorOk = agentsOk || agentFiles.some((c) => c.file.endsWith('.cursor/rules') || c.file === '.cursor/rules');
|
|
1191
|
+
const cursorLegacyOnly = !cursorOk && hasFile('.cursorrules');
|
|
1192
|
+
const mark = (ok) => (ok ? '<b class="cov-yes">✓</b>' : '<b class="cov-no">—</b>');
|
|
1193
|
+
const coverage = agentFiles.length ? `<p class="sub coverage">Readable by: Claude Code ${mark(claudeOk)} · Codex ${mark(agentsOk)} · Cursor ${cursorLegacyOnly ? '<b class="cov-no">legacy file only</b>' : mark(cursorOk)}${claudeOk && !agentsOk ? ' · <span class="dim">copy the same rules into an AGENTS.md and the other tools see them too</span>' : ''}${agentsOk && !claudeOk ? ' · <span class="dim">Claude Code skips AGENTS.md: add a CLAUDE.md containing the single line <span class="mono">@AGENTS.md</span> and it reads the same rules</span>' : ''}</p>` : '';
|
|
1155
1194
|
const stale = h.staleRules ?? [];
|
|
1156
1195
|
const staleRows = stale.map((s) => `
|
|
1157
1196
|
<div class="ledger-row">
|
|
@@ -1166,7 +1205,7 @@ function agentSection() {
|
|
|
1166
1205
|
</div>` : '';
|
|
1167
1206
|
return `<section class="glass pad agent">
|
|
1168
1207
|
${sectionHead('What your AI agent sees today', '')}
|
|
1169
|
-
${msg}${have ? `<div class="chips">${have}</div>` : ''}${staleBlock}</section>`;
|
|
1208
|
+
${msg}${have ? `<div class="chips">${have}</div>` : ''}${coverage}${staleBlock}</section>`;
|
|
1170
1209
|
}
|
|
1171
1210
|
|
|
1172
1211
|
// ---------- page ----------
|
|
@@ -1274,6 +1313,9 @@ const html = `<!doctype html>
|
|
|
1274
1313
|
.chip { border:1px solid var(--line); background:var(--card); border-radius:99px;
|
|
1275
1314
|
padding:4px 12px; font:500 11.5px/1.5 var(--sans); color:var(--text); }
|
|
1276
1315
|
.chip-agent { background:var(--text); color:var(--bg); border-color:var(--text); font-weight:600; }
|
|
1316
|
+
.coverage { margin-top:12px; }
|
|
1317
|
+
.cov-yes { color:var(--ok); font-weight:700; }
|
|
1318
|
+
.cov-no { color:var(--coral); font-weight:700; }
|
|
1277
1319
|
.excl { margin-top:10px; font:500 12px/1.6 var(--sans); color:var(--dim); }
|
|
1278
1320
|
.excl .mono { font-size:11.5px; color:var(--text); }
|
|
1279
1321
|
.stale { margin-top:18px; }
|
|
@@ -1336,14 +1378,28 @@ const html = `<!doctype html>
|
|
|
1336
1378
|
.only-light { display:none; }
|
|
1337
1379
|
html[data-theme="light"] .only-light { display:block; }
|
|
1338
1380
|
html[data-theme="light"] .only-dark { display:none; }
|
|
1339
|
-
/* theme toggle
|
|
1340
|
-
|
|
1341
|
-
|
|
1381
|
+
/* theme toggle: a slide switch. The knob is a lightly rounded square that
|
|
1382
|
+
carries the current mode's icon (moon in dark, sun in light) and slides
|
|
1383
|
+
to the other side on click. The icons themselves never animate. */
|
|
1384
|
+
.theme-toggle { position:fixed; top:18px; right:18px; z-index:10; width:88px; height:32px; border-radius:10px;
|
|
1385
|
+
cursor:pointer; color:var(--dim); padding:0; }
|
|
1342
1386
|
.theme-toggle:hover { color:var(--text); }
|
|
1343
|
-
.
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
.
|
|
1387
|
+
.tt-lab { position:absolute; top:0; bottom:0; display:flex; align-items:center; font:700 9px var(--sans);
|
|
1388
|
+
letter-spacing:.14em; text-transform:uppercase; transition:opacity .3s ease; }
|
|
1389
|
+
.tt-day { left:13px; opacity:0; }
|
|
1390
|
+
.tt-night { right:13px; opacity:1; }
|
|
1391
|
+
html[data-theme="light"] .tt-day { opacity:1; }
|
|
1392
|
+
html[data-theme="light"] .tt-night { opacity:0; }
|
|
1393
|
+
.tt-knob { position:absolute; top:calc(50% - 13px); left:3px; width:26px; height:26px; border-radius:8px;
|
|
1394
|
+
background:var(--card-solid); box-shadow:0 1px 4px rgba(0,0,0,.3), 0 0 0 1px var(--line-soft);
|
|
1395
|
+
display:grid; place-items:center; transition:left .3s ease; }
|
|
1396
|
+
html[data-theme="light"] .theme-toggle .tt-knob { left:59px; }
|
|
1397
|
+
.tt-knob svg { grid-area:1/1; width:15px; height:15px; fill:none; stroke:currentColor; stroke-width:2;
|
|
1398
|
+
stroke-linecap:round; stroke-linejoin:round; transition:opacity .3s ease; }
|
|
1399
|
+
.tt-moon { opacity:1; } .tt-sun { opacity:0; }
|
|
1400
|
+
html[data-theme="light"] .tt-moon { opacity:0; }
|
|
1401
|
+
html[data-theme="light"] .tt-sun { opacity:1; }
|
|
1402
|
+
@media (prefers-reduced-motion: reduce) { .tt-knob, .tt-knob svg, .tt-lab { transition:none; } }
|
|
1347
1403
|
|
|
1348
1404
|
/* palette */
|
|
1349
1405
|
.palette-grid { display:grid; grid-template-columns:1fr 1.6fr; gap:16px; }
|
|
@@ -1413,6 +1469,23 @@ const html = `<!doctype html>
|
|
|
1413
1469
|
user-select:all; -webkit-user-select:all; }
|
|
1414
1470
|
@media print { .start-actions { display:none; } }
|
|
1415
1471
|
|
|
1472
|
+
.whywrap { margin-top:12px; }
|
|
1473
|
+
.whybtn { font:500 11.5px/1 var(--sans); color:var(--dim); background:none; border:none; padding:0; cursor:pointer;
|
|
1474
|
+
display:inline-flex; align-items:center; gap:5px; }
|
|
1475
|
+
.whybtn .why-lab { text-decoration:underline; text-underline-offset:3px; }
|
|
1476
|
+
.why-ic { width:15.5px; height:15.5px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
|
|
1477
|
+
.whybtn:hover { color:var(--accent); }
|
|
1478
|
+
/* On the light-mode gradient hero the dim grey is unreadable: follow the
|
|
1479
|
+
card's own white text treatment instead. */
|
|
1480
|
+
html[data-theme="light"] .grad .whybtn { color:rgba(255,255,255,.82); }
|
|
1481
|
+
html[data-theme="light"] .grad .whybtn:hover { color:#fff; }
|
|
1482
|
+
html[data-theme="light"] .grad .whytext.shown { border-color:rgba(255,255,255,.35); }
|
|
1483
|
+
html[data-theme="light"] .grad .whytext p { color:rgba(255,255,255,.88); }
|
|
1484
|
+
.whytext.shown { display:block; border:1px solid var(--line); border-radius:10px; padding:12px 14px; margin-top:8px; }
|
|
1485
|
+
.whytext p { font:13px/1.7 var(--sans); color:var(--dim); margin:0 0 10px; }
|
|
1486
|
+
.whytext p:last-child { margin-bottom:0; }
|
|
1487
|
+
@media print { .whybtn { display:none; } .whytext.shown { border:none; padding:0; } }
|
|
1488
|
+
|
|
1416
1489
|
.pill { border-radius:99px; padding:3px 10px; font:500 10.5px/1.5 var(--sans); white-space:nowrap; }
|
|
1417
1490
|
.pill-mint { background:var(--ok-soft); color:var(--ok); }
|
|
1418
1491
|
.pill-coral { background:var(--coral-soft); color:var(--coral); }
|
|
@@ -1478,14 +1551,19 @@ const html = `<!doctype html>
|
|
|
1478
1551
|
.foot-sub .feedback:hover { text-decoration:underline; }
|
|
1479
1552
|
.foot-sub .pulse { width:14px; height:14px; vertical-align:-3px; margin-right:5px; fill:none;
|
|
1480
1553
|
stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
|
|
1481
|
-
/* The line traces itself and sweeps out, like a heart monitor.
|
|
1554
|
+
/* The line traces itself and sweeps out, like a heart monitor. 44 is the
|
|
1482
1555
|
polyline's own length, so the dash covers it exactly. */
|
|
1483
|
-
.foot-sub .pulse polyline { stroke-dasharray:
|
|
1484
|
-
@keyframes pulse-trace { 0% { stroke-dashoffset:
|
|
1556
|
+
.foot-sub .pulse polyline { stroke-dasharray:44; animation:pulse-trace 3.4s cubic-bezier(.5,0,.5,1) infinite; }
|
|
1557
|
+
@keyframes pulse-trace { 0% { stroke-dashoffset:44; } 55% { stroke-dashoffset:0; } 100% { stroke-dashoffset:-44; } }
|
|
1485
1558
|
@media (prefers-reduced-motion: reduce) {
|
|
1486
1559
|
.foot-sub .pulse polyline { animation:none; stroke-dashoffset:0; }
|
|
1487
1560
|
}
|
|
1488
1561
|
.scan-id { font-family:var(--mono); font-size:11px; letter-spacing:.04em; }
|
|
1562
|
+
.copy-pill { font:700 9.5px/1 var(--sans); letter-spacing:.06em; text-transform:uppercase; padding:3px 8px;
|
|
1563
|
+
border-radius:99px; border:1px solid var(--line); background:transparent; color:var(--dim);
|
|
1564
|
+
cursor:pointer; vertical-align:1px; }
|
|
1565
|
+
.copy-pill:hover { color:var(--accent); border-color:var(--accent); }
|
|
1566
|
+
@media print { .copy-pill { display:none; } }
|
|
1489
1567
|
footer .brand { color:var(--accent); font-weight:700; text-decoration:none; }
|
|
1490
1568
|
footer .brand:hover { text-decoration:underline; }
|
|
1491
1569
|
footer .creds { display:flex; gap:16px; font:700 9.5px/1.6 var(--sans); letter-spacing:.16em; text-transform:uppercase; color:var(--dim2); }
|
|
@@ -1560,8 +1638,12 @@ const html = `<!doctype html>
|
|
|
1560
1638
|
<!-- rmds-schema: ${NS_TAG} -->
|
|
1561
1639
|
</head><body>
|
|
1562
1640
|
<button class="theme-toggle glass" aria-label="Switch between light and dark mode" onclick="(function(){var r=document.documentElement,t=r.getAttribute('data-theme')==='light'?'dark':'light';r.setAttribute('data-theme',t);try{localStorage.setItem('roast-theme',t)}catch(e){}})()">
|
|
1563
|
-
<
|
|
1564
|
-
<
|
|
1641
|
+
<span class="tt-lab tt-day">Day</span>
|
|
1642
|
+
<span class="tt-lab tt-night">Night</span>
|
|
1643
|
+
<span class="tt-knob">
|
|
1644
|
+
<svg class="tt-moon" viewBox="0 0 24 24"><path d="M20.5 13.2A8.8 8.8 0 1 1 10.8 3.5a6.9 6.9 0 0 0 9.7 9.7z"/></svg>
|
|
1645
|
+
<svg class="tt-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4.2"/><line x1="12" y1="2.2" x2="12" y2="5.4"/><line x1="12" y1="18.6" x2="12" y2="21.8"/><line x1="2.2" y1="12" x2="5.4" y2="12"/><line x1="18.6" y1="12" x2="21.8" y2="12"/><line x1="5.1" y1="5.1" x2="7.3" y2="7.3"/><line x1="16.7" y1="16.7" x2="18.9" y2="18.9"/><line x1="5.1" y1="18.9" x2="7.3" y2="16.7"/><line x1="16.7" y1="7.3" x2="18.9" y2="5.1"/></svg>
|
|
1646
|
+
</span>
|
|
1565
1647
|
</button>
|
|
1566
1648
|
<script>try{var t=localStorage.getItem('roast-theme');if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t)}catch(e){}</script>
|
|
1567
1649
|
<div class="wrap">
|
|
@@ -1591,6 +1673,8 @@ ${extraSectionsHtml()}
|
|
|
1591
1673
|
|
|
1592
1674
|
${whereToStartSection()}
|
|
1593
1675
|
|
|
1676
|
+
${agentSection()}
|
|
1677
|
+
|
|
1594
1678
|
${giftSection()}
|
|
1595
1679
|
|
|
1596
1680
|
${trapsBlock()}
|
|
@@ -1599,8 +1683,6 @@ ${trapsBlock()}
|
|
|
1599
1683
|
|
|
1600
1684
|
${packagesSection()}
|
|
1601
1685
|
|
|
1602
|
-
${agentSection()}
|
|
1603
|
-
|
|
1604
1686
|
<section style="margin-top:16px">${paletteSection()}</section>
|
|
1605
1687
|
|
|
1606
1688
|
${spacingBars()}
|
|
@@ -1614,7 +1696,7 @@ ${componentsSection()}
|
|
|
1614
1696
|
|
|
1615
1697
|
<footer>
|
|
1616
1698
|
<div class="foot-left">
|
|
1617
|
-
<div>Generated by <a class="brand" href="https://github.com/gregkozakiewicz/roast-my-design-system">roast-my-design-system</a> <span class="ver">ver. ${VERSION}</span
|
|
1699
|
+
<div>Generated by <a class="brand" href="https://github.com/gregkozakiewicz/roast-my-design-system">roast-my-design-system</a> <span class="ver">ver. ${VERSION}</span> · free to run: <a class="brand" href="https://www.npmjs.com/package/roast-my-design-system"><span class="mono" id="npx-cmd">npx roast-my-design-system</span></a> <button type="button" class="copy-pill" id="npx-copy">copy</button> · also a Claude Code + Codex skill and a local MCP server</div>
|
|
1618
1700
|
<div class="foot-sub">Designed and built by <a class="author" href="https://gregkozakiewicz.com/?utm_source=roast-report"><span class="gk-mark"></span>Greg Kozakiewicz</a> · <span class="scan-id" title="scan id">${scanId}</span> · ${FEEDBACK_ASK} <a class="feedback" href="${feedbackUrl(VERSION).replace(/&/g, '&')}">${PULSE_ICON}${FEEDBACK_CTA}</a></div>
|
|
1619
1701
|
</div>
|
|
1620
1702
|
<span class="creds"><span>Non-destructive scan</span><span>Read-only</span><span>Paths are real</span></span>
|
|
@@ -1673,6 +1755,26 @@ ${componentsSection()}
|
|
|
1673
1755
|
v.textContent=show?'hide it':'view it first';
|
|
1674
1756
|
});
|
|
1675
1757
|
});
|
|
1758
|
+
var npxCopy=document.getElementById('npx-copy');
|
|
1759
|
+
if(npxCopy){
|
|
1760
|
+
npxCopy.addEventListener('click', function(){
|
|
1761
|
+
var text=document.getElementById('npx-cmd').textContent;
|
|
1762
|
+
var done=function(){ npxCopy.textContent='copied'; setTimeout(function(){ npxCopy.textContent='copy'; },1600); };
|
|
1763
|
+
var manual=function(){ var r=document.createRange(); r.selectNodeContents(document.getElementById('npx-cmd'));
|
|
1764
|
+
var s=getSelection(); s.removeAllRanges(); s.addRange(r); npxCopy.textContent='select + copy'; setTimeout(function(){ npxCopy.textContent='copy'; },2600); };
|
|
1765
|
+
if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(done, manual); }
|
|
1766
|
+
else { manual(); }
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
document.querySelectorAll('.whybtn').forEach(function(b){
|
|
1770
|
+
b.addEventListener('click', function(){
|
|
1771
|
+
var holder=document.getElementById('why-'+b.getAttribute('data-why'));
|
|
1772
|
+
if(!holder) return;
|
|
1773
|
+
var show=holder.hidden;
|
|
1774
|
+
holder.hidden=!show; holder.classList.toggle('shown', show);
|
|
1775
|
+
var lab=b.querySelector('.why-lab'); if(lab) lab.textContent=show?'hide':'why this matters';
|
|
1776
|
+
});
|
|
1777
|
+
});
|
|
1676
1778
|
var copyBtn=document.getElementById('gift-copy');
|
|
1677
1779
|
function rulesText(){ return document.getElementById('gift-md').textContent; }
|
|
1678
1780
|
copyBtn.addEventListener('click', function(){
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// "Why this matters" toggle copy — one entry per finding, unfolds under the
|
|
2
|
+
// finding's own evidence block. The finding stays brutal; this text is the calm
|
|
3
|
+
// explanation behind it: innocent mechanism, compounding cost, agent amplifier,
|
|
4
|
+
// why the ideal sits where it sits.
|
|
5
|
+
//
|
|
6
|
+
// The copy is Greg-approved word for word (docs/why-behind-copy-approved.md,
|
|
7
|
+
// 2026-09-04) and follows docs/greg-copy-style.md. Do not edit casually: every
|
|
8
|
+
// number in these texts is a published calibration (ideals + benchmark medians)
|
|
9
|
+
// and must move together with benchmark.json if the norms are ever re-tuned.
|
|
10
|
+
|
|
11
|
+
export const WHY = {
|
|
12
|
+
colors: [
|
|
13
|
+
'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.',
|
|
14
|
+
'When an agent looks for Brand Blue it scans the repo, sees dozens of blues and logically picks the one used most often, which is how the most-used stray outvotes the actual token.',
|
|
15
|
+
'24 covers a whole product: a brand hue with its tints, an accent, the greys, the status colours. The median across 10 reputable design systems is exactly 24.',
|
|
16
|
+
],
|
|
17
|
+
nearPairs: [
|
|
18
|
+
'A palette is a set of decisions, and two colours a screen can not tell apart are one decision recorded twice. Nobody chooses to own both: someone could not find the first value quickly enough, made a twin by eye, and it stayed.',
|
|
19
|
+
'The cost is never tonight’s screen, which looks fine. It is every choice that comes after: colour pickers now offer both, a search for one misses the other, a dark-mode pass updates one twin and ships the other unchanged, and an agent asked for the dark surface copies whichever it happens to find, so the pair breeds.',
|
|
20
|
+
'The ideal is 0 because each pair collapses in minutes: keep the one that is already a token, point the stray at it, and the multiplication stops. Pound for pound, the cheapest credibility on this page.',
|
|
21
|
+
],
|
|
22
|
+
greys: [
|
|
23
|
+
'A grey scale is a ladder: each step needs a job (background, border, muted text, disabled) and a visible gap to its neighbours. Around 12 rungs covers every job a real interface has; the reputable systems median is 5.',
|
|
24
|
+
'Past 13 the gaps close, steps stop being distinguishable, and each new grey is no longer a rung but a guess between rungs, which is exactly where twins come from.',
|
|
25
|
+
],
|
|
26
|
+
spacing: [
|
|
27
|
+
'Nobody starts a design system with 50 spacing values. A deadline, a last-minute change, and someone nudged a layout until it sat right, and 13px honestly sat better than 12px. The next person can not tell a deliberate exception from a new value, or honestly never noticed, and copies it either way.',
|
|
28
|
+
'That is how layout rhythm starts drifting. Two lists almost align, two cards almost match, and the page stops looking right. An agent copies nudge after nudge, because to an agent 13px looks as intentional as 12px. And once one page carries enough exceptions, the agent treats them as canonical, and the poison spreads to the other pages.',
|
|
29
|
+
'The 12 is a budget for values outside the scale, and it is a generous one: the 10 reputable design systems we scanned hold a median of 6. The average product repo carries 34, which is a second, unwritten scale.',
|
|
30
|
+
],
|
|
31
|
+
typefaces: [
|
|
32
|
+
'One product should have a maximum of 3 font families. When they arrive on their own, it is usually one with a component library, one with a marketing page, one left over from an old logo. Bold and italic do not count; those are weights of one family. And no one will take the risk of removing a font, because nobody is sure what still uses it and what will break.',
|
|
33
|
+
'Every extra font makes pages heavier to load, adds a licence to track, and splits the product’s voice. An agent starting a new page scans the repo, sees several fonts in use, and may feel obligated to use all of them. The split grows on its own.',
|
|
34
|
+
'The ideal is 3, one for each real job: a sans for the interface, a serif if the brand wants an accent, a mono for code.',
|
|
35
|
+
],
|
|
36
|
+
duplicates: [
|
|
37
|
+
'Nobody builds a second Button on purpose. The first one was hard to find, or almost right but easier to rewrite than to change. On the day, rewriting was the faster choice. And no one deletes the old one later, because nobody is sure what still uses it and what will break.',
|
|
38
|
+
'From then on, every fix reaches one copy and misses the other. Every search has a wrong answer on offer. An agent picks whichever version it happens to find, and every new page makes the more common copy stronger. Common wins over correct, and the drift only deepens.',
|
|
39
|
+
'The ideal is 0, because the fix is cheap: point one copy at the other and the multiplying stops. The average product repo carries 20 duplicated components.',
|
|
40
|
+
],
|
|
41
|
+
inline: [
|
|
42
|
+
'An inline style is the fastest way to make one element behave. A deadline, one stubborn element, done. On the day, a fair trade.',
|
|
43
|
+
'The cost is that the design system can not see it. Theming misses it, dark mode misses it, a token change misses it. Every inline style is a private exception and requires someone to find it by hand. And agents like inline styles, because they always work and they need no knowledge of your system. Every block already in the repo teaches the agent that this is how styling is done here, so the habit spreads.',
|
|
44
|
+
'The ideal is 0, and the count is fair: only fully static blocks are counted, runtime positioning and canvas work are exempt. The average product repo carries 49 blocks. The 10 reputable systems hold a median of 12.',
|
|
45
|
+
],
|
|
46
|
+
important: [
|
|
47
|
+
'Every !important marks a styling fight someone had to win by the end of the day. One is harmless. And no one removes it later, because nobody is sure what will break.',
|
|
48
|
+
'But each one raises the floor. The next override in that area has to shout at least as loud, so the count only climbs. An agent that loses a styling fight reaches for !important straight away, because it is the most common CSS fix it has ever seen.',
|
|
49
|
+
'The ideal is 0, because !important is CSS admitting defeat. Fix the rule that kept losing, and the next one is never needed. The average product repo carries 7.',
|
|
50
|
+
],
|
|
51
|
+
neverImported: [
|
|
52
|
+
'A component nobody imports was built for a future that moved, or replaced and never removed. Writing it was probably right at the time. Leaving it on the shelf is the only mistake.',
|
|
53
|
+
'Every dead component makes the catalogue slower to search and harder to trust. An agent reading your components folder can not tell retired from current, so a dead component becomes a live example to copy.',
|
|
54
|
+
'The ideal is 0, because a design system is the set of things actually in use. And these are the safest deletes in the repo: nothing imports them, so nothing can break. Delete, or mark as deprecated where the agent will read it.',
|
|
55
|
+
],
|
|
56
|
+
arbitrary: [
|
|
57
|
+
'Brackets exist for a reason. Sometimes the scale really lacks a value, and one w-[137px] for a stubborn third-party embed is craft, done on purpose.',
|
|
58
|
+
'But a bracket is a value with no name. A search for the token will never find it, and a scale change leaves it untouched. An agent that sees brackets in the repo learns that the scale is optional. It can not tell which brackets were deliberate, so it feels free to add its own, and the escape hatch becomes the main door.',
|
|
59
|
+
'The 20 is a budget for real exceptions, and it is a generous one: 9 of the 10 reputable systems we scanned sit at 0, including systems built on Tailwind. The average product repo carries 70.',
|
|
60
|
+
],
|
|
61
|
+
};
|
|
@@ -2,17 +2,26 @@
|
|
|
2
2
|
* Context-file harvest — the agent-facing and design-facing docs the repo
|
|
3
3
|
* already has. Step 5 (wire up their agent) needs to know what exists so it
|
|
4
4
|
* extends rather than clobbers; the diagnosis mentions what's missing.
|
|
5
|
+
*
|
|
6
|
+
* Two passes: the root checklist below (known doors at known paths), then a
|
|
7
|
+
* nested sweep for rules files living inside subfolders, because that is how
|
|
8
|
+
* monorepos actually do it (twenty carries 35 AGENTS.md files; a root-only
|
|
9
|
+
* look reported 1 and called it the whole story). Nested entries carry
|
|
10
|
+
* nested:true so the report can summarise them instead of flooding chips.
|
|
5
11
|
*/
|
|
6
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
7
13
|
import { join } from 'node:path';
|
|
8
14
|
|
|
9
15
|
const CONTEXT_FILES = [
|
|
10
16
|
{ file: 'CLAUDE.md', kind: 'agent-rules', tool: 'Claude Code' },
|
|
11
17
|
{ file: 'AGENTS.md', kind: 'agent-rules', tool: 'generic (AGENTS.md standard)' },
|
|
18
|
+
{ file: 'GEMINI.md', kind: 'agent-rules', tool: 'Gemini CLI' },
|
|
12
19
|
{ file: '.cursorrules', kind: 'agent-rules', tool: 'Cursor (legacy format)' },
|
|
13
20
|
{ file: '.cursor/rules', kind: 'agent-rules', tool: 'Cursor', dir: true },
|
|
14
21
|
{ file: '.github/copilot-instructions.md', kind: 'agent-rules', tool: 'GitHub Copilot' },
|
|
15
|
-
{ file: '.
|
|
22
|
+
{ file: '.github/instructions', kind: 'agent-rules', tool: 'GitHub Copilot (scoped)', dir: true },
|
|
23
|
+
{ file: '.windsurfrules', kind: 'agent-rules', tool: 'Windsurf (legacy format)' },
|
|
24
|
+
{ file: '.windsurf/rules', kind: 'agent-rules', tool: 'Windsurf', dir: true },
|
|
16
25
|
{ file: 'components.json', kind: 'design-config', tool: 'shadcn/ui' },
|
|
17
26
|
{ file: 'tailwind.config.js', kind: 'design-config', tool: 'Tailwind' },
|
|
18
27
|
{ file: 'tailwind.config.ts', kind: 'design-config', tool: 'Tailwind' },
|
|
@@ -21,6 +30,63 @@ const CONTEXT_FILES = [
|
|
|
21
30
|
{ file: 'STYLEGUIDE.md', kind: 'design-doc' },
|
|
22
31
|
];
|
|
23
32
|
|
|
33
|
+
// Nested sweep targets: the doors that tools officially read from
|
|
34
|
+
// subdirectories (closer file wins, per the AGENTS.md standard and Cursor's
|
|
35
|
+
// docs). Only these — a nested tailwind.config is build detail, not a door.
|
|
36
|
+
const NESTED_FILES = {
|
|
37
|
+
'CLAUDE.md': 'Claude Code',
|
|
38
|
+
'AGENTS.md': 'generic (AGENTS.md standard)',
|
|
39
|
+
'GEMINI.md': 'Gemini CLI',
|
|
40
|
+
};
|
|
41
|
+
const NESTED_DIRS = {
|
|
42
|
+
'.cursor': { child: 'rules', tool: 'Cursor' },
|
|
43
|
+
'.windsurf': { child: 'rules', tool: 'Windsurf' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const SKIP_DIRS = new Set([
|
|
47
|
+
'node_modules', '.next', '.git', 'dist', 'build', 'out', 'coverage',
|
|
48
|
+
'.turbo', '.cache', 'vendor', 'tmp',
|
|
49
|
+
]);
|
|
50
|
+
const MAX_DEPTH = 6;
|
|
51
|
+
const MAX_NESTED = 60; // entries recorded; the true count keeps counting
|
|
52
|
+
|
|
53
|
+
function sweepNested(root) {
|
|
54
|
+
const found = [];
|
|
55
|
+
let total = 0;
|
|
56
|
+
const walk = (dir, rel, depth) => {
|
|
57
|
+
if (depth > MAX_DEPTH) return;
|
|
58
|
+
let entries;
|
|
59
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name;
|
|
62
|
+
if (e.isDirectory()) {
|
|
63
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
64
|
+
const nd = NESTED_DIRS[e.name];
|
|
65
|
+
if (nd) {
|
|
66
|
+
// rel is empty at the root, where the checklist above already owns
|
|
67
|
+
// these paths; the sweep only reports the nested ones.
|
|
68
|
+
if (rel && existsSync(join(dir, e.name, nd.child))) {
|
|
69
|
+
total++;
|
|
70
|
+
if (found.length < MAX_NESTED) found.push({ file: `${relPath}/${nd.child}`, kind: 'agent-rules', tool: nd.tool, nested: true, size: null, mentionsDesign: null });
|
|
71
|
+
}
|
|
72
|
+
continue; // never descend into tool dot-dirs
|
|
73
|
+
}
|
|
74
|
+
if (e.name.startsWith('.')) continue;
|
|
75
|
+
walk(join(dir, e.name), relPath, depth + 1);
|
|
76
|
+
} else if (depth > 0 && NESTED_FILES[e.name]) {
|
|
77
|
+
total++;
|
|
78
|
+
if (found.length < MAX_NESTED) {
|
|
79
|
+
let size = 0;
|
|
80
|
+
try { size = statSync(join(dir, e.name)).size; } catch { /* unreadable */ }
|
|
81
|
+
found.push({ file: relPath, kind: 'agent-rules', tool: NESTED_FILES[e.name], nested: true, size, mentionsDesign: null });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
walk(root, '', 0);
|
|
87
|
+
return { found, total };
|
|
88
|
+
}
|
|
89
|
+
|
|
24
90
|
export function harvestContext(root) {
|
|
25
91
|
const found = [];
|
|
26
92
|
for (const c of CONTEXT_FILES) {
|
|
@@ -38,5 +104,10 @@ export function harvestContext(root) {
|
|
|
38
104
|
} catch { /* unreadable */ }
|
|
39
105
|
found.push({ file: c.file, kind: c.kind, tool: c.tool ?? null, size, mentionsDesign });
|
|
40
106
|
}
|
|
107
|
+
const nested = sweepNested(root);
|
|
108
|
+
found.push(...nested.found);
|
|
109
|
+
if (nested.total > nested.found.length) {
|
|
110
|
+
found.push({ file: `(+${nested.total - nested.found.length} more nested rules files)`, kind: 'agent-rules', tool: null, nested: true, size: null, mentionsDesign: null });
|
|
111
|
+
}
|
|
41
112
|
return found;
|
|
42
113
|
}
|
|
@@ -30,7 +30,9 @@ const repoName = h.profile?.name ?? 'this repo';
|
|
|
30
30
|
if (!compact) {
|
|
31
31
|
lines.push(`<!-- Generated by roast-my-design-system from a scan of ${repoName} on ${scanDate}.`);
|
|
32
32
|
lines.push(' Paste into CLAUDE.md, .cursor/rules or AGENTS.md. Regenerate after big refactors:');
|
|
33
|
-
lines.push(' npx roast-my-design-system@latest --rules
|
|
33
|
+
lines.push(' npx roast-my-design-system@latest --rules');
|
|
34
|
+
lines.push(' Door note: Codex and Cursor read AGENTS.md; Claude Code reads CLAUDE.md only.');
|
|
35
|
+
lines.push(' If this lives in AGENTS.md, add the line "@AGENTS.md" to CLAUDE.md so Claude sees it too. -->');
|
|
34
36
|
lines.push('');
|
|
35
37
|
}
|
|
36
38
|
// Mirror of the report's "no design system" banner: with almost no colour or
|