/** * carouselSchema.ts * Zod schemas and TypeScript types for the job-carousel pipeline. * All pipeline stages use these types for type safety and runtime validation. */ import { z } from 'zod'; // --------------------------------------------------------------------------- // Education — structured extraction from resume education section // --------------------------------------------------------------------------- export const EducationSchema = z.object({ degree: z.string(), institution: z.string(), graduationYear: z.string().optional(), field: z.string().optional(), }); export type Education = z.infer; // --------------------------------------------------------------------------- // TimelineEntry — a single node on the career/education timeline slide // --------------------------------------------------------------------------- export const TimelineEntrySchema = z.object({ year: z.string(), label: z.string(), // job title or degree sublabel: z.string().optional(), // employer or institution isHighlighted: z.boolean().default(false), // relevant to target role isEducation: z.boolean().default(false), // education node (vs work) isCurrent: z.boolean().default(false), }); export type TimelineEntry = z.infer; // --------------------------------------------------------------------------- // CandidateProfile — structured extraction from resume // --------------------------------------------------------------------------- export const RoleSchema = z.object({ title: z.string(), employer: z.string(), startDate: z.string().optional(), // e.g. "2020-01" or "Jan 2020" endDate: z.string().optional(), // "Present" or date string isCurrent: z.boolean().default(false), bullets: z.array(z.string()), }); export type Role = z.infer; export const CandidateProfileSchema = z.object({ roles: z.array(RoleSchema), employers: z.array(z.string()), education: z.array(EducationSchema).default([]), allBullets: z.array(z.string()), // flat list of all achievement bullets projects: z.array(z.string()), // named projects or initiatives mentioned tools: z.array(z.string()), // tools, languages, platforms metrics: z.array(z.string()), // raw metric strings extracted ("40%", "$2M") domains: z.array(z.string()), // industries or functional areas ownershipIndicators: z.array(z.string()), // verbs suggesting ownership ("led", "built", "owned") collaborationIndicators: z.array(z.string()), // verbs suggesting participation rawText: z.string(), // original resume text for fabrication checks }); export type CandidateProfile = z.infer; // --------------------------------------------------------------------------- // TargetRoleProfile — structured extraction from job description // --------------------------------------------------------------------------- export const TargetRoleProfileSchema = z.object({ title: z.string(), seniority: z.enum(['entry', 'mid', 'senior', 'staff', 'principal', 'director', 'unknown']), company: z.string().optional(), requiredSkills: z.array(z.string()), preferredSkills: z.array(z.string()), domainClues: z.array(z.string()), // industry, business type, product area recruiterPriorities: z.array(z.string()), // top themes the JD emphasizes themes: z.array(z.string()), // broad themes: "scale", "cross-functional", "0-to-1" leadershipSignals: z.array(z.string()), // leadership/scope signals from JD rawText: z.string(), }); export type TargetRoleProfile = z.infer; // --------------------------------------------------------------------------- // FitAnalysis — result of comparing CandidateProfile vs TargetRoleProfile // --------------------------------------------------------------------------- export const AlignmentItemSchema = z.object({ area: z.string(), // e.g. "A/B testing", "Python", "cross-functional leadership" evidence: z.string(), // specific resume phrase or bullet that supports this strength: z.number().min(0).max(10), }); export type AlignmentItem = z.infer; export const FitAnalysisSchema = z.object({ bestFitStory: z.string(), // 1–2 sentence narrative strongestAlignments: z.array(AlignmentItemSchema), // score >= 7 moderateAlignments: z.array(AlignmentItemSchema), // score 4–6 notableGaps: z.array(z.string()), // JD asks for things not in resume bestAchievements: z.array(z.string()), // top 5–7 bullets for this role overallFitScore: z.number().min(0).max(10), // aggregate fit signal inferredMode: z.enum(['recruiter', 'hiring-manager', 'personal-brand']), }); export type FitAnalysis = z.infer; // --------------------------------------------------------------------------- // HighlightCandidate — a scored resume bullet for ranking // --------------------------------------------------------------------------- export const ScoringWeightsSchema = z.object({ roleRelevance: z.number().default(0.25), businessImpact: z.number().default(0.20), quantitativeStrength: z.number().default(0.15), technicalDepth: z.number().default(0.10), ownership: z.number().default(0.10), scale: z.number().default(0.05), uniqueness: z.number().default(0.05), recency: z.number().default(0.05), recruiterAppeal: z.number().default(0.05), }); export type ScoringWeights = z.infer; export const HighlightScoresSchema = z.object({ roleRelevance: z.number().min(0).max(10), businessImpact: z.number().min(0).max(10), quantitativeStrength: z.number().min(0).max(10), technicalDepth: z.number().min(0).max(10), ownership: z.number().min(0).max(10), scale: z.number().min(0).max(10), uniqueness: z.number().min(0).max(10), recency: z.number().min(0).max(10), recruiterAppeal: z.number().min(0).max(10), }); export type HighlightScores = z.infer; export const HighlightCandidateSchema = z.object({ text: z.string(), // the original bullet text source: z.string(), // role/employer where this came from scores: HighlightScoresSchema, totalScore: z.number().min(0).max(10), rationale: z.string(), // why this was ranked here isSoftened: z.boolean().default(false), // true if phrasing was hedged for weak evidence softenedText: z.string().optional(), // the softened version if applicable }); export type HighlightCandidate = z.infer; // --------------------------------------------------------------------------- // PositioningAngle — the chosen narrative for the carousel // --------------------------------------------------------------------------- export const PositioningModeSchema = z.enum(['recruiter', 'hiring-manager', 'personal-brand']); export type PositioningMode = z.infer; export const PositioningAngleSchema = z.object({ headline: z.string(), // e.g. "Product analytics engineer who turns data into decisions" subheading: z.string(), // Supporting line (1 sentence) rationale: z.string(), // Why this angle was chosen mode: PositioningModeSchema, fitScore: z.number().min(0).max(10), clarityScore: z.number().min(0).max(10), distinctivenessScore: z.number().min(0).max(10), credibilityScore: z.number().min(0).max(10), compositeScore: z.number().min(0).max(10), }); export type PositioningAngle = z.infer; // --------------------------------------------------------------------------- // SlideContent — content for a single carousel slide // --------------------------------------------------------------------------- export const SlideTemplateTypeSchema = z.enum([ 'hook', 'timeline', 'fit', 'proof', 'workstyle', 'valueprop', 'cta', ]); export type SlideTemplateType = z.infer; export const SlideContentSchema = z.object({ slideNumber: z.number().int().min(1), templateType: SlideTemplateTypeSchema, title: z.string(), subtitle: z.string().optional(), body: z.string().optional(), bullets: z.array(z.string()).max(3).optional(), metric: z.string().optional(), // hero metric (one per slide max) cta: z.string().optional(), // call-to-action text (primarily for slide 7) timelineEntries: z.array(TimelineEntrySchema).optional(), // populated for 'timeline' slides speakerNote: z.string().optional(), // internal note for what this slide is doing }); export type SlideContent = z.infer; // Outline step before copy is written export const SlideOutlineSchema = z.object({ slideNumber: z.number().int().min(1), templateType: SlideTemplateTypeSchema, intent: z.string(), // what this slide must accomplish highlightRef: z.string().optional(), // which highlight this slide draws from positioningRef: z.string().optional(), // which positioning element it uses }); export type SlideOutline = z.infer; // --------------------------------------------------------------------------- // CarouselDocument — the full structured source of truth // --------------------------------------------------------------------------- export const CarouselDocumentSchema = z.object({ candidateProfile: CandidateProfileSchema, targetRoleProfile: TargetRoleProfileSchema, fitAnalysis: FitAnalysisSchema, highlights: z.array(HighlightCandidateSchema), positioningAngle: PositioningAngleSchema, slides: z.array(SlideContentSchema), }); export type CarouselDocument = z.infer; // --------------------------------------------------------------------------- // CarouselManifest — metadata about the generated package // --------------------------------------------------------------------------- export const CarouselManifestSchema = z.object({ generatedAt: z.string(), // ISO timestamp version: z.string(), // pipeline version inputFiles: z.object({ resume: z.string(), jd: z.string(), }), outputFiles: z.object({ pdf: z.string(), pngs: z.array(z.string()), caption: z.string(), json: z.string(), manifest: z.string(), }), mode: PositioningModeSchema, theme: z.string(), slideCount: z.number().int(), positioningAngle: z.object({ headline: z.string(), mode: PositioningModeSchema, compositeScore: z.number(), }), topHighlights: z.array(z.object({ text: z.string(), totalScore: z.number(), rationale: z.string(), })), notableGaps: z.array(z.string()), overallFitScore: z.number(), }); export type CarouselManifest = z.infer; // --------------------------------------------------------------------------- // Validation result type (shared across validators) // --------------------------------------------------------------------------- export const ValidationResultSchema = z.object({ valid: z.boolean(), field: z.string().optional(), message: z.string(), severity: z.enum(['error', 'warning', 'info']).default('error'), }); export type ValidationResult = z.infer;