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 +1 -1
- package/skills/roast-my-design-system/scripts/lib/avoidedimports.mjs +27 -0
- package/skills/roast-my-design-system/scripts/lib/exempt.mjs +6 -0
- package/skills/roast-my-design-system/scripts/lib/guard-api.mjs +17 -2
- package/skills/roast-my-design-system/scripts/lib/tokentwins.mjs +22 -0
- package/skills/roast-my-design-system/scripts/lib/version.mjs +1 -1
- package/skills/roast-my-design-system/scripts/mcp/knowledge.mjs +6 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "8.6.
|
|
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
|
|
97
|
-
|
|
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.
|
|
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
|
|
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 {
|
|
28
|
-
import {
|
|
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
|
|
127
|
-
|
|
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,
|