import type { ResumeProfile, ResumeProjectCatalogItem, ResumeProjectsSettings, } from "@shared/types"; import { stripHtmlTags } from "@shared/utils/string"; type ResumeProjectSelectionItem = ResumeProjectCatalogItem & { summaryText: string; }; export function extractProjectsFromProfile(profile: ResumeProfile): { catalog: ResumeProjectCatalogItem[]; selectionItems: ResumeProjectSelectionItem[]; } { const items = profile?.sections?.projects?.items; if (!Array.isArray(items)) return { catalog: [], selectionItems: [] }; const catalog: ResumeProjectCatalogItem[] = []; const selectionItems: ResumeProjectSelectionItem[] = []; for (const item of items) { if (!item || typeof item !== "object") continue; const id = item.id || ""; if (!id) continue; const name = item.name || ""; const description = item.description || ""; const date = item.date || ""; const isVisibleInBase = Boolean(item.visible); const summary = item.summary || ""; const summaryText = stripHtml(summary); const base: ResumeProjectCatalogItem = { id, name, description, date, isVisibleInBase, }; catalog.push(base); selectionItems.push({ ...base, summaryText }); } return { catalog, selectionItems }; } export function buildDefaultResumeProjectsSettings( catalog: ResumeProjectCatalogItem[], ): ResumeProjectsSettings { const lockedProjectIds = catalog .filter((p) => p.isVisibleInBase) .map((p) => p.id); const lockedSet = new Set(lockedProjectIds); const aiSelectableProjectIds = catalog .map((p) => p.id) .filter((id) => !lockedSet.has(id)); const total = catalog.length; const preferredMax = Math.max(lockedProjectIds.length, 3); const maxProjects = total === 0 ? 0 : Math.min(total, preferredMax); return normalizeResumeProjectsSettings( { maxProjects, lockedProjectIds, aiSelectableProjectIds }, new Set(catalog.map((p) => p.id)), ); } export function parseResumeProjectsSettings( raw: string | null, ): ResumeProjectsSettings | null { if (!raw) return null; try { const parsed = JSON.parse(raw) as Partial; if (!parsed || typeof parsed !== "object") return null; const maxProjects = parsed.maxProjects; const lockedProjectIds = parsed.lockedProjectIds; const aiSelectableProjectIds = parsed.aiSelectableProjectIds; if (typeof maxProjects !== "number") return null; if ( !Array.isArray(lockedProjectIds) || !Array.isArray(aiSelectableProjectIds) ) return null; if (!lockedProjectIds.every((v: unknown) => typeof v === "string")) return null; if (!aiSelectableProjectIds.every((v: unknown) => typeof v === "string")) return null; return { maxProjects, lockedProjectIds, aiSelectableProjectIds, }; } catch { return null; } } export function normalizeResumeProjectsSettings( settings: ResumeProjectsSettings, allowedProjectIds?: ReadonlySet, ): ResumeProjectsSettings { const allowed = allowedProjectIds && allowedProjectIds.size > 0 ? allowedProjectIds : null; const lockedProjectIds = uniqueStrings(settings.lockedProjectIds).filter( (id) => (allowed ? allowed.has(id) : true), ); const lockedSet = new Set(lockedProjectIds); const aiSelectableProjectIds = uniqueStrings(settings.aiSelectableProjectIds) .filter((id) => (allowed ? allowed.has(id) : true)) .filter((id) => !lockedSet.has(id)); const maxCap = allowed ? allowed.size : Number.POSITIVE_INFINITY; const maxProjectsRaw = Number.isFinite(settings.maxProjects) ? settings.maxProjects : 0; const maxProjectsInt = Math.max(0, Math.floor(maxProjectsRaw)); const minRequired = lockedProjectIds.length; const maxProjects = Math.min(maxCap, Math.max(minRequired, maxProjectsInt)); return { maxProjects, lockedProjectIds, aiSelectableProjectIds }; } export function resolveResumeProjectsSettings(args: { catalog: ResumeProjectCatalogItem[]; overrideRaw: string | null; }): { profileProjects: ResumeProjectCatalogItem[]; defaultResumeProjects: ResumeProjectsSettings; overrideResumeProjects: ResumeProjectsSettings | null; resumeProjects: ResumeProjectsSettings; } { const profileProjects = args.catalog; const allowed = new Set(profileProjects.map((p) => p.id)); const defaultResumeProjects = buildDefaultResumeProjectsSettings(profileProjects); const overrideParsed = parseResumeProjectsSettings(args.overrideRaw); const overrideResumeProjects = overrideParsed ? normalizeResumeProjectsSettings(overrideParsed, allowed) : null; const resumeProjects = overrideResumeProjects ? normalizeResumeProjectsSettings(overrideResumeProjects, allowed) : defaultResumeProjects; return { profileProjects, defaultResumeProjects, overrideResumeProjects, resumeProjects, }; } export function stripHtml(input: string): string { return stripHtmlTags(input); } function uniqueStrings(values: string[]): string[] { const seen = new Set(); const out: string[] = []; for (const value of values) { const trimmed = value.trim(); if (!trimmed) continue; if (seen.has(trimmed)) continue; seen.add(trimmed); out.push(trimmed); } return out; } export type { ResumeProjectSelectionItem };