// Placeholder substitution for skill text. // Convention: placeholders look like {{KEY}} (uppercase + underscores). // Inputs may be passed as either {KEY: value} or {key: value} — both resolve. export interface SubstituteResult { text: string; bound: string[]; unbound: string[]; } const PLACEHOLDER_RE = /\{\{([A-Z][A-Z0-9_]*)\}\}/g; export function substitute(text: string, inputs: Record): string { return text.replace(PLACEHOLDER_RE, (match, key: string) => { if (inputs[key] !== undefined) return inputs[key]; const lc = key.toLowerCase(); if (inputs[lc] !== undefined) return inputs[lc]; return match; }); } export function substituteWithReport( text: string, inputs: Record, ): SubstituteResult { const bound = new Set(); const unbound = new Set(); const out = text.replace(PLACEHOLDER_RE, (match, key: string) => { if (inputs[key] !== undefined) { bound.add(`{{${key}}}`); return inputs[key]; } const lc = key.toLowerCase(); if (inputs[lc] !== undefined) { bound.add(`{{${key}}}`); return inputs[lc]; } unbound.add(`{{${key}}}`); return match; }); return { text: out, bound: [...bound], unbound: [...unbound] }; } export function findPlaceholders(text: string): string[] { const found = new Set(); for (const m of text.matchAll(PLACEHOLDER_RE)) { found.add(`{{${m[1]}}}`); } return [...found]; }