math: dollar delimiters end-to-end (exact answers, escaping examples, context rendering)

- switch the math authoring/rendering contract from \(...\)/\[...\] to the
  standard $...$ inline / $$...$$ display: prompts (20-question-tools,
  40-formatting), singleDollarTextMath on the renderer, legacy reader kept,
  and escape-debris recovery extended to dollar spans (mirroring micromark
  delimiter-run rules)
- exact answer membership now precedes debris cleanup in
  coerceCorrectAnswer (calculus incident: an exact sentence-final-period
  label was stripped as model debris, then rejected); option keys are
  passed at the boundary/grading seams, the dock no longer pre-coerces
- a complete, phase-correct JSON-escaping example rides the probe/quiz
  tool descriptions (serialized from one canonical body; quiz leads with
  type+nodeId), plus the one-sentence JSON-escaping rule in 40-formatting
- ask option descriptions render markdown/LaTeX in the pending dock;
  settled cards keep the details line and option descriptions
  retroactively, read from the persisted call arguments (chat passes
  part.input to AnsweredCard)

1566/1566 tests on both navigator configs; typecheck, lint, production
build, standalone packaging, and Electron-runtime smoke all green.
This commit is contained in:
amosblomqvist
2026-10-13 06:24:33 +03:00
parent a395f37727
commit bf29a2070f
19 changed files with 628 additions and 93 deletions
+43 -1
View File
@@ -21,13 +21,14 @@ import {
createTeacherTools,
probeQuizTool,
probeExplainTool,
quizExampleBody,
teachExplainToolFor,
teachQuizToolFor,
type TeacherToolContext,
} from "../src/agent/tools.ts";
import { TOGGLEABLE_TOOLS } from "../src/core/pending.ts";
import { jobStore } from "../src/store/jobs.ts";
import { probeQuizInputSchema, quizInputObject } from "../src/core/question-core.ts";
import { probeQuizInputSchema, quizInputObject, quizInputProblems } from "../src/core/question-core.ts";
import { chatStore } from "../src/store/chats.ts";
import { PATHS } from "../config.ts";
@@ -208,6 +209,47 @@ describe("the prober's quiz: no nodeId field at all", () => {
assert.equal(Value.Check(probe.parameters, { ...validQuiz, nodeId: "anything" }), true, "no session check to fail");
});
});
// ─── the description example (owner, 2026-10-12) ─────────────────────────
describe("quiz tool descriptions carry the complete, phase-correct example", () => {
it("the canonical body is schema-valid and authoring-clean (never ship a broken exemplar)", () => {
assert.deepEqual(quizInputProblems(quizExampleBody), []);
assert.equal(Value.Check(probeQuizInputSchema, quizExampleBody), true);
const teachExample = { type: "check", nodeId: "chain-rule", ...quizExampleBody };
assert.equal(Value.Check(quizInputObject, teachExample), true);
});
it("probe: the serialized body rides the description with correct JSON escaping, no phase metadata", () => {
const desc = probeQuizTool.description;
assert.ok(
desc.includes("```json\n" + JSON.stringify(quizExampleBody, null, 2) + "\n```"),
"the example is the exact JSON.stringify of the canonical body",
);
// The escaping exemplar must be VISIBLE as wire text — the runtime
// string carries literal double backslashes, exactly what the model
// must type inside JSON.
assert.ok(desc.includes("A signal is $y=\\\\sin(2x)$"), "LaTeX command backslash shows doubled");
assert.ok(desc.includes("a LaTeX backslash is \\\\ and a newline is \\n"), "the one-line escaping hint");
assert.ok(!desc.includes("nodeId"), "probe example carries no phase metadata");
});
it("quiz: the required pair leads the example, with the plan-id caveat", () => {
const chatId = newChat("Teacher: description example");
const desc = teachQuizToolFor({ chatId, chatStore }).description;
const teachExample = { type: "check", nodeId: "chain-rule", ...quizExampleBody };
assert.ok(desc.includes("```json\n" + JSON.stringify(teachExample, null, 2) + "\n```"));
// The 2026-10-09 drop-order ruling: type+nodeId LEAD the object —
// fields generated first get dropped least.
assert.ok(
/```json\n\{\n {2}"type": "check",\n {2}"nodeId": "chain-rule",/.test(desc),
"type and nodeId lead the serialized example",
);
assert.ok(
desc.includes("illustrative — copy an exact id from this session's plan_init"),
"the illustrative-nodeId caveat",
);
});
});
// ─── tool availability (2026-09 settings seam) ───────────────────────────────
describe("tool toggles (core/pending.ts + factories)", () => {