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.
@@ -87,37 +87,6 @@ function readInputContractState(paths) {
87
87
  }
88
88
  }
89
89
 
90
- function normalizeScreenRole(screen) {
91
- const token = `${screen.screenId || ""} ${screen.title || ""}`.toLowerCase();
92
- if (token.includes("home")) return "home";
93
- if (token.includes("category")) return "category";
94
- if (token.includes("product-detail") || token.includes("product detail") || token.includes("pdp")) return "product-detail";
95
- if (token.includes("search")) return "search";
96
- if (token.includes("cart")) return "cart";
97
- if (token.includes("checkout")) return "checkout";
98
- if (token.includes("order-history") || token.includes("order history")) return "order-history";
99
- if (token.includes("order-detail") || token.includes("order detail")) return "order-detail";
100
- if (token.includes("account")) return "account-info";
101
- if (token.includes("configurator") || token.includes("mode-b")) return "mode-b-configurator";
102
- return screen.screenId || "screen";
103
- }
104
-
105
- function requiredRoleMarkers(role) {
106
- const map = {
107
- home: ["hero", "featured products", "configurator"],
108
- category: ["filter sidebar", "product grid"],
109
- "product-detail": ["spec-table", "add to cart"],
110
- search: ["search-toolbar", "no-result"],
111
- cart: ["cart-table", "checkout"],
112
- checkout: ["checkout-stepper", "summary card"],
113
- "order-history": ["order history list"],
114
- "order-detail": ["timeline", "detail summary"],
115
- "account-info": ["account sidebar", "view or edit form"],
116
- "mode-b-configurator": ["configurator wizard", "preview panel", "bom table", "recalculate", "add bom to cart"],
117
- };
118
- return map[role] || [];
119
- }
120
-
121
90
  function listPrototypeScreenFiles(paths) {
122
91
  if (!fs.existsSync(paths.prototypeScreensPath) || !fs.statSync(paths.prototypeScreensPath).isDirectory()) {
123
92
  return [];
@@ -129,27 +98,9 @@ function listPrototypeScreenFiles(paths) {
129
98
  .map((name) => path.join(paths.prototypeScreensPath, name));
130
99
  }
131
100
 
132
- function combinedPrototypeReviewContent({ artifactPath, artifactContent, paths }) {
133
- const screenFiles = listPrototypeScreenFiles(paths);
134
- if (screenFiles.length === 0) {
135
- return artifactContent;
136
- }
137
- const parts = [artifactContent];
138
- if (path.resolve(artifactPath) !== path.resolve(paths.prototypeIndexPath) && fs.existsSync(paths.prototypeIndexPath)) {
139
- parts.push(fs.readFileSync(paths.prototypeIndexPath, "utf-8"));
140
- }
141
- for (const filePath of screenFiles) {
142
- if (path.resolve(filePath) === path.resolve(artifactPath)) {
143
- continue;
144
- }
145
- parts.push(fs.readFileSync(filePath, "utf-8"));
146
- }
147
- return parts.join("\n");
148
- }
149
-
150
101
  function resolveAntiSlopLintMode(env = process.env) {
151
- const value = String(env.SDTK_DESIGN_ANTI_SLOP_LINT || "warn").toLowerCase();
152
- return value === "on" || value === "off" || value === "warn" ? value : "warn";
102
+ const value = String(env.SDTK_DESIGN_ANTI_SLOP_LINT || "on").toLowerCase();
103
+ return value === "off" ? "off" : "on";
153
104
  }
154
105
 
155
106
  function collectPrototypeHtmlMap({ artifactPath, artifactContent, paths }) {
@@ -157,19 +108,18 @@ function collectPrototypeHtmlMap({ artifactPath, artifactContent, paths }) {
157
108
  const add = (screenId, html) => {
158
109
  if (!map.has(screenId)) map.set(screenId, html);
159
110
  };
160
- add(path.basename(artifactPath, ".html") || "artifact", artifactContent);
161
- if (fs.existsSync(paths.prototypeIndexPath)) {
162
- add("index", fs.readFileSync(paths.prototypeIndexPath, "utf-8"));
163
- }
164
111
  for (const filePath of listPrototypeScreenFiles(paths)) {
165
112
  add(path.basename(filePath, ".html"), fs.readFileSync(filePath, "utf-8"));
166
113
  }
114
+ if (map.size === 0 && path.resolve(path.dirname(artifactPath)) === path.resolve(paths.prototypeScreensPath)) {
115
+ add(path.basename(artifactPath, ".html") || "artifact", artifactContent);
116
+ }
167
117
  return map;
168
118
  }
169
119
 
170
120
  function applyAntiSlopVerdict({ structuralVerdict, lintFindings, lintMode }) {
171
121
  const lintSummary = summarizeLintFindings(lintFindings);
172
- const lintBlocking = lintMode === "on" && lintSummary.p0 > 0;
122
+ const lintBlocking = lintMode !== "off" && lintSummary.p0 > 0;
173
123
  return {
174
124
  verdict: lintBlocking ? "FAIL_WITH_RENDERER_SLOP" : structuralVerdict,
175
125
  lintSummary,
@@ -177,78 +127,112 @@ function applyAntiSlopVerdict({ structuralVerdict, lintFindings, lintMode }) {
177
127
  };
178
128
  }
179
129
 
180
- function evaluateHighFidelityGate({ artifactContent, statePayload, projectPath, paths }) {
181
- const html = String(artifactContent || "").toLowerCase();
130
+ function extractTagContent(html, tagName) {
131
+ const match = String(html || "").match(new RegExp(`<${tagName}\\b[^>]*>([\\s\\S]*?)<\\/${tagName}>`, "i"));
132
+ return match ? match[1] : "";
133
+ }
134
+
135
+ function stripTags(value) {
136
+ return String(value || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
137
+ }
138
+
139
+ function countTag(html, tagName) {
140
+ return (String(html || "").match(new RegExp(`<${tagName}\\b`, "gi")) || []).length;
141
+ }
142
+
143
+ function collectDesignTokenReferences(paths) {
144
+ if (!fs.existsSync(paths.designTokensPath)) return [];
145
+ try {
146
+ const payload = JSON.parse(fs.readFileSync(paths.designTokensPath, "utf-8"));
147
+ const values = [];
148
+ const visit = (value) => {
149
+ if (value == null) return;
150
+ if (typeof value === "string" || typeof value === "number") {
151
+ const token = String(value).trim();
152
+ if (token) values.push(token.toLowerCase());
153
+ return;
154
+ }
155
+ if (Array.isArray(value)) {
156
+ value.forEach(visit);
157
+ return;
158
+ }
159
+ if (typeof value === "object") Object.values(value).forEach(visit);
160
+ };
161
+ visit(payload);
162
+ return values.filter((value) => value.length >= 3);
163
+ } catch (_err) {
164
+ return [];
165
+ }
166
+ }
167
+
168
+ function hasDesignTokenReference(html, tokenReferences) {
169
+ const lower = String(html || "").toLowerCase();
170
+ if (/var\(--[a-z0-9-]+\)/i.test(lower)) return true;
171
+ return tokenReferences.some((token) => lower.includes(token));
172
+ }
173
+
174
+ function screenStructuralFindings({ screenId, html, tokenReferences }) {
175
+ const findings = [];
176
+ const lower = String(html || "").toLowerCase();
177
+ const title = stripTags(extractTagContent(html, "title"));
178
+ const body = extractTagContent(html, "body");
179
+ if (!lower.trimStart().startsWith("<!doctype html>")) findings.push("missing doctype");
180
+ if (!/<html\b/i.test(html)) findings.push("missing html element");
181
+ if (!/<head\b/i.test(html)) findings.push("missing head element");
182
+ if (!/<body\b/i.test(html)) findings.push("missing body element");
183
+ if (!title) findings.push("missing or empty title");
184
+ const h1Count = countTag(html, "h1");
185
+ if (h1Count !== 1) findings.push(`expected exactly one h1, found ${h1Count}`);
186
+ if (!stripTags(body)) findings.push("empty body");
187
+ if (!hasDesignTokenReference(html, tokenReferences)) findings.push("missing design token reference");
188
+ return {
189
+ screenId,
190
+ pass: findings.length === 0,
191
+ findings,
192
+ };
193
+ }
194
+
195
+ function evaluateHighFidelityGate({ statePayload, projectPath, paths }) {
182
196
  const screens = statePayload.screenModel.screens || [];
183
197
  const profile = statePayload.profileSelection || "none";
184
198
  const hasTokens = fs.existsSync(paths.designTokensPath);
185
199
  const hasComponentLibrary = fs.existsSync(paths.componentPatternLibraryPath);
186
- const hasPre = html.includes("<pre");
187
- const hasRawJsonRisk = html.includes("json.stringify") || html.includes("raw json") || html.includes("{\"") || html.includes("not_found");
188
- const hasNav = html.includes('aria-label="screen navigation"') || html.includes("screen navigation");
189
- const hasStateChips = html.includes('data-chip-type="state"') || html.includes("chip");
200
+ const tokenReferences = collectDesignTokenReferences(paths);
190
201
  const sectionRows = [];
191
- let missingCount = 0;
202
+ const blockers = [];
192
203
  for (const screen of screens) {
193
204
  const id = String(screen.screenId || "");
194
205
  const title = String(screen.title || id);
195
- const role = normalizeScreenRole(screen);
196
- const idMarker = `id="screen-${id}"`;
197
- const navMarkers = [`href="#screen-${id}"`, `href="screens/${id}.html"`, `href="${id}.html"`];
198
- const roleMarkers = requiredRoleMarkers(role);
199
- const missing = [];
200
- if (!html.includes(idMarker.toLowerCase())) missing.push("missing screen section id");
201
- if (!navMarkers.some((marker) => html.includes(marker.toLowerCase()))) missing.push("missing navigation link");
202
- for (const marker of roleMarkers) {
203
- if (!html.includes(marker)) missing.push(`missing component marker: ${marker}`);
206
+ const filePath = path.join(paths.prototypeScreensPath, `${id}.html`);
207
+ if (!fs.existsSync(filePath)) {
208
+ const missing = [`missing screen file: docs/design/prototype/screens/${id}.html`];
209
+ blockers.push(`${title}: ${missing.join("; ")}`);
210
+ sectionRows.push({ title, role: id, missing, pass: false });
211
+ continue;
204
212
  }
205
- if (missing.length > 0) missingCount += 1;
206
- sectionRows.push({
207
- title,
208
- role,
209
- missing,
210
- pass: missing.length === 0,
213
+ const structural = screenStructuralFindings({
214
+ screenId: id,
215
+ html: fs.readFileSync(filePath, "utf-8"),
216
+ tokenReferences,
211
217
  });
218
+ if (!structural.pass) blockers.push(`${title}: ${structural.findings.join("; ")}`);
219
+ sectionRows.push({ title, role: id, missing: structural.findings, pass: structural.pass });
212
220
  }
213
221
 
214
222
  let verdict = "PASS_HIGH_FIDELITY_READY";
215
- const blockers = [];
216
223
  if (screens.length === 0) {
217
- verdict = "FAIL_WITH_PRODUCT_GAPS";
224
+ verdict = "FAIL_STRUCTURE";
218
225
  blockers.push("explicit screen model is empty");
219
226
  }
220
227
  if (!hasComponentLibrary) {
221
- verdict = "FAIL_WITH_PRODUCT_GAPS";
228
+ verdict = "FAIL_STRUCTURE";
222
229
  blockers.push("component pattern library missing");
223
230
  }
224
231
  if (!hasTokens) {
225
- verdict = "FAIL_WITH_PRODUCT_GAPS";
232
+ verdict = "FAIL_STRUCTURE";
226
233
  blockers.push("design tokens missing");
227
234
  }
228
- if (!hasNav) {
229
- verdict = "FAIL_WITH_PRODUCT_GAPS";
230
- blockers.push("inter-screen navigation contract missing");
231
- }
232
- if (!hasStateChips) {
233
- verdict = "FAIL_WITH_PRODUCT_GAPS";
234
- blockers.push("state chip coverage missing");
235
- }
236
- if (missingCount > 0) {
237
- verdict = "FAIL_WITH_PRODUCT_GAPS";
238
- blockers.push(`${missingCount} screen(s) missing required component markers`);
239
- }
240
- if (hasPre || hasRawJsonRisk) {
241
- verdict = "FAIL_WITH_PRODUCT_GAPS";
242
- blockers.push("debug/raw JSON UI risk detected");
243
- }
244
- if (profile === "b2b-commerce" && !html.includes("configurator wizard")) {
245
- verdict = "FAIL_WITH_PRODUCT_GAPS";
246
- blockers.push("configurator completeness missing");
247
- }
248
- if (html.includes("main layout</h3>") && html.includes("primary action</h3>")) {
249
- verdict = "FAIL_HIGH_FIDELITY_NOT_READY";
250
- blockers.push("generic scaffold markers detected (Main layout / Primary action)");
251
- }
235
+ if (sectionRows.some((row) => !row.pass)) verdict = "FAIL_STRUCTURE";
252
236
  return {
253
237
  verdict,
254
238
  blockers,
@@ -256,10 +240,6 @@ function evaluateHighFidelityGate({ artifactContent, statePayload, projectPath,
256
240
  sectionRows,
257
241
  hasTokens,
258
242
  hasComponentLibrary,
259
- hasNav,
260
- hasStateChips,
261
- hasPre,
262
- hasRawJsonRisk,
263
243
  projectPath,
264
244
  };
265
245
  }
@@ -272,12 +252,8 @@ function fidelityReviewContent(result) {
272
252
  `- Profile: ${result.profile}`,
273
253
  `- Component library: ${result.hasComponentLibrary ? "present" : "missing"}`,
274
254
  `- Design tokens: ${result.hasTokens ? "present" : "missing"}`,
275
- `- Navigation contract: ${result.hasNav ? "present" : "missing"}`,
276
- `- State coverage markers: ${result.hasStateChips ? "present" : "missing"}`,
277
- `- Raw <pre> debug risk: ${result.hasPre ? "detected" : "not detected"}`,
278
- `- Raw JSON debug risk: ${result.hasRawJsonRisk ? "detected" : "not detected"}`,
279
255
  "",
280
- "## Screen-By-Screen Coverage",
256
+ "## Screen-By-Screen Structural Sanity",
281
257
  "",
282
258
  "| Screen | Role | Result | Actionable notes |",
283
259
  "|---|---|---|---|",
@@ -300,41 +276,61 @@ function fidelityReviewContent(result) {
300
276
  "",
301
277
  "## Actions",
302
278
  "",
303
- "- Fill missing component markers per failed screen.",
279
+ "- Fix failed screen HTML documents so each has doctype, html/head/body, title, one h1, non-empty body, and token references.",
304
280
  "- Ensure design tokens and component pattern library are generated before review.",
305
- "- Remove raw debug UI surfaces (`<pre>` and JSON dumps) from customer-facing prototype output.",
306
- "- Re-run `sdtk-design prototype` and `sdtk-design review --artifact docs/design/prototype/index.html`.",
281
+ "- Remove anti-slop P0 findings from customer-facing prototype output.",
282
+ "- Re-run the design-prototype skill and `sdtk-design review --artifact docs/design/prototype/index.html`.",
307
283
  "",
308
284
  ].join("\n");
309
285
  }
310
286
 
311
287
  function visualPolishChecks({ artifactRelativePath, artifactContent, prototypeHtml }) {
312
288
  const lower = String(artifactContent || "").toLowerCase();
313
- const hasLanding = lower.includes("section-landing") || lower.includes("landing");
314
- const hasOnboarding = lower.includes("section-onboarding") || lower.includes("onboarding");
315
- const hasDashboard = lower.includes("section-dashboard") || lower.includes("dashboard");
316
- const hasPreset = lower.includes("data-style-preset") || lower.includes("style-premium-dashboard") || lower.includes("style-minimal-saas") || lower.includes("style-bold-founder") || lower.includes("style-warm-editorial");
317
289
  const hasResponsive = lower.includes("@media") || lower.includes("viewport") || lower.includes("mobile");
318
- const hasCta = includesAny(artifactContent, ["btn-primary", "Primary CTA", "Create workspace", "Add lead", "Continue", "Start"]);
319
- const hasDashboardDensity = hasDashboard && includesAny(artifactContent, ["metric", "kpi", "pipeline", "status", "lead", "table", "card"]);
290
+ const hasCta = includesAny(artifactContent, ["btn-primary", "Primary CTA", "Create workspace", "Add lead", "Continue", "Start", "button", "href="]);
320
291
  const hasAccessibility = includesAny(artifactContent, ["accessibility", "aria-", "button", "label", "44px", "focus"]);
321
292
  const hasSpacing = includesAny(artifactContent, ["gap:", "padding:", "spacing", "density", "grid", "margin"]);
322
293
  const modeEvidence = prototypeHtml
323
- ? "Prototype HTML is checked as visual direction only, not production application code."
294
+ ? "Prototype HTML visual polish uses standalone HTML signals; structural readiness is checked separately by the fidelity gate."
324
295
  : "Markdown artifact is checked for implementation-ready visual direction.";
325
296
 
297
+ const rows = [];
298
+ if (prototypeHtml) {
299
+ const hasSemanticLandmarks = includesAny(artifactContent, ["<main", "<header", "<nav", "<section", "role=", "aria-label"]);
300
+ const h1Count = (String(artifactContent || "").match(/<h1\b/gi) || []).length;
301
+ const hasSupportingHeading = /<h[2-3]\b/i.test(String(artifactContent || ""));
302
+ const hasHeadingOutline = h1Count === 1 && hasSupportingHeading;
303
+ const hasTokenUsage = /var\(--[a-z0-9-]+\)/i.test(String(artifactContent || ""));
304
+ const hasStubRisk = includesAny(artifactContent, ["placeholder text", "sample content", "lorem ipsum", "wireframe"]);
305
+ rows.push(
306
+ `| HTML Semantics | ${hasSemanticLandmarks ? "Pass" : "Needs attention"} | ${hasSemanticLandmarks ? "Prototype includes semantic landmarks or labelled regions." : "Add semantic landmarks such as main, nav, header, section, or labelled regions."} |`,
307
+ `| Heading Outline | ${hasHeadingOutline ? "Pass" : "Needs attention"} | ${hasHeadingOutline ? "Prototype has one h1 and supporting h2/h3 structure." : "Use one h1 with supporting section headings."} |`,
308
+ `| Responsive / Mobile Risk | ${hasResponsive ? "Pass" : "Needs attention"} | ${hasResponsive ? "Responsive viewport, media, or mobile behavior is represented." : "Add viewport or mobile breakpoint behavior before implementation."} |`,
309
+ `| Token Usage | ${hasTokenUsage ? "Pass" : "Needs attention"} | ${hasTokenUsage ? "Prototype references CSS custom properties for visual tokens." : "Reference design tokens via CSS custom properties in generated HTML."} |`,
310
+ `| CTA Clarity | ${hasCta ? "Pass" : "Needs attention"} | ${hasCta ? "A concrete action link or button is visible in the artifact." : "Add one dominant action link or button with concrete copy."} |`,
311
+ `| Stub / Wireframe Risk | ${!hasStubRisk ? "Pass" : "Needs attention"} | ${!hasStubRisk ? "No obvious placeholder or wireframe copy is present in the reviewed HTML." : "Replace placeholder or wireframe copy with generated product content."} |`
312
+ );
313
+ } else {
314
+ const hasLanding = lower.includes("landing");
315
+ const hasOnboarding = lower.includes("onboarding");
316
+ const hasDashboard = lower.includes("dashboard");
317
+ const hasDashboardDensity = hasDashboard && includesAny(artifactContent, ["metric", "kpi", "pipeline", "status", "lead", "table", "card"]);
318
+ rows.push(
319
+ `| Hierarchy | ${hasLanding && hasOnboarding && hasDashboard ? "Pass" : "Needs attention"} | ${hasLanding && hasOnboarding && hasDashboard ? "Landing, onboarding, and dashboard sections create a clear read order." : "Ensure the artifact clearly separates landing, onboarding, and dashboard intent."} |`,
320
+ `| Spacing / Density | ${hasSpacing ? "Pass" : "Needs attention"} | ${hasSpacing ? "Artifact includes grid, gap, padding, margin, or spacing guidance." : "Add spacing and density rules so the implementation does not regress into a flat wireframe."} |`,
321
+ `| CTA Clarity | ${hasCta ? "Pass" : "Needs attention"} | ${hasCta ? "A concrete primary action is visible in the artifact." : "Add one dominant CTA with concrete action copy."} |`,
322
+ `| Dashboard Information Density | ${hasDashboardDensity ? "Pass" : "Needs attention"} | ${hasDashboardDensity ? "Dashboard direction includes metrics, pipeline/status, cards, lists, or table surfaces." : "Dashboard needs enough KPI, status, list, or table detail to guide a real MVP screen."} |`,
323
+ `| Responsive / Mobile Risk | ${hasResponsive ? "Pass" : "Needs attention"} | ${hasResponsive ? "Responsive or mobile behavior is represented." : "Add mobile breakpoint or narrow-screen behavior before implementation."} |`,
324
+ `| Accessibility Baseline | ${hasAccessibility ? "Pass" : "Needs attention"} | ${hasAccessibility ? "Artifact includes accessible-control or baseline accessibility signals." : "Add labels, focus, touch-target, and color-not-alone guidance."} |`
325
+ );
326
+ }
327
+
326
328
  return [
327
329
  "## Visual Polish Checks",
328
330
  "",
329
331
  "| Check | Result | Evidence |",
330
332
  "|---|---|---|",
331
- `| Hierarchy | ${hasLanding && hasOnboarding && hasDashboard ? "Pass" : "Needs attention"} | ${hasLanding && hasOnboarding && hasDashboard ? "Landing, onboarding, and dashboard sections create a clear read order." : "Ensure the artifact clearly separates landing, onboarding, and dashboard intent."} |`,
332
- `| Spacing / Density | ${hasSpacing ? "Pass" : "Needs attention"} | ${hasSpacing ? "Artifact includes grid, gap, padding, margin, or spacing guidance." : "Add spacing and density rules so the implementation does not regress into a flat wireframe."} |`,
333
- `| CTA Clarity | ${hasCta ? "Pass" : "Needs attention"} | ${hasCta ? "A concrete primary action is visible in the artifact." : "Add one dominant CTA with concrete action copy."} |`,
334
- `| Dashboard Information Density | ${hasDashboardDensity ? "Pass" : "Needs attention"} | ${hasDashboardDensity ? "Dashboard direction includes metrics, pipeline/status, cards, lists, or table surfaces." : "Dashboard needs enough KPI, status, list, or table detail to guide a real MVP screen."} |`,
335
- `| Responsive / Mobile Risk | ${hasResponsive ? "Pass" : "Needs attention"} | ${hasResponsive ? "Responsive or mobile behavior is represented." : "Add mobile breakpoint or narrow-screen behavior before implementation."} |`,
336
- `| Accessibility Baseline | ${hasAccessibility ? "Pass" : "Needs attention"} | ${hasAccessibility ? "Artifact includes accessible-control or baseline accessibility signals." : "Add labels, focus, touch-target, and color-not-alone guidance."} |`,
337
- `| Anti-Wireframe / Mockup Warning | ${prototypeHtml && hasPreset ? "Pass" : "Needs attention"} | ${prototypeHtml && hasPreset ? "Prototype includes style preset tokens/classes and should guide visual polish beyond basic wireframes." : "Do not treat the artifact as complete UI; add concrete style preset tokens/classes or visual contract details."} |`,
333
+ ...rows,
338
334
  "",
339
335
  `- Reviewed visual artifact: \`${artifactRelativePath}\`.`,
340
336
  `- ${modeEvidence}`,
@@ -440,13 +436,7 @@ function runDesignReview({ artifact, projectPath, force = false }) {
440
436
  let fidelityVerdict = null;
441
437
  let antiSlopLint = null;
442
438
  if (shouldRunFidelityGate) {
443
- const fidelityArtifactContent = combinedPrototypeReviewContent({
444
- artifactPath,
445
- artifactContent,
446
- paths,
447
- });
448
439
  const fidelityResult = evaluateHighFidelityGate({
449
- artifactContent: fidelityArtifactContent,
450
440
  statePayload,
451
441
  projectPath: resolvedProjectPath,
452
442
  paths,
@@ -500,7 +490,7 @@ function cmdReview(args) {
500
490
  console.log(`[design] Fidelity verdict: ${result.fidelityVerdict}`);
501
491
  if (result.antiSlopLint) {
502
492
  if (result.antiSlopLint.mode === "off") {
503
- console.log("[design] Anti-slop lint: OFF (deprecated emergency rollback)");
493
+ console.log("[design] Anti-slop lint: OFF (emergency rollback)");
504
494
  } else if (result.antiSlopLint.summary.p0 > 0) {
505
495
  console.log(`[design] Anti-slop lint: ${result.antiSlopLint.summary.p0} P0 findings; see review report`);
506
496
  } else {
@@ -11,7 +11,7 @@ const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths
11
11
  const { availableProfileNames } = require("../lib/design-profiles");
12
12
  const { buildInputContractState, writeInputContractState } = require("../lib/design-input-contract");
13
13
  const { writeScreenBriefArtifacts } = require("../lib/screen-briefs");
14
- const { writeComponentContractArtifacts } = require("../lib/component-contract");
14
+ const { writeComponentContractArtifacts, loadBrandTokensV2, loadProjectFactsV1 } = require("../lib/component-contract");
15
15
  const { ValidationError } = require("../lib/errors");
16
16
  const { DEFAULT_STYLE, availableStyleNames, resolveStyleName } = require("../lib/style-presets");
17
17
 
@@ -25,6 +25,8 @@ const START_FLAG_DEFS = {
25
25
  "design-brief": { type: "string" },
26
26
  "reference-dir": { type: "string" },
27
27
  profile: { type: "string" },
28
+ "brand-tokens": { type: "string" },
29
+ "project-facts": { type: "string" },
28
30
  };
29
31
 
30
32
  const REQUIRED_WIREFRAME_FILES = ["LANDING.md", "ONBOARDING.md", "DASHBOARD.md"];
@@ -34,12 +36,14 @@ function cmdStartHelp() {
34
36
 
35
37
  Usage:
36
38
  sdtk-design start --idea "<rough MVP idea>" [--style <preset>] [--project-path <path>] [--force]
37
- sdtk-design start --from-spec <projectPath> [--design-brief <file>] [--reference-dir <dir>] [--profile <name>] [--project-path <path>]
39
+ sdtk-design start --from-spec <projectPath> [--design-brief <file>] [--reference-dir <dir>] [--profile <name>] [--brand-tokens <file>] [--project-facts <file>] [--project-path <path>]
38
40
 
39
41
  Example:
40
42
  sdtk-design start --idea "I want to build a lightweight CRM for solo consultants to track leads."
41
43
  sdtk-design start --idea "ClientPulse for consultants" --style premium-dashboard
42
44
  sdtk-design start --from-spec . --reference-dir ./docs/design/reference-export --profile b2b-commerce
45
+ sdtk-design start --from-spec . --profile b2b-commerce --brand-tokens ./brand-tokens.json
46
+ sdtk-design start --from-spec . --profile b2b-industrial-commerce --project-facts ./facts.json
43
47
 
44
48
  Style presets:
45
49
  ${availableStyleNames().join(", ")}
@@ -134,7 +138,12 @@ function runDesignStartFromSpec({
134
138
  designBrief,
135
139
  referenceDir,
136
140
  profile,
141
+ brandTokensPath,
142
+ projectFactsPath,
137
143
  }) {
144
+ // BK-223 + BK-229: validate explicit inputs BEFORE any artifact write (fail-closed).
145
+ if (brandTokensPath) loadBrandTokensV2(brandTokensPath);
146
+ if (projectFactsPath) loadProjectFactsV1(projectFactsPath);
138
147
  const contractState = buildInputContractState({
139
148
  fromSpecPath,
140
149
  projectPath,
@@ -147,7 +156,10 @@ function runDesignStartFromSpec({
147
156
  let componentContract = null;
148
157
  if (contractState.analysisStatus === "INPUT_CONTRACT_READY") {
149
158
  screenBriefs = writeScreenBriefArtifacts(contractState.projectPath, contractState);
150
- componentContract = writeComponentContractArtifacts(contractState.projectPath, contractState);
159
+ componentContract = writeComponentContractArtifacts(contractState.projectPath, contractState, {
160
+ brandTokensPath: brandTokensPath || null,
161
+ projectFactsPath: projectFactsPath || null,
162
+ });
151
163
  }
152
164
  return {
153
165
  ...contractState,
@@ -162,7 +174,10 @@ function cmdStart(args) {
162
174
  const { flags } = parseFlags(args || [], START_FLAG_DEFS);
163
175
  if (flags.help) return cmdStartHelp();
164
176
 
165
- const contractMode = Boolean(flags["from-spec"] || flags["design-brief"] || flags["reference-dir"] || flags.profile);
177
+ const contractMode = Boolean(
178
+ flags["from-spec"] || flags["design-brief"] || flags["reference-dir"] ||
179
+ flags.profile || flags["brand-tokens"] || flags["project-facts"]
180
+ );
166
181
  if (contractMode) {
167
182
  const contractResult = runDesignStartFromSpec({
168
183
  fromSpecPath: flags["from-spec"],
@@ -170,6 +185,8 @@ function cmdStart(args) {
170
185
  designBrief: flags["design-brief"],
171
186
  referenceDir: flags["reference-dir"],
172
187
  profile: flags.profile,
188
+ brandTokensPath: flags["brand-tokens"] || null,
189
+ projectFactsPath: flags["project-facts"] || null,
173
190
  });
174
191
  if (contractResult.blockers.length > 0) {
175
192
  throw new ValidationError(
@@ -216,7 +233,7 @@ function cmdStart(args) {
216
233
  console.log(`[design] Style: ${result.style}`);
217
234
  console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
218
235
  console.log("[design] No review, handoff, URL, browser, screenshot, vision, network, .sdtk/atlas, or SDTK-WIKI output was used.");
219
- console.log("[design] Next: sdtk-design prototype");
236
+ console.log("[design] Next: sdtk-design handoff");
220
237
  return 0;
221
238
  }
222
239
 
@@ -85,13 +85,21 @@ function inspectDesignStatus(projectPath) {
85
85
  if (inputContractState && inputContractState.mode === "from-spec") {
86
86
  if (Array.isArray(inputContractState.blockers) && inputContractState.blockers.length > 0) {
87
87
  nextCommand = "Provide missing explicit SPEC/design artifacts and re-run sdtk-design start --from-spec.";
88
- } else {
88
+ } else if (prototypeMissing) {
89
89
  nextCommand = "sdtk-design prototype";
90
+ } else if (!reviewExists) {
91
+ nextCommand = "sdtk-design review --artifact docs/design/prototype/index.html";
92
+ } else if (handoffMissing) {
93
+ nextCommand = "sdtk-design handoff";
94
+ } else {
95
+ nextCommand = "SDTK-CODE can consume docs/design/DESIGN_HANDOFF.md";
90
96
  }
91
97
  } else if (coreMissing.length > 0) {
92
98
  nextCommand = 'sdtk-design start --idea "<idea>"';
99
+ } else if (prototypeMissing && handoffMissing) {
100
+ nextCommand = "sdtk-design handoff";
93
101
  } else if (prototypeMissing) {
94
- nextCommand = "sdtk-design prototype";
102
+ nextCommand = "Run sdtk-design start --from-spec with explicit design inputs before sdtk-design prototype.";
95
103
  } else if (!reviewExists) {
96
104
  nextCommand = "sdtk-design review --artifact docs/design/prototype/index.html";
97
105
  } else if (handoffMissing) {