fix: preserve source data and recover tool math at presentation
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { repairMathForDisplay, restoreMathEscapes, transformMathDelimiters } from "../src/core/math-delims.ts";
|
||||
|
||||
const math = String.raw;
|
||||
test("document paragraphs and display framing recover without interpreting n-commands as newlines", () => {
|
||||
const source = math`First.\n\nSecond: $\nabla f\neq0$.\n\n$$\n\frac{1}{2}\n$$`;
|
||||
assert.equal(repairMathForDisplay(source), "First.\n\nSecond: " + math`$\nabla f\neq0$.` + "\n\n$$\n" + math`\frac{1}{2}` + "\n$$");
|
||||
});
|
||||
test("literal document escapes fused to unknown math commands use document context", () => {
|
||||
const source = math`Jacobian:\n\n$$\nJ=\begin{pmatrix}1&2\\3&4\end{pmatrix}\n$$`;
|
||||
assert.equal(repairMathForDisplay(source), "Jacobian:\n\n$$\n" + math`J=\begin{pmatrix}1&2\\3&4\end{pmatrix}` + "\n$$");
|
||||
});
|
||||
test("encoded environment profiles decode row separators by their factor, not to one slash", () => {
|
||||
for (const [source, expected] of [
|
||||
[math`$\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}$`, math`$\begin{pmatrix}a&b\\c&d\end{pmatrix}$`],
|
||||
[math`$\\begin{pmatrix}1&2\\\\3&4\\end{pmatrix}$`, math`$\begin{pmatrix}1&2\\3&4\end{pmatrix}$`],
|
||||
[math`$\\begin{pmatrix}a&b\\\\\\nabla f&d\\end{pmatrix}$`, math`$\begin{pmatrix}a&b\\\nabla f&d\end{pmatrix}$`],
|
||||
[math`$\\substack{a\\\\beta}$`, math`$\substack{a\\beta}$`],
|
||||
] as const) {
|
||||
assert.equal(repairMathForDisplay(source), expected);
|
||||
assert.equal(repairMathForDisplay(expected), expected);
|
||||
}
|
||||
});
|
||||
test("native substack rows and literal document escapes in genuine comments remain literal", () => {
|
||||
const source = math`$\substack{a\\beta}$`;
|
||||
assert.equal(repairMathForDisplay(source), source);
|
||||
const comment = "$x % " + math`literal \n\n` + "\ny$";
|
||||
assert.equal(repairMathForDisplay(comment), comment);
|
||||
});
|
||||
test("inline lost prefixes recover only known full commands; display newlines remain whitespace", () => {
|
||||
assert.equal(repairMathForDisplay("$\nabla f$"), math`$\nabla f$`);
|
||||
assert.equal(repairMathForDisplay("$x+\ny$"), "$x+\ny$");
|
||||
assert.equal(repairMathForDisplay("$x+\nu$"), math`$x+\nu$`); // approved typical-inline-authoring policy
|
||||
assert.equal(repairMathForDisplay("$$\nabla f\n$$"), "$$\nabla f\n$$"); // display source whitespace
|
||||
});
|
||||
test("physical display newlines and genuine row breaks/comments survive", () => {
|
||||
const source = "$$\n" + math`\begin{aligned}a&=b\\% row comment` + "\nc&=d\end{aligned}\n$$";
|
||||
assert.equal(repairMathForDisplay(source), source);
|
||||
});
|
||||
test("proper nu survives every context, including fields with document escapes", () => {
|
||||
for (const source of [math`$\nu$`, "$$\n" + math`\nu` + "\n$$", math`$\nabla f+\nu+\neq$`]) {
|
||||
assert.equal(repairMathForDisplay(source), source);
|
||||
}
|
||||
assert.equal(repairMathForDisplay(math`First.\n\n$\nu$`), "First.\n\n" + math`$\nu$`);
|
||||
assert.equal(repairMathForDisplay("$$\nu\n$$"), "$$\nu\n$$"); // LF + variable u in display
|
||||
});
|
||||
test("tabs recover known commands including to, not arbitrary whitespace", () => {
|
||||
assert.equal(repairMathForDisplay("$$\nx\t+\ty\n$$"), "$$\nx\t+\ty\n$$");
|
||||
assert.equal(repairMathForDisplay("$a\to b$"), math`$a\to b$`);
|
||||
assert.equal(repairMathForDisplay("$$\na\to b\n$$"), "$$\n" + math`a\to b` + "\n$$");
|
||||
assert.equal(repairMathForDisplay("$$\r\nx+1\r\n$$"), "$$\r\nx+1\r\n$$");
|
||||
});
|
||||
test("Markdown containers protect literal code and do not swallow following content", () => {
|
||||
for (const source of [
|
||||
"> ~~~text\n> " + math`$\\alpha$ \n\n` + "\n> ~~~",
|
||||
"> > ~~~text\n> > " + math`$\\alpha$` + "\n> > ~~~",
|
||||
"- ~~~text\n " + math`$\\alpha$` + "\n ~~~",
|
||||
"> ~~~text\n> " + math`$\\alpha$` + "\n\n~~~text\n" + math`$\\beta$` + "\n~~~",
|
||||
]) {
|
||||
assert.equal(repairMathForDisplay(source), source);
|
||||
assert.equal(transformMathDelimiters(source), source);
|
||||
assert.equal(repairMathForDisplay(source + "\n\n" + math`$\\gamma$`), source + "\n\n" + math`$\gamma$`);
|
||||
}
|
||||
});
|
||||
test("numeric percentages recover, including percentages inside text groups", () => {
|
||||
for (const [source, expected] of [
|
||||
[math`$60\\%$`, math`$60\%$`],
|
||||
[math`$50\\%+10\\%=60\\%$`, math`$50\%+10\%=60\%$`],
|
||||
[math`$\text{ (50\\%)}$`, math`$\text{ (50\%)}$`],
|
||||
] as const) assert.equal(repairMathForDisplay(source), expected);
|
||||
});
|
||||
test("numeric row breaks followed by genuine comments are not percentage escapes", () => {
|
||||
const source = "$60\\\\% explanation of this row\nx$";
|
||||
assert.equal(repairMathForDisplay(source), source);
|
||||
const ambiguous = math`$x\\%$`;
|
||||
assert.equal(repairMathForDisplay(ambiguous), ambiguous);
|
||||
});
|
||||
test("inline and fenced code retain all bytes, including legacy delimiters and actual control characters", () => {
|
||||
const code = "```text\n\\(x\nabla\\)\n\\n\\n\n```\n" + "`" + math`\n\n $60\\%$ \(x\)` + "`";
|
||||
assert.equal(repairMathForDisplay(code), code);
|
||||
assert.equal(restoreMathEscapes(code), code);
|
||||
});
|
||||
test("math matching cannot consume a blank paragraph or a code span", () => {
|
||||
const source = "Price $5.\n\nThen $x$ and `code` remains.";
|
||||
assert.equal(repairMathForDisplay(source), source);
|
||||
assert.equal(repairMathForDisplay("$not `code` math$"), "$not `code` math$");
|
||||
});
|
||||
test("Windows paths and correctly written n-commands are not document escapes", () => {
|
||||
const source = math`Path C:\new\notes; symbols $\nu+\ni+\neq+\nabla f$.\n\nNext paragraph.`;
|
||||
assert.equal(repairMathForDisplay(source), math`Path C:\new\notes; symbols $\nu+\ni+\neq+\nabla f$.` + "\n\nNext paragraph.");
|
||||
});
|
||||
test("declared n-prefixed macros are commands, not document escapes", () => {
|
||||
const source = math`$\newcommand{\nThing}{x}\nThing$.\n\nNext.`;
|
||||
assert.equal(repairMathForDisplay(source), math`$\newcommand{\nThing}{x}\nThing$.` + "\n\nNext.");
|
||||
});
|
||||
test("partial recovery cannot turn otherwise parseable TeX into a missing-argument error", () => {
|
||||
const source = math`$x\\frac{a}$`;
|
||||
assert.equal(repairMathForDisplay(source), source);
|
||||
});
|
||||
test("legacy compatibility and recovery share code barriers, including tilde and indentation", () => {
|
||||
for (const source of [" " + math`\(x\) \n\n`, "\\[\n~~~tex\n" + math`\n\n \(x\)` + "\n\\]\n~~~"]) {
|
||||
assert.equal(repairMathForDisplay(source), source);
|
||||
assert.equal(transformMathDelimiters(source), source);
|
||||
}
|
||||
const comment = "$60% " + math`\n\n` + "\nx$";
|
||||
assert.equal(repairMathForDisplay(comment), comment);
|
||||
});
|
||||
test("recovery converges after one pass, including multiple debris characters", () => {
|
||||
for (const source of ["$\forall x,\theta+\to$", math`First.\n\n$$\nJ=\\frac{1}{2}+60\\%\n$$`,
|
||||
math`$\begin{pmatrix}a&b\\c&d\end{pmatrix}$`, "```text\n\\(\nabla\\)\n```", "$\nabla f$",
|
||||
]) {
|
||||
const once = repairMathForDisplay(source);
|
||||
assert.equal(repairMathForDisplay(once), once);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
[
|
||||
{
|
||||
"group": "multilingual-20261025-01",
|
||||
"chatId": "chat_mvo321f7_4202fae0",
|
||||
"source": "messages.1.content.1.arguments.explanation",
|
||||
"model": "google/gemini-3.7-flash",
|
||||
"kind": "percent-at-display-end",
|
||||
"raw": "To find the posterior probability $P(D \\\\mid +)$, we apply Bayes' theorem:\n$$\nP(D \\\\mid +) = \\\\frac{P(D \\\\cap +)}{P(+)} = \\\\frac{P(D)P(+ \\\\mid D)}{P(D)P(+ \\\\mid D) + P(\\\\neg D)P(+ \\\\mid \\\\neg D)}\n$$\n\nGiven:\n- $P(D) = 0.01$ (and thus $P(\\\\neg D) = 0.99$)\n- $P(+ \\\\mid D) = 0.90$\n- $P(+ \\\\mid \\\\neg D) = 0.05$\n\nEvaluating the numerator and denominator:\n$$\nP(D \\\\cap +) = 0.01 \\\\times 0.90 = 0.009\n$$\n$$\nP(\\\\neg D \\\\cap +) = 0.99 \\\\times 0.05 = 0.0495\n$$\n$$\nP(+) = 0.009 + 0.0495 = 0.0585\n$$\n\nTherefore:\n$$\nP(D \\\\mid +) = \\\\frac{0.009}{0.0585} = \\\\frac{9}{58.5} = \\\\frac{2}{13} \\\\approx 15.4\\\\%\n$$"
|
||||
},
|
||||
{
|
||||
"group": "multilingual-20261025-01",
|
||||
"chatId": "chat_mvo321f7_4202fae0",
|
||||
"source": "messages.5.content.1.arguments.explanation",
|
||||
"model": "google/gemini-3.7-flash",
|
||||
"kind": "percent-at-display-end",
|
||||
"raw": "Because the two tests are conditionally independent given disease status:\n$$\nP(T_1 = +, T_2 = + \\\\mid D) = P(T_1 = + \\\\mid D) \\\\cdot P(T_2 = + \\\\mid D) = 0.90 \\\\times 0.90 = 0.81\n$$\n$$\nP(T_1 = +, T_2 = + \\\\mid \\\\neg D) = P(T_1 = + \\\\mid \\\\neg D) \\\\cdot P(T_2 = + \\\\mid \\\\neg D) = 0.10 \\\\times 0.10 = 0.01\n$$\n\nNow calculate the joint probabilities:\n$$\nP(D \\\\cap T_1 = + \\\\cap T_2 = +) = P(D) \\\\cdot P(T_1 = +, T_2 = + \\\\mid D) = 0.01 \\\\times 0.81 = 0.0081\n$$\n$$\nP(\\\\neg D \\\\cap T_1 = + \\\\cap T_2 = +) = P(\\\\neg D) \\\\cdot P(T_1 = +, T_2 = + \\\\mid \\\\neg D) = 0.99 \\\\times 0.01 = 0.0099\n$$\n\nTotal probability of two positive tests:\n$$\nP(T_1 = +, T_2 = +) = 0.0081 + 0.0099 = 0.0180\n$$\n\nApplying Bayes' theorem:\n$$\nP(D \\\\mid T_1 = +, T_2 = +) = \\\\frac{0.0081}{0.0180} = \\\\frac{81}{180} = \\\\frac{9}{20} = 45\\\\%\n$$"
|
||||
},
|
||||
{
|
||||
"group": "multilingual-20261025-01",
|
||||
"chatId": "chat_mvo4op90_74572548",
|
||||
"source": "messages.1.content.2.arguments.explanation",
|
||||
"language": "pt",
|
||||
"model": "deepseek/deepseek-v4.1-flash",
|
||||
"kind": "percent-at-display-end",
|
||||
"raw": "Em $10\\\\,000$ pessoas: $100$ estão doentes e $99$ dessas testam positivo; $9\\\\,900$ estão saudáveis e $5\\\\%$ delas, isto é $495$, testam positivo. Total de positivos: $99 + 495 = 594$. Entre os positivos, a fração de doentes é\\n$$\\n\\\\frac{99}{594} = \\\\frac{1}{6} \\\\approx 16{,}7\\\\%\\n$$\\nA base pequena de doentes faz os falsos positivos ($495$) dominarem os verdadeiros positivos ($99$)."
|
||||
},
|
||||
{
|
||||
"group": "multilingual-20261025-01",
|
||||
"chatId": "chat_mvo4op90_74572548",
|
||||
"source": "messages.3.content.2.arguments.explanation",
|
||||
"language": "pt",
|
||||
"model": "deepseek/deepseek-v4.1-flash",
|
||||
"kind": "percent-at-display-end",
|
||||
"raw": "Continua valendo a mesma estrutura de Bayes, agora com números diferentes. Em $10\\\\,000$ pessoas: $200$ doentes, das quais $90\\\\%$ testam positivo $= 180$; $9\\\\,800$ saudáveis, das quais $5\\\\%$ testam positivo $= 490$ falsos positivos. Total de positivos: $180 + 490 = 670$.\\n$$\\nP(\\\\text{doente} \\\\mid +) = \\\\frac{180}{670} \\\\approx 26{,}9\\\\%\\n$$\\nNote que a especificidade $95\\\\%$ entra na conta pela sua taxa de falso positivo $1 - 0{,}95 = 0{,}05$, pesada por $P(\\\\text{saudável}) = 0{,}98$."
|
||||
},
|
||||
{
|
||||
"group": "multilingual-20261025-01",
|
||||
"chatId": "chat_mvo8kgtc_65767ffd",
|
||||
"source": "messages.5.content.1.arguments.question",
|
||||
"language": "es",
|
||||
"model": "xiaomi/mimo-v2.6-flash",
|
||||
"kind": "document-math-framing",
|
||||
"raw": "Una enfermedad afecta al $1\\%$ de una población. La prueba tiene una sensibilidad del $99\\%$ (probabilidad de dar positivo si la enfermedad está presente) y una tasa de falsos positivos del $5\\%$ (probabilidad de dar positivo si no está presente). Una persona al azar da positivo. ¿Cuál es la probabilidad de que realmente tenga la enfermedad?\\n\\n$$\\nP(\\text{enf} \\mid +) \\;=\\; ?$$"
|
||||
},
|
||||
{
|
||||
"group": "multilingual-20261025-01",
|
||||
"chatId": "chat_mvo91klg_5332fa79",
|
||||
"source": "messages.3.content.1.arguments.explanation",
|
||||
"language": "zh-Hans",
|
||||
"model": "xiaomi/mimo-v2.6-flash",
|
||||
"kind": "percent-at-display-end",
|
||||
"raw": "条件概率把样本空间缩小到条件所指定的那一部分。已知\"检测阳性\",人群就从 $1000$ 人缩到 $140$ 人,其中患病的有 $45$ 人,所以\\n$$\\nP(\\\\text{患病}\\\\mid\\\\text{阳性})=\\\\frac{P(\\\\text{患病}\\\\cap\\\\text{阳性})}{P(\\\\text{阳性})}=\\\\frac{45/1000}{140/1000}=\\\\frac{45}{140}\\\\approx 32.1\\\\%.\\n$$\\n另外三个数各有各的含义:$\\\\frac{45}{50}=90\\\\%$ 是 $P(\\\\text{阳性}\\\\mid\\\\text{患病})$(敏感度,条件方向反了);$\\\\frac{45}{1000}=4.5\\\\%$ 是联合概率 $P(\\\\text{患病}\\\\cap\\\\text{阳性})$(没做条件化);$\\\\frac{140}{1000}=14\\\\%$ 是 $P(\\\\text{阳性})$(不分是否患病的总阳性率)。"
|
||||
},
|
||||
{
|
||||
"group": "multilingual-20261025-01",
|
||||
"chatId": "chat_mvo91klg_5332fa79",
|
||||
"source": "messages.7.content.1.arguments.explanation",
|
||||
"language": "zh-Hans",
|
||||
"model": "xiaomi/mimo-v2.6-flash",
|
||||
"kind": "percent-at-display-end",
|
||||
"raw": "用 $10000$ 人(或直接用全概率公式)算分母:\\n$$\\n\\\\begin{aligned}\\n\\\\text{患病且阳性}&=10000\\\\times 0.01\\\\times 0.99=99,\\\\\\\\\\n\\\\text{未患病但阳性}&=10000\\\\times 0.99\\\\times 0.05=495.\\n\\\\end{aligned}\\n$$\\n阳性总数 $=99+495=594$,所以\\n$$\\nP(D\\\\mid +)=\\\\frac{99}{594}\\\\approx 16.7\\\\%.\\n$$\\n各错误选项对应的具体失误:$99\\\\%$ 是把敏感度 $P(+\\\\mid D)$ 当成了 $P(D\\\\mid +)$(条件方向颠倒);$95.2\\\\%=\\\\frac{0.99}{0.99+0.05}$ 是把阳性率和假阳性率直接当人数用,漏掉了基础发生率的加权($99\\\\%$ 的人未患病,所以少量假阳性也架不住人多);$1\\\\%$ 是看到了阳性结果却不更新概率。"
|
||||
},
|
||||
{
|
||||
"group": "multilingual-20261025-01",
|
||||
"chatId": "chat_mvo91klg_5332fa79",
|
||||
"source": "messages.9.content.1.arguments.explanation",
|
||||
"language": "zh-Hans",
|
||||
"model": "xiaomi/mimo-v2.6-flash",
|
||||
"kind": "percent-at-display-end",
|
||||
"raw": "两次阳性后的后验,用同样的公式但似然要平方:\\n$$\\n\\\\begin{aligned}\\nP(++\\\\mid D)&=0.99^2=0.9801,\\\\\\\\\\nP(++\\\\mid D')&=0.05^2=0.0025.\\n\\\\end{aligned}\\n$$\\n$$\\nP(D\\\\mid ++)\\\\frac{0.99^2\\\\times 0.01}{0.99^2\\\\times 0.01+0.05^2\\\\times 0.99}=\\\\frac{0.009801}{0.009801+0.002475}\\\\approx 79.8\\\\%.\\n$$\\n关键在于**两条分支都要平方**:患病分支的似然变成 $0.99^2$,未患病分支的假阳性率变成 $0.05^2$($5\\%$ 是很小的数,平方后变得更小,所以假阳性这一侧收缩得比患病侧更狠,后验从 $16.7\\\\%$ 跳到约 $80\\\\%$)。其他选项的失误:$98\\\\%$ 是把 $P(++\\\\mid D)$ 当成了 $P(D\\\\mid ++)$;$16.5\\\\%$ 只对患病分支用了两次独立,未患病分支仍用单次假阳性率;$33\\\\%$ 是把单次后验直接乘 $2$——两次阳性提供的证据不是相加的,而要在似然层面相乘。"
|
||||
},
|
||||
{
|
||||
"group": "multilingual-20261025-01",
|
||||
"chatId": "chat_mvo9c830_3866e4a6",
|
||||
"source": "messages.7.content.1.arguments.explanation",
|
||||
"language": "zh-Hans",
|
||||
"model": "anthropic/claude-haiku-5.5",
|
||||
"kind": "percent-at-display-end",
|
||||
"raw": "阳性总人数:真阳性 $100\\\\times0.9=90$,假阳性 $9900\\\\times0.05=495$,合计 $585$。\n\n所求的是阳性者中患病者的占比,分母是全部阳性者:\n$$\n\\\\frac{90}{585}\\\\approx15.4\\\\%\n$$\n\n$90\\\\%$ 是患病者中阳性的比例,分母错成了患病者;$84.6\\\\%$ 是阳性者中健康者的占比,回答的是另一个问题;$5\\\\%$ 是健康者中的假阳性率,分母是健康人群。"
|
||||
},
|
||||
{
|
||||
"group": "multilingual-20261025-01",
|
||||
"chatId": "chat_mvo9nu8w_dcc7add9",
|
||||
"source": "messages.3.content.1.arguments.explanation",
|
||||
"language": "id",
|
||||
"model": "xiaomi/mimo-v2.6-flash",
|
||||
"kind": "percent-at-display-end",
|
||||
"raw": "Pakai aturan Bayes dengan menghitung dulu kejadian gabungan dalam satu kelompok besar, misalnya $10.000$ orang:\\n\\n- Sakit dan positif: $10.000 \\\\times 0{,}01 \\\\times 0{,}99 = 99$ orang.\\n- Sehat dan positif: $10.000 \\\\times 0{,}99 \\\\times 0{,}05 = 495$ orang.\\n\\nSemua yang hasilnya positif: $99 + 495 = 594$ orang. Maka\\n$$\\nP(\\\\text{sakit}\\\\mid +) = \\\\frac{99}{594} = \\\\frac{1}{6} \\\\approx 16{,}7\\\\%.\\n$$\\n\\nYang menipu di sini adalah base rate: kelompok sehat jauh lebih besar, sehingga meski hanya 5% dari mereka yang salah positif, jumlahnya tetap 5 kali lipat jumlah positif yang benar. Nilai $95{,}2\\\\%$ muncul kalau kelompok sakit dan sehat dianggap sama besar ($\\\\frac{99}{99+5}$); nilai $99\\\\%$ muncul kalau arah kondisinya dibalik, membaca $P(+\\\\mid \\\\text{sakit})$ sebagai $P(\\\\text{sakit}\\\\mid +)$."
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"notice": "Unmodified model-authored fields from the frozen multilingual corpus. oldDisplay records the diagnosed application damage, not an expected output.",
|
||||
"cases": [
|
||||
{
|
||||
"run": "pilot-20261025-01",
|
||||
"model": "openai/gpt-6-luna",
|
||||
"language": "en",
|
||||
"replicate": 1,
|
||||
"chatId": "chat_mvo1rzjs_056eac45",
|
||||
"source": "messages.5.content.1.arguments.explanation",
|
||||
"raw": "For a $2\\times2$ matrix, $\\det\\begin{bmatrix}a&b\\\\c&d\\end{bmatrix}=ad-bc$. Here $3\\cdot2-6\\cdot1=0$. A square matrix is invertible exactly when its determinant is nonzero, so $M$ is singular. Its columns are also proportional, which matches the determinant-zero result.",
|
||||
"oldDisplay": "For a $2\\times2$ matrix, $\\det\\begin{bmatrix}a&b\\c&d\\end{bmatrix}=ad-bc$. Here $3\\cdot2-6\\cdot1=0$. A square matrix is invertible exactly when its determinant is nonzero, so $M$ is singular. Its columns are also proportional, which matches the determinant-zero result."
|
||||
},
|
||||
{
|
||||
"run": "pilot-20261025-01",
|
||||
"model": "openai/gpt-6-luna",
|
||||
"language": "en",
|
||||
"replicate": 1,
|
||||
"chatId": "chat_mvo1sdnj_8f2d8c5e",
|
||||
"source": "messages.9.content.1.arguments.explanation",
|
||||
"raw": "The Jacobian has one row per output and one column per input: $J_F=\\begin{pmatrix}2x&1\\\\y&x\\end{pmatrix}$. At $(1,2)$ this is $\\begin{pmatrix}2&1\\\\2&1\\end{pmatrix}$.",
|
||||
"oldDisplay": "The Jacobian has one row per output and one column per input: $J_F=\\begin{pmatrix}2x&1\\y&x\\end{pmatrix}$. At $(1,2)$ this is $\\begin{pmatrix}2&1\\\\2&1\\end{pmatrix}$."
|
||||
},
|
||||
{
|
||||
"run": "pilot-20261025-01",
|
||||
"model": "anthropic/claude-haiku-5.5",
|
||||
"language": "en",
|
||||
"replicate": 1,
|
||||
"chatId": "chat_mvo30cfe_57114723",
|
||||
"source": "messages.3.content.1.arguments.explanation",
|
||||
"raw": "$A-\\lambda I=\\begin{pmatrix}2-\\lambda&1\\\\1&2-\\lambda\\end{pmatrix}$, and for a $2\\times2$ matrix $\\det\\begin{pmatrix}p&q\\\\r&s\\end{pmatrix}=ps-qr$. So $$\\det(A-\\lambda I)=(2-\\lambda)^2-1\\cdot1=\\lambda^2-4\\lambda+3.$$ Its roots $\\lambda=1,3$ are the eigenvalues. Option $b$ drops the minus sign in $ps-qr$; $c$ subtracts the constant incorrectly; $d$ flips the sign of the linear term.",
|
||||
"oldDisplay": "$A-\\lambda I=\\begin{pmatrix}2-\\lambda&1\\\\1&2-\\lambda\\end{pmatrix}$, and for a $2\\times2$ matrix $\\det\\begin{pmatrix}p&q\\r&s\\end{pmatrix}=ps-qr$. So $$\\det(A-\\lambda I)=(2-\\lambda)^2-1\\cdot1=\\lambda^2-4\\lambda+3.$$ Its roots $\\lambda=1,3$ are the eigenvalues. Option $b$ drops the minus sign in $ps-qr$; $c$ subtracts the constant incorrectly; $d$ flips the sign of the linear term."
|
||||
},
|
||||
{
|
||||
"run": "multilingual-20261025-01-zh-Hans-r1",
|
||||
"model": "openai/gpt-6-luna",
|
||||
"language": "zh-Hans",
|
||||
"replicate": 1,
|
||||
"chatId": "chat_mvo60bsn_1cbbe884",
|
||||
"source": "messages.3.content.1.arguments.question",
|
||||
"raw": "对 $2\\times2$ 矩阵 $A=\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}$,以下哪组关于行列式的说法全都正确?",
|
||||
"oldDisplay": "对 $2\\times2$ 矩阵 $A=\\begin{pmatrix}a&b\\c&d\\end{pmatrix}$,以下哪组关于行列式的说法全都正确?"
|
||||
},
|
||||
{
|
||||
"run": "multilingual-20261025-01-zh-Hans-r1",
|
||||
"model": "xiaomi/mimo-v2.6-flash",
|
||||
"language": "zh-Hans",
|
||||
"replicate": 1,
|
||||
"chatId": "chat_mvo66u6w_a233fc16",
|
||||
"source": "messages.9.content.2.arguments.explanation",
|
||||
"raw": "雅可比矩阵按“第 $i$ 行 = 第 $i$ 个分量对所有自变量的偏导”排布:\n$$\nJ_{\\mathbf F}=\n\\begin{pmatrix}\n\\dfrac{\\partial (xy)}{\\partial x} & \\dfrac{\\partial (xy)}{\\partial y}\\\\[6pt]\n\\dfrac{\\partial (x+y^2)}{\\partial x} & \\dfrac{\\partial (x+y^2)}{\\partial y}\n\\end{pmatrix}\n=\n\\begin{pmatrix}\ny & x\\\\\n1 & 2y\n\\end{pmatrix}.\n$$\n注意 $\\partial(xy)/\\partial x=y$($y$ 当常数),$\\partial(xy)/\\partial y=x$。转置形式 $\\begin{pmatrix}y&1\\\\x&2y\\end{pmatrix}$ 把偏导排成了列;$\\begin{pmatrix}y&x\\\\2y&1\\end{pmatrix}$ 把第二行的两个分量次序调换了;$\\begin{pmatrix}x&y\\\\1&2y\\end{pmatrix}$ 把第一行的 $x,y$ 位置写反了。",
|
||||
"oldDisplay": "雅可比矩阵按“第 $i$ 行 = 第 $i$ 个分量对所有自变量的偏导”排布:\n$$\nJ_{\\mathbf F}=\n\\begin{pmatrix}\n\\dfrac{\\partial (xy)}{\\partial x} & \\dfrac{\\partial (xy)}{\\partial y}\\\\[6pt]\n\\dfrac{\\partial (x+y^2)}{\\partial x} & \\dfrac{\\partial (x+y^2)}{\\partial y}\n\\end{pmatrix}\n=\n\\begin{pmatrix}\ny & x\\\\\n1 & 2y\n\\end{pmatrix}.\n$$\n注意 $\\partial(xy)/\\partial x=y$($y$ 当常数),$\\partial(xy)/\\partial y=x$。转置形式 $\\begin{pmatrix}y&1\\x&2y\\end{pmatrix}$ 把偏导排成了列;$\\begin{pmatrix}y&x\\\\2y&1\\end{pmatrix}$ 把第二行的两个分量次序调换了;$\\begin{pmatrix}x&y\\\\1&2y\\end{pmatrix}$ 把第一行的 $x,y$ 位置写反了。"
|
||||
},
|
||||
{
|
||||
"run": "multilingual-20261025-01-id-r1",
|
||||
"model": "openai/gpt-6-luna",
|
||||
"language": "id",
|
||||
"replicate": 1,
|
||||
"chatId": "chat_mvo6q2lq_6af14437",
|
||||
"source": "messages.7.content.1.arguments.explanation",
|
||||
"raw": "Pada $(x,y)=(1,0)$, $H=(1,1)$. Jacobian $G$ di $(1,1)$ adalah $\\begin{pmatrix}2u&1\\\\v&u\\end{pmatrix}=\\begin{pmatrix}2&1\\\\1&1\\end{pmatrix}$, sedangkan $J_H=\\begin{pmatrix}1&1\\\\1&-1\\end{pmatrix}$. Aturan rantai dengan konvensi ini memberi $J_{G\\circ H}=J_G(H(x,y))J_H(x,y)$, yakni $\\begin{pmatrix}3&1\\\\2&0\\end{pmatrix}$.",
|
||||
"oldDisplay": "Pada $(x,y)=(1,0)$, $H=(1,1)$. Jacobian $G$ di $(1,1)$ adalah $\\begin{pmatrix}2u&1\\v&u\\end{pmatrix}=\\begin{pmatrix}2&1\\\\1&1\\end{pmatrix}$, sedangkan $J_H=\\begin{pmatrix}1&1\\\\1&-1\\end{pmatrix}$. Aturan rantai dengan konvensi ini memberi $J_{G\\circ H}=J_G(H(x,y))J_H(x,y)$, yakni $\\begin{pmatrix}3&1\\\\2&0\\end{pmatrix}$."
|
||||
},
|
||||
{
|
||||
"run": "multilingual-20261025-01-id-r1",
|
||||
"model": "z-ai/glm-5.3-flash",
|
||||
"language": "id",
|
||||
"replicate": 1,
|
||||
"chatId": "chat_mvo77f3x_135f8d66",
|
||||
"source": "messages.7.content.2.arguments.explanation",
|
||||
"raw": "Vektor eigen memenuhi $(A-\\lambda I)v=0$. Untuk $\\lambda=2$:\n$$(A-2I)v=\\begin{pmatrix}1&1\\\\0&0\\end{pmatrix}\\begin{pmatrix}v_1\\\\v_2\\end{pmatrix}=0\\;\\Rightarrow\\;v_1+v_2=0\\;\\Rightarrow\\;v=\\begin{pmatrix}1\\\\-1\\end{pmatrix}$$\n(bisa dikali skalar berapa pun). Cek: $Av=\\begin{pmatrix}3-1\\\\-2\\end{pmatrix}=\\begin{pmatrix}2\\\\-2\\end{pmatrix}=2v$. Perhatikan $v=(1,0)$ adalah vektor eigen untuk $\\lambda=3$, bukan $\\lambda=2$.",
|
||||
"oldDisplay": "Vektor eigen memenuhi $(A-\\lambda I)v=0$. Untuk $\\lambda=2$:\n$$(A-2I)v=\\begin{pmatrix}1&1\\\\0&0\\end{pmatrix}\\begin{pmatrix}v_1\\v_2\\end{pmatrix}=0\\;\\Rightarrow\\;v_1+v_2=0\\;\\Rightarrow\\;v=\\begin{pmatrix}1\\\\-1\\end{pmatrix}$$\n(bisa dikali skalar berapa pun). Cek: $Av=\\begin{pmatrix}3-1\\\\-2\\end{pmatrix}=\\begin{pmatrix}2\\\\-2\\end{pmatrix}=2v$. Perhatikan $v=(1,0)$ adalah vektor eigen untuk $\\lambda=3$, bukan $\\lambda=2$."
|
||||
},
|
||||
{
|
||||
"run": "multilingual-20261025-01-en-r2",
|
||||
"model": "openai/gpt-6-luna",
|
||||
"language": "en",
|
||||
"replicate": 2,
|
||||
"chatId": "chat_mvo7ei0l_9263330e",
|
||||
"source": "messages.3.content.1.arguments.question",
|
||||
"raw": "For a $2\\times2$ matrix $A=\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}$, which statement correctly links its determinant to area scaling and invertibility?",
|
||||
"oldDisplay": "For a $2\\times2$ matrix $A=\\begin{pmatrix}a&b\\c&d\\end{pmatrix}$, which statement correctly links its determinant to area scaling and invertibility?"
|
||||
},
|
||||
{
|
||||
"run": "multilingual-20261025-01-en-r2",
|
||||
"model": "openai/gpt-6-luna",
|
||||
"language": "en",
|
||||
"replicate": 2,
|
||||
"chatId": "chat_mvo7exl3_e1fe93cd",
|
||||
"source": "messages.1.content.1.arguments.explanation",
|
||||
"raw": "First evaluate the Jacobian of $F$ at $(1,2)$:\n$$\nJ_F(x,y)=\\begin{pmatrix}2x&1\\\\y&x\\end{pmatrix},\\qquad J_F(1,2)=\\begin{pmatrix}2&1\\\\2&1\\end{pmatrix}.\n$$\nThe velocity is $\\gamma'(1)=(1,2)$. The chain rule gives\n$$\n\\frac{d}{dt}F(\\gamma(t))\\bigg|_{t=1}=J_F(1,2)\\begin{pmatrix}1\\\\2\\end{pmatrix}=\\begin{pmatrix}4\\\\4\\end{pmatrix}.\n$$\nWait, recomputing the component derivatives directly: $F(\\gamma(t))=(t^2+(1+t^2),\\ t(1+t^2))=(1+2t^2,t+t^3)$, whose derivative at $1$ is $(4,4)$. The listed options do not include this value, so this question is invalid.",
|
||||
"oldDisplay": "First evaluate the Jacobian of $F$ at $(1,2)$:\n$$\nJ_F(x,y)=\\begin{pmatrix}2x&1\\y&x\\end{pmatrix},\\qquad J_F(1,2)=\\begin{pmatrix}2&1\\\\2&1\\end{pmatrix}.\n$$\nThe velocity is $\\gamma'(1)=(1,2)$. The chain rule gives\n$$\n\\frac{d}{dt}F(\\gamma(t))\\bigg|_{t=1}=J_F(1,2)\\begin{pmatrix}1\\\\2\\end{pmatrix}=\\begin{pmatrix}4\\\\4\\end{pmatrix}.\n$$\nWait, recomputing the component derivatives directly: $F(\\gamma(t))=(t^2+(1+t^2),\\ t(1+t^2))=(1+2t^2,t+t^3)$, whose derivative at $1$ is $(4,4)$. The listed options do not include this value, so this question is invalid."
|
||||
},
|
||||
{
|
||||
"run": "multilingual-20261025-01-en-r2",
|
||||
"model": "openai/gpt-6-luna",
|
||||
"language": "en",
|
||||
"replicate": 2,
|
||||
"chatId": "chat_mvo7exl3_e1fe93cd",
|
||||
"source": "messages.3.content.1.arguments.explanation",
|
||||
"raw": "For $F=(F_1,F_2)$, each row contains the partial derivatives of one output component: $J_F=\\begin{pmatrix}\\partial F_1/\\partial x&\\partial F_1/\\partial y\\\\\\partial F_2/\\partial x&\\partial F_2/\\partial y\\end{pmatrix}=\\begin{pmatrix}2x&1\\\\y&x\\end{pmatrix}$. Substituting $(x,y)=(1,2)$ gives $\\begin{pmatrix}2&1\\\\2&1\\end{pmatrix}$.",
|
||||
"oldDisplay": "For $F=(F_1,F_2)$, each row contains the partial derivatives of one output component: $J_F=\\begin{pmatrix}\\partial F_1/\\partial x&\\partial F_1/\\partial y\\\\\\partial F_2/\\partial x&\\partial F_2/\\partial y\\end{pmatrix}=\\begin{pmatrix}2x&1\\y&x\\end{pmatrix}$. Substituting $(x,y)=(1,2)$ gives $\\begin{pmatrix}2&1\\\\2&1\\end{pmatrix}$."
|
||||
},
|
||||
{
|
||||
"run": "multilingual-20261025-01-pt-r2",
|
||||
"model": "openai/gpt-6-luna",
|
||||
"language": "pt",
|
||||
"replicate": 2,
|
||||
"chatId": "chat_mvo7wn6c_d63ef3f1",
|
||||
"source": "messages.7.content.1.arguments.explanation",
|
||||
"raw": "Para uma matriz $2\\times2$, $\\det\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}=ad-bc$. Logo, $\\det(C)=2\\cdot3-1\\cdot5=1$. O determinante é o fator assinado de escala de área: valor absoluto $1$ preserva área, e sinal positivo indica que a orientação não é invertida. Como o determinante é diferente de zero, $C$ é invertível.",
|
||||
"oldDisplay": "Para uma matriz $2\\times2$, $\\det\\begin{pmatrix}a&b\\c&d\\end{pmatrix}=ad-bc$. Logo, $\\det(C)=2\\cdot3-1\\cdot5=1$. O determinante é o fator assinado de escala de área: valor absoluto $1$ preserva área, e sinal positivo indica que a orientação não é invertida. Como o determinante é diferente de zero, $C$ é invertível."
|
||||
},
|
||||
{
|
||||
"run": "multilingual-20261025-01-es-r2",
|
||||
"model": "openai/gpt-6-luna",
|
||||
"language": "es",
|
||||
"replicate": 2,
|
||||
"chatId": "chat_mvo8j89a_f6fc1cc0",
|
||||
"source": "messages.3.content.1.arguments.explanation",
|
||||
"raw": "La regla de la cadena es $D(F\\circ g)=DF(g(s,t))Dg(s,t)$. Aquí $g(1,2)=(3,2)$, $DF(u,v)=\\begin{pmatrix}2u&1\\\\v&u\\end{pmatrix}$ y $Dg(s,t)=\\begin{pmatrix}1&1\\\\t&s\\end{pmatrix}$. Así, $\\begin{pmatrix}6&1\\\\2&3\\end{pmatrix}\\begin{pmatrix}1&1\\\\2&1\\end{pmatrix}=\\begin{pmatrix}8&7\\\\8&5\\end{pmatrix}$.",
|
||||
"oldDisplay": "La regla de la cadena es $D(F\\circ g)=DF(g(s,t))Dg(s,t)$. Aquí $g(1,2)=(3,2)$, $DF(u,v)=\\begin{pmatrix}2u&1\\v&u\\end{pmatrix}$ y $Dg(s,t)=\\begin{pmatrix}1&1\\t&s\\end{pmatrix}$. Así, $\\begin{pmatrix}6&1\\\\2&3\\end{pmatrix}\\begin{pmatrix}1&1\\\\2&1\\end{pmatrix}=\\begin{pmatrix}8&7\\\\8&5\\end{pmatrix}$."
|
||||
},
|
||||
{
|
||||
"run": "multilingual-20261025-01-id-r2",
|
||||
"model": "openai/gpt-6-luna",
|
||||
"language": "id",
|
||||
"replicate": 2,
|
||||
"chatId": "chat_mvo9mesu_dbb56f1f",
|
||||
"source": "messages.9.content.1.arguments.explanation",
|
||||
"raw": "Untuk $\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}$, determinannya $ad-bc$. Jadi $\\det(C)=2\\cdot3-5\\cdot1=1$.",
|
||||
"oldDisplay": "Untuk $\\begin{pmatrix}a&b\\c&d\\end{pmatrix}$, determinannya $ad-bc$. Jadi $\\det(C)=2\\cdot3-5\\cdot1=1$."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"provenance": "multilingual-20261025-01; original collection findings, not the failed r1 rendering",
|
||||
"cases": [
|
||||
{
|
||||
"chatId": "chat_mvo6kxzl_540b570e",
|
||||
"source": "messages.1.content.2.arguments.explanation",
|
||||
"model": "z-ai/glm-5.3-flash",
|
||||
"raw": "乘法规则是\"左行的元素依次乘右列的元素再相加\":\\n$$\\nAB=\\begin{pmatrix}1\\\\cdot3+2\\\\cdot0 & 1\\\\cdot1+2\\\\cdot2\\\\\\\\ 0\\\\cdot3+1\\\\cdot0 & 0\\\\cdot1+1\\\\cdot2\\\\end{pmatrix}=\\begin{pmatrix}3 & 5\\\\\\\\ 0 & 2\\\\end{pmatrix}\\n$$\\n选项 b 其实是 $BA$($\\\\begin{pmatrix}3 & 7\\\\\\\\ 0 & 2\\\\end{pmatrix}$)——顺序交换后结果不同,这正是矩阵乘法不交换的例子。选项 c 是对应位置直接相加,那是加法不是乘法。",
|
||||
"oldDisplay": "乘法规则是\"左行的元素依次乘右列的元素再相加\":\\n$$\\nAB=\\begin{pmatrix}1\\cdot3+2\\cdot0 & 1\\cdot1+2\\cdot2\\\\\\\\ 0\\cdot3+1\\cdot0 & 0\\cdot1+1\\cdot2\\end{pmatrix}=\\begin{pmatrix}3 & 5\\\\\\\\ 0 & 2\\end{pmatrix}\\n$$\\n选项 b 其实是 $BA$($\\begin{pmatrix}3 & 7\\\\\\\\ 0 & 2\\end{pmatrix}$)——顺序交换后结果不同,这正是矩阵乘法不交换的例子。选项 c 是对应位置直接相加,那是加法不是乘法。"
|
||||
},
|
||||
{
|
||||
"chatId": "chat_mvo9oquq_74bfd7f6",
|
||||
"source": "messages.3.content.1.arguments.explanation",
|
||||
"model": "xiaomi/mimo-v2.6-flash",
|
||||
"raw": "Perkalian matriks $AB$ = baris-kolom: elemen $(i,j)$ hasil adalah titik-antara baris $i$ dari $A$ dengan kolom $j$ dari $B$.\\n\\n- $(1,1): (1,2)\\cdot(2,0) = 1\\cdot2 + 2\\cdot1 = 4$\\n- $(1,2): (1,2)\\cdot(0,2) = 1\\cdot0 + 2\\cdot2 = 4$\\n- $(2,1): (3,4)\\cdot(2,0) = 3\\cdot2 + 4\\cdot1 = 10$\\n- $(2,2): (3,4)\\cdot(0,2) = 3\\cdot0 + 4\\cdot2 = 8$\\n\\n$$AB = \\\\begin{pmatrix} 4 & 4 \\\\\\\\ 10 & 8 \\\\end{pmatrix}$$\\n\\nPilihan lain adalah tiga kesalahan berbeda: $\\begin{pmatrix} 2 & 4 \\\\ 7 & 10 \\\\end{pmatrix}$ adalah $BA$ (urutan dibalik), $\\begin{pmatrix} 4 & 10 \\\\ 4 & 8 \\\\end{pmatrix}$ adalah transpos hasil (peletakan elemen tertukar), dan $\\begin{pmatrix} 2 & 0 \\\\ 3 & 8 \\\\end{pmatrix}$ adalah perkalian elemen-per-elemen, bukan baris-kolom. Perhatikan juga bahwa $AB \\neq BA$ — perkalian matriks tidak komutatif.",
|
||||
"oldDisplay": "Perkalian matriks $AB$ = baris-kolom: elemen $(i,j)$ hasil adalah titik-antara baris $i$ dari $A$ dengan kolom $j$ dari $B$.\\n\\n- $(1,1): (1,2)\\cdot(2,0) = 1\\cdot2 + 2\\cdot1 = 4$\\n- $(1,2): (1,2)\\cdot(0,2) = 1\\cdot0 + 2\\cdot2 = 4$\\n- $(2,1): (3,4)\\cdot(2,0) = 3\\cdot2 + 4\\cdot1 = 10$\\n- $(2,2): (3,4)\\cdot(0,2) = 3\\cdot0 + 4\\cdot2 = 8$\\n\\n$$AB = \\begin{pmatrix} 4 & 4 \\\\\\\\ 10 & 8 \\end{pmatrix}$$\\n\\nPilihan lain adalah tiga kesalahan berbeda: $\\begin{pmatrix} 2 & 4 \\\\ 7 & 10 \\end{pmatrix}$ adalah $BA$ (urutan dibalik), $\\begin{pmatrix} 4 & 10 \\\\ 4 & 8 \\end{pmatrix}$ adalah transpos hasil (peletakan elemen tertukar), dan $\\begin{pmatrix} 2 & 0 \\\\ 3 & 8 \\end{pmatrix}$ adalah perkalian elemen-per-elemen, bukan baris-kolom. Perhatikan juga bahwa $AB \\neq BA$ — perkalian matriks tidak komutatif."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { PracticeDock } from "../components/practice-dock.tsx";
|
||||
import { QuestionDock } from "../components/question-dock.tsx";
|
||||
import { LatexGlyph } from "../components/latex-glyph.tsx";
|
||||
import { ProblemCard, ConcludeCard } from "../components/practice-cards.tsx";
|
||||
import { MessageResponse } from "../components/ai-elements/message.tsx";
|
||||
import { MessageResponse, ToolContentResponse } from "../components/ai-elements/message.tsx";
|
||||
import {
|
||||
repairMathForDisplay, healOverescapedCommands, restoreMathEscapes,
|
||||
parseQuizInput, parseAskInput, parseExplainInput, activeProblemOf, projectView,
|
||||
@@ -206,17 +206,17 @@ describe("practice display surfaces", () => {
|
||||
assert.ok(!plain.glyphs.includes("∇")); // Same unchanged assistant/user MessageResponse path.
|
||||
});
|
||||
|
||||
it("teaching quiz/ask/explain still repair the same bytes without mutating their inputs", () => {
|
||||
it("teaching shape parsing retains source bytes; presentation alone recovers them", () => {
|
||||
const quiz = { question: doubled, options: [{ label: "yes" }, { label: "no" }], correctAnswer: "yes", explanation: debris };
|
||||
const ask = { question: doubled };
|
||||
const explain = { question: doubled, correctAnswer: debris };
|
||||
const before = structuredClone({ quiz, ask, explain });
|
||||
assert.equal(parseQuizInput(quiz)?.question, clean);
|
||||
assert.equal(parseQuizInput(quiz)?.explanation, `$${repairedDebris}$`);
|
||||
assert.equal(parseAskInput(ask)?.question, clean);
|
||||
assert.equal(parseExplainInput(explain)?.question, clean);
|
||||
assert.equal(parseExplainInput(explain)?.correctAnswer, `$${repairedDebris}$`);
|
||||
assertMath(renderToStaticMarkup(<MessageResponse>{parseAskInput(ask)!.question}</MessageResponse>));
|
||||
assert.equal(parseQuizInput(quiz)?.question, doubled);
|
||||
assert.equal(parseQuizInput(quiz)?.explanation, debris);
|
||||
assert.equal(parseAskInput(ask)?.question, doubled);
|
||||
assert.equal(parseExplainInput(explain)?.question, doubled);
|
||||
assert.equal(parseExplainInput(explain)?.correctAnswer, debris);
|
||||
assertMath(renderToStaticMarkup(<ToolContentResponse>{parseAskInput(ask)!.question}</ToolContentResponse>));
|
||||
assert.deepEqual({ quiz, ask, explain }, before);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { act } from "react";
|
||||
(globalThis as { React?: typeof React }).React = React;
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { parseHTML } from "linkedom";
|
||||
import { QuestionDock } from "../components/question-dock.tsx";
|
||||
import { AnsweredCard } from "../components/answered-card.tsx";
|
||||
import { newAnswerResult, type QuestionAnswerData } from "../src/core/index.ts";
|
||||
|
||||
const dom = parseHTML("<html><body></body></html>");
|
||||
Object.assign(globalThis, { window: dom.window, document: dom.document, Element: dom.window.Element,
|
||||
HTMLElement: dom.window.HTMLElement, IS_REACT_ACT_ENVIRONMENT: true });
|
||||
|
||||
test("actual select → submit → saved result → historical Why uses source data and presentation-only recovery", async () => {
|
||||
const question = String.raw`Find $\\nabla f$ for $\begin{pmatrix}a&b\\c&d\end{pmatrix}$.`;
|
||||
const label = String.raw`$\\nabla f$`;
|
||||
const explanation = String.raw`First paragraph.\n\nThe matrix is $\begin{pmatrix}p&q\\r&s\end{pmatrix}$ and $\\nabla f$ is its gradient.`;
|
||||
const input = { question, options: [{ label }, { label: "no" }], correctAnswer: label, explanation };
|
||||
const before = structuredClone(input);
|
||||
let answer: QuestionAnswerData | undefined;
|
||||
const host = document.createElement("div"); document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
try {
|
||||
await act(async () => root.render(<QuestionDock toolName="probe" toolCallId="interactive_source" input={input} expanded
|
||||
dictationClaim={false} onExpand={() => {}} onSubmit={a => { answer = a; }} />));
|
||||
assert.equal(host.querySelectorAll("mtr").length, 2, "the live matrix retains two rows");
|
||||
const choice = [...host.querySelectorAll<HTMLButtonElement>('[role="radio"]')].find(button => button.querySelector(".katex"))!;
|
||||
assert.ok(choice);
|
||||
await act(async () => choice.click());
|
||||
assert.equal(choice.getAttribute("aria-checked"), "true");
|
||||
const submit = [...host.querySelectorAll<HTMLButtonElement>("button")].find(button => button.textContent?.includes("Submit answer"))!;
|
||||
assert.equal(submit.disabled, false);
|
||||
// linkedom does not implement the browser's default button→form
|
||||
// action; dispatch its native submit event through React instead.
|
||||
await act(async () => submit.closest("form")!.dispatchEvent(new window.Event("submit", { bubbles: true, cancelable: true })));
|
||||
assert.ok(answer);
|
||||
assert.equal(answer.questionText, question);
|
||||
assert.equal(answer.explanation, explanation);
|
||||
assert.equal(answer.answers[0]!.value, label, "display recovery must not alter the submitted identity");
|
||||
assert.equal(answer.answers[0]!.label, label);
|
||||
assert.equal(answer.correct, true);
|
||||
const persisted = JSON.parse(JSON.stringify(newAnswerResult("interactive_source", "probe", answer)));
|
||||
assert.deepEqual(persisted.details, JSON.parse(JSON.stringify(answer)));
|
||||
assert.equal(persisted.details.explanation, explanation);
|
||||
const saved = structuredClone(persisted.details);
|
||||
await act(async () => root.render(<AnsweredCard answer={persisted.details} input={input} />));
|
||||
const why = [...host.querySelectorAll<HTMLButtonElement>("button")].find(button => button.textContent === "Why")!;
|
||||
await act(async () => why.click());
|
||||
assert.equal(host.querySelectorAll("mtr").length, 4, "question and revealed explanation retain their rows");
|
||||
const paragraphs = host.querySelectorAll('[data-testid="tucked-explanation"] p');
|
||||
assert.equal(paragraphs.length, 2, "recovered paragraphs remain distinct in Why");
|
||||
assert.ok(paragraphs[0]!.parentElement!.classList.contains("[&>p:first-child]:inline"));
|
||||
assert.ok(!paragraphs[0]!.parentElement!.classList.contains("[&_p]:inline"));
|
||||
const annotations = [...host.querySelectorAll('annotation[encoding="application/x-tex"]')].map(n => n.textContent);
|
||||
assert.equal(annotations.filter(t => t === String.raw`\nabla f`).length, 3);
|
||||
assert.deepEqual(persisted.details, saved);
|
||||
assert.deepEqual(input, before);
|
||||
} finally { await act(async () => root.unmount()); host.remove(); }
|
||||
});
|
||||
|
||||
test("committed-answer reveals keep the first paragraph inline without flattening later paragraphs", async () => {
|
||||
const committed = String.raw`First paragraph.\n\nSecond paragraph with $\\nabla f$ and $\nu$.`;
|
||||
const answer: QuestionAnswerData = { kind: "explain", questionText: "Explain the gradient.", answers: [], text: "Synthetic answer", committed, dontKnow: false, answeredAt: "2026-10-26T00:00:00.000Z" };
|
||||
const input = { question: answer.questionText, correctAnswer: committed };
|
||||
const before = structuredClone(answer);
|
||||
const host = document.createElement("div"); document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
try {
|
||||
await act(async () => root.render(<AnsweredCard answer={answer} input={input} />));
|
||||
const paragraphs = host.querySelectorAll("p");
|
||||
assert.equal(paragraphs.length, 3, "question plus two committed-answer paragraphs");
|
||||
assert.ok(paragraphs[1]!.parentElement!.classList.contains("[&>p:first-child]:inline"));
|
||||
assert.ok(!paragraphs[1]!.parentElement!.classList.contains("[&_p]:inline"));
|
||||
assert.equal(host.querySelectorAll(".katex-error").length, 0);
|
||||
assert.deepEqual(answer, before);
|
||||
} finally { await act(async () => root.unmount()); host.remove(); }
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import React from "react";
|
||||
(globalThis as { React?: typeof React }).React = React;
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "node:test";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { parseHTML } from "linkedom";
|
||||
import { MessageResponse, ToolContentResponse } from "../components/ai-elements/message.tsx";
|
||||
import { QuestionDock } from "../components/question-dock.tsx";
|
||||
import { AnsweredCard } from "../components/answered-card.tsx";
|
||||
import { answerForPresentation, buildAnswer, normalizeOptions, parseAskInput, parseExplainInput, parseQuizInput,
|
||||
repairMathForDisplay, resolveCorrect, type QuestionAnswerData, type QuestionData } from "../src/core/index.ts";
|
||||
|
||||
const fixtures = JSON.parse(readFileSync(new URL("./fixtures/tool-rendering/matrix-regressions.json", import.meta.url), "utf8")) as {
|
||||
cases: Array<{ chatId: string; source: string; raw: string; oldDisplay: string }>;
|
||||
};
|
||||
const a = String.raw`$\begin{pmatrix}a&b\\c&d\end{pmatrix}$`;
|
||||
const b = String.raw`$\begin{pmatrix}p&q\\r&s\end{pmatrix}$`;
|
||||
const brokenA = a.replace(String.raw`\\c`, String.raw`\c`);
|
||||
const brokenB = b.replace(String.raw`\\r`, String.raw`\r`);
|
||||
const doubled = String.raw`$\\nabla f$`;
|
||||
const input = { question: a, options: [{ label: a, value: "a" }, { label: b, value: "b" }], correctAnswer: "a", explanation: a };
|
||||
const dom = (html: string) => parseHTML(`<html><body>${html}</body></html>`).document;
|
||||
const render = (text: string, recovered = true) => renderToStaticMarkup(
|
||||
recovered ? <ToolContentResponse mode="static">{text}</ToolContentResponse> : <MessageResponse mode="static">{text}</MessageResponse>);
|
||||
const tex = (html: string) => [...dom(html).querySelectorAll('annotation[encoding="application/x-tex"]')].map(n => n.textContent);
|
||||
|
||||
for (const fixture of fixtures.cases) test(`preserves corpus matrix rows: ${fixture.chatId} ${fixture.source}`, () => {
|
||||
assert.notEqual(fixture.raw, fixture.oldDisplay);
|
||||
assert.equal(repairMathForDisplay(fixture.raw), fixture.raw);
|
||||
const expected = dom(render(fixture.raw, false));
|
||||
const actual = dom(render(fixture.raw));
|
||||
assert.deepEqual(tex(actual.body.innerHTML), tex(expected.body.innerHTML));
|
||||
assert.equal(actual.querySelectorAll("mtr").length, expected.querySelectorAll("mtr").length);
|
||||
assert.ok(actual.querySelectorAll("mtr").length > dom(render(fixture.oldDisplay, false)).querySelectorAll("mtr").length);
|
||||
assert.equal(actual.querySelectorAll(".katex-error").length, 0);
|
||||
});
|
||||
|
||||
test("generic nested environments, comments, and bare row-leading command names are protected", () => {
|
||||
for (const environment of ["matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix", "smallmatrix", "array", "aligned", "cases"]) {
|
||||
const source = `$\\begin{${environment}}a&b\\\\beta&d\\end{${environment}}$`;
|
||||
assert.equal(repairMathForDisplay(source), source);
|
||||
}
|
||||
const nested = "$$\n" + String.raw`\begin{aligned}x&=\begin{pmatrix}a&b\\c&d\end{pmatrix}\\% \end{aligned} is a comment` + "\n" + String.raw`r&=s\end{aligned}` + "\n$$";
|
||||
assert.equal(repairMathForDisplay(nested), nested);
|
||||
const spaced = String.raw`$\begin {pmatrix}a&b\\c&d\end {pmatrix}$`;
|
||||
assert.equal(repairMathForDisplay(spaced), spaced);
|
||||
const mixed = String.raw`$\\lambda+\begin{pmatrix}a&b\\c&d\end{pmatrix}$`;
|
||||
assert.equal(repairMathForDisplay(mixed), mixed.replace(String.raw`\\lambda`, String.raw`\lambda`));
|
||||
});
|
||||
|
||||
test("corpus document framing and final-display percentages recover without new errors", () => {
|
||||
const cases = JSON.parse(readFileSync(new URL("./fixtures/tool-rendering/document-percent-regressions.json", import.meta.url), "utf8"));
|
||||
for (const fixture of cases) {
|
||||
const recovered = repairMathForDisplay(fixture.raw);
|
||||
const document = dom(render(fixture.raw));
|
||||
assert.equal(document.querySelectorAll(".katex-error").length, 0, fixture.chatId);
|
||||
assert.equal(repairMathForDisplay(recovered), recovered, fixture.chatId);
|
||||
if (fixture.kind === "percent-at-display-end") {
|
||||
assert.ok([...document.querySelectorAll(".katex-html")].some(node => node.textContent?.includes("%")), fixture.chatId);
|
||||
assert.ok(!recovered.includes("\\\\%"), fixture.chatId);
|
||||
} else {
|
||||
assert.match(recovered, /\n\$\$\n/);
|
||||
assert.match(recovered, /\n\$\$$/);
|
||||
}
|
||||
}
|
||||
});
|
||||
test("mixed-escaped environment markers retain existing recovery rather than treating an opener as native", () => {
|
||||
const mixedFixtures = JSON.parse(readFileSync(new URL("./fixtures/tool-rendering/mixed-escaping-regressions.json", import.meta.url), "utf8")) as typeof fixtures;
|
||||
assert.equal(mixedFixtures.cases.length, 2);
|
||||
for (const fixture of mixedFixtures.cases) {
|
||||
const recovered = repairMathForDisplay(fixture.raw);
|
||||
assert.equal(repairMathForDisplay(recovered), recovered, fixture.chatId);
|
||||
assert.doesNotMatch(recovered, /\\\\end\{/);
|
||||
assert.equal(dom(render(fixture.raw)).querySelectorAll(".katex-error").length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test("presentation-equivalent labels cannot make a source-invalid correctAnswer gradeable", () => {
|
||||
const source = { question: "Which?", options: [{ label: String.raw`$\nabla f$` }, { label: "no" }],
|
||||
correctAnswer: doubled, explanation: "A model-authored identity mismatch" };
|
||||
const before = structuredClone(source);
|
||||
assert.equal(repairMathForDisplay(doubled), source.options[0]!.label);
|
||||
assert.equal(parseQuizInput(source), undefined);
|
||||
assert.deepEqual(source, before);
|
||||
});
|
||||
|
||||
test("shape parsers retain source text and identities; recovery is presentation only", () => {
|
||||
const quiz = { ...input, question: doubled, explanation: "$\forall x$", options: [{ label: doubled }, { label: "no" }], correctAnswer: doubled };
|
||||
const ask = { question: doubled, options: [{ label: doubled, value: doubled, description: doubled }, { label: "no" }] };
|
||||
const explain = { question: doubled, correctAnswer: "$\forall x$" };
|
||||
const before = structuredClone({ quiz, ask, explain });
|
||||
assert.equal(parseQuizInput(quiz)!.question, doubled);
|
||||
assert.equal(parseQuizInput(quiz)!.explanation, quiz.explanation);
|
||||
assert.equal(parseQuizInput(quiz)!.correctAnswer, doubled);
|
||||
assert.deepEqual(parseAskInput(ask), ask);
|
||||
assert.deepEqual(parseExplainInput(explain), explain);
|
||||
assert.deepEqual(tex(render(doubled)), [String.raw`\nabla f`]);
|
||||
assert.deepEqual(tex(render(doubled, false)), [String.raw`\\nabla f`]);
|
||||
assert.deepEqual({ quiz, ask, explain }, before);
|
||||
});
|
||||
|
||||
test("the live dock recovers only its rendering, including question, context and option labels", () => {
|
||||
const source = { question: doubled, details: doubled, options: [{ label: doubled }, { label: "no" }] };
|
||||
const before = structuredClone(source);
|
||||
const html = renderToStaticMarkup(<QuestionDock toolName="ask_user_question" toolCallId="source_q" input={source} expanded onExpand={() => {}} onSubmit={() => {}} />);
|
||||
assert.deepEqual(tex(html), Array(3).fill(String.raw`\nabla f`));
|
||||
assert.deepEqual(source, before);
|
||||
});
|
||||
|
||||
test("answers snapshot source text, not recovered labels or identities", () => {
|
||||
const source = { ...input, question: doubled, explanation: doubled, options: [{ label: doubled }, { label: "no" }], correctAnswer: doubled };
|
||||
const parsed = parseQuizInput(source)!;
|
||||
const options = normalizeOptions(parsed.options);
|
||||
const qd: QuestionData = { id: "source", kind: "quiz", question: parsed.question, mode: "single-select", options,
|
||||
correctIndices: resolveCorrect(parsed.correctAnswer, options).indices, explanation: parsed.explanation, createdAt: "synthetic" };
|
||||
const answer = buildAnswer(qd, { values: [doubled], confidence: 70 }, "probe");
|
||||
assert.equal(answer.questionText, doubled);
|
||||
assert.equal(answer.options![0]!.label, doubled);
|
||||
assert.equal(answer.answers[0]!.value, doubled);
|
||||
assert.equal(answer.answers[0]!.label, doubled);
|
||||
assert.equal(answer.explanation, doubled);
|
||||
assert.equal(answer.correct, true);
|
||||
const before = structuredClone(answer);
|
||||
assert.deepEqual(tex(renderToStaticMarkup(<AnsweredCard answer={answer} input={source} />)), Array(2).fill(String.raw`\nabla f`));
|
||||
assert.deepEqual(answer, before);
|
||||
});
|
||||
|
||||
function historical(): QuestionAnswerData {
|
||||
return { kind: "quiz", questionText: brokenA, options: [{ index: 1, label: brokenB }, { index: 2, label: brokenA }],
|
||||
answers: [{ index: 1, label: brokenB, value: "b" }], correct: false, correctIndices: [2], dontKnow: false,
|
||||
explanation: brokenA, confidence: 70, answeredAt: "synthetic" };
|
||||
}
|
||||
test("historical source join restores matrices while retaining recorded order, answer and grade", () => {
|
||||
const answer = historical();
|
||||
const before = structuredClone({ answer, input });
|
||||
const projection = answerForPresentation(answer, input);
|
||||
assert.equal(projection.questionText, a);
|
||||
assert.equal(projection.explanation, a);
|
||||
assert.deepEqual(projection.options, [{ index: 1, label: b }, { index: 2, label: a }]);
|
||||
assert.equal(projection.answers, answer.answers);
|
||||
assert.equal(projection.correctIndices, answer.correctIndices);
|
||||
assert.equal(projection.correct, false);
|
||||
assert.equal(projection.confidence, 70);
|
||||
const html = renderToStaticMarkup(<AnsweredCard answer={answer} input={input} />);
|
||||
assert.deepEqual(tex(html), [a.slice(1,-1), b.slice(1,-1), a.slice(1,-1)]);
|
||||
assert.match(html, /incorrect/);
|
||||
assert.deepEqual({ answer, input }, before);
|
||||
});
|
||||
|
||||
test("historical ambiguous options and absent/invalid original calls retain their snapshots", () => {
|
||||
const source = { ...input, options: [...input.options, { label: String.raw`$\begin{matrix}x\\y\end{matrix}$`, value: "c" }] };
|
||||
const answer = { ...historical(), answers: [], dontKnow: true, options: [
|
||||
{ index: 1, label: brokenB }, { index: 2, label: brokenA }, { index: 3, label: String.raw`$\begin{matrix}x\y\end{matrix}$` },
|
||||
] };
|
||||
assert.deepEqual(answerForPresentation(answer, source).options, answer.options);
|
||||
assert.equal(answerForPresentation(answer, undefined), answer);
|
||||
assert.equal(answerForPresentation(answer, { question: a }), answer);
|
||||
const dismissed = { kind: "quiz", dismissed: true } as QuestionAnswerData;
|
||||
assert.equal(answerForPresentation(dismissed, input), dismissed);
|
||||
const before = structuredClone(answer);
|
||||
renderToStaticMarkup(<AnsweredCard answer={answer} />);
|
||||
assert.deepEqual(answer, before);
|
||||
});
|
||||
+30
-32
@@ -12,7 +12,7 @@ import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { quizInputProblems } from "../src/core/question-core.ts";
|
||||
import { Value } from "typebox/value";
|
||||
import { restoreMathEscapes } from "../src/core/math-delims.ts";
|
||||
import { repairMathForDisplay, restoreMathEscapes } from "../src/core/math-delims.ts";
|
||||
import {
|
||||
parseAskInput,
|
||||
parseProbeDoneInput,
|
||||
@@ -170,7 +170,7 @@ describe("LaTeX-escape restoration (the JSON-debris fix, owner ruling 2026-09-26
|
||||
// The exact live shapes from the modal-logic session: single-backslash
|
||||
// LaTeX inside JSON args — JSON escape resolution ate the command's
|
||||
// first letter (\f → FF ate 'f' of \forall, \v → VT ate 'v' of \varphi).
|
||||
it("parseQuizInput restores escape debris inside math spans (question + options + explanation)", () => {
|
||||
it("presentation restores debris from parsed source fields (question + options + explanation)", () => {
|
||||
const parsed = parseQuizInput({
|
||||
question: "What does \\(\\forall x\\, \\varphi(x)\\) mean?",
|
||||
options: [
|
||||
@@ -182,18 +182,18 @@ describe("LaTeX-escape restoration (the JSON-debris fix, owner ruling 2026-09-26
|
||||
});
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.question, "What does \\(\\forall x\\, \\varphi(x)\\) mean?");
|
||||
assert.equal(parsed.options[0]!.label, "\\(\\varphi\\) is true when every element satisfies it");
|
||||
assert.equal(parsed.options[1]!.label, "\\(\\forall\\) and \\(\\exists\\) are just shorthand");
|
||||
assert.equal(parsed.explanation, "\\(\\forall\\) quantifies over the domain.");
|
||||
assert.equal(repairMathForDisplay(parsed.options[0]!.label), "\\(\\varphi\\) is true when every element satisfies it");
|
||||
assert.equal(repairMathForDisplay(parsed.options[1]!.label), "\\(\\forall\\) and \\(\\exists\\) are just shorthand");
|
||||
assert.equal(repairMathForDisplay(parsed.explanation), "\\(\\forall\\) quantifies over the domain.");
|
||||
});
|
||||
|
||||
it("parseAskInput restores debris in ask option labels too", () => {
|
||||
it("presentation restores debris from parsed ask option labels too", () => {
|
||||
const parsed = parseAskInput({
|
||||
question: "Goal?",
|
||||
options: [{ label: "\\(\x0corall\\) rigor" }, { label: "Intuition first" }],
|
||||
});
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.options![0]!.label, "\\(\\forall\\) rigor");
|
||||
assert.equal(repairMathForDisplay(parsed.options![0]!.label), "\\(\\forall\\) rigor");
|
||||
});
|
||||
|
||||
it("restores TAB debris (the \\to case) inside spans only", () => {
|
||||
@@ -204,7 +204,7 @@ describe("LaTeX-escape restoration (the JSON-debris fix, owner ruling 2026-09-26
|
||||
explanation: "",
|
||||
});
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.question, "Is \\(p\\to q\\) valid?");
|
||||
assert.equal(repairMathForDisplay(parsed.question), "Is \\(p\\to q\\) valid?");
|
||||
});
|
||||
|
||||
it("control chars OUTSIDE math spans are left alone (real prose/pasted text)", () => {
|
||||
@@ -253,7 +253,7 @@ describe("display-math newline rule (Option A, owner ruling 2026-09-27)", () =>
|
||||
explanation: "",
|
||||
});
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.question, "Evaluate \\[\\left( a \\right)\\] carefully.");
|
||||
assert.equal(repairMathForDisplay(parsed.question), "Evaluate \\[\\left( a \\right)\\] carefully.");
|
||||
});
|
||||
|
||||
it("sloppy \\neq in display math stays broken — the accepted cost (newline kept, byte-identical to a real line break)", () => {
|
||||
@@ -276,7 +276,7 @@ describe("display-math newline rule (Option A, owner ruling 2026-09-27)", () =>
|
||||
explanation: "",
|
||||
});
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.question, "Is \\(p \\neq q\\) valid?");
|
||||
assert.equal(repairMathForDisplay(parsed.question), "Is \\(p \\neq q\\) valid?");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -289,9 +289,9 @@ describe("dollar-delimiter recognition — existing escape restoration rules", (
|
||||
explanation: "Here $p\to q$ is an implication.",
|
||||
});
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.question, "What does $\\forall x$ mean?");
|
||||
assert.equal(parsed.options[0]!.label, "$\\varphi$ holds");
|
||||
assert.equal(parsed.explanation, "Here $p\\to q$ is an implication.");
|
||||
assert.equal(repairMathForDisplay(parsed.question), "What does $\\forall x$ mean?");
|
||||
assert.equal(repairMathForDisplay(parsed.options[0]!.label), "$\\varphi$ holds");
|
||||
assert.equal(repairMathForDisplay(parsed.explanation), "Here $p\\to q$ is an implication.");
|
||||
});
|
||||
|
||||
it("keeps the same six-character inline recovery map", () => {
|
||||
@@ -320,21 +320,21 @@ describe("dollar-delimiter recognition — existing escape restoration rules", (
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves legacy replacement frequency when a span has multiple debris characters", () => {
|
||||
it("restores all debris in one pass consistently across delimiter syntaxes", () => {
|
||||
const body = "p\to q, \rho";
|
||||
const legacy = restoreMathEscapes(`\\(${body}\\)`).slice(2, -2);
|
||||
assert.equal(restoreMathEscapes(`$${body}$`), `$${legacy}$`);
|
||||
assert.equal(legacy, "p\\to q, \rho", "the existing first-match-only behavior is unchanged");
|
||||
assert.equal(legacy, "p\\to q, \\rho", "recovery converges instead of requiring a second render");
|
||||
});
|
||||
|
||||
it("shares dollar recovery with ask and free-recall fields", () => {
|
||||
const ask = parseAskInput({ question: "Goal?", options: [{ label: "$\x0corall$ rigor" }, { label: "Intuition" }] });
|
||||
assert.ok(ask);
|
||||
assert.equal(ask.options![0]!.label, "$\\forall$ rigor");
|
||||
assert.equal(repairMathForDisplay(ask.options![0]!.label), "$\\forall$ rigor");
|
||||
const explain = parseExplainInput({ question: "Explain $p\to q$", correctAnswer: "$p\neq q$ is different." });
|
||||
assert.ok(explain);
|
||||
assert.equal(explain.question, "Explain $p\\to q$");
|
||||
assert.equal(explain.correctAnswer, "$p\\neq q$ is different.");
|
||||
assert.equal(repairMathForDisplay(explain.question), "Explain $p\\to q$");
|
||||
assert.equal(repairMathForDisplay(explain.correctAnswer), "$p\\neq q$ is different.");
|
||||
});
|
||||
|
||||
it("leaves non-math debris, escaped opening dollars, and code examples untouched", () => {
|
||||
@@ -401,13 +401,9 @@ describe("parseExplainInput: the dock's display seam for explain calls", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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.
|
||||
describe("over-escaped command recovery after shape parsing", () => {
|
||||
// Keep these real authoring shapes, but recover only for presentation.
|
||||
// Parsed source is also used to build durable answers and identities.
|
||||
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, "\\\\");
|
||||
@@ -420,19 +416,21 @@ describe("over-escaped command heal at the parse seam (owner ruling 2026-10-15)"
|
||||
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)", () => {
|
||||
it("presentation 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);
|
||||
assert.equal(parsed.question, DOUBLE(CLEAN), "shape parsing retains source");
|
||||
assert.equal(repairMathForDisplay(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)", () => {
|
||||
it("presentation 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);
|
||||
assert.equal(parsed.question, DOUBLE(DOUBLE(CLEAN)), "shape parsing retains source");
|
||||
assert.equal(repairMathForDisplay(parsed.question), CLEAN);
|
||||
});
|
||||
|
||||
it("doubled option labels and explanations heal too (the inference-runtime msg-7 shape)", () => {
|
||||
@@ -448,10 +446,10 @@ describe("over-escaped command heal at the parse seam (owner ruling 2026-10-15)"
|
||||
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}$$");
|
||||
assert.equal(repairMathForDisplay(parsed.explanation), "$$c_{ij} = \\sum_{\\ell=1}^{k} a_{i\\ell}\\, b_{\\ell j}$$");
|
||||
});
|
||||
|
||||
it("parseAskInput heals doubled ask-option descriptions the same way", () => {
|
||||
it("presentation heals doubled ask-option descriptions the same way", () => {
|
||||
const parsed = parseAskInput({
|
||||
question: "Depth?",
|
||||
options: [
|
||||
@@ -460,7 +458,7 @@ describe("over-escaped command heal at the parse seam (owner ruling 2026-10-15)"
|
||||
],
|
||||
});
|
||||
assert.ok(parsed, "parses");
|
||||
assert.equal(parsed.options![0]!.description, "why $-\\nabla f$ is steepest descent");
|
||||
assert.equal(repairMathForDisplay(parsed.options![0]!.description!), "why $-\\nabla f$ is steepest descent");
|
||||
});
|
||||
|
||||
it("clean records pass through byte-identical (no over-heal)", () => {
|
||||
|
||||
Reference in New Issue
Block a user