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 +1 -1
- package/src/commands/help.js +3 -0
- package/src/commands/prototype.js +172 -21
- package/src/commands/start.js +83 -8
- package/src/commands/status.js +37 -1
- package/src/lib/design-input-contract.js +423 -0
- package/src/lib/design-paths.js +3 -0
- package/src/lib/design-profiles.js +58 -0
package/package.json
CHANGED
package/src/commands/help.js
CHANGED
|
@@ -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
|
|
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 =
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
key
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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.");
|
package/src/commands/start.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
};
|
package/src/commands/status.js
CHANGED
|
@@ -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 (
|
|
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
|
+
};
|
package/src/lib/design-paths.js
CHANGED
|
@@ -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
|
+
};
|