sdtk-design-kit 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdtk-design-kit",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Local-first MVP design planner and reviewer for SDTK workspaces.",
5
5
  "bin": {
6
6
  "sdtk-design": "bin/sdtk-design.js"
@@ -8,6 +8,7 @@ Usage:
8
8
  sdtk-design --version
9
9
  sdtk-design init
10
10
  sdtk-design start --idea "<idea>" --style premium-dashboard
11
+ sdtk-design start --from-spec . --reference-dir ./docs/ui/claude-design-export --profile b2b-commerce
11
12
  sdtk-design brief --idea "<idea>"
12
13
  sdtk-design screens
13
14
  sdtk-design wireframe --screen landing
@@ -19,6 +20,7 @@ Usage:
19
20
 
20
21
  Examples:
21
22
  sdtk-design start --idea "I want to build a lightweight CRM for solo consultants to track leads." --style premium-dashboard
23
+ sdtk-design start --from-spec . --reference-dir ./docs/ui/claude-design-export --profile b2b-commerce
22
24
  sdtk-design prototype --style premium-dashboard
23
25
  sdtk-design review --artifact docs/design/prototype/index.html
24
26
  sdtk-design handoff
@@ -31,6 +33,7 @@ Visual style presets:
31
33
  Foundation Beta purpose:
32
34
  SDTK-DESIGN is a local-first MVP design planner and reviewer for solo founders.
33
35
  It turns a rough MVP idea into reviewable design artifacts and a deterministic SDTK-CODE handoff.
36
+ For SPEC-driven flows, it consumes explicit design artifacts via start --from-spec; it does not parse raw requirement prose.
34
37
 
35
38
  Required human-facing output:
36
39
  docs/design/DESIGN_BRIEF.md
@@ -67,6 +67,17 @@ const THEME_TOKENS = {
67
67
  },
68
68
  };
69
69
 
