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
+26 -4
View File
@@ -34,6 +34,7 @@ import {
import { parseQuizInput } from "../src/core/tool-parts.ts";
import { resolveTargetId } from "../src/core/chains.ts";
import { bareQuestionTools } from "../src/agent/teacher-agent.ts";
import { probeQuizTool } from "../src/agent/tools.ts";
/** The incident's exact encoding: the value wrapped in literal quotes. */
function QUOTED(value: string): string {
@@ -126,9 +127,9 @@ describe("the colon shape — correctAnswer with copy-syntax debris", () => {
assert.equal(stripModelValueDebris('["x"]'), '["x"]'); // interior quotes are content (JSON-array strings)
});
it("coerceCorrectAnswer strips every entry (boundary check + grading seam)", () => {
assert.deepEqual(coerceCorrectAnswer([":parmenides"]), ["parmenides"]);
assert.deepEqual(coerceCorrectAnswer(QUOTED("primary-external")), ["primary-external"]);
it("coerceCorrectAnswer cleans non-matching entries (boundary check + grading seam)", () => {
assert.deepEqual(coerceCorrectAnswer([":parmenides"], ["parmenides"]), ["parmenides"]);
assert.deepEqual(coerceCorrectAnswer(QUOTED("primary-external"), ["primary-external"]), ["primary-external"]);
});
it("the incident's exact call passes the semantic boundary (quizInputProblems)", () => {
@@ -153,6 +154,27 @@ describe("the colon shape — correctAnswer with copy-syntax debris", () => {
});
});
describe("the calculus retry loop — an exact answer was incorrectly stripped", () => {
it("the real probe tool accepts a label with its sentence-final period", async () => {
const input = {
question: "Which claim?",
options: [{ label: "The claim holds." }, { label: "The claim need not hold." }],
explanation: "The premise does not imply the claim.",
correctAnswer: "The claim need not hold.",
};
const validated = validateToolArguments(probeQuizTool, {
type: "toolCall",
id: "calculus-period",
name: "probe",
arguments: probeQuizTool.prepareArguments!(input),
});
assert.equal(validated.correctAnswer, input.correctAnswer);
const result = await probeQuizTool.execute("calculus-period", validated);
assert.notEqual(result.isError, true, "an already-exact answer must not trigger a retry");
assert.equal(result.terminate, true, "the valid question is posed normally");
});
});
describe("the display mirror — raw records heal on read", () => {
it("parseQuizInput renders the incident's persisted raw record (no invisible quiz)", () => {
const raw = whiteheadQuiz(QUOTED("predict"), [":parmenides"]);
@@ -163,6 +185,6 @@ describe("the display mirror — raw records heal on read", () => {
// which strips the recorded wobble to the intended role.
assert.equal(normalizeQuizType(parsed?.type), "predict");
// The graded answer resolves to the option despite the recorded colon.
assert.deepEqual(parsed ? coerceCorrectAnswer(parsed.correctAnswer) : [], ["parmenides"]);
assert.deepEqual(parsed ? coerceCorrectAnswer(parsed.correctAnswer, parsed.options.map((o) => o.value ?? o.label)) : [], ["parmenides"]);
});
});