From b3c9f9e18b0b7ecf0c69a471f4ed60b507e1b8c5 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Fri, 16 Oct 2026 21:24:14 +0300 Subject: [PATCH] math: heal overescaped LaTeX commands in question displays --- src/core/math-delims.ts | 97 +++++++++++++++++++++++++++++++ src/core/tool-parts.ts | 11 +++- tests/render-markdown.test.tsx | 101 ++++++++++++++++++++++++++++++++- tests/tool-parts.test.ts | 78 +++++++++++++++++++++++++ 4 files changed, 283 insertions(+), 4 deletions(-) diff --git a/src/core/math-delims.ts b/src/core/math-delims.ts index 3367969..9299dd2 100644 --- a/src/core/math-delims.ts +++ b/src/core/math-delims.ts @@ -468,3 +468,100 @@ function restoreDollarMathEscapes(text: string): string { } return out.join(""); } + +/** + * Over-escaped command heal (owner ruling 2026-10-15) — the doubled- + * backslash fix. + * + * Some models stochastically emit DOUBLED backslashes inside JSON tool-call + * arguments: the saved record holds `\\nabla` (two real backslashes) where + * `\nabla` was meant. KaTeX reads the `\\` as a row break and then renders + * the command name as plain letters — the learner sees "the word nabla, not + * the symbol". Field evidence (2026-10-15, real data): xiaomi/mimo-v2.6-pro + * doubled per-call, per-field (one probe's question doubled while its + * explanation and options in the SAME call stayed clean — authoring, not + * transport), and deepseek-v4.1-flash once; quadrupling appeared when the + * in-session agent "compensated" for its own doubled first attempt. + * + * THE RULE. Inside a balanced math span ONLY (legacy `\(…\)`/`\[…\]` and + * dollar `$…$`/`$$…$$`, code spans and fences excluded), an EVEN-length + * backslash run (2, 4, …) immediately followed by a command start — a letter + * (any command name; bare letters after a row break are never authored + * content) or a punctuation command (`,` `;` `:` `!` `|` — thin spaces, + * norm bars) — collapses to a single backslash. Even runs before anything + * else (space, newline, `&`, `[`, `$`, digits) are legitimate row breaks and + * stay; ODD runs (3, 5, …) are row break(s) plus a real command with its own + * backslash — e.g. matrix cells written `a \\\nabla` — and stay untouched, + * so matrix content cannot be corrupted. Outside spans nothing is touched: + * doubled backslashes in prose or code are not ours to judge. Unbalanced + * spans pass through, exactly like the debris passes. + */ +const COMMAND_START_CLASS = "[A-Za-z,;:!|]"; + +/** Collapse even backslash runs before a command start; keep odd runs. */ +function halveOverescapedCommandRuns(inner: string): string { + return inner.replace( + new RegExp("(\\\\+)(?=" + COMMAND_START_CLASS + ")", "g"), + (run: string) => (run.length % 2 === 0 ? "\\" : run), + ); +} + +export function healOverescapedCommands(text: string): string { + if (!text.includes("\\\\")) return text; + const out: string[] = []; + let i = 0; + while (i < text.length) { + if (atLineStart(text, i) && FENCE_OPEN_RE.test(text.slice(i))) { + i = copyFencedBlock(text, i, out); + continue; + } + if (text[i] === "`") { + const run = backtickRun(text, i); + let j = i + run; + while (j < text.length) { + const nextRun = backtickRun(text, j); + if (nextRun === run) break; + j += Math.max(nextRun, 1); + } + const after = j < text.length ? j + run : i + run; + out.push(text.slice(i, after)); + i = after; + continue; + } + if (text[i] === "\\" && !isEscaped(text, i)) { + const next = text[i + 1]; + if (next === "(" || next === "[") { + const isDisplay = next === "["; + const open = isDisplay ? DISPLAY_OPEN : INLINE_OPEN; + const close = isDisplay ? DISPLAY_CLOSE : INLINE_CLOSE; + const closeAt = findClose(text, i + open.length, close); + if (closeAt !== -1) { + const inner = text.slice(i + open.length, closeAt); + out.push(open + halveOverescapedCommandRuns(inner) + close); + i = closeAt + close.length; + continue; + } + } + } + if (text[i] === "$" && !isEscaped(text, i)) { + const size = dollarRun(text, i); + const isDisplay = dollarIsDisplay(text, i, size); + const close = findDollarClose(text, i + size, size, isDisplay); + if (close) { + out.push( + text.slice(i, i + size) + + halveOverescapedCommandRuns(text.slice(i + size, close.at)) + + text.slice(close.at, close.after), + ); + i = close.after; + continue; + } + out.push(text.slice(i, i + size)); + i += size; + continue; + } + out.push(text[i]!); + i += 1; + } + return out.join(""); +} diff --git a/src/core/tool-parts.ts b/src/core/tool-parts.ts index 189a8ff..980166e 100644 --- a/src/core/tool-parts.ts +++ b/src/core/tool-parts.ts @@ -30,7 +30,7 @@ import { Value } from "typebox/value"; import type { TSchema } from "typebox"; import { quizDisplayInputSchema, askInputSchema, askOptionProblems, quizInputProblems, explainInputProblems, explainDisplayInputSchema, type AskToolInput, type QuizToolInput, type ExplainToolInput } from "./question-core.ts"; -import { restoreMathEscapes } from "./math-delims.ts"; +import { restoreMathEscapes, healOverescapedCommands } from "./math-delims.ts"; import { healEncodedArguments } from "./argument-heal.ts"; import { probeDoneInputSchema, type ProbeDoneInput } from "./contracts/probe.ts"; @@ -72,9 +72,14 @@ function boundaryParse(schema: TSchema, input: unknown): T | undefined { * single-backslash LaTeX eaten by JSON escape resolution (FF ate the 'f' of * \forall, VT the 'v' of \varphi, TAB the 't' of \to) survives to the * display path. This is the ONE seam every rendered question passes - * through — new turns and already-poisoned records alike heal here. */ + * through — new turns and already-poisoned records alike heal here. + * healOverescapedCommands (owner ruling 2026-10-15) rides the same walk: + * the same models also stochastically DOUBLE command backslashes + * (`\\nabla` saved where `\nabla` was meant — KaTeX shows the row break + * plus "nabla" as letters), so even runs before a command start inside + * balanced math spans collapse to one backslash. */ function restoreMathEscapesDeep(value: unknown): unknown { - if (typeof value === "string") return restoreMathEscapes(value); + if (typeof value === "string") return healOverescapedCommands(restoreMathEscapes(value)); if (Array.isArray(value)) return value.map(restoreMathEscapesDeep); if (typeof value === "object" && value !== null) { const out: Record = {}; diff --git a/tests/render-markdown.test.tsx b/tests/render-markdown.test.tsx index 4263d98..a3f9f8f 100644 --- a/tests/render-markdown.test.tsx +++ b/tests/render-markdown.test.tsx @@ -15,7 +15,7 @@ import { describe, it } from "node:test"; import { renderToStaticMarkup } from "react-dom/server"; import { MessageResponse } from "../components/ai-elements/message.tsx"; import { MermaidError } from "../components/mermaid-error.tsx"; -import { transformMathDelimiters } from "../src/core/math-delims.ts"; +import { transformMathDelimiters, healOverescapedCommands } from "../src/core/math-delims.ts"; /** Render through MessageResponse in static mode (no streaming/remend artifacts). */ function render(md: string): string { @@ -345,3 +345,102 @@ describe("escaped dollars inside math — rendering", () => { assert.ok(html.includes("$x = \\$5$"), "the literal source stays visible"); }); }); + +describe("over-escaped command heal — doubled backslashes in tool args (owner ruling 2026-10-15)", () => { + // Field evidence: xiaomi/mimo-v2.6-pro and deepseek-v4.1-flash + // stochastically DOUBLE LaTeX command backslashes inside JSON tool + // arguments — sometimes per-field within one call (the gradient-descent + // session's first probe: question doubled, explanation+options clean). + // KaTeX reads `\\` as a row break and renders the command name as plain + // letters — "renders as LaTeX but reads the word nabla". The heal lives + // at the tool-parts argument seam (restoreMathEscapesDeep); these are its + // pure units. TS literals: 4 backslashes = 2 real, 6 = 3 real. + + /** The gradient-descent session's probe question (chat_mv7r02og, msg 3) + * in its CLEAN single-backslash form. */ + const GD_QUESTION = + "Let $f : \\mathbb{R}^n \\to \\mathbb{R}$ be differentiable at $x$, with $\\nabla f(x) \\neq 0$. The directional derivative along a unit direction $u$ is $D_u f(x) = \\nabla f(x)^\\top u$. Among all unit directions $\\|u\\|_2 = 1$, which one makes $D_u f(x) **most negative** (steepest descent)?"; + + it("heals a doubled letter command inside $…$", () => { + assert.equal(healOverescapedCommands("$\\\\nabla f$"), "$\\nabla f$"); + }); + + it("heals a quadrupled letter command (the agent's over-correction)", () => { + assert.equal(healOverescapedCommands("$\\\\\\\\mathbb{R}^n$"), "$\\mathbb{R}^n$"); + }); + + it("heals inside multi-line $$…$$ display spans", () => { + assert.equal( + healOverescapedCommands("$$\n\\\\nabla f(x)\n$$"), + "$$\n\\nabla f(x)\n$$", + ); + }); + + it("heals legacy \\(…\\) spans too", () => { + assert.equal(healOverescapedCommands("\\(\\\\nabla\\)"), "\\(\\nabla\\)"); + }); + + it("heals the punctuation commands: \\| \\, \\; \\: \\!", () => { + assert.equal(healOverescapedCommands("$\\\\|u\\\\|_2$"), "$\\|u\\|_2$"); + assert.equal(healOverescapedCommands("$a\\\\,b$"), "$a\\,b$"); + assert.equal(healOverescapedCommands("$a\\\\;b$"), "$a\\;b$"); + assert.equal(healOverescapedCommands("$a\\\\:b$"), "$a\\:b$"); + assert.equal(healOverescapedCommands("$a\\\\!b$"), "$a\\!b$"); + }); + + it("KEEPS legitimate \\ row breaks — space, newline, &, [, digits follow", () => { + const matrix = "\\begin{pmatrix} 1 \\\\ 2 \\\\\\ 3\\end{pmatrix}"; // \\ then \\\\ + assert.equal(healOverescapedCommands(matrix), matrix); + const aligned = "f(x) \\\\& g(x) \\\\[6pt] h(x)"; + assert.equal(healOverescapedCommands(aligned), aligned); + }); + + it("KEEPS odd runs — a row break plus a real command cell", () => { + // 3 real backslashes then a letter: \\ row break + \\nabla command. + const cell = "$a \\\\\\nabla b$"; + assert.equal(healOverescapedCommands(cell), cell); + }); + + it("touches nothing outside math spans (prose, code spans, fences)", () => { + assert.equal(healOverescapedCommands("path C:\\\\Users\\\\amos"), "path C:\\\\Users\\\\amos"); + assert.equal(healOverescapedCommands("`$\\\\nabla$` in code"), "`$\\\\nabla$` in code"); + assert.equal( + healOverescapedCommands("```\n$\\\\nabla$\n```"), + "```\n$\\\\nabla$\n```", + ); + }); + + it("unbalanced spans pass through untouched (mid-stream safety)", () => { + assert.equal(healOverescapedCommands("$\\\\nabla"), "$\\\\nabla"); + }); + + it("the raw doubled gradient-descent question renders the word-signature (the bug this heal exists for)", () => { + // The real session record (chat_mv7r02og, msg 3), expressed as the + // clean form with every backslash doubled — exactly what mimo emitted. + const raw = GD_QUESTION.replace(/\\/g, "\\\\"); + const html = render(raw); + assert.ok(!html.includes("∇"), "doubled form does not produce the glyph"); + assert.ok(html.includes("katex-error"), "doubled form errors in KaTeX"); + }); + + it("the healed question renders ∇ and ⊤ with zero KaTeX errors", () => { + const html = render(healOverescapedCommands(GD_QUESTION.replace(/\\/g, "\\\\"))); + assert.ok(html.includes("∇"), "nabla glyph renders"); + assert.ok(html.includes("mathbb"), "mathbb R renders (double-struck class)"); + assert.ok(html.includes("⊤"), "top renders"); + assert.ok(!html.includes("katex-error"), "no KaTeX errors"); + }); + + it("the healed inference-runtime question renders ∈ and ×", () => { + // The real session record (chat_mv8ybpvs, msg 7) — mimo doubled the + // whole field; clean form with every backslash doubled. + const ir = + "Let $A \\in \\mathbb{R}^{m \\times k}$ and $B \\in \\mathbb{R}^{k \\times n}$, and let $C = AB$ be their matrix product."; + const html = render(healOverescapedCommands(ir.replace(/\\/g, "\\\\"))); + assert.ok(html.includes("∈"), "in renders"); + assert.ok(html.includes("mathbb"), "mathbb renders (double-struck class)"); + assert.ok(html.includes("×"), "times renders"); + assert.ok(!html.includes("katex-error"), "no KaTeX errors"); + }); +}); + diff --git a/tests/tool-parts.test.ts b/tests/tool-parts.test.ts index e1482d2..baccdfe 100644 --- a/tests/tool-parts.test.ts +++ b/tests/tool-parts.test.ts @@ -400,3 +400,81 @@ describe("parseExplainInput: the dock's display seam for explain calls", () => { assert.equal(parseExplainInput({ question: "q", explanation: "e", correctAnswer: 42 })?.correctAnswer, "42"); }); }); + +describe("over-escaped command heal at the parse seam (owner ruling 2026-10-15)", () => { + // xiaomi/mimo-v2.6-pro and deepseek-v4.1-flash stochastically DOUBLE + // LaTeX command backslashes inside JSON tool arguments — per-call and + // per-field (gradient-descent msg 3: question doubled, explanation and + // options in the SAME call clean). restoreMathEscapesDeep now rides + // healOverescapedCommands after the debris restore, so doubled spans + // heal at every display parse: live dock, settled cards, old records. + const CLEAN = + "Let $f : \\mathbb{R}^n \\to \\mathbb{R}$ be differentiable at $x$, with $\\nabla f(x) \\neq 0$. Among all unit directions $\\|u\\|_2 = 1$, which makes $D_u f(x)$ most negative?"; + const DOUBLE = (s: string): string => s.replace(/\\/g, "\\\\"); + const BASE = { + options: [ + { label: "$u^\\star = -\\nabla f(x)$", value: "neg" }, + { label: "$u^\\star = \\nabla f(x)$", value: "pos" }, + ], + correctAnswer: "neg", + explanation: "By Cauchy\u2013Schwarz: $$u^\\star = -\\frac{\\nabla f(x)}{\\|\\nabla f(x)\\|_2}$$", + }; + + it("parseQuizInput heals a doubled question field (the gradient-descent msg-3 shape)", () => { + const parsed = parseQuizInput({ ...BASE, question: DOUBLE(CLEAN) }); + assert.ok(parsed, "parses"); + assert.equal(parsed.question, CLEAN); + // The CLEAN sibling fields of the same call stay byte-identical. + assert.equal(parsed.options[0]!.label, BASE.options[0]!.label); + assert.equal(parsed.explanation, BASE.explanation); + }); + + it("parseQuizInput heals a quadrupled field (the agent's over-correction, msg 6)", () => { + const parsed = parseQuizInput({ ...BASE, question: DOUBLE(DOUBLE(CLEAN)) }); + assert.ok(parsed, "parses"); + assert.equal(parsed.question, CLEAN); + }); + + it("doubled option labels and explanations heal too (the inference-runtime msg-7 shape)", () => { + const parsed = parseQuizInput({ + question: "Let $A \\in \\mathbb{R}^{m \\times k}$ and $C = AB$.", + options: [ + { label: DOUBLE("$mkn$"), value: "mkn" }, + { label: DOUBLE("$2mkn$"), value: "2" }, + ], + correctAnswer: "mkn", + explanation: DOUBLE("$$c_{ij} = \\sum_{\\ell=1}^{k} a_{i\\ell}\\, b_{\\ell j}$$"), + }); + assert.ok(parsed, "parses"); + assert.equal(parsed.options[0]!.label, "$mkn$"); + assert.equal(parsed.options[1]!.label, "$2mkn$"); + assert.equal(parsed.explanation, "$$c_{ij} = \\sum_{\\ell=1}^{k} a_{i\\ell}\\, b_{\\ell j}$$"); + }); + + it("parseAskInput heals doubled ask-option descriptions the same way", () => { + const parsed = parseAskInput({ + question: "Depth?", + options: [ + { label: "Rigorous", description: DOUBLE("why $-\\nabla f$ is steepest descent") }, + { label: "Intuition" }, + ], + }); + assert.ok(parsed, "parses"); + assert.equal(parsed.options![0]!.description, "why $-\\nabla f$ is steepest descent"); + }); + + it("clean records pass through byte-identical (no over-heal)", () => { + const parsed = parseQuizInput({ ...BASE, question: CLEAN }); + assert.ok(parsed, "parses"); + assert.equal(parsed.question, CLEAN); + assert.equal(parsed.explanation, BASE.explanation); + }); + + it("legitimate row breaks inside matrices survive the seam", () => { + const matrix = + "Which matrix matches? $$\\begin{pmatrix} 1 \\ 2 \\\\ 3 \\ 4\\end{pmatrix}$$"; + const parsed = parseQuizInput({ ...BASE, question: matrix }); + assert.ok(parsed, "parses"); + assert.equal(parsed.question, matrix); + }); +});