/** * buildOutline.ts * Generates the slide outline (structure + intent) from fit analysis, highlights, and positioning. * This is the planning stage before copy is written. */ import type { FitAnalysis, HighlightCandidate, PositioningAngle, SlideOutline, SlideTemplateType, } from '../schema/carouselSchema'; // --------------------------------------------------------------------------- // Default 7-slide structure // --------------------------------------------------------------------------- interface SlideBlueprint { templateType: SlideTemplateType; intent: string; pullHighlight: boolean; // Whether to assign a highlight reference pullPositioning: boolean; // Whether to reference the positioning angle } const DEFAULT_SLIDE_STRUCTURE: SlideBlueprint[] = [ { templateType: 'hook', intent: 'Identity hook: powerful recruiter-facing headline that stops the scroll. Not a generic title page — it must convey the positioning angle in a single punchy statement.', pullHighlight: false, pullPositioning: true, }, { templateType: 'fit', intent: 'Why this candidate fits this role: present 3 concise strength areas that map directly to the JD. Each card should name a skill/theme and connect it to a real experience.', pullHighlight: false, pullPositioning: false, }, { templateType: 'proof', intent: 'Signature proof slide: the single strongest role-relevant achievement. Lead with the metric or outcome. Give just enough context to make it credible.', pullHighlight: true, pullPositioning: false, }, { templateType: 'proof', intent: 'Supporting proof slide: second strongest achievement. Contrast with slide 3 — different skill area, different time period, or different scale of impact.', pullHighlight: true, pullPositioning: false, }, { templateType: 'workstyle', intent: 'How they work: operational or capability pattern. Not an achievement — a working philosophy or repeatable approach. Could be: "I move fast and validate early", "I build for maintainability first", "I align stakeholders before I write a line of code."', pullHighlight: false, pullPositioning: false, }, { templateType: 'valueprop', intent: 'Value proposition: synthesis of what this person brings to this specific role. The most direct "hire me for this" slide. Should feel like a closing argument, not a summary.', pullHighlight: false, pullPositioning: true, }, { templateType: 'cta', intent: 'CTA: clear next step. Open to opportunities, connect request, or portfolio/email link. Must not be generic — reference the target role or domain.', pullHighlight: false, pullPositioning: true, }, ]; // The timeline slide blueprint — inserted after hook when --timeline is used const TIMELINE_BLUEPRINT: SlideBlueprint = { templateType: 'timeline', intent: 'Career trajectory: visual timeline of roles (and education if relevant) showing the candidate\'s growth arc. Highlighted nodes mark experience most relevant to this role.', pullHighlight: false, pullPositioning: false, }; // Extended structures for 6 or 8 slides (without timeline) const SIX_SLIDE_STRUCTURE = DEFAULT_SLIDE_STRUCTURE.filter( (_, i) => i !== 4 // Remove workstyle slide for 6-slide version ); const EIGHT_SLIDE_STRUCTURE: SlideBlueprint[] = [ DEFAULT_SLIDE_STRUCTURE[0], // hook DEFAULT_SLIDE_STRUCTURE[1], // fit DEFAULT_SLIDE_STRUCTURE[2], // proof 1 DEFAULT_SLIDE_STRUCTURE[3], // proof 2 { // Extra proof slide for 8-slide version templateType: 'proof', intent: 'Third proof or domain signal: either a third achievement or a domain-specific credential (open source, publication, leadership program, etc.).', pullHighlight: true, pullPositioning: false, }, DEFAULT_SLIDE_STRUCTURE[4], // workstyle DEFAULT_SLIDE_STRUCTURE[5], // value prop DEFAULT_SLIDE_STRUCTURE[6], // cta ]; /** * Insert the timeline slide after hook in any structure. * The remaining slides keep their order. */ function withTimeline(structure: SlideBlueprint[]): SlideBlueprint[] { return [structure[0], TIMELINE_BLUEPRINT, ...structure.slice(1)]; } // --------------------------------------------------------------------------- // Outline builder // --------------------------------------------------------------------------- /** * Assign the N-th best highlight to a proof slide. * Ensures different slides get different highlights (no reuse). */ function assignHighlights( outlines: SlideBlueprint[], highlights: HighlightCandidate[] ): string[] { const highlightRefs: string[] = []; let highlightIndex = 0; for (const blueprint of outlines) { if (blueprint.pullHighlight && highlightIndex < highlights.length) { highlightRefs.push(highlights[highlightIndex].text); highlightIndex++; } else { highlightRefs.push(''); } } return highlightRefs; } /** * Build the ordered slide outline array given fit data, ranked highlights, and positioning angle. * When showTimeline is true, a timeline slide is inserted after the hook. */ export function buildCarouselOutline( fit: FitAnalysis, highlights: HighlightCandidate[], angle: PositioningAngle, slideCount: number, showTimeline = false ): SlideOutline[] { // Select the appropriate base structure // For 9-10 slides, use the 8-slide base and add extra proof slides let structure: SlideBlueprint[]; if (slideCount === 6) { structure = SIX_SLIDE_STRUCTURE; } else if (slideCount === 8) { structure = EIGHT_SLIDE_STRUCTURE; } else if (slideCount >= 9) { // Extra proof slides inserted before workstyle const extraProofs: SlideBlueprint[] = Array.from({ length: slideCount - 8 }, (_, k) => ({ templateType: 'proof' as const, intent: `Additional proof slide ${k + 4}: a domain credential, award, or notable project that adds credibility. Contrast with earlier proof slides on skill area and scope.`, pullHighlight: true, pullPositioning: false, })); structure = [ EIGHT_SLIDE_STRUCTURE[0], // hook EIGHT_SLIDE_STRUCTURE[1], // fit EIGHT_SLIDE_STRUCTURE[2], // proof 1 EIGHT_SLIDE_STRUCTURE[3], // proof 2 EIGHT_SLIDE_STRUCTURE[4], // proof 3 ...extraProofs, // proof 4+ (for 9+ slides) EIGHT_SLIDE_STRUCTURE[5], // workstyle EIGHT_SLIDE_STRUCTURE[6], // value prop EIGHT_SLIDE_STRUCTURE[7], // cta ]; } else { structure = DEFAULT_SLIDE_STRUCTURE; // default 7 } // Inject timeline slide after hook if requested if (showTimeline) { structure = withTimeline(structure); } const highlightRefs = assignHighlights(structure, highlights); return structure.map((blueprint, index) => ({ slideNumber: index + 1, templateType: blueprint.templateType, intent: blueprint.intent, highlightRef: blueprint.pullHighlight ? highlightRefs[index] : undefined, positioningRef: blueprint.pullPositioning ? angle.headline : undefined, })); }