baldart 5.1.0 → 5.2.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/CHANGELOG.md +77 -0
- package/README.md +3 -3
- package/VERSION +1 -1
- package/framework/.claude/agents/CHANGELOG.md +45 -0
- package/framework/.claude/agents/REGISTRY.md +2 -2
- package/framework/.claude/agents/coder.md +3 -3
- package/framework/.claude/agents/remotion-animator-orchestrator.md +4 -3
- package/framework/.claude/agents/ui-expert.md +4 -10
- package/framework/.claude/agents/ui-quality-critic.md +27 -11
- package/framework/.claude/skills/design-system-init/CHANGELOG.md +5 -0
- package/framework/.claude/skills/design-system-init/SKILL.md +2 -2
- package/framework/.claude/skills/e2e-review/CHANGELOG.md +5 -0
- package/framework/.claude/skills/e2e-review/SKILL.md +2 -2
- package/framework/.claude/skills/frontend-design/CHANGELOG.md +20 -0
- package/framework/.claude/skills/frontend-design/SKILL.md +39 -216
- package/framework/.claude/skills/gamification-design/CHANGELOG.md +5 -0
- package/framework/.claude/skills/gamification-design/SKILL.md +2 -2
- package/framework/.claude/skills/ui-design/CHANGELOG.md +60 -1
- package/framework/.claude/skills/ui-design/SKILL.md +127 -75
- package/framework/.claude/skills/ui-design/references/anti-slop.md +106 -0
- package/framework/.claude/skills/ui-design/references/craft-standards.md +259 -0
- package/framework/.claude/skills/ui-design/references/design-brief.md +92 -0
- package/framework/.claude/skills/ui-design/references/design-directions.md +188 -0
- package/framework/.claude/skills/ui-design/references/evaluation.md +125 -165
- package/framework/.claude/skills/ui-design/references/generation.md +125 -92
- package/framework/.claude/skills/ui-design/references/inventory.md +9 -2
- package/framework/.claude/skills/ui-design/scripts/craft-check.mjs +248 -0
- package/framework/agents/component-manifest-schema.md +1 -1
- package/framework/agents/design-system-protocol.md +6 -6
- package/framework/agents/index.md +1 -0
- package/framework/agents/skills-mapping.md +25 -23
- package/framework/docs/PROJECT-CONFIGURATION.md +5 -5
- package/package.json +1 -1
- package/src/commands/configure.js +1 -1
- package/src/utils/__tests__/classify-divergence.test.js +42 -0
- package/src/utils/git.js +45 -9
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* craft-check.mjs — deterministic anti-slop / craft gate for ui-design mockups.
|
|
4
|
+
*
|
|
5
|
+
* Scans self-contained mockup HTML for the mechanically-checkable subset of
|
|
6
|
+
* references/anti-slop.md + references/craft-standards.md. Runs identically on
|
|
7
|
+
* Claude Code and Codex (zero deps, Node >= 18) — it is Gate 0 of Step D.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node craft-check.mjs <file.html> [more.html ...] [--json] [--strict-palette]
|
|
11
|
+
*
|
|
12
|
+
* --strict-palette escalate palette-reflex findings (AI-indigo, beige
|
|
13
|
+
* battery, trust gradient) from warn to block. Use in
|
|
14
|
+
* exploration regime; leave off in-context (the project's
|
|
15
|
+
* own brand may legitimately use these families).
|
|
16
|
+
*
|
|
17
|
+
* Exit codes: 0 = PASS (no blockers), 1 = FAIL (>=1 blocker), 2 = usage error.
|
|
18
|
+
*
|
|
19
|
+
* Rule provenance: distilled from impeccable (Apache-2.0, © Paul Bakaus),
|
|
20
|
+
* taste-skill (MIT, © Leonxlnx), open-design (Apache-2.0, nexu-io).
|
|
21
|
+
* Heuristics are conservative: binary rules block, judgment-adjacent rules warn.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const asJson = args.includes('--json');
|
|
28
|
+
const strictPalette = args.includes('--strict-palette');
|
|
29
|
+
const files = args.filter((a) => !a.startsWith('--'));
|
|
30
|
+
|
|
31
|
+
if (files.length === 0) {
|
|
32
|
+
console.error('usage: node craft-check.mjs <file.html> [...] [--json] [--strict-palette]');
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const INDIGO_HEXES = ['#6366f1', '#4f46e5', '#4338ca', '#3730a3', '#8b5cf6', '#7c3aed', '#a855f7'];
|
|
37
|
+
const BEIGE_BG_HEXES = ['#f5f1ea', '#f7f5f1', '#fbf8f1', '#efeae0', '#ece6db', '#faf7f1', '#e8dfcb'];
|
|
38
|
+
const BEIGE_ACCENT_HEXES = ['#b08947', '#b6553a', '#9a2436', '#9c6e2a', '#bc7c3a', '#7d5621'];
|
|
39
|
+
const EMOJI_RE = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{FE0F}]/u;
|
|
40
|
+
|
|
41
|
+
function lineOf(src, index) {
|
|
42
|
+
return src.slice(0, index).split('\n').length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function excerptAt(src, index, len = 80) {
|
|
46
|
+
const start = Math.max(0, src.lastIndexOf('\n', index) + 1);
|
|
47
|
+
const end = src.indexOf('\n', index);
|
|
48
|
+
return src.slice(start, end === -1 ? start + len : Math.min(end, start + len)).trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Collect matches of a regex as {line, excerpt} evidence (capped). */
|
|
52
|
+
function findAll(src, re, cap = 5) {
|
|
53
|
+
const out = [];
|
|
54
|
+
let m;
|
|
55
|
+
const rex = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
|
|
56
|
+
while ((m = rex.exec(src)) !== null && out.length < cap) {
|
|
57
|
+
out.push({ line: lineOf(src, m.index), excerpt: excerptAt(src, m.index) });
|
|
58
|
+
if (m.index === rex.lastIndex) rex.lastIndex++;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Strip script/style/comments, then tags → visible text (index-preserving via spaces). */
|
|
64
|
+
function visibleText(src) {
|
|
65
|
+
const blank = (s) => s.replace(/[^\n]/g, ' ');
|
|
66
|
+
return src
|
|
67
|
+
.replace(/<script[\s\S]*?<\/script>/gi, blank)
|
|
68
|
+
.replace(/<style[\s\S]*?<\/style>/gi, blank)
|
|
69
|
+
.replace(/<!--[\s\S]*?-->/g, blank)
|
|
70
|
+
.replace(/<[^>]+>/g, blank);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function checkFile(path) {
|
|
74
|
+
const src = readFileSync(path, 'utf8');
|
|
75
|
+
const text = visibleText(src);
|
|
76
|
+
const findings = [];
|
|
77
|
+
const add = (rule, severity, message, evidence) => {
|
|
78
|
+
if (evidence.length > 0) findings.push({ rule, severity, message, count: evidence.length, evidence });
|
|
79
|
+
};
|
|
80
|
+
const palSev = strictPalette ? 'block' : 'warn';
|
|
81
|
+
|
|
82
|
+
// ---- Absolute bans (binary) ------------------------------------------
|
|
83
|
+
add('em-dash', 'block',
|
|
84
|
+
'Em-dash (or en-dash separator) in visible copy — zero tolerance (anti-slop.md).',
|
|
85
|
+
findAll(text, /—|\s–\s/));
|
|
86
|
+
|
|
87
|
+
add('filler-copy', 'block',
|
|
88
|
+
'Filler copy (lorem ipsum / "Feature One") — an empty section is a composition problem.',
|
|
89
|
+
findAll(text, /lorem ipsum|\bfeature (one|two|three)\b/i));
|
|
90
|
+
|
|
91
|
+
add('gradient-text', 'block',
|
|
92
|
+
'Gradient text (background-clip: text) — decorative, never meaningful.',
|
|
93
|
+
findAll(src, /background-clip:\s*text|bg-clip-text/i));
|
|
94
|
+
|
|
95
|
+
add('scroll-cue', 'block',
|
|
96
|
+
'"Scroll to explore" cue — they know what scroll is.',
|
|
97
|
+
findAll(text, /scroll (down )?to (explore|discover|see)/i));
|
|
98
|
+
|
|
99
|
+
add('scroll-listener', 'block',
|
|
100
|
+
'window scroll listener in script — use IntersectionObserver / animation-library scroll values.',
|
|
101
|
+
findAll(src, /addEventListener\(\s*['"]scroll['"]/));
|
|
102
|
+
|
|
103
|
+
if (!/<meta[^>]+name=["']viewport["']/i.test(src)) {
|
|
104
|
+
findings.push({ rule: 'missing-viewport', severity: 'block', message: 'Missing <meta name="viewport"> — the mockup cannot be judged responsively.', count: 1, evidence: [{ line: 1, excerpt: '<head>' }] });
|
|
105
|
+
}
|
|
106
|
+
if (!/<html[^>]+lang=/i.test(src)) {
|
|
107
|
+
findings.push({ rule: 'missing-lang', severity: 'block', message: 'Missing lang attribute on <html> (identity.language).', count: 1, evidence: [{ line: 1, excerpt: '<html>' }] });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Emoji as icons inside headings/buttons
|
|
111
|
+
{
|
|
112
|
+
const ev = [];
|
|
113
|
+
const rex = /<(h[1-6]|button)\b[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
114
|
+
let m;
|
|
115
|
+
while ((m = rex.exec(src)) !== null && ev.length < 5) {
|
|
116
|
+
const inner = m[2].replace(/<[^>]+>/g, '');
|
|
117
|
+
if (EMOJI_RE.test(inner)) ev.push({ line: lineOf(src, m.index), excerpt: excerptAt(src, m.index) });
|
|
118
|
+
}
|
|
119
|
+
add('emoji-icons', 'block',
|
|
120
|
+
'Emoji as feature icons in headings/buttons — use monoline SVG with currentColor.', ev);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---- Rationed patterns (count-bounded) --------------------------------
|
|
124
|
+
{
|
|
125
|
+
const sections = (src.match(/<section\b/gi) || []).length || 1;
|
|
126
|
+
const eyebrows = findAll(src, /class=["'][^"']*\buppercase\b[^"']*\btracking-/g, 50);
|
|
127
|
+
const cap = Math.ceil(sections / 3);
|
|
128
|
+
if (eyebrows.length > cap) {
|
|
129
|
+
findings.push({
|
|
130
|
+
rule: 'eyebrow-overuse', severity: 'block',
|
|
131
|
+
message: `Eyebrow kickers: ${eyebrows.length} for ${sections} section(s) — cap is ceil(sections/3) = ${cap}. Drop them; the headline alone is enough.`,
|
|
132
|
+
count: eyebrows.length, evidence: eyebrows.slice(0, 5),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
add('numbered-markers', 'warn',
|
|
138
|
+
'Numbered section markers (01 / 02 · Label) — the eyebrow trope one tier deeper.',
|
|
139
|
+
findAll(text, /\b0\d\s*[\/·]\s*\S/));
|
|
140
|
+
|
|
141
|
+
add('status-dots', 'warn',
|
|
142
|
+
'Pulsing decorative status dots — zero by default.',
|
|
143
|
+
findAll(src, /class=["'][^"']*animate-pulse[^"']*["']/));
|
|
144
|
+
|
|
145
|
+
add('marquee-overuse', 'warn',
|
|
146
|
+
'More than one marquee/auto-scroll strip per page.',
|
|
147
|
+
findAll(src, /marquee|animate-\[?scroll/gi).slice(1));
|
|
148
|
+
|
|
149
|
+
// ---- Palette reflexes (warn; block with --strict-palette) -------------
|
|
150
|
+
add('ai-indigo', palSev,
|
|
151
|
+
'Default-Tailwind indigo/violet accent — the textbook AI tell (LILA rule).',
|
|
152
|
+
findAll(src, new RegExp(INDIGO_HEXES.join('|'), 'i')));
|
|
153
|
+
|
|
154
|
+
add('beige-battery', palSev,
|
|
155
|
+
'Premium-consumer beige/brass battery — banned as a default; rotate palette families (design-directions.md).',
|
|
156
|
+
findAll(src, new RegExp([...BEIGE_BG_HEXES, ...BEIGE_ACCENT_HEXES].join('|'), 'i')));
|
|
157
|
+
|
|
158
|
+
add('trust-gradient', palSev,
|
|
159
|
+
'Two-stop purple→blue/cyan/pink "trust" gradient — a flat surface + intentional type beats it.',
|
|
160
|
+
findAll(src, /from-(purple|indigo|violet)-\d+[^"']*to-(blue|cyan|pink|fuchsia)-\d+/i));
|
|
161
|
+
|
|
162
|
+
// ---- Craft warnings ----------------------------------------------------
|
|
163
|
+
add('side-stripe', 'warn',
|
|
164
|
+
'Colored side-stripe accent (border-left/right > 1px) — the canonical AI dashboard tile.',
|
|
165
|
+
findAll(src, /border-l-[2-8]\b|border-r-[2-8]\b|border-left:\s*[2-9]px\s+solid\s+(?!transparent)/i));
|
|
166
|
+
|
|
167
|
+
add('h-screen', 'warn',
|
|
168
|
+
'h-screen used — prefer min-h-[100dvh] (iOS Safari).',
|
|
169
|
+
findAll(src, /\bh-screen\b/));
|
|
170
|
+
|
|
171
|
+
add('transition-all', 'warn',
|
|
172
|
+
'`transition: all` — animate transform/opacity explicitly.',
|
|
173
|
+
findAll(src, /transition:\s*all|\btransition-all\b/i));
|
|
174
|
+
|
|
175
|
+
add('br-in-heading', 'warn',
|
|
176
|
+
'<br> inside a heading — plan font scale with the copy instead of breaking lines manually.',
|
|
177
|
+
findAll(src, /<h[1-3][^>]*>(?:(?!<\/h[1-3]>)[\s\S])*?<br\s*\/?>/gi));
|
|
178
|
+
|
|
179
|
+
add('caps-no-tracking', 'warn',
|
|
180
|
+
'ALL-CAPS element without letter-spacing — uppercase requires +0.06–0.1em tracking.',
|
|
181
|
+
findAll(src, /class=["'](?![^"']*tracking-)[^"']*\buppercase\b[^"']*["']/g));
|
|
182
|
+
|
|
183
|
+
add('justify-text', 'warn',
|
|
184
|
+
'Justified text — never text-align: justify in UI.',
|
|
185
|
+
findAll(src, /text-align:\s*justify|\btext-justify\b/i));
|
|
186
|
+
|
|
187
|
+
add('fake-metrics', 'warn',
|
|
188
|
+
'Fake-perfect metric (99.9%, 10×) — real source, labelled mock, or absent (Jane Doe rule).',
|
|
189
|
+
findAll(text, /\b99\.9+\s*%|\b10×|\b100\s*%\s+(faster|better)/i));
|
|
190
|
+
|
|
191
|
+
// First <img> lazy / dimensionless images (CLS)
|
|
192
|
+
{
|
|
193
|
+
const imgs = [...src.matchAll(/<img\b[^>]*>/gi)];
|
|
194
|
+
if (imgs.length > 0 && /loading=["']lazy["']/i.test(imgs[0][0])) {
|
|
195
|
+
findings.push({ rule: 'lcp-lazy', severity: 'warn', message: 'First image is loading="lazy" — never lazy-load the LCP candidate.', count: 1, evidence: [{ line: lineOf(src, imgs[0].index), excerpt: excerptAt(src, imgs[0].index) }] });
|
|
196
|
+
}
|
|
197
|
+
const dimless = imgs.filter((m) => !/\bwidth=|\baspect-ratio|aspect-\[/i.test(m[0]));
|
|
198
|
+
add('img-no-dimensions', 'warn',
|
|
199
|
+
'<img> without width/height or aspect-ratio — reserves no space (CLS).',
|
|
200
|
+
dimless.slice(0, 5).map((m) => ({ line: lineOf(src, m.index), excerpt: excerptAt(src, m.index) })));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Token discipline: distinct raw hexes + off-scale arbitrary values
|
|
204
|
+
{
|
|
205
|
+
const hexes = new Set((src.match(/#[0-9a-fA-F]{6}\b/g) || []).map((h) => h.toLowerCase()));
|
|
206
|
+
if (hexes.size > 12) {
|
|
207
|
+
findings.push({ rule: 'hex-sprawl', severity: 'warn', message: `${hexes.size} distinct raw hex colors — an incomplete palette; consolidate to CSS custom properties.`, count: hexes.size, evidence: [] });
|
|
208
|
+
}
|
|
209
|
+
const offScale = [...src.matchAll(/\b(?:[mp][trblxy]?|gap|space-[xy])-\[(\d+)px\]/g)]
|
|
210
|
+
.filter((m) => Number(m[1]) % 4 !== 0);
|
|
211
|
+
add('off-scale-spacing', 'warn',
|
|
212
|
+
'Arbitrary spacing off the base-4 scale (e.g. p-[13px]) — token violation.',
|
|
213
|
+
offScale.slice(0, 5).map((m) => ({ line: lineOf(src, m.index), excerpt: m[0] })));
|
|
214
|
+
const sizes = new Set([
|
|
215
|
+
...[...src.matchAll(/text-\[(\d+)px\]/g)].map((m) => m[1]),
|
|
216
|
+
...[...src.matchAll(/font-size:\s*(\d+)px/gi)].map((m) => m[1]),
|
|
217
|
+
]);
|
|
218
|
+
if (sizes.size > 9) {
|
|
219
|
+
findings.push({ rule: 'type-scale-sprawl', severity: 'warn', message: `${sizes.size} distinct px font sizes — one scale, 6–9 deliberate steps.`, count: sizes.size, evidence: [] });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const blockers = findings.filter((f) => f.severity === 'block');
|
|
224
|
+
return { file: path, verdict: blockers.length === 0 ? 'PASS' : 'FAIL', blockers: blockers.length, warnings: findings.length - blockers.length, findings };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const results = files.map((f) => {
|
|
228
|
+
try {
|
|
229
|
+
return checkFile(f);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return { file: f, verdict: 'ERROR', error: e.message, findings: [] };
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (asJson) {
|
|
236
|
+
console.log(JSON.stringify({ strictPalette, results }, null, 2));
|
|
237
|
+
} else {
|
|
238
|
+
for (const r of results) {
|
|
239
|
+
console.log(`\n${r.verdict === 'PASS' ? '✓' : '✗'} ${r.file} — ${r.verdict}` + (r.error ? ` (${r.error})` : ''));
|
|
240
|
+
for (const f of r.findings || []) {
|
|
241
|
+
console.log(` [${f.severity.toUpperCase()}] ${f.rule} ×${f.count}: ${f.message}`);
|
|
242
|
+
for (const ev of f.evidence.slice(0, 3)) console.log(` L${ev.line}: ${ev.excerpt}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
console.log('');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
process.exit(results.some((r) => r.verdict === 'FAIL' || r.verdict === 'ERROR') ? 1 : 0);
|
|
@@ -220,7 +220,7 @@ This keeps consumers that have not yet upgraded (and non-TS stacks) from becomin
|
|
|
220
220
|
- **Single-element edit**: `/ds-edit` (update one existing component/token —
|
|
221
221
|
resync / extend / breaking / re-govern; regenerates the HEAD from the new source
|
|
222
222
|
via the same scripts, **preserving non-empty agentic fields + prose**).
|
|
223
|
-
- **Per-task write**: `ui-expert` / `ui-design` / `
|
|
223
|
+
- **Per-task write**: `ui-expert` / `ui-design` / `ui-implement` in the
|
|
224
224
|
Post-Intervention Coherence Check — a new/modified primitive regenerates its
|
|
225
225
|
HEAD in the same change.
|
|
226
226
|
- **Curation owner**: `doc-reviewer` (agentic fields).
|
|
@@ -12,7 +12,7 @@ twofold:
|
|
|
12
12
|
implementation, so existing primitives are reused instead of duplicated.
|
|
13
13
|
|
|
14
14
|
This is the textual SSOT for the registry-first protocol. Skills (`ui-design`,
|
|
15
|
-
`
|
|
15
|
+
`ui-implement`, `design-system-init`), agents (`ui-expert`, `code-reviewer`),
|
|
16
16
|
and the `/design-review` command all reference this module instead of
|
|
17
17
|
duplicating the BLOCKING-read cascade.
|
|
18
18
|
|
|
@@ -468,7 +468,7 @@ its own — it **proposes, a human confirms**:
|
|
|
468
468
|
## Functional Traceability Gate (BLOCKING — unconditional)
|
|
469
469
|
|
|
470
470
|
A mockup is a fidelity target, **not** a requirements oracle. Design tools
|
|
471
|
-
(Claude Design, Figma, the `ui-design
|
|
471
|
+
(Claude Design, Figma, the `ui-design` generator) routinely
|
|
472
472
|
add interactive-looking chrome — buttons, icons, menu entries, toggles, badges —
|
|
473
473
|
that no requirement asked for. Implementing a mockup **1:1 for fidelity**
|
|
474
474
|
materialises that chrome into real markup + dead handlers + unused icon imports,
|
|
@@ -524,7 +524,7 @@ For every **orphan**, never decide silently (consistent with the framework's
|
|
|
524
524
|
|
|
525
525
|
### Where it runs
|
|
526
526
|
|
|
527
|
-
1. **Design / generation time** (`ui-design`, `
|
|
527
|
+
1. **Design / generation time** (`ui-design`, `ui-expert`
|
|
528
528
|
designing) — when producing a mockup, do not invent interactive chrome with
|
|
529
529
|
no backing requirement; mark genuinely decorative elements as such in the
|
|
530
530
|
inventory so the downstream gate is deterministic, not a guess.
|
|
@@ -698,7 +698,7 @@ complete** when `features.has_design_system: true` until the registry has
|
|
|
698
698
|
been verified against the change that was just made.
|
|
699
699
|
|
|
700
700
|
The check runs at end-of-task by the agent that performed the work (`ui-expert`,
|
|
701
|
-
`ui-design`, `
|
|
701
|
+
`ui-design`, `code-reviewer`). It is **not** delegated to
|
|
702
702
|
the weekly `ds-drift` routine — that routine is a safety net, not the primary
|
|
703
703
|
mechanism. Drift introduced today must be reconciled today, not next Monday.
|
|
704
704
|
|
|
@@ -820,9 +820,9 @@ The following files reference this module instead of duplicating its rules:
|
|
|
820
820
|
- `framework/.claude/agents/ui-expert.md`
|
|
821
821
|
- `framework/.claude/agents/code-reviewer.md`
|
|
822
822
|
- `framework/.claude/agents/ui-quality-critic.md` (Design Quality Rubric — the
|
|
823
|
-
intrinsic-quality critic consumed by `/e2e-review` Phase 4b
|
|
823
|
+
intrinsic-quality critic consumed by `/e2e-review` Phase 4b and `/ui-design`
|
|
824
|
+
Step D)
|
|
824
825
|
- `framework/.claude/skills/ui-design/SKILL.md`
|
|
825
|
-
- `framework/.claude/skills/frontend-design/SKILL.md`
|
|
826
826
|
- `framework/.claude/skills/design-system-init/SKILL.md` (bulk registry bootstrap/upgrade)
|
|
827
827
|
- `framework/.claude/skills/ds-new/SKILL.md` (single-element guided creation)
|
|
828
828
|
- `framework/.claude/skills/ds-edit/SKILL.md` (single-element guided edit — resync / extend / breaking / re-govern)
|
|
@@ -20,6 +20,7 @@ Route agents to the right module with minimal reading.
|
|
|
20
20
|
- If touching database schema or fields -> read `agents/data-model.md` and `${paths.references_dir}/data-model.md`.
|
|
21
21
|
- If touching UI pages/routes or flows -> read `${paths.references_dir}/ui/index.md` (then specific UI module for your domain).
|
|
22
22
|
- If touching ANY visual surface (component, page, mockup, design review) and `features.has_design_system: true` -> read `agents/design-system-protocol.md` and treat the BLOCKING cascade (`${paths.design_system}/INDEX.md` + `tokens-reference.md` + `components/<Name>.md`) as mandatory pre-work. When the flag is `false`, recommend `/design-system-init` to scaffold a registry.
|
|
23
|
+
- If the task is a DESIGN DECISION (new page/screen/component to design, redesign/restyle, design options to explore, a local prototype/mockup) -> route to the `/ui-design` skill (the local design studio: Design Read & direction lock, deterministic craft gate, dual-lens evaluation). An ALREADY-APPROVED mockup to implement routes to `/ui-implement`; a direct in-code UI change with no design decision delegates to the `ui-expert` agent (`AGENTS.md` § delegation). NEVER route design work to `frontend-design` (a router since v5.2.0) or `ui-ux-pro-max` (superseded by `ui-design/references/`).
|
|
23
24
|
- If a UI change might INTRODUCE a component (designing from a mockup, building a new primitive) and `features.has_design_system: true` -> read `agents/design-system-protocol.md` § "Closed-Set Selection Policy": query the registry by ROLE (`INDEX.md` § Selection Policy), reuse the canonical member of any `closed` family, honor the card's `component_bindings`, and run `baldart ds-gate` (a `DS_CLOSED_SET_VIOLATION` — a new canonical in a closed family, the "third header" failure mode — BLOCKS). A new member of a closed family is a governance decision (`/prd` reconciliation + ADR), never an implementation act.
|
|
24
25
|
- If creating ONE new canonical design-system element on-the-fly (a component or a token) and `features.has_design_system: true` -> use the `/ds-new` skill (reuse-first → optional scaffold via `ui-expert` → document + register + govern + verify); it produces the standardized spec by filling `framework/templates/component-spec.template.md` via the serializer, never hand-writing a HEAD. The bulk analogue (whole registry) is `/design-system-init`.
|
|
25
26
|
- If EDITING one existing design-system element (add a variant/prop, breaking change, re-govern, or resync the spec) and `features.has_design_system: true` -> use the `/ds-edit` skill (classify the change → apply source via `ui-expert` → regenerate the spec via the serializer, PRESERVING agentic fields + prose, same canonical template as `/ds-new` → verify). The pure mechanical doc-resync after a code change is already automatic (serializer + `DS_COMPONENT_STALE` gate); `/ds-edit` is for the deliberate change.
|
|
@@ -191,38 +191,38 @@ competing methodology — it detects the task type and passes the matching
|
|
|
191
191
|
|
|
192
192
|
## UI/UX Skills
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
|
|
202
|
-
**Triggers**:
|
|
203
|
-
|
|
204
|
-
- "Create [page]"
|
|
205
|
-
- "Build [component]"
|
|
206
|
-
- "Design [interface]"
|
|
194
|
+
> **UI routing rule (since v5.2.0).** ALL design-decision work routes to
|
|
195
|
+
> `ui-design` (the local design studio). `frontend-design` is a RETIRED
|
|
196
|
+
> generator kept as a router: its broad triggers still land there, but its
|
|
197
|
+
> body immediately reroutes (design → `/ui-design`, approved mockup → code →
|
|
198
|
+
> `/ui-implement`, direct in-code UI → `ui-expert` agent per `AGENTS.md`
|
|
199
|
+
> § delegation). Never follow pre-v5.2.0 `frontend-design` design guidance.
|
|
200
|
+
> `ui-ux-pro-max` is superseded by `ui-design/references/`
|
|
201
|
+
> (design-directions / craft-standards / anti-slop) — never route to it.
|
|
207
202
|
|
|
208
203
|
### ui-design
|
|
209
204
|
|
|
210
205
|
**When to use**:
|
|
211
206
|
|
|
212
|
-
- Designing
|
|
213
|
-
|
|
214
|
-
-
|
|
207
|
+
- Designing, prototyping or restyling ANY page/screen/component locally —
|
|
208
|
+
the internal twin of the Claude Design handoff
|
|
209
|
+
- The `/prd` skill reaches its UI design phase (Step 3)
|
|
210
|
+
- Redesigning existing pages / exploring design options or aesthetic
|
|
211
|
+
directions for review
|
|
215
212
|
|
|
216
213
|
**Triggers**:
|
|
217
214
|
|
|
218
215
|
- "/ui-design"
|
|
219
|
-
- "Design UI" / "mockup" / "opzioni di design"
|
|
220
|
-
|
|
216
|
+
- "Design UI" / "mockup" / "prototipo" / "opzioni di design" / "restyle" /
|
|
217
|
+
"rifai la UI" / "migliora il design" / "progetta interfaccia"
|
|
218
|
+
- Design-heavy features, visual refinement, greenfield landing/dashboard work
|
|
219
|
+
|
|
220
|
+
### frontend-design (router — retired generator)
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
**When to use**: never follow it as a workflow. If a trigger lands here
|
|
223
|
+
("create/build/style [page/component/UI]"), apply its routing table:
|
|
224
|
+
design work → `ui-design` · approved mockup → `ui-implement` · in-code UI
|
|
225
|
+
change → `ui-expert` agent · motion → `motion-design`.
|
|
226
226
|
|
|
227
227
|
### ui-implement
|
|
228
228
|
|
|
@@ -674,7 +674,9 @@ Is it a bug fix?
|
|
|
674
674
|
|
|
|
675
675
|
v
|
|
676
676
|
Is it UI/visual work?
|
|
677
|
-
|-- YES -->
|
|
677
|
+
|-- YES --> design decision (new/redesign/restyle/options) → /ui-design;
|
|
678
|
+
| approved mockup → code → /ui-implement;
|
|
679
|
+
| direct in-code UI change → ui-expert agent (AGENTS.md § delegation);
|
|
678
680
|
| motion → /motion-design
|
|
679
681
|
|
|
|
680
682
|
v
|
|
@@ -94,8 +94,8 @@ Empty string `""` means the concept doesn't exist in your project. Skills gated
|
|
|
94
94
|
|
|
95
95
|
| Key | Typical value | Used by |
|
|
96
96
|
|---|---|---|
|
|
97
|
-
| `design_system` | `docs/design-system` | ui-design,
|
|
98
|
-
| `ui_guidelines` | `docs/references/ui-guidelines.md` | ui-design,
|
|
97
|
+
| `design_system` | `docs/design-system` | ui-design, ui-implement, prd, bug, kie-ai, simplify, playwright-skill, webapp-testing, gamification-design |
|
|
98
|
+
| `ui_guidelines` | `docs/references/ui-guidelines.md` | ui-design, copywriting, gamification-design |
|
|
99
99
|
| `api_index` | `docs/references/api/index.md` | doc-writing-for-rag |
|
|
100
100
|
| `api_schemas` | `docs/references/api/schemas.md` | doc-writing-for-rag |
|
|
101
101
|
| `api_errors` | `docs/references/errors.md` | doc-writing-for-rag |
|
|
@@ -157,7 +157,7 @@ Lifecycle:
|
|
|
157
157
|
| Key | Type | Notes |
|
|
158
158
|
|---|---|---|
|
|
159
159
|
| `brand_name` | string | Public product name |
|
|
160
|
-
| `design_philosophy` | string | Free-form, one line. Examples: `"Neo-Brutalism"`, `"Glassmorphism"`, `"Minimalist"`, `"Material 3"`, `""` (no opinion). Drives aesthetic decisions in `ui-design`, `
|
|
160
|
+
| `design_philosophy` | string | Free-form, one line. Examples: `"Neo-Brutalism"`, `"Glassmorphism"`, `"Minimalist"`, `"Material 3"`, `""` (no opinion). Drives aesthetic decisions in `ui-design`, `kie-ai`, `motion-design`. |
|
|
161
161
|
| `language` | BCP-47 tag | `"en"`, `"it"`, etc. Drives copy register in `copywriting`, error-message language in `ui-design`. |
|
|
162
162
|
| `audience_segments` | array of strings | Domain split, e.g. `["merchant","customer"]` or `["b2b","b2c"]`. Empty `[]` = no segmentation. Drives prompts in `copywriting`, evaluation in `ui-design`. |
|
|
163
163
|
|
|
@@ -257,7 +257,7 @@ Every flag MUST be present as `true` or `false` once `baldart configure` has run
|
|
|
257
257
|
|
|
258
258
|
| Flag | Meaning |
|
|
259
259
|
|---|---|
|
|
260
|
-
| `has_design_system` | The project has docs rooted at `paths.design_system`. When `true`, `ui-design`, `
|
|
260
|
+
| `has_design_system` | The project has docs rooted at `paths.design_system`. When `true`, `ui-design`, `ui-implement`, `bug`, `simplify`, `playwright-skill` etc. treat the INDEX as BLOCKING. |
|
|
261
261
|
| `multi_tenant_theming` | The project supports per-tenant theme overrides. Activates themed-surface text/bg pairing rules in `ui-design`, `playwright-skill`, `webapp-testing`. |
|
|
262
262
|
| `has_api_docs` | Canonical API docs at `paths.api_index`. Gates `doc-writing-for-rag` schemas/errors workflow. |
|
|
263
263
|
| `has_backlog` | Card-driven workflow (`paths.backlog_dir` + AGENTS.md card protocol). `new` skill refuses to run without it. |
|
|
@@ -337,7 +337,7 @@ These skills had heavy hard-coding before v3.0.0; if your project has opinions i
|
|
|
337
337
|
|---|---|
|
|
338
338
|
| `ui-design` | Design philosophy mandates, theming pairing rules, charting wrappers, browser open command, evaluation thresholds |
|
|
339
339
|
| `copywriting` | Brand voice pillars, audience register matrix, forbidden vocabulary, language-only rules |
|
|
340
|
-
| `frontend-design` |
|
|
340
|
+
| `frontend-design` | (retired to a router since v5.2.0 — port aesthetic mandates / illustration-motion contract to the `ui-design` overlay) |
|
|
341
341
|
| `prd` / `prd-add` | Project terminology, mandatory review gates, design-system reads for UI PRDs |
|
|
342
342
|
| `kie-ai` | Illustration character library, video-prompt motion vocabulary, forbidden aesthetics |
|
|
343
343
|
| `bug` | Project debug entry points (env summary helper, cache debug switch, error-code module) |
|
package/package.json
CHANGED
|
@@ -790,7 +790,7 @@ async function interactivePrompts(merged, detected) {
|
|
|
790
790
|
' /design-system-init (or invoke the skill)\n' +
|
|
791
791
|
'to scaffold INDEX.md + tokens-reference.md + per-component specs at\n' +
|
|
792
792
|
'docs/design-system/. The registry-first protocol (ui-expert, ui-design,\n' +
|
|
793
|
-
'
|
|
793
|
+
'ui-implement, /design-review) needs these files to enforce coherence.'
|
|
794
794
|
);
|
|
795
795
|
} else if (merged.features.has_design_system === false) {
|
|
796
796
|
UI.info(
|
|
@@ -290,5 +290,47 @@ console.log('\n─── overlayCoversTouched (fs-backed) ───');
|
|
|
290
290
|
fsMod.rmSync(tmp, { recursive: true, force: true });
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
// absorbablePayloadSubset(touched) — pure. The subset whose byte-content must
|
|
294
|
+
// match upstream for a commit to be "absorbed". Consumer files (outside
|
|
295
|
+
// `.framework/`) and subtree bookkeeping (VERSION/CHANGELOG/README) are
|
|
296
|
+
// EXCLUDED. Fixtures reproduce the exact mayo v5.0.1→v5.1.0 divergence that
|
|
297
|
+
// falsely blocked the update pre-v5.1.1 (mixed FEAT commits + add-then-revert
|
|
298
|
+
// pairs touching VERSION/CHANGELOG).
|
|
299
|
+
console.log('\n─── absorbablePayloadSubset (pure) ───');
|
|
300
|
+
{
|
|
301
|
+
const cases = [
|
|
302
|
+
{ name: 'mixed FEAT commit: framework doc + app files → only the doc survives',
|
|
303
|
+
touched: ['.framework/framework/agents/design-system-protocol.md',
|
|
304
|
+
'backlog/FEAT-0054-03-enforcement.yml',
|
|
305
|
+
'docs/references/ssot-registry.md'],
|
|
306
|
+
expected: ['.framework/framework/agents/design-system-protocol.md'] },
|
|
307
|
+
{ name: 'mixed FEAT commit: framework doc + CLAUDE.md + eslint config → only the doc',
|
|
308
|
+
touched: ['.framework/framework/agents/design-system-protocol.md',
|
|
309
|
+
'CLAUDE.md', 'eslint.responsive.config.mjs'],
|
|
310
|
+
expected: ['.framework/framework/agents/design-system-protocol.md'] },
|
|
311
|
+
{ name: 'bookkeeping-only touch (VERSION/CHANGELOG/README) → empty subset',
|
|
312
|
+
touched: ['.framework/VERSION', '.framework/CHANGELOG.md', '.framework/README.md'],
|
|
313
|
+
expected: [] },
|
|
314
|
+
{ name: 'revert commit: bookkeeping + payload scripts → only the scripts survive',
|
|
315
|
+
touched: ['.framework/CHANGELOG.md', '.framework/README.md', '.framework/VERSION',
|
|
316
|
+
'.framework/framework/.claude/skills/design-system-init/scripts/extract-ts.mjs',
|
|
317
|
+
'.framework/framework/agents/component-manifest-schema.md'],
|
|
318
|
+
expected: ['.framework/framework/.claude/skills/design-system-init/scripts/extract-ts.mjs',
|
|
319
|
+
'.framework/framework/agents/component-manifest-schema.md'] },
|
|
320
|
+
{ name: 'consumer-only files → empty subset',
|
|
321
|
+
touched: ['src/app/page.tsx', 'README.md'], expected: [] },
|
|
322
|
+
{ name: 'empty touched → empty subset',
|
|
323
|
+
touched: [], expected: [] },
|
|
324
|
+
{ name: 'null touched → empty subset (no throw)',
|
|
325
|
+
touched: null, expected: [] },
|
|
326
|
+
];
|
|
327
|
+
for (const c of cases) {
|
|
328
|
+
const actual = GitUtils.absorbablePayloadSubset(c.touched);
|
|
329
|
+
const ok = JSON.stringify(actual) === JSON.stringify(c.expected);
|
|
330
|
+
if (ok) { pass++; console.log(` ✓ ${c.name}`); }
|
|
331
|
+
else { fail++; console.log(` ✗ ${c.name} → ${JSON.stringify(actual)} (expected ${JSON.stringify(c.expected)})`); }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
293
335
|
console.log(`\nResult: ${pass} pass, ${fail} fail`);
|
|
294
336
|
process.exit(fail > 0 ? 1 : 0);
|
package/src/utils/git.js
CHANGED
|
@@ -209,10 +209,13 @@ class GitUtils {
|
|
|
209
209
|
// - subtree-merge: `git subtree pull --squash` merge commits
|
|
210
210
|
// - subtree-squash: the squash payload commits themselves
|
|
211
211
|
// - chore-wrapper: CLI-generated [CHORE]/chore(baldart): commits
|
|
212
|
-
// - absorbed: a would-be custom commit whose touched .framework/
|
|
213
|
-
// are now byte-identical to upstream (FETCH_HEAD) — the
|
|
214
|
-
// has been absorbed into upstream (or reset away), so a
|
|
215
|
-
// re-sync loses nothing.
|
|
212
|
+
// - absorbed: a would-be custom commit whose touched .framework/ PAYLOAD
|
|
213
|
+
// files are now byte-identical to upstream (FETCH_HEAD) — the
|
|
214
|
+
// edit has been absorbed into upstream (or reset away), so a
|
|
215
|
+
// re-sync loses nothing. Consumer files (outside .framework/)
|
|
216
|
+
// and subtree bookkeeping (VERSION/CHANGELOG/README) are not
|
|
217
|
+
// counted (see absorbablePayloadSubset). Pure history, not
|
|
218
|
+
// live drift → noise.
|
|
216
219
|
// - custom-overlay-able: user edited a framework agent/skill/command
|
|
217
220
|
// → should migrate to .baldart/overlays/
|
|
218
221
|
// - custom-other: anything else (src/, CHANGELOG, ad-hoc fixes)
|
|
@@ -297,18 +300,51 @@ class GitUtils {
|
|
|
297
300
|
});
|
|
298
301
|
}
|
|
299
302
|
|
|
300
|
-
//
|
|
303
|
+
// The subset of a commit's touched paths whose byte-content must match
|
|
304
|
+
// upstream for the commit to count as "absorbed". Two classes of path are
|
|
305
|
+
// EXCLUDED because they can never conflict in a `.framework/` subtree sync:
|
|
306
|
+
// 1. Anything outside `.framework/` — the consumer's OWN files. A mixed
|
|
307
|
+
// commit that edits both a framework doc and app code (e.g. a FEAT that
|
|
308
|
+
// also tweaks a protocol module) is common; the subtree merge never
|
|
309
|
+
// touches the app files, so they are irrelevant to whether the commit's
|
|
310
|
+
// framework portion is absorbed. (Pre-v5.1.1 the check bailed to
|
|
311
|
+
// not-absorbed on the FIRST such path → mixed commits falsely blocked
|
|
312
|
+
// the update even when their `.framework/` payload matched upstream.)
|
|
313
|
+
// 2. Subtree bookkeeping (`.framework/{VERSION,CHANGELOG.md,README.md}`) —
|
|
314
|
+
// framework-owned metadata the merge reconciles wholesale, which ALWAYS
|
|
315
|
+
// differs across versions (VERSION bumps, CHANGELOG grows). Counting it
|
|
316
|
+
// meant any commit touching it (e.g. an add-then-revert pair) could
|
|
317
|
+
// never be absorbed. It is never consumer content, so dropping it is
|
|
318
|
+
// safe: a hand-edit to it SHOULD be overwritten by upstream anyway.
|
|
319
|
+
// Pure (no fs / no git) so it stays unit-testable.
|
|
320
|
+
static absorbablePayloadSubset(touched) {
|
|
321
|
+
const prefix = `${FRAMEWORK_DIR}/`;
|
|
322
|
+
const META = new Set([
|
|
323
|
+
`${prefix}VERSION`,
|
|
324
|
+
`${prefix}CHANGELOG.md`,
|
|
325
|
+
`${prefix}README.md`,
|
|
326
|
+
]);
|
|
327
|
+
return (touched || []).filter((p) => p.startsWith(prefix) && !META.has(p));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// True when EVERY `.framework/` PAYLOAD file touched by the commit is already
|
|
301
331
|
// byte-identical to the upstream (FETCH_HEAD) blob — i.e. the commit's edit
|
|
302
332
|
// has been fully absorbed into upstream (or reset away) and a re-sync would
|
|
303
333
|
// lose nothing. Compares blob SHAs (cheap, exact). The subtree prefix
|
|
304
334
|
// `.framework/` is stripped to reach the upstream path (FETCH_HEAD has no
|
|
305
|
-
// `.framework/` prefix).
|
|
306
|
-
//
|
|
335
|
+
// `.framework/` prefix). The comparison runs over `absorbablePayloadSubset`
|
|
336
|
+
// ONLY (consumer files + subtree bookkeeping excluded — see there). A commit
|
|
337
|
+
// whose payload subset is EMPTY (it touched `.framework/` only via bookkeeping
|
|
338
|
+
// and/or only touched consumer files) carries no subtree drift → absorbed.
|
|
339
|
+
// Conservative on the payload itself: a file missing at HEAD (deleted) or
|
|
340
|
+
// missing upstream (net-new), or whose SHA differs → not absorbed (real
|
|
341
|
+
// drift, keep flagging).
|
|
307
342
|
async isAbsorbedAgainstUpstream(touched) {
|
|
308
343
|
if (!touched || !touched.length) return false;
|
|
309
344
|
const prefix = `${FRAMEWORK_DIR}/`;
|
|
310
|
-
|
|
311
|
-
|
|
345
|
+
const payload = GitUtils.absorbablePayloadSubset(touched);
|
|
346
|
+
if (!payload.length) return true;
|
|
347
|
+
for (const p of payload) {
|
|
312
348
|
const upstreamPath = p.slice(prefix.length);
|
|
313
349
|
let headSha;
|
|
314
350
|
let upSha;
|