affaan-m/ECC · error · Error

template has unfilled placeholders

Error message

template has unfilled placeholders: ${[...new Set(leftover)].join(', ')}

What it means

render() substitutes {{KEY}} placeholders in the agreement template with values. After substitution, it scans for leftover {{UPPERCASE_PLACEHOLDER}} tokens; any survivors mean the values object did not cover the template. It throws listing the distinct unfilled placeholders, so drafts never ship with visible template syntax.

Solutions

  1. Read the error — it names each unfilled placeholder (e.g. {{EFFECTIVE_DATE}}).
  2. Add the missing key(s) to the values object returned by buildValues(), or fill them in your caller before rendering.
  3. Check whether you are passing an outdated/edited template that no longer matches the script's values; restore the matching template version.
  4. Verify key case matches exactly — placeholders are uppercase; substitution is split/join on exact '{{KEY}}'.

Example fix

// before
return { file: spec.file, cp, role: spec.role }; // template also uses {{NOW}}
// after
return { file: spec.file, cp, role: spec.role, now: now.toISOString().slice(0, 10) };
Defensive patterns

Strategy: try-catch

Validate before calling

function checkPlaceholders(template, values) {
  let out = template;
  for (const [k, v] of Object.entries(values)) out = out.split(`{{${k}}}`).join(v);
  const leftover = out.match(/\{\{[A-Z_]+\}\}/g);
  if (leftover) throw new Error(`unfilled: ${[...new Set(leftover)].join(', ')}`);
}

Type guard

null

Try / catch

try {
  const draft = render(template, values);
} catch (e) {
  if (e.message.startsWith('template has unfilled placeholders')) {
    console.error(`Template/values mismatch: ${e.message}`);
  } else throw e;
}

Prevention

When it happens

Trigger: A values object missing a key that the template references (e.g. template uses {{CP_LONG}} but values only supplies cp); a modified/custom template with new placeholders not mirrored in values; keys in values that differ by case from the template.

Common situations: Editing the template to add a field but forgetting to add the corresponding value in buildValues()'s return object; version mismatch between template and script after an update; hand-crafted values passed directly to render().

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/7777e2e5b8ec9805. Report an issue: GitHub.

Appendix: source

Thrown at skills/master-agreement-generator/scripts/build-agreement.js:128

    FEE_CLAUSE: fill(clauses.fee),
    SCHEDULE_ROWS: renderScheduleRows(spec.schedule),
    SUPPLEMENT_CLAUSE: supplement,
    CP_SIGBLOCK: (spec.legal || cp).toUpperCase(),
    CP_SIGNER: spec.signer || BLANK,
    CP_TITLE: spec.title || BLANK,
    CP_EMAIL: spec.email || BLANK,
  };
}

function render(template, spec, now) {
  const values = buildValues(spec, now);
  let output = template;
  for (const [key, value] of Object.entries(values)) {
    output = output.split(`{{${key}}}`).join(value);
  }
  const leftover = output.match(/\{\{[A-Z_]+\}\}/g);
  if (leftover) {
    throw new Error(`template has unfilled placeholders: ${[...new Set(leftover)].join(', ')}`);
  }
  return `${DRAFT_NOTICE}\n\n${output}`;
}

function pandocAvailable() {
  const probe = spawnSync('pandoc', ['--version'], CONVERTER_OPTIONS);
  return !probe.error && probe.status === 0;
}

function outputPaths(outDir, file) {
  const root = path.resolve(outDir);
  const destinations = ['md', 'docx'].map(extension => path.resolve(root, `${file} MASTER.${extension}`));
  for (const destination of destinations) {
    if (path.dirname(destination) !== root) {
      throw new Error('spec.file must keep generated files directly inside the output directory');
    }
    // lstat also detects dangling links. Check BOTH outputs before the first write,
    // even when conversion is disabled. The caller must control this directory;

View on GitHub (pinned to 8321021c54)