sdtk-design-kit 0.2.1 → 0.3.1

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.
@@ -4,620 +4,189 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { parseFlags } = require("../lib/args");
6
6
  const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths");
7
- const { inferDomainProfile } = require("../lib/domain-profile");
8
7
  const { ValidationError } = require("../lib/errors");
9
- const { renderMultiPagePrototype, screenFileName } = require("../lib/prototype-renderer");
10
- const { DEFAULT_STYLE, availableStyleNames, resolveStyleName } = require("../lib/style-presets");
11
8
 
12
9
  const PROTOTYPE_FLAG_DEFS = {
13
10
  help: { type: "boolean" },
14
11
  "project-path": { type: "string" },
15
12
  force: { type: "boolean" },
16
- style: { type: "string" },
17
13
  };
18
14
 
19
- const REQUIRED_ARTIFACTS = [
20
- ["docs/design/DESIGN_BRIEF.md", "designBriefPath"],
21
- ["docs/design/SCREEN_MAP.md", "screenMapPath"],
22
- ["docs/design/DESIGN_SYSTEM.md", "designSystemPath"],
23
- ["docs/design/wireframes/LANDING.md", "landingWireframePath"],
24
- ["docs/design/wireframes/ONBOARDING.md", "onboardingWireframePath"],
25
- ["docs/design/wireframes/DASHBOARD.md", "dashboardWireframePath"],
26
- ];
27
-
28
- const THEME_TOKENS = {
29
- "minimal-saas": {
30
- bg: "#F7F8FA",
31
- surface: "#FFFFFF",
32
- text: "#1F2933",
33
- muted: "#667085",
34
- primary: "#2563EB",
35
- accent: "#0F8A5F",
36
- border: "#D9DEE7",
37
- shadow: "0 14px 40px rgba(31,41,51,0.10)",
38
- },
39
- "premium-dashboard": {
40
- bg: "#F5F7FB",
41
- surface: "#FFFFFF",
42
- text: "#111827",
43
- muted: "#64748B",
44
- primary: "#0F766E",
45
- accent: "#2563EB",
46
- border: "#D8E0EA",
47
- shadow: "0 18px 48px rgba(15,23,42,0.12)",
48
- },
49
- "bold-founder": {
50
- bg: "#111111",
51
- surface: "#18181B",
52
- text: "#FAFAFA",
53
- muted: "#A1A1AA",
54
- primary: "#F97316",
55
- accent: "#22C55E",
56
- border: "#27272A",
57
- shadow: "0 20px 0 rgba(249,115,22,0.24)",
58
- },
59
- "warm-editorial": {
60
- bg: "#FAF7F2",
61
- surface: "#FFFFFF",
62
- text: "#1C1A17",
63
- muted: "#8A817A",
64
- primary: "#C0512F",
65
- accent: "#2F5B4F",
66
- border: "rgba(47,91,79,0.16)",
67
- shadow: "0 16px 36px rgba(28,26,23,0.08)",
68
- },
69
- };
15
+ function toPosix(value) {
16
+ return String(value || "").replace(/\\/g, "/");
17
+ }
70
18
 