70
+ function readInputContractState(paths) {
71
+ if (!fs.existsSync(paths.designStartInputStatePath) || !fs.statSync(paths.designStartInputStatePath).isFile()) {
72
+ return null;
73
+ }
74
+ try {
75
+ return JSON.parse(fs.readFileSync(paths.designStartInputStatePath, "utf-8"));
76
+ } catch (_err) {
77
+ return null;
78
+ }
79
+ }
80
+
70
81
  function cmdPrototypeHelp() {
71
82
  console.log(`SDTK-DESIGN Prototype
72
83
 
@@ -87,6 +98,7 @@ Reads:
87
98
  docs/design/wireframes/LANDING.md
88
99
  docs/design/wireframes/ONBOARDING.md
89
100
  docs/design/wireframes/DASHBOARD.md
101
+ .sdtk/design/START_INPUT_STATE.json (optional; enables explicit multi-screen rendering when ready)
90
102
 
91
103
  Creates:
92
104
  docs/design/prototype/index.html
@@ -275,6 +287,127 @@ function prototypeContent({ style, briefContent, screenMapContent, designSystemC
275
287
  `;
276
288
  }
277
289
 
290
+ function normalizeScreenRole(screen) {
291
+ const token = `${screen.screenId || ""} ${screen.title || ""}`.toLowerCase();
292
+ if (token.includes("home")) return "home";
293
+ if (token.includes("category")) return "category";
294
+ if (token.includes("product-detail") || token.includes("product detail") || token.includes("pdp")) return "product-detail";
295
+ if (token.includes("search")) return "search";
296
+ if (token.includes("cart")) return "cart";
297
+ if (token.includes("checkout")) return "checkout";
298
+ if (token.includes("order-history") || token.includes("order history")) return "order-history";
299
+ if (token.includes("order-detail") || token.includes("order detail")) return "order-detail";
300
+ if (token.includes("account")) return "account-info";
301
+ if (token.includes("configurator") || token.includes("mode-b")) return "mode-b-configurator";
302
+ return screen.screenId || "screen";
303
+ }
304
+
305
+ function renderPrimitiveChips(primitiveNames) {
306
+ if (!Array.isArray(primitiveNames) || primitiveNames.length === 0) return "";
307
+ return `<div class="chip-row">${primitiveNames.map((item) => `<span class="chip">${escapeHtml(item)}</span>`).join("")}</div>`;
308
+ }
309
+
310
+ function renderStateChips(requiredStates) {
311
+ if (!Array.isArray(requiredStates) || requiredStates.length === 0) return "";
312
+ return `<div class="chip-row">${requiredStates.slice(0, 3).map((item) => `<span class="chip chip-state">${escapeHtml(item)}</span>`).join("")}</div>`;
313
+ }
314
+
315
+ function multiScreenPrototypeContent({ style, statePayload }) {
316
+ const styleName = resolveStyleName(style);
317
+ const tokens = THEME_TOKENS[styleName];
318
+ const screens = statePayload.screenModel.screens;
319
+ const profileSelection = statePayload.profileSelection || null;
320
+ const profilePrimitives = statePayload.profilePrimitives || null;
321
+ const mappedRefs = statePayload.referenceMap && typeof statePayload.referenceMap.mappedCount === "number" ? statePayload.referenceMap.mappedCount : 0;
322
+
323
+ const sections = screens
324
+ .map((screen) => {
325
+ const screenRole = normalizeScreenRole(screen);
326
+ const primitiveHints =
327
+ profilePrimitives &&
328
+ profilePrimitives.screenRoleHints &&
329
+ Array.isArray(profilePrimitives.screenRoleHints[screenRole])
330
+ ? profilePrimitives.screenRoleHints[screenRole]
331
+ : [];
332
+ const majorSections = Array.isArray(screen.majorSections) && screen.majorSections.length > 0 ? screen.majorSections : ["Primary content", "Supporting panel"];
333
+ return `
334
+ <section class="prototype-section screen-${escapeHtml(screen.screenId)}" id="screen-${escapeHtml(screen.screenId)}" aria-labelledby="title-${escapeHtml(screen.screenId)}">
335
+ <div class="section-inner">
336
+ <div class="section-header">
337
+ <p class="eyebrow">${escapeHtml(screenRole.replace(/-/g, " "))}</p>
338
+ <h2 id="title-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</h2>
339
+ ${screen.route ? `<p class="support">Route: <code>${escapeHtml(screen.route)}</code></p>` : ""}
340
+ ${screen.purpose ? `<p class="support">${escapeHtml(screen.purpose)}</p>` : ""}
341
+ </div>
342
+ ${renderPrimitiveChips(primitiveHints)}
343
+ ${renderStateChips(screen.requiredStates)}
344
+ <div class="screen-grid">
345
+ <article class="panel">
346
+ <h3>Main layout</h3>
347
+ <ul>${majorSections.slice(0, 5).map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
348
+ </article>
349
+ <article class="panel">
350
+ <h3>Primary action</h3>
351
+ <p>${escapeHtml(screen.primaryAction || "Primary CTA from explicit input")}</p>
352
+ <div class="button-row"><a class="btn btn-primary" href="#screen-${escapeHtml(screen.screenId)}">Trigger CTA</a></div>
353
+ </article>
354
+ </div>
355
+ </div>
356
+ </section>`;
357
+ })
358
+ .join("");
359
+
360
+ return `<!doctype html>
361
+ <html lang="en">
362
+ <head>
363
+ <meta charset="utf-8">
364
+ <meta name="viewport" content="width=device-width, initial-scale=1">
365
+ <title>SDTK-DESIGN Multi-Screen Prototype</title>
366
+ <style>
367
+ :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}; }
368
+ * { box-sizing:border-box; }
369
+ 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; }
370
+ .shell { width:min(1200px, calc(100vw - 24px)); margin:0 auto; padding:24px 0 44px; }
371
+ .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; }
372
+ .topline { display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
373
+ .meta { color:var(--muted); font-size:12px; font-weight:700; }
374
+ .nav { display:flex; gap:8px; overflow:auto; margin-top:10px; padding-bottom:4px; }
375
+ .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; }
376
+ .prototype-section { background:var(--surface); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); margin:14px 0; }
377
+ .section-inner { padding:20px; }
378
+ .eyebrow { margin:0 0 6px; color:var(--primary); font-size:12px; font-weight:800; text-transform:uppercase; }
379
+ h2,h3,p { margin-top:0; }
380
+ h2 { margin-bottom:8px; font-size:28px; }
381
+ .support { color:var(--muted); margin-bottom:10px; }
382
+ .chip-row { display:flex; gap:8px; flex-wrap:wrap; margin:10px 0 12px; }
383
+ .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)); }
384
+ .chip-state { color:var(--primary); border-color:color-mix(in srgb, var(--primary) 35%, var(--border)); }
385
+ .screen-grid { display:grid; grid-template-columns:1.2fr 1fr; gap:12px; }
386
+ .panel { border:1px solid var(--border); border-radius:8px; padding:14px; }
387
+ .panel ul { margin:0; padding-left:18px; }
388
+ .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; }
389
+ .btn-primary { background:var(--primary); color:#fff; }
390
+ @media (max-width: 820px) { .screen-grid { grid-template-columns:1fr; } h2 { font-size:24px; } }
391
+ </style>
392
+ </head>
393
+ <body>
394
+ <main class="shell style-${styleName}" data-style-preset="${styleName}">
395
+ <header class="topbar">
396
+ <div class="topline">
397
+ <strong>SDTK-DESIGN multi-screen prototype</strong>
398
+ <span class="meta">screens=${screens.length} | profile=${escapeHtml(profileSelection || "none")} | mapped references=${mappedRefs}</span>
399
+ </div>
400
+ <nav class="nav" aria-label="Screen navigation">
401
+ ${screens.map((screen) => `<a href="#screen-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</a>`).join("")}
402
+ </nav>
403
+ </header>
404
+ ${sections}
405
+ </main>
406
+ </body>
407
+ </html>
408
+ `;
409
+ }
410
+
278
411
  function runDesignPrototype({ projectPath, force = false, style }) {
279
412
  const explicitStyleName = typeof style === "string" && style.trim() ? resolveStyleName(style) : null;
280
413
  const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
@@ -283,7 +416,15 @@ function runDesignPrototype({ projectPath, force = false, style }) {
283
416
  }
284
417
 
285
418
  const paths = describeDesignPaths(resolvedProjectPath);
286
- const missing = requiredArtifactTargets(paths).filter((target) => !fs.existsSync(target.filePath));
419
+ const inputContractState = readInputContractState(paths);
420
+ const canUseMultiScreen =
421
+ inputContractState &&
422
+ inputContractState.mode === "from-spec" &&
423
+ inputContractState.analysisStatus === "INPUT_CONTRACT_READY" &&
424
+ inputContractState.screenModel &&
425
+ Array.isArray(inputContractState.screenModel.screens) &&
426
+ inputContractState.screenModel.screens.length > 0;
427
+ const missing = canUseMultiScreen ? [] : requiredArtifactTargets(paths).filter((target) => !fs.existsSync(target.filePath));
287
428
  if (missing.length > 0) {
288
429
  const list = missing.map((target) => target.relativePath).join(", ");
289
430
  throw new ValidationError(`Missing required design artifacts: ${list}. Run sdtk-design start --idea "<idea>" first. No project files were changed.`);
@@ -292,26 +433,33 @@ function runDesignPrototype({ projectPath, force = false, style }) {
292
433
  throw new ValidationError("docs/design/prototype/index.html already exists. Re-run with --force to replace this managed prototype.");
293
434
  }
294
435
 
295
- const designSystemContent = fs.readFileSync(paths.designSystemPath, "utf-8");
296
- const styleName = explicitStyleName || inferStyleFromDesignSystem(designSystemContent);
297
- const wireframeContents = REQUIRED_ARTIFACTS
298
- .filter(([relativePath]) => relativePath.startsWith("docs/design/wireframes/"))
299
- .map(([, key]) => {
300
- const filePath =
301
- key === "landingWireframePath"
302
- ? wireframePath(paths, "LANDING.md")
303
- : key === "onboardingWireframePath"
304
- ? wireframePath(paths, "ONBOARDING.md")
305
- : wireframePath(paths, "DASHBOARD.md");
306
- return fs.readFileSync(filePath, "utf-8");
307
- });
308
- const content = prototypeContent({
309
- style: styleName,
310
- briefContent: fs.readFileSync(paths.designBriefPath, "utf-8"),
311
- screenMapContent: fs.readFileSync(paths.screenMapPath, "utf-8"),
312
- designSystemContent,
313
- wireframeContents,
314
- });
436
+ const designSystemContent = fs.existsSync(paths.designSystemPath) ? fs.readFileSync(paths.designSystemPath, "utf-8") : "";
437
+ const styleName = explicitStyleName || (designSystemContent ? inferStyleFromDesignSystem(designSystemContent) : DEFAULT_STYLE);
438
+ const wireframeContents = canUseMultiScreen
439
+ ? []
440
+ : REQUIRED_ARTIFACTS
441
+ .filter(([relativePath]) => relativePath.startsWith("docs/design/wireframes/"))
442
+ .map(([, key]) => {
443
+ const filePath =
444
+ key === "landingWireframePath"
445
+ ? wireframePath(paths, "LANDING.md")
446
+ : key === "onboardingWireframePath"
447
+ ? wireframePath(paths, "ONBOARDING.md")
448
+ : wireframePath(paths, "DASHBOARD.md");
449
+ return fs.readFileSync(filePath, "utf-8");
450
+ });
451
+ const content = canUseMultiScreen
452
+ ? multiScreenPrototypeContent({
453
+ style: styleName,
454
+ statePayload: inputContractState,
455
+ })
456
+ : prototypeContent({
457
+ style: styleName,
458
+ briefContent: fs.readFileSync(paths.designBriefPath, "utf-8"),
459
+ screenMapContent: fs.readFileSync(paths.screenMapPath, "utf-8"),
460
+ designSystemContent,
461
+ wireframeContents,
462
+ });
315
463
 
316
464
  fs.mkdirSync(path.dirname(paths.prototypeIndexPath), { recursive: true });
317
465
  fs.writeFileSync(paths.prototypeIndexPath, content, "utf-8");
@@ -321,6 +469,8 @@ function runDesignPrototype({ projectPath, force = false, style }) {
321
469
  relativePrototypePath: "docs/design/prototype/index.html",
322
470
  forced: Boolean(force),
323
471
  style: styleName,
472
+ mode: canUseMultiScreen ? "multi-screen" : "simple",
473
+ screenCount: canUseMultiScreen ? inputContractState.screenModel.screens.length : 3,
324
474
  };
325
475
  }
326
476
 
@@ -336,6 +486,7 @@ function cmdPrototype(args) {
336
486
 
337
487
  console.log(`[design] Wrote ${result.relativePrototypePath}: ${result.projectPath}`);
338
488
  console.log(`[design] Style: ${result.style}`);
489
+ console.log(`[design] Mode: ${result.mode} (${result.screenCount} screen section(s))`);
339
490
  console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
340
491
  console.log("[design] Prototype mode: static HTML/CSS preview only.");
341
492
  console.log("[design] No app code outside docs/design/prototype, server, network, .sdtk/atlas, or SDTK-WIKI output was modified.");
@@ -8,6 +8,8 @@ const { runDesignSystem } = require("./system");
8
8
  const { runDesignWireframe } = require("./wireframe");
9
9
  const { parseFlags } = require("../lib/args");
10
10
  const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths");
11
+ const { availableProfileNames } = require("../lib/design-profiles");
12
+ const { buildInputContractState, writeInputContractState } = require("../lib/design-input-contract");
11
13
  const { ValidationError } = require("../lib/errors");
12
14
  const { DEFAULT_STYLE, availableStyleNames, resolveStyleName } = require("../lib/style-presets");
13
15
 
@@ -17,6 +19,10 @@ const START_FLAG_DEFS = {
17
19
  "project-path": { type: "string" },
18
20
  force: { type: "boolean" },
19
21
  style: { type: "string" },
22
+ "from-spec": { type: "string" },
23
+ "design-brief": { type: "string" },
24
+ "reference-dir": { type: "string" },
25
+ profile: { type: "string" },
20
26
  };
21
27
 
22
28
  const REQUIRED_WIREFRAME_FILES = ["LANDING.md", "ONBOARDING.md", "DASHBOARD.md"];
@@ -26,29 +32,41 @@ function cmdStartHelp() {
26
32
 
27
33
  Usage:
28
34
  sdtk-design start --idea "<rough MVP idea>" [--style <preset>] [--project-path <path>] [--force]
35
+ sdtk-design start --from-spec <projectPath> [--design-brief <file>] [--reference-dir <dir>] [--profile <name>] [--project-path <path>]
29
36
 
30
37
  Example:
31
38
  sdtk-design start --idea "I want to build a lightweight CRM for solo consultants to track leads."
32
39
  sdtk-design start --idea "ClientPulse for consultants" --style premium-dashboard
40
+ sdtk-design start --from-spec . --reference-dir ./docs/ui/claude-design-export --profile b2b-commerce
33
41
 
34
- Style presets:
42
+ Style presets:
35
43
  ${availableStyleNames().join(", ")}
36
44
  Default: ${DEFAULT_STYLE}
37
45
 
46
+ Design profiles (optional in from-spec mode):
47
+ ${availableProfileNames().join(", ")}
48
+
38
49
  Runs:
39
- brief -> screens -> wireframe --screen all -> system
50
+ idea mode:
51
+ brief -> screens -> wireframe --screen all -> system
52
+ from-spec mode:
53
+ explicit SPEC/design artifact intake -> contract state write
40
54
 
41
55
  Creates:
42
- docs/design/DESIGN_BRIEF.md
43
- docs/design/SCREEN_MAP.md
44
- docs/design/wireframes/LANDING.md
45
- docs/design/wireframes/ONBOARDING.md
46
- docs/design/wireframes/DASHBOARD.md
47
- docs/design/DESIGN_SYSTEM.md
56
+ idea mode:
57
+ docs/design/DESIGN_BRIEF.md
58
+ docs/design/SCREEN_MAP.md
59
+ docs/design/wireframes/LANDING.md
60
+ docs/design/wireframes/ONBOARDING.md
61
+ docs/design/wireframes/DASHBOARD.md
62
+ docs/design/DESIGN_SYSTEM.md
63
+ from-spec mode:
64
+ .sdtk/design/START_INPUT_STATE.json
48
65
 
49
66
  Safety:
50
67
  Local files only.
51
68
  Existing managed core design outputs are not overwritten unless --force is explicit.
69
+ --from-spec consumes explicit artifacts; no raw requirement semantic parsing.
52
70
  No review, handoff, URL, browser, screenshot, vision, or DOM work.
53
71
  No .sdtk/atlas creation or mutation.
54
72
  No SDTK-WIKI output mutation.
@@ -104,10 +122,66 @@ function runDesignStart({ idea, projectPath, force = false, style = DEFAULT_STYL
104
122
  };
105
123
  }
106
124
 
125
+ function runDesignStartFromSpec({
126
+ fromSpecPath,
127
+ projectPath,
128
+ designBrief,
129
+ referenceDir,
130
+ profile,
131
+ }) {
132
+ const contractState = buildInputContractState({
133
+ fromSpecPath,
134
+ projectPath,
135
+ designBriefPath: designBrief,
136
+ referenceDir,
137
+ profile,
138
+ });
139
+ const statePath = writeInputContractState(contractState.projectPath, contractState);
140
+ return {
141
+ ...contractState,
142
+ statePath,
143
+ stateRelativePath: ".sdtk/design/START_INPUT_STATE.json",
144
+ };
145
+ }
146
+
107
147
  function cmdStart(args) {
108
148
  const { flags } = parseFlags(args || [], START_FLAG_DEFS);
109
149
  if (flags.help) return cmdStartHelp();
110
150
 
151
+ const contractMode = Boolean(flags["from-spec"] || flags["design-brief"] || flags["reference-dir"] || flags.profile);
152
+ if (contractMode) {
153
+ const contractResult = runDesignStartFromSpec({
154
+ fromSpecPath: flags["from-spec"],
155
+ projectPath: flags["project-path"],
156
+ designBrief: flags["design-brief"],
157
+ referenceDir: flags["reference-dir"],
158
+ profile: flags.profile,
159
+ });
160
+ if (contractResult.blockers.length > 0) {
161
+ throw new ValidationError(
162
+ `SDTK-DESIGN input contract blocked: ${contractResult.blockers.join(", ")}. Review ${contractResult.stateRelativePath}.`
163
+ );
164
+ }
165
+ console.log(`[design] Started SDTK-DESIGN package: ${contractResult.projectPath}`);
166
+ console.log(`[design] Mode: from-spec`);
167
+ console.log(`[design] Wrote ${contractResult.stateRelativePath}`);
168
+ console.log(`[design] Screen model: ${contractResult.screenModel.totalScreens} explicit screen(s), readiness=${contractResult.screenModel.readiness}`);
169
+ if (contractResult.referenceDirectory && contractResult.referenceDirectory.provided) {
170
+ console.log(
171
+ `[design] Reference map: ${contractResult.referenceMap.mappedCount}/${contractResult.referenceMap.totalFiles} mapped (${contractResult.referenceMap.unmappedCount} unmapped)`
172
+ );
173
+ }
174
+ console.log(
175
+ `[design] Inputs: ${Object.entries(contractResult.artifacts)
176
+ .filter(([, artifact]) => artifact.found)
177
+ .map(([name, artifact]) => `${name}=${artifact.relativeToSpecRoot}`)
178
+ .join(", ")}`
179
+ );
180
+ console.log("[design] No raw requirement semantic parsing, .sdtk/atlas, SDTK-WIKI output, or network activity was used.");
181
+ console.log("[design] Next: sdtk-design prototype");
182
+ return 0;
183
+ }
184
+
111
185
  const result = runDesignStart({
112
186
  idea: flags.idea,
113
187
  projectPath: flags["project-path"],
@@ -128,5 +202,6 @@ module.exports = {
128
202
  cmdStart,
129
203
  cmdStartHelp,
130
204
  coreOutputTargets,
205
+ runDesignStartFromSpec,
131
206
  runDesignStart,
132
207
  };
@@ -53,6 +53,17 @@ function hasReview(paths) {
53
53
  return fs.readdirSync(paths.reviewsPath).some((name) => /^DESIGN_REVIEW_\d{8}\.md$/.test(name));
54
54
  }
55
55
 
56
+ function readInputContractState(paths) {
57
+ if (!fs.existsSync(paths.designStartInputStatePath) || !fs.statSync(paths.designStartInputStatePath).isFile()) {
58
+ return null;
59
+ }
60
+ try {
61
+ return JSON.parse(fs.readFileSync(paths.designStartInputStatePath, "utf-8"));
62
+ } catch (_err) {
63
+ return null;
64
+ }
65
+ }
66
+
56
67
  function inspectDesignStatus(projectPath) {
57
68
  const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
58
69
  if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
@@ -65,12 +76,19 @@ function inspectDesignStatus(projectPath) {
65
76
  exists: fs.existsSync(artifact.filePath),
66
77
  }));
67
78
  const reviewExists = hasReview(paths);
79
+ const inputContractState = readInputContractState(paths);
68
80
  const coreMissing = artifacts.filter((artifact) => artifact.phase === "core" && !artifact.exists);
69
81
  const prototypeMissing = artifacts.find((artifact) => artifact.phase === "prototype" && !artifact.exists);
70
82
  const handoffMissing = artifacts.find((artifact) => artifact.phase === "handoff" && !artifact.exists);
71
83
 
72
84
  let nextCommand = "SDTK-CODE can consume docs/design/DESIGN_HANDOFF.md";
73
- if (coreMissing.length > 0) {
85
+ if (inputContractState && inputContractState.mode === "from-spec") {
86
+ if (Array.isArray(inputContractState.blockers) && inputContractState.blockers.length > 0) {
87
+ nextCommand = "Provide missing explicit SPEC/design artifacts and re-run sdtk-design start --from-spec.";
88
+ } else {
89
+ nextCommand = "sdtk-design prototype";
90
+ }
91
+ } else if (coreMissing.length > 0) {
74
92
  nextCommand = 'sdtk-design start --idea "<idea>"';
75
93
  } else if (prototypeMissing) {
76
94
  nextCommand = "sdtk-design prototype";
@@ -83,6 +101,7 @@ function inspectDesignStatus(projectPath) {
83
101
  return {
84
102
  projectPath: resolvedProjectPath,
85
103
  artifacts,
104
+ inputContractState,
86
105
  reviewExists,
87
106
  nextCommand,
88
107
  };
@@ -122,6 +141,23 @@ function cmdStatus(args) {
122
141
  }
123
142
  }
124
143
  console.log("");
144
+ console.log("Input contract:");
145
+ if (!status.inputContractState) {
146
+ console.log(" - not found");
147
+ } else {
148
+ console.log(` - mode: ${status.inputContractState.mode}`);
149
+ console.log(` - status: ${status.inputContractState.analysisStatus}`);
150
+ if (status.inputContractState.screenModel && typeof status.inputContractState.screenModel.totalScreens === "number") {
151
+ console.log(` - explicit screens: ${status.inputContractState.screenModel.totalScreens}`);
152
+ console.log(` - readiness: ${status.inputContractState.screenModel.readiness}`);
153
+ }
154
+ if (status.inputContractState.profileSelection) {
155
+ console.log(` - profile: ${status.inputContractState.profileSelection}`);
156
+ }
157
+ const blockers = Array.isArray(status.inputContractState.blockers) ? status.inputContractState.blockers : [];
158
+ console.log(` - blockers: ${blockers.length > 0 ? blockers.join(", ") : "none"}`);
159
+ }
160
+ console.log("");
125
161
  console.log(`Next recommended command: ${status.nextCommand}`);
126
162
  console.log("No .sdtk/atlas, SDTK-WIKI output, network, or app code was modified.");
127
163
  return 0;
@@ -0,0 +1,423 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { describeDesignPaths, resolveProjectPath } = require("./design-paths");
6
+ const { resolveProfile } = require("./design-profiles");
7
+ const { ValidationError } = require("./errors");
8
+
9
+ function toPosixRelative(basePath, targetPath) {
10
+ return path.relative(basePath, targetPath).split(path.sep).join("/");
11
+ }
12
+
13
+ function findArtifactByPattern(rootPath, relativeDir, pattern) {
14
+ const absoluteDir = path.join(rootPath, relativeDir);
15
+ if (!fs.existsSync(absoluteDir) || !fs.statSync(absoluteDir).isDirectory()) {
16
+ return null;
17
+ }
18
+ const entries = fs
19
+ .readdirSync(absoluteDir, { withFileTypes: true })
20
+ .filter((entry) => entry.isFile() && pattern.test(entry.name))
21
+ .map((entry) => entry.name)
22
+ .sort();
23
+ if (entries.length === 0) {
24
+ return null;
25
+ }
26
+ const fileName = entries[0];
27
+ const absolutePath = path.join(absoluteDir, fileName);
28
+ return {
29
+ relativePath: `${relativeDir.replace(/\\/g, "/")}/${fileName}`,
30
+ absolutePath,
31
+ };
32
+ }
33
+
34
+ function resolveReferenceDirectory(value, projectPath) {
35
+ if (!value) {
36
+ return { provided: false, status: "NOT_PROVIDED", absolutePath: null };
37
+ }
38
+ const absolutePath = path.resolve(projectPath, value);
39
+ if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isDirectory()) {
40
+ throw new ValidationError(`--reference-dir is not a valid directory: ${absolutePath}. No project files were changed.`);
41
+ }
42
+ return {
43
+ provided: true,
44
+ status: "READY_READ_ONLY",
45
+ absolutePath,
46
+ };
47
+ }
48
+
49
+ function listFilesRecursive(rootDirectory) {
50
+ const results = [];
51
+ const stack = [rootDirectory];
52
+ while (stack.length > 0) {
53
+ const current = stack.pop();
54
+ const entries = fs.readdirSync(current, { withFileTypes: true });
55
+ for (const entry of entries) {
56
+ const absolutePath = path.join(current, entry.name);
57
+ if (entry.isDirectory()) {
58
+ stack.push(absolutePath);
59
+ continue;
60
+ }
61
+ if (entry.isFile()) {
62
+ results.push(absolutePath);
63
+ }
64
+ }
65
+ }
66
+ results.sort();
67
+ return results;
68
+ }
69
+
70
+ function tokenSet(value) {
71
+ return new Set(
72
+ String(value || "")
73
+ .toLowerCase()
74
+ .replace(/[^a-z0-9]+/g, " ")
75
+ .split(" ")
76
+ .map((token) => token.trim())
77
+ .filter(Boolean)
78
+ );
79
+ }
80
+
81
+ function readHtmlHints(filePath) {
82
+ try {
83
+ const raw = fs.readFileSync(filePath, "utf-8");
84
+ const titleMatch = raw.match(/<title[^>]*>(.*?)<\/title>/i);
85
+ const headingMatch = raw.match(/<h[1-2][^>]*>(.*?)<\/h[1-2]>/i);
86
+ return {
87
+ title: titleMatch ? titleMatch[1].replace(/<[^>]+>/g, " ").trim() : "",
88
+ heading: headingMatch ? headingMatch[1].replace(/<[^>]+>/g, " ").trim() : "",
89
+ };
90
+ } catch (_err) {
91
+ return { title: "", heading: "" };
92
+ }
93
+ }
94
+
95
+ function confidenceFromScore(score) {
96
+ if (score >= 4) return "high";
97
+ if (score >= 2) return "medium";
98
+ if (score >= 1) return "low";
99
+ return "low";
100
+ }
101
+
102
+ function mapReferenceFiles({ referenceDir, screens }) {
103
+ const files = listFilesRecursive(referenceDir);
104
+ const entries = [];
105
+ const screenTargets = Array.isArray(screens) ? screens : [];
106
+ const supportedExtensions = new Set([".html", ".htm", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg"]);
107
+ for (const absolutePath of files) {
108
+ const relativePath = toPosixRelative(referenceDir, absolutePath);
109
+ const extension = path.extname(absolutePath).toLowerCase();
110
+ if (!supportedExtensions.has(extension)) {
111
+ entries.push({
112
+ sourceFileRelativePath: relativePath,
113
+ fileType: extension || "unknown",
114
+ mappedScreenId: null,
115
+ mappedScreenTitle: null,
116
+ confidence: "low",
117
+ evidenceNotes: ["unsupported file type"],
118
+ status: "unmapped",
119
+ });
120
+ continue;
121
+ }
122
+
123
+ const baseTokens = tokenSet(path.basename(absolutePath, extension));
124
+ const htmlHints = extension === ".html" || extension === ".htm" ? readHtmlHints(absolutePath) : { title: "", heading: "" };
125
+ const titleTokens = tokenSet(htmlHints.title);
126
+ const headingTokens = tokenSet(htmlHints.heading);
127
+
128
+ let bestMatch = null;
129
+ for (const screen of screenTargets) {
130
+ const screenTokens = tokenSet(`${screen.screenId} ${screen.title}`);
131
+ let score = 0;
132
+ const evidence = [];
133
+ for (const token of screenTokens) {
134
+ if (baseTokens.has(token)) {
135
+ score += 2;
136
+ evidence.push(`filename token match: ${token}`);
137
+ }
138
+ if (titleTokens.has(token)) {
139
+ score += 1;
140
+ evidence.push(`title token match: ${token}`);
141
+ }
142
+ if (headingTokens.has(token)) {
143
+ score += 1;
144
+ evidence.push(`heading token match: ${token}`);
145
+ }
146
+ }
147
+ if (!bestMatch || score > bestMatch.score) {
148
+ bestMatch = { score, screen, evidence };
149
+ }
150
+ }
151
+
152
+ if (!bestMatch || bestMatch.score <= 0) {
153
+ entries.push({
154
+ sourceFileRelativePath: relativePath,
155
+ fileType: extension,
156
+ mappedScreenId: null,
157
+ mappedScreenTitle: null,
158
+ confidence: "low",
159
+ evidenceNotes: ["no deterministic filename/title/heading match"],
160
+ status: "unmapped",
161
+ });
162
+ continue;
163
+ }
164
+
165
+ entries.push({
166
+ sourceFileRelativePath: relativePath,
167
+ fileType: extension,
168
+ mappedScreenId: bestMatch.screen.screenId,
169
+ mappedScreenTitle: bestMatch.screen.title,
170
+ confidence: confidenceFromScore(bestMatch.score),
171
+ evidenceNotes: bestMatch.evidence,
172
+ status: "mapped",
173
+ });
174
+ }
175
+
176
+ return {
177
+ totalFiles: entries.length,
178
+ mappedCount: entries.filter((entry) => entry.status === "mapped").length,
179
+ unmappedCount: entries.filter((entry) => entry.status !== "mapped").length,
180
+ entries,
181
+ };
182
+ }
183
+
184
+ function writeInputContractState(projectPath, statePayload) {
185
+ const paths = describeDesignPaths(projectPath);
186
+ fs.mkdirSync(paths.designStatePath, { recursive: true });
187
+ fs.writeFileSync(paths.designStartInputStatePath, `${JSON.stringify(statePayload, null, 2)}\n`, "utf-8");
188
+ return paths.designStartInputStatePath;
189
+ }
190
+
191
+ function toScreenId(value) {
192
+ return String(value || "")
193
+ .trim()
194
+ .toLowerCase()
195
+ .replace(/[^a-z0-9]+/g, "-")
196
+ .replace(/^-+|-+$/g, "");
197
+ }
198
+
199
+ function normalizedScreenRecord(raw, sourceArtifact, fallbackTitle) {
200
+ const title = String(
201
+ (raw && (raw.title || raw.name || raw.screen || raw.label || raw.id)) || fallbackTitle || ""
202
+ ).trim();
203
+ if (!title) {
204
+ return null;
205
+ }
206
+ const screenId = toScreenId((raw && (raw.id || raw.slug)) || title);
207
+ const route = raw && typeof raw.route === "string" ? raw.route.trim() : "";
208
+ const purpose = raw && typeof raw.purpose === "string" ? raw.purpose.trim() : "";
209
+ const primaryAction = raw && typeof raw.primaryAction === "string" ? raw.primaryAction.trim() : "";
210
+ const majorSections = Array.isArray(raw && raw.majorSections)
211
+ ? raw.majorSections.map((value) => String(value).trim()).filter(Boolean)
212
+ : [];
213
+ const requiredStates = Array.isArray(raw && raw.requiredStates)
214
+ ? raw.requiredStates.map((value) => String(value).trim()).filter(Boolean)
215
+ : [];
216
+ const populatedCount = [route, purpose, primaryAction].filter(Boolean).length + (majorSections.length > 0 ? 1 : 0) + (requiredStates.length > 0 ? 1 : 0);
217
+ const confidence = populatedCount >= 3 ? "high" : populatedCount >= 1 ? "medium" : "low";
218
+ return {
219
+ screenId,
220
+ title,
221
+ route: route || null,
222
+ purpose: purpose || null,
223
+ primaryAction: primaryAction || null,
224
+ majorSections,
225
+ requiredStates,
226
+ sourceArtifact,
227
+ sourceStatus: "explicit",
228
+ confidence,
229
+ };
230
+ }
231
+
232
+ function extractScreensFromHandoffJson(handoffPath, relativePath) {
233
+ const raw = fs.readFileSync(handoffPath, "utf-8");
234
+ let payload = null;
235
+ try {
236
+ payload = JSON.parse(raw);
237
+ } catch (_err) {
238
+ return [];
239
+ }
240
+ const candidateArrays = [
241
+ payload && payload.screens,
242
+ payload && payload.screenInventory,
243
+ payload && payload.screen_inventory,
244
+ payload && payload.screenList,
245
+ payload && payload.screen_list,
246
+ ].filter(Array.isArray);
247
+ const screens = [];
248
+ for (const candidateArray of candidateArrays) {
249
+ for (const item of candidateArray) {
250
+ const record =
251
+ typeof item === "string"
252
+ ? normalizedScreenRecord({ title: item }, relativePath, item)
253
+ : normalizedScreenRecord(item || {}, relativePath, "");
254
+ if (record) {
255
+ screens.push(record);
256
+ }
257
+ }
258
+ }
259
+ return screens;
260
+ }
261
+
262
+ function extractScreensFromInventoryMarkdown(inventoryPath, relativePath) {
263
+ const lines = fs.readFileSync(inventoryPath, "utf-8").split(/\r?\n/);
264
+ const screens = [];
265
+ for (const line of lines) {
266
+ const headingMatch = line.match(/^###\s+(.+?)\s*$/);
267
+ if (headingMatch) {
268
+ const record = normalizedScreenRecord({ title: headingMatch[1] }, relativePath, headingMatch[1]);
269
+ if (record) screens.push(record);
270
+ continue;
271
+ }
272
+ const listMatch = line.match(/^\s*-\s+(.+?)\s*$/);
273
+ if (listMatch) {
274
+ const candidate = listMatch[1].replace(/`/g, "").trim();
275
+ if (!candidate || candidate.includes(":")) {
276
+ continue;
277
+ }
278
+ const record = normalizedScreenRecord({ title: candidate }, relativePath, candidate);
279
+ if (record) screens.push(record);
280
+ }
281
+ }
282
+ return screens;
283
+ }
284
+
285
+ function dedupeScreens(screens) {
286
+ const byId = new Map();
287
+ for (const screen of screens) {
288
+ if (!screen || !screen.screenId) {
289
+ continue;
290
+ }
291
+ if (!byId.has(screen.screenId)) {
292
+ byId.set(screen.screenId, screen);
293
+ continue;
294
+ }
295
+ const existing = byId.get(screen.screenId);
296
+ const existingScore = (existing.route ? 1 : 0) + (existing.purpose ? 1 : 0) + (existing.primaryAction ? 1 : 0) + existing.majorSections.length + existing.requiredStates.length;
297
+ const newScore = (screen.route ? 1 : 0) + (screen.purpose ? 1 : 0) + (screen.primaryAction ? 1 : 0) + screen.majorSections.length + screen.requiredStates.length;
298
+ if (newScore > existingScore) {
299
+ byId.set(screen.screenId, screen);
300
+ }
301
+ }
302
+ return Array.from(byId.values());
303
+ }
304
+
305
+ function buildInputContractState({
306
+ fromSpecPath,
307
+ projectPath,
308
+ designBriefPath,
309
+ referenceDir,
310
+ profile,
311
+ }) {
312
+ const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
313
+ const resolvedSpecPath = resolveProjectPath(fromSpecPath || resolvedProjectPath);
314
+ if (!fs.existsSync(resolvedSpecPath) || !fs.statSync(resolvedSpecPath).isDirectory()) {
315
+ throw new ValidationError(`--from-spec is not a valid directory: ${resolvedSpecPath}. No project files were changed.`);
316
+ }
317
+
318
+ const selectedProfile = resolveProfile(profile);
319
+
320
+ const artifactRules = [
321
+ { key: "designHandoff", relativeDir: "docs/design", pattern: /^SPEC_TO_DESIGN_HANDOFF_.*\.json$/i, blocker: "NEEDS_DESIGN_HANDOFF" },
322
+ { key: "screenInventory", relativeDir: "docs/design", pattern: /^SCREEN_INVENTORY_.*\.md$/i, blocker: null },
323
+ { key: "userFlows", relativeDir: "docs/design", pattern: /^USER_FLOWS_.*\.md$/i, blocker: "NEEDS_USER_FLOWS" },
324
+ { key: "domainModel", relativeDir: "docs/specs", pattern: /^DOMAIN_MODEL_.*\.md$/i, blocker: "NEEDS_DOMAIN_MODEL" },
325
+ { key: "archDesign", relativeDir: "docs/architecture", pattern: /^ARCH_DESIGN_.*\.md$/i, blocker: null },
326
+ { key: "baSpec", relativeDir: "docs/specs", pattern: /^BA_SPEC_.*\.md$/i, blocker: null },
327
+ ];
328
+
329
+ const artifacts = {};
330
+ for (const rule of artifactRules) {
331
+ artifacts[rule.key] = findArtifactByPattern(resolvedSpecPath, rule.relativeDir, rule.pattern);
332
+ }
333
+
334
+ let explicitDesignBrief = null;
335
+ if (designBriefPath) {
336
+ const absoluteDesignBrief = path.resolve(resolvedProjectPath, designBriefPath);
337
+ if (!fs.existsSync(absoluteDesignBrief) || !fs.statSync(absoluteDesignBrief).isFile()) {
338
+ throw new ValidationError(`--design-brief is not a valid file: ${absoluteDesignBrief}. No project files were changed.`);
339
+ }
340
+ explicitDesignBrief = {
341
+ absolutePath: absoluteDesignBrief,
342
+ relativeToProject: toPosixRelative(resolvedProjectPath, absoluteDesignBrief),
343
+ };
344
+ }
345
+
346
+ const reference = resolveReferenceDirectory(referenceDir, resolvedProjectPath);
347
+ const parsedScreens = [];
348
+ if (artifacts.designHandoff) {
349
+ parsedScreens.push(
350
+ ...extractScreensFromHandoffJson(artifacts.designHandoff.absolutePath, artifacts.designHandoff.relativePath)
351
+ );
352
+ }
353
+ if (artifacts.screenInventory) {
354
+ parsedScreens.push(
355
+ ...extractScreensFromInventoryMarkdown(artifacts.screenInventory.absolutePath, artifacts.screenInventory.relativePath)
356
+ );
357
+ }
358
+ const screens = dedupeScreens(parsedScreens);
359
+
360
+ const blockers = artifactRules.filter((rule) => rule.blocker && !artifacts[rule.key]).map((rule) => rule.blocker);
361
+ if (screens.length === 0) {
362
+ blockers.push("NEEDS_SCREEN_INVENTORY");
363
+ }
364
+ const analysisStatus = blockers.length === 0 ? "INPUT_CONTRACT_READY" : "INPUT_CONTRACT_BLOCKED";
365
+
366
+ const artifactSummary = {};
367
+ for (const rule of artifactRules) {
368
+ const value = artifacts[rule.key];
369
+ artifactSummary[rule.key] = value
370
+ ? {
371
+ found: true,
372
+ relativeToSpecRoot: value.relativePath,
373
+ absolutePath: value.absolutePath,
374
+ }
375
+ : {
376
+ found: false,
377
+ relativeToSpecRoot: null,
378
+ absolutePath: null,
379
+ };
380
+ }
381
+
382
+ return {
383
+ schema: "sdtk.design.input-contract.v1",
384
+ mode: "from-spec",
385
+ projectPath: resolvedProjectPath,
386
+ fromSpecPath: resolvedSpecPath,
387
+ profileSelection: selectedProfile ? selectedProfile.name : null,
388
+ profilePrimitives: selectedProfile
389
+ ? {
390
+ name: selectedProfile.name,
391
+ summary: selectedProfile.summary,
392
+ primitives: selectedProfile.primitives,
393
+ screenRoleHints: selectedProfile.screenRoleHints,
394
+ }
395
+ : null,
396
+ explicitDesignBrief,
397
+ referenceDirectory: {
398
+ provided: reference.provided,
399
+ status: reference.status,
400
+ absolutePath: reference.absolutePath,
401
+ },
402
+ referenceMap: reference.provided
403
+ ? mapReferenceFiles({ referenceDir: reference.absolutePath, screens })
404
+ : { totalFiles: 0, mappedCount: 0, unmappedCount: 0, entries: [] },
405
+ artifacts: artifactSummary,
406
+ screenModel: {
407
+ totalScreens: screens.length,
408
+ missingMetadataCount: screens.filter((screen) => !screen.route || !screen.purpose || !screen.primaryAction).length,
409
+ legacyFallbackUsed: false,
410
+ readiness: screens.length > 0 ? "MULTI_SCREEN_READY" : "NEEDS_EXPLICIT_SCREEN_INPUT",
411
+ screens,
412
+ },
413
+ blockers,
414
+ analysisStatus,
415
+ nextRecommendedCommand:
416
+ blockers.length > 0 ? "Provide missing explicit SPEC/design artifacts and re-run sdtk-design start --from-spec." : "sdtk-design prototype",
417
+ };
418
+ }
419
+
420
+ module.exports = {
421
+ buildInputContractState,
422
+ writeInputContractState,
423
+ };
@@ -14,6 +14,7 @@ const DESIGN_SYSTEM_RELATIVE = path.join("docs", "design", "DESIGN_SYSTEM.md");
14
14
  const DESIGN_HANDOFF_RELATIVE = path.join("docs", "design", "DESIGN_HANDOFF.md");
