roast-my-design-system 8.4.4 → 8.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "8.4.4",
3
+ "version": "8.4.6",
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": [
@@ -12,7 +12,9 @@
12
12
  import { extname, join } from 'node:path';
13
13
  import { walkRepo, readSource, profileRepo } from '../harvest/walk.mjs';
14
14
  import { decideProfile, profileOf, installedDirs } from '../profiles/index.mjs';
15
- import { PALETTE_CLASS_RE } from '../harvest/paint.mjs';
15
+ import { KITS } from '../profiles/kit-common.mjs';
16
+ import { kitPaintFindings } from './kitpaint.mjs';
17
+ import { PALETTE_CLASS_RE, blankComments } from '../harvest/paint.mjs';
16
18
  import { harvestTokens, extractStyling, normalizeHex, isGrey } from '../harvest/tokens.mjs';
17
19
  import { harvestComponents, definedComponents } from '../harvest/components.mjs';
18
20
  import { loadExclusions } from './exclusions.mjs';
@@ -35,7 +37,17 @@ export { EMAIL_PRINT_RE, ARTWORK_NAME_RE, SVG_MARKUP_RE, exemptReason } from './
35
37
  export { WIDGET_CSS_RE, WIDGET_CONFIG_RE, LIBRARY_CLASS_RE, isLibraryClass } from './exempt.mjs';
36
38
  // A palette class (bg-blue-500, text-gray-600) where a theme variable exists:
37
39
  // the shadcn paint check (7.2). Only meaningful when system.profile.paletteReady.
38
- export { PALETTE_CLASS_RE };
40
+ // blankComments (8.4.5) is what the report and the live checks run on a file
41
+ // before matching, so a class named in a comment paints nothing; a guard that
42
+ // matches the raw text counts it, and disagrees with the report.
43
+ export { PALETTE_CLASS_RE, blankComments };
44
+ // A product built on a kit (MUI, Mantine, Chakra UI, Ant Design), 8.4.6: a
45
+ // colour or a pixel size written onto a kit component where the theme has a
46
+ // value. kitPaintFindings(fileText, system.profile.kit, { file }) returns the
47
+ // worded findings the report's live checks give for the same file, with the
48
+ // index of each in the text; null when the file is not a kit file, { exempt }
49
+ // when it is not judged. Only meaningful when system.profile.kit is set.
50
+ export { kitPaintFindings };
39
51
 
40
52
  // Radius, font size, shadow and typeface: the patterns, so both checkers agree
41
53
  // on what a declaration is and what counts as a disciplined value.
