Common LLM mistakes & repair

The 10 most common mistakes LLMs make generating Rho — each with symptom / cause / auto-fix code / prompt-hardening hint. Integrate these patterns into your server-side validation pipeline to lift accuracy from ~95% to 99%+.


Mistake 1: JavaScript syntax (Math.X)

Symptom:

\```interact
slider angle 0 360 0 1
template stl: [{Math.sin(angle * Math.PI / 180):.2f}]
\```

Cause: LLMs default to JS.

Auto-fix (regex):

function fixMathPrefix(text: string): string {
  return text
    .replace(/Math\.PI/g, 'pi')
    .replace(/Math\.E/g, 'e')
    .replace(/Math\.(\w+)/g, '$1')   // Math.sin → sin / Math.sqrt → sqrt
    .replace(/Number\.(\w+)/g, '$1');
}

Prompt hardening:

NEVER use Math.X or Number.X — Rho mini DSL has built-in functions: sin, cos, tan, sqrt, pow, abs, min, max, floor, ceil, log, exp, round.
NEVER use JavaScript syntax — use ternary `cond ? a : b` instead of if/else.

Mistake 2: if/else statements

Symptom:

computed cat = if (bmi < 18.5) "thin" else if (bmi < 24) "normal" else "over"

Cause: mini DSL has no if/else statements.

Auto-fix (regex → ternary):

function fixIfElse(text: string): string {
  // Simple case: if (cond) val1 else val2
  return text.replace(
    /if\s*\(([^)]+)\)\s+("[^"]*"|\d+\.?\d*)\s+else\s+("[^"]*"|\d+\.?\d*)/g,
    '$1 ? $2 : $3'
  );
  // Nested if/else complex — multiple passes needed
}

Prompt hardening:

For conditional logic, use ternary `cond ? trueValue : falseValue`:
- Simple: `bmi < 18.5 ? "thin" : "normal"`
- Nested: `bmi < 18.5 ? "thin" : bmi < 24 ? "normal" : "over"`
NEVER use `if (...) ... else ...` syntax.

Mistake 3: forgot namespace (multi-block sharing intent)

Symptom:

\```interact
slider rate 0 0.15 0.05 0.01
\```

\```interact
template stl: [Rate] -> [{rate}]    ← rate not declared in this block
\```

Cause: LLM doesn't know namespace is required.

Auto-fix (AST check):

function detectMissingNamespace(blocks: InteractBlock[]): Issue[] {
  const issues: Issue[] = [];
  const declared: Map<string, Set<string>> = new Map();

  for (const block of blocks) {
    const ns = block.namespace || '__default__';
    if (!declared.has(ns)) declared.set(ns, new Set());
    block.controls.forEach(c => declared.get(ns)!.add(c.name));
  }

  for (const block of blocks) {
    const ns = block.namespace || '__default__';
    const refs = extractTemplateRefs(block.template);
    for (const ref of refs) {
      if (!declared.get(ns)!.has(ref) && !block.controls.some(c => c.name === ref)) {
        issues.push({
          block,
          message: `Template references "${ref}" but it's not declared in this block. Did you mean to add namespace?`
        });
      }
    }
  }

  return issues;
}

Prompt hardening:

When multiple interact blocks should share variables, ALL of them must use the same `namespace`:

\```interact namespace finance
slider rate ...
\```

\```interact namespace finance
template stl: [{rate}]
\```

If blocks are intentionally independent, omit namespace.

Mistake 4: HTML tags instead of Rho DSL

Symptom:

<details>
<summary>Click to expand</summary>
content
</details>

Cause: LLM knows HTML, may default to it.

Auto-fix (rule mapping):

const HTML_TO_RHO: Array<{ pattern: RegExp; replacement: (match: RegExpMatchArray) => string }> = [
  {
    pattern: /<details>\s*<summary>(.+?)<\/summary>\s*([\s\S]+?)\s*<\/details>/g,
    replacement: (m) => `\`\`\`modal trigger="${m[1]}"\n${m[2].trim()}\n\`\`\``
  },
  {
    pattern: /<div class="grid">([\s\S]+?)<\/div>/g,
    replacement: (m) => `\`\`\`layout grid cols=2\n${convertDivCellsToCards(m[1])}\n\`\`\``
  }
];

