math: heal overescaped LaTeX commands in question displays

This commit is contained in:
amosblomqvist
2026-10-16 21:24:14 +03:00
parent 1a11de5cee
commit b3c9f9e18b
4 changed files with 283 additions and 4 deletions
+97
View File
@@ -468,3 +468,100 @@ function restoreDollarMathEscapes(text: string): string {
} }
return out.join(""); 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("");
}
+8 -3
View File
@@ -30,7 +30,7 @@
import { Value } from "typebox/value"; import { Value } from "typebox/value";
import type { TSchema } from "typebox"; import type { TSchema } from "typebox";
import { quizDisplayInputSchema, askInputSchema, askOptionProblems, quizInputProblems, explainInputProblems, explainDisplayInputSchema, type AskToolInput, type QuizToolInput, type ExplainToolInput } from "./question-core.ts"; 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 { healEncodedArguments } from "./argument-heal.ts";
import { probeDoneInputSchema, type ProbeDoneInput } from "./contracts/probe.ts"; import { probeDoneInputSchema, type ProbeDoneInput } from "./contracts/probe.ts";
@@ -72,9 +72,14 @@ function boundaryParse<T>(schema: TSchema, input: unknown): T | undefined {
* single-backslash LaTeX eaten by JSON escape resolution (FF ate the 'f' of * 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 * \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 * 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 { 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 (Array.isArray(value)) return value.map(restoreMathEscapesDeep);
if (typeof value === "object" && value !== null) { if (typeof value === "object" && value !== null) {
const out: Record<string, unknown> = {}; const out: Record<string, unknown> = {};
+100 -1
View File
@@ -15,7 +15,7 @@ import { describe, it } from "node:test";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { MessageResponse } from "../components/ai-elements/message.tsx"; import { MessageResponse } from "../components/ai-elements/message.tsx";
import { MermaidError } from "../components/mermaid-error.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). */ /** Render through MessageResponse in static mode (no streaming/remend artifacts). */
function render(md: string): string { 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"); 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");
});
});
+78
View File
@@ -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"); 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);
});
});