{"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":"Error","httpStatus":null,"severity":"error","filePath":"generate-cover-letter.mjs","lineNumber":222,"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":204,"sourceCodeEnd":240,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/generate-cover-letter.mjs#L204-L240","documentation":"buildHtml does a single-pass regex substitution of {{TOKEN}} patterns against a replacements map. After the pass, any remaining {{TOKEN}} in the output means the template used a token the renderer does not support. This fails loudly because shipping a cover letter with a literal {{TOKEN}} visible to the recipient is worse than not producing one. The supported tokens are listed in the replacements map: 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.","triggerScenarios":"A custom cover-letter template (installed via cv-templates.mjs KINDS.cover) contains {{SOMETHING_NEW}} that the renderer does not know about; a typo in the template like {{NANE}} instead of {{NAME}}; the template references a token that was renamed or removed in a newer version.","commonSituations":"User installs a custom cover-letter template pack with extra tokens; an agent edits the template and introduces a typo; a version update renamed a token but the custom template was not migrated.","solutions":["Check the custom cover-letter template for {{TOKEN}} patterns and ensure each one is in the supported set (NAME, CONTACT_LINE, ROLE_TITLE, OPENING, PROFILE_INTRO, etc.).","Fix typos: {{NANE}} → {{NAME}}, {{ROL_TITLE}} → {{ROLE_TITLE}}.","Remove unsupported tokens from the template, or extend the replacements map in buildHtml if the token is intentionally new."],"exampleFix":"<!-- before (custom template has a typo) -->\n<h1>{{NANE}}</h1>\n<p>{{ROLE_TITEL}}</p>\n\n<!-- after -->\n<h1>{{NAME}}</h1>\n<p>{{ROLE_TITLE}}</p>","handlingStrategy":"validation","validationCode":"import { readFileSync } from 'fs';\n\nconst SUPPORTED_TOKENS = new Set([\n  '{{NAME}}', '{{CONTACT_LINE}}', '{{CREDENTIALS_BLOCK}}', '{{ROLE_TITLE}}',\n  '{{DATELINE}}', '{{GREETING_BLOCK}}', '{{OPENING}}', '{{PROFILE_INTRO}}',\n  '{{ACHIEVEMENTS_BLOCK}}', '{{PROBLEMS_BLOCK}}', '{{CLOSING_BLOCK}}',\n  '{{LANGUAGE_CLOSING_BLOCK}}', '{{SIGNATURE_BLOCK}}', '{{FOOTNOTES_BLOCK}}',\n]);\n\nfunction validateTemplateTokens(templatePath) {\n  const html = readFileSync(templatePath, 'utf-8');\n  const tokens = [...html.matchAll(/\\{\\{[A-Z_]+\\}\\}/g)].map(m => m[0]);\n  const unsupported = tokens.filter(t => !SUPPORTED_TOKENS.has(t));\n  if (unsupported.length) {\n    throw new Error(`Unsupported tokens in template: ${[...new Set(unsupported)].join(', ')}`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const html = buildHtml(payload, templatePath);\n} catch (err) {\n  if (err.message.startsWith('Unresolved placeholders')) {\n    console.error('Template has unsupported tokens:', err.message);\n    // Fix the template, then retry\n    process.exit(1);\n  }\n  throw err;\n}","preventionTips":["After installing or editing a custom cover-letter template, validate its tokens against the supported set.","Watch for typos: {{NANE}}, {{ROL_TITLE}}, etc.","When upgrading career-ops, re-validate custom templates — token names may have changed."],"tags":["template","validation","cover-letter","placeholder"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}