roast-my-design-system 7.0.0 → 7.1.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 +3 -3
- package/package.json +1 -1
- package/skills/roast-my-design-system/scripts/diagnose/index.mjs +6 -4
- package/skills/roast-my-design-system/scripts/diagnose/score.mjs +6 -5
- package/skills/roast-my-design-system/scripts/harvest/index.mjs +6 -31
- package/skills/roast-my-design-system/scripts/lib/version.mjs +1 -1
- package/skills/roast-my-design-system/scripts/mcp/knowledge.mjs +6 -2
- package/skills/roast-my-design-system/scripts/profiles/index.mjs +90 -0
- package/skills/roast-my-design-system/scripts/profiles/library.mjs +29 -0
- package/skills/roast-my-design-system/scripts/profiles/product.mjs +21 -0
- package/skills/roast-my-design-system/scripts/rules/build.mjs +5 -4
- package/skills/roast-my-design-system/scripts/sarif/index.mjs +3 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<img src="https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/roaster_logo_300px.png?v=7.
|
|
1
|
+
<img src="https://raw.githubusercontent.com/gregkozakiewicz/roast-my-design-system/main/assets/roaster_logo_300px.png?v=7.1.0" width="72" alt="roast-my-design-system">
|
|
2
2
|
|
|
3
3
|
# roast-my-design-system
|
|
4
4
|
|
|
@@ -73,11 +73,11 @@ Not to be confused with each other: **"Why this matters"** is generic, ships wit
|
|
|
73
73
|
|
|
74
74
|
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:
|
|
75
75
|
|
|
76
|
-

|
|
77
77
|
|
|
78
78
|
The same report in light mode (one file, built-in toggle):
|
|
79
79
|
|
|
80
|
-

