explain refinements: no opt-out row, one committed field (owner 2026-09-27)
Two follow-up rulings on the explain tool:
1. No 'I don't know' row — not needed in free recall. If he doesn't
know, he says so in his own words; his text is the only answer
channel. Dock: text mode is rowless again (the composer IS the
surface); doctrine and the shared prompt chunk updated; the format
branch and card revert to quiz-only dontKnow.
2. explanation + correctAnswer collapse into ONE field — the separate
explanation was quiz remnant (MC needs the why outside its options;
a free-text commitment carries its own reasoning). Schema is now
{question, correctAnswer, details?}; the reveal shows the single
committed answer; no explanation snapshots anywhere on the explain
path.
1047/1047 tests, typecheck/lint/build green.
This commit is contained in:
@@ -1057,8 +1057,7 @@ describe("background-turn runner (owner ruling 2026-09-25: every turn is a serve
|
||||
it("explain tool call persists as a pending part; the free-text answer reveals the commitment and reaches the model", async () => {
|
||||
const EXPLAIN_INPUT = {
|
||||
question: "Explain why Mercury is hard to observe.",
|
||||
explanation: "It never strays far from the Sun in our sky.",
|
||||
correctAnswer: "Its small angular separation from the Sun.",
|
||||
correctAnswer: "Its small angular separation from the Sun — it never strays far from the Sun in our sky.",
|
||||
nodeId: "planet-order",
|
||||
};
|
||||
const { engine, calls } = scriptedTeacher((cs) => [
|
||||
@@ -1108,7 +1107,6 @@ it("explain tool call persists as a pending part; the free-text answer reveals t
|
||||
mode: "text",
|
||||
options: [],
|
||||
committedAnswer: EXPLAIN_INPUT.correctAnswer,
|
||||
explanation: EXPLAIN_INPUT.explanation,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const answer = buildAnswer(qd, { text: "It stays near the Sun." });
|
||||
@@ -1122,7 +1120,7 @@ it("explain tool call persists as a pending part; the free-text answer reveals t
|
||||
assert.equal((part as { state?: string }).state, "output-available");
|
||||
const out = (part as { output?: { committed?: string; explanation?: string } }).output!;
|
||||
assert.equal(out.committed, EXPLAIN_INPUT.correctAnswer, "the commitment snapshots into the answer record");
|
||||
assert.equal(out.explanation, EXPLAIN_INPUT.explanation);
|
||||
assert.equal(out.explanation, undefined, "no explanation remnant — one committed field (owner 2026-09-27)");
|
||||
|
||||
// The model's continuation saw the answer JSON (fork 1: the answer IS
|
||||
// the call's tool result) — his text AND the commitment snapshot ride it.
|
||||
@@ -1130,5 +1128,6 @@ it("explain tool call persists as a pending part; the free-text answer reveals t
|
||||
assert.match(seen, /It stays near the Sun\./);
|
||||
assert.match(seen, /committed/);
|
||||
assert.match(seen, /small angular separation/);
|
||||
assert.ok(!/"explanation"/.test(seen), "the answer record carries no explanation field");
|
||||
assert.equal(danglingQuestionOf(saved2.messages), null, "no pending question remains");
|
||||
});
|
||||
|
||||
@@ -550,11 +550,11 @@ describe("dock selection (toggleOption / toggleOther)", () => {
|
||||
// ─── the explain tool: free-recall quizzing (owner 2026-09-27) ─────────────
|
||||
|
||||
describe("explainInputBase / explainInputProblems (the commitment contract)", () => {
|
||||
it("commits the argument order: explanation (reasoning) BEFORE correctAnswer (the pick)", () => {
|
||||
// Same ruling as the quiz (2026-09-25): models emit tool args in
|
||||
// schema order — reasoning first, then the answer it supports.
|
||||
it("has ONE authoring field — no explanation remnant (owner ruling 2026-09-27)", () => {
|
||||
// The MC quiz's explanation/answer split was quiz baggage: free
|
||||
// recall commits a single correctAnswer, reasoning included.
|
||||
const keys = Object.keys(explainInputBase.properties);
|
||||
assert.ok(keys.indexOf("explanation") < keys.indexOf("correctAnswer"));
|
||||
assert.deepEqual(keys, ["question", "correctAnswer", "details"]);
|
||||
});
|
||||
|
||||
it("the teach object adds the required nodeId AFTER the base fields", () => {
|
||||
@@ -577,8 +577,7 @@ describe("buildAnswer / formatAnswerMessage: explain", () => {
|
||||
question: "Why does the Michelson-Morley null result matter?",
|
||||
mode: "text",
|
||||
options: [],
|
||||
committedAnswer: "It ruled out the luminiferous aether and set up special relativity.",
|
||||
explanation: "A stationary aether would produce a fringe shift; none was observed.",
|
||||
committedAnswer: "It ruled out the luminiferous aether — a stationary aether would produce a fringe shift, none was observed — and set up special relativity.",
|
||||
createdAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
});
|
||||
@@ -587,7 +586,7 @@ describe("buildAnswer / formatAnswerMessage: explain", () => {
|
||||
const ad = buildAnswer(explainQD(), { text: "Something about light speed being constant?" });
|
||||
assert.equal(ad.correct, undefined, "no client verdict — the model grades in its reply");
|
||||
assert.equal(ad.correctIndices, undefined);
|
||||
assert.equal(ad.committed, "It ruled out the luminiferous aether and set up special relativity.");
|
||||
assert.equal(ad.committed, "It ruled out the luminiferous aether — a stationary aether would produce a fringe shift, none was observed — and set up special relativity.");
|
||||
assert.equal(ad.text, "Something about light speed being constant?");
|
||||
});
|
||||
|
||||
@@ -608,9 +607,4 @@ describe("buildAnswer / formatAnswerMessage: explain", () => {
|
||||
assert.match(text, /Grade his answer strictly/);
|
||||
});
|
||||
|
||||
it("formats dontKnow as a gap, not a guess", () => {
|
||||
const text = formatAnswerMessage(buildAnswer(explainQD(), { dontKnow: true }));
|
||||
assert.match(text, /did NOT attempt/);
|
||||
assert.ok(!text.includes("The learner answered:"), "no phantom answer beside the gap");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -317,8 +317,7 @@ describe("prepareQuestionArguments (the string-encoded options wobble)", () => {
|
||||
describe("teachExplainToolFor: the explain variant validates nodeId like the quiz", () => {
|
||||
const validExplain = {
|
||||
question: "Explain why the null result matters.",
|
||||
explanation: "A stationary aether predicts a fringe shift.",
|
||||
correctAnswer: "It ruled out the aether.",
|
||||
correctAnswer: "It ruled out the aether — a stationary aether predicts a fringe shift, and none was seen.",
|
||||
nodeId: "arith",
|
||||
};
|
||||
|
||||
@@ -374,13 +373,11 @@ describe("the prober's explain: no nodeId field at all", () => {
|
||||
assert.ok(!("nodeId" in probeExplainTool.parameters.properties));
|
||||
const res = await boundaryParse(probeExplainTool, {
|
||||
question: "Explain what a derivative measures.",
|
||||
explanation: "It is instantaneous rate of change.",
|
||||
correctAnswer: "The instantaneous rate of change.",
|
||||
correctAnswer: "The instantaneous rate of change at a point — the limit the slopes converge to.",
|
||||
});
|
||||
assert.equal(res.success, true);
|
||||
const empty = await boundaryParse(probeExplainTool, {
|
||||
question: "q",
|
||||
explanation: "e",
|
||||
correctAnswer: " ",
|
||||
});
|
||||
assert.equal(empty.success, false);
|
||||
|
||||
@@ -467,18 +467,18 @@ describe("AnsweredCard: the explain variant", () => {
|
||||
question: "Explain why the null result matters.",
|
||||
mode: "text",
|
||||
options: [],
|
||||
committedAnswer: "It ruled out the luminiferous aether.",
|
||||
explanation: "A stationary aether predicts a fringe shift; none was seen.",
|
||||
committedAnswer: "It ruled out the luminiferous aether — a stationary aether predicts a fringe shift, and none was seen.",
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
it("reveals the committed answer + explanation with NO verdict chips", () => {
|
||||
it("reveals the single committed answer (reasoning inside it) with NO verdict chips", () => {
|
||||
const html = renderToStaticMarkup(<AnsweredCard answer={buildAnswer(explainQD(), { text: "Something about relativity?" })} />);
|
||||
assert.match(html, /Explain/, "labeled as the explain kind");
|
||||
assert.match(html, /The answer:/);
|
||||
assert.match(html, /luminiferous aether/);
|
||||
assert.match(html, /Why:/);
|
||||
assert.ok(!html.includes("incorrect") && !html.includes("correct</") , "no client grading");
|
||||
assert.match(html, /fringe shift/);
|
||||
assert.ok(!html.includes("Why:"), "no separate explanation field — one committed answer (owner 2026-09-27)");
|
||||
assert.ok(!html.includes("incorrect") && !html.includes("correct</"), "no client grading");
|
||||
assert.ok(!html.includes('data-mark="x"') && !html.includes('data-mark="check"'));
|
||||
});
|
||||
|
||||
@@ -487,9 +487,4 @@ describe("AnsweredCard: the explain variant", () => {
|
||||
assert.match(html, /Something about relativity\?/);
|
||||
});
|
||||
|
||||
it("dontKnow renders as the gap, not a wrong answer", () => {
|
||||
const html = renderToStaticMarkup(<AnsweredCard answer={buildAnswer(explainQD(), { dontKnow: true })} />);
|
||||
assert.match(html, /I don't know/);
|
||||
assert.ok(!html.includes("The learner answered:"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -273,8 +273,7 @@ describe("display-math newline rule (Option A, owner ruling 2026-09-27)", () =>
|
||||
describe("parseExplainInput: the dock's display seam for explain calls", () => {
|
||||
const good = {
|
||||
question: "Explain why Mercury is hard to observe.",
|
||||
explanation: "It never strays far from the Sun in our sky.",
|
||||
correctAnswer: "Its small angular separation from the Sun.",
|
||||
correctAnswer: "Its small angular separation from the Sun — it never strays far from the Sun in our sky.",
|
||||
nodeId: "planet-order", // the teacher's field — STRIPPED by Clean, like quiz
|
||||
details: "Think about where it appears at dusk.",
|
||||
};
|
||||
@@ -282,7 +281,7 @@ describe("parseExplainInput: the dock's display seam for explain calls", () => {
|
||||
it("parses a valid call and strips the teacher-only nodeId", () => {
|
||||
const parsed = parseExplainInput(good)!;
|
||||
assert.equal(parsed.question, "Explain why Mercury is hard to observe.");
|
||||
assert.equal(parsed.correctAnswer, "Its small angular separation from the Sun.");
|
||||
assert.equal(parsed.correctAnswer, "Its small angular separation from the Sun — it never strays far from the Sun in our sky.");
|
||||
assert.equal(parsed.details, "Think about where it appears at dusk.");
|
||||
assert.ok(!("nodeId" in parsed), "the teacher-only key is cleaned away");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user