Prompt hardening:

Don't use raw HTML. Use Rho DSL instead:
- <details> → ```modal trigger="..."
- <div class="grid"> → ```layout grid cols=N + :::card
- <summary> / accordions → ```modal or ```tabs

All output must be valid markdown + Rho DSL only.

Mistake 5: directive outside its container

Symptom:

:::card accent=blue
content
:::

(Standalone, not inside a layout)

Cause: LLM mistakenly treats directive as a standalone DSL.

Auto-fix (wrap in layout):

function wrapOrphanDirectives(markdown: string): string {
  // Find standalone :::card / :::tab / :::step / :::event
  // Context: no preceding ```layout/tabs/stepper/timeline opening
  // Fix: wrap in the corresponding layout / tabs / stepper / timeline
  // Implementation omitted — needs markdown AST analysis
}

Prompt hardening:

Directives MUST be inside their proper container:
- :::card → must be inside ```layout grid cols=N + ```
- :::tab → must be inside ```tabs + ```
- :::step → must be inside ```stepper + ```
- :::event → must be inside ```timeline + ```

Never write a :::card outside a ```layout fenced block.

Mistake 6: vega-lite spec is invalid JSON

Symptom:

template vega-lite:
{
  "mark": "line"
  "encoding": { ... }    ← Missing comma
}

Cause: LLMs writing complex nested objects easily drop commas / quotes.

Auto-fix (JSON repair lib):

import { jsonrepair } from 'jsonrepair';

function fixVegaLiteJSON(spec: string): string {
  try {
    JSON.parse(spec);
    return spec;
  } catch {
    try {
      return jsonrepair(spec);   // npm: jsonrepair
    } catch {
      throw new Error('Vega-Lite spec invalid even after repair');
    }
  }
}

Prompt hardening:

Vega-Lite spec must be strict JSON:
- All keys in double quotes
- Comma between every key:value pair
- No trailing commas
- Use https://vega.github.io/editor/ format as reference

Mistake 7: SVG with script / event handlers

Symptom:

template svg:
<svg viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="40" onclick="alert('hi')"/>
  <script>console.log('rendered')</script>
</svg>

Cause: LLM learned interactive-SVG patterns from web tutorials.

Auto-fix (SVG sanitizer library):

import DOMPurify from 'isomorphic-dompurify';

function sanitizeSVGTemplate(svg: string): string {
  return DOMPurify.sanitize(svg, {
    USE_PROFILES: { svg: true },
    FORBID_TAGS: ['script', 'foreignObject'],
    FORBID_ATTR: [/^on/]    // All on* attributes
  });
}

@rho/md ships its own sanitizer — this patch is an extra defense layer for early server-side validation.

Prompt hardening:

SVG templates: never use <script>, <foreignObject>, on* event attributes (onclick, onload, etc.), or javascript: URLs.
For interactivity, drive SVG attributes from sliders/timers via {expr} placeholders.

Mistake 8: wrong format spec

Symptom:

template stl: [BMI] -> [{bmi:.1}]      ← Missing type char (should be .1f)
template stl: [BMI] -> [{bmi:1f}]      ← Missing . prefix

Cause: LLM unfamiliar with Python f-string-style format spec.

Auto-fix (regex):

function fixFormatSpec(text: string): string {
  return text
    // {x:.1} → {x:.1f}
    .replace(/\{(\w+):(\.\d+)\}/g, '{$1$2f}')
    // {x:1f} → {x:.1f}
    .replace(/\{(\w+):(\d+)f\}/g, '{$1:.$2f}');
}

Prompt hardening:

Format spec syntax (similar to Python f-string):
- {value:.2f}  - 2 decimal places (always include the .)
- {value:.0f}  - 0 decimals (integer)
- {value:%}    - percentage (auto ×100)
NEVER use {value:.1} (missing type char) or {value:1f} (missing dot).

Mistake 9: computed circular dependency

Symptom:

\```interact
computed a = b + 1
computed b = a + 1     ← circular
\```

Cause: LLM didn't realize expression dependencies must be acyclic.

Auto-fix (cycle detection via topological sort):

function detectCycle(computeds: Computed[]): boolean {
  const graph: Map<string, Set<string>> = new Map();
  for (const c of computeds) {
    graph.set(c.name, new Set(extractIdentifiers(c.expression)));
  }

  // DFS cycle detection
  const visiting = new Set<string>();
  const visited = new Set<string>();

  function dfs(node: string): boolean {
    if (visiting.has(node)) return true;
    if (visited.has(node)) return false;
    visiting.add(node);
    for (const dep of graph.get(node) || []) {
      if (graph.has(dep) && dfs(dep)) return true;
    }
    visiting.delete(node);
    visited.add(node);
    return false;
  }

  for (const c of computeds) {
    if (dfs(c.name)) return true;
  }
  return false;
}

Prompt hardening:

Computed values must form a DAG (no cycles):
- ✅ `computed a = sliderX + 1; computed b = a * 2`  (b depends on a, OK)
- ❌ `computed a = b + 1; computed b = a + 1`  (cycle)
For "previous-value" patterns, use button self-reference instead.

Mistake 10: undeclared variable referenced

Symptom:

\```interact
slider x 0 10 5 1
template stl: [{y}]    ← y not declared
\```

Cause: LLM typo / imagined a variable but forgot to add the slider.

Auto-fix (symbol table check):

function detectUndeclaredVars(block: InteractBlock): Issue[] {
  const declared = new Set<string>([
    ...block.controls.map(c => c.name),
    ...(block.computed || []).map(c => c.name),
    'pi', 'e'   // Built-in constants
  ]);

  const refs = extractTemplateRefs(block.template);
  return refs
    .filter(r => !declared.has(r))
    .map(r => ({
      block,
      message: `Template references undeclared "${r}"`
    }));
}

Prompt hardening:

Every variable referenced in `{...}` placeholders must first be declared via:
- slider name min max initial step
- input name "default"
- select name "opt1" "opt2"
- toggle name true|false
- button name "label" value
- computed name = expression

Or it must be a built-in constant: pi, e.

Bonus: complete server-side validation pipeline

async function validateAndRepair(llmOutput: string): Promise<{ markdown: string; warnings: string[] }> {
  let md = llmOutput;
  const warnings: string[] = [];

  // 1. Auto-fix Math.X / Number.X
  const fixed1 = fixMathPrefix(md);
  if (fixed1 !== md) { warnings.push('Removed Math/Number prefixes'); md = fixed1; }

  // 2. Auto-fix if/else → ternary
  const fixed2 = fixIfElse(md);
  if (fixed2 !== md) { warnings.push('Converted if/else to ternary'); md = fixed2; }

  // 3. Auto-fix HTML → Rho DSL
  const fixed3 = convertHTMLToRho(md);
  if (fixed3 !== md) { warnings.push('Converted HTML tags to Rho DSL'); md = fixed3; }

  // 4. Sanitize SVG
  md = sanitizeAllSVGTemplates(md);

  // 5. Repair vega-lite JSON
  md = repairAllVegaSpecs(md);

  // 6. Fix format specs
  md = fixFormatSpec(md);

  // 7. Final parse check
  const ast = await parseRho(md);
  const issues = [
    ...detectOrphanDirectives(ast),
    ...detectMissingNamespace(ast),
    ...detectCircularComputeds(ast),
    ...detectUndeclaredVars(ast),
  ];

  if (issues.length > 0) {
    // Not auto-fixable → re-prompt LLM or return error
    throw new RhoValidationError(issues);
  }

  return { markdown: md, warnings };
}

Re-prompt strategy

If auto-fix fails / validation still errors:

async function generateWithRetry(userPrompt: string, maxRetries = 3) {
  let lastError: string | null = null;

  for (let i = 0; i < maxRetries; i++) {
    const messages = [
      { role: 'system', content: SYSTEM_PROMPT },
      { role: 'user', content: userPrompt },
    ];

    if (lastError) {
      messages.push({
        role: 'user',
        content: `Previous attempt had error: ${lastError}\nPlease fix and try again.`
      });
    }

    const llmOutput = await llm.generate({ messages });

    try {
      return await validateAndRepair(llmOutput);
    } catch (e) {
      lastError = String(e);
    }
  }

  throw new Error('Max retries exceeded');
}

See also