15
15
  const DESIGN_STATE_RELATIVE = path.join(".sdtk", "design");
16
16
  const DESIGN_MANIFEST_RELATIVE = path.join(".sdtk", "design", "manifest.json");
17
+ const DESIGN_START_INPUT_STATE_RELATIVE = path.join(".sdtk", "design", "START_INPUT_STATE.json");
17
18
 
18
19
  function resolveProjectPath(projectPath) {
19
20
  return path.resolve(projectPath || process.cwd());
@@ -55,6 +56,7 @@ function describeDesignPaths(projectPath) {
55
56
  designHandoffPath: path.join(root, DESIGN_HANDOFF_RELATIVE),
56
57
  designStatePath: path.join(root, DESIGN_STATE_RELATIVE),
57
58
  manifestPath: path.join(root, DESIGN_MANIFEST_RELATIVE),
59
+ designStartInputStatePath: path.join(root, DESIGN_START_INPUT_STATE_RELATIVE),
58
60
  };
59
61
  }
60
62
 
@@ -68,6 +70,7 @@ module.exports = {
68
70
  DESIGN_README_RELATIVE,
69
71
  DESIGN_REVIEWS_RELATIVE,
70
72
  DESIGN_SCREEN_MAP_RELATIVE,
73
+ DESIGN_START_INPUT_STATE_RELATIVE,
71
74
  DESIGN_STATE_RELATIVE,
72
75
  DESIGN_SYSTEM_RELATIVE,
73
76
  DESIGN_WIREFRAMES_RELATIVE,
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ const { ValidationError } = require("./errors");
4
+
5
+ const DESIGN_PROFILES = {
6
+ "b2b-commerce": {
7
+ name: "b2b-commerce",
8
+ summary: "Reusable B2B commerce primitives for multi-screen product catalogs and purchasing workflows.",
9
+ primitives: [
10
+ "catalog grid",
11
+ "filter sidebar",
12
+ "product card",
13
+ "pdp gallery/spec/cta",
14
+ "search results/no-result",
15
+ "cart line items/summary",
16
+ "checkout steps",
17
+ "order history/detail",
18
+ "account shell",
19
+ "configurator wizard",
20
+ "bom/material table",
21
+ ],
22
+ screenRoleHints: {
23
+ home: ["catalog grid", "product card", "sticky purchase/cta"],
24
+ category: ["filter sidebar", "catalog grid", "product card"],
25
+ "product-detail": ["pdp gallery/spec/cta", "sticky purchase/cta"],
26
+ search: ["search results/no-result", "product card", "filter sidebar"],
27
+ cart: ["cart line items/summary", "sticky purchase/cta"],
28
+ checkout: ["checkout steps", "cart line items/summary", "sticky purchase/cta"],
29
+ "order-history": ["order history/detail"],
30
+ "order-detail": ["order history/detail"],
31
+ "account-info": ["account shell"],
32
+ "mode-b-configurator": ["configurator wizard", "bom/material table", "sticky purchase/cta"],
33
+ },
34
+ },
35
+ };
36
+
37
+ function availableProfileNames() {
38
+ return Object.keys(DESIGN_PROFILES);
39
+ }
40
+
41
+ function resolveProfile(profileName) {
42
+ if (!profileName) {
43
+ return null;
44
+ }
45
+ const normalized = String(profileName).trim().toLowerCase();
46
+ if (DESIGN_PROFILES[normalized]) {
47
+ return DESIGN_PROFILES[normalized];
48
+ }
49
+ throw new ValidationError(
50
+ `Unsupported --profile "${profileName}". Supported profiles: ${availableProfileNames().join(", ")}. No project files were changed.`
51
+ );
52
+ }
53
+
54
+ module.exports = {
55
+ DESIGN_PROFILES,
56
+ availableProfileNames,
57
+ resolveProfile,
58
+ };