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.
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const { describeDesignPaths } = require("./design-paths");
5
+ const { ValidationError } = require("./errors");
6
+
7
+ const ECOMMERCE_PATTERNS = [
8
+ "Header and navigation shell",
9
+ "Category filter sidebar",
10
+ "Product card grid",
11
+ "Product detail spec table and gallery area",
12
+ "Search result list and no-result state",
13
+ "Cart table and quantity controls",
14
+ "Checkout stepper and summary card",
15
+ "Order history list or table",
16
+ "Order status timeline and detail summary",
17
+ "Account sidebar and view or edit form",
18
+ "Configurator wizard shell",
19
+ "Preview panel",
20
+ "BOM and material table",
21
+ "Exclude and recalculate action",
22
+ ];
23
+
24
+ const BASELINE_PATTERNS = [
25
+ "Header and navigation shell",
26
+ "Primary content section",
27
+ "Secondary support panel",
28
+ "Summary card",
29
+ "State badge and status chip",
30
+ "Primary and secondary action row",
31
+ ];
32
+
33
+ function contractTokens(profile, statePayload) {
34
+ const mappedRefs =
35
+ statePayload &&
36
+ statePayload.referenceMap &&
37
+ typeof statePayload.referenceMap.mappedCount === "number"
38
+ ? statePayload.referenceMap.mappedCount
39
+ : 0;
40
+ return {
41
+ schema: "sdtk.design.tokens.v1",
42
+ profile: profile || "generic",
43
+ color: {
44
+ surface: "#FFFFFF",
45
+ surfaceAlt: "#F7F8FA",
46
+ textPrimary: "#111827",
47
+ textMuted: "#64748B",
48
+ border: "#D8E0EA",
49
+ accentPrimary: profile === "b2b-commerce" ? "#0F766E" : "#2563EB",
50
+ accentSecondary: profile === "b2b-commerce" ? "#2563EB" : "#0F8A5F",
51
+ },
52
+ typography: {
53
+ fontFamily: "Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif",
54
+ headingWeight: 800,
55
+ bodyWeight: 400,
56
+ uiWeight: 700,
57
+ letterSpacing: 0,
58
+ },
59
+ spacing: {
60
+ xs: 4,
61
+ sm: 8,
62
+ md: 12,
63
+ lg: 16,
64
+ xl: 24,
65
+ sectionGap: 32,
66
+ },
67
+ radius: {
68
+ card: 8,
69
+ control: 8,
70
+ chip: 999,
71
+ },
72
+ metadata: {
73
+ generatedFrom: "START_INPUT_STATE",
74
+ explicitScreenCount: statePayload.screenModel.totalScreens,
75
+ mappedReferenceCount: mappedRefs,
76
+ },
77
+ };
78
+ }
79
+
80
+ function markdownLibrary(profile, statePayload) {
81
+ const mappedRefs =
82
+ statePayload &&
83
+ statePayload.referenceMap &&
84
+ typeof statePayload.referenceMap.mappedCount === "number"
85
+ ? statePayload.referenceMap.mappedCount
86
+ : 0;
87
+ const patterns = profile === "b2b-commerce" ? ECOMMERCE_PATTERNS : BASELINE_PATTERNS;
88
+ return `# Component Pattern Library
89
+
90
+ ## Contract Meta
91
+ - Profile: ${profile || "generic"}
92
+ - Source: .sdtk/design/START_INPUT_STATE.json
93
+ - Explicit screens: ${statePayload.screenModel.totalScreens}
94
+ - Mapped references: ${mappedRefs}
95
+
96
+ ## Core Pattern Set
97
+ ${patterns.map((item) => `- ${item}`).join("\n")}
98
+
99
+ ## Usage Rules
100
+ - Keep component usage screen-role driven from explicit screen model.
101
+ - Reuse token contract from docs/design/DESIGN_TOKENS.json.
102
+ - Keep customer specifics project-local; runtime library stays profile-generic.
103
+
104
+ ## Accessibility Baseline
105
+ - Interactive controls should preserve clear focus styling.
106
+ - Keep readable contrast and deterministic heading hierarchy.
107
+ - Keep touch targets at least 44px where practical in prototype guidance.
108
+ `;
109
+ }
110
+
111
+ function writeComponentContractArtifacts(projectPath, inputContractState) {
112
+ const paths = describeDesignPaths(projectPath);
113
+ if (
114
+ !inputContractState ||
115
+ inputContractState.analysisStatus !== "INPUT_CONTRACT_READY" ||
116
+ !inputContractState.screenModel ||
117
+ !Array.isArray(inputContractState.screenModel.screens) ||
118
+ inputContractState.screenModel.screens.length === 0
119
+ ) {
120
+ throw new ValidationError(
121
+ "Component and token contract generation requires INPUT_CONTRACT_READY with explicit screens."
122
+ );
123
+ }
124
+ const profile = inputContractState.profileSelection || null;
125
+ const tokensPayload = contractTokens(profile, inputContractState);
126
+ const libraryMarkdown = markdownLibrary(profile, inputContractState);
127
+
128
+ fs.writeFileSync(paths.componentPatternLibraryPath, `${libraryMarkdown}\n`, "utf-8");
129
+ fs.writeFileSync(paths.designTokensPath, `${JSON.stringify(tokensPayload, null, 2)}\n`, "utf-8");
130
+
131
+ return {
132
+ componentLibraryRelativePath: "docs/design/COMPONENT_PATTERN_LIBRARY.md",
133
+ designTokensRelativePath: "docs/design/DESIGN_TOKENS.json",
134
+ profile: profile || "generic",
135
+ patternCount: profile === "b2b-commerce" ? ECOMMERCE_PATTERNS.length : BASELINE_PATTERNS.length,
136
+ };
137
+ }
138
+
139
+ module.exports = {
140
+ writeComponentContractArtifacts,
141
+ };
142
+
@@ -6,10 +6,46 @@ const { describeDesignPaths, resolveProjectPath } = require("./design-paths");
6
6
  const { resolveProfile } = require("./design-profiles");
