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
+1 -1
View File
@@ -70,7 +70,7 @@ export function listSourceSegments(agent: PromptAgent): Array<{ path: string; sh
// file mechanics stay out (the doctrine contradicts them; 00-base.md
// plays the base role), but the FORMATTING guide composes for every
// agent (owner, 2026-10-04 late): LaTeX delimiters and markdown apply
// in all settings — practice must write \(...\)/\[...\] too.
// in all settings — practice shares the $...$/$$...$$ authoring contract.
const segments: Array<{ path: string; shared: boolean; file: string }> = agent === "practice" ? [] : [
{ path: "system-prompt.md", shared: true, file: PATHS.teacherPrompt },
];
@@ -7,7 +7,7 @@ Two instruments. The quiz tool (its exact name depends on the quizzing mechanic
Applies to every graded question you ask, whatever its role:
- Multiple choice carries a separate `explanation` (why the correct option is correct, written BEFORE `correctAnswer`: reason it out first, then commit). Free recall has ONE field — `correctAnswer`, the committed answer with its reasoning inside.
- **LaTeX everywhere.** Every question field renders markdown and LaTeX — the question text, every option, every explanation. Anything expressible as math goes in math: formulas, symbols, even single variables (`\(x\)`, `\(\alpha\)`). Plain-text math in a question is a craft failure; `\(...\)` and `\[...\]` are the only delimiters (never `$`).
- **LaTeX everywhere.** Every question field renders markdown and LaTeX — the question text, every option, every explanation. Anything expressible as math goes in math: formulas, symbols, even single variables (`$x$`, `$\alpha$`). Plain-text math in a question is a craft failure; use `$...$` for inline math and `$$...$$` on separate lines for display math.
- **The option craft.** The giveaway is never the distractors' wrongness, it's asymmetry: the correct option carrying its reasoning while the others are bare, or key-term bolding that flags it. Build evenness in: (1) every option is a bare claim — zero "why" in any of them; all reasoning goes in the explanation, shown only after he answers; (2) write the correct claim first, then mutate it into each distractor — what would someone holding a specific misconception claim? Same skeleton, same grain, same register; (3) each distractor is a real, BELIEVABLE error — the answer he would actually give under a specific wrong model, wrong on knowledge, never obviously wrong on sight. An option a cold reader eliminates instantly is a wasted option: his pick is diagnostic only when the wrong model is what selects it; (4) no asymmetric bolding. If you can tell the right one cold without knowing the material, regenerate — don't patch.
- **Free-recall grading.** Grade his words strictly against the committed answer; never mark a wrong answer right to be kind — a near-miss is partial understanding: name exactly what he got and what he missed. If he genuinely doesn't know, he says so in his own words — that is a genuine gap, not a wrong answer.
+1 -1
View File
@@ -2,6 +2,6 @@
Everything you write — every message, every quiz question, every quiz option and explanation — renders as markdown to him, and the full set is available: headings, lists, tables, fenced code, inline code, bold, mermaid diagrams. Use it maximally when it fits — the default is a structured document, not a wall of prose. Headings for the sections a message naturally has; lists and tables for anything enumerable or comparative; code blocks for anything literal. Structure is not decoration — it makes the content readable at a glance — and when in doubt, structure it.
- Math: anything expressible in LaTeX goes in LaTeX — formulas, symbols, even single variables — everywhere text renders, and that means EVERY field: prose, quiz questions, quiz options, explanations, problem statements, answers. Inline `\(f(x)\)`, display `\[` fenced on its own lines. NEVER `$` or `$$` — a bare `$` is literal currency here; `\(...\)` and `\[...\]` are the only delimiters that render.
- Math: anything expressible in LaTeX goes in LaTeX — formulas, symbols, even single variables — everywhere text renders, and that means EVERY field: prose, quiz questions, quiz options, explanations, problem statements, answers. Inline `$f(x)$`, display `$$` fenced on its own lines. Use `$...$` for inline math and `$$...$$` on separate lines for display math. In JSON strings (tool arguments), use `\\` for LaTeX command backslashes, `\\\\` for TeX row breaks, and `\n` for newlines.
- Key ideas: a GitHub-style `> [!IMPORTANT]` callout renders as a distinct outlined block — the one emphasis device for load-bearing ideas. Use it sparingly: an unconditional truth as it's laid down, a payoff when it lands, a definition the whole chain hinges on. Everything else stays in prose — if everything is boxed, nothing is.
- Diagrams: ```mermaid fenced blocks render as real diagrams — reach for one whenever structure, flow, or relationships are the point; a good diagram beats three paragraphs. Node labels with spaces or symbols must use quoted bracket form (`B["u = x^3"]`).
+43 -3
View File
@@ -106,12 +106,42 @@ export const askUserQuestionTool: AgentTool<typeof askInputSchema> = {
// quiz variants): duplicates stay honest by sharing SCHEMA ATOMS
// (question-core) + PROMPT CHUNKS, and each tool declares only its
// mechanical delta.
//
// ONE sanctioned exception (owner, 2026-10-12): each quiz tool's
// description ends with a COMPLETE example call — compact, phase-correct
// (probe carries no phase metadata; quiz leads with type+nodeId), doubling
// as the JSON-escaping exemplar (LaTeX backslashes double inside JSON
// strings). Placement ruling: WITH THE TOOL, beside its exact schema — a
// shared-prompt example is phase-ambiguous (the models fumbled exactly
// these metadata fields), per-agent prompt copies would drift. Serialized
// from ONE canonical body so the variants cannot disagree; craft doctrine
// stays in the prompts.
/** The canonical example body (owner-approved content, 2026-10-12): a
* compact applied question whose fields exercise the escaping hazards —
* braced commands (\frac, \cos), inline and display math, newlines —
* while doubling as an exemplar of good craft: uniform options, stable
* values, reasoning (explanation) committed before correctAnswer.
* JSON.stringify renders the wire text, so the escaping the model sees
* is by construction the correct one. Exported for the description pins. */
export const quizExampleBody = {
question: "A signal is $y=\\sin(2x)$ and $\\frac{dx}{dt}=3$. What is $\\frac{dy}{dt}$?",
options: [
{ label: "$6\\cos(2x)$", value: "a" },
{ label: "$2\\cos(2x)$", value: "b" },
{ label: "$3\\cos(2x)$", value: "c" },
],
explanation:
"The phase changes twice as fast as $x$; multiply by the sine's slope:\n$$\n\\frac{dy}{dt}=\\cos(2x)\\cdot2\\cdot3=6\\cos(2x)\n$$",
correctAnswer: "a",
};
/** The quiz mechanics every variant shares; `delta` appends the variant's
* own fields (its NAME among them — the probe-phase tools self-reference
* as probe/recall, owner 2026-10-09). All mechanics, zero doctrine. */
* as probe/recall, owner 2026-10-09). All mechanics, zero doctrine — plus
* the one sanctioned example (above). */
function quizDescription(delta: { name: "quiz" | "probe"; nodeId: boolean; types: boolean }): string {
return (
const base =
"Ask the learner a GRADED question with a known correct answer, then instantly grade and give feedback. " +
"Unlike ask_user_question (which collects preferences/decisions with no right answer), " +
delta.name +
@@ -129,7 +159,17 @@ function quizDescription(delta: { name: "quiz" | "probe"; nodeId: boolean; types
: "") +
"ONE question per turn: call a single " +
delta.name +
", then end your turn — his answer determines the next."
", then end your turn — his answer determines the next.";
// Phase-correct shape: the teach example LEADS with the required pair
// (type, nodeId — the 2026-10-09 drop-order ruling); the probe example
// carries no phase metadata at all, exactly like the probe schema.
const example = delta.nodeId ? { type: "check" as const, nodeId: "chain-rule", ...quizExampleBody } : quizExampleBody;
return (
base +
"\nExample arguments — copy the escaping exactly: inside JSON strings a LaTeX backslash is \\\\ and a newline is \\n.\n```json\n" +
JSON.stringify(example, null, 2) +
"\n```" +
(delta.nodeId ? " The example's nodeId is illustrative — copy an exact id from this session's plan_init." : "")
);
}