71
- function readInputContractState(paths) {
72
- if (!fs.existsSync(paths.designStartInputStatePath) || !fs.statSync(paths.designStartInputStatePath).isFile()) {
73
- return null;
74
- }
75
- try {
76
- return JSON.parse(fs.readFileSync(paths.designStartInputStatePath, "utf-8"));
77
- } catch (_err) {
78
- return null;
79
- }
19
+ function escapeHtml(value) {
20
+ return String(value == null ? "" : value)
21
+ .replace(/&/g, "&")
22
+ .replace(/</g, "&lt;")
23
+ .replace(/>/g, "&gt;")
24
+ .replace(/"/g, "&quot;");
80
25
  }
81
26
 
82
27
  function cmdPrototypeHelp() {
83
28
  console.log(`SDTK-DESIGN Prototype
84
29
 
85
30
  Usage:
86
- sdtk-design prototype [--style <preset>] [--project-path <path>] [--force]
31
+ sdtk-design prototype [--project-path <path>] [--force]
87
32
 
88
33
  Example:
89
- sdtk-design prototype --style premium-dashboard
90
-
91
- Style presets:
92
- ${availableStyleNames().join(", ")}
93
- Default: inferred from docs/design/DESIGN_SYSTEM.md, then ${DEFAULT_STYLE}
34
+ sdtk-design prototype
94
35
 
95
36
  Reads:
96
- docs/design/DESIGN_BRIEF.md
97
- docs/design/SCREEN_MAP.md
37
+ .sdtk/design/START_INPUT_STATE.json (must be INPUT_CONTRACT_READY)
98
38
  docs/design/DESIGN_SYSTEM.md
99
- docs/design/wireframes/LANDING.md
100
- docs/design/wireframes/ONBOARDING.md
101
- docs/design/wireframes/DASHBOARD.md
102
- .sdtk/design/START_INPUT_STATE.json (optional; enables explicit multi-screen rendering when ready)
39
+ docs/design/DESIGN_TOKENS.json
40
+ docs/design/screens/*_DESIGN_BRIEF.md
103
41
 
104
42
  Creates:
105
- legacy/simple mode:
106
- docs/design/prototype/index.html
107
- from-spec multi-page mode:
108
- docs/design/prototype/index.html
109
- docs/design/prototype/screens/*.html
110
- docs/design/prototype/assets/prototype.css
111
- docs/design/prototype/assets/prototype.js
43
+ docs/design/prototype/.manifest.json
44
+ docs/design/prototype/index.html
45
+ docs/design/prototype/screens/ (empty output directory for the agent-owned screen HTML)
112
46
 
113
47
  Safety:
114
48
  Local files only.
115
- Existing prototype index is not overwritten unless --force is explicit.
49
+ Existing prototype manifest and index are not overwritten unless --force is explicit.
50
+ Screen HTML files under docs/design/prototype/screens are never overwritten by this command.
116
51
  No production app code outside docs/design/prototype.
117
52
  No JavaScript app runtime, server, network call, .sdtk/atlas, or SDTK-WIKI output mutation.`);
118
53
  return 0;
119
54
  }
120
55
 
121
- function resolvePrototypeMode(canUseMultiScreen) {
122
- if (!canUseMultiScreen) {
123
- return "single";
124
- }
125
- const raw = String(process.env.SDTK_DESIGN_PROTOTYPE_MODE || "").trim().toLowerCase();
126
- if (!raw) {
127
- return "multi";
56
+ function readInputContractState(paths) {
57
+ if (!fs.existsSync(paths.designStartInputStatePath) || !fs.statSync(paths.designStartInputStatePath).isFile()) {
58
+ return null;
128
59
  }
129
- if (raw === "single" || raw === "multi") {
130
- return raw;
60
+ try {
61
+ return JSON.parse(fs.readFileSync(paths.designStartInputStatePath, "utf-8"));
62
+ } catch (_err) {
63
+ return null;
131
64
  }
132
- throw new ValidationError(
133
- `Unsupported SDTK_DESIGN_PROTOTYPE_MODE "${process.env.SDTK_DESIGN_PROTOTYPE_MODE}". Use "single" or "multi". No project files were changed.`
134
- );
135
- }
136
-
137
- function wireframePath(paths, fileName) {
138
- return path.join(paths.wireframesPath, fileName);
139
65
  }
140
66
 
141
- function requiredArtifactTargets(paths) {
142
- return REQUIRED_ARTIFACTS.map(([relativePath, key]) => {
143
- const filePath =
144
- key === "landingWireframePath"
145
- ? wireframePath(paths, "LANDING.md")
146
- : key === "onboardingWireframePath"
147
- ? wireframePath(paths, "ONBOARDING.md")
148
- : key === "dashboardWireframePath"
149
- ? wireframePath(paths, "DASHBOARD.md")
150
- : paths[key];
151
- return { relativePath, filePath };
152
- });
153
- }
154
-
155
- function firstLine(content, fallback) {
156
- const line = String(content || "")
157
- .split(/\r?\n/)
158
- .map((value) => value.trim())
159
- .find(Boolean);
160
- return line ? line.replace(/^#\s*/, "") : fallback;
161
- }
162
-
163
- function escapeHtml(value) {
164
- return String(value)
165
- .replace(/&/g, "&amp;")
166
- .replace(/</g, "&lt;")
167
- .replace(/>/g, "&gt;")
168
- .replace(/"/g, "&quot;");
67
+ function assertReadyInputContract(statePayload) {
68
+ return (
69
+ statePayload &&
70
+ statePayload.mode === "from-spec" &&
71
+ statePayload.analysisStatus === "INPUT_CONTRACT_READY" &&
72
+ statePayload.screenModel &&
73
+ Array.isArray(statePayload.screenModel.screens) &&
74
+ statePayload.screenModel.screens.length > 0
75
+ );
169
76
  }
170
77
 
171
- function inferStyleFromDesignSystem(designSystemContent) {
172
- const content = String(designSystemContent || "");
173
- return availableStyleNames().find((styleName) => new RegExp(`Preset:\\s*${styleName}\\b`, "i").test(content)) || DEFAULT_STYLE;
78
+ function screenBriefRelativePath(screenId) {
79
+ return `docs/design/screens/${String(screenId || "")
80
+ .replace(/[^a-zA-Z0-9_-]+/g, "-")}_DESIGN_BRIEF.md`;
174
81
  }
175
82
 
176
- function prototypeContent({ style, briefContent, screenMapContent, designSystemContent, wireframeContents = [] }) {
177
- const styleName = resolveStyleName(style);
178
- const tokens = THEME_TOKENS[styleName];
179
- const profile = inferDomainProfile({ briefContent, screenMapContent, designSystemContent, wireframeContents });
180
-
181
- return `<!doctype html>
182
- <html lang="en">
183
- <head>
184
- <meta charset="utf-8">
185
- <meta name="viewport" content="width=device-width, initial-scale=1">
186
- <title>${escapeHtml(profile.productName)} Prototype</title>
187
- <style>
188
- :root {
189
- --bg: ${tokens.bg};
190
- --surface: ${tokens.surface};
191
- --text: ${tokens.text};
192
- --muted: ${tokens.muted};
193
- --primary: ${tokens.primary};
194
- --accent: ${tokens.accent};
195
- --border: ${tokens.border};
196
- --shadow: ${tokens.shadow};
197
- }
198
- * { box-sizing: border-box; }
199
- body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
200
- .sdtk-prototype { width: min(1180px, calc(100vw - 32px)); margin: 0 auto; padding: 32px 0 56px; }
201
- .prototype-header { display: flex; justify-content: space-between; gap: 20px; align-items: center; margin-bottom: 22px; }
202
- .brand { font-weight: 800; letter-spacing: 0; }
203
- .style-pill, .status-pill { display: inline-flex; align-items: center; min-height: 28px; border: 1px solid var(--border); border-radius: 999px; padding: 4px 10px; color: var(--muted); background: var(--surface); font-size: 12px; font-weight: 750; }
204
- .prototype-section { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; box-shadow: var(--shadow); margin: 18px 0; overflow: hidden; }
205
- .section-inner { padding: clamp(22px, 4vw, 44px); }
206
- .eyebrow { margin: 0 0 8px; color: var(--primary); font-size: 12px; font-weight: 850; text-transform: uppercase; letter-spacing: 0; }
207
- h1, h2, h3, p { margin-top: 0; }
208
- h1 { max-width: 860px; font-size: clamp(34px, 7vw, 72px); line-height: 1.02; margin-bottom: 16px; }
209
- h2 { font-size: clamp(26px, 4vw, 40px); line-height: 1.12; margin-bottom: 12px; }
210
- h3 { font-size: 16px; margin-bottom: 8px; }
211
- .support { max-width: 700px; color: var(--muted); font-size: 18px; }
212
- .button-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 22px; }
213
- .btn { display: inline-flex; align-items: center; justify-content: center; min-height: 42px; border-radius: 8px; padding: 0 18px; font-weight: 800; text-decoration: none; }
214
- .btn-primary { background: var(--primary); color: #fff; }
215
- .btn-secondary { color: var(--text); border: 1px solid var(--border); background: transparent; }
216
- .workflow-grid, .metric-grid, .dashboard-grid { display: grid; gap: 14px; }
217
- .workflow-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 28px; }
218
- .workflow-card, .metric-card, .item-card, .form-card, .empty-state { border: 1px solid var(--border); border-radius: 8px; padding: 16px; background: color-mix(in srgb, var(--surface) 92%, var(--bg)); }
219
- .workflow-card strong, .metric-card strong { display: block; font-size: 24px; }
220
- .onboarding-layout { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(320px, 0.55fr); gap: 20px; align-items: start; }
221
- .field { display: grid; gap: 6px; margin-bottom: 12px; }
222
- .input-preview { min-height: 42px; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center; padding: 0 12px; color: var(--muted); }
223
- .metric-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 16px 0; }
224
- .dashboard-grid { grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr); }
225
- .item-card { display: grid; gap: 8px; }
226
- .item-meta { display: flex; flex-wrap: wrap; gap: 8px; }
227
- .status-pill { color: var(--primary); }
228
- .empty-state { color: var(--muted); border-style: dashed; }
229
- .style-premium-dashboard .metric-card strong { font-size: 30px; }
230
- .style-bold-founder h1 { text-transform: uppercase; }
231
- .style-warm-editorial h1, .style-warm-editorial h2 { font-family: Georgia, "Times New Roman", serif; }
232
- @media (max-width: 780px) {
233
- .prototype-header, .onboarding-layout { display: block; }
234
- .workflow-grid, .metric-grid, .dashboard-grid { grid-template-columns: 1fr; }
235
- h1 { font-size: 36px; }
236
- }
237
- </style>
238
- </head>
239
- <body>
240
- <main class="sdtk-prototype style-${styleName}" data-style-preset="${styleName}">
241
- <header class="prototype-header">
242
- <div class="brand">${escapeHtml(profile.productName)}</div>
243
- <span class="style-pill">Style preset: ${styleName}</span>
244
- </header>
245
-
246
- <section class="prototype-section section-landing" aria-labelledby="landing-title">
247
- <div class="section-inner">
248
- <p class="eyebrow">Landing</p>
249
- <h1 id="landing-title">${escapeHtml(profile.promise)}</h1>
250
- <p class="support">A demo-ready first screen with one clear CTA, a focused product promise, and a workflow preview that shows what happens after signup.</p>
251
- <div class="button-row">
252
- <a class="btn btn-primary" href="#onboarding">${escapeHtml(profile.primaryAction)}</a>
253
- <a class="btn btn-secondary" href="#dashboard">${escapeHtml(profile.secondaryAction)}</a>
254
- </div>
255
- <div class="workflow-grid">
256
- <div class="workflow-card"><strong>01</strong><span>Capture a ${escapeHtml(profile.itemSingular)} with status and context.</span></div>
257
- <div class="workflow-card"><strong>02</strong><span>Record the next action before it slips.</span></div>
258
- <div class="workflow-card"><strong>03</strong><span>Scan the dashboard and choose what to do next.</span></div>
259
- </div>
260
- </div>
261
- </section>
262
-
263
- <section class="prototype-section section-onboarding" id="onboarding" aria-labelledby="onboarding-title">
264
- <div class="section-inner onboarding-layout">
265
- <div>
266
- <p class="eyebrow">Onboarding</p>
267
- <h2 id="onboarding-title">Create a focused ${escapeHtml(profile.setupLabel.toLowerCase())} in one step.</h2>
268
- <p class="support">The setup flow asks only for the minimum needed before the user can create the first ${escapeHtml(profile.itemSingular)}.</p>
269
- </div>
270
- <div class="form-card">
271
- <div class="field"><strong>${escapeHtml(profile.setupNameLabel)}</strong><div class="input-preview">${escapeHtml(profile.setupNameValue)}</div></div>
272
- <div class="field"><strong>Primary workflow</strong><div class="input-preview">${escapeHtml(profile.itemAction)}</div></div>
273
- <a class="btn btn-primary" href="#dashboard">Continue</a>
274
- </div>
275
- </div>
276
- </section>
277
-
278
- <section class="prototype-section section-dashboard" id="dashboard" aria-labelledby="dashboard-title">
279
- <div class="section-inner">
280
- <p class="eyebrow">Dashboard</p>
281
- <h2 id="dashboard-title">${escapeHtml(profile.dashboardTitle)}</h2>
282
- <div class="metric-grid">
283
- <div class="metric-card"><span>${escapeHtml(profile.metricLabels[0])}</span><strong>${escapeHtml(profile.metricValues[0])}</strong></div>
284
- <div class="metric-card"><span>${escapeHtml(profile.metricLabels[1])}</span><strong>${escapeHtml(profile.metricValues[1])}</strong></div>
285
- <div class="metric-card"><span>${escapeHtml(profile.metricLabels[2])}</span><strong>${escapeHtml(profile.metricValues[2])}</strong></div>
286
- <div class="metric-card"><span>${escapeHtml(profile.metricLabels[3])}</span><strong>${escapeHtml(profile.metricValues[3])}</strong></div>
287
- </div>
288
- <div class="dashboard-grid">
289
- <div class="form-card">
290
- <h3>${escapeHtml(profile.itemAction)}</h3>
291
- <div class="field"><strong>${escapeHtml(profile.itemNameLabel)}</strong><div class="input-preview">${escapeHtml(profile.itemNameValue)}</div></div>
292
- <div class="field"><strong>${escapeHtml(profile.itemContextLabel)}</strong><div class="input-preview">${escapeHtml(profile.itemContextValue)}</div></div>
293
- <a class="btn btn-primary" href="#dashboard">${escapeHtml(profile.saveAction)}</a>
294
- </div>
295
- <div class="item-card">
296
- <h3>${escapeHtml(profile.itemNameValue)}</h3>
297
- <div class="item-meta">
298
- <span class="status-pill">${escapeHtml(profile.statusExample)}</span>
299
- <span class="status-pill">${escapeHtml(profile.itemContextValue)}</span>
300
- </div>
301
- <p class="support">Keep context, status, and next action visible in the ${escapeHtml(profile.collectionSurface)}.</p>
302
- <div class="empty-state">${escapeHtml(profile.emptyState)}</div>
303
- </div>
304
- </div>
305
- </div>
306
- </section>
307
- </main>
308
- </body>
309
- </html>
310
- `;
83
+ function screenOutRelativePath(screenId) {
84
+ return `docs/design/prototype/screens/${String(screenId || "")
85
+ .replace(/[^a-zA-Z0-9_-]+/g, "-")}.html`;
311
86
  }
312
87
 
313
88
  function normalizeScreenRole(screen) {
314
- const token = `${screen.screenId || ""} ${screen.title || ""}`.toLowerCase();
89
+ const token = `${screen && screen.screenId ? screen.screenId : ""} ${screen && screen.title ? screen.title : ""} ${
90
+ screen && screen.templateRole ? screen.templateRole : ""
91
+ } ${screen && screen.template_role ? screen.template_role : ""}`.toLowerCase();
315
92
  if (token.includes("home")) return "home";
316
- if (token.includes("category")) return "category";
93
+ if (token.includes("category") || token.includes("catalog")) return "category";
317
94
  if (token.includes("product-detail") || token.includes("product detail") || token.includes("pdp")) return "product-detail";
318
95
  if (token.includes("search")) return "search";
319
- if (token.includes("cart")) return "cart";
96
+ if (token.includes("cart") && !token.includes("checkout")) return "cart";
320
97
  if (token.includes("checkout")) return "checkout";
321
98
  if (token.includes("order-history") || token.includes("order history")) return "order-history";
322
99
  if (token.includes("order-detail") || token.includes("order detail")) return "order-detail";
323
100
  if (token.includes("account")) return "account-info";
324
- if (token.includes("configurator") || token.includes("mode-b")) return "mode-b-configurator";
325
- return screen.screenId || "screen";
326
- }
327
-
328
- function renderPrimitiveChips(primitiveNames) {
329
- if (!Array.isArray(primitiveNames) || primitiveNames.length === 0) return "";
330
- return `<div class="chip-row">${primitiveNames.map((item) => `<span class="chip">${escapeHtml(item)}</span>`).join("")}</div>`;
101
+ if (token.includes("configurator") || token.includes("bom") || token.includes("mode-b")) return "mode-b-configurator";
102
+ return screen && screen.screenId ? screen.screenId : "screen";
331
103
  }
332
104
 
333
- function renderStateChips(requiredStates) {
334
- if (!Array.isArray(requiredStates) || requiredStates.length === 0) return "";
335
- return `<div class="chip-row">${requiredStates
336
- .slice(0, 3)
337
- .map((item) => `<span class="chip" data-chip-type="state">${escapeHtml(item)}</span>`)
338
- .join("")}</div>`;
105
+ function requiredPrototypeInputs(paths, screens) {
106
+ return [
107
+ { relativePath: "docs/design/DESIGN_SYSTEM.md", filePath: paths.designSystemPath },
108
+ { relativePath: "docs/design/DESIGN_TOKENS.json", filePath: paths.designTokensPath },
109
+ ...screens.map((screen) => {
110
+ const relativePath = screenBriefRelativePath(screen.screenId);
111
+ return {
112
+ relativePath,
113
+ filePath: path.join(paths.projectPath, relativePath),
114
+ };
115
+ }),
116
+ ];
339
117
  }
340
118
 
341
- function screenBriefPath(paths, screenId) {
342
- return path.join(paths.screensPath, `${screenId}_DESIGN_BRIEF.md`);
343
- }
344
-
345
- function screenBriefSidecarPath(paths, screenId) {
346
- return path.join(paths.designScreenBriefsStatePath, `${screenId}.json`);
347
- }
348
-
349
- function readJsonSafe(filePath) {
350
- try {
351
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
352
- } catch (_err) {
353
- return null;
354
- }
355
- }
356
-
357
- function loadHighFidelityContracts(paths, statePayload) {
358
- const warnings = [];
359
- if (!fs.existsSync(paths.componentPatternLibraryPath)) {
360
- warnings.push("missing docs/design/COMPONENT_PATTERN_LIBRARY.md");
361
- }
362
- if (!fs.existsSync(paths.designTokensPath)) {
363
- warnings.push("missing docs/design/DESIGN_TOKENS.json");
364
- }
365
- const missingBriefs = [];
366
- const screenContracts = new Map();
367
- for (const screen of statePayload.screenModel.screens) {
368
- const briefPath = screenBriefPath(paths, screen.screenId);
369
- if (!fs.existsSync(briefPath)) {
370
- missingBriefs.push(screen.screenId);
371
- continue;
372
- }
373
- const sidecarPath = screenBriefSidecarPath(paths, screen.screenId);
374
- screenContracts.set(screen.screenId, {
375
- briefPath,
376
- briefContent: fs.readFileSync(briefPath, "utf-8"),
377
- sidecar: fs.existsSync(sidecarPath) ? readJsonSafe(sidecarPath) : null,
378
- });
379
- }
380
- if (missingBriefs.length > 0) {
381
- warnings.push(`missing per-screen briefs for: ${missingBriefs.join(", ")}`);
382
- }
383
- if (warnings.length > 0) {
384
- return { ready: false, warnings, screenContracts: new Map(), tokens: null };
385
- }
386
- const tokens = readJsonSafe(paths.designTokensPath);
387
- if (!tokens || typeof tokens !== "object") {
388
- return { ready: false, warnings: ["invalid docs/design/DESIGN_TOKENS.json"], screenContracts: new Map(), tokens: null };
389
- }
390
- return { ready: true, warnings: [], screenContracts, tokens };
391
- }
392
-
393
- function managedPrototypeTargets(paths, statePayload, mode) {
394
- const targets = [{ relativePath: "docs/design/prototype/index.html", filePath: paths.prototypeIndexPath }];
395
- if (
396
- mode === "multi" &&
397
- statePayload &&
398
- statePayload.screenModel &&
399
- Array.isArray(statePayload.screenModel.screens)
400
- ) {
401
- targets.push(
402
- { relativePath: "docs/design/prototype/assets/prototype.css", filePath: paths.prototypeCssPath },
403
- { relativePath: "docs/design/prototype/assets/prototype.js", filePath: paths.prototypeJsPath },
404
- ...statePayload.screenModel.screens.map((screen) => ({
405
- relativePath: `docs/design/prototype/screens/${screenFileName(screen)}`,
406
- filePath: path.join(paths.prototypeScreensPath, screenFileName(screen)),
407
- }))
408
- );
409
- }
410
- return targets;
411
- }
412
-
413
- function tokenOrDefault(tokens, keyPath, fallback) {
414
- const value = keyPath.reduce((obj, key) => (obj && Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : undefined), tokens);
415
- return value == null ? fallback : value;
416
- }
417
-
418
- function renderHighFidelityScreen(screenRole, title) {
419
- switch (screenRole) {
420
- case "home":
421
- return `
422
- <section class="hf-hero panel"><h3>${escapeHtml(title)} hero</h3><p>Category-first commerce landing with featured products and guided CTA.</p><div class="button-row"><a class="btn btn-primary" href="#">Start purchase</a><a class="btn btn-secondary" href="#">Open configurator</a></div></section>
423
- <section class="hf-feature-grid"><article class="panel">Category range</article><article class="panel">Featured products</article><article class="panel">Configurator quick start</article></section>`;
424
- case "category":
425
- return `<section class="hf-split"><aside class="panel">Filter sidebar</aside><div class="panel"><h3>Product grid</h3><div class="hf-card-grid"><article class="product-card">Card A</article><article class="product-card">Card B</article><article class="product-card">Card C</article></div></div></section>`;
426
- case "product-detail":
427
- return `<section class="hf-split"><div class="panel"><h3>Gallery and spec area</h3><table class="spec-table"><tr><th>Spec</th><th>Value</th></tr><tr><td>Voltage</td><td>High pressure</td></tr></table></div><aside class="panel"><h3>Price and quantity</h3><div class="qty-stepper"><button>-</button><span>1</span><button>+</button></div><a class="btn btn-primary" href="#">Add to cart</a></aside></section>`;
428
- case "search":
429
- return `<section class="panel"><h3>Search and result state</h3><div class="search-toolbar"><input aria-label="Search query" placeholder="Search product"><button class="btn btn-primary">Search</button></div><div class="hf-list"><article class="result-row">Result item</article><article class="result-row no-result">No-result state</article></div></section>`;
430
- case "cart":
431
- return `<section class="hf-split"><div class="panel"><h3>Cart table</h3><table class="cart-table"><tr><th>Item</th><th>Qty</th><th>Total</th></tr><tr><td>Product</td><td><div class="qty-stepper"><button>-</button><span>2</span><button>+</button></div></td><td>100000</td></tr></table></div><aside class="panel"><h3>Summary</h3><p>Subtotal, tax, total</p><a class="btn btn-primary" href="#">Checkout</a></aside></section>`;
432
- case "checkout":
433
- return `<section class="panel"><ol class="checkout-stepper"><li>Information</li><li>Confirmation</li><li>Complete</li></ol><div class="hf-split"><div class="panel"><h3>Checkout form</h3><div class="field-row">Recipient / Address / Phone</div></div><aside class="panel"><h3>Summary card</h3><a class="btn btn-primary" href="#">Place order</a></aside></div></section>`;
434
- case "order-history":
435
- return `<section class="panel"><h3>Order history list</h3><div class="hf-list"><article class="result-row">Order #001 / paid</article><article class="result-row">Order #002 / shipping</article></div></section>`;
436
- case "order-detail":
437
- return `<section class="hf-split"><div class="panel"><h3>Order timeline</h3><ul class="timeline"><li>Placed</li><li>Paid</li><li>Shipping</li></ul></div><aside class="panel"><h3>Detail summary</h3><p>Delivery and payment snapshot</p></aside></section>`;
438
- case "account-info":
439
- return `<section class="hf-split"><aside class="panel">Account sidebar</aside><div class="panel"><h3>View or edit form</h3><div class="field-row">Company / Contact / Email / Phone</div><a class="btn btn-primary" href="#">Save profile</a></div></section>`;
440
- case "mode-b-configurator":
441
- return `<section class="panel"><h3>Configurator wizard</h3><ol class="checkout-stepper"><li>Project</li><li>Assembly</li><li>Construction</li><li>Review</li></ol><div class="hf-split"><div class="panel"><h4>Preview panel</h4><p>Derived material preview</p></div><div class="panel"><h4>BOM table</h4><table class="cart-table"><tr><th>Material</th><th>Qty</th><th>Action</th></tr><tr><td>Cable</td><td>4</td><td><button>Exclude</button></td></tr></table><div class="button-row"><a class="btn btn-secondary" href="#">Recalculate</a><a class="btn btn-primary" href="#">Add BOM to cart</a></div></div></div></section>`;
442
- default:
443
- return `<section class="panel"><h3>Screen contract section</h3><p>Render screen-specific layout from brief + component contract.</p></section>`;
444
- }
119
+ function buildPrototypeManifest(screens) {
120
+ return {
121
+ schema: "sdtk.design.prototype-manifest.v1",
122
+ screens: screens.map((screen) => ({
123
+ screenId: screen.screenId,
124
+ title: screen.title || screen.screenId,
125
+ role: normalizeScreenRole(screen),
126
+ briefPath: screenBriefRelativePath(screen.screenId),
127
+ outPath: screenOutRelativePath(screen.screenId),
128
+ })),
129
+ };
445
130
  }
446
131
 
447
- function highFidelityMultiScreenPrototypeContent({ style, statePayload, contractBundle }) {
448
- const styleName = resolveStyleName(style);
449
- const tokens = contractBundle.tokens || {};
450
- const screens = statePayload.screenModel.screens;
451
- const profileSelection = statePayload.profileSelection || null;
452
- const mappedRefs = statePayload.referenceMap && typeof statePayload.referenceMap.mappedCount === "number" ? statePayload.referenceMap.mappedCount : 0;
453
- const sections = screens
132
+ function launcherHtml(manifest) {
133
+ const links = manifest.screens
454
134
  .map((screen) => {
455
- const screenRole = normalizeScreenRole(screen);
456
- const contract = contractBundle.screenContracts.get(screen.screenId);
457
- const sidecar = contract && contract.sidecar ? contract.sidecar : null;
458
- const stateChips = renderStateChips(sidecar && Array.isArray(sidecar.required_states) ? sidecar.required_states : screen.requiredStates);
459
- return `
460
- <section class="prototype-section screen-${escapeHtml(screen.screenId)}" id="screen-${escapeHtml(screen.screenId)}" aria-labelledby="title-${escapeHtml(screen.screenId)}">
461
- <div class="section-inner">
462
- <div class="section-header">
463
- <p class="eyebrow">${escapeHtml(screenRole.replace(/-/g, " "))}</p>
464
- <h2 id="title-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</h2>
465
- ${screen.route ? `<p class="support">Route: <code>${escapeHtml(screen.route)}</code></p>` : ""}
466
- <p class="support">Source brief: <code>${escapeHtml(path.basename(contract.briefPath))}</code></p>
467
- </div>
468
- ${stateChips}
469
- ${renderHighFidelityScreen(screenRole, screen.title)}
470
- </div>
471
- </section>`;
135
+ const href = toPosix(path.relative("docs/design/prototype", screen.outPath));
136
+ return ` <article class="screen-card">
137
+ <p class="role">${escapeHtml(screen.role)}</p>
138
+ <h2>${escapeHtml(screen.title)}</h2>
139
+ <p><code>${escapeHtml(screen.screenId)}</code></p>
140
+ <a href="${escapeHtml(href)}">Open generated screen</a>
141
+ </article>`;
472
142
  })
473
- .join("");
143
+ .join("\n");
474
144
 
475
145
  return `<!doctype html>
476
146
  <html lang="en">
477
147
  <head>
478
148
  <meta charset="utf-8">
479
149
  <meta name="viewport" content="width=device-width, initial-scale=1">
480
- <title>SDTK-DESIGN High-Fidelity Prototype</title>
150
+ <title>SDTK-DESIGN Prototype Launcher</title>
481
151
  <style>
482
- :root { --bg:${tokenOrDefault(tokens, ["color", "surfaceAlt"], "#F5F7FB")}; --surface:${tokenOrDefault(tokens, ["color", "surface"], "#FFFFFF")}; --text:${tokenOrDefault(tokens, ["color", "textPrimary"], "#111827")}; --muted:${tokenOrDefault(tokens, ["color", "textMuted"], "#64748B")}; --primary:${tokenOrDefault(tokens, ["color", "accentPrimary"], "#0F766E")}; --accent:${tokenOrDefault(tokens, ["color", "accentSecondary"], "#2563EB")}; --border:${tokenOrDefault(tokens, ["color", "border"], "#D8E0EA")}; --shadow:0 18px 40px rgba(15,23,42,0.10); }
483
- * { box-sizing:border-box; } body { margin:0; background:var(--bg); color:var(--text); font:15px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
484
- .shell { width:min(1240px, calc(100vw - 24px)); margin:0 auto; padding:24px 0 48px; }
485
- .topbar { position:sticky; top:0; z-index:4; background:color-mix(in srgb, var(--bg) 92%, var(--surface)); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); padding:14px; margin-bottom:18px; }
486
- .topline { display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
487
- .meta { color:var(--muted); font-size:12px; font-weight:700; }
488
- .nav { display:flex; gap:8px; overflow:auto; margin-top:10px; padding-bottom:4px; } .nav a { text-decoration:none; border:1px solid var(--border); border-radius:999px; padding:6px 10px; white-space:nowrap; color:var(--text); background:var(--surface); font-size:12px; font-weight:700; }
489
- .prototype-section { background:var(--surface); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); margin:14px 0; } .section-inner { padding:20px; }
490
- .eyebrow { margin:0 0 6px; color:var(--primary); font-size:12px; font-weight:800; text-transform:uppercase; } h2,h3,h4,p { margin-top:0; } h2 { margin-bottom:8px; font-size:28px; } .support { color:var(--muted); margin-bottom:10px; }
491
- .chip-row { display:flex; gap:8px; flex-wrap:wrap; margin:10px 0 12px; } .chip { border:1px solid var(--border); border-radius:999px; padding:4px 10px; font-size:12px; background:color-mix(in srgb, var(--surface) 92%, var(--bg)); } .chip-state { color:var(--primary); border-color:color-mix(in srgb, var(--primary) 35%, var(--border)); }
492
- .panel { border:1px solid var(--border); border-radius:8px; padding:14px; background:color-mix(in srgb, var(--surface) 95%, var(--bg)); }
493
- .hf-split { display:grid; grid-template-columns:minmax(220px,0.35fr) minmax(0,1fr); gap:12px; }
494
- .hf-feature-grid, .hf-card-grid { display:grid; gap:12px; grid-template-columns:repeat(3, minmax(0, 1fr)); }
495
- .product-card, .result-row { border:1px solid var(--border); border-radius:8px; padding:10px; background:var(--surface); }
496
- .search-toolbar { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:10px; } input { min-height:40px; border:1px solid var(--border); border-radius:8px; padding:0 12px; min-width:220px; }
497
- .button-row { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; } .btn { display:inline-flex; min-height:40px; align-items:center; justify-content:center; border-radius:8px; padding:0 14px; text-decoration:none; font-weight:800; border:1px solid transparent; } .btn-primary { background:var(--primary); color:#fff; } .btn-secondary { border-color:var(--border); color:var(--text); background:transparent; }
498
- .spec-table, .cart-table { width:100%; border-collapse:collapse; font-size:13px; } .spec-table th, .spec-table td, .cart-table th, .cart-table td { border-bottom:1px solid var(--border); text-align:left; padding:8px 6px; }
499
- .qty-stepper { display:inline-flex; align-items:center; gap:8px; border:1px solid var(--border); border-radius:8px; padding:4px 8px; }
500
- .checkout-stepper { display:flex; gap:10px; list-style:none; padding:0; margin:0 0 12px; } .checkout-stepper li { border:1px solid var(--border); border-radius:999px; padding:4px 10px; font-size:12px; font-weight:700; }
501
- .timeline { margin:0; padding-left:16px; } .field-row { border:1px dashed var(--border); border-radius:8px; padding:10px; color:var(--muted); }
502
- @media (max-width: 920px) { .hf-split,.hf-feature-grid,.hf-card-grid { grid-template-columns:1fr; } h2 { font-size:24px; } }
152
+ :root { --bg:#f7f8fa; --surface:#ffffff; --text:#111827; --muted:#64748b; --border:#d8e0ea; --accent:#0f766e; }
153
+ * { box-sizing:border-box; }
154
+ body { margin:0; background:var(--bg); color:var(--text); font:15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
155
+ main { width:min(1120px, calc(100vw - 32px)); margin:0 auto; padding:32px 0 56px; }
156
+ header { margin-bottom:24px; }
157
+ h1 { margin:0 0 8px; font-size:32px; line-height:1.15; }
158
+ .summary { margin:0; color:var(--muted); max-width:72ch; }
159
+ .grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:14px; }
160
+ .screen-card { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:16px; }
161
+ .role { color:var(--accent); font-size:12px; font-weight:800; margin:0 0 6px; text-transform:uppercase; }
162
+ h2 { margin:0 0 8px; font-size:18px; }
163
+ code { color:var(--muted); }
164
+ a { display:inline-flex; min-height:40px; align-items:center; margin-top:10px; color:var(--accent); font-weight:750; }
503
165
  </style>
504
166
  </head>
505
167
  <body>
506
- <main class="shell style-${styleName}" data-style-preset="${styleName}">
507
- <header class="topbar">
508
- <div class="topline">
509
- <strong>SDTK-DESIGN high-fidelity prototype</strong>
510
- <span class="meta">screens=${screens.length} | profile=${escapeHtml(profileSelection || "none")} | mapped references=${mappedRefs}</span>
511
- </div>
512
- <nav class="nav" aria-label="Screen navigation">
513
- ${screens.map((screen) => `<a href="#screen-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</a>`).join("")}
514
- </nav>
168
+ <main>
169
+ <header>
170
+ <h1>Prototype generation manifest ready</h1>
171
+ <p class="summary">This launcher lists the screen HTML files owned by the running agent via the design-prototype skill. The CLI emitted the manifest and did not generate screen HTML.</p>
515
172
  </header>
516
- ${sections}
173
+ <section class="grid" aria-label="Prototype screens">
174
+ ${links}
175
+ </section>
517
176
  </main>
518
177
  </body>
519
178
  </html>
520
179
  `;
521
180
  }
522
181
 
523
- function multiScreenPrototypeContent({ style, statePayload }) {
524
- const styleName = resolveStyleName(style);
525
- const tokens = THEME_TOKENS[styleName];
526
- const screens = statePayload.screenModel.screens;
527
- const profileSelection = statePayload.profileSelection || null;
528
- const profilePrimitives = statePayload.profilePrimitives || null;
529
- const mappedRefs = statePayload.referenceMap && typeof statePayload.referenceMap.mappedCount === "number" ? statePayload.referenceMap.mappedCount : 0;
530
-
531
- const sections = screens
532
- .map((screen) => {
533
- const screenRole = normalizeScreenRole(screen);
534
- const primitiveHints =
535
- profilePrimitives &&
536
- profilePrimitives.screenRoleHints &&
537
- Array.isArray(profilePrimitives.screenRoleHints[screenRole])
538
- ? profilePrimitives.screenRoleHints[screenRole]
539
- : [];
540
- const majorSections = Array.isArray(screen.majorSections) && screen.majorSections.length > 0 ? screen.majorSections : ["Primary content", "Supporting panel"];
541
- return `
542
- <section class="prototype-section screen-${escapeHtml(screen.screenId)}" id="screen-${escapeHtml(screen.screenId)}" aria-labelledby="title-${escapeHtml(screen.screenId)}">
543
- <div class="section-inner">
544
- <div class="section-header">
545
- <p class="eyebrow">${escapeHtml(screenRole.replace(/-/g, " "))}</p>
546
- <h2 id="title-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</h2>
547
- ${screen.route ? `<p class="support">Route: <code>${escapeHtml(screen.route)}</code></p>` : ""}
548
- ${screen.purpose ? `<p class="support">${escapeHtml(screen.purpose)}</p>` : ""}
549
- </div>
550
- ${renderPrimitiveChips(primitiveHints)}
551
- ${renderStateChips(screen.requiredStates)}
552
- <div class="screen-grid">
553
- <article class="panel">
554
- <h3>Main layout</h3>
555
- <ul>${majorSections.slice(0, 5).map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
556
- </article>
557
- <article class="panel">
558
- <h3>Primary action</h3>
559
- <p>${escapeHtml(screen.primaryAction || "Primary CTA from explicit input")}</p>
560
- <div class="button-row"><a class="btn btn-primary" href="#screen-${escapeHtml(screen.screenId)}">Trigger CTA</a></div>
561
- </article>
562
- </div>
563
- </div>
564
- </section>`;
565
- })
566
- .join("");
567
-
568
- return `<!doctype html>
569
- <html lang="en">
570
- <head>
571
- <meta charset="utf-8">
572
- <meta name="viewport" content="width=device-width, initial-scale=1">
573
- <title>SDTK-DESIGN Multi-Screen Prototype</title>
574
- <style>
575
- :root { --bg:${tokens.bg}; --surface:${tokens.surface}; --text:${tokens.text}; --muted:${tokens.muted}; --primary:${tokens.primary}; --accent:${tokens.accent}; --border:${tokens.border}; --shadow:${tokens.shadow}; }
576
- * { box-sizing:border-box; }
577
- body { margin:0; background:var(--bg); color:var(--text); font:15px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
578
- .shell { width:min(1200px, calc(100vw - 24px)); margin:0 auto; padding:24px 0 44px; }
579
- .topbar { position:sticky; top:0; z-index:4; background:color-mix(in srgb, var(--bg) 92%, var(--surface)); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); padding:14px; margin-bottom:18px; }
580
- .topline { display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
581
- .meta { color:var(--muted); font-size:12px; font-weight:700; }
582
- .nav { display:flex; gap:8px; overflow:auto; margin-top:10px; padding-bottom:4px; }
583
- .nav a { text-decoration:none; border:1px solid var(--border); border-radius:999px; padding:6px 10px; white-space:nowrap; color:var(--text); background:var(--surface); font-size:12px; font-weight:700; }
584
- .prototype-section { background:var(--surface); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); margin:14px 0; }
585
- .section-inner { padding:20px; }
586
- .eyebrow { margin:0 0 6px; color:var(--primary); font-size:12px; font-weight:800; text-transform:uppercase; }
587
- h2,h3,p { margin-top:0; }
588
- h2 { margin-bottom:8px; font-size:28px; }
589
- .support { color:var(--muted); margin-bottom:10px; }
590
- .chip-row { display:flex; gap:8px; flex-wrap:wrap; margin:10px 0 12px; }
591
- .chip { border:1px solid var(--border); border-radius:999px; padding:4px 10px; font-size:12px; background:color-mix(in srgb, var(--surface) 92%, var(--bg)); }
592
- .chip-state { color:var(--primary); border-color:color-mix(in srgb, var(--primary) 35%, var(--border)); }
593
- .screen-grid { display:grid; grid-template-columns:1.2fr 1fr; gap:12px; }
594
- .panel { border:1px solid var(--border); border-radius:8px; padding:14px; }
595
- .panel ul { margin:0; padding-left:18px; }
596
- .btn { display:inline-flex; min-height:40px; align-items:center; justify-content:center; border-radius:8px; padding:0 14px; text-decoration:none; font-weight:800; }
597
- .btn-primary { background:var(--primary); color:#fff; }
598
- @media (max-width: 820px) { .screen-grid { grid-template-columns:1fr; } h2 { font-size:24px; } }
599
- </style>
600
- </head>
601
- <body>
602
- <main class="shell style-${styleName}" data-style-preset="${styleName}">
603
- <header class="topbar">
604
- <div class="topline">
605
- <strong>SDTK-DESIGN multi-screen prototype</strong>
606
- <span class="meta">screens=${screens.length} | profile=${escapeHtml(profileSelection || "none")} | mapped references=${mappedRefs}</span>
607
- </div>
608
- <nav class="nav" aria-label="Screen navigation">
609
- ${screens.map((screen) => `<a href="#screen-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</a>`).join("")}
610
- </nav>
611
- </header>
612
- ${sections}
613
- </main>
614
- </body>
615
- </html>
616
- `;
182
+ function managedPrototypeTargets(paths) {
183
+ return [
184
+ { relativePath: "docs/design/prototype/.manifest.json", filePath: paths.prototypeManifestPath },
185
+ { relativePath: "docs/design/prototype/index.html", filePath: paths.prototypeIndexPath },
186
+ ];
617
187
  }
618
188
 
619
- function runDesignPrototype({ projectPath, force = false, style }) {
620
- const explicitStyleName = typeof style === "string" && style.trim() ? resolveStyleName(style) : null;
189
+ function runDesignPrototype({ projectPath, force = false }) {
621
190
  const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
622
191
  if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
623
192
  throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}. No project files were changed.`);
@@ -625,152 +194,70 @@ function runDesignPrototype({ projectPath, force = false, style }) {
625
194
 
626
195
  const paths = describeDesignPaths(resolvedProjectPath);
627
196
  const inputContractState = readInputContractState(paths);
628
- const canUseMultiScreen =
629
- inputContractState &&
630
- inputContractState.mode === "from-spec" &&
631
- inputContractState.analysisStatus === "INPUT_CONTRACT_READY" &&
632
- inputContractState.screenModel &&
633
- Array.isArray(inputContractState.screenModel.screens) &&
634
- inputContractState.screenModel.screens.length > 0;
635
- const missing = canUseMultiScreen ? [] : requiredArtifactTargets(paths).filter((target) => !fs.existsSync(target.filePath));
197
+ if (!assertReadyInputContract(inputContractState)) {
198
+ throw new ValidationError(
199
+ "sdtk-design prototype requires .sdtk/design/START_INPUT_STATE.json with analysisStatus=INPUT_CONTRACT_READY. Run sdtk-design start --from-spec with explicit design inputs first. No project files were changed."
200
+ );
201
+ }
202
+
203
+ const screens = inputContractState.screenModel.screens;
204
+ const missing = requiredPrototypeInputs(paths, screens).filter((target) => !fs.existsSync(target.filePath));
636
205
  if (missing.length > 0) {
637
206
  const list = missing.map((target) => target.relativePath).join(", ");
638
- throw new ValidationError(`Missing required design artifacts: ${list}. Run sdtk-design start --idea "<idea>" first. No project files were changed.`);
207
+ throw new ValidationError(`Missing required prototype generation artifacts: ${list}. No project files were changed.`);
639
208
  }
640
- const prototypeMode = resolvePrototypeMode(canUseMultiScreen);
641
- const existingManagedPrototype = managedPrototypeTargets(paths, inputContractState, prototypeMode).filter((target) =>
642
- fs.existsSync(target.filePath)
643
- );
209
+
210
+ const existingManagedPrototype = managedPrototypeTargets(paths).filter((target) => fs.existsSync(target.filePath));
644
211
  if (existingManagedPrototype.length > 0 && !force) {
645
212
  throw new ValidationError(
646
- `docs/design/prototype/index.html already exists. Managed prototype output already exists: ${existingManagedPrototype
213
+ `Managed prototype launcher output already exists: ${existingManagedPrototype
647
214
  .map((target) => target.relativePath)
648
- .join(", ")}. Re-run with --force to replace managed prototype outputs.`
215
+ .join(", ")}. Re-run with --force to replace manifest and launcher. Screen HTML files are never overwritten by this command.`
649
216
  );
650
217
  }
651
218
 
652
- const designSystemContent = fs.existsSync(paths.designSystemPath) ? fs.readFileSync(paths.designSystemPath, "utf-8") : "";
653
- const styleName = explicitStyleName || (designSystemContent ? inferStyleFromDesignSystem(designSystemContent) : DEFAULT_STYLE);
654
- const wireframeContents = canUseMultiScreen
655
- ? []
656
- : REQUIRED_ARTIFACTS
657
- .filter(([relativePath]) => relativePath.startsWith("docs/design/wireframes/"))
658
- .map(([, key]) => {
659
- const filePath =
660
- key === "landingWireframePath"
661
- ? wireframePath(paths, "LANDING.md")
662
- : key === "onboardingWireframePath"
663
- ? wireframePath(paths, "ONBOARDING.md")
664
- : wireframePath(paths, "DASHBOARD.md");
665
- return fs.readFileSync(filePath, "utf-8");
666
- });
667
- let renderResult = null;
668
- let content = null;
669
- let bundle = null;
670
- if (canUseMultiScreen) {
671
- bundle = loadHighFidelityContracts(paths, inputContractState);
672
- if (prototypeMode === "multi" && bundle.ready) {
673
- renderResult = renderMultiPagePrototype({
674
- paths,
675
- statePayload: inputContractState,
676
- contractBundle: bundle,
677
- styleName,
678
- });
679
- } else if (bundle.ready) {
680
- content = highFidelityMultiScreenPrototypeContent({
681
- style: styleName,
682
- statePayload: inputContractState,
683
- contractBundle: bundle,
684
- });
685
- } else {
686
- content = multiScreenPrototypeContent({
687
- style: styleName,
688
- statePayload: inputContractState,
689
- });
690
- }
691
- } else {
692
- content = prototypeContent({
693
- style: styleName,
694
- briefContent: fs.readFileSync(paths.designBriefPath, "utf-8"),
695
- screenMapContent: fs.readFileSync(paths.screenMapPath, "utf-8"),
696
- designSystemContent,
697
- wireframeContents,
698
- });
699
- }
700
-
701
- if (!renderResult) {
702
- fs.mkdirSync(path.dirname(paths.prototypeIndexPath), { recursive: true });
703
- fs.writeFileSync(paths.prototypeIndexPath, content, "utf-8");
704
- }
219
+ const manifest = buildPrototypeManifest(screens);
220
+ fs.mkdirSync(paths.prototypePath, { recursive: true });
221
+ fs.mkdirSync(paths.prototypeScreensPath, { recursive: true });
222
+ fs.writeFileSync(paths.prototypeManifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
223
+ fs.writeFileSync(paths.prototypeIndexPath, launcherHtml(manifest), "utf-8");
705
224
 
706
225
  return {
707
226
  projectPath: resolvedProjectPath,
227
+ relativeManifestPath: "docs/design/prototype/.manifest.json",
708
228
  relativePrototypePath: "docs/design/prototype/index.html",
229
+ screenCount: manifest.screens.length,
709
230
  forced: Boolean(force),
710
- style: styleName,
711
- mode: renderResult
712
- ? renderResult.mode
713
- : canUseMultiScreen
714
- ? bundle && bundle.ready
715
- ? "high-fidelity-single-file"
716
- : "multi-screen"
717
- : "simple",
718
- screenCount: canUseMultiScreen ? inputContractState.screenModel.screens.length : 3,
719
- warnings: canUseMultiScreen && bundle ? bundle.warnings : [],
720
- generatedPages: renderResult ? renderResult.screenPages : [],
721
- densityReport: renderResult ? renderResult.densityReport : null,
722
231
  };
723
232
  }
724
233
 
725
234
  function cmdPrototype(args) {
726
- const { flags } = parseFlags(args || [], PROTOTYPE_FLAG_DEFS);
235
+ const { flags, positionals } = parseFlags(args || [], PROTOTYPE_FLAG_DEFS);
727
236
  if (flags.help) return cmdPrototypeHelp();
237
+ if (positionals.length > 0) {
238
+ throw new ValidationError(
239
+ `Unsupported prototype argument: ${positionals.join(" ")}. Use sdtk-design prototype [--project-path <path>] [--force]. No project files were changed.`
240
+ );
241
+ }
728
242
 
729
243
  const result = runDesignPrototype({
730
244
  projectPath: flags["project-path"],
731
245
  force: Boolean(flags.force),
732
- style: flags.style,
733
246
  });
734
247
 
248
+ console.log(`[design] Wrote ${result.relativeManifestPath}: ${result.projectPath}`);
735
249
  console.log(`[design] Wrote ${result.relativePrototypePath}: ${result.projectPath}`);
736
- console.log(`[design] Style: ${result.style}`);
737
- console.log(`[design] Mode: ${result.mode} (${result.screenCount} screen(s))`);
738
- if (result.densityReport) {
739
- if (result.densityReport.mode === "bytes") {
740
- console.log("[design] Density mode: bytes (deprecated rollback; report shape only, no filler emission)");
741
- console.log(
742
- `[design] Density: ${result.densityReport.pass ? "PASS" : "FAIL"} total=${result.densityReport.totalBytes}/${result.densityReport.minTotalBytes} bytes`
743
- );
744
- for (const page of result.densityReport.pages) {
745
- console.log(`[design] Density page: ${page.screenId} ${page.byteLength}/${page.minBytes} bytes ${page.pass ? "PASS" : "FAIL"}`);
746
- }
747
- } else {
748
- const aggregate = result.densityReport.aggregate || {};
749
- console.log(
750
- `[design] Density: ${result.densityReport.pass ? "PASS" : "FAIL"} mode=semantic sections=${aggregate.sectionCount || 0} components=${aggregate.componentCount || 0} dataSlots=${aggregate.dataSlotCount || 0} states=${aggregate.stateCount || 0}`
751
- );
752
- for (const page of result.densityReport.pages) {
753
- const missing = Array.isArray(page.findings) && page.findings.length > 0 ? ` (${page.findings.join("; ")})` : "";
754
- console.log(
755
- `[design] Density page: ${page.screenId} sections=${page.sectionCount} components=${page.componentCount} dataSlots=${page.dataSlotCount} states=${page.stateCount} ${page.pass ? "PASS" : "FAIL"}${missing}`
756
- );
757
- }
758
- }
759
- }
760
- if (Array.isArray(result.warnings) && result.warnings.length > 0) {
761
- console.log(`[design] Contract fallback: ${result.warnings.join("; ")}`);
762
- }
250
+ console.log(`[design] Mode: manifest-only (${result.screenCount} screen(s))`);
763
251
  console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
764
- console.log("[design] Prototype mode: static HTML/CSS preview only.");
252
+ console.log("[design] Screen HTML generation is owned by the design-prototype skill and running agent.");
765
253
  console.log("[design] No app code outside docs/design/prototype, server, network, .sdtk/atlas, or SDTK-WIKI output was modified.");
766
- console.log("[design] Next: sdtk-design review --artifact docs/design/prototype/index.html");
767
- return result.densityReport && result.densityReport.mode !== "bytes" && !result.densityReport.pass ? 1 : 0;
254
+ console.log("[design] Next: run the design-prototype skill, then sdtk-design review --artifact docs/design/prototype/index.html");
255
+ return 0;
768
256
  }
769
257
 
770
258
  module.exports = {
259
+ buildPrototypeManifest,
771
260
  cmdPrototype,
772
261
  cmdPrototypeHelp,
773
- inferStyleFromDesignSystem,
774
- prototypeContent,
775
262
  runDesignPrototype,
776
263
  };