stitch-forge 0.3.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.
Files changed (145) hide show
  1. package/.claude/skills/forge-build/SKILL.md +79 -0
  2. package/.claude/skills/forge-design/SKILL.md +64 -0
  3. package/.claude/skills/forge-discover/SKILL.md +139 -0
  4. package/.claude/skills/forge-generate/SKILL.md +77 -0
  5. package/.claude/skills/forge-preview/SKILL.md +26 -0
  6. package/.claude/skills/forge-research/SKILL.md +42 -0
  7. package/.claude/skills/forge-sync/SKILL.md +45 -0
  8. package/DESIGN.md +113 -0
  9. package/LICENSE +21 -0
  10. package/README.es.md +242 -0
  11. package/README.md +242 -0
  12. package/dist/adapters/astro.d.ts +8 -0
  13. package/dist/adapters/astro.js +24 -0
  14. package/dist/adapters/astro.js.map +1 -0
  15. package/dist/adapters/index.d.ts +3 -0
  16. package/dist/adapters/index.js +9 -0
  17. package/dist/adapters/index.js.map +1 -0
  18. package/dist/adapters/nextjs.d.ts +7 -0
  19. package/dist/adapters/nextjs.js +136 -0
  20. package/dist/adapters/nextjs.js.map +1 -0
  21. package/dist/adapters/static.d.ts +7 -0
  22. package/dist/adapters/static.js +43 -0
  23. package/dist/adapters/static.js.map +1 -0
  24. package/dist/adapters/types.d.ts +22 -0
  25. package/dist/adapters/types.js +6 -0
  26. package/dist/adapters/types.js.map +1 -0
  27. package/dist/commands/build.d.ts +7 -0
  28. package/dist/commands/build.js +98 -0
  29. package/dist/commands/build.js.map +1 -0
  30. package/dist/commands/design.d.ts +3 -0
  31. package/dist/commands/design.js +39 -0
  32. package/dist/commands/design.js.map +1 -0
  33. package/dist/commands/discover.d.ts +9 -0
  34. package/dist/commands/discover.js +91 -0
  35. package/dist/commands/discover.js.map +1 -0
  36. package/dist/commands/generate.d.ts +7 -0
  37. package/dist/commands/generate.js +105 -0
  38. package/dist/commands/generate.js.map +1 -0
  39. package/dist/commands/init.d.ts +1 -0
  40. package/dist/commands/init.js +99 -0
  41. package/dist/commands/init.js.map +1 -0
  42. package/dist/commands/preview.d.ts +5 -0
  43. package/dist/commands/preview.js +41 -0
  44. package/dist/commands/preview.js.map +1 -0
  45. package/dist/commands/research.d.ts +1 -0
  46. package/dist/commands/research.js +38 -0
  47. package/dist/commands/research.js.map +1 -0
  48. package/dist/commands/sync.d.ts +1 -0
  49. package/dist/commands/sync.js +53 -0
  50. package/dist/commands/sync.js.map +1 -0
  51. package/dist/commands/workflow.d.ts +1 -0
  52. package/dist/commands/workflow.js +38 -0
  53. package/dist/commands/workflow.js.map +1 -0
  54. package/dist/index.d.ts +2 -0
  55. package/dist/index.js +113 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/mcp/auth.d.ts +15 -0
  58. package/dist/mcp/auth.js +56 -0
  59. package/dist/mcp/auth.js.map +1 -0
  60. package/dist/mcp/client.d.ts +65 -0
  61. package/dist/mcp/client.js +302 -0
  62. package/dist/mcp/client.js.map +1 -0
  63. package/dist/mcp/tools.d.ts +26 -0
  64. package/dist/mcp/tools.js +46 -0
  65. package/dist/mcp/tools.js.map +1 -0
  66. package/dist/research/business-researcher.d.ts +41 -0
  67. package/dist/research/business-researcher.js +888 -0
  68. package/dist/research/business-researcher.js.map +1 -0
  69. package/dist/research/crawler.d.ts +11 -0
  70. package/dist/research/crawler.js +56 -0
  71. package/dist/research/crawler.js.map +1 -0
  72. package/dist/research/design-synthesizer.d.ts +46 -0
  73. package/dist/research/design-synthesizer.js +628 -0
  74. package/dist/research/design-synthesizer.js.map +1 -0
  75. package/dist/research/differ.d.ts +19 -0
  76. package/dist/research/differ.js +58 -0
  77. package/dist/research/differ.js.map +1 -0
  78. package/dist/research/known-state.json +68 -0
  79. package/dist/research/research-cache.d.ts +6 -0
  80. package/dist/research/research-cache.js +62 -0
  81. package/dist/research/research-cache.js.map +1 -0
  82. package/dist/research/types.d.ts +98 -0
  83. package/dist/research/types.js +6 -0
  84. package/dist/research/types.js.map +1 -0
  85. package/dist/research/updater.d.ts +5 -0
  86. package/dist/research/updater.js +43 -0
  87. package/dist/research/updater.js.map +1 -0
  88. package/dist/templates/design-md.d.ts +52 -0
  89. package/dist/templates/design-md.js +315 -0
  90. package/dist/templates/design-md.js.map +1 -0
  91. package/dist/templates/prompts.d.ts +31 -0
  92. package/dist/templates/prompts.js +39 -0
  93. package/dist/templates/prompts.js.map +1 -0
  94. package/dist/templates/workflows.d.ts +9 -0
  95. package/dist/templates/workflows.js +21 -0
  96. package/dist/templates/workflows.js.map +1 -0
  97. package/dist/tui/App.d.ts +1 -0
  98. package/dist/tui/App.js +87 -0
  99. package/dist/tui/App.js.map +1 -0
  100. package/dist/tui/Dashboard.d.ts +5 -0
  101. package/dist/tui/Dashboard.js +23 -0
  102. package/dist/tui/Dashboard.js.map +1 -0
  103. package/dist/tui/DesignEditor.d.ts +6 -0
  104. package/dist/tui/DesignEditor.js +76 -0
  105. package/dist/tui/DesignEditor.js.map +1 -0
  106. package/dist/tui/PromptBuilder.d.ts +5 -0
  107. package/dist/tui/PromptBuilder.js +102 -0
  108. package/dist/tui/PromptBuilder.js.map +1 -0
  109. package/dist/tui/components/QuotaMeter.d.ts +8 -0
  110. package/dist/tui/components/QuotaMeter.js +10 -0
  111. package/dist/tui/components/QuotaMeter.js.map +1 -0
  112. package/dist/tui/components/ScreenCard.d.ts +7 -0
  113. package/dist/tui/components/ScreenCard.js +6 -0
  114. package/dist/tui/components/ScreenCard.js.map +1 -0
  115. package/dist/tui/components/Spinner.d.ts +5 -0
  116. package/dist/tui/components/Spinner.js +7 -0
  117. package/dist/tui/components/Spinner.js.map +1 -0
  118. package/dist/tui/components/StatusBar.d.ts +7 -0
  119. package/dist/tui/components/StatusBar.js +6 -0
  120. package/dist/tui/components/StatusBar.js.map +1 -0
  121. package/dist/utils/config.d.ts +26 -0
  122. package/dist/utils/config.js +66 -0
  123. package/dist/utils/config.js.map +1 -0
  124. package/dist/utils/design-validator.d.ts +44 -0
  125. package/dist/utils/design-validator.js +396 -0
  126. package/dist/utils/design-validator.js.map +1 -0
  127. package/dist/utils/logger.d.ts +8 -0
  128. package/dist/utils/logger.js +10 -0
  129. package/dist/utils/logger.js.map +1 -0
  130. package/dist/utils/output-validator.d.ts +18 -0
  131. package/dist/utils/output-validator.js +194 -0
  132. package/dist/utils/output-validator.js.map +1 -0
  133. package/dist/utils/preview.d.ts +4 -0
  134. package/dist/utils/preview.js +49 -0
  135. package/dist/utils/preview.js.map +1 -0
  136. package/dist/utils/prompt-enhancer.d.ts +21 -0
  137. package/dist/utils/prompt-enhancer.js +104 -0
  138. package/dist/utils/prompt-enhancer.js.map +1 -0
  139. package/dist/utils/quota.d.ts +18 -0
  140. package/dist/utils/quota.js +49 -0
  141. package/dist/utils/quota.js.map +1 -0
  142. package/dist/utils/validators.d.ts +125 -0
  143. package/dist/utils/validators.js +110 -0
  144. package/dist/utils/validators.js.map +1 -0
  145. package/package.json +77 -0
