santifer/career-ops · error · Error

Unresolved placeholders: ${[...unresolved].join(', ')}

Error message

Unresolved placeholders: ${[...unresolved].join(', ')}

What it means

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.

Source

Thrown at generate-cover-letter.mjs:222

  // a custom cover-letter template (KINDS.cover in cv-templates.mjs) carrying a
  // typo'd or unsupported token. Collect those DURING the pass rather than
  // scanning the result: a scan of the output cannot tell a template token from
  // the same sequence appearing inside a substituted value, which is exactly
  // what the single pass above is careful to leave literal.
  const unresolved = new Set();
  const rendered = html.replace(/\{\{[A-Z_]+\}\}/g, (token) => {
    const value = replacements[token];
    if (value == null) {
      unresolved.add(token);
      return token;
    }
    return value;
  });

  // Fail loudly, matching build-cv-html.mjs and build-cv-latex.mjs. Shipping a
  // letter with a literal {{TOKEN}} in it is worse than not producing one.
  if (unresolved.size) {
    throw new Error(`Unresolved placeholders: ${[...unresolved].join(', ')}`);
  }
  return rendered;
}

/** Parse a payload, run the fact gate, and render the cover-letter PDF. */
async function main() {
  const { values: args } = parseArgs({
    options: {
      payload: { type: "string" },
      out:     { type: "string" },
      format:  { type: "string" },
      report:  { type: "string" },
      help:    { type: "boolean", short: "h" },
    },
    strict: false,
  });

  if (args.help || !args.payload) {

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. 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.).
  2. Fix typos: {{NANE}} → {{NAME}}, {{ROL_TITLE}} → {{ROLE_TITLE}}.
  3. Remove unsupported tokens from the template, or extend the replacements map in buildHtml if the token is intentionally new.

Example fix

<!-- before (custom template has a typo) -->
<h1>{{NANE}}</h1>
<p>{{ROLE_TITEL}}</p>

<!-- after -->
<h1>{{NAME}}</h1>
<p>{{ROLE_TITLE}}</p>
Defensive patterns

Strategy: validation

Validate before calling

import { readFileSync } from 'fs';

const SUPPORTED_TOKENS = new Set([
  '{{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}}',
]);

function validateTemplateTokens(templatePath) {
  const html = readFileSync(templatePath, 'utf-8');
  const tokens = [...html.matchAll(/\{\{[A-Z_]+\}\}/g)].map(m => m[0]);
  const unsupported = tokens.filter(t => !SUPPORTED_TOKENS.has(t));
  if (unsupported.length) {
    throw new Error(`Unsupported tokens in template: ${[...new Set(unsupported)].join(', ')}`);
  }
}

Try / catch

try {
  const html = buildHtml(payload, templatePath);
} catch (err) {
  if (err.message.startsWith('Unresolved placeholders')) {
    console.error('Template has unsupported tokens:', err.message);
    // Fix the template, then retry
    process.exit(1);
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/560b72bfef253e99. Report an issue: GitHub.