roast-my-design-system 8.9.1 → 8.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,7 +49,7 @@ Run it on your codebase and get, in about a second:
49
49
 
50
50
  ## Why this exists
51
51
 
52
- Your AI agent (Claude, Cursor, Copilot) builds UI by imitating what's already in your repo. If your repo has 112 colours and 4 Button implementations, your agent guesses which one is canonical, and it picks wrong half the time. That's why AI-generated UI looks *almost-but-not-quite* right. The first step to fixing it is seeing the mess measured.
52
+ Your AI agent (Claude, Cursor, Copilot) builds UI by copying what is already in your repo. We measured it: 164 agent sessions on 10 real products, with and without this tool. On everyday work the agent reused the components and tokens it found and stayed on-system. It went off-system where the design system had no answer, a chart with no palette or a whole new theme, and it filled the gap the way the repo's own code did: by hand. So the mess an agent adds is a map of the gaps in your system. This tool measures the mess, names the gaps, and puts the check inside the agent's editor, so it runs whether or not the agent thinks to ask.
53
53
 
54
54
  ## Live examples
55
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "8.9.1",
3
+ "version": "8.9.2",
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 112 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": [
@@ -159,7 +159,11 @@ const duplicates = findDuplicates(components, profile.uiDir, target, profile.uiD
159
159
  const context = harvestContext(target);
160
160
  // The chart palette the repo keeps, or the charts painting by hand without
161
161
  // one, and the gaps an agent will fill by inventing (lib/charts, lib/gaps).
162
- const charts = chartSystemOf(files, (f) => readSource(resolve(target, f)));
162
+ const charts = (() => {
163
+ const installed = installedDirs(profileOf(profile));
164
+ const own = (f) => !installed.some((d) => f === d || f.startsWith(`${d}/`));
165
+ return chartSystemOf(files, (f) => readSource(resolve(target, f)), { own });
166
+ })();
163
167
  const gaps = designGaps({ charts, tokenFile: tokens.tokenFile ?? null });
164
168
  const staleRules = ruleStaleness(target, components,
165
169
  new Set(neverImportedComponents(components, profile.uiDir).map((c) => c.name)),
@@ -137,6 +137,9 @@ const isTokenRef = (v) =>
137
137
  const FONTFAMILY_PROPS = /font-family\s*:\s*([^;{}]+)[;}]/g;
138
138
  const SHADOW_PROPS = /box-shadow\s*:\s*([^;{}]+)[;}]/g;
139
139
  const LENGTH_RE = /-?\d*\.?\d+(?:px|rem|em|%|vh|vw|ch)\b/g;