@@ -0,0 +1,888 @@
1
+ import { load } from 'cheerio';
2
+ // ─── Color Extraction ──────────────────────────────────────────────
3
+ const HEX_PATTERN = /#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
4
+ function normalizeHex(hex) {
5
+ const h = hex.toUpperCase();
6
+ if (h.length === 4) {
7
+ return `#${h[1]}${h[1]}${h[2]}${h[2]}${h[3]}${h[3]}`;
8
+ }
9
+ return h;
10
+ }
11
+ function classifyContext(declaration) {
12
+ const lower = declaration.toLowerCase();
13
+ if (/\bborder-color\b|\bborder\b/.test(lower))
14
+ return 'border';
15
+ if (/\bbackground-color\b|\bbackground\b|\bbg\b/.test(lower))
16
+ return 'background';
17
+ if (/\bcolor\b/.test(lower))
18
+ return 'text';
19
+ return 'unknown';
20
+ }
21
+ /**
22
+ * Extract color palette from HTML by parsing style blocks and inline styles.
23
+ */
24
+ export function extractPalette(html) {
25
+ const $ = load(html);
26
+ const colorMap = new Map();
27
+ function recordColor(hex, context) {
28
+ const normalized = normalizeHex(hex);
29
+ const existing = colorMap.get(normalized);
30
+ if (existing) {
31
+ existing.frequency += 1;
32
+ // Prefer more specific context over 'unknown'
33
+ if (existing.context === 'unknown' && context !== 'unknown') {
34
+ existing.context = context;
35
+ }
36
+ }
37
+ else {
38
+ colorMap.set(normalized, { frequency: 1, context });
39
+ }
40
+ }
41
+ function extractFromCSS(css) {
42
+ // Match CSS custom properties: --var: #hex
43
+ const customPropRegex = /--[\w-]+\s*:\s*(#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b)/g;
44
+ let match;
45
+ while ((match = customPropRegex.exec(css)) !== null) {
46
+ recordColor(match[1], 'unknown');
47
+ }
48
+ // Match property: value pairs containing hex colors
49
+ const declRegex = /([\w-]+)\s*:\s*([^;}{]+)/g;
50
+ while ((match = declRegex.exec(css)) !== null) {
51
+ const property = match[1];
52
+ const value = match[2];
53
+ const hexMatches = value.match(HEX_PATTERN);
54
+ if (hexMatches) {
55
+ const ctx = classifyContext(property);
56
+ for (const hex of hexMatches) {
57
+ recordColor(hex, ctx);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ // Extract from <style> blocks
63
+ $('style').each((_i, el) => {
64
+ const css = $(el).text();
65
+ extractFromCSS(css);
66
+ });
67
+ // Extract from inline style attributes
68
+ $('[style]').each((_i, el) => {
69
+ const style = $(el).attr('style') ?? '';
70
+ extractFromCSS(style);
71
+ });
72
+ // Build sorted color list
73
+ const colors = [];
74
+ for (const [hex, data] of colorMap) {
75
+ colors.push({ hex, frequency: data.frequency, context: data.context });
76
+ }
77
+ colors.sort((a, b) => b.frequency - a.frequency);
78
+ // Filter out structural colors — grays, blacks, whites used for layout, not brand identity
79
+ const isStructuralColor = (hex) => {
80
+ const r = parseInt(hex.slice(1, 3), 16);
81
+ const g = parseInt(hex.slice(3, 5), 16);
82
+ const b = parseInt(hex.slice(5, 7), 16);
83
+ const maxDiff = Math.max(Math.abs(r - g), Math.abs(g - b), Math.abs(r - b));
84
+ // Any near-gray (R≈G≈B) is structural: text colors, borders, backgrounds
85
+ if (maxDiff < 20)
86
+ return true;
87
+ // Pure white or near-white
88
+ if (r > 240 && g > 240 && b > 240)
89
+ return true;
90
+ return false;
91
+ };
92
+ const brandColors = colors.filter(c => !isStructuralColor(c.hex));
93
+ const brandBgColors = brandColors.filter(c => c.context === 'background');
94
+ const brandAccentColors = brandColors.filter(c => c.context !== 'background');
95
+ // Dominant = most frequent brand-colored background, or most frequent brand color overall
96
+ const dominantHex = brandBgColors.length > 0
97
+ ? brandBgColors[0].hex
98
+ : brandColors.length > 0
99
+ ? brandColors[0].hex
100
+ : colors.length > 0 ? colors[0].hex : '';
101
+ const accentHex = brandAccentColors.length > 0
102
+ ? brandAccentColors.find(c => c.hex !== dominantHex)?.hex
103
+ : brandColors.find(c => c.hex !== dominantHex)?.hex;
104
+ return { colors, dominantHex, accentHex };
105
+ }
106
+ // ─── Typography Extraction ─────────────────────────────────────────
107
+ const GENERIC_FONTS = new Set([
108
+ 'sans-serif',
109
+ 'serif',
110
+ 'monospace',
111
+ 'cursive',
112
+ 'fantasy',
113
+ 'system-ui',
114
+ 'ui-sans-serif',
115
+ 'ui-serif',
116
+ 'ui-monospace',
117
+ 'ui-rounded',
118
+ 'inherit',
119
+ 'initial',
120
+ 'unset',
121
+ ]);
122
+ // Icon fonts that should never be used as heading/body fonts
123
+ const ICON_FONTS = new Set([
124
+ 'dashicons',
125
+ 'material icons',
126
+ 'material symbols outlined',
127
+ 'material symbols rounded',
128
+ 'fontawesome',
129
+ 'font awesome',
130
+ 'fa',
131
+ 'glyphicons',
132
+ 'icomoon',
133
+ 'ionicons',
134
+ 'feather',
135
+ 'lucide',
136
+ 'phosphor',
137
+ 'heroicons',
138
+ 'bootstrap-icons',
139
+ ]);
140
+ function cleanFontName(name) {
141
+ return name
142
+ .replace(/['"]/g, '')
143
+ .replace(/\+/g, ' ')
144
+ .trim();
145
+ }
146
+ /**
147
+ * Extract typography information from HTML.
148
+ */
149
+ export function extractTypography(html) {
150
+ const $ = load(html);
151
+ const fontsSet = new Set();
152
+ let headingFont;
153
+ let bodyFont;
154
+ // Check for Google Fonts <link> tags
155
+ $('link[href*="fonts.googleapis.com"]').each((_i, el) => {
156
+ const href = $(el).attr('href') ?? '';
157
+ // Match family=Font+Name patterns (CSS2 and legacy formats)
158
+ const familyMatches = href.matchAll(/family=([^&:;]+)/g);
159
+ for (const m of familyMatches) {
160
+ // CSS2 format may have weight specs after colon
161
+ const familyStr = m[1].split(':')[0];
162
+ const cleaned = cleanFontName(familyStr);
163
+ if (cleaned)
164
+ fontsSet.add(cleaned);
165
+ }
166
+ });
167
+ // Parse font-family from CSS
168
+ const cssBlocks = [];
169
+ function parseCSS(css) {
170
+ // Simple CSS rule parser: selector { body }
171
+ const ruleRegex = /([^{}]+)\{([^}]+)\}/g;
172
+ let match;
173
+ while ((match = ruleRegex.exec(css)) !== null) {
174
+ cssBlocks.push({ selector: match[1].trim(), body: match[2] });
175
+ }
176
+ }
177
+ $('style').each((_i, el) => {
178
+ parseCSS($(el).text());
179
+ });
180
+ for (const block of cssBlocks) {
181
+ const fontFamilyMatch = block.body.match(/font-family\s*:\s*([^;]+)/);
182
+ if (!fontFamilyMatch)
183
+ continue;
184
+ const families = fontFamilyMatch[1].split(',');
185
+ for (const raw of families) {
186
+ const cleaned = cleanFontName(raw);
187
+ if (cleaned && !GENERIC_FONTS.has(cleaned.toLowerCase()) && !ICON_FONTS.has(cleaned.toLowerCase())) {
188
+ fontsSet.add(cleaned);
189
+ }
190
+ }
191
+ // Heuristic: heading vs body font
192
+ const selector = block.selector.toLowerCase();
193
+ const firstFont = families
194
+ .map((f) => cleanFontName(f))
195
+ .find((f) => f && !GENERIC_FONTS.has(f.toLowerCase()) && !ICON_FONTS.has(f.toLowerCase()));
196
+ if (firstFont) {
197
+ if (/\bh[1-3]\b/.test(selector) && !headingFont) {
198
+ headingFont = firstFont;
199
+ }
200
+ if (/\bbody\b|\bp\b/.test(selector) && !bodyFont) {
201
+ bodyFont = firstFont;
202
+ }
203
+ }
204
+ }
205
+ return {
206
+ fonts: Array.from(fontsSet),
207
+ headingFont,
208
+ bodyFont,
209
+ };
210
+ }
211
+ // ─── Layout Detection ──────────────────────────────────────────────
212
+ /**
213
+ * Detect common layout patterns from HTML DOM structure.
214
+ */
215
+ export function detectLayoutPatterns(html) {
216
+ const $ = load(html);
217
+ const patterns = [];
218
+ // Hero banner: first <section> or large div with background image/gradient + heading
219
+ const firstSection = $('section').first();
220
+ if (firstSection.length) {
221
+ const style = (firstSection.attr('style') ?? '').toLowerCase();
222
+ const cls = (firstSection.attr('class') ?? '').toLowerCase();
223
+ const hasH1 = firstSection.find('h1').length > 0;
224
+ const hasBg = style.includes('background-image') ||
225
+ style.includes('background:') ||
226
+ cls.includes('hero');
227
+ if (hasH1 || hasBg) {
228
+ patterns.push('hero-banner');
229
+ }
230
+ }
231
+ // Sticky/fixed nav
232
+ $('nav, header').each((_i, el) => {
233
+ const style = ($(el).attr('style') ?? '').toLowerCase();
234
+ const cls = ($(el).attr('class') ?? '').toLowerCase();
235
+ if (style.includes('position: sticky') ||
236
+ style.includes('position: fixed') ||
237
+ style.includes('position:sticky') ||
238
+ style.includes('position:fixed') ||
239
+ cls.includes('sticky') ||
240
+ cls.includes('fixed')) {
241
+ if (!patterns.includes('sticky-nav')) {
242
+ patterns.push('sticky-nav');
243
+ }
244
+ }
245
+ });
246
+ // Card patterns: repeating child elements with image + text
247
+ $('div, section, ul').each((_i, el) => {
248
+ const children = $(el).children();
249
+ if (children.length >= 3) {
250
+ let cardLikeCount = 0;
251
+ children.each((_j, child) => {
252
+ const hasImage = $(child).find('img').length > 0;
253
+ const hasText = $(child).find('p, span, h2, h3, h4').length > 0 ||
254
+ $(child).text().trim().length > 0;
255
+ if (hasImage && hasText)
256
+ cardLikeCount++;
257
+ });
258
+ if (cardLikeCount >= 3 && !patterns.includes('card-grid')) {
259
+ patterns.push('card-grid');
260
+ }
261
+ }
262
+ });
263
+ // Multi-column grid: 3+ sibling elements
264
+ $('div, section').each((_i, el) => {
265
+ const children = $(el).children();
266
+ const style = ($(el).attr('style') ?? '').toLowerCase();
267
+ const cls = ($(el).attr('class') ?? '').toLowerCase();
268
+ const isGrid = style.includes('grid') ||
269
+ style.includes('flex') ||
270
+ cls.includes('grid') ||
271
+ cls.includes('columns');
272
+ if (children.length >= 3 && isGrid) {
273
+ if (children.length === 3 && !patterns.includes('three-column-grid')) {
274
+ patterns.push('three-column-grid');
275
+ }
276
+ else if (children.length > 3 && !patterns.includes('multi-column-grid')) {
277
+ patterns.push('multi-column-grid');
278
+ }
279
+ }
280
+ });
281
+ // Full-width sections: checking for vw units or full-width classes
282
+ $('section, div').each((_i, el) => {
283
+ const style = ($(el).attr('style') ?? '').toLowerCase();
284
+ const cls = ($(el).attr('class') ?? '').toLowerCase();
285
+ if (style.includes('100vw') ||
286
+ style.includes('width: 100%') ||
287
+ style.includes('width:100%') ||
288
+ cls.includes('full-width') ||
289
+ cls.includes('container-fluid')) {
290
+ if (!patterns.includes('full-width-sections')) {
291
+ patterns.push('full-width-sections');
292
+ }
293
+ }
294
+ });
295
+ return patterns;
296
+ }
297
+ const INDUSTRY_PROFILES = [
298
+ {
299
+ keywords: /\b(retail|grocery|discount|store|tienda|supermercado|mart)\b/i,
300
+ trustSignals: [
301
+ 'Visible pricing on every product',
302
+ 'Location finder',
303
+ 'Weekly deals prominently displayed',
304
+ 'Clean organized layout',
305
+ ],
306
+ accessibilityNeeds: [
307
+ 'High contrast text',
308
+ 'Large tap targets for mobile',
309
+ 'Simple navigation',
310
+ ],
311
+ expectations: [
312
+ 'Find nearest store',
313
+ 'See current deals',
314
+ 'Browse product categories',
315
+ 'Check prices quickly',
316
+ ],
317
+ },
318
+ {
319
+ keywords: /\b(fintech|banking|payments?|financial|finance|bank)\b/i,
320
+ trustSignals: [
321
+ 'Security certifications visible',
322
+ 'Regulatory compliance badges',
323
+ 'Data encryption indicators',
324
+ ],
325
+ accessibilityNeeds: [
326
+ 'Screen reader support for financial data',
327
+ 'Color-blind safe charts',
328
+ ],
329
+ expectations: [
330
+ 'Clear pricing/fee structure',
331
+ 'API documentation',
332
+ 'Dashboard preview',
333
+ 'Customer support access',
334
+ ],
335
+ },
336
+ {
337
+ keywords: /\b(saas|software|technology|tech|platform|app|cloud)\b/i,
338
+ trustSignals: [
339
+ 'Customer logos',
340
+ 'Usage metrics',
341
+ 'SOC2/ISO badges',
342
+ ],
343
+ accessibilityNeeds: [
344
+ 'Keyboard navigation',
345
+ 'Responsive design',
346
+ ],
347
+ expectations: [
348
+ 'Product demo/screenshots',
349
+ 'Pricing tiers',
350
+ 'Integration list',
351
+ 'Documentation link',
352
+ ],
353
+ },
354
+ {
355
+ keywords: /\b(healthcare|health|wellness|medical|clinic|hospital)\b/i,
356
+ trustSignals: [
357
+ 'Provider credentials',
358
+ 'Patient testimonials',
359
+ 'HIPAA compliance',
360
+ ],
361
+ accessibilityNeeds: [
362
+ 'WCAG AA compliance',
363
+ 'Large readable text',
364
+ 'Clear form labels',
365
+ ],
366
+ expectations: [
367
+ 'Appointment booking',
368
+ 'Service listings',
369
+ 'Insurance information',
370
+ ],
371
+ },
372
+ {
373
+ keywords: /\b(education|school|university|learning|academy|course)\b/i,
374
+ trustSignals: [
375
+ 'Accreditation badges',
376
+ 'Student outcomes',
377
+ 'Faculty credentials',
378
+ ],
379
+ accessibilityNeeds: [
380
+ 'Screen reader compatible',
381
+ 'Captioned media',
382
+ 'Dyslexia-friendly fonts',
383
+ ],
384
+ expectations: [
385
+ 'Course catalog',
386
+ 'Enrollment process',
387
+ 'Student portal access',
388
+ ],
389
+ },
390
+ ];
391
+ const LOCALE_PROFILES = [
392
+ {
393
+ keywords: /\b(es-mx|mexican|mexico|m[eé]xico)\b/i,
394
+ considerations: [
395
+ 'Use Spanish copy naturally, not translated',
396
+ 'Reflect Mexican family values',
397
+ 'Avoid US-centric imagery',
398
+ 'Consider regional color meanings',
399
+ ],
400
+ },
401
+ {
402
+ keywords: /\b(es-|latin|latino|latina|latinx|hispano|hispanic)\b/i,
403
+ considerations: [
404
+ 'Warm, family-oriented imagery',
405
+ 'Community-focused messaging',
406
+ ],
407
+ },
408
+ ];
409
+ /**
410
+ * Infer audience insights from industry and audience descriptions.
411
+ * Knowledge-base lookup, not web search.
412
+ */
413
+ export function inferAudienceInsights(industry, audience, locale) {
414
+ const combined = `${industry} ${audience}`;
415
+ const trustSignals = [];
416
+ const accessibilityNeeds = [];
417
+ const expectations = [];
418
+ const culturalConsiderations = [];
419
+ // Match industry profiles
420
+ for (const profile of INDUSTRY_PROFILES) {
421
+ if (profile.keywords.test(combined)) {
422
+ for (const s of profile.trustSignals) {
423
+ if (!trustSignals.includes(s))
424
+ trustSignals.push(s);
425
+ }
426
+ for (const a of profile.accessibilityNeeds) {
427
+ if (!accessibilityNeeds.includes(a))
428
+ accessibilityNeeds.push(a);
429
+ }
430
+ for (const e of profile.expectations) {
431
+ if (!expectations.includes(e))
432
+ expectations.push(e);
433
+ }
434
+ }
435
+ }
436
+ // Fallback if no industry matched
437
+ if (trustSignals.length === 0) {
438
+ trustSignals.push('Clear contact information', 'Professional design');
439
+ accessibilityNeeds.push('Responsive design', 'Readable font sizes');
440
+ expectations.push('Easy navigation', 'Clear value proposition');
441
+ }
442
+ // Locale/cultural considerations
443
+ const localeStr = `${locale ?? ''} ${audience}`;
444
+ for (const lp of LOCALE_PROFILES) {
445
+ if (lp.keywords.test(localeStr)) {
446
+ for (const c of lp.considerations) {
447
+ if (!culturalConsiderations.includes(c))
448
+ culturalConsiderations.push(c);
449
+ }
450
+ }
451
+ }
452
+ return {
453
+ trustSignals,
454
+ accessibilityNeeds,
455
+ culturalConsiderations,
456
+ expectations,
457
+ };
458
+ }
459
+ // ─── Market Position ───────────────────────────────────────────────
460
+ /**
461
+ * Infer market position from the business brief text.
462
+ */
463
+ export function inferMarketPosition(brief, _siteAnalysis) {
464
+ const text = `${brief.industry} ${brief.targetAudience} ${brief.aesthetic} ${brief.companyName}`.toLowerCase();
465
+ // Price point
466
+ let pricePoint = 'mid-range';
467
+ if (/\b(luxury|haute|bespoke)\b/.test(text)) {
468
+ pricePoint = 'luxury';
469
+ }
470
+ else if (/\b(premium|enterprise|exclusive)\b/.test(text)) {
471
+ pricePoint = 'premium';
472
+ }
473
+ else if (/\b(discount|cheap|ahorro|barato|value|affordable|budget)\b/.test(text)) {
474
+ pricePoint = 'budget';
475
+ }
476
+ // Reach
477
+ let reach = 'national';
478
+ if (/\b(neighborhood|barrio|local|community)\b/.test(text)) {
479
+ reach = 'local';
480
+ }
481
+ else if (/\b(state|regional)\b/.test(text)) {
482
+ reach = 'regional';
483
+ }
484
+ else if (/\b(global|international|worldwide)\b/.test(text)) {
485
+ reach = 'global';
486
+ }
487
+ else if (/\b(national|nationwide|country-wide|pa[ií]s)\b/.test(text)) {
488
+ reach = 'national';
489
+ }
490
+ // Personality
491
+ let personality = 'modern';
492
+ if (/\b(disruptive|revolutionary)\b/.test(text)) {
493
+ personality = 'disruptive';
494
+ }
495
+ else if (/\b(innovative|cutting-edge|ai|tech-forward)\b/.test(text)) {
496
+ personality = 'innovative';
497
+ }
498
+ else if (/\b(traditional|heritage|classic|established)\b/.test(text)) {
499
+ personality = 'traditional';
500
+ }
501
+ else if (/\b(modern|contemporary)\b/.test(text)) {
502
+ personality = 'modern';
503
+ }
504
+ return { pricePoint, reach, personality };
505
+ }
506
+ // ─── Business Model Inference ─────────────────────────────────────
507
+ // Signal patterns for detecting business model from site content
508
+ const PHYSICAL_RETAIL_SIGNALS = /\b(sucursal|tienda|ubicaci|locali|horario|store|location|find.?us|visit|nearest)\b/i;
509
+ const ECOMMERCE_SIGNALS = /\b(carrito|cart|checkout|comprar|buy|shop|add.?to.?cart|wishlist|order)\b/i;
510
+ const SAAS_SIGNALS = /\b(login|sign.?up|dashboard|api|pricing|plan|trial|demo|docs|documentation)\b/i;
511
+ const SERVICE_SIGNALS = /\b(contact|agendar|book|cita|appointment|schedule|consult|quote)\b/i;
512
+ /**
513
+ * Infer what kind of business this is and what the website should (and should NOT) do.
514
+ *
515
+ * Uses a combination of site navigation signals, CTA text, layout patterns, and
516
+ * industry keywords from the brief to determine the business model. This prevents
517
+ * generating e-commerce pages for physical-only retailers, or store locators for SaaS products.
518
+ */
519
+ export function inferBusinessModel(brief, siteAnalysis) {
520
+ const hasSite = siteAnalysis != null;
521
+ // Combine all text signals from the site
522
+ const navText = hasSite ? siteAnalysis.navItems.join(' ') : '';
523
+ const ctaText = hasSite ? siteAnalysis.ctaTexts.join(' ') : '';
524
+ const allSiteText = `${navText} ${ctaText}`;
525
+ // Step 1: Detect signals from site content
526
+ const hasPhysicalRetailSignals = PHYSICAL_RETAIL_SIGNALS.test(allSiteText);
527
+ const hasEcommerceSignals = ECOMMERCE_SIGNALS.test(allSiteText);
528
+ const hasSaasSignals = SAAS_SIGNALS.test(allSiteText);
529
+ const hasServiceSignals = SERVICE_SIGNALS.test(allSiteText);
530
+ // Check layout patterns for additional hints
531
+ const hasCardGridWithoutEcommerce = hasSite &&
532
+ siteAnalysis.layoutPatterns.includes('card-grid') &&
533
+ !hasEcommerceSignals;
534
+ // Step 2: Detect from industry keywords in brief
535
+ const industry = brief.industry.toLowerCase();
536
+ const audience = brief.targetAudience.toLowerCase();
537
+ const briefText = `${industry} ${audience}`;
538
+ let type = 'other';
539
+ // Site signals take priority when available
540
+ if (hasSite) {
541
+ if (hasEcommerceSignals && !hasPhysicalRetailSignals) {
542
+ type = 'e-commerce';
543
+ }
544
+ else if (hasPhysicalRetailSignals && !hasEcommerceSignals) {
545
+ type = 'physical-retail';
546
+ }
547
+ else if (hasSaasSignals && !hasPhysicalRetailSignals && !hasEcommerceSignals) {
548
+ type = 'saas';
549
+ }
550
+ else if (hasServiceSignals && !hasEcommerceSignals && !hasSaasSignals) {
551
+ type = 'service';
552
+ }
553
+ else if (hasCardGridWithoutEcommerce && /\b(retail|grocery|tienda|store|supermercado|despensa|mercado)\b/.test(briefText)) {
554
+ // Card grid without e-commerce signals + retail industry = likely physical retail catalog
555
+ type = 'physical-retail';
556
+ }
557
+ }
558
+ // Fall back to industry keywords if site didn't determine the type
559
+ if (type === 'other') {
560
+ if (/\b(tienda|store|retail|grocery|supermercado|despensa|discount|mercado)\b/.test(briefText)) {
561
+ // Check if e-commerce signals contradict physical retail
562
+ if (hasEcommerceSignals) {
563
+ type = 'e-commerce';
564
+ }
565
+ else {
566
+ type = 'physical-retail';
567
+ }
568
+ }
569
+ else if (/\b(saas|software|platform|app\b|cloud)\b/.test(briefText)) {
570
+ type = 'saas';
571
+ }
572
+ else if (/\b(marketplace|market.?place)\b/.test(briefText)) {
573
+ type = 'marketplace';
574
+ }
575
+ else if (/\b(agency|consulting|lawyer|doctor|dentist|clinic|salon|service|freelance)\b/.test(briefText)) {
576
+ type = 'service';
577
+ }
578
+ else if (/\b(blog|news|media|magazine|publication|journal)\b/.test(briefText)) {
579
+ type = 'media';
580
+ }
581
+ else if (/\b(nonprofit|non-profit|ngo|charity|foundation)\b/.test(briefText)) {
582
+ type = 'nonprofit';
583
+ }
584
+ }
585
+ // Step 3: Build the full context based on detected type
586
+ const result = buildModelContext(type, hasSite);
587
+ // Step 4: Extract differentiators from site content
588
+ if (hasSite) {
589
+ const allItems = [...siteAnalysis.navItems, ...siteAnalysis.ctaTexts];
590
+ for (const item of allItems) {
591
+ // Look for text containing numbers (e.g., "3,300+ tiendas", "50 estados")
592
+ if (/\d[\d,]*\+?\s*\w+/.test(item) && item.length < 80) {
593
+ result.differentiators.push(item.trim());
594
+ }
595
+ }
596
+ }
597
+ return result;
598
+ }
599
+ /**
600
+ * Build a complete BusinessModelContext for a given business type.
601
+ */
602
+ function buildModelContext(type, hasSiteData) {
603
+ switch (type) {
604
+ case 'physical-retail':
605
+ return {
606
+ type: 'physical-retail',
607
+ primaryRevenue: 'Physical store sales',
608
+ websitePurpose: 'Drive foot traffic to nearest store location. Inform about deals and product availability.',
609
+ primaryUserGoals: [
610
+ 'Find the nearest store location',
611
+ 'Check current deals and prices',
612
+ 'Browse available product categories',
613
+ 'Get store hours and contact information',
614
+ ],
615
+ keyFeatures: ['Store locator', 'Weekly deals/flyer', 'Product category browse', 'Store hours', 'Brand information'],
616
+ notFeatures: ['Shopping cart', 'Online checkout', 'Add to cart buttons', 'User accounts', 'Wishlist', 'Online payment'],
617
+ differentiators: [],
618
+ confidence: hasSiteData ? 80 : 50,
619
+ };
620
+ case 'e-commerce':
621
+ return {
622
+ type: 'e-commerce',
623
+ primaryRevenue: 'Online product sales',
624
+ websitePurpose: 'Sell products directly to customers online.',
625
+ primaryUserGoals: [
626
+ 'Browse and search products',
627
+ 'Compare prices and options',
628
+ 'Add items to cart and checkout',
629
+ 'Track orders and manage account',
630
+ ],
631
+ keyFeatures: ['Product catalog', 'Shopping cart', 'Checkout', 'User accounts', 'Order tracking', 'Search'],
632
+ notFeatures: [],
633
+ differentiators: [],
634
+ confidence: hasSiteData ? 80 : 50,
635
+ };
636
+ case 'saas':
637
+ return {
638
+ type: 'saas',
639
+ primaryRevenue: 'Software subscriptions',
640
+ websitePurpose: 'Convert visitors into trial/paid users.',
641
+ primaryUserGoals: [
642
+ 'Understand what the product does',
643
+ 'See pricing and compare plans',
644
+ 'Start a free trial or demo',
645
+ 'Access documentation',
646
+ ],
647
+ keyFeatures: ['Product demo/screenshots', 'Pricing tiers', 'Free trial CTA', 'Documentation', 'Customer testimonials'],
648
+ notFeatures: ['Physical store locator', 'Inventory browsing'],
649
+ differentiators: [],
650
+ confidence: hasSiteData ? 80 : 50,
651
+ };
652
+ case 'marketplace':
653
+ return {
654
+ type: 'marketplace',
655
+ primaryRevenue: 'Transaction fees or commissions',
656
+ websitePurpose: 'Connect buyers and sellers on a shared platform.',
657
+ primaryUserGoals: [
658
+ 'Browse listings from multiple sellers',
659
+ 'Compare options and prices',
660
+ 'Complete secure transactions',
661
+ 'Manage buyer/seller profile',
662
+ ],
663
+ keyFeatures: ['Search and filtering', 'Seller profiles', 'Reviews/ratings', 'Secure checkout', 'Messaging'],
664
+ notFeatures: ['Single-brand store locator'],
665
+ differentiators: [],
666
+ confidence: hasSiteData ? 75 : 45,
667
+ };
668
+ case 'service':
669
+ return {
670
+ type: 'service',
671
+ primaryRevenue: 'Service fees',
672
+ websitePurpose: 'Generate leads and bookings.',
673
+ primaryUserGoals: [
674
+ 'Understand available services',
675
+ 'Book an appointment or consultation',
676
+ 'Contact the business',
677
+ 'Read reviews and credentials',
678
+ ],
679
+ keyFeatures: ['Service catalog', 'Booking form', 'Contact information', 'Testimonials', 'Team/credentials'],
680
+ notFeatures: ['Shopping cart', 'Product inventory'],
681
+ differentiators: [],
682
+ confidence: hasSiteData ? 70 : 40,
683
+ };
684
+ case 'media':
685
+ return {
686
+ type: 'media',
687
+ primaryRevenue: 'Advertising or subscriptions',
688
+ websitePurpose: 'Publish and distribute content to readers.',
689
+ primaryUserGoals: [
690
+ 'Read articles and news',
691
+ 'Browse by category or topic',
692
+ 'Subscribe for updates',
693
+ 'Share content',
694
+ ],
695
+ keyFeatures: ['Article listings', 'Category navigation', 'Search', 'Newsletter signup', 'Social sharing'],
696
+ notFeatures: ['Shopping cart', 'Store locator'],
697
+ differentiators: [],
698
+ confidence: hasSiteData ? 70 : 40,
699
+ };
700
+ case 'nonprofit':
701
+ return {
702
+ type: 'nonprofit',
703
+ primaryRevenue: 'Donations and grants',
704
+ websitePurpose: 'Inspire action and facilitate donations.',
705
+ primaryUserGoals: [
706
+ 'Understand the mission',
707
+ 'Make a donation',
708
+ 'Find volunteer opportunities',
709
+ 'Learn about impact',
710
+ ],
711
+ keyFeatures: ['Mission statement', 'Donate button', 'Impact metrics', 'Volunteer signup', 'Event calendar'],
712
+ notFeatures: ['Shopping cart', 'Product catalog'],
713
+ differentiators: [],
714
+ confidence: hasSiteData ? 70 : 40,
715
+ };
716
+ case 'other':
717
+ default:
718
+ return {
719
+ type: 'other',
720
+ primaryRevenue: 'Unknown',
721
+ websitePurpose: 'Inform visitors about the organization.',
722
+ primaryUserGoals: ['Learn about the organization', 'Find contact information'],
723
+ keyFeatures: ['About page', 'Contact form'],
724
+ notFeatures: [],
725
+ differentiators: [],
726
+ confidence: 20,
727
+ };
728
+ }
729
+ }
730
+ // ─── Site Analysis ─────────────────────────────────────────────────
731
+ /**
732
+ * Detect the content tone of a page.
733
+ */
734
+ function detectContentTone(text) {
735
+ const lower = text.toLowerCase();
736
+ const formalWords = ['enterprise', 'solutions', 'leverage', 'optimize', 'facilitate', 'utilize'];
737
+ const casualWords = ['hey', 'awesome', 'check out', 'cool', 'love', "let's"];
738
+ const technicalWords = ['api', 'sdk', 'integration', 'deploy', 'infrastructure', 'latency'];
739
+ const warmWords = ['family', 'together', 'community', 'home', 'welcome', 'care', 'heart'];
740
+ const scores = {
741
+ formal: formalWords.filter((w) => lower.includes(w)).length,
742
+ casual: casualWords.filter((w) => lower.includes(w)).length,
743
+ technical: technicalWords.filter((w) => lower.includes(w)).length,
744
+ warm: warmWords.filter((w) => lower.includes(w)).length,
745
+ };
746
+ const max = Math.max(scores.formal, scores.casual, scores.technical, scores.warm);
747
+ if (max === 0)
748
+ return 'neutral';
749
+ if (scores.formal === max)
750
+ return 'formal';
751
+ if (scores.casual === max)
752
+ return 'casual';
753
+ if (scores.technical === max)
754
+ return 'technical';
755
+ if (scores.warm === max)
756
+ return 'warm';
757
+ return 'neutral';
758
+ }
759
+ /**
760
+ * Analyze a website by fetching it and extracting design signals.
761
+ * Returns null on fetch failure (graceful degradation).
762
+ */
763
+ export async function analyzeSite(url) {
764
+ try {
765
+ const response = await fetch(url, {
766
+ headers: { 'User-Agent': 'StitchForge/0.3.0' },
767
+ signal: AbortSignal.timeout(15000),
768
+ });
769
+ if (!response.ok)
770
+ return null;
771
+ const html = await response.text();
772
+ const $ = load(html);
773
+ const palette = extractPalette(html);
774
+ const typography = extractTypography(html);
775
+ const layoutPatterns = detectLayoutPatterns(html);
776
+ // Content tone from body text
777
+ const bodyText = $('body').text().replace(/\s+/g, ' ').trim();
778
+ const contentTone = detectContentTone(bodyText);
779
+ // Extract nav items
780
+ const navItems = [];
781
+ $('nav a, header a').each((_i, el) => {
782
+ const text = $(el).text().trim();
783
+ if (text && text.length < 50 && !navItems.includes(text)) {
784
+ navItems.push(text);
785
+ }
786
+ });
787
+ // Extract CTA texts from buttons and prominent links
788
+ const ctaTexts = [];
789
+ $('button, a.btn, [role="button"], .cta, [class*="cta"], [class*="button"]').each((_i, el) => {
790
+ const text = $(el).text().trim();
791
+ if (text && text.length < 50 && !ctaTexts.includes(text)) {
792
+ ctaTexts.push(text);
793
+ }
794
+ });
795
+ return {
796
+ url,
797
+ palette,
798
+ typography,
799
+ layoutPatterns,
800
+ contentTone,
801
+ navItems,
802
+ ctaTexts,
803
+ fetchedAt: new Date().toISOString(),
804
+ };
805
+ }
806
+ catch {
807
+ return null;
808
+ }
809
+ }
810
+ // ─── Orchestrator ──────────────────────────────────────────────────
811
+ /**
812
+ * Perform complete business research for a brief.
813
+ * Orchestrates site analysis, competitor analysis, audience insights,
814
+ * market positioning, and business model inference.
815
+ */
816
+ export async function researchBusiness(brief) {
817
+ const fallbacksUsed = [];
818
+ let confidence = 10; // baseline
819
+ // Analyze current site
820
+ let currentSite;
821
+ if (brief.websiteUrl) {
822
+ const analysis = await analyzeSite(brief.websiteUrl);
823
+ if (analysis) {
824
+ currentSite = analysis;
825
+ confidence += 30;
826
+ }
827
+ else {
828
+ fallbacksUsed.push('current_site_unavailable');
829
+ }
830
+ }
831
+ else {
832
+ fallbacksUsed.push('current_site_unavailable');
833
+ }
834
+ // Analyze competitors
835
+ const competitors = [];
836
+ if (brief.competitorUrls && brief.competitorUrls.length > 0) {
837
+ for (const compUrl of brief.competitorUrls) {
838
+ const analysis = await analyzeSite(compUrl);
839
+ if (analysis) {
840
+ // Derive a name from the URL
841
+ let name;
842
+ try {
843
+ name = new URL(compUrl).hostname.replace('www.', '').split('.')[0];
844
+ }
845
+ catch {
846
+ name = compUrl;
847
+ }
848
+ competitors.push({
849
+ url: compUrl,
850
+ name,
851
+ palette: analysis.palette,
852
+ typography: analysis.typography,
853
+ strengths: [],
854
+ commonPatterns: analysis.layoutPatterns,
855
+ fetchedAt: analysis.fetchedAt,
856
+ });
857
+ confidence += 20;
858
+ }
859
+ }
860
+ if (competitors.length === 0) {
861
+ fallbacksUsed.push('competitors_unavailable');
862
+ }
863
+ }
864
+ else {
865
+ fallbacksUsed.push('competitors_unavailable');
866
+ }
867
+ // Audience insights (always available)
868
+ const audienceInsights = inferAudienceInsights(brief.industry, brief.targetAudience, brief.locale);
869
+ confidence += 20;
870
+ // Market position
871
+ const marketPosition = inferMarketPosition(brief, currentSite);
872
+ // Business model inference
873
+ const businessModel = inferBusinessModel(brief, currentSite);
874
+ // Cap confidence at 100
875
+ confidence = Math.min(confidence, 100);
876
+ return {
877
+ brief,
878
+ businessModel,
879
+ currentSite,
880
+ competitors,
881
+ audienceInsights,
882
+ marketPosition,
883
+ researchedAt: new Date().toISOString(),
884
+ confidence,
885
+ fallbacksUsed,
886
+ };
887
+ }
888
+ //# sourceMappingURL=business-researcher.js.map