@@ -78,6 +90,10 @@ export function learnSystem(repoRoot, { exclude = [] } = {}) {
78
90
  const profile = profileRepo(repoRoot, files);
79
91
  decideProfile(profile, components, files, repoRoot);
80
92
  const P = profileOf(profile);
93
+ // the kit the product is built on, with its definition attached, the way
94
+ // the MCP knowledge reads it (mcp/knowledge.mjs): the theme's colours are
95
+ // the token set on a kit repo, whatever stylesheet holds the most literals
96
+ const kit = P.isKit && profile.kit && KITS[profile.kit.name] ? { ...profile.kit, def: KITS[profile.kit.name] } : null;
81
97
  const widgetDirs = files.code.filter((f) => /(^|\/)tailwind\.config\.[mc]?[jt]s$/.test(f))
82
98
  .filter((f) => WIDGET_CONFIG_RE.test(readSource(join(repoRoot, f)) ?? ''))
83
99
  .map((f) => f.slice(0, f.lastIndexOf('/') + 1));
@@ -104,7 +120,7 @@ export function learnSystem(repoRoot, { exclude = [] } = {}) {
104
120
  // report counts a token's base statement only, because a dark theme is
105
121
  // the system working rather than sprawl; a guard needs the opposite, the
106
122
  // full set, or it cannot recognise a dark-theme value as on-system.
107
- tokens: t.tokenColors ?? t.colors.filter((c) => c.isToken).map((c) => c.value),
123
+ tokens: [...new Set([...(t.tokenColors ?? t.colors.filter((c) => c.isToken).map((c) => c.value)), ...(kit?.themeValues ?? [])])],
108
124
  tokenNames,
109
125
  spacing: t.spacing,
110
126
  radii: t.radii,
@@ -129,6 +145,9 @@ export function learnSystem(repoRoot, { exclude = [] } = {}) {
129
145
  // only a shadcn kit has installed code; a hand-written components/ui on a
130
146
  // plain product is the team's own (the report splits nothing there either)
131
147
  installedDirs: P.isShadcn && !P.isRegistry ? installedDirs(P) : [],
148
+ // a product built on a kit: name, theme files and values, spacing step,
149
+ // the team's own layers over it, and the definition kitPaintFindings reads
150
+ kit,
132
151
  // a shadcn kit whose theme file holds the variables, in CSS-variable
133
152
  // mode: a palette class in own code is paint from a tin
134
153
  paletteReady: P.isShadcn && (P.shadcn?.sheet?.shadcnPresent ?? 0) >= 5 && P.designSystem?.cssVariables !== false,
@@ -234,3 +234,47 @@ export function countKitPaint(root, codeFiles, { importRe: kitImportRe, themeRe,
234
234
  exempt: exempt.slice(0, 20),
235
235
  };
236
236
  }
237
+
238
+ const aKit = (name) => (/^(?:[AEIOU]|MUI)/.test(name) ? `an ${name}` : `a ${name}`);
239
+ const plain = (html) => String(html ?? '').replace(/<\/?code>/g, '').replace(/&quot;/g, '"');
240
+
241
+ /**
242
+ * One file's kit findings, worded: what the MCP validate and review say about
243
+ * a line, and what guard-my-design-system says about the same line. One
244
+ * function, so the three never disagree. `kit` is the profile's kit with its
245
+ * definition attached ({ name, def, themeFiles, themeValues, spacingUnit,
246
+ * layers }). Returns null when the file is not a kit file or is the theme;
247
+ * { exempt } when the file is not judged, and why; otherwise
248
+ * { exempt: null, findings: [{ rule, index, value, label, note, message, fix }] }:
249
+ * label is the finding in a few words ("colour written onto an MUI
250
+ * component"), note what the theme says about it or null, message the two
251
+ * as one sentence for the live checks.
252
+ */
253
+ export function kitPaintFindings(src, kit, { file = null } = {}) {
254
+ if (!kit?.def) return null;
255
+ const paint = kitPaintInSource(src, kit.def, { file, layers: kit.layers ?? [] });
256
+ if (!paint) return null;
257
+ if (paint.exempt) return { exempt: paint.exempt, findings: [] };
258
+ const adv = kit.def.advice, themeFile = kit.themeFiles?.[0] ?? null;
259
+ const themeSet = new Set(kit.themeValues ?? []);
260
+ const findings = [];
261
+ const colourLabel = `colour written onto ${aKit(kit.name)} component`;
262
+ for (const c of paint.colours) {
263
+ findings.push(themeSet.has(c.value)
264
+ ? { rule: 'kit-colour', index: c.index, value: c.value, label: colourLabel,
265
+ note: `the theme already holds it${themeFile ? ` (${themeFile})` : ''}`,
266
+ message: `Colour ${c.value} written onto ${aKit(kit.name)} component. The theme already holds it${themeFile ? ` (${themeFile})` : ''}.`,
267
+ fix: plain(adv.colourHow) }
268
+ : { rule: 'kit-colour', index: c.index, value: c.value, label: colourLabel,
269
+ note: 'the theme has no such colour',
270
+ message: `Colour ${c.value} written onto ${aKit(kit.name)} component, and the theme has no such colour.`,
271
+ fix: `Add it to the theme once${themeFile ? ` (${themeFile})` : ` with ${adv.themeCall}`}, then read it there. ${plain(adv.colourHow)}` });
272
+ }
273
+ for (const h of paint.px) {
274
+ const raw = h.value.split(': ')[1];
275
+ findings.push({ rule: 'kit-px', index: h.index, value: h.value, label: `pixel size on ${aKit(kit.name)} component`, note: null,
276
+ message: `Pixel size ${h.value} on ${aKit(kit.name)} component.`,
277
+ fix: adv.step ? adv.step(kit, parseFloat(raw)) : plain(adv.spacingHow?.(kit)) });
278
+ }
279
+ return { exempt: null, findings };
280
+ }
@@ -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.4.4';
4
+ export const VERSION = '8.4.6';
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.
@@ -16,7 +16,7 @@ import { hexRgb } from '../lib/nearpairs.mjs';
16
16
  import { exemptReason } from '../lib/exempt.mjs';
17
17
  import { extraDeclarations, fontDeclarations } from '../lib/declarations.mjs';
18
18
  import { typefaceOf, GENERIC_FONTS } from '../lib/typefaces.mjs';
19
- import { kitPaintInSource } from '../lib/kitpaint.mjs';
19
+ import { kitPaintFindings } from '../lib/kitpaint.mjs';
20
20
  import { PALETTE_CLASS_RE, GREY_HUE_RE, DARK_WB_RE, DEMO_PATH_RE, blankComments } from '../harvest/paint.mjs';
21
21
 
22
22
  // What this engine measures — shipped with every result, clean or not.
@@ -41,8 +41,6 @@ export function checksFor(k) {
41
41
  return CHECKS;
42
42
  }
43
43
  // "an MUI component", "an Ant Design component", "a Mantine component"
44
- const aKit = (name) => (/^(?:[AEIOU]|MUI)/.test(name) ? `an ${name}` : `a ${name}`);
45
- const plain = (html) => String(html ?? '').replace(/<\/?code>/g, '').replace(/&quot;/g, '"');
46
44
 
47
45
  const CSS_FILE_RE = /\.(css|scss|sass|less|styl)$/i;
48
46
  const lineOf = (text, index) => text.slice(0, index).split('\n').length;
@@ -115,29 +113,12 @@ export function validateContent(content, k) {
115
113
  // compare, SVG artwork), so the generic check stays out of its way.
116
114
  let kitOwnsColours = false;
117
115
  if (k.kit?.def && !css) {
118
- const paint = kitPaintInSource(text, k.kit.def, { file, layers: k.kit.layers ?? [] });
119
- if (paint && !paint.exempt) {
116
+ const judged = kitPaintFindings(text, k.kit, { file });
117
+ if (judged && !judged.exempt) {
120
118
  kitOwnsColours = true;
121
- const kitName = k.kit.name, adv = k.kit.def.advice, themeFile = k.kit.themeFiles?.[0] ?? null;
122
- const themeSet = new Set(k.kit.themeValues ?? []);
123
- for (const c of paint.colours) {
124
- if (themeSet.has(c.value)) {
125
- add('kit-colour', 'violation', c.index,
126
- `Colour ${c.value} written onto ${aKit(kitName)} component. The theme already holds it${themeFile ? ` (${themeFile})` : ''}.`,
127
- plain(adv.colourHow));
128
- } else {
129
- add('kit-colour', 'violation', c.index,
130
- `Colour ${c.value} written onto ${aKit(kitName)} component, and the theme has no such colour.`,
131
- `Add it to the theme once${themeFile ? ` (${themeFile})` : ` with ${adv.themeCall}`}, then read it there. ${plain(adv.colourHow)}`);
132
- }
133
- }
134
- for (const h of paint.px) {
135
- const [prop, raw] = h.value.split(': ');
136
- kitPx.add(raw);
137
- add('kit-px', 'violation', h.index,
138
- `Pixel size ${h.value} on ${aKit(kitName)} component.`,
139
- adv.step ? adv.step(k.kit, parseFloat(raw)) : plain(adv.spacingHow?.(k.kit)));
140
- void prop;
119
+ for (const f of judged.findings) {
120
+ if (f.rule === 'kit-px') kitPx.add(f.value.split(': ')[1]);
121
+ add(f.rule, 'violation', f.index, f.message, f.fix);
141
122
  }
142
123
  }
143
124
  }