roast-my-design-system 8.6.0 → 8.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "8.6.0",
3
+ "version": "8.6.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": [
@@ -11,6 +11,7 @@
11
11
  * use have no wrong pick, and saying otherwise would be inventing one.
12
12
  */
13
13
  import { posix } from 'node:path';
14
+ import { extractStyling } from '../harvest/tokens.mjs';
14
15
 
15
16
  const CODE_EXT_RE = /\.(?:tsx|ts|jsx|js|mjs|cjs|vue|svelte)$/;
16
17
  const stem = (f) => f.replace(CODE_EXT_RE, '').replace(/\/index$/, '');
@@ -57,6 +58,32 @@ export function canonicalCopy(copies) {
57
58
  return top;
58
59
  }
59
60
 
61
+ /**
62
+ * The copies of each duplicated name, with their usage and the colours each
63
+ * non-canonical copy hard-codes: the `dupes` map avoidedImportFindings reads.
64
+ * `hardDupes` is findDuplicates' exactDuplicates minus wrapped pairs;
65
+ * `components` the harvested ledger; `read(file)` a file's text or null.
66
+ * Shared by the MCP knowledge and the guard doorway.
67
+ */
68
+ export function dupeCopiesOf(hardDupes, components, read) {
69
+ const dupes = new Map();
70
+ for (const d of hardDupes) {
71
+ const paths = d.files.map((f) => (typeof f === 'string' ? f : f.file));
72
+ const copies = paths.map((file) => ({
73
+ file, usageCount: components.find((c) => c.name === d.name && c.file === file)?.usageCount ?? 0,
74
+ }));
75
+ const canon = canonicalCopy(copies);
76
+ const strays = new Map();
77
+ for (const file of paths) {
78
+ if (file === canon?.file) continue;
79
+ const text = read(file);
80
+ if (text) strays.set(file, [...new Set(extractStyling(text).colors.map((c) => c.value))]);
81
+ }
82
+ dupes.set(d.name, { copies, strays });
83
+ }
84
+ return dupes;
85
+ }
86
+
60
87
  /**
61
88
  * Findings for imports of a non-canonical duplicate that the change ADDS.
62
89
  * @param dupes Map<name, { copies: [{ file, usageCount }], strays?: Map<file, [colour]> }>
@@ -71,6 +71,11 @@ export const CONTENT_CSS_NAME_RE = /(^|\/)_?(?:highlight(?:-\w+)?|hljs(?:-theme)
71
71
  export const EXTENSION_CSS_PATH_RE = /(^|\/)(?:chrome-extension|browser-extension|webextension)\/[\s\S]*\/(?:content|inject)[\w-]*\.(?:css|scss|less)$|(^|\/)content[_-]?script[\w-]*\.(?:css|scss|less)$/i;
72
72
  // minified: one very long line, whoever shipped it
73
73
  export const minifiedCss = (text) => text.split('\n').some((l) => l.length > 2000);
74
+ // Tailwind's own build output, committed: it opens with the licence banner and
75
+ // restates every --tw-* internal. Cal.com keeps a 343 KB one, pretty-printed so
76
+ // the minified test misses it, and until 8.6.2 the twin check paired a new
77
+ // theme token with --tw-ring-offset-color from it (benchmark, 2026-09-25).
78
+ export const COMPILED_TAILWIND_RE = /^\s*\/\*!\s*tailwindcss v\d/;
74
79
 
75
80
  /** Why this stylesheet is not the product's own, or null. */
76
81
  export function foreignStylesheet(file, text = '') {
@@ -78,6 +83,7 @@ export function foreignStylesheet(file, text = '') {
78
83
  if (/\.module\.(?:css|scss|less)$/i.test(file)) return null;
79
84
  if (VENDOR_CSS_NAME_RE.test(file)) return "a library's stylesheet kept in the repo";
80
85
  if (minifiedCss(text)) return 'a minified stylesheet, shipped rather than written here';
86
+ if (COMPILED_TAILWIND_RE.test(text)) return "Tailwind's compiled output, generated from the source rather than written here";
81
87
  if (CONTENT_CSS_NAME_RE.test(file)) return 'a code or markdown theme: it styles content, not the interface';
82
88
  if (EXTENSION_CSS_PATH_RE.test(file)) return "CSS injected into other people's pages by a browser extension";
83
89
  return null;
@@ -17,6 +17,9 @@ import { kitPaintFindings } from './kitpaint.mjs';
17
17
  import { PALETTE_CLASS_RE, blankComments } from '../harvest/paint.mjs';
18
18
  import { harvestTokens, extractStyling, normalizeHex, isGrey } from '../harvest/tokens.mjs';
19
19
  import { harvestComponents, definedComponents } from '../harvest/components.mjs';
20
+ import { findDuplicates } from '../harvest/duplicates.mjs';
21
+ import { repoTokenDefs } from './tokentwins.mjs';
22
+ import { dupeCopiesOf } from './avoidedimports.mjs';
20
23
  import { loadExclusions } from './exclusions.mjs';
21
24
  import { WIDGET_CONFIG_RE } from './exempt.mjs';
22
25
  import { hexRgb } from './nearpairs.mjs';
@@ -86,6 +89,8 @@ export const isStyleFile = (p) => STYLE_EXTS.has(extname(p));
86
89
  * fontFamilies, // [{ value, count, files }] every family declared
87
90
  * tailwind, // { colors, spacing, radii, textSizes, arbitrary }
88
91
  * components, // [{ name, file, usageCount, isPage }] the ledger
92
+ * tokenDefs, // [{ name, file, value, canon, darkValue?, darkCanon? }] colour tokens
93
+ * duplicates, // Map<name, { copies: [{ file, usageCount }], strays }>
89
94
  * files, // { styles: n, code: n } — how much was read
90
95
  * }
91
96
  */
@@ -93,10 +98,14 @@ export function learnSystem(repoRoot, { exclude = [] } = {}) {
93
98
  const exclusions = loadExclusions(repoRoot, exclude);
94
99
  const files = walkRepo(repoRoot, 14, exclusions);
95
100
  const t = harvestTokens(repoRoot, files.styles, files.code);
96
- const components = harvestComponents(repoRoot, files.code).components
97
- .map(({ name, file, usageCount, isPage }) => ({ name, file, usageCount, isPage }));
101
+ const ledger = harvestComponents(repoRoot, files.code).components;
102
+ const components = ledger.map(({ name, file, usageCount, isPage }) => ({ name, file, usageCount, isPage }));
98
103
  const profile = profileRepo(repoRoot, files);
99
104
  decideProfile(profile, components, files, repoRoot);
105
+ // the same two lists the MCP knowledge builds (mcp/knowledge.mjs), from the
106
+ // same helpers, for tokenTwinFindings and avoidedImportFindings (8.6.1)
107
+ const read = (f) => readSource(join(repoRoot, f));
108
+ const hardDupes = (findDuplicates(ledger, profile.uiDir, repoRoot).exactDuplicates ?? []).filter((d) => !d.wrapped);
100
109
  const P = profileOf(profile);
101
110
  // the kit the product is built on, with its definition attached, the way
102
111
  // the MCP knowledge reads it (mcp/knowledge.mjs): the theme's colours are
@@ -140,6 +149,12 @@ export function learnSystem(repoRoot, { exclude = [] } = {}) {
140
149
  // it lives, how much the repo leans on it, and whether it is a page (pages
141
150
  // are routes, not reusable parts, so two of a name is not a duplicate).
142
151
  components,
152
+ // every colour token each stylesheet defines, light and dark: the
153
+ // `others` tokenTwinFindings compares a new token with (8.6.1)
154
+ tokenDefs: repoTokenDefs(files.styles, read),
155
+ // each duplicated name's copies, their usage and the colours each
156
+ // non-canonical copy hard-codes: the `dupes` avoidedImportFindings reads
157
+ duplicates: dupeCopiesOf(hardDupes, ledger, read),
143
158
  files: { styles: files.styles.length, code: files.code.length },
144
159
  // How the repo was read (7.1 to 7.7), decided once by the same profiles
145
160
  // the report uses, so a guard treats installed code, a registry's
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { canonical, oklab } from './color.mjs';
23
23
  import { KNOWN_ROWS } from '../profiles/shadcn-data.mjs';
24
+ import { foreignStylesheet } from './exempt.mjs';
24
25
 
25
26
  export const TWIN_STEPS = 8;
26
27
  // Two tokens are deliberate, so a pair of them must also be closer than the
@@ -102,6 +103,9 @@ export function tokenDefsOf(raw) {
102
103
  const defs = new Map();
103
104
  for (const m of text.matchAll(/(--[\w-]+)\s*:\s*([^;{}]+)[;}]/g)) {
104
105
  const [, name, rawValue] = m;
106
+ // Tailwind's internals (--tw-ring-offset-color, --tw-prose-*) are its
107
+ // plumbing, set per element; no one picks them as a design decision
108
+ if (/^--tw-/.test(name)) continue;
105
109
  const value = rawValue.trim();
106
110
  const canon = canonical(value);
107
111
  if (!canon) continue;
@@ -116,6 +120,24 @@ export function tokenDefsOf(raw) {
116
120
  return defs;
117
121
  }
118
122
 
123
+ /**
124
+ * Every colour token the repo's stylesheets define, one entry per name per
125
+ * file: [{ name, file, value, canon, darkValue?, darkCanon? }]. `read(file)`
126
+ * returns a file's text or null. Shared by the MCP knowledge and the guard
127
+ * doorway, so both compare a new token with the same list.
128
+ */
129
+ export function repoTokenDefs(styleFiles, read) {
130
+ const out = [];
131
+ for (const file of styleFiles) {
132
+ const text = read(file);
133
+ if (!text || !text.includes('--')) continue;
134
+ // a library's CSS or a compiled build: the report leaves it out, so does this
135
+ if (foreignStylesheet(file, text)) continue;
136
+ for (const [name, e] of tokenDefsOf(text)) out.push({ name, file, ...e });
137
+ }
138
+ return out;
139
+ }
140
+
119
141
  /**
120
142
  * The existing token a token is a twin of, or null. `pool` is
121
143
  * [{ name, canon, value, darkCanon?, darkValue?, file? }].
@@ -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.6.0';
4
+ export const VERSION = '8.6.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.
@@ -14,7 +14,7 @@ import { statSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
  import { walkRepo, profileRepo, readSource } from '../harvest/walk.mjs';
16
16
  import { harvestComponents } from '../harvest/components.mjs';
17
- import { harvestTokens, extractStyling } from '../harvest/tokens.mjs';
17
+ import { harvestTokens } from '../harvest/tokens.mjs';
18
18
  import { findDuplicates } from '../harvest/duplicates.mjs';
19
19
  import { harvestContext } from '../harvest/context.mjs';
20
20
  import { loadExclusions } from '../lib/exclusions.mjs';
@@ -24,8 +24,8 @@ import { typefaceOf } from '../lib/typefaces.mjs';
24
24
  import { hexRgb } from '../lib/nearpairs.mjs';
25
25
  import { decideProfile, profileOf } from '../profiles/index.mjs';
26
26
  import { KITS } from '../profiles/kit-common.mjs';
27
- import { tokenDefsOf } from '../lib/tokentwins.mjs';
28
- import { canonicalCopy } from '../lib/avoidedimports.mjs';
27
+ import { repoTokenDefs } from '../lib/tokentwins.mjs';
28
+ import { dupeCopiesOf } from '../lib/avoidedimports.mjs';
29
29
 
30
30
  const MAX_DEPTH = 14; // same ruler as the harvest CLI
31
31
 
@@ -123,28 +123,11 @@ export function loadKnowledge(root) {
123
123
 
124
124
  // The copies of each duplicated name with their usage, and the colours each
125
125
  // copy hard-codes: what the avoided-import check names (lib/avoidedimports).
126
- const dupeCopies = new Map();
127
- for (const d of hardDupes) {
128
- const paths = d.files.map((f) => (typeof f === 'string' ? f : f.file));
129
- const copies = paths.map((file) => ({ file, usageCount: (byName.get(d.name) ?? []).find((c) => c.file === file)?.usageCount ?? 0 }));
130
- const canon = canonicalCopy(copies);
131
- const strays = new Map();
132
- for (const file of paths) {
133
- if (file === canon?.file) continue;
134
- const text = readSource(join(root, file));
135
- if (text) strays.set(file, [...new Set(extractStyling(text).colors.map((c) => c.value))]);
136
- }
137
- dupeCopies.set(d.name, { copies, strays });
138
- }
139
-
126
+ const read = (file) => readSource(join(root, file));
127
+ const dupeCopies = dupeCopiesOf(hardDupes, components, read);
140
128
  // Every colour token each stylesheet defines, light and dark, so a new one
141
129
  // can be compared with the ones already there (lib/tokentwins).
142
- const tokenDefs = [];
143
- for (const file of files.styles) {
144
- const text = readSource(join(root, file));
145
- if (!text || !text.includes('--')) continue;
146
- for (const [name, e] of tokenDefsOf(text)) tokenDefs.push({ name, file, ...e });
147
- }
130
+ const tokenDefs = repoTokenDefs(files.styles, read);
148
131
 
149
132
  return {
150
133
  root,