|
|
81
81
|
|
|
82
82
|
## What makes the numbers trustworthy
|
|
83
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.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": [
|
|
@@ -32,6 +32,7 @@ import { fixPrompt } from '../lib/fixprompt.mjs';
|
|
|
32
32
|
import { WHY } from './why.mjs';
|
|
33
33
|
import { parseColor, luminance, isGrey } from '../lib/color.mjs';
|
|
34
34
|
import { loadBenchmark, benchHelpers, makeHealthOf, coreMetrics, tileHealths, scoreOfTiles, scorePackage as scorePackageOf, ZERO_IDEAL, WARN_TOLERANCE, SCORE_OF, SCHEMA_VERSION } from './score.mjs';
|
|
35
|
+
import { profileOf } from '../profiles/index.mjs';
|
|
35
36
|
|
|
36
37
|
// The benchmark and every judgement made against it live in score.mjs; this
|
|
37
38
|
// file only draws. The same numbers reach summary.json through scoreHarvest.
|
|
@@ -243,15 +244,16 @@ const nearPairs = nearColorPairs(colors);
|
|
|
243
244
|
// rather than wallpaper. Theme variants are not in here; those are the system.
|
|
244
245
|
const collisions = h.tokens.tokenCollisions ?? [];
|
|
245
246
|
|
|
246
|
-
const
|
|
247
|
+
const P = profileOf(h);
|
|
248
|
+
const neverImported = neverImportedComponents(h.components, P.uiDir);
|
|
247
249
|
|
|
248
|
-
// Measurability and
|
|
250
|
+
// Measurability and kind, decided once in profiles/ (telekom/scale, 2026-09-01):
|
|
249
251
|
// when the component detector could not read this repo's stack, the component
|
|
250
252
|
// metrics say so and take no score credit; when the repo is a published
|
|
251
253
|
// library, usage means composition (how the system uses itself), never
|
|
252
254
|
// adoption, and every accusation about orphans is softened to match.
|
|
253
255
|
const { componentsMeasured, isLibrary, vendoredUi } = M;
|
|
254
|
-
const notMeasuredReason =
|
|
256
|
+
const notMeasuredReason = P.notMeasuredReason;
|
|
255
257
|
// A shadcn ui folder is a vendored catalogue: `shadcn add` copies the source
|
|
256
258
|
// in, people take the whole set at once, and an unused component there is
|
|
257
259
|
// stock on a shelf rather than something a team built and abandoned. It is
|
|
@@ -1767,7 +1769,7 @@ if (summaryPath) {
|
|
|
1767
1769
|
score: healthScore,
|
|
1768
1770
|
noSystemLikely,
|
|
1769
1771
|
verdict,
|
|
1770
|
-
role:
|
|
1772
|
+
role: P.kind,
|
|
1771
1773
|
componentsMeasured,
|
|
1772
1774
|
metrics: (({ colors, colorTokens, colorStrays, greys, greyStrays, spacing, exactDuplicates, inlineStyles, nearPairs, important, neverImported, arbitrary, tokenLed }) =>
|
|
1773
1775
|
({ colors, colorTokens, colorStrays, greys, greyStrays, spacing, exactDuplicates, inlineStyles, nearPairs, important, neverImported, arbitrary, tokenLed }))(M),
|
|
@@ -26,6 +26,7 @@ import { isGrey } from '../lib/color.mjs';
|
|
|
26
26
|
import { nearColorPairs } from '../lib/nearpairs.mjs';
|
|
27
27
|
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
28
28
|
import { SCHEMA_VERSION } from '../lib/version.mjs';
|
|
29
|
+
import { profileOf } from '../profiles/index.mjs';
|
|
29
30
|
|
|
30
31
|
export { SCHEMA_VERSION };
|
|
31
32
|
|
|
@@ -131,14 +132,14 @@ export function coreMetrics(h) {
|
|
|
131
132
|
inlineStyles: h.tokens?.inlineStyles?.count ?? 0,
|
|
132
133
|
nearPairs: nearColorPairs(colors).length,
|
|
133
134
|
important: h.tokens?.important?.count ?? 0,
|
|
134
|
-
neverImported: neverImportedComponents(h.components, h.
|
|
135
|
+
neverImported: neverImportedComponents(h.components, profileOf(h).uiDir).length,
|
|
135
136
|
arbitrary: (h.tokens?.tailwind?.arbitrary ?? []).reduce((sum, a) => sum + a.count, 0),
|
|
136
|
-
// Measurability and
|
|
137
|
+
// Measurability and kind, decided once in profiles/: an unreadable stack
|
|
137
138
|
// takes no score credit; a library's orphans are shown, not judged; a
|
|
138
139
|
// vendored shadcn catalogue is stock on a shelf, not abandonment.
|
|
139
|
-
componentsMeasured: h.
|
|
140
|
-
isLibrary: h.
|
|
141
|
-
vendoredUi: h.
|
|
140
|
+
componentsMeasured: profileOf(h).componentsMeasured,
|
|
141
|
+
isLibrary: profileOf(h).isLibrary,
|
|
142
|
+
vendoredUi: profileOf(h).vendoredUi,
|
|
142
143
|
};
|
|
143
144
|
}
|
|
144
145
|
|
|
@@ -27,6 +27,7 @@ import { ruleStaleness } from '../lib/staleness.mjs';
|
|
|
27
27
|
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
28
28
|
import { lastTouchedDates } from '../lib/lasttouched.mjs';
|
|
29
29
|
import { SCHEMA_VERSION } from '../lib/version.mjs';
|
|
30
|
+
import { decideProfile } from '../profiles/index.mjs';
|
|
30
31
|
|
|
31
32
|
function arg(name, fallback) {
|
|
32
33
|
const i = process.argv.indexOf(`--${name}`);
|
|
@@ -63,37 +64,11 @@ const staleRules = ruleStaleness(target, components,
|
|
|
63
64
|
new Set(neverImportedComponents(components, profile.uiDir).map((c) => c.name)),
|
|
64
65
|
[...files.code, ...files.styles, ...files.other]);
|
|
65
66
|
|
|
66
|
-
// Repo
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
// component metrics are declared NOT MEASURED instead of scoring zeros as
|
|
72
|
-
// discipline (the telekom/scale lesson, 2026-09-01).
|
|
73
|
-
{
|
|
74
|
-
const reusableCount = components.filter((c) => !c.isPage).length;
|
|
75
|
-
// A web-components repo with many tag-registered components and no pages
|
|
76
|
-
// is a library by construction, whatever its packages are named (Ionic's
|
|
77
|
-
// @ionic/core and Material Web both dodge the name rule).
|
|
78
|
-
// A handful of pages next to hundreds of components is a demo or test app
|
|
79
|
-
// riding in the library's monorepo (Siemens iX ships two), not a product.
|
|
80
|
-
const pageCount = components.length - reusableCount;
|
|
81
|
-
const fewPages = pageCount <= reusableCount * 0.05;
|
|
82
|
-
profile.role = (profile.libraryPkg && fewPages && reusableCount >= 5)
|
|
83
|
-
|| (String(profile.framework).startsWith('web components') && fewPages && reusableCount >= 10)
|
|
84
|
-
? 'library' : 'product';
|
|
85
|
-
// The gate fires on the situation, never a list of known frameworks: a
|
|
86
|
-
// repo with real code volume where the detector found almost nothing is a
|
|
87
|
-
// repo we could not read, whatever it is written in (the Shoelace lesson:
|
|
88
|
-
// its .define() idiom was invisible and the named-framework gate stayed
|
|
89
|
-
// silent, which would have shipped blind zeros as discipline again).
|
|
90
|
-
// Either trigger suffices: a named unreadable stack (Vue/Angular/Svelte
|
|
91
|
-
// markers, however small the repo), or sheer volume the detector saw
|
|
92
|
-
// nothing in (the Shoelace near-miss: unknown idiom, no marker).
|
|
93
|
-
profile.componentDetection = reusableCount < 3 && (profile.unreadableComponentStack || files.code.length >= 40)
|
|
94
|
-
? { measured: false, reason: profile.unreadableComponentStack ?? 'a component pattern this scan cannot read' }
|
|
95
|
-
: { measured: true };
|
|
96
|
-
}
|
|
67
|
+
// Repo kind and measurability, decided once in profiles/ and read everywhere
|
|
68
|
+
// (a published components package with no app pages is a LIBRARY; a stack the
|
|
69
|
+
// detector cannot read is NOT MEASURED, never scored as zeros). The decision
|
|
70
|
+
// lands on the profile with its evidence, so the JSON says why.
|
|
71
|
+
decideProfile(profile, components, files);
|
|
97
72
|
|
|
98
73
|
// Orphans carry a receipt: the last time git saw anyone touch the file.
|
|
99
74
|
// Only never-imported components are dated (the adoption map's evidence);
|
|
@@ -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 = '7.
|
|
4
|
+
export const VERSION = '7.1.0';
|
|
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,6 +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
26
|
|
|
26
27
|
const MAX_DEPTH = 14; // same ruler as the harvest CLI
|
|
27
28
|
|
|
@@ -45,6 +46,9 @@ export function loadKnowledge(root) {
|
|
|
45
46
|
const files = walkRepo(root, MAX_DEPTH, exclusions);
|
|
46
47
|
const profile = profileRepo(root, files);
|
|
47
48
|
const { components } = harvestComponents(root, files.code);
|
|
49
|
+
// The MCP path used to skip the kind decision the CLI harvest makes, so a
|
|
50
|
+
// library read as a product here. Same call, same answer, both doors.
|
|
51
|
+
decideProfile(profile, components, files);
|
|
48
52
|
const tokens = harvestTokens(root, files.styles, files.code);
|
|
49
53
|
const duplicates = findDuplicates(components, profile.uiDir, root);
|
|
50
54
|
const context = harvestContext(root);
|
|
@@ -120,8 +124,8 @@ export function loadKnowledge(root) {
|
|
|
120
124
|
canonical,
|
|
121
125
|
dupeByName,
|
|
122
126
|
neverImported,
|
|
123
|
-
// stock, not debt: see
|
|
124
|
-
vendoredUi: profile.vendoredUi
|
|
127
|
+
// stock, not debt: see profiles/index.mjs
|
|
128
|
+
vendoredUi: profileOf(profile).vendoredUi,
|
|
125
129
|
agentFiles: (context ?? []).filter((c) => c.kind === 'agent-rules'),
|
|
126
130
|
};
|
|
127
131
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profiles — what kind of repo is this, decided once and read everywhere.
|
|
3
|
+
*
|
|
4
|
+
* The scanner used to answer "is this a library?", "is this a vendored
|
|
5
|
+
* catalogue?", "can I measure components here?" in three separate places:
|
|
6
|
+
* the profiler in harvest/walk.mjs, a block inside harvest/index.mjs, and
|
|
7
|
+
* every consumer re-deriving flags from the raw profile. The MCP path never
|
|
8
|
+
* ran the harvest block at all, so it had no role. Each new kind of repo
|
|
9
|
+
* would have meant one more "unless" clause in each counter, and that is how
|
|
10
|
+
* a scanner stops meaning one thing.
|
|
11
|
+
*
|
|
12
|
+
* The shape now: one file per kind under profiles/, each answering the same
|
|
13
|
+
* questions in the same order. `decideProfile` picks the first kind that
|
|
14
|
+
* recognises the repo (product is the fallback and always matches), records
|
|
15
|
+
* how sure it is and why, and writes the decision onto the harvest profile.
|
|
16
|
+
* `profileOf` is the one accessor consumers read. No counter, report or rules
|
|
17
|
+
* builder asks the raw flags any more.
|
|
18
|
+
*
|
|
19
|
+
* Step 2 of the shadcn-profile plan: this move changes no number. Every
|
|
20
|
+
* fixture's expected output is byte-identical before and after.
|
|
21
|
+
*/
|
|
22
|
+
import library from './library.mjs';
|
|
23
|
+
import product from './product.mjs';
|
|
24
|
+
|
|
25
|
+
// Specific kinds first; product is the fallback and must stay last.
|
|
26
|
+
export const PROFILES = [library, product];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decide the repo's kind from the profiler's facts and what the scan found.
|
|
30
|
+
* Mutates `profile` (the harvest.profile object) so the JSON on disk carries
|
|
31
|
+
* the decision: `role` (the field consumers and summary.json already read),
|
|
32
|
+
* `componentDetection` (measurability), and the new `kind`, `kindConfidence`
|
|
33
|
+
* and `kindEvidence` (the receipt: why this kind, in words).
|
|
34
|
+
*/
|
|
35
|
+
export function decideProfile(profile, components, files) {
|
|
36
|
+
const reusable = components.filter((c) => !c.isPage);
|
|
37
|
+
const counts = {
|
|
38
|
+
reusable: reusable.length,
|
|
39
|
+
pages: components.length - reusable.length,
|
|
40
|
+
codeFiles: files.code.length,
|
|
41
|
+
};
|
|
42
|
+
// A handful of pages next to hundreds of components is a demo or test app
|
|
43
|
+
// riding in a library's monorepo (Siemens iX ships two), not a product.
|
|
44
|
+
counts.fewPages = counts.pages <= counts.reusable * 0.05;
|
|
45
|
+
|
|
46
|
+
let picked = product, decision = null;
|
|
47
|
+
for (const p of PROFILES) {
|
|
48
|
+
const d = p.recognise(profile, counts);
|
|
49
|
+
if (d) { picked = p; decision = d; break; }
|
|
50
|
+
}
|
|
51
|
+
profile.role = picked.kind;
|
|
52
|
+
profile.kind = picked.kind;
|
|
53
|
+
profile.kindConfidence = decision.confidence;
|
|
54
|
+
profile.kindEvidence = decision.evidence;
|
|
55
|
+
|
|
56
|
+
// Measurability. The gate fires on the situation, never a list of known
|
|
57
|
+
// frameworks: a repo with real code volume where the detector found almost
|
|
58
|
+
// nothing is a repo we could not read, whatever it is written in (the
|
|
59
|
+
// Shoelace lesson: its .define() idiom was invisible and the named-framework
|
|
60
|
+
// gate stayed silent, which would have shipped blind zeros as discipline).
|
|
61
|
+
// Either trigger suffices: a named unreadable stack (Vue/Angular/Svelte
|
|
62
|
+
// markers, however small the repo), or sheer volume the detector saw
|
|
63
|
+
// nothing in (the Shoelace near-miss: unknown idiom, no marker).
|
|
64
|
+
profile.componentDetection = counts.reusable < 3 && (profile.unreadableComponentStack || counts.codeFiles >= 40)
|
|
65
|
+
? { measured: false, reason: profile.unreadableComponentStack ?? 'a component pattern this scan cannot read' }
|
|
66
|
+
: { measured: true };
|
|
67
|
+
return profile;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The one accessor. Consumers read these fields and nothing else about the
|
|
72
|
+
* repo's kind. Works on a harvest (h.profile) or a bare profile object, and
|
|
73
|
+
* on harvests written before the profile layer existed (no `kind` field).
|
|
74
|
+
*/
|
|
75
|
+
export function profileOf(h) {
|
|
76
|
+
const p = h?.profile ?? h ?? {};
|
|
77
|
+
const kind = p.kind ?? p.role ?? 'product';
|
|
78
|
+
return {
|
|
79
|
+
kind,
|
|
80
|
+
confidence: p.kindConfidence ?? null,
|
|
81
|
+
evidence: p.kindEvidence ?? [],
|
|
82
|
+
isLibrary: kind === 'library',
|
|
83
|
+
// A vendored shadcn catalogue is stock on a shelf, not abandonment.
|
|
84
|
+
vendoredUi: p.vendoredUi === true,
|
|
85
|
+
uiDir: p.uiDir ?? null,
|
|
86
|
+
componentsMeasured: p.componentDetection?.measured !== false,
|
|
87
|
+
notMeasuredReason: p.componentDetection?.reason ?? 'an unrecognised component pattern',
|
|
88
|
+
designSystem: p.designSystem ?? { kind: 'none' },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library — a repo whose consumers live in other repos entirely.
|
|
3
|
+
*
|
|
4
|
+
* A published components package with no app pages: usage counts mean
|
|
5
|
+
* composition, not adoption, orphans are showroom stock rather than dead
|
|
6
|
+
* weight, and the report's language follows. Two ways in, either sufficient:
|
|
7
|
+
*
|
|
8
|
+
* 1. A publishable package with a components-ish name (or the root package
|
|
9
|
+
* itself being publishable), found by the profiler as `libraryPkg`, next
|
|
10
|
+
* to at least 5 reusable components and almost no pages.
|
|
11
|
+
* 2. A web-components repo with many tag-registered components and no pages,
|
|
12
|
+
* whatever its packages are named (Ionic's @ionic/core and Material Web
|
|
13
|
+
* both dodge the name rule).
|
|
14
|
+
*/
|
|
15
|
+
export default {
|
|
16
|
+
kind: 'library',
|
|
17
|
+
|
|
18
|
+
/** @returns {{ confidence: string, evidence: string[] } | null} */
|
|
19
|
+
recognise(profile, counts) {
|
|
20
|
+
const evidence = [];
|
|
21
|
+
if (profile.libraryPkg && counts.fewPages && counts.reusable >= 5) {
|
|
22
|
+
evidence.push(`publishable package ${profile.libraryPkg} with ${counts.reusable} reusable components and ${counts.pages} page${counts.pages === 1 ? '' : 's'}`);
|
|
23
|
+
}
|
|
24
|
+
if (String(profile.framework).startsWith('web components') && counts.fewPages && counts.reusable >= 10) {
|
|
25
|
+
evidence.push(`${counts.reusable} registered web components and ${counts.pages} page${counts.pages === 1 ? '' : 's'}`);
|
|
26
|
+
}
|
|
27
|
+
return evidence.length ? { confidence: 'high', evidence } : null;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product — the default. An app, a site, a monorepo of apps: the components
|
|
3
|
+
* are used where they are defined, so usage means adoption, an orphan is a
|
|
4
|
+
* wrong answer waiting to be picked, and every tile scores as written.
|
|
5
|
+
*
|
|
6
|
+
* Always matches. It is the fallback, and it must stay the last entry in
|
|
7
|
+
* profiles/index.mjs. It carries no special knowledge on purpose: anything a
|
|
8
|
+
* kind of repo needs read differently belongs in that kind's own file.
|
|
9
|
+
*/
|
|
10
|
+
export default {
|
|
11
|
+
kind: 'product',
|
|
12
|
+
|
|
13
|
+
recognise(profile) {
|
|
14
|
+
return {
|
|
15
|
+
confidence: 'high',
|
|
16
|
+
evidence: [profile.libraryPkg
|
|
17
|
+
? `publishable package ${profile.libraryPkg} present, but the repo has app pages next to its components`
|
|
18
|
+
: 'no publishable components package; components are used where they are defined'],
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -8,6 +8,7 @@ import { distinctTypefaces } from '../lib/typefaces.mjs';
|
|
|
8
8
|
import { nearColorPairs } from './../lib/nearpairs.mjs';
|
|
9
9
|
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
10
10
|
import { VERSION } from '../lib/version.mjs';
|
|
11
|
+
import { profileOf } from '../profiles/index.mjs';
|
|
11
12
|
|
|
12
13
|
export function rulesMarkdown(h, opts = {}) {
|
|
13
14
|
// compact: the size-aware variant for agent files with tight practical
|
|
@@ -75,7 +76,7 @@ const repoName = h.profile?.name ?? 'this repo';
|
|
|
75
76
|
if (near.length) rule(`Never eyeball a colour from memory: the scan found ${near.length} nearly identical pair${near.length === 1 ? '' : 's'} (like ${near[0].a.value} next to ${near[0].b.value}). Look the exact value up, or better, use its token.`);
|
|
76
77
|
const ds = h.profile?.designSystem;
|
|
77
78
|
if (ds?.kind && !['none', 'custom'].includes(ds.kind) && ds.confidence !== 'low') {
|
|
78
|
-
rule(`This repo uses ${ds.name ?? ds.kind}${h.
|
|
79
|
+
rule(`This repo uses ${ds.name ?? ds.kind}${profileOf(h).uiDir ? `; its components live in \`${profileOf(h).uiDir}\`` : ''}. Prefer extending it over building parallel pieces.`);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -130,14 +131,14 @@ const repoName = h.profile?.name ?? 'this repo';
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
// ---------- never-imported components ----------
|
|
133
|
-
const neverImported = neverImportedComponents(h.components, h.
|
|
134
|
+
const neverImported = neverImportedComponents(h.components, profileOf(h).uiDir);
|
|
134
135
|
if (neverImported.length >= 3) {
|
|
135
136
|
// A vendored catalogue is stock, not debt: the rule points the agent AT it
|
|
136
137
|
// rather than telling it to prune. An unused Calendar beats a hand-rolled
|
|
137
138
|
// date picker written under time pressure.
|
|
138
|
-
if (h.
|
|
139
|
+
if (profileOf(h).vendoredUi) {
|
|
139
140
|
section('Catalogue components already installed');
|
|
140
|
-
rule(`${neverImported.length} components sit installed and unused in \`${h.
|
|
141
|
+
rule(`${neverImported.length} components sit installed and unused in \`${profileOf(h).uiDir}\` (${neverImported.slice(0, 3).map((c) => `\`<${c.name}>\``).join(', ')}…). Reach for one of these before building your own version of the same thing. Do not delete them to tidy up.`);
|
|
141
142
|
} else {
|
|
142
143
|
section('Components nobody imports');
|
|
143
144
|
rule(`${neverImported.length} components are defined but never imported (${neverImported.slice(0, 3).map((c) => `\`<${c.name}>\``).join(', ')}…). Before writing any new component, check this list first; adopt one or flag it for deletion instead of adding another.`);
|
|
@@ -12,6 +12,7 @@ import { resolve } from 'node:path';
|
|
|
12
12
|
import { VERSION } from '../lib/version.mjs';
|
|
13
13
|
import { nearColorPairs } from '../lib/nearpairs.mjs';
|
|
14
14
|
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
15
|
+
import { profileOf } from '../profiles/index.mjs';
|
|
15
16
|
|
|
16
17
|
function arg(name, fallback) {
|
|
17
18
|
const i = process.argv.indexOf(`--${name}`);
|
|
@@ -46,8 +47,8 @@ for (const d of (h.duplicates?.exactDuplicates ?? [])) {
|
|
|
46
47
|
}
|
|
47
48
|
// A vendored shadcn catalogue is stock, not a defect: nothing to annotate in
|
|
48
49
|
// code scanning, and a finding per component would bury the real ones.
|
|
49
|
-
if (!h.
|
|
50
|
-
for (const c of neverImportedComponents(h.components ?? [], h.
|
|
50
|
+
if (!profileOf(h).vendoredUi) {
|
|
51
|
+
for (const c of neverImportedComponents(h.components ?? [], profileOf(h).uiDir)) {
|
|
51
52
|
push('never-imported-component', 'note', `<${c.name}> is defined here and imported by nothing.`, c.file);
|
|
52
53
|
}
|
|
53
54
|
}
|