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.
- package/package.json +1 -1
- package/src/commands/handoff.js +357 -203
- package/src/commands/help.js +2 -2
- package/src/commands/prototype.js +291 -18
- package/src/commands/review.js +298 -1
- package/src/commands/start.js +23 -1
- package/src/lib/anti-slop-lint.js +210 -0
- package/src/lib/component-contract.js +142 -0
- package/src/lib/design-input-contract.js +271 -11
- package/src/lib/design-paths.js +24 -0
- package/src/lib/prototype-briefs.js +125 -0
- package/src/lib/prototype-component-map.js +219 -0
- package/src/lib/prototype-density.js +377 -0
- package/src/lib/prototype-renderer.js +382 -0
- package/src/lib/screen-briefs.js +340 -0
|
@@ -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
|
|
208
|
-
const purpose = raw
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
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(
|
|
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,
|
package/src/lib/design-paths.js
CHANGED
|
@@ -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
|
+
};
|