{"record":{"id":"560b72bfef253e99","repo":"santifer/career-ops","slug":"unresolved-placeholders-unresolved-join","errorCode":null,"errorMessage":"Unresolved placeholders: ${[...unresolved].join(', ')}","messagePattern":"Unresolved placeholders: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"generate-cover-letter.mjs","lineNumber":264,"sourceCode":"  // a custom cover-letter template (KINDS.cover in cv-templates.mjs) carrying a\n  // typo'd or unsupported token. Collect those DURING the pass rather than\n  // scanning the result: a scan of the output cannot tell a template token from\n  // the same sequence appearing inside a substituted value, which is exactly\n  // what the single pass above is careful to leave literal.\n  const unresolved = new Set();\n  const rendered = html.replace(/\\{\\{[A-Z_]+\\}\\}/g, (token) => {\n    const value = replacements[token];\n    if (value == null) {\n      unresolved.add(token);\n      return token;\n    }\n    return value;\n  });\n\n  // Fail loudly, matching build-cv-html.mjs and build-cv-latex.mjs. Shipping a\n  // letter with a literal {{TOKEN}} in it is worse than not producing one.\n  if (unresolved.size) {\n    throw new Error(`Unresolved placeholders: ${[...unresolved].join(', ')}`);\n  }\n  return rendered;\n}\n\n/** Parse a payload, run the fact gate, and render the cover-letter PDF. */\nasync function main() {\n  const { values: args } = parseArgs({\n    options: {\n      payload: { type: \"string\" },\n      out:     { type: \"string\" },\n      format:  { type: \"string\" },\n      report:  { type: \"string\" },\n      help:    { type: \"boolean\", short: \"h\" },\n    },\n    strict: false,\n  });\n\n  if (args.help || !args.payload) {","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/generate-cover-letter.mjs#L246-L282","documentation":"generate-cover-letter.mjs's buildHtml renders a cover-letter HTML template by single-pass substituting a fixed map of {{TOKEN}} placeholders. If the template contains an all-caps {{TOKEN}} that has no entry in the replacement map (e.g. a typo'd or unsupported token in a custom template), it is left literal, collected, and the function throws listing every unresolved token — refusing to ship a letter containing a literal {{TOKEN}}.","triggerScenarios":"Calling buildHtml (or the CLI with --format cover) against a cover-letter template that contains a {{TOKEN}} not in the replacements map: a typo like {{NAMEE}} or {{COMPANY}}, a hand-edited custom cover template in cv-templates.mjs with a token the renderer never fills, or an upstream template update adding a token before the renderer gained a replacement for it.","commonSituations":"Users customizing templates/cv-template variants for cover letters add a new section with a placeholder they invent; template and renderer versions drift after a career-ops update; a fat-fingered token name during template editing.","solutions":["Read the listed token names from the error and fix the template: either correct the typo to a supported token or remove the unsupported token","Check which tokens the renderer supports (NAME, CONTACT_LINE, CREDENTIALS_BLOCK, ROLE_TITLE, DATELINE, GREETING_BLOCK, OPENING, PROFILE_INTRO, ACHIEVEMENTS_BLOCK, PROBLEMS_BLOCK, CLOSING_BLOCK, LANGUAGE_CLOSING_BLOCK, SIGNATURE_BLOCK, FOOTNOTES_BLOCK) and use only those","If the token is intentionally literal text in the letter, rewrite it so it does not match /\\{\\{[A-Z_]+\\}\\}/ (e.g. lowercase or extra spacing)","Re-run the generator after editing the template"],"exampleFix":"<!-- before -->\nDear {{HIRING_MANAGER}},\n<!-- after -->\n{{GREETING_BLOCK}}\n\n<!-- or, for a literal double-brace example in text: -->\n<!-- before --> use {{TOKEN}} syntax\n<!-- after --> use &#123;&#123;TOKEN&#125;&#125; syntax","handlingStrategy":"validation","validationCode":"const KNOWN = ['NAME','CONTACT_LINE','CREDENTIALS_BLOCK','ROLE_TITLE','DATELINE','GREETING_BLOCK','OPENING','PROFILE_INTRO','ACHIEVEMENTS_BLOCK','PROBLEMS_BLOCK','CLOSING_BLOCK','LANGUAGE_CLOSING_BLOCK','SIGNATURE_BLOCK','FOOTNOTES_BLOCK'];\nconst found = [...template.matchAll(/\\{\\{([A-Z_]+)\\}\\}/g)].map(m => m[1]);\nconst bad = found.filter(t => !KNOWN.includes(t));\nif (bad.length) throw new Error(`Unsupported placeholders: ${bad.join(', ')}`);","typeGuard":"const isSupportedToken = (token) => typeof token === 'string' && Object.prototype.hasOwnProperty.call(replacements, token);","tryCatchPattern":"try {\n  const html = buildHtml(payload);\n} catch (err) {\n  if (err.message.startsWith('Unresolved placeholders:')) {\n    console.error('Fix these template tokens:', err.message.slice('Unresolved placeholders:'.length));\n    process.exit(1);\n  }\n  throw err;\n}","preventionTips":["Only use tokens from the documented replacement map when editing cover-letter templates","Lint templates against the token list in CI before running the generator","Never write literal {{UPPERCASE}} text in letter content; escape or reformat it","After a career-ops update, diff your custom templates against the shipped ones for new/renamed tokens"],"tags":["template","placeholder","validation"],"backgroundTag":"unresolved-template-placeholder","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}