sdtk-design-kit 0.1.2 → 0.2.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.
@@ -6,6 +6,7 @@ const { parseFlags } = require("../lib/args");
6
6
  const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths");
7
7
  const { inferDomainProfile } = require("../lib/domain-profile");
8
8
  const { ValidationError } = require("../lib/errors");
9
+ const { renderMultiPagePrototype, screenFileName } = require("../lib/prototype-renderer");
9
10
  const { DEFAULT_STYLE, availableStyleNames, resolveStyleName } = require("../lib/style-presets");
10
11
 
11
12
  const PROTOTYPE_FLAG_DEFS = {
@@ -101,16 +102,38 @@ Reads:
101
102
  .sdtk/design/START_INPUT_STATE.json (optional; enables explicit multi-screen rendering when ready)
102
103
 
103
104
  Creates:
104
- docs/design/prototype/index.html
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
105
112
 
106
113
  Safety:
107
114
  Local files only.
108
115
  Existing prototype index is not overwritten unless --force is explicit.
109
116
  No production app code outside docs/design/prototype.
110
- No JavaScript runtime, server, network call, .sdtk/atlas, or SDTK-WIKI output mutation.`);
117
+ No JavaScript app runtime, server, network call, .sdtk/atlas, or SDTK-WIKI output mutation.`);
111
118
  return 0;
112
119
  }
113
120
 
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";
128
+ }
129
+ if (raw === "single" || raw === "multi") {
130
+ return raw;
131
+ }
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
+
114
137
  function wireframePath(paths, fileName) {
115
138
  return path.join(paths.wireframesPath, fileName);
116
139
  }
@@ -309,7 +332,192 @@ function renderPrimitiveChips(primitiveNames) {
309
332
 
310
333
  function renderStateChips(requiredStates) {
311
334
  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>`;
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>`;
339
+ }
340
+
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
+ }
445
+ }
446
+
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
454
+ .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>`;
472
+ })
473
+ .join("");
474
+
475
+ return `<!doctype html>
476
+ <html lang="en">
477
+ <head>
478
+ <meta charset="utf-8">
479
+ <meta name="viewport" content="width=device-width, initial-scale=1">
480
+ <title>SDTK-DESIGN High-Fidelity Prototype</title>
481
+ <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; } }
503
+ </style>
504
+ </head>
505
+ <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>
515
+ </header>
516
+ ${sections}
517
+ </main>
518
+ </body>
519
+ </html>
520
+ `;
313
521
  }
314
522
 
315
523
  function multiScreenPrototypeContent({ style, statePayload }) {
@@ -429,8 +637,16 @@ function runDesignPrototype({ projectPath, force = false, style }) {
429
637
  const list = missing.map((target) => target.relativePath).join(", ");
430
638
  throw new ValidationError(`Missing required design artifacts: ${list}. Run sdtk-design start --idea "<idea>" first. No project files were changed.`);
431
639
  }
432
- if (fs.existsSync(paths.prototypeIndexPath) && !force) {
433
- throw new ValidationError("docs/design/prototype/index.html already exists. Re-run with --force to replace this managed prototype.");
640
+ const prototypeMode = resolvePrototypeMode(canUseMultiScreen);
641
+ const existingManagedPrototype = managedPrototypeTargets(paths, inputContractState, prototypeMode).filter((target) =>
642
+ fs.existsSync(target.filePath)
643
+ );
644
+ if (existingManagedPrototype.length > 0 && !force) {
645
+ throw new ValidationError(
646
+ `docs/design/prototype/index.html already exists. Managed prototype output already exists: ${existingManagedPrototype
647
+ .map((target) => target.relativePath)
648
+ .join(", ")}. Re-run with --force to replace managed prototype outputs.`
649
+ );
434
650
  }
435
651
 
436
652
  const designSystemContent = fs.existsSync(paths.designSystemPath) ? fs.readFileSync(paths.designSystemPath, "utf-8") : "";
@@ -448,29 +664,61 @@ function runDesignPrototype({ projectPath, force = false, style }) {
448
664
  : wireframePath(paths, "DASHBOARD.md");
449
665
  return fs.readFileSync(filePath, "utf-8");
450
666
  });
451
- const content = canUseMultiScreen
452
- ? multiScreenPrototypeContent({
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({
453
681
  style: styleName,
454
682
  statePayload: inputContractState,
455
- })
456
- : prototypeContent({
683
+ contractBundle: bundle,
684
+ });
685
+ } else {
686
+ content = multiScreenPrototypeContent({
457
687
  style: styleName,
458
- briefContent: fs.readFileSync(paths.designBriefPath, "utf-8"),
459
- screenMapContent: fs.readFileSync(paths.screenMapPath, "utf-8"),
460
- designSystemContent,
461
- wireframeContents,
688
+ statePayload: inputContractState,
462
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
+ }
463
700
 
464
- fs.mkdirSync(path.dirname(paths.prototypeIndexPath), { recursive: true });
465
- fs.writeFileSync(paths.prototypeIndexPath, content, "utf-8");
701
+ if (!renderResult) {
702
+ fs.mkdirSync(path.dirname(paths.prototypeIndexPath), { recursive: true });
703
+ fs.writeFileSync(paths.prototypeIndexPath, content, "utf-8");
704
+ }
466
705
 
467
706
  return {
468
707
  projectPath: resolvedProjectPath,
469
708
  relativePrototypePath: "docs/design/prototype/index.html",
470
709
  forced: Boolean(force),
471
710
  style: styleName,
472
- mode: canUseMultiScreen ? "multi-screen" : "simple",
711
+ mode: renderResult
712
+ ? renderResult.mode
713
+ : canUseMultiScreen
714
+ ? bundle && bundle.ready
715
+ ? "high-fidelity-single-file"
716
+ : "multi-screen"
717
+ : "simple",
473
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,
474
722
  };
475
723
  }
476
724
 
@@ -486,12 +734,37 @@ function cmdPrototype(args) {
486
734
 
487
735
  console.log(`[design] Wrote ${result.relativePrototypePath}: ${result.projectPath}`);
488
736
  console.log(`[design] Style: ${result.style}`);
489
- console.log(`[design] Mode: ${result.mode} (${result.screenCount} screen section(s))`);
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
+ }
490
763
  console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
491
764
  console.log("[design] Prototype mode: static HTML/CSS preview only.");
492
765
  console.log("[design] No app code outside docs/design/prototype, server, network, .sdtk/atlas, or SDTK-WIKI output was modified.");
493
766
  console.log("[design] Next: sdtk-design review --artifact docs/design/prototype/index.html");
494
- return 0;
767
+ return result.densityReport && result.densityReport.mode !== "bytes" && !result.densityReport.pass ? 1 : 0;
495
768
  }
496
769
 
497
770
  module.exports = {