7
7
  const { ValidationError } = require("./errors");
8
8
 
9
+ const EVALUATION_REFERENCE_SEGMENTS = ["docs", "ui"];
10
+ const TEMPLATE_ROLES = new Set([
11
+ "home",
12
+ "category",
13
+ "product-detail",
14
+ "search",
15
+ "cart",
16
+ "checkout",
17
+ "order-history",
18
+ "order-detail",
19
+ "account-info",
20
+ "configurator-bom",
21
+ "generic",
22
+ ]);
23
+
9
24
  function toPosixRelative(basePath, targetPath) {
10
25
  return path.relative(basePath, targetPath).split(path.sep).join("/");
11
26
  }
12
27
 
28
+ function evaluationReferenceLabel() {
29
+ return EVALUATION_REFERENCE_SEGMENTS.join("/");
30
+ }
31
+
32
+ function hasEvaluationReferenceSegments(value) {
33
+ const normalized = path
34
+ .normalize(String(value || ""))
35
+ .split(/[\\/]+/)
36
+ .map((segment) => segment.toLowerCase())
37
+ .filter(Boolean);
38
+ for (let index = 0; index < normalized.length - 1; index++) {
39
+ if (
40
+ normalized[index] === EVALUATION_REFERENCE_SEGMENTS[0] &&
41
+ normalized[index + 1] === EVALUATION_REFERENCE_SEGMENTS[1]
42
+ ) {
43
+ return true;
44
+ }
45
+ }
46
+ return false;
47
+ }
48
+
13
49
  function findArtifactByPattern(rootPath, relativeDir, pattern) {
14
50
  const absoluteDir = path.join(rootPath, relativeDir);
15
51
  if (!fs.existsSync(absoluteDir) || !fs.statSync(absoluteDir).isDirectory()) {
@@ -35,7 +71,17 @@ function resolveReferenceDirectory(value, projectPath) {
35
71
  if (!value) {
36
72
  return { provided: false, status: "NOT_PROVIDED", absolutePath: null };
37
73
  }
74
+ if (hasEvaluationReferenceSegments(value)) {
75
+ throw new ValidationError(
76
+ `--reference-dir cannot point at ${evaluationReferenceLabel()}; that folder is evaluation-only and not a generation input. No project files were changed.`
77
+ );
78
+ }
38
79
  const absolutePath = path.resolve(projectPath, value);
80
+ if (hasEvaluationReferenceSegments(absolutePath)) {
81
+ throw new ValidationError(
82
+ `--reference-dir cannot point at ${evaluationReferenceLabel()}; that folder is evaluation-only and not a generation input. No project files were changed.`
83
+ );
84
+ }
39
85
  if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isDirectory()) {
40
86
  throw new ValidationError(`--reference-dir is not a valid directory: ${absolutePath}. No project files were changed.`);
41
87
  }
@@ -196,6 +242,180 @@ function toScreenId(value) {
196
242
  .replace(/^-+|-+$/g, "");
197
243
  }
198
244
 
245
+ function compactString(value) {
246
+ return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
247
+ }
248
+
249
+ function firstString(raw, fieldNames) {
250
+ for (const fieldName of fieldNames) {
251
+ const value = raw && raw[fieldName];
252
+ const compacted = compactString(value);
253
+ if (compacted) {
254
+ return compacted;
255
+ }
256
+ }
257
+ return "";
258
+ }
259
+
260
+ function normalizeStringList(value) {
261
+ if (!Array.isArray(value)) {
262
+ const single = compactString(value);
263
+ return single ? [single] : [];
264
+ }
265
+ return value
266
+ .map((item) => {
267
+ if (typeof item === "string") {
268
+ return compactString(item);
269
+ }
270
+ if (item && typeof item === "object") {
271
+ return compactString(item.title || item.name || item.label || item.id || item.description);
272
+ }
273
+ return "";
274
+ })
275
+ .filter(Boolean);
276
+ }
277
+
278
+ function firstStringList(raw, fieldNames) {
279
+ for (const fieldName of fieldNames) {
280
+ const values = normalizeStringList(raw && raw[fieldName]);
281
+ if (values.length > 0) {
282
+ return values;
283
+ }
284
+ }
285
+ return [];
286
+ }
287
+
288
+ function normalizeSectionRecord(value, index) {
289
+ if (typeof value === "string") {
290
+ const title = compactString(value);
291
+ return title
292
+ ? {
293
+ id: toScreenId(title) || `section-${index + 1}`,
294
+ title,
295
+ purpose: null,
296
+ components: [],
297
+ states: [],
298
+ interactions: [],
299
+ data_slots: [],
300
+ }
301
+ : null;
302
+ }
303
+ if (!value || typeof value !== "object") {
304
+ return null;
305
+ }
306
+ const title = firstString(value, ["title", "name", "label", "heading"]);
307
+ if (!title) {
308
+ return null;
309
+ }
310
+ return {
311
+ id: toScreenId(firstString(value, ["id", "slug"]) || title) || `section-${index + 1}`,
312
+ title,
313
+ purpose: firstString(value, ["purpose", "intent", "description"]) || null,
314
+ components: firstStringList(value, ["components", "requiredComponents", "required_components"]),
315
+ states: firstStringList(value, ["states", "requiredStates", "required_states"]),
316
+ interactions: firstStringList(value, ["interactions", "interactionNotes", "interaction_notes"]),
317
+ data_slots: firstStringList(value, ["dataSlots", "data_slots", "contentSlots", "content_slots"]),
318
+ };
319
+ }
320
+
321
+ function firstSectionList(raw) {
322
+ const sectionCandidates = [
323
+ raw && raw.sections,
324
+ raw && raw.majorSections,
325
+ raw && raw.major_sections,
326
+ raw && raw.sectionsTopToBottom,
327
+ raw && raw.sections_top_to_bottom,
328
+ ].filter(Array.isArray);
329
+ for (const candidate of sectionCandidates) {
330
+ const sections = candidate.map((item, index) => normalizeSectionRecord(item, index)).filter(Boolean);
331
+ if (sections.length > 0) {
332
+ return sections;
333
+ }
334
+ }
335
+ return [];
336
+ }
337
+
338
+ function normalizeVisualTokenUsage(value) {
339
+ const payload = value && typeof value === "object" && !Array.isArray(value) ? value : {};
340
+ return {
341
+ palette_keys: firstStringList(payload, ["palette_keys", "paletteKeys"]),
342
+ typography_keys: firstStringList(payload, ["typography_keys", "typographyKeys"]),
343
+ spacing_keys: firstStringList(payload, ["spacing_keys", "spacingKeys"]),
344
+ radius_keys: firstStringList(payload, ["radius_keys", "radiusKeys"]),
345
+ };
346
+ }
347
+
348
+ function normalizeTemplateRole(value) {
349
+ const normalized = toScreenId(value);
350
+ if (!normalized) {
351
+ return "";
352
+ }
353
+ const aliases = {
354
+ product: "product-detail",
355
+ pdp: "product-detail",
356
+ productdetail: "product-detail",
357
+ orders: "order-history",
358
+ order: "order-detail",
359
+ account: "account-info",
360
+ configurator: "configurator-bom",
361
+ bom: "configurator-bom",
362
+ "mode-b-configurator": "configurator-bom",
363
+ };
364
+ const role = aliases[normalized] || normalized;
365
+ return TEMPLATE_ROLES.has(role) ? role : "";
366
+ }
367
+
368
+ function inferTemplateRole(raw, screenId, title) {
369
+ const explicit = normalizeTemplateRole(
370
+ firstString(raw, ["templateRole", "template_role", "screenRole", "screen_role", "role"])
371
+ );
372
+ if (explicit) {
373
+ return explicit;
374
+ }
375
+ const combined = toScreenId(`${screenId} ${title}`);
376
+ const roleMatchers = [
377
+ ["product-detail", ["product-detail", "productdetail", "pdp"]],
378
+ ["order-history", ["order-history", "orderhistory", "orders"]],
379
+ ["order-detail", ["order-detail", "orderdetail"]],
380
+ ["account-info", ["account-info", "accountinfo", "account"]],
381
+ ["configurator-bom", ["configurator", "bom", "mode-b"]],
382
+ ["checkout", ["checkout"]],
383
+ ["category", ["category", "catalog"]],
384
+ ["search", ["search"]],
385
+ ["cart", ["cart"]],
386
+ ["home", ["home", "landing"]],
387
+ ];
388
+ for (const [role, needles] of roleMatchers) {
389
+ if (needles.some((needle) => combined.includes(needle))) {
390
+ return role;
391
+ }
392
+ }
393
+ return "generic";
394
+ }
395
+
396
+ function normalizeSourceEvidence(raw, sourceArtifact) {
397
+ const payload =
398
+ raw && raw.sourceEvidence && typeof raw.sourceEvidence === "object"
399
+ ? raw.sourceEvidence
400
+ : raw && raw.source_evidence && typeof raw.source_evidence === "object"
401
+ ? raw.source_evidence
402
+ : {};
403
+ return {
404
+ source_artifact: firstString(payload, ["source_artifact", "sourceArtifact"]) || sourceArtifact || null,
405
+ source_status: firstString(raw, ["sourceStatus", "source_status"]) || "explicit",
406
+ source_quote: firstString(raw, ["source_quote", "sourceQuote"]) || firstString(payload, ["source_quote", "sourceQuote"]) || null,
407
+ source_line_range: Array.isArray(raw && raw.source_line_range)
408
+ ? raw.source_line_range
409
+ : Array.isArray(raw && raw.sourceLineRange)
410
+ ? raw.sourceLineRange
411
+ : Array.isArray(payload.source_line_range)
412
+ ? payload.source_line_range
413
+ : Array.isArray(payload.sourceLineRange)
414
+ ? payload.sourceLineRange
415
+ : null,
416
+ };
417
+ }
418
+
199
419
  function normalizedScreenRecord(raw, sourceArtifact, fallbackTitle) {
200
420
  const title = String(
201
421
  (raw && (raw.title || raw.name || raw.screen || raw.label || raw.id)) || fallbackTitle || ""
@@ -204,25 +424,54 @@ function normalizedScreenRecord(raw, sourceArtifact, fallbackTitle) {
204
424
  return null;
205
425
  }
206
426
  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);
427
+ const route = firstString(raw, ["route", "path", "url"]);
428
+ const purpose = firstString(raw, ["purpose", "screenPurpose", "screen_purpose", "goal"]);
429
+ const userIntent = firstString(raw, ["userIntent", "user_intent", "intent"]);
430
+ const primaryAction = firstString(raw, ["primaryAction", "primary_action", "cta"]);
431
+ const sections = firstSectionList(raw);
432
+ const majorSections = sections.map((section) => section.title);
433
+ const layoutModel = firstString(raw, ["layoutModel", "layout_model"]);
434
+ const density = firstString(raw, ["density", "layoutDensity", "layout_density"]);
435
+ const requiredComponents = firstStringList(raw, ["components", "requiredComponents", "required_components", "componentRequirements", "component_requirements"]);
436
+ const requiredStates = firstStringList(raw, ["states", "requiredStates", "required_states"]);
437
+ const interactions = firstStringList(raw, ["interactions", "interactionNotes", "interaction_notes"]);
438
+ const dataSlots = firstStringList(raw, ["dataSlots", "data_slots", "contentSlots", "content_slots"]);
439
+ const accessibilityNotes = firstStringList(raw, ["accessibilityNotes", "accessibility_notes", "a11y"]);
440
+ const visualTokenUsage = normalizeVisualTokenUsage((raw && (raw.visualTokenUsage || raw.visual_token_usage)) || {});
441
+ const acceptanceCriteria = firstStringList(raw, ["acceptanceCriteria", "acceptance_criteria", "criteria"]);
442
+ const templateRole = inferTemplateRole(raw || {}, screenId, title);
443
+ const sourceEvidence = normalizeSourceEvidence(raw || {}, sourceArtifact);
444
+ const populatedCount =
445
+ [route, purpose, userIntent, primaryAction, layoutModel, density].filter(Boolean).length +
446
+ (sections.length > 0 ? 1 : 0) +
447
+ (requiredComponents.length > 0 ? 1 : 0) +
448
+ (requiredStates.length > 0 ? 1 : 0) +
449
+ (interactions.length > 0 ? 1 : 0) +
450
+ (dataSlots.length > 0 ? 1 : 0) +
451
+ (accessibilityNotes.length > 0 ? 1 : 0) +
452
+ (acceptanceCriteria.length > 0 ? 1 : 0) +
453
+ (sourceEvidence.source_quote ? 1 : 0);
217
454
  const confidence = populatedCount >= 3 ? "high" : populatedCount >= 1 ? "medium" : "low";
218
455
  return {
219
456
  screenId,
220
457
  title,
221
458
  route: route || null,
222
459
  purpose: purpose || null,
460
+ userIntent: userIntent || null,
223
461
  primaryAction: primaryAction || null,
462
+ sections,
224
463
  majorSections,
464
+ layoutModel: layoutModel || null,
465
+ density: density || null,
466
+ requiredComponents,
225
467
  requiredStates,
468
+ interactions,
469
+ dataSlots,
470
+ accessibilityNotes,
471
+ visualTokenUsage,
472
+ acceptanceCriteria,
473
+ templateRole,
474
+ sourceEvidence,
226
475
  sourceArtifact,
227
476
  sourceStatus: "explicit",
228
477
  confidence,
@@ -405,7 +654,18 @@ function buildInputContractState({
405
654
  artifacts: artifactSummary,
406
655
  screenModel: {
407
656
  totalScreens: screens.length,
408
- missingMetadataCount: screens.filter((screen) => !screen.route || !screen.purpose || !screen.primaryAction).length,
657
+ missingMetadataCount: screens.filter(
658
+ (screen) =>
659
+ !screen.route ||
660
+ !screen.purpose ||
661
+ !screen.userIntent ||
662
+ screen.sections.length === 0 ||
663
+ screen.requiredComponents.length === 0 ||
664
+ screen.requiredStates.length === 0 ||
665
+ screen.interactions.length === 0 ||
666
+ screen.dataSlots.length === 0 ||
667
+ screen.acceptanceCriteria.length === 0
668
+ ).length,
409
669
  legacyFallbackUsed: false,
410
670
  readiness: screens.length > 0 ? "MULTI_SCREEN_READY" : "NEEDS_EXPLICIT_SCREEN_INPUT",
411
671
  screens,
@@ -6,15 +6,23 @@ const DESIGN_DOCS_RELATIVE = path.join("docs", "design");
6
6
  const DESIGN_WIREFRAMES_RELATIVE = path.join("docs", "design", "wireframes");
7
7
  const DESIGN_REVIEWS_RELATIVE = path.join("docs", "design", "reviews");
8
8
  const DESIGN_PROTOTYPE_RELATIVE = path.join("docs", "design", "prototype");
9
+ const DESIGN_PROTOTYPE_SCREENS_RELATIVE = path.join("docs", "design", "prototype", "screens");
10
+ const DESIGN_PROTOTYPE_ASSETS_RELATIVE = path.join("docs", "design", "prototype", "assets");
11
+ const DESIGN_SCREENS_RELATIVE = path.join("docs", "design", "screens");
9
12
  const DESIGN_PROTOTYPE_INDEX_RELATIVE = path.join("docs", "design", "prototype", "index.html");
13
+ const DESIGN_PROTOTYPE_CSS_RELATIVE = path.join("docs", "design", "prototype", "assets", "prototype.css");
14
+ const DESIGN_PROTOTYPE_JS_RELATIVE = path.join("docs", "design", "prototype", "assets", "prototype.js");
10
15
  const DESIGN_README_RELATIVE = path.join("docs", "design", "README.md");
11
16
  const DESIGN_BRIEF_RELATIVE = path.join("docs", "design", "DESIGN_BRIEF.md");
12
17
  const DESIGN_SCREEN_MAP_RELATIVE = path.join("docs", "design", "SCREEN_MAP.md");
13
18
  const DESIGN_SYSTEM_RELATIVE = path.join("docs", "design", "DESIGN_SYSTEM.md");
19
+ const DESIGN_COMPONENT_LIBRARY_RELATIVE = path.join("docs", "design", "COMPONENT_PATTERN_LIBRARY.md");
20
+ const DESIGN_TOKENS_RELATIVE = path.join("docs", "design", "DESIGN_TOKENS.json");
14
21
  const DESIGN_HANDOFF_RELATIVE = path.join("docs", "design", "DESIGN_HANDOFF.md");
15
22
  const DESIGN_STATE_RELATIVE = path.join(".sdtk", "design");
16
23
  const DESIGN_MANIFEST_RELATIVE = path.join(".sdtk", "design", "manifest.json");
17
24
  const DESIGN_START_INPUT_STATE_RELATIVE = path.join(".sdtk", "design", "START_INPUT_STATE.json");
25
+ const DESIGN_SCREEN_BRIEFS_STATE_RELATIVE = path.join(".sdtk", "design", "screen-briefs");
18
26
 
19
27
  function resolveProjectPath(projectPath) {
20
28
  return path.resolve(projectPath || process.cwd());
@@ -48,31 +56,47 @@ function describeDesignPaths(projectPath) {
48
56
  wireframesPath: path.join(root, DESIGN_WIREFRAMES_RELATIVE),
49
57
  reviewsPath: path.join(root, DESIGN_REVIEWS_RELATIVE),
50
58
  prototypePath: path.join(root, DESIGN_PROTOTYPE_RELATIVE),
59
+ prototypeScreensPath: path.join(root, DESIGN_PROTOTYPE_SCREENS_RELATIVE),
60
+ prototypeAssetsPath: path.join(root, DESIGN_PROTOTYPE_ASSETS_RELATIVE),
61
+ screensPath: path.join(root, DESIGN_SCREENS_RELATIVE),
51
62
  prototypeIndexPath: path.join(root, DESIGN_PROTOTYPE_INDEX_RELATIVE),
63
+ prototypeCssPath: path.join(root, DESIGN_PROTOTYPE_CSS_RELATIVE),
64
+ prototypeJsPath: path.join(root, DESIGN_PROTOTYPE_JS_RELATIVE),
52
65
  designReadmePath: path.join(root, DESIGN_README_RELATIVE),
53
66
  designBriefPath: path.join(root, DESIGN_BRIEF_RELATIVE),
54
67
  screenMapPath: path.join(root, DESIGN_SCREEN_MAP_RELATIVE),
55
68
  designSystemPath: path.join(root, DESIGN_SYSTEM_RELATIVE),
69
+ componentPatternLibraryPath: path.join(root, DESIGN_COMPONENT_LIBRARY_RELATIVE),
70
+ designTokensPath: path.join(root, DESIGN_TOKENS_RELATIVE),
56
71
  designHandoffPath: path.join(root, DESIGN_HANDOFF_RELATIVE),
57
72
  designStatePath: path.join(root, DESIGN_STATE_RELATIVE),
58
73
  manifestPath: path.join(root, DESIGN_MANIFEST_RELATIVE),
59
74
  designStartInputStatePath: path.join(root, DESIGN_START_INPUT_STATE_RELATIVE),
75
+ designScreenBriefsStatePath: path.join(root, DESIGN_SCREEN_BRIEFS_STATE_RELATIVE),
60
76
  };
61
77
  }
62
78
 
63
79
  module.exports = {
64
80
  DESIGN_BRIEF_RELATIVE,
81
+ DESIGN_COMPONENT_LIBRARY_RELATIVE,
65
82
  DESIGN_DOCS_RELATIVE,
66
83
  DESIGN_HANDOFF_RELATIVE,
67
84
  DESIGN_MANIFEST_RELATIVE,
68
85
  DESIGN_PROTOTYPE_INDEX_RELATIVE,
86
+ DESIGN_PROTOTYPE_ASSETS_RELATIVE,
87
+ DESIGN_PROTOTYPE_CSS_RELATIVE,
88
+ DESIGN_PROTOTYPE_JS_RELATIVE,
69
89
  DESIGN_PROTOTYPE_RELATIVE,
90
+ DESIGN_PROTOTYPE_SCREENS_RELATIVE,
91
+ DESIGN_SCREEN_BRIEFS_STATE_RELATIVE,
92
+ DESIGN_SCREENS_RELATIVE,
70
93
  DESIGN_README_RELATIVE,
71
94
  DESIGN_REVIEWS_RELATIVE,
72
95
  DESIGN_SCREEN_MAP_RELATIVE,
73
96
  DESIGN_START_INPUT_STATE_RELATIVE,
74
97
  DESIGN_STATE_RELATIVE,
75
98
  DESIGN_SYSTEM_RELATIVE,
99
+ DESIGN_TOKENS_RELATIVE,
76
100
  DESIGN_WIREFRAMES_RELATIVE,
77
101
  assertProjectLocalPath,
78
102
  describeDesignPaths,
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ function slugify(value, fallback = "item") {
7
+ const slug = String(value == null ? "" : value)
8
+ .trim()
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9]+/g, "-")
11
+ .replace(/^-+|-+$/g, "");
12
+ return slug || fallback;
13
+ }
14
+
15
+ function normalizeSlot(slot, fallbackId) {
16
+ if (slot && typeof slot === "object") {
17
+ const id = slugify(slot.id || slot.key || slot.name || slot.label || fallbackId, fallbackId);
18
+ return {
19
+ id,
20
+ label: String(slot.label || slot.name || slot.title || slot.id || id),
21
+ };
22
+ }
23
+ const label = String(slot == null ? fallbackId : slot);
24
+ return { id: slugify(label, fallbackId), label };
25
+ }
26
+
27
+ function normalizeComponent(component, fallbackId) {
28
+ if (component && typeof component === "object") {
29
+ const id = slugify(component.id || component.type || component.name || component.label || fallbackId, fallbackId);
30
+ return {
31
+ id,
32
+ label: String(component.label || component.name || component.title || component.type || component.id || id),
33
+ };
34
+ }
35
+ const label = String(component == null ? fallbackId : component);
36
+ return { id: slugify(label, fallbackId), label };
37
+ }
38
+
39
+ function normalizeSection(section, index) {
40
+ const source = section && typeof section === "object" ? section : {};
41
+ const title = source.title || source.name || `Section ${index + 1}`;
42
+ const id = slugify(source.id || title, `section-${index + 1}`);
43
+ const slots = source.data_slots || source.dataSlots || [];
44
+ const components = source.components || [];
45
+ return {
46
+ id,
47
+ title: String(title),
48
+ purpose: String(source.purpose || source.description || "Brief-defined section."),
49
+ components: (Array.isArray(components) ? components : []).map((component, componentIndex) =>
50
+ normalizeComponent(component, `${id}-component-${componentIndex + 1}`)
51
+ ),
52
+ states: Array.isArray(source.states) ? source.states.map((state) => String(state)) : [],
53
+ interactions: Array.isArray(source.interactions) ? source.interactions.map((interaction) => String(interaction)) : [],
54
+ data_slots: (Array.isArray(slots) ? slots : []).map((slot, slotIndex) => normalizeSlot(slot, `${id}-slot-${slotIndex + 1}`)),
55
+ };
56
+ }
57
+
58
+ function normalizeBrief(raw, filePath) {
59
+ const source = raw && typeof raw === "object" ? raw : {};
60
+ const screenId = slugify(source.screen_id || source.screenId || source.id || path.basename(filePath || "screen", ".json"), "screen");
61
+ const sections = Array.isArray(source.sections) ? source.sections.map(normalizeSection) : [];
62
+ const topLevelSlots = source.data_slots || source.dataSlots || [];
63
+ const topLevelComponents = source.components || [];
64
+ return {
65
+ screen_id: screenId,
66
+ screen_title: String(source.screen_title || source.screenTitle || source.title || screenId),
67
+ purpose: String(source.purpose || ""),
68
+ user_intent: String(source.user_intent || source.userIntent || ""),
69
+ route: String(source.route || ""),
70
+ primary_action: String(source.primary_action || source.primaryAction || "Continue"),
71
+ template_role: source.template_role || source.templateRole || null,
72
+ sections,
73
+ components: (Array.isArray(topLevelComponents) ? topLevelComponents : []).map((component, index) =>
74
+ normalizeComponent(component, `${screenId}-component-${index + 1}`)
75
+ ),
76
+ states: Array.isArray(source.states || source.required_states || source.requiredStates)
77
+ ? (source.states || source.required_states || source.requiredStates).map((state) => String(state))
78
+ : [],
79
+ interactions: Array.isArray(source.interactions) ? source.interactions.map((interaction) => String(interaction)) : [],
80
+ data_slots: (Array.isArray(topLevelSlots) ? topLevelSlots : []).map((slot, index) => normalizeSlot(slot, `${screenId}-slot-${index + 1}`)),
81
+ acceptance_criteria: Array.isArray(source.acceptance_criteria || source.acceptanceCriteria)
82
+ ? (source.acceptance_criteria || source.acceptanceCriteria).map((item) => String(item))
83
+ : [],
84
+ source_evidence: source.source_evidence || source.sourceEvidence || null,
85
+ file_path: filePath,
86
+ };
87
+ }
88
+
89
+ function loadPrototypeBriefIndex({ paths }) {
90
+ const briefsByScreenId = new Map();
91
+ const findings = [];
92
+ const root = paths && paths.designScreenBriefsStatePath ? paths.designScreenBriefsStatePath : null;
93
+ if (!root || !fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
94
+ return {
95
+ briefsByScreenId,
96
+ findings: [{ code: "BRIEF_DIR_MISSING", message: ".sdtk/design/screen-briefs directory is missing." }],
97
+ };
98
+ }
99
+ for (const filePath of fs.readdirSync(root).filter((name) => name.toLowerCase().endsWith(".json")).sort()) {
100
+ const absolutePath = path.join(root, filePath);
101
+ try {
102
+ const brief = normalizeBrief(JSON.parse(fs.readFileSync(absolutePath, "utf-8")), absolutePath);
103
+ if (briefsByScreenId.has(brief.screen_id)) {
104
+ findings.push({ screenId: brief.screen_id, code: "BRIEF_DUPLICATE", message: `Duplicate brief skipped: ${filePath}` });
105
+ } else {
106
+ briefsByScreenId.set(brief.screen_id, brief);
107
+ }
108
+ } catch (err) {
109
+ findings.push({ code: "BRIEF_INVALID_JSON", message: `${filePath}: ${err.message}` });
110
+ }
111
+ }
112
+ return { briefsByScreenId, findings };
113
+ }
114
+
115
+ function briefForScreen({ screen, briefIndex }) {
116
+ if (!screen || !briefIndex || !briefIndex.briefsByScreenId) return null;
117
+ return briefIndex.briefsByScreenId.get(slugify(screen.screenId || screen.id || screen.title, "screen")) || null;
118
+ }
119
+
120
+ module.exports = {
121
+ briefForScreen,
122
+ loadPrototypeBriefIndex,
123
+ normalizeBrief,
124
+ slugify,
125
+ };