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.
Files changed (36) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +3 -3
  3. package/VERSION +1 -1
  4. package/framework/.claude/agents/CHANGELOG.md +45 -0
  5. package/framework/.claude/agents/REGISTRY.md +2 -2
  6. package/framework/.claude/agents/coder.md +3 -3
  7. package/framework/.claude/agents/remotion-animator-orchestrator.md +4 -3
  8. package/framework/.claude/agents/ui-expert.md +4 -10
  9. package/framework/.claude/agents/ui-quality-critic.md +27 -11
  10. package/framework/.claude/skills/design-system-init/CHANGELOG.md +5 -0
  11. package/framework/.claude/skills/design-system-init/SKILL.md +2 -2
  12. package/framework/.claude/skills/e2e-review/CHANGELOG.md +5 -0
  13. package/framework/.claude/skills/e2e-review/SKILL.md +2 -2
  14. package/framework/.claude/skills/frontend-design/CHANGELOG.md +20 -0
  15. package/framework/.claude/skills/frontend-design/SKILL.md +39 -216
  16. package/framework/.claude/skills/gamification-design/CHANGELOG.md +5 -0
  17. package/framework/.claude/skills/gamification-design/SKILL.md +2 -2
  18. package/framework/.claude/skills/ui-design/CHANGELOG.md +60 -1
  19. package/framework/.claude/skills/ui-design/SKILL.md +127 -75
  20. package/framework/.claude/skills/ui-design/references/anti-slop.md +106 -0
  21. package/framework/.claude/skills/ui-design/references/craft-standards.md +259 -0
  22. package/framework/.claude/skills/ui-design/references/design-brief.md +92 -0
  23. package/framework/.claude/skills/ui-design/references/design-directions.md +188 -0
  24. package/framework/.claude/skills/ui-design/references/evaluation.md +125 -165
  25. package/framework/.claude/skills/ui-design/references/generation.md +125 -92
  26. package/framework/.claude/skills/ui-design/references/inventory.md +9 -2
  27. package/framework/.claude/skills/ui-design/scripts/craft-check.mjs +248 -0
  28. package/framework/agents/component-manifest-schema.md +1 -1
  29. package/framework/agents/design-system-protocol.md +6 -6
  30. package/framework/agents/index.md +1 -0
  31. package/framework/agents/skills-mapping.md +25 -23
  32. package/framework/docs/PROJECT-CONFIGURATION.md +5 -5
  33. package/package.json +1 -1
  34. package/src/commands/configure.js +1 -1
  35. package/src/utils/__tests__/classify-divergence.test.js +42 -0
  36. 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` / `frontend-design` in the
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
- `frontend-design`, `design-system-init`), agents (`ui-expert`, `code-reviewer`),
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`/`frontend-design` generators) routinely
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`, `frontend-design`, `ui-expert`
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`, `frontend-design`, `code-reviewer`). It is **not** delegated to
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
- ### frontend-design
195
-
196
- **When to use**:
197
-
198
- - Building UI components
199
- - Creating pages
200
- - Styling work
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 new pages or components (context-aware mockups)
213
- - The `/prd` skill reaches its UI design phase
214
- - Redesigning existing pages / creating design options for review
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
- - Design-heavy features, visual refinement, design-system work
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
- > `ui-ux-pro-max` is a **tool used inside** the `ui-expert` agent / `ui-design`
223
- > skill (see REGISTRY.md listed as a Key Tool of `ui-expert`), not a
224
- > standalone invocable BALDART skill. Route UI work through `ui-design` /
225
- > `ui-expert`, not to `ui-ux-pro-max` directly.
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 --> /ui-design (mockups/options) or frontend-design;
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, frontend-design, prd, bug, kie-ai, simplify, playwright-skill, webapp-testing, gamification-design |
98
- | `ui_guidelines` | `docs/references/ui-guidelines.md` | ui-design, frontend-design, copywriting, gamification-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`, `frontend-design`, `kie-ai`, `motion-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`, `bug`, `frontend-design`, `simplify`, `playwright-skill` etc. treat the INDEX as BLOCKING. |
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` | Aesthetic mandates, illustration-motion contract |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"
@@ -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
- 'frontend-design, /design-review) needs these files to enforce coherence.'
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/ files
213
- // are now byte-identical to upstream (FETCH_HEAD) — the edit
214
- // has been absorbed into upstream (or reset away), so a
215
- // re-sync loses nothing. Pure history, not live drift → noise.
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
- // True when EVERY touched `.framework/` file's content at HEAD is already
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). Conservative: any file outside `.framework/`, missing
306
- // upstream, or whose SHA differs not absorbed (real drift, keep flagging).
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
- for (const p of touched) {
311
- if (!p.startsWith(prefix)) return false;
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;