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
- Prompt templates — Proactively avoid errors at prompt time
- Schema-aware generation — Schema reduces error sources
- AINP protocol — §21 cheat sheet — every fence and spec line on one page
- AINP protocol — §16 For LLM authors
- Writer's Guide common pitfalls — Writers also stumble on these (humans err too)