{"record":{"id":"d63e5c481b244e61","repo":"santifer/career-ops","slug":"unresolved-placeholders-new-set-unresolved","errorCode":null,"errorMessage":"Unresolved placeholders: ${[...new Set(unresolved)].join(', ')}","messagePattern":"Unresolved placeholders: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"build-cv-html.mjs","lineNumber":702,"sourceCode":"\n  const { substitutions, candidate } = renderReport(payload, partials);\n\n  // The contact row and photo carry conditional markup (dropped separators /\n  // no <img>), so they are rebuilt as whole blocks before placeholder fill.\n  let html = template.replace(CONTACT_ROW_RE, () => buildContactRow(candidate));\n  html = html.replace(/\\{\\{PHOTO\\}\\}/g, () => buildPhoto(candidate, candidate.name));\n\n  // Drop the optional sections (projects, education) that have no entries, so\n  // an absent one leaves no bare header behind. See cv-sections-core.mjs.\n  html = stripEmptySections(html, payload, 'html');\n\n  for (const [key, value] of Object.entries(substitutions)) {\n    html = html.replace(new RegExp(`\\\\{\\\\{${key}\\\\}\\\\}`, 'g'), () => value);\n  }\n\n  const unresolved = html.match(PLACEHOLDER_RE);\n  if (unresolved) {\n    throw new Error(`Unresolved placeholders: ${[...new Set(unresolved)].join(', ')}`);\n  }\n  return html;\n}\n\n// Payload validation lives in lib/cv-payload-schema.mjs, shared with\n// build-cv-latex.mjs: the two formats have different key contracts (this one's\n// education entry is {title, org, year, description}; the LaTeX one is\n// {institution, degree, dates, coursework}), and keeping both tables in one\n// place is what lets each reject the other's vocabulary by name (#3523).\n\nfunction countBullets(payload) {\n  const ex = Array.isArray(payload.experience)\n    ? payload.experience.flatMap(e => (Array.isArray(e?.bullets) ? e.bullets : []))\n    : [];\n  return ex.length;\n}\n\nasync function writeAndReport(html, absOutput, payload, extra = {}) {","sourceCodeStart":684,"sourceCodeEnd":720,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/build-cv-html.mjs#L684-L720","documentation":"After substituting all `{{key}}` placeholders with values from the substitutions map, the builder re-scans the HTML with PLACEHOLDER_RE. Any `{{...}}` token still present means the caller supplied no substitution for it, so the builder refuses to emit HTML containing unreplaced placeholders and throws with the deduplicated list of offending tokens.","triggerScenarios":"Calling the render/substitute function where the template contains a `{{SOME_KEY}}` placeholder but the substitutions object either omits that key, spells it differently (case mismatch), or the value itself contains literal `{{...}}` text that re-matches PLACEHOLDER_RE after replacement.","commonSituations":"A new field was added to the CV template but not to the payload; the payload key casing doesn't match the template token; a job description or bio string legitimately contains `{{...}}` syntax that gets injected verbatim; schema drift between the HTML and LaTeX builders' key contracts.","solutions":["Read the error's placeholder list and add matching keys to the substitutions object.","Fix casing/spelling mismatches between template tokens and payload keys.","Escape or strip literal `{{...}}` sequences from user-supplied values (e.g. job descriptions) before substitution.","Validate the payload against lib/cv-payload-schema.mjs before rendering to catch missing keys early."],"exampleFix":"// before\nrenderTemplate(html, { NAME: 'Ada' });\n\n// after\nrenderTemplate(html, { NAME: 'Ada', EMAIL: 'ada@example.com', SUMMARY: 'Engineer' });","handlingStrategy":"validation","validationCode":"const requiredKeys = [...template.matchAll(/\\{\\{([A-Z0-9_]+)\\}\\}/g)].map(m => m[1]);\nconst missing = [...new Set(requiredKeys)].filter(k => !(k in substitutions));\nif (missing.length) throw new Error(`Missing substitution keys: ${missing.join(', ')}`);","typeGuard":null,"tryCatchPattern":"try {\n  return renderTemplate(tpl, subs);\n} catch (e) {\n  if (e.message.startsWith('Unresolved placeholders:')) {\n    const keys = e.message.slice('Unresolved placeholders: '.length);\n    console.error(`Payload missing keys: ${keys}`);\n    // merge defaults and retry once\n    return renderTemplate(tpl, { ...defaults, ...subs });\n  }\n  throw e;\n}","preventionTips":["Validate payloads against lib/cv-payload-schema.mjs before rendering.","Keep template tokens and payload keys in one shared constants file.","Sanitize user-supplied strings (job descriptions, bios) to escape `{{` sequences.","Add a render test with a fully-populated payload to catch schema drift."],"tags":["template","substitution","placeholders"],"backgroundTag":"schema-validation-failed","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}