140
+ // the spacing keys of a style object, camelCase: padding, paddingTop, margin,
141
+ // marginInline, gap, rowGap, top, inset, insetBlock
142
+ const INLINE_SPACING_RE = /\b(padding(?:Top|Right|Bottom|Left|Block|Inline|X|Y)?|margin(?:Top|Right|Bottom|Left|Block|Inline|X|Y)?|gap|rowGap|columnGap|top|right|bottom|left|inset(?:Block|Inline)?)\s*:\s*(['"`][^'"`]*['"`])/g;
140
143
 
141
144
  // ---------- Tailwind utility classes ----------
142
145
  // Colour-bearing utilities: bg-red-500, text-zinc-400, border-gray-200, arbitrary bg-[#1a1a1a]
@@ -274,7 +277,12 @@ export function extractStyling(src, { css = false } = {}) {
274
277
  const at = src.indexOf(b);
275
278
  if (isStaticInline(b)) out.inlineBlocks.push({ index: at });
276
279
  for (const m of b.matchAll(HEX_RE)) out.colors.push({ value: normalizeHex(m[0]), index: at + m.index });
277
- for (const m of b.matchAll(LENGTH_RE)) out.spacing.push({ value: m[0], index: at + m.index });
280
+ // Only the spacing properties, the same set the CSS rule reads: a width,
281
+ // a font size or a shadow's offsets (boxShadow: '0 3px 9px …') are not
282
+ // spacing, and the live check reported them as such until 8.9.2.
283
+ for (const m of b.matchAll(INLINE_SPACING_RE)) {
284
+ for (const len of (m[2].match(LENGTH_RE) ?? [])) out.spacing.push({ value: len, index: at + m.index });
285
+ }
278
286
  }
279
287
  // hex literals outside class strings / style blocks (styled-components,
280
288
  // colour consts) — everything not already collected above
@@ -62,11 +62,13 @@ const refRe = (name) => name.startsWith('--')
62
62
 
63
63
  /**
64
64
  * What the repo already knows about charts, from the same file set the rest
65
- * of the knowledge reads.
65
+ * of the knowledge reads. `own` says whether a file is the team's own code;
66
+ * installed kit code (a shadcn catalogue, a registry block) is left out of
67
+ * the precedents, and a fresh install therefore has no chart gap.
66
68
  * @returns { palette: { names, file, shadcnDefault, referenced } | null,
67
69
  * precedents: [{ file, count, sample }], chartFiles }
68
70
  */
69
- export function chartSystemOf(files, read) {
71
+ export function chartSystemOf(files, read, { own = () => true } = {}) {
70
72
  const names = [], where = new Map();
71
73
  const noteToken = (name, file) => { if (!where.has(name)) { names.push(name); where.set(name, file); } };
72
74
  for (const f of files.styles ?? []) {
@@ -84,7 +86,9 @@ export function chartSystemOf(files, read) {
84
86
  if (COLOUR_LITERAL_RE.test(text.slice(m.index, m.index + 600))) noteToken(m[1], f);
85
87
  }
86
88
  }
87
- if (isChartFile(f, text)) chartTexts.push({ f, text });
89
+ // a chart inside installed kit code (shadcn's components/ui/chart.tsx, a
90
+ // registry block) is the kit's, not a precedent the team set
91
+ if (own(f) && isChartFile(f, text)) chartTexts.push({ f, text });
88
92
  }
89
93
  const palette = names.length ? { names, file: where.get(names[0]), shadcnDefault: names.every((n) => SHADCN_DEFAULT_RE.test(n)), referenced: false } : null;
90
94
  const precedents = [];
@@ -110,9 +110,10 @@ export function learnSystem(repoRoot, { exclude = [] } = {}) {
110
110
  // the same two lists the MCP knowledge builds (mcp/knowledge.mjs), from the
111
111
  // same helpers, for tokenTwinFindings and avoidedImportFindings (8.6.1)
112
112
  const read = (f) => readSource(join(repoRoot, f));
113
- const charts = chartSystemOf(files, read);
114
113
  const hardDupes = (findDuplicates(ledger, profile.uiDir, repoRoot).exactDuplicates ?? []).filter((d) => !d.wrapped);
115
114
  const P = profileOf(profile);
115
+ const installedNow = installedDirs(P);
116
+ const charts = chartSystemOf(files, read, { own: (f) => !installedNow.some((d) => f === d || f.startsWith(`${d}/`)) });
116
117
  // the kit the product is built on, with its definition attached, the way
117
118
  // the MCP knowledge reads it (mcp/knowledge.mjs): the theme's colours are
118
119
  // the token set on a kit repo, whatever stylesheet holds the most literals
@@ -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 = '8.9.1';
4
+ export const VERSION = '8.9.2';
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.
@@ -22,7 +22,7 @@ import { resolveWorkspaces } from '../lib/workspaces.mjs';
22
22
  import { neverImportedComponents } from '../lib/neverimported.mjs';
23
23
  import { typefaceOf } from '../lib/typefaces.mjs';
24
24
  import { hexRgb } from '../lib/nearpairs.mjs';
25
- import { decideProfile, profileOf } from '../profiles/index.mjs';
25
+ import { decideProfile, profileOf, installedDirs } from '../profiles/index.mjs';
26
26
  import { KITS } from '../profiles/kit-common.mjs';
27
27
  import { repoTokenDefs } from '../lib/tokentwins.mjs';
28
28
  import { dupeCopiesOf } from '../lib/avoidedimports.mjs';
@@ -132,7 +132,10 @@ export function loadKnowledge(root) {
132
132
  const tokenDefs = repoTokenDefs(files.styles, read);
133
133
  // the chart palette the repo keeps, or the charts that paint by hand
134
134
  // without one (lib/charts.mjs)
135
- const charts = chartSystemOf(files, read);
135
+ // installed kit code is the kit's own door, never a precedent (profiles/index)
136
+ const installed = installedDirs(P);
137
+ const ownCode = (f) => !installed.some((d) => f === d || f.startsWith(`${d}/`));
138
+ const charts = chartSystemOf(files, read, { own: ownCode });
136
139
  const gaps = designGaps({ charts, tokenFile: tokens.tokenFile ?? null });
137
140
 
138
141
  return {