krz/brand-bench

A dynamic brand documentation generator with Ollama integration.

clone: git clone https://gitbay.org/krz/brand-bench.git

main: src/engine/generator.ts · raw

  1import type {
  2  BrandInputs,
  3  BrandOutputs,
  4  ToneGuidance,
  5  VisualDirection,
  6  LogoConcept,
  7  UsageExample,
  8  ColorPalette,
  9  ColorSwatch,
 10  Typography,
 11  TypographyToken,
 12} from '../types';
 13
 14type CategoryType = 'developer' | 'creative' | 'product' | 'services' | 'personal' | 'general';
 15
 16interface GenContext {
 17  name: string;
 18  category: string;
 19  purpose: string;
 20  audience: string;
 21  tone: string[];
 22  avoid: string[];
 23  notes: string;
 24  catType: CategoryType;
 25  hasTone: (t: string) => boolean;
 26  hasAvoid: (a: string) => boolean;
 27}
 28
 29// ── Helpers ──────────────────────────────────────────────────────────────────
 30
 31function detectCategory(raw: string): CategoryType {
 32  const c = raw.toLowerCase();
 33  if (/\b(dev|developer|tool|cli|sdk|api|library|lib|framework|plugin|compiler|linter|utility)\b/.test(c))
 34    return 'developer';
 35  if (/\b(design|creative|studio|agency|art|visual|branding|illustration|photography)\b/.test(c))
 36    return 'creative';
 37  if (/\b(saas|platform|service|app|software|product|startup|dashboard)\b/.test(c))
 38    return 'product';
 39  if (/\b(consult|advisory|freelance|firm|practice|coach|training)\b/.test(c))
 40    return 'services';
 41  if (/\b(personal|portfolio|blog|brand|me|writer|designer|maker|creator)\b/.test(c))
 42    return 'personal';
 43  return 'general';
 44}
 45
 46function buildContext(inputs: BrandInputs): GenContext {
 47  const catType = detectCategory(inputs.category);
 48  const hasTone = (t: string) =>
 49    inputs.tone.some(x => x.toLowerCase().includes(t.toLowerCase()));
 50  const hasAvoid = (a: string) =>
 51    inputs.avoid.some(x => x.toLowerCase().includes(a.toLowerCase()));
 52
 53  return {
 54    name: inputs.name.trim() || 'Untitled',
 55    category: inputs.category.trim() || 'project',
 56    purpose: inputs.purpose.trim() || 'solve a specific problem',
 57    audience: inputs.audience.trim() || 'its intended users',
 58    tone: inputs.tone,
 59    avoid: inputs.avoid,
 60    notes: inputs.notes.trim(),
 61    catType,
 62    hasTone,
 63    hasAvoid,
 64  };
 65}
 66
 67function pick<T>(arr: T[]): T {
 68  return arr[Math.floor(Math.random() * arr.length)];
 69}
 70
 71function pickN<T>(arr: T[], n: number): T[] {
 72  const copy = [...arr];
 73  const result: T[] = [];
 74  while (result.length < n && copy.length > 0) {
 75    const i = Math.floor(Math.random() * copy.length);
 76    result.push(copy.splice(i, 1)[0]);
 77  }
 78  return result;
 79}
 80
 81function cap(s: string): string {
 82  return s.charAt(0).toUpperCase() + s.slice(1);
 83}
 84
 85// ── Overview ─────────────────────────────────────────────────────────────────
 86
 87function generateOverview(ctx: GenContext): string {
 88  const { name, category, purpose, audience, catType } = ctx;
 89
 90  const base = pick([
 91    `${name} is a ${category} for ${audience}. ${cap(purpose)}.`,
 92    `A ${category} built for ${audience}. ${name} is built to ${purpose}.`,
 93    `${name} helps ${audience} ${purpose}.`,
 94    `${name} is a ${category} built to ${purpose}. Made for ${audience}.`,
 95  ]);
 96
 97  let suffix = '';
 98  if (ctx.hasTone('minimal')) suffix = ' Nothing more.';
 99  else if (ctx.hasTone('technical')) suffix = ' Stays out of the way while doing its job.';
100  else if (ctx.hasTone('calm')) suffix = ' Designed to reduce friction, not add to it.';
101  else if (ctx.hasTone('bold')) suffix = ' No compromises.';
102  else if (ctx.hasTone('warm')) suffix = ' Made with care.';
103  else if (catType === 'developer') suffix = ' Built to stay composable and predictable.';
104  else if (catType === 'creative') suffix = ' Work that speaks before the introduction.';
105
106  return base + suffix;
107}
108
109// ── Positioning ───────────────────────────────────────────────────────────────
110
111function generatePositioning(ctx: GenContext): string {
112  const { name, catType } = ctx;
113
114  const templates: Record<CategoryType, string[]> = {
115    developer: [
116      `${name} sits in the space between "build it yourself" and "platform lock-in." It's a workflow layer, not an abstraction. You keep control; ${name} handles the repetitive parts.`,
117      `Most tools in this space try to do too much. ${name} doesn't. It handles exactly what it promises — then stays out of your way.`,
118      `${name} is not a platform. It's a tool. You own the infrastructure; ${name} owns the workflow. That distinction matters.`,
119    ],
120    creative: [
121      `${name} is defined by its process as much as its output. The work should be recognizable without a logo — that's the goal.`,
122      `${name} occupies a deliberate position: between personal and professional, between output and craft. Not trying to be everything. Trying to be specific.`,
123      `There's no shortage of creative work. ${name} earns attention through quality and consistency, not volume or novelty.`,
124    ],
125    product: [
126      `${name} doesn't try to replace your existing workflow. It fits into it. The goal is to reduce friction in a specific, measurable place.`,
127      `The space ${name} plays in has no shortage of tools. What it offers is focus — one problem, done well, with clear boundaries.`,
128      `${name} is built for people who've tried the alternatives and found them too complicated, too expensive, or too broad.`,
129    ],
130    services: [
131      `${name} is not a generalist practice. It works on a specific type of problem with a specific type of client. That specificity is the positioning.`,
132      `Clients come to ${name} when they need someone who has solved this problem before. The brand should communicate experience, not aspiration.`,
133      `${name} does one thing well and charges accordingly. Clarity of scope is the offer.`,
134    ],
135    personal: [
136      `${name} is a deliberate presence — not a portfolio, not a platform, not a personal brand in the marketing sense. A clear point of view, consistently expressed.`,
137      `${name} doesn't try to appeal to everyone. It's built around a specific set of interests and a specific way of working.`,
138      `There are a lot of personal sites. ${name} is distinct by being specific — not broad, not aspirational, not trying to cover every base.`,
139    ],
140    general: [
141      `${name} occupies a clear position: built for a specific audience with a specific need. Not trying to be everything.`,
142      `${name} is not for everyone. That's intentional. Clarity of purpose is more valuable than breadth of appeal.`,
143    ],
144  };
145
146  return pick(templates[catType] ?? templates.general);
147}
148
149// ── Tone Guidance ─────────────────────────────────────────────────────────────
150
151function generateToneGuidance(ctx: GenContext): ToneGuidance {
152  const { name, purpose, tone, avoid, catType, hasTone } = ctx;
153
154  const voiceMap: Record<string, string> = {
155    minimal: 'Short sentences. No adjectives unless load-bearing. Direct.',
156    technical: 'Precise nouns, specific verbs. Write for experts. Avoid explaining what the reader already knows.',
157    calm: 'Measured pace. Confident statements. No urgency cues. Let the work speak.',
158    bold: 'Strong verbs. Active voice. Make a claim and stand behind it.',
159    warm: 'Approachable but not casual. Human without being informal.',
160    dry: 'Deadpan. Understate. Trust the reader to get it.',
161    focused: 'Stay on topic. One idea per sentence. Cut the rest.',
162  };
163
164  const catVoiceDefaults: Record<CategoryType, string> = {
165    developer: 'Write for engineers. Assume technical literacy. Specificity earns trust.',
166    creative: 'Lead with the work. Copy should serve the visual, not explain it.',
167    product: 'Clear over clever. Features earn their mention by solving something real.',
168    services: "Experience over enthusiasm. What you've done, not what you'll do.",
169    personal: 'First person where appropriate. Honest and considered.',
170    general: 'Clear, direct, grounded. Earn attention with specificity.',
171  };
172
173  const toneVoiceParts = tone.map(t => voiceMap[t.toLowerCase()]).filter(Boolean);
174  const voiceNotes =
175    toneVoiceParts.length > 0
176      ? toneVoiceParts.join(' ')
177      : catVoiceDefaults[catType] ?? catVoiceDefaults.general;
178
179  const avoidDefaults: Record<CategoryType, string[]> = {
180    developer: ['"seamlessly"', '"game-changing"', '"powerful" as an adjective', 'passive voice'],
181    creative: ['"unique"', '"innovative"', '"passion-driven"', 'agency-speak'],
182    product: ['"revolutionary"', '"disrupts"', '"leverage"', '"synergy"'],
183    services: ['"partner"', '"solutions"', '"holistic"', '"best-in-class"'],
184    personal: ['"journey"', '"passionate about"', '"thought leader"', '"excited to announce"'],
185    general: ['"world-class"', '"cutting-edge"', '"innovative"', '"paradigm"'],
186  };
187
188  const avoidList = [
189    ...avoid,
190    ...(avoidDefaults[catType] ?? avoidDefaults.general).filter(
191      a => !avoid.some(ua => ua.toLowerCase().includes(a.toLowerCase().replace(/"/g, '')))
192    ),
193  ];
194
195  const phrasesByTone: string[] = [];
196  if (hasTone('minimal')) phrasesByTone.push(`"${name}. ${cap(purpose)}."`);
197  if (hasTone('technical'))
198    phrasesByTone.push('"Typed, composable, deterministic."');
199  if (hasTone('calm'))
200    phrasesByTone.push('"Reliable tools, clearly documented, quietly maintained."');
201  if (hasTone('bold')) phrasesByTone.push('"Pick it up. It works. Put it down."');
202  if (hasTone('warm'))
203    phrasesByTone.push('"Made for people who care about their tools."');
204
205  const catPhrases: Record<CategoryType, string[]> = {
206    developer: [
207      '"One command. Done."',
208      '"Less ceremony. More output."',
209      '"Configure once. Forget about it."',
210    ],
211    creative: [
212      '"The work is the argument."',
213      '"No portfolio lorem ipsum."',
214      '"Good work, clearly presented."',
215    ],
216    product: [
217      '"It fits where you already work."',
218      '"No setup tax."',
219      '"Does one thing. Does it well."',
220    ],
221    services: [
222      '"You\'ve seen this problem before. So have we."',
223      '"Specific outcomes, clear process."',
224      '"We don\'t do vague."',
225    ],
226    personal: [
227      '"This is what I work on."',
228      '"Specific interests, honest opinions."',
229      '"No personal brand. Just work."',
230    ],
231    general: [
232      '"Built for a reason."',
233      '"Useful before impressive."',
234      '"Does what it says."',
235    ],
236  };
237
238  const allPhrases = [
239    ...phrasesByTone,
240    ...(catPhrases[catType] ?? catPhrases.general),
241  ];
242
243  return {
244    attributes: tone.length > 0 ? tone : ['direct', 'clear'],
245    voiceNotes,
246    avoidList,
247    examplePhrases: pickN(allPhrases, Math.min(4, allPhrases.length)),
248  };
249}
250
251// ── Titles ────────────────────────────────────────────────────────────────────
252
253function generateTitles(ctx: GenContext): string[] {
254  const { name, category, purpose, audience, hasTone } = ctx;
255
256  const purposeWords = purpose.split(/\s+/);
257  const coreVerb = purposeWords[0] ?? 'build';
258  const audienceShort = audience.split(/\s+/).slice(0, 3).join(' ');
259
260  // Distinct structural patterns — each must look meaningfully different
261  const variants: string[] = [
262    `${name}${category} for ${audienceShort}`,
263    `${name} / ${audience}`,
264    `${name}: ${cap(coreVerb)} without ceremony`,
265    `${name} for ${audienceShort}`,
266    `${name} — built to ${coreVerb}`,
267  ];
268
269  // Tone-specific variants
270  if (hasTone('minimal')) variants.push(`${name}.`);
271  if (hasTone('technical')) {
272    variants.push(`${name}${category} utility`);
273    variants.push(`${name}${coreVerb}, ship, repeat`);
274  }
275  if (hasTone('bold')) {
276    variants.push(`${name}. Built for ${audienceShort}.`);
277    variants.push(`${name}. No ceremony.`);
278  }
279  if (hasTone('calm')) variants.push(`${name} — a ${category} for ${audienceShort}`);
280
281  // Deduplicate and ensure first is always bare name
282  const pool = variants.filter(v => v !== name);
283  const extras = pickN(pool, 2);
284  return [name, ...extras];
285}
286
287// ── Subtitles ─────────────────────────────────────────────────────────────────
288
289function generateSubtitles(ctx: GenContext): string[] {
290  const { name, category, purpose, audience } = ctx;
291  const purposeShort = purpose.split(/\s+/).slice(0, 6).join(' ');
292
293  const all: string[] = [
294    `${cap(category)} for ${audience}.`,
295    `${cap(purpose)}.`,
296    `A ${category} built to ${purpose}.`,
297    `Built for ${audience} who need to ${purposeShort}.`,
298    `${name}: the ${category} for ${audience}.`,
299    `${cap(category)}. For ${audience}.`,
300    `${cap(purpose)}. No overhead.`,
301    `The ${category} for ${audience} who know what they need.`,
302  ];
303
304  return pickN(all, 3);
305}
306
307// ── Taglines ──────────────────────────────────────────────────────────────────
308
309function generateTaglines(ctx: GenContext): string[] {
310  const { category, purpose, audience, catType, hasTone } = ctx;
311
312  const purposeWords = purpose.split(/\s+/);
313  const coreVerb = purposeWords[0] ?? 'build';
314  // Take only the object noun (skip prepositions like "without", "for", "with")
315  const stopWords = new Set(['without', 'with', 'for', 'and', 'or', 'the', 'a', 'an']);
316  const coreNounWords = purposeWords.slice(1).filter(w => !stopWords.has(w.toLowerCase()));
317  const coreNoun = coreNounWords[0] || 'your work';
318  const audienceShort = audience.split(/\s+/).slice(0, 2).join(' ');
319
320  const catTaglines: Record<CategoryType, string[]> = {
321    developer: [
322      `${cap(coreVerb)}, ship, move on.`,
323      `Less boilerplate. More control.`,
324      `${cap(category)} that stays out of your way.`,
325      `One command. Done.`,
326      `Configure once. Forget about it.`,
327      `Less ceremony. More output.`,
328      `Built for ${audienceShort} who ship.`,
329      `${cap(coreNoun)}, no overhead.`,
330    ],
331    creative: [
332      `The work is the argument.`,
333      `Good work, clearly presented.`,
334      `No introduction needed.`,
335      `${cap(coreNoun)}, done properly.`,
336      `Craft over noise.`,
337      `Say less. Show more.`,
338      `Quality, consistently.`,
339    ],
340    product: [
341      `Does one thing. Does it well.`,
342      `Fits where you already work.`,
343      `No setup tax.`,
344      `Built for ${audienceShort}.`,
345      `${cap(coreVerb)} without the friction.`,
346      `One less problem.`,
347      `Useful before impressive.`,
348    ],
349    services: [
350      `You've seen this problem before. So have we.`,
351      `Specific outcomes. Clear process.`,
352      `Experience, not enthusiasm.`,
353      `We don't do vague.`,
354      `The result is the product.`,
355      `Built for the problem you actually have.`,
356    ],
357    personal: [
358      `This is what I work on.`,
359      `Specific interests. Honest opinions.`,
360      `Work, not performance.`,
361      `Making things that matter.`,
362      `No brand. Just work.`,
363    ],
364    general: [
365      `Built for a reason.`,
366      `Useful before impressive.`,
367      `Does what it says.`,
368      `${cap(coreVerb)} without the noise.`,
369      `For ${audienceShort} who know what they want.`,
370    ],
371  };
372
373  const toneTaglines: string[] = [];
374  if (hasTone('minimal')) toneTaglines.push(`${cap(coreVerb)}. Ship.`, `Simple by design.`);
375  if (hasTone('calm'))
376    toneTaglines.push(`Reliable tools, quietly maintained.`, `Steady. Dependable. Yours.`);
377  if (hasTone('bold'))
378    toneTaglines.push(`Pick it up. It works.`, `No compromises.`, `Built to be used.`);
379  if (hasTone('technical'))
380    toneTaglines.push(`Typed. Composable. Predictable.`, `Deterministic by design.`);
381
382  const pool = [...(catTaglines[catType] ?? catTaglines.general), ...toneTaglines];
383  return pickN(pool, 3);
384}
385
386// ── Visual Directions ─────────────────────────────────────────────────────────
387
388function generateVisualDirections(ctx: GenContext): VisualDirection[] {
389  const { catType, hasTone } = ctx;
390
391  const allDirections: VisualDirection[] = [];
392
393  if (catType === 'developer' || hasTone('technical') || hasTone('minimal')) {
394    allDirections.push(
395      {
396        id: 'terminal-minimal',
397        name: 'Terminal Minimal',
398        description:
399          'Dark background, monospace type throughout, no ornament. Functional and uncompromising. Every element earns its place.',
400        palette: 'Near-black ground (#0d0d0d), off-white text (#e0e0e0), single muted accent (amber or green).',
401        typography: 'Monospace primary — JetBrains Mono or Iosevka. Consistent weight. No italic.',
402        references: 'htop, k9s, the Stripe CLI, Linear issue view.',
403      },
404      {
405        id: 'technical-document',
406        name: 'Technical Document',
407        description:
408          'Off-white ground, dense information layout, RFC/spec aesthetic. Designed for reading, not scanning. Typography does all the work.',
409        palette: 'Warm white (#f5f3ef), dark text (#1a1a1a), minimal color — one functional accent only.',
410        typography: 'Sans-serif (Inter or similar) for body, mono for code. Tight line height. Strong hierarchy through size and weight alone.',
411        references: 'Stripe docs, Oxide Computer RFCs, GNU manpages reformatted.',
412      },
413      {
414        id: 'precision-interface',
415        name: 'Precision Interface',
416        description:
417          'Neutral mid-range palette, strong grid, engineering-tool aesthetic. Balanced between document and application. Calm but capable.',
418        palette: 'Mid-grey ground (#f0f0f0 or #1c1c1c), charcoal type, restrained use of blue or slate as action color.',
419        typography: 'Sans-serif for UI, mono for data. Clear size differentiation. No decorative weight use.',
420        references: 'Figma sidebar, Retool, Zed editor, TablePlus.',
421      }
422    );
423  }
424
425  if (catType === 'creative' || catType === 'personal') {
426    allDirections.push(
427      {
428        id: 'editorial',
429        name: 'Editorial',
430        description:
431          'Strong typographic hierarchy, restrained palette, print-design influences. Work foreground, everything else background.',
432        palette: 'Off-white or cream (#f7f4ef), near-black type, accent used once — a single warm or cool tone.',
433        typography: 'A good serif for display, neutral sans for body. Generous leading. No decorative fonts.',
434        references: 'Are.na, Typewolf, Emigre back catalog, Letterform Archive.',
435      },
436      {
437        id: 'quiet-studio',
438        name: 'Quiet Studio',
439        description:
440          'Neutral and considered. Nothing decorative. Space used to direct attention, not fill it.',
441        palette: 'Warm white (#fafaf8) or deep neutral (#141414), type-only color use. No gradients.',
442        typography: 'One typeface family, two weights. Let leading and spacing create rhythm.',
443        references: 'Pentagram case studies, Swiss International Style, Muji product design.',
444      },
445      {
446        id: 'contemporary-craft',
447        name: 'Contemporary Craft',
448        description:
449          'Tactile references — paper, grain, texture — applied with restraint. Warmth without nostalgia.',
450        palette: 'Off-white base with a warm paper tone, subtle texture overlays, earthy accent.',
451        typography: 'Mix: display serif + utility sans. Comfortable reading size. Generous margins.',
452        references: 'Oak Studio, Analog, Offscreen Magazine, Present & Correct.',
453      }
454    );
455  }
456
457  if (catType === 'product' || catType === 'services') {
458    allDirections.push(
459      {
460        id: 'focused-product',
461        name: 'Focused Product',
462        description:
463          'Clean, professional, information-forward. Looks like it was built to be used, not to be admired. Trust through clarity.',
464        palette: 'White or light grey ground, dark neutral type, one brand color used only for primary actions.',
465        typography: 'Neutral sans-serif, systematic sizing, no personality — the product is the personality.',
466        references: 'Linear, Cron (v1), Vercel dashboard, Raycast.',
467      },
468      {
469        id: 'minimal-commerce',
470        name: 'Minimal Commerce',
471        description:
472          'Premium restraint. No decorative elements. White space signals quality. Type-driven.',
473        palette: 'White ground, black type, one warm accent for selective emphasis.',
474        typography: 'A refined sans-serif. Large display size for key claims. Small, tracked caps for labels.',
475        references: 'Stripe marketing, Basecamp, Arc browser landing page.',
476      },
477      {
478        id: 'structured-trust',
479        name: 'Structured Trust',
480        description:
481          'Grid-heavy, methodical, legible. Communicates that things are in order. More function, less flourish.',
482        palette: 'Light neutral ground, two text weights (body + emphasis), a contained accent color.',
483        typography: 'Professional sans-serif — GT Walsheim or Plus Jakarta or similar. Tight tracking for headings.',
484        references: 'Harvest app, FreshBooks, Notion, Loom landing page.',
485      }
486    );
487  }
488
489  if (allDirections.length === 0) {
490    allDirections.push(
491      {
492        id: 'type-forward',
493        name: 'Type Forward',
494        description:
495          'Typography as the only design element. No illustration, no photography, no pattern. Words do everything.',
496        palette: 'Black and white, one optional accent. No gradients.',
497        typography: 'One great typeface. Multiple weights. Extreme size contrast. Nothing else needed.',
498        references: 'Early Bloomberg Businessweek covers, Helvetica film poster, The Economist.',
499      },
500      {
501        id: 'system-neutral',
502        name: 'System Neutral',
503        description:
504          'Invisible design — system fonts, default spacing, no signature. The brand is in the content, not the container.',
505        palette: 'System defaults. One custom color — the brand color. Everything else inherited.',
506        typography: 'System UI stack. Optimized for the OS it runs on.',
507        references: 'HN, iA Writer, Pinboard, older Stripe.',
508      }
509    );
510  }
511
512  return pickN(allDirections, Math.min(3, allDirections.length));
513}
514
515// ── Logo Concepts ─────────────────────────────────────────────────────────────
516
517function generateLogoConcepts(ctx: GenContext): LogoConcept[] {
518  const { name, catType } = ctx;
519  const initial = name.charAt(0).toUpperCase();
520  const initials =
521    name
522      .split(/\s+/)
523      .slice(0, 2)
524      .map(w => w.charAt(0).toUpperCase())
525      .join('') || initial;
526
527  const concepts: LogoConcept[] = [];
528
529  if (catType === 'developer') {
530    concepts.push(
531      {
532        id: 'geometric-letterform',
533        title: 'Geometric Letterform',
534        concept: `The letter "${initial}" treated as a structural element — not styled, just precise. Think grid construction, not calligraphy.`,
535        mark: 'Monoweight geometric construction. Works at 16px and 1600px. No gradients, no effects.',
536        execution:
537          'Build on a strict grid. Consider negative space as intentional, not leftover. Test at 16×16 favicon size first.',
538      },
539      {
540        id: 'wordmark-mono',
541        title: 'Wordmark in Mono',
542        concept: `"${name}" set in a monospace typeface, tracked slightly loose. The choice of mono is the signal.`,
543        mark: 'Wordmark only. No icon. The name is the mark.',
544        execution:
545          'Try JetBrains Mono, Iosevka, or Commit Mono at medium weight. Adjust tracking. Optically align.',
546      },
547      {
548        id: 'abstract-structure',
549        title: 'Abstract Structure',
550        concept:
551          'A geometric mark suggesting assembly, layering, or composition — aligned with the product metaphor.',
552        mark: 'Two or three simple shapes in precise relation. No ornamentation.',
553        execution:
554          'Explore grid fragments, interlocking forms, or stacked bars. Test inversion on dark and light.',
555      }
556    );
557  } else if (catType === 'creative' || catType === 'personal') {
558    concepts.push(
559      {
560        id: 'custom-wordmark',
561        title: 'Custom Wordmark',
562        concept: `"${name}" as a custom letterform — not a font off the shelf, but drawn. The craft shows.`,
563        mark: 'Wordmark with subtle custom refinements: adjusted spacing, modified terminals, intentional details.',
564        execution:
565          'Start with a base typeface. Modify key letterforms. The goal is invisible craft, not obvious customization.',
566      },
567      {
568        id: 'monogram',
569        title: 'Monogram',
570        concept: `"${initials}" as a tight, legible monogram. Simple enough to stamp, refined enough to scale up.`,
571        mark: 'Two letterforms in structural relation. Not overlapping decoratively — compositionally.',
572        execution:
573          'Grid-align. Consider positive/negative figure-ground play. Must read clearly at 24px.',
574      }
575    );
576  } else {
577    concepts.push(
578      {
579        id: 'clean-wordmark',
580        title: 'Wordmark',
581        concept: `"${name}" set in a well-chosen typeface, thoughtfully spaced. No icon needed.`,
582        mark: 'Wordmark. The typeface selection and spacing carry the identity.',
583        execution:
584          'Choose a typeface with character but not personality. Adjust tracking. Optically center.',
585      },
586      {
587        id: 'initial-mark',
588        title: `"${initial}" Mark`,
589        concept: `A standalone "${initial}" mark that works as a favicon, app icon, and small-scale identifier.`,
590        mark: 'Single letter, geometric or structured. Consistent weight with wordmark.',
591        execution:
592          'Build on an 8-unit grid. Test at 16px, 32px, and 512px. Must work in one color.',
593      }
594    );
595  }
596
597  concepts.push({
598    id: 'symbol-plus-wordmark',
599    title: 'Symbol + Wordmark System',
600    concept:
601      'A mark system: standalone symbol for small contexts, symbol + name for full contexts. Flexible.',
602    mark: 'Two formats: symbol alone, symbol left-aligned with wordmark right.',
603    execution:
604      'Define the relationship (size ratio, spacing) precisely. Lock it. Never deviate. Test both formats in context.',
605  });
606
607  return pickN(concepts, 2);
608}
609
610// ── Usage Examples ────────────────────────────────────────────────────────────
611
612function generateUsageExamples(ctx: GenContext): UsageExample[] {
613  const { name, category, purpose, audience, catType } = ctx;
614  // Take a clean verb phrase for landing copy — stop before prepositions
615  const stopWords = new Set(['without', 'for', 'with', 'and', 'or', 'the', 'a', 'an', 'via', 'using']);
616  const purposeWords = purpose.split(/\s+/);
617  const heroWords: string[] = [];
618  for (const w of purposeWords) {
619    if (stopWords.has(w.toLowerCase()) && heroWords.length > 0) break;
620    heroWords.push(w);
621  }
622  const heroVerb = heroWords.join(' ') || purpose;
623
624  const examples: UsageExample[] = [
625    {
626      context: 'README header',
627      text: `# ${name}\n\n${cap(category)} for ${audience}. ${cap(purpose)}.`,
628    },
629    {
630      context: 'Landing page hero',
631      text: `${cap(heroVerb)} without ceremony.\n\n${name} is a ${category} built for ${audience} who need to ${purpose}.`,
632    },
633    {
634      context: 'Social / bio',
635      text: `Building ${name}${category} for ${audience}. ${cap(purpose)}, no overhead.`,
636    },
637    {
638      context: 'One-liner',
639      text: `${name}: ${category} built to ${purpose}.`,
640    },
641  ];
642
643  if (catType === 'developer') {
644    examples.push({
645      context: 'Package registry description',
646      text: `${name} is a ${category} for ${audience}. ${cap(purpose)}. No configuration required.`,
647    });
648    examples.push({
649      context: 'CLI help text intro',
650      text: `${name}${cap(purpose)}.`,
651    });
652  }
653
654  if (catType === 'creative' || catType === 'personal') {
655    examples.push({
656      context: 'Portfolio about line',
657      text: `${name} is the practice of ${audience.split(/\s+/).slice(0, 2).join(' ')} ${heroVerb}.`,
658    });
659  }
660
661  if (catType === 'product') {
662    examples.push({
663      context: 'App store description',
664      text: `${name} is a ${category} for ${audience}. It ${purpose} — without the complexity of larger platforms.`,
665    });
666  }
667
668  return examples.slice(0, 6);
669}
670
671// ── Constraints ───────────────────────────────────────────────────────────────
672
673function generateConstraints(ctx: GenContext): string[] {
674  const { tone, avoid, hasTone } = ctx;
675  const list: string[] = [];
676
677  if (tone.length > 0) {
678    list.push(`Tone: ${tone.join(', ')}.`);
679  }
680
681  if (avoid.length > 0) {
682    list.push(`Avoid: ${avoid.join(', ')}.`);
683  }
684
685  if (hasTone('minimal')) {
686    list.push('Headlines: 6 words maximum.');
687    list.push('Body copy: 2 sentences per paragraph maximum.');
688  }
689
690  if (hasTone('technical')) {
691    list.push('Assume reader has domain knowledge. Skip definitions.');
692    list.push('Prefer specific nouns over categorical ones (say the actual thing).');
693  }
694
695  if (hasTone('calm')) {
696    list.push('No urgency language ("act now", "limited time", "don\'t miss").');
697    list.push('No exclamation points.');
698  }
699
700  if (hasTone('bold')) {
701    list.push('Active voice always. No passive constructions.');
702    list.push('Every claim should be substantiable.');
703  }
704
705  list.push('No em dashes in casual contexts. Use a period or restructure.');
706  list.push('Spell out numbers under 10 in prose. Use numerals for data.');
707  list.push('One idea per sentence. Split if in doubt.');
708
709  return list;
710}
711
712// ── Typography ────────────────────────────────────────────────────────────────
713
714export const TYPE_SCALE: TypographyToken[] = [
715  { label: 'Display',    size: '56px', weight: '700', lineHeight: '1.1',  usage: 'Hero headlines, major landing sections' },
716  { label: 'Heading 1',  size: '40px', weight: '700', lineHeight: '1.2',  usage: 'Page titles, primary headers' },
717  { label: 'Heading 2',  size: '28px', weight: '600', lineHeight: '1.25', usage: 'Section headers, card titles' },
718  { label: 'Heading 3',  size: '20px', weight: '600', lineHeight: '1.3',  usage: 'Sub-section headers, feature titles' },
719  { label: 'Body Large', size: '18px', weight: '400', lineHeight: '1.6',  usage: 'Lead paragraphs, key descriptions' },
720  { label: 'Body',       size: '16px', weight: '400', lineHeight: '1.65', usage: 'Default body copy' },
721  { label: 'Caption',    size: '13px', weight: '400', lineHeight: '1.5',  usage: 'Meta info, timestamps, helper text' },
722  { label: 'Label',      size: '11px', weight: '600', lineHeight: '1.4',  usage: 'UI labels, tags, overlines' },
723];
724
725type FontPair = { primary: string; secondary: string; mono: string; pairNote: string };
726
727const FONT_PAIRS: Record<string, FontPair[]> = {
728  developer: [
729    { primary: 'Geist',         secondary: 'Geist',         mono: 'Geist Mono',       pairNote: 'Single-family system. Clean, neutral, interface-optimized.' },
730    { primary: 'Inter',         secondary: 'Inter',         mono: 'JetBrains Mono',   pairNote: 'Inter for all UI copy; JetBrains Mono for code.' },
731    { primary: 'IBM Plex Sans', secondary: 'IBM Plex Sans', mono: 'IBM Plex Mono',    pairNote: 'IBM Plex family — coherent, technical, widely legible.' },
732  ],
733  creative: [
734    { primary: 'Playfair Display',    secondary: 'Lato',       mono: 'Courier Prime', pairNote: 'High-contrast editorial serif for display; Lato for body.' },
735    { primary: 'Fraunces',            secondary: 'DM Sans',    mono: 'DM Mono',       pairNote: 'Optical-size serif for headlines; DM Sans for body. Expressive and modern.' },
736    { primary: 'Cormorant Garamond',  secondary: 'Nunito Sans', mono: 'Courier Prime', pairNote: 'Refined luxury serif for display; Nunito Sans for readable body.' },
737  ],
738  product: [
739    { primary: 'Plus Jakarta Sans', secondary: 'Plus Jakarta Sans', mono: 'DM Mono',        pairNote: 'Jakarta Sans at varying weights; DM Mono for data and code.' },
740    { primary: 'Inter',             secondary: 'Inter',             mono: 'Fira Code',       pairNote: 'Inter throughout — modern, neutral, excellent hinting.' },
741    { primary: 'Manrope',           secondary: 'Manrope',           mono: 'JetBrains Mono',  pairNote: 'Geometric Manrope for all UI; JetBrains Mono for code blocks.' },
742  ],
743  services: [
744    { primary: 'Libre Baskerville', secondary: 'Source Sans 3', mono: 'Source Code Pro', pairNote: 'Baskerville for authority and trust; Source Sans for approachable body.' },
745    { primary: 'Merriweather',      secondary: 'Open Sans',     mono: 'Roboto Mono',     pairNote: 'Merriweather for credibility; Open Sans keeps body warm.' },
746    { primary: 'Lora',              secondary: 'Nunito Sans',   mono: 'Courier Prime',   pairNote: 'Lora brings warmth to headlines; Nunito Sans lightens the reading.' },
747  ],
748  personal: [
749    { primary: 'Lora',              secondary: 'Nunito',       mono: 'DM Mono',       pairNote: 'Lora for expressive headlines; Nunito for friendly body copy.' },
750    { primary: 'DM Serif Display',  secondary: 'DM Sans',      mono: 'DM Mono',       pairNote: 'Unified DM family. Serif display for character; sans for clarity.' },
751    { primary: 'Playfair Display',  secondary: 'Source Sans 3', mono: 'Courier Prime', pairNote: 'Playfair adds personality; Source Sans 3 grounds body text.' },
752  ],
753  general: [
754    { primary: 'Inter',             secondary: 'Inter',        mono: 'JetBrains Mono', pairNote: 'Inter throughout with weight variation. Universal starting point.' },
755    { primary: 'Plus Jakarta Sans', secondary: 'Lora',         mono: 'Fira Code',      pairNote: 'Geometric sans for UI; Lora serif for long-form content.' },
756    { primary: 'Manrope',           secondary: 'Merriweather', mono: 'Source Code Pro', pairNote: 'Friendly geometric sans paired with a trusted editorial serif.' },
757  ],
758};
759
760function generateTypography(ctx: GenContext): Typography {
761  const pairs = FONT_PAIRS[ctx.catType] ?? FONT_PAIRS.general;
762  return { ...pick(pairs), scale: TYPE_SCALE };
763}
764
765// ── Color Palette ─────────────────────────────────────────────────────────────
766
767function makeSwatches(entries: [string, string, string][]): ColorSwatch[] {
768  return entries.map(([name, hex, role], i) => ({ id: `s${i}`, name, hex, role }));
769}
770
771function generateColorPalette(ctx: GenContext): ColorPalette {
772  type SwatchEntry = [string, string, string]; // [name, hex, role]
773  type PaletteSet = SwatchEntry[][];
774
775  const palettes: Record<string, PaletteSet> = {
776    developer: [
777      [
778        ['Background', '#0d1117', 'background'],
779        ['Surface', '#161b22', 'neutral'],
780        ['Border', '#30363d', 'neutral'],
781        ['Primary', '#58a6ff', 'primary'],
782        ['Accent', '#3fb950', 'accent'],
783        ['Text', '#f0f6fc', 'text'],
784      ],
785      [
786        ['Background', '#0a0010', 'background'],
787        ['Surface', '#160025', 'neutral'],
788        ['Neutral', '#2d1f3d', 'neutral'],
789        ['Primary', '#7c3aed', 'primary'],
790        ['Accent', '#a78bfa', 'accent'],
791        ['Text', '#e2d9f3', 'text'],
792      ],
793      [
794        ['Background', '#0a0a0a', 'background'],
795        ['Surface', '#141414', 'neutral'],
796        ['Neutral', '#292929', 'neutral'],
797        ['Primary', '#e5e5e5', 'primary'],
798        ['Accent', '#c9a96e', 'accent'],
799        ['Text', '#f5f5f5', 'text'],
800      ],
801    ],
802    creative: [
803      [
804        ['Background', '#faf7f2', 'background'],
805        ['Surface', '#f5efe4', 'neutral'],
806        ['Neutral', '#e8d9c4', 'neutral'],
807        ['Primary', '#c07850', 'primary'],
808        ['Accent', '#4a7c5f', 'accent'],
809        ['Text', '#1a1410', 'text'],
810      ],
811      [
812        ['Background', '#0f0f0f', 'background'],
813        ['Surface', '#1a1a1a', 'neutral'],
814        ['Neutral', '#2e2e2e', 'neutral'],
815        ['Primary', '#ff6b35', 'primary'],
816        ['Accent', '#ffd700', 'accent'],
817        ['Text', '#f8f8f8', 'text'],
818      ],
819      [
820        ['Background', '#f8f4ef', 'background'],
821        ['Surface', '#efe9e0', 'neutral'],
822        ['Neutral', '#d4c4b0', 'neutral'],
823        ['Primary', '#8b5e3c', 'primary'],
824        ['Accent', '#6b8f71', 'accent'],
825        ['Text', '#2c2018', 'text'],
826      ],
827    ],
828    product: [
829      [
830        ['Background', '#fafbfc', 'background'],
831        ['Surface', '#f0f4f8', 'neutral'],
832        ['Neutral', '#d1dce8', 'neutral'],
833        ['Primary', '#2563eb', 'primary'],
834        ['Accent', '#7c3aed', 'accent'],
835        ['Text', '#1e293b', 'text'],
836      ],
837      [
838        ['Background', '#0f172a', 'background'],
839        ['Surface', '#1e293b', 'neutral'],
840        ['Neutral', '#334155', 'neutral'],
841        ['Primary', '#6366f1', 'primary'],
842        ['Accent', '#22d3ee', 'accent'],
843        ['Text', '#f1f5f9', 'text'],
844      ],
845      [
846        ['Background', '#f0fafa', 'background'],
847        ['Surface', '#e0f5f5', 'neutral'],
848        ['Neutral', '#b2dede', 'neutral'],
849        ['Primary', '#0d9488', 'primary'],
850        ['Accent', '#f59e0b', 'accent'],
851        ['Text', '#134e4a', 'text'],
852      ],
853    ],
854    services: [
855      [
856        ['Background', '#f8fafd', 'background'],
857        ['Surface', '#edf2fa', 'neutral'],
858        ['Neutral', '#ccd9ee', 'neutral'],
859        ['Primary', '#1d4ed8', 'primary'],
860        ['Accent', '#0f9e6e', 'accent'],
861        ['Text', '#1a2038', 'text'],
862      ],
863      [
864        ['Background', '#0c1421', 'background'],
865        ['Surface', '#152035', 'neutral'],
866        ['Neutral', '#243450', 'neutral'],
867        ['Primary', '#3b82f6', 'primary'],
868        ['Accent', '#34d399', 'accent'],
869        ['Text', '#f8fafc', 'text'],
870      ],
871      [
872        ['Background', '#faf8f5', 'background'],
873        ['Surface', '#f0ebe0', 'neutral'],
874        ['Neutral', '#d9cdb8', 'neutral'],
875        ['Primary', '#78523a', 'primary'],
876        ['Accent', '#2d6a4f', 'accent'],
877        ['Text', '#1c1410', 'text'],
878      ],
879    ],
880    personal: [
881      [
882        ['Background', '#fffef9', 'background'],
883        ['Surface', '#fdf8ee', 'neutral'],
884        ['Neutral', '#f0e6cc', 'neutral'],
885        ['Primary', '#c9a96e', 'primary'],
886        ['Accent', '#7c9e87', 'accent'],
887        ['Text', '#2d2d2d', 'text'],
888      ],
889      [
890        ['Background', '#fafafa', 'background'],
891        ['Surface', '#f5f5f5', 'neutral'],
892        ['Neutral', '#e5e5e5', 'neutral'],
893        ['Primary', '#171717', 'primary'],
894        ['Accent', '#737373', 'accent'],
895        ['Text', '#404040', 'text'],
896      ],
897      [
898        ['Background', '#f9f8ff', 'background'],
899        ['Surface', '#f0eeff', 'neutral'],
900        ['Neutral', '#ddd8f7', 'neutral'],
901        ['Primary', '#4f46e5', 'primary'],
902        ['Accent', '#ec4899', 'accent'],
903        ['Text', '#1e1b4b', 'text'],
904      ],
905    ],
906    general: [
907      [
908        ['Background', '#111111', 'background'],
909        ['Surface', '#1a1a1a', 'neutral'],
910        ['Neutral', '#2e2e2e', 'neutral'],
911        ['Primary', '#c9a96e', 'primary'],
912        ['Accent', '#6b8f71', 'accent'],
913        ['Text', '#dedede', 'text'],
914      ],
915      [
916        ['Background', '#ffffff', 'background'],
917        ['Surface', '#f5f5f5', 'neutral'],
918        ['Neutral', '#e0e0e0', 'neutral'],
919        ['Primary', '#1a1a1a', 'primary'],
920        ['Accent', '#3b82f6', 'accent'],
921        ['Text', '#333333', 'text'],
922      ],
923      [
924        ['Background', '#0f0f1a', 'background'],
925        ['Surface', '#1a1a2e', 'neutral'],
926        ['Neutral', '#252545', 'neutral'],
927        ['Primary', '#4f8ef7', 'primary'],
928        ['Accent', '#a78bfa', 'accent'],
929        ['Text', '#e2e8f0', 'text'],
930      ],
931    ],
932  };
933
934  const pool = palettes[ctx.catType] ?? palettes.general;
935  return { swatches: makeSwatches(pick(pool) as SwatchEntry[]) };
936}
937
938// ── Entry point ───────────────────────────────────────────────────────────────
939
940export function generate(inputs: BrandInputs): BrandOutputs {
941  const ctx = buildContext(inputs);
942
943  return {
944    overview: generateOverview(ctx),
945    positioning: generatePositioning(ctx),
946    tone: generateToneGuidance(ctx),
947    titles: generateTitles(ctx),
948    subtitles: generateSubtitles(ctx),
949    taglines: generateTaglines(ctx),
950    visualDirections: generateVisualDirections(ctx),
951    palette: generateColorPalette(ctx),
952    typography: generateTypography(ctx),
953    logoConcepts: generateLogoConcepts(ctx),
954    usageExamples: generateUsageExamples(ctx),
955    constraints: generateConstraints(ctx),
956  };
957}