import { z } from "zod"; /** * The resume document. This is the contract between the UI, the database * (Resume.data) and the MCP tools Claude calls. Keep it boring and flat — * every field is optional-with-default so a partially generated document * still renders. */ export const linkSchema = z.object({ label: z.string().default(""), url: z.string().default(""), }); export const headerSchema = z.object({ name: z.string().default(""), title: z.string().default(""), email: z.string().default(""), phone: z.string().default(""), location: z.string().default(""), links: z.array(linkSchema).default([]), }); export const experienceItemSchema = z.object({ id: z.string().default(""), company: z.string().default(""), title: z.string().default(""), location: z.string().default(""), startDate: z.string().default(""), endDate: z.string().default(""), isCurrent: z.boolean().default(false), summary: z.string().default(""), bullets: z.array(z.string()).default([]), /// Optional back-reference to the Role this was tailored from. roleId: z.string().optional(), }); export const educationItemSchema = z.object({ id: z.string().default(""), school: z.string().default(""), degree: z.string().default(""), field: z.string().default(""), location: z.string().default(""), startDate: z.string().default(""), endDate: z.string().default(""), details: z.array(z.string()).default([]), }); export const projectItemSchema = z.object({ id: z.string().default(""), name: z.string().default(""), role: z.string().default(""), url: z.string().default(""), startDate: z.string().default(""), endDate: z.string().default(""), description: z.string().default(""), bullets: z.array(z.string()).default([]), }); export const skillGroupSchema = z.object({ name: z.string().default(""), skills: z.array(z.string()).default([]), }); export const certItemSchema = z.object({ name: z.string().default(""), issuer: z.string().default(""), date: z.string().default(""), }); export const customItemSchema = z.object({ title: z.string().default(""), subtitle: z.string().default(""), meta: z.string().default(""), bullets: z.array(z.string()).default([]), }); export const SECTION_KINDS = [ "summary", "experience", "education", "projects", "skills", "certifications", "custom", ] as const; export const sectionSchema = z.object({ id: z.string().default(""), kind: z.enum(SECTION_KINDS), /// Display heading, e.g. "Experience" or "Selected Work" heading: z.string().default(""), visible: z.boolean().default(true), /// Only one of these is used, depending on `kind`. text: z.string().default(""), experience: z.array(experienceItemSchema).default([]), education: z.array(educationItemSchema).default([]), projects: z.array(projectItemSchema).default([]), skills: z.array(skillGroupSchema).default([]), certifications: z.array(certItemSchema).default([]), items: z.array(customItemSchema).default([]), }); export const resumeDocSchema = z.object({ header: headerSchema.default({}), sections: z.array(sectionSchema).default([]), }); export type ResumeLink = z.infer; export type ResumeHeader = z.infer; export type ExperienceItem = z.infer; export type EducationItem = z.infer; export type ProjectItem = z.infer; export type SkillGroupItem = z.infer; export type CertItem = z.infer; export type CustomItem = z.infer; export type ResumeSection = z.infer; export type ResumeDoc = z.infer; export type SectionKind = (typeof SECTION_KINDS)[number]; let idCounter = 0; export function rid(prefix = "s") { idCounter += 1; return `${prefix}_${Math.random().toString(36).slice(2, 8)}${idCounter}`; } /** * Give every section and entry an id of its own. * * Every id defaults to "" and RESUME_DOC_SHAPE never mentions ids, so a * document written through create_resume — the product's main path — arrives * with "" on every section. The editor addressed sections by id, so such a * document had one identity for all of them: hiding one section hid the lot, * and deleting one deleted the document. Healed here rather than at the write, * because the first edit happens before the first save. * * Deterministic on purpose. rid() is random, and a random id minted on every * read would churn between one get_resume and the next, and would make two * parses of the same stored document unequal. Positional names are stable, so * a document parsed twice is the same document. */ export function ensureIds(doc: ResumeDoc): ResumeDoc { const used = new Set(); // Keep an id the document already carries, unless it is blank or a // duplicate; otherwise take the positional name, and walk it until it is // free. Total: it always returns something no other entry holds. const claim = (id: string, positional: string) => { let next = id && !used.has(id) ? id : positional; let n = 2; while (used.has(next)) next = `${positional}_${n++}`; used.add(next); return next; }; return { ...doc, sections: doc.sections.map((section, s) => ({ ...section, id: claim(section.id, `sec_${s}`), experience: section.experience.map((item, i) => ({ ...item, id: claim(item.id, `exp_${s}_${i}`), })), education: section.education.map((item, i) => ({ ...item, id: claim(item.id, `edu_${s}_${i}`), })), projects: section.projects.map((item, i) => ({ ...item, id: claim(item.id, `prj_${s}_${i}`), })), })), }; } /** * For READING a stored row: never throws, so a damaged document still opens. * Never use it on a write — see parseResumeDocStrict. */ export function parseResumeDoc(value: unknown): ResumeDoc { const parsed = resumeDocSchema.safeParse(value); if (parsed.success) return ensureIds(parsed.data); return emptyResumeDoc(); } /** null means "nothing here" to every assistant; to zod it is a type error. */ function dropNulls(value: unknown): unknown { if (Array.isArray(value)) return value.filter((item) => item !== null).map(dropNulls); if (value && typeof value === "object") { return Object.fromEntries( Object.entries(value).filter(([, v]) => v !== null).map(([k, v]) => [k, dropNulls(v)]), ); } return value; } /** * For WRITING a document that arrived from a caller: refuses, naming every * field that is wrong, rather than saving anything. * * The lenient parse above returns an empty document on failure, and for a * while every write went through it — so an assistant that sent one null * endDate or `isCurrent: "true"` replaced someone's whole resume with a blank * one and was told it had worked. A refusal that names `sections[2].kind` is * something an assistant fixes on its next call. Nulls are forgiven, because * they mean "empty" and the field's default says the same thing. */ export function parseResumeDocStrict(value: unknown): ResumeDoc { const parsed = resumeDocSchema.safeParse(dropNulls(value)); if (parsed.success) return ensureIds(parsed.data); const problems = parsed.error.issues.slice(0, 8).map((issue) => { const path = issue.path .map((part, i) => (typeof part === "number" ? `[${part}]` : i === 0 ? part : `.${part}`)) .join(""); return `${path || "(document)"}: ${issue.message}`; }); const more = parsed.error.issues.length - problems.length; throw new Error( `That resume document does not match the format, so nothing was saved. ` + `${problems.join("; ")}${more > 0 ? `; and ${more} more` : ""}. ` + `get_resume_format shows the exact shape.`, ); } export function emptyResumeDoc(): ResumeDoc { return { header: { name: "", title: "", email: "", phone: "", location: "", links: [] }, sections: [ { ...blankSection("summary"), heading: "Summary" }, { ...blankSection("experience"), heading: "Experience" }, { ...blankSection("education"), heading: "Education" }, { ...blankSection("skills"), heading: "Skills" }, ], }; } export function blankSection(kind: SectionKind): ResumeSection { return { id: rid("sec"), kind, heading: defaultHeading(kind), visible: true, text: "", experience: [], education: [], projects: [], skills: [], certifications: [], items: [], }; } export function defaultHeading(kind: SectionKind) { switch (kind) { case "summary": return "Summary"; case "experience": return "Experience"; case "education": return "Education"; case "projects": return "Projects"; case "skills": return "Skills"; case "certifications": return "Certifications"; default: return "Additional"; } } export function blankExperience(): ExperienceItem { return { id: rid("exp"), company: "", title: "", location: "", startDate: "", endDate: "", isCurrent: false, summary: "", bullets: [""], }; } export function blankEducation(): EducationItem { return { id: rid("edu"), school: "", degree: "", field: "", location: "", startDate: "", endDate: "", details: [], }; } export function blankProject(): ProjectItem { return { id: rid("prj"), name: "", role: "", url: "", startDate: "", endDate: "", description: "", bullets: [""], }; } /** A compact, human-readable description of the doc shape for the MCP tool docs. */ export const RESUME_DOC_SHAPE = `{ "header": { "name": string, "title": string, "email": string, "phone": string, "location": string, "links": [{ "label": string, "url": string }] }, "sections": [ { "kind": "summary", "heading": "Summary", "visible": true, "text": string }, { "kind": "experience", "heading": "Experience", "visible": true, "experience": [{ "roleId": string, "company": string, "title": string, "location": string, "startDate": "YYYY-MM", "endDate": "YYYY-MM", "isCurrent": boolean, "summary": string, "bullets": [string] }] }, { "kind": "education", "heading": "Education", "visible": true, "education": [{ "school": string, "degree": string, "field": string, "location": string, "startDate": "YYYY-MM", "endDate": "YYYY-MM", "details": [string] }] }, { "kind": "projects", "heading": "Projects", "visible": true, "projects": [{ "name": string, "role": string, "url": string, "startDate": "YYYY-MM", "endDate": "YYYY-MM", "description": string, "bullets": [string] }] }, { "kind": "skills", "heading": "Skills", "visible": true, "skills": [{ "name": string, "skills": [string] }] }, { "kind": "certifications", "heading": "Certifications", "visible": true, "certifications": [{ "name": string, "issuer": string, "date": string }] }, { "kind": "custom", "heading": string, "visible": true, "items": [{ "title": string, "subtitle": string, "meta": string, "bullets": [string] }] } ] }`;