{"record":{"id":"7777e2e5b8ec9805","repo":"affaan-m/ECC","slug":"template-has-unfilled-placeholders-new-set-leftover-join","errorCode":null,"errorMessage":"template has unfilled placeholders: ${[...new Set(leftover)].join(', ')}","messagePattern":"template has unfilled placeholders: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"skills/master-agreement-generator/scripts/build-agreement.js","lineNumber":128,"sourceCode":"    FEE_CLAUSE: fill(clauses.fee),\n    SCHEDULE_ROWS: renderScheduleRows(spec.schedule),\n    SUPPLEMENT_CLAUSE: supplement,\n    CP_SIGBLOCK: (spec.legal || cp).toUpperCase(),\n    CP_SIGNER: spec.signer || BLANK,\n    CP_TITLE: spec.title || BLANK,\n    CP_EMAIL: spec.email || BLANK,\n  };\n}\n\nfunction render(template, spec, now) {\n  const values = buildValues(spec, now);\n  let output = template;\n  for (const [key, value] of Object.entries(values)) {\n    output = output.split(`{{${key}}}`).join(value);\n  }\n  const leftover = output.match(/\\{\\{[A-Z_]+\\}\\}/g);\n  if (leftover) {\n    throw new Error(`template has unfilled placeholders: ${[...new Set(leftover)].join(', ')}`);\n  }\n  return `${DRAFT_NOTICE}\\n\\n${output}`;\n}\n\nfunction pandocAvailable() {\n  const probe = spawnSync('pandoc', ['--version'], CONVERTER_OPTIONS);\n  return !probe.error && probe.status === 0;\n}\n\nfunction outputPaths(outDir, file) {\n  const root = path.resolve(outDir);\n  const destinations = ['md', 'docx'].map(extension => path.resolve(root, `${file} MASTER.${extension}`));\n  for (const destination of destinations) {\n    if (path.dirname(destination) !== root) {\n      throw new Error('spec.file must keep generated files directly inside the output directory');\n    }\n    // lstat also detects dangling links. Check BOTH outputs before the first write,\n    // even when conversion is disabled. The caller must control this directory;","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/master-agreement-generator/scripts/build-agreement.js#L110-L146","documentation":"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.","triggerScenarios":"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.","commonSituations":"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().","solutions":["Read the error — it names each unfilled placeholder (e.g. {{EFFECTIVE_DATE}}).","Add the missing key(s) to the values object returned by buildValues(), or fill them in your caller before rendering.","Check whether you are passing an outdated/edited template that no longer matches the script's values; restore the matching template version.","Verify key case matches exactly — placeholders are uppercase; substitution is split/join on exact '{{KEY}}'."],"exampleFix":"// before\nreturn { file: spec.file, cp, role: spec.role }; // template also uses {{NOW}}\n// after\nreturn { file: spec.file, cp, role: spec.role, now: now.toISOString().slice(0, 10) };","handlingStrategy":"try-catch","validationCode":"function checkPlaceholders(template, values) {\n  let out = template;\n  for (const [k, v] of Object.entries(values)) out = out.split(`{{${k}}}`).join(v);\n  const leftover = out.match(/\\{\\{[A-Z_]+\\}\\}/g);\n  if (leftover) throw new Error(`unfilled: ${[...new Set(leftover)].join(', ')}`);\n}","typeGuard":"null","tryCatchPattern":"try {\n  const draft = render(template, values);\n} catch (e) {\n  if (e.message.startsWith('template has unfilled placeholders')) {\n    console.error(`Template/values mismatch: ${e.message}`);\n  } else throw e;\n}","preventionTips":["Keep template and buildValues()'s values object in sync; change them together in one commit.","Add a smoke test that renders the shipped template with default values and asserts no leftovers.","Never hand-edit templates without checking which {{KEYS}} they reference."],"tags":["template","rendering","validation"],"backgroundTag":"internal-invariant-violation","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}