sdtk-design-kit 0.1.0 → 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.
@@ -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
+ };
@@ -5,6 +5,8 @@ const path = require("path");
5
5
  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
+ const DESIGN_PROTOTYPE_RELATIVE = path.join("docs", "design", "prototype");
9
+ const DESIGN_PROTOTYPE_INDEX_RELATIVE = path.join("docs", "design", "prototype", "index.html");
8
10
  const DESIGN_README_RELATIVE = path.join("docs", "design", "README.md");
9
11
  const DESIGN_BRIEF_RELATIVE = path.join("docs", "design", "DESIGN_BRIEF.md");
10
12
  const DESIGN_SCREEN_MAP_RELATIVE = path.join("docs", "design", "SCREEN_MAP.md");
@@ -12,6 +14,7 @@ const DESIGN_SYSTEM_RELATIVE = path.join("docs", "design", "DESIGN_SYSTEM.md");
12
14
  const DESIGN_HANDOFF_RELATIVE = path.join("docs", "design", "DESIGN_HANDOFF.md");
13
15
  const DESIGN_STATE_RELATIVE = path.join(".sdtk", "design");
14
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");
15
18
 
16
19
  function resolveProjectPath(projectPath) {
17
20
  return path.resolve(projectPath || process.cwd());
@@ -44,6 +47,8 @@ function describeDesignPaths(projectPath) {
44
47
  designDocsPath: path.join(root, DESIGN_DOCS_RELATIVE),
45
48
  wireframesPath: path.join(root, DESIGN_WIREFRAMES_RELATIVE),
46
49
  reviewsPath: path.join(root, DESIGN_REVIEWS_RELATIVE),
50
+ prototypePath: path.join(root, DESIGN_PROTOTYPE_RELATIVE),
51
+ prototypeIndexPath: path.join(root, DESIGN_PROTOTYPE_INDEX_RELATIVE),
47
52
  designReadmePath: path.join(root, DESIGN_README_RELATIVE),
48
53
  designBriefPath: path.join(root, DESIGN_BRIEF_RELATIVE),
49
54
  screenMapPath: path.join(root, DESIGN_SCREEN_MAP_RELATIVE),
@@ -51,6 +56,7 @@ function describeDesignPaths(projectPath) {
51
56
  designHandoffPath: path.join(root, DESIGN_HANDOFF_RELATIVE),
52
57
  designStatePath: path.join(root, DESIGN_STATE_RELATIVE),
53
58
  manifestPath: path.join(root, DESIGN_MANIFEST_RELATIVE),
59
+ designStartInputStatePath: path.join(root, DESIGN_START_INPUT_STATE_RELATIVE),
54
60
  };
55
61
  }
56
62
 
@@ -59,9 +65,12 @@ module.exports = {
59
65
  DESIGN_DOCS_RELATIVE,
60
66
  DESIGN_HANDOFF_RELATIVE,
61
67
  DESIGN_MANIFEST_RELATIVE,
68
+ DESIGN_PROTOTYPE_INDEX_RELATIVE,
69
+ DESIGN_PROTOTYPE_RELATIVE,
62
70
  DESIGN_README_RELATIVE,
63
71
  DESIGN_REVIEWS_RELATIVE,
64
72
  DESIGN_SCREEN_MAP_RELATIVE,
73
+ DESIGN_START_INPUT_STATE_RELATIVE,
65
74
  DESIGN_STATE_RELATIVE,
66
75
  DESIGN_SYSTEM_RELATIVE,
67
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
+ };
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+
3
+ function normalizeText(value) {
4
+ return String(value || "").replace(/\s+/g, " ").trim();
5
+ }
6
+
7
+ function extractSourceIdea(briefContent) {
8
+ const match = String(briefContent || "").match(/## Source Idea\s+([\s\S]*?)(?:\n## |\s*$)/i);
9
+ return normalizeText(match ? match[1] : "");
10
+ }
11
+
12
+ function extractProductName(sourceIdea, crmLike) {
13
+ const clean = normalizeText(sourceIdea);
14
+ const namedBuild = clean.match(/\bbuild\s+([A-Z][A-Za-z0-9-]{2,40})(?:,|\s+for|\s+to|\s+that|\s+-)/);
15
+ if (namedBuild) return namedBuild[1];
16
+ return crmLike ? "Client Workspace" : "MVP Workspace";
17
+ }
18
+
19
+ function slugify(value) {
20
+ const slug = normalizeText(value)
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]+/g, "-")
23
+ .replace(/^-+|-+$/g, "");
24
+ return slug || "mvp-workspace";
25
+ }
26
+
27
+ function inferDomainProfile({ briefContent = "", screenMapContent = "", designSystemContent = "", wireframeContents = [] } = {}) {
28
+ const sourceIdea = extractSourceIdea(briefContent);
29
+ const combined = normalizeText([sourceIdea, briefContent, screenMapContent, designSystemContent, ...wireframeContents].join(" "));
30
+ const domainSource = normalizeText([sourceIdea, briefContent].join(" "));
31
+ const lower = domainSource.toLowerCase();
32
+ const crmLike = /\b(crm|lead|leads|pipeline|follow-up|follow up|consultant|client conversation|opportunity)\b/.test(lower);
33
+ const productName = extractProductName(sourceIdea || combined, crmLike);
34
+ const slug = slugify(productName);
35
+
36
+ if (crmLike) {
37
+ return {
38
+ kind: "crm",
39
+ productName,
40
+ slug,
41
+ targetUser: "the primary user",
42
+ promise: "Track leads, notes, follow-ups, and simple pipeline status without enterprise CRM weight.",
43
+ setupLabel: "Workspace",
44
+ setupNameLabel: "Workspace name",
45
+ setupNameValue: `${productName} Consulting`,
46
+ primaryAction: "Create workspace",
47
+ secondaryAction: "Preview dashboard",
48
+ itemSingular: "lead",
49
+ itemPlural: "leads",
50
+ itemLabel: "Lead",
51
+ itemNameLabel: "Name",
52
+ itemNameValue: "Avery Lee",
53
+ itemContextLabel: "Next follow-up",
54
+ itemContextValue: "2026-05-27",
55
+ itemAction: "Add lead",
56
+ saveAction: "Save lead",
57
+ collectionSurface: "lead list",
58
+ dashboardTitle: "Pipeline dashboard with next actions visible.",
59
+ emptyState: "Empty state: when no leads exist, keep the first-create CTA prominent.",
60
+ statusTaxonomy: ["New", "Contacted", "Proposal", "Won", "Lost"],
61
+ statusExample: "Proposal",
62
+ metricLabels: ["Active leads", "Due follow-ups", "Proposal value", "Won this month"],
63
+ metricValues: ["12", "4", "$18k", "3"],
64
+ modelName: "Lead",
65
+ storageKeys: [`${slug}.workspace`, `${slug}.leads`],
66
+ nonGoals: "Enterprise CRM automation, complex analytics, billing, or multi-team administration.",
67
+ };
68
+ }
69
+
70
+ return {
71
+ kind: "generic",
72
+ productName,
73
+ slug,
74
+ targetUser: "the primary user",
75
+ promise: "Turn the core MVP workflow into a clear launch, setup, and dashboard experience.",
76
+ setupLabel: "Workspace",
77
+ setupNameLabel: "Workspace name",
78
+ setupNameValue: `${productName} Team`,
79
+ primaryAction: "Start setup",
80
+ secondaryAction: "Preview dashboard",
81
+ itemSingular: "work item",
82
+ itemPlural: "work items",
83
+ itemLabel: "Work item",
84
+ itemNameLabel: "Item name",
85
+ itemNameValue: "First workflow",
86
+ itemContextLabel: "Next action",
87
+ itemContextValue: "Review progress",
88
+ itemAction: "Add item",
89
+ saveAction: "Save item",
90
+ collectionSurface: "work queue",
91
+ dashboardTitle: "Dashboard with current work and next actions visible.",
92
+ emptyState: "Empty state: when no work items exist, keep the first-create CTA prominent.",
93
+ statusTaxonomy: ["New", "In Progress", "Review", "Done"],
94
+ statusExample: "In Progress",
95
+ metricLabels: ["Active items", "Due actions", "In review", "Completed"],
96
+ metricValues: ["8", "3", "2", "5"],
97
+ modelName: "WorkItem",
98
+ storageKeys: [`${slug}.workspace`, `${slug}.items`],
99
+ nonGoals: "Enterprise workflow automation, advanced analytics, billing, or multi-role administration.",
100
+ };
101
+ }
102
+
103
+ module.exports = {
104
+ extractSourceIdea,
105
+ inferDomainProfile,
106
+ normalizeText,
107
+ slugify,
108
+ };