baldart 4.74.0 → 4.76.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 (27) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/VERSION +1 -1
  3. package/bin/baldart.js +26 -0
  4. package/framework/.claude/agents/code-reviewer.md +15 -1
  5. package/framework/.claude/agents/prd-card-writer.md +7 -0
  6. package/framework/.claude/agents/ui-expert.md +41 -0
  7. package/framework/.claude/commands/design-review.md +1 -1
  8. package/framework/.claude/skills/design-sync/SKILL.md +174 -0
  9. package/framework/.claude/skills/design-system-init/SKILL.md +23 -0
  10. package/framework/.claude/skills/design-system-init/scripts/compile-ds-cards.mjs +119 -0
  11. package/framework/.claude/skills/design-system-init/scripts/render-manifest.mjs +187 -0
  12. package/framework/.claude/skills/ds-render/SKILL.md +75 -0
  13. package/framework/.claude/skills/e2e-review/SKILL.md +16 -3
  14. package/framework/.claude/skills/new/references/implement.md +40 -3
  15. package/framework/.claude/skills/prd/references/discovery-phase.md +25 -7
  16. package/framework/.claude/skills/prd/references/ui-design-phase.md +13 -0
  17. package/framework/.claude/skills/ui-design/SKILL.md +11 -0
  18. package/framework/agents/card-schema.md +16 -0
  19. package/framework/agents/design-system-protocol.md +25 -3
  20. package/framework/routines/ds-drift.routine.yml +13 -1
  21. package/framework/scripts/extract-mockup-design.mjs +109 -0
  22. package/package.json +1 -1
  23. package/src/commands/render.js +120 -0
  24. package/src/utils/design-sync-state.js +162 -0
  25. package/src/utils/render-adapters/claude-design-seed.js +51 -0
  26. package/src/utils/render-adapters/index.js +59 -0
  27. package/src/utils/render-adapters/storybook.js +104 -0
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ // extract-mockup-design.mjs — FALLBACK extractor for a Claude Design "Standalone
3
+ // HTML (offline)" export, whose readable code lives inside an encoded
4
+ // `<script type="__bundler/template">` blob. It decodes that blob into readable
5
+ // HTML + CSS under the PRD's mockups/_src/ so `ui-expert` can build FROM the code
6
+ // instead of reinterpreting a screenshot.
7
+ //
8
+ // This is the LAST-RESORT path. The preferred inputs are already-readable source:
9
+ // 1. the DesignSync MCP handoff (pulled at /prd time), or
10
+ // 2. a Download-as-.zip export (raw .jsx/.css on disk).
11
+ // Use this only when all the user has is the offline single-file HTML.
12
+ //
13
+ // Contract (mirrors canvas-intake-recipe.md:111 — NEVER abort the PRD):
14
+ // node extract-mockup-design.mjs --input <file> --out <dir>
15
+ // - always exits 0; prints a single JSON line to stdout describing the outcome
16
+ // - status: "ok" | "no-code" | "skipped"
17
+ // - on any internal error → {status:"skipped", reason} exit 0 (caller proceeds
18
+ // with the visual/image path unchanged).
19
+
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+
23
+ function parseArgs(argv) {
24
+ const a = { input: null, out: null };
25
+ for (let i = 2; i < argv.length; i++) {
26
+ if (argv[i] === '--input') a.input = argv[++i];
27
+ else if (argv[i] === '--out') a.out = argv[++i];
28
+ }
29
+ return a;
30
+ }
31
+
32
+ function emit(obj) {
33
+ process.stdout.write(JSON.stringify(obj) + '\n');
34
+ process.exit(0); // never non-zero: a fallback must not break /prd
35
+ }
36
+
37
+ function extractStyleBlocks(html) {
38
+ const out = [];
39
+ const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
40
+ let m;
41
+ while ((m = re.exec(html)) !== null) out.push(m[1]);
42
+ return out.join('\n\n');
43
+ }
44
+
45
+ function main() {
46
+ const { input, out } = parseArgs(process.argv);
47
+ if (!input || !out) emit({ status: 'skipped', reason: 'missing --input/--out' });
48
+ if (!fs.existsSync(input)) emit({ status: 'skipped', reason: 'input not found: ' + input });
49
+
50
+ const base = path.basename(input).replace(/\.[^.]+$/, '');
51
+ const raw = fs.readFileSync(input, 'utf8');
52
+
53
+ // No-code forms: a real image / binary, or text without any markup. The caller
54
+ // keeps using the visual (image-load) path for these.
55
+ if (!/[<{]/.test(raw.substring(0, 4096))) emit({ status: 'no-code', reason: 'no markup detected' });
56
+
57
+ fs.mkdirSync(out, { recursive: true });
58
+ const written = [];
59
+
60
+ // (a) Offline Claude Design bundle: readable code is the __bundler/template blob.
61
+ const tplMatch = raw.match(/<script type="__bundler\/template"[^>]*>([\s\S]*?)<\/script>/);
62
+ if (tplMatch) {
63
+ let template;
64
+ try {
65
+ template = JSON.parse(tplMatch[1].trim()); // the value is the full rendered HTML string
66
+ } catch (e) {
67
+ emit({ status: 'skipped', reason: 'template blob not JSON-parseable (format may have changed)' });
68
+ }
69
+ if (typeof template !== 'string' || template.length < 64) {
70
+ emit({ status: 'skipped', reason: 'template blob empty or not a string' });
71
+ }
72
+ const htmlPath = path.join(out, base + '.decoded.html');
73
+ fs.writeFileSync(htmlPath, template);
74
+ written.push(htmlPath);
75
+
76
+ const css = extractStyleBlocks(template);
77
+ if (css.trim()) {
78
+ const cssPath = path.join(out, base + '.styles.css');
79
+ fs.writeFileSync(cssPath, css);
80
+ written.push(cssPath);
81
+ }
82
+ emit({
83
+ status: 'ok',
84
+ source_form: 'offline-html-bundler-template',
85
+ input,
86
+ files: written,
87
+ note: 'Decoded the offline bundle into readable HTML+CSS. Binary assets in __bundler/manifest are NOT decoded (not needed to build from the code).',
88
+ });
89
+ }
90
+
91
+ // (b) Already-readable code (.jsx/.css/plain HTML with classes): just copy it in.
92
+ const ext = path.extname(input).toLowerCase();
93
+ const looksReadable = ['.jsx', '.tsx', '.css', '.js', '.ts'].includes(ext) ||
94
+ (ext === '.html' && /class=|className=/.test(raw));
95
+ if (looksReadable) {
96
+ const dest = path.join(out, path.basename(input));
97
+ fs.copyFileSync(input, dest);
98
+ emit({ status: 'ok', source_form: 'readable-source', input, files: [dest] });
99
+ }
100
+
101
+ // (c) Markup present but neither a known bundle nor readable component source.
102
+ emit({ status: 'no-code', reason: 'no __bundler/template and not readable component source' });
103
+ }
104
+
105
+ try {
106
+ main();
107
+ } catch (e) {
108
+ emit({ status: 'skipped', reason: String((e && e.message) || e) });
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.74.0",
3
+ "version": "4.76.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"
@@ -0,0 +1,120 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+ const { execFileSync } = require('child_process');
5
+ const renderAdapters = require('../utils/render-adapters');
6
+
7
+ /**
8
+ * `baldart render build|shot` (Stage C — the render harness engine).
9
+ *
10
+ * build : render-manifest.mjs over the registry specs → pick adapter → generate
11
+ * ephemeral render surface (Storybook stories) under <design_system>/.harness/.
12
+ * shot : build the surface + Playwright screenshot each entry → PNG + render-check.json.
13
+ * Consumer-run (needs the project's Storybook + Playwright); degrades cleanly.
14
+ *
15
+ * Rides on `features.has_design_system` (no new flag). No Storybook → clean no-op.
16
+ * The PNGs are for ISOLATED quality verification (e2e-review Phase 4b/4c) — NEVER a
17
+ * fidelity diff (the `mockup_source.level: harness-render` guard enforces that).
18
+ */
19
+
20
+ const CONFIG_FILE = 'baldart.config.yml';
21
+
22
+ function loadConfig(cwd) {
23
+ try { return yaml.load(fs.readFileSync(path.join(cwd, CONFIG_FILE), 'utf8')) || {}; }
24
+ catch (_) { return null; }
25
+ }
26
+
27
+ // Resolve a framework script path: installed (.framework/...) or this repo (framework/...).
28
+ function resolveScript(cwd, rel) {
29
+ for (const base of [path.join(cwd, '.framework', 'framework'), path.join(cwd, 'framework'), path.join(__dirname, '..', '..', 'framework')]) {
30
+ const p = path.join(base, rel);
31
+ if (fs.existsSync(p)) return p;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ function harnessDir(cwd, cfg) {
37
+ const ds = (cfg.paths && cfg.paths.design_system) || 'docs/design-system';
38
+ return path.join(cwd, ds, '.harness');
39
+ }
40
+
41
+ function specsDir(cwd, cfg) {
42
+ const ds = (cfg.paths && cfg.paths.design_system) || 'docs/design-system';
43
+ return path.join(cwd, ds, 'components');
44
+ }
45
+
46
+ /** build: generate the render-manifest + the adapter's render surface. */
47
+ function build({ cwd, full = false, log = console.error }) {
48
+ const cfg = loadConfig(cwd);
49
+ if (!cfg) return { status: 'skipped', reason: `no ${CONFIG_FILE}` };
50
+ if (!(cfg.features && cfg.features.has_design_system)) return { status: 'skipped', reason: 'features.has_design_system not true' };
51
+
52
+ const adapterName = renderAdapters.detect(cwd);
53
+ if (!adapterName) return { status: 'no-op', reason: 'no render adapter detected (no Storybook) — harness is optional; manifest still emitted' };
54
+
55
+ const specs = specsDir(cwd, cfg);
56
+ const out = harnessDir(cwd, cfg);
57
+ fs.mkdirSync(out, { recursive: true });
58
+ const manifestPath = path.join(out, 'render-manifest.json');
59
+
60
+ const script = resolveScript(cwd, '.claude/skills/design-system-init/scripts/render-manifest.mjs');
61
+ if (!script) return { status: 'skipped', reason: 'render-manifest.mjs not found (framework not installed?)' };
62
+ try {
63
+ execFileSync(process.execPath, [script, '--components', specs, '--out', manifestPath, ...(full ? ['--full'] : [])], { cwd, stdio: ['ignore', 'ignore', 'inherit'] });
64
+ } catch (e) {
65
+ return { status: 'skipped', reason: 'render-manifest failed: ' + String((e && e.message) || e) };
66
+ }
67
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
68
+
69
+ const adapter = renderAdapters.getAdapter(adapterName, cwd);
70
+ const gen = adapter.generate({ manifest, outDir: out });
71
+ log(`render build: adapter=${adapterName} components=${manifest.components.length} stories=${(gen.written || []).length}${gen.status ? ' status=' + gen.status : ''}`);
72
+ return { status: 'ok', adapter: adapterName, manifest: manifestPath, generated: gen, harness: out };
73
+ }
74
+
75
+ /**
76
+ * shot: render each entry and capture a PNG + render-check.json. CONSUMER-RUN:
77
+ * needs the project's Storybook build + Playwright. Implemented best-effort;
78
+ * degrades to a reasoned skip when the toolchain is missing (never throws).
79
+ */
80
+ function shot({ cwd, full = false, probe = false, log = console.error }) {
81
+ const built = build({ cwd, full, log });
82
+ if (built.status !== 'ok') return built;
83
+
84
+ // Preflight: Playwright must be reachable (do NOT auto-install in a consumer repo).
85
+ try { execFileSync('npx', ['playwright', '--version'], { cwd, stdio: 'ignore' }); }
86
+ catch (_) { return { ...built, shot: { status: 'skipped', reason: 'playwright not installed (npm i -D playwright && npx playwright install chromium)' } }; }
87
+
88
+ const adapter = renderAdapters.getAdapter(built.adapter, cwd);
89
+ if (typeof adapter.buildSurfaceCommand !== 'function') {
90
+ return { ...built, shot: { status: 'skipped', reason: `adapter ${built.adapter} has no static render surface (probe: ${probe})` } };
91
+ }
92
+ // The actual storybook-build + Playwright screenshot loop is consumer-run; we
93
+ // emit a render-check.json scaffold so e2e-review Phase 4b/4c has the certificate
94
+ // contract even before a live screenshot session. The screenshot loop itself is
95
+ // performed by the /ds-render skill via playwright-skill (it owns the browser).
96
+ const manifest = JSON.parse(fs.readFileSync(built.manifest, 'utf8'));
97
+ const total = manifest.components.reduce((n, c) => n + (c.entries || []).filter((e) => !e.truncated).length, 0);
98
+ const check = {
99
+ schema: 'baldart.render-check/1',
100
+ adapter: built.adapter,
101
+ surface_command: adapter.buildSurfaceCommand(),
102
+ total, rendered: 0, empty: 0, error: 0, variantsIdentical: 0, shimPartial: 0,
103
+ i18n_incomplete: null, // set true by the skill when a primitive rendered raw t() keys
104
+ note: 'Scaffold — the /ds-render skill runs the storybook build + Playwright screenshot loop and fills the counts. PNGs go to e2e-review Phase 4b/4c (quality) ONLY, never Phase 4 (fidelity).',
105
+ };
106
+ const checkPath = path.join(built.harness, 'render-check.json');
107
+ fs.writeFileSync(checkPath, JSON.stringify(check, null, 2) + '\n');
108
+ return { ...built, shot: { status: 'scaffolded', render_check: checkPath, total } };
109
+ }
110
+
111
+ function run(argv, cwd = process.cwd()) {
112
+ const sub = argv[0];
113
+ const full = argv.includes('--full');
114
+ const probe = argv.includes('--probe');
115
+ if (sub === 'build') return build({ cwd, full });
116
+ if (sub === 'shot') return shot({ cwd, full, probe });
117
+ throw new Error('usage: baldart render <build|shot> [--full] [--probe]');
118
+ }
119
+
120
+ module.exports = { run, build, shot, loadConfig, resolveScript };
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Consumer-side closed-loop state — `.baldart/design-sync.json` (since v4.76.0).
3
+ *
4
+ * Records the binding between the in-repo design-system registry (the SSOT) and
5
+ * its Claude Design satellite "mirror" project, plus the SYNCHRONIZED BASELINE
6
+ * used to detect drift in either direction. Sibling of `state.json`; consumer-
7
+ * owned, committed, never overwritten by `baldart update`. NOT a
8
+ * `baldart.config.yml` key (so the schema-change propagation rule does NOT apply).
9
+ *
10
+ * INVARIANTS (SSOT discipline — see framework/agents/design-system-protocol.md):
11
+ * - `source_of_truth` is ALWAYS "code". The satellite is a mirror, never authority.
12
+ * - The baseline is CONTENT-FREE: it stores ONLY hashes/revs
13
+ * (`tokens_sha`, per-component `source_sha`+`spec_sha`, satellite `rev`), never
14
+ * the component bodies. A baseline that carried content would become a 2nd SSOT.
15
+ * - The `source_sha` reused here is the SAME `git hash-object` value
16
+ * `extract-manifest.mjs` computes and `ds-drift` diffs — no new hashing scheme.
17
+ *
18
+ * Schema (v1):
19
+ * {
20
+ * "design_sync_version": 1,
21
+ * "project_id": "<claude-design design-system projectId>" | null,
22
+ * "source_of_truth": "code", // invariant
23
+ * "bound_at": "<iso>" | null,
24
+ * "baseline": {
25
+ * "tokens_sha": "<sha>" | null,
26
+ * "components": { "<Name>": { "source_sha": "...", "spec_sha": "...", "satellite_rev": "..." } }
27
+ * },
28
+ * "last_publish": { "version": "x.y.z", "ts": "...", "files_written": N } | null,
29
+ * "history": [ { "ts", "event": "bootstrap"|"publish"|"reconcile"|"seed", ... } ] // rolling 20
30
+ * }
31
+ *
32
+ * NOTE on 1:1 vs N:N (M5): v1 is 1:1 (one registry ↔ one satellite project) — the
33
+ * recommended default. A future N:N (multi-brand) layout would key `project_id` +
34
+ * `baseline` by design-system path; that is a deliberate schema bump, not silent.
35
+ */
36
+
37
+ const fs = require('fs');
38
+ const path = require('path');
39
+
40
+ const STATE_DIR = '.baldart';
41
+ const STATE_FILE = path.join(STATE_DIR, 'design-sync.json');
42
+ const DESIGN_SYNC_VERSION = 1;
43
+ const HISTORY_LIMIT = 20;
44
+
45
+ function defaultState() {
46
+ return {
47
+ design_sync_version: DESIGN_SYNC_VERSION,
48
+ project_id: null,
49
+ source_of_truth: 'code', // invariant — never "design"
50
+ bound_at: null,
51
+ baseline: { tokens_sha: null, components: {} },
52
+ last_publish: null,
53
+ history: [],
54
+ };
55
+ }
56
+
57
+ function fullPath(cwd = process.cwd()) {
58
+ return path.join(cwd, STATE_FILE);
59
+ }
60
+
61
+ function exists(cwd = process.cwd()) {
62
+ return fs.existsSync(fullPath(cwd));
63
+ }
64
+
65
+ function load(cwd = process.cwd()) {
66
+ const full = fullPath(cwd);
67
+ if (!fs.existsSync(full)) return defaultState();
68
+ try {
69
+ const raw = JSON.parse(fs.readFileSync(full, 'utf8'));
70
+ const merged = { ...defaultState(), ...raw };
71
+ // Deep-merge the baseline so a partial file never loses the nested shape.
72
+ merged.baseline = { ...defaultState().baseline, ...(raw.baseline || {}) };
73
+ merged.baseline.components = (raw.baseline && raw.baseline.components) || {};
74
+ merged.source_of_truth = 'code'; // enforce the invariant on read — never trust a tampered value
75
+ return merged;
76
+ } catch (_) {
77
+ // Corrupt file — fall back to defaults, never throw (auxiliary state).
78
+ return defaultState();
79
+ }
80
+ }
81
+
82
+ function save(state, cwd = process.cwd()) {
83
+ const dir = path.join(cwd, STATE_DIR);
84
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
85
+ state.source_of_truth = 'code'; // enforce on write
86
+ fs.writeFileSync(fullPath(cwd), JSON.stringify(state, null, 2) + '\n', 'utf8');
87
+ }
88
+
89
+ function appendHistory(state, entry) {
90
+ const next = [...(state.history || []), { ts: new Date().toISOString(), ...entry }];
91
+ return next.slice(-HISTORY_LIMIT);
92
+ }
93
+
94
+ /** Bind the registry to a satellite project (idempotent on project_id). */
95
+ function recordBootstrap({ projectId, initialState }, cwd = process.cwd()) {
96
+ const state = load(cwd);
97
+ const wasFirst = !state.project_id;
98
+ state.project_id = projectId || state.project_id;
99
+ if (wasFirst && state.project_id) state.bound_at = new Date().toISOString();
100
+ state.history = appendHistory(state, { event: 'bootstrap', project_id: state.project_id, initial_state: initialState || null });
101
+ save(state, cwd);
102
+ return state;
103
+ }
104
+
105
+ /**
106
+ * Record a publish and update the baseline. `components` is a map
107
+ * {<Name>: {source_sha, spec_sha, satellite_rev?}} — CONTENT-FREE.
108
+ */
109
+ function recordPublish({ version, components, tokensSha, filesWritten }, cwd = process.cwd()) {
110
+ const state = load(cwd);
111
+ state.last_publish = { version: version || null, ts: new Date().toISOString(), files_written: filesWritten || 0 };
112
+ if (tokensSha) state.baseline.tokens_sha = tokensSha;
113
+ for (const [name, anchors] of Object.entries(components || {})) {
114
+ state.baseline.components[name] = {
115
+ source_sha: anchors.source_sha || null,
116
+ spec_sha: anchors.spec_sha || null,
117
+ satellite_rev: anchors.satellite_rev || null,
118
+ };
119
+ }
120
+ state.history = appendHistory(state, { event: 'publish', version: version || null, files_written: filesWritten || 0 });
121
+ save(state, cwd);
122
+ return state;
123
+ }
124
+
125
+ /** Record a governed reconcile (a designer edit recepted via /ds-edit). */
126
+ function recordReconcile({ summary, components }, cwd = process.cwd()) {
127
+ const state = load(cwd);
128
+ // Reconcile updates the satellite_rev anchor for the touched components so the
129
+ // next drift-check measures from the just-reconciled point. NEVER writes content.
130
+ for (const [name, anchors] of Object.entries(components || {})) {
131
+ state.baseline.components[name] = { ...(state.baseline.components[name] || {}), ...pickAnchors(anchors) };
132
+ }
133
+ state.history = appendHistory(state, { event: 'reconcile', summary: summary || null });
134
+ save(state, cwd);
135
+ return state;
136
+ }
137
+
138
+ function pickAnchors(a = {}) {
139
+ const out = {};
140
+ if (a.source_sha) out.source_sha = a.source_sha;
141
+ if (a.spec_sha) out.spec_sha = a.spec_sha;
142
+ if (a.satellite_rev) out.satellite_rev = a.satellite_rev;
143
+ return out;
144
+ }
145
+
146
+ /** The per-component baseline anchors (for the drift-check to diff against). */
147
+ function getBaseline(cwd = process.cwd()) {
148
+ return load(cwd).baseline;
149
+ }
150
+
151
+ module.exports = {
152
+ load,
153
+ save,
154
+ exists,
155
+ appendHistory,
156
+ recordBootstrap,
157
+ recordPublish,
158
+ recordReconcile,
159
+ getBaseline,
160
+ STATE_FILE,
161
+ DESIGN_SYNC_VERSION,
162
+ };
@@ -0,0 +1,51 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Claude-Design-seed render adapter (Stage F — CONDITIONAL, probe-gated).
6
+ *
7
+ * The "design-led" (mayo) case: when /design-system-init seeded the registry FROM
8
+ * a Claude Design project, the seed brought the `.jsx` primitives + a
9
+ * `Design System.html` gallery into the repo. IF Playwright can isolate one
10
+ * primitive×variant from that gallery, this adapter renders per-primitive for free.
11
+ *
12
+ * REFUTED ASSUMPTION (kept honest): the "free render path" is NOT guaranteed —
13
+ * `.jsx` may be absent (source_kind html-css/image-only) and `Design System.html`
14
+ * may only render as a WHOLE gallery. So this adapter is gated on an empirical
15
+ * probe (`render shot --probe`): if per-primitive isolation fails, it degrades to
16
+ * a single gallery screenshot flagged `shimPartial`, and the canonical render path
17
+ * falls back to Storybook-regenerate. Render canonical = Storybook when present;
18
+ * Playwright-gallery only for the one-time seed (C4).
19
+ *
20
+ * v1 is a STUB that detects the seed marker and reports `needs_probe` — the
21
+ * Playwright isolation logic lands only after the probe confirms feasibility on a
22
+ * real seeded export (see the plan's must-measure-first #5).
23
+ */
24
+ class ClaudeDesignSeedAdapter {
25
+ constructor(cwd = process.cwd()) { this.cwd = cwd; }
26
+
27
+ static get name() { return 'claude-design-seed'; }
28
+ get label() { return 'Claude Design seed gallery'; }
29
+
30
+ /**
31
+ * Detect: a seed marker recorded in .baldart/design-sync.json history AND a
32
+ * Design System gallery html present in the seed staging dir. Conservative:
33
+ * only fires when both are true, else lets Storybook win.
34
+ */
35
+ static detect(cwd = process.cwd()) {
36
+ try {
37
+ const stateFile = path.join(cwd, '.baldart', 'design-sync.json');
38
+ if (!fs.existsSync(stateFile)) return false;
39
+ const st = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
40
+ const seeded = (st.history || []).some((h) => h.event === 'seed' || (h.event === 'bootstrap' && h.initial_state === 'only-claude-design'));
41
+ return Boolean(seeded);
42
+ } catch (_) { return false; }
43
+ }
44
+
45
+ /** v1: no generation — the probe must confirm Playwright isolation first. */
46
+ generate() {
47
+ return { written: [], skipped: [], status: 'needs_probe', note: 'Run `baldart render shot --probe` on the seeded export to confirm per-primitive Playwright isolation before this adapter generates. Until then, Storybook is the canonical render path.' };
48
+ }
49
+ }
50
+
51
+ module.exports = ClaudeDesignSeedAdapter;
@@ -0,0 +1,59 @@
1
+ const StorybookAdapter = require('./storybook');
2
+ const ClaudeDesignSeedAdapter = require('./claude-design-seed');
3
+
4
+ /**
5
+ * Render-harness adapter registry (Stage C, since v4.76.0).
6
+ *
7
+ * Mirrors the lsp/toolchain/routine adapter-registry pattern (REGISTRY +
8
+ * getAdapter + detectAll). An adapter turns a `render-manifest.json` into a real
9
+ * render surface (mounted primitives) per ONE strategy. The harness exists to
10
+ * render registry primitives in isolation — today BALDART documents components but
11
+ * never renders them isolated.
12
+ *
13
+ * Adding a render strategy:
14
+ * 1. Create `src/utils/render-adapters/<name>.js` with the shape of
15
+ * StorybookAdapter (static get name, get label, static detect(cwd),
16
+ * generate({manifest,outDir})).
17
+ * 2. Add it to REGISTRY below, in PRIORITY order (detectAll returns the first
18
+ * that fires — Storybook is canonical when present).
19
+ *
20
+ * REFUTED (do NOT add): a per-stack framework-native preview-route generator +
21
+ * an auto-provider-shim. Those are code-generators with unbounded surface that
22
+ * mount providers out-of-tree → wrong-but-green renders (false fidelity). Use
23
+ * Storybook where present; the seed-gallery only for the one-time seed case.
24
+ */
25
+ const REGISTRY = {
26
+ storybook: StorybookAdapter,
27
+ 'claude-design-seed': ClaudeDesignSeedAdapter,
28
+ };
29
+
30
+ // Priority order for detection: Storybook is the canonical render path when
31
+ // present; the seed gallery is a conditional one-time fallback.
32
+ const PRIORITY = ['storybook', 'claude-design-seed'];
33
+
34
+ function listAdapters() { return Object.keys(REGISTRY); }
35
+
36
+ function getAdapter(name, cwd) {
37
+ const Cls = REGISTRY[name];
38
+ if (!Cls) throw new Error(`Unknown render adapter: ${name}. Available: ${listAdapters().join(', ')}`);
39
+ return new Cls(cwd);
40
+ }
41
+
42
+ /**
43
+ * Return the FIRST adapter (by priority) whose static detect() fires, or null
44
+ * when none does (→ the harness is a no-op for this consumer, which is fine:
45
+ * Stage C degrades cleanly without Storybook). Pure: no side effects.
46
+ */
47
+ function detect(cwd = process.cwd()) {
48
+ for (const name of PRIORITY) {
49
+ try { if (REGISTRY[name].detect(cwd)) return name; } catch (_) { /* keep scanning */ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /** All adapters whose detect() fires (diagnostic). */
55
+ function detectAll(cwd = process.cwd()) {
56
+ return PRIORITY.filter((name) => { try { return REGISTRY[name].detect(cwd); } catch { return false; } });
57
+ }
58
+
59
+ module.exports = { REGISTRY, listAdapters, getAdapter, detect, detectAll };
@@ -0,0 +1,104 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Storybook render adapter (Stage C, v1 — the ONLY render path the adversarial
6
+ * pass kept). When the consumer already has Storybook, we generate EPHEMERAL CSF
7
+ * stories from the render-manifest and let Storybook's OWN runtime do the
8
+ * context-shimming (ThemeProvider/router/i18n via the project's real
9
+ * `.storybook/preview` decorators). We never re-grep a provider chain and never
10
+ * mount out-of-tree — that refuted "auto-provider-shim" is exactly what produces
11
+ * wrong-but-green renders. Storybook exists to solve isolated mounting; reuse it.
12
+ *
13
+ * The generated stories carry a marker so they are identifiable + safe to delete;
14
+ * co-located hand-authored stories are NEVER touched.
15
+ */
16
+ const MARKER = '// @baldart-render-harness — generated from render-manifest.json, safe to delete';
17
+
18
+ class StorybookAdapter {
19
+ constructor(cwd = process.cwd()) { this.cwd = cwd; }
20
+
21
+ static get name() { return 'storybook'; }
22
+ get label() { return 'Storybook'; }
23
+
24
+ /** Detect: a `.storybook/` config dir at the repo root (matches configure.js:234). */
25
+ static detect(cwd = process.cwd()) {
26
+ return fs.existsSync(path.join(cwd, '.storybook'));
27
+ }
28
+
29
+ /**
30
+ * Generate one ephemeral `.stories.tsx` per component into outDir.
31
+ * Returns { written: [paths], skipped: [names] }.
32
+ * i18n note (M6): the project's own preview decorators supply the i18n provider
33
+ * if it has one; if it doesn't, the story still renders but text shows raw keys —
34
+ * such PNGs are flagged i18n-incomplete by `render shot`, never fed to a fidelity diff.
35
+ */
36
+ generate({ manifest, outDir }) {
37
+ fs.mkdirSync(outDir, { recursive: true });
38
+ const written = [];
39
+ const skipped = [];
40
+ for (const comp of (manifest.components || [])) {
41
+ if (!comp.source) { skipped.push(comp.name); continue; } // no source → can't import → skip honestly
42
+ const file = path.join(outDir, `${comp.name}.baldart.stories.tsx`);
43
+ fs.writeFileSync(file, this._story(comp, outDir));
44
+ written.push(file);
45
+ }
46
+ return { written, skipped };
47
+ }
48
+
49
+ _story(comp, outDir) {
50
+ const abs = path.join(this.cwd, comp.source);
51
+ let imp = path.relative(outDir, abs).replace(/\.(tsx?|jsx?)$/, '');
52
+ if (!imp.startsWith('.')) imp = './' + imp;
53
+ imp = imp.split(path.sep).join('/');
54
+ const lines = [
55
+ MARKER,
56
+ `import { ${comp.name} } from '${imp}';`,
57
+ ``,
58
+ `export default { title: 'Baldart Harness/${comp.name}', component: ${comp.name} };`,
59
+ ``,
60
+ ];
61
+ const usedNames = new Set();
62
+ for (const entry of (comp.entries || [])) {
63
+ if (entry.truncated) continue;
64
+ let exp = this._exportName(entry, comp.name);
65
+ while (usedNames.has(exp)) exp += '_';
66
+ usedNames.add(exp);
67
+ lines.push(`export const ${exp} = { args: ${this._args(entry.props)} };`);
68
+ }
69
+ return lines.join('\n') + '\n';
70
+ }
71
+
72
+ _exportName(entry, compName) {
73
+ let tail = entry.id.startsWith(compName + '--') ? entry.id.slice(compName.length + 2) : entry.id;
74
+ const parts = tail.split('--').map((p) => p.replace(/[^A-Za-z0-9]+/g, ' ').trim());
75
+ let name = parts.map((p) => p.split(' ').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('')).join('');
76
+ if (!name || /^\d/.test(name)) name = 'Story' + name;
77
+ return name;
78
+ }
79
+
80
+ _args(props) {
81
+ const out = Object.entries(props || {}).map(([k, v]) => `${JSON.stringify(k)}: ${this._coerce(v)}`);
82
+ return `{ ${out.join(', ')} }`;
83
+ }
84
+
85
+ _coerce(v) {
86
+ if (v === 'true' || v === true) return 'true';
87
+ if (v === 'false' || v === false) return 'false';
88
+ if (typeof v === 'string' && /^-?\d+(\.\d+)?$/.test(v)) return v;
89
+ return JSON.stringify(v);
90
+ }
91
+
92
+ /** How `render shot` builds a static render surface (consumer-run, needs deps). */
93
+ buildSurfaceCommand() {
94
+ return { cmd: 'npx', args: ['storybook', 'build', '-o', '.harness/storybook-static'] };
95
+ }
96
+
97
+ /** The iframe URL pattern for a story id in a static Storybook build. */
98
+ storyUrl(baseUrl, storyId) {
99
+ return `${baseUrl}/iframe.html?id=${storyId}&viewMode=story`;
100
+ }
101
+ }
102
+
103
+ module.exports = StorybookAdapter;
104
+ module.exports.MARKER = MARKER;