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:
@@ -101,7 +101,7 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{answer.text && <div className="lh-prose mt-2 whitespace-pre-wrap">“{answer.text}”</div>}
|
{answer.text && <div className="lh-prose mt-2 whitespace-pre-wrap">“{answer.text}”</div>}
|
||||||
{answer.dontKnow && (isQuiz || isExplain) && (
|
{answer.dontKnow && isQuiz && (
|
||||||
<div className="mt-2 text-sm text-muted-foreground">Answered “I don't know”.</div>
|
<div className="mt-2 text-sm text-muted-foreground">Answered “I don't know”.</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -126,23 +126,12 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* The explain reveal (owner ruling 2026-09-27): the committed answer
|
{/* The explain reveal (owner ruling 2026-09-27): the committed answer
|
||||||
and its explanation, shown on submit — no verdict, the model's next
|
— one field, reasoning included — shown on submit; no verdict,
|
||||||
reply IS the grading. */}
|
the model's next reply IS the grading. */}
|
||||||
{isExplain && (answer.committed || answer.explanation) && (
|
{isExplain && answer.committed && (
|
||||||
<div className="mt-3 rounded-lg bg-muted py-2 text-sm text-muted-foreground">
|
<div className="mt-3 rounded-lg bg-muted py-2 text-sm text-muted-foreground">
|
||||||
{answer.committed && (
|
|
||||||
<>
|
|
||||||
<span className="font-medium text-foreground">The answer: </span>
|
<span className="font-medium text-foreground">The answer: </span>
|
||||||
<MessageResponse className="inline [&_p]:inline [&_p]:m-0">{answer.committed}</MessageResponse>
|
<MessageResponse className="inline [&_p]:inline [&_p]:m-0">{answer.committed}</MessageResponse>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{answer.committed && answer.explanation && <div className="my-1" />}
|
|
||||||
{answer.explanation && (
|
|
||||||
<>
|
|
||||||
<span className="font-medium text-foreground">Why: </span>
|
|
||||||
<MessageResponse className="inline [&_p]:inline [&_p]:m-0">{answer.explanation}</MessageResponse>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -248,8 +248,9 @@ function QuestionDockForm({
|
|||||||
if (explainInput) {
|
if (explainInput) {
|
||||||
// The commitment rides the question data (never sent to the
|
// The commitment rides the question data (never sent to the
|
||||||
// client before answering) and snapshots into the answer at submit.
|
// client before answering) and snapshots into the answer at submit.
|
||||||
|
// One field, reasoning included — no separate explanation
|
||||||
|
// (owner ruling 2026-09-27).
|
||||||
q.committedAnswer = explainInput.correctAnswer;
|
q.committedAnswer = explainInput.correctAnswer;
|
||||||
q.explanation = explainInput.explanation;
|
|
||||||
}
|
}
|
||||||
return q;
|
return q;
|
||||||
}, [toolCallId, kind, input]);
|
}, [toolCallId, kind, input]);
|
||||||
@@ -285,7 +286,7 @@ function QuestionDockForm({
|
|||||||
|
|
||||||
const canSubmit =
|
const canSubmit =
|
||||||
qd.mode === "text"
|
qd.mode === "text"
|
||||||
? sel.dontKnow || text.trim().length > 0 // explain: dontKnow is a first-class answer
|
? text.trim().length > 0
|
||||||
: sel.dontKnow || (sel.otherSelected && other.trim().length > 0) || sel.selected.size > 0;
|
: sel.dontKnow || (sel.otherSelected && other.trim().length > 0) || sel.selected.size > 0;
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
@@ -324,14 +325,14 @@ function QuestionDockForm({
|
|||||||
const otherInputRef = useRef<HTMLInputElement>(null);
|
const otherInputRef = useRef<HTMLInputElement>(null);
|
||||||
const thinkingRef = useRef<HTMLTextAreaElement>(null);
|
const thinkingRef = useRef<HTMLTextAreaElement>(null);
|
||||||
// One row past the options is the trailing row: quiz → don't-know,
|
// One row past the options is the trailing row: quiz → don't-know,
|
||||||
// ask → Other (a question never has both). Explain has exactly ONE
|
// ask → Other (a question never has both). Text mode (ask/explain)
|
||||||
// row — its don't-know (there are no options to walk).
|
// has no rows — the composer IS the answer surface.
|
||||||
const optionCount = qd.mode === "text" ? 0 : qd.options.length;
|
const optionCount = qd.mode === "text" ? 0 : qd.options.length;
|
||||||
const rowCount = qd.mode === "text" ? (kind === "explain" ? 1 : 0) : optionCount + 1;
|
const rowCount = qd.mode === "text" ? 0 : optionCount + 1;
|
||||||
|
|
||||||
const kbdAct = (idx: number) => {
|
const kbdAct = (idx: number) => {
|
||||||
if (idx < optionCount) toggle(idx);
|
if (idx < optionCount) toggle(idx);
|
||||||
else if (kind === "quiz" || kind === "explain")
|
else if (kind === "quiz")
|
||||||
setSel((prev) => ({ ...prev, selected: new Set(), otherSelected: false, dontKnow: !prev.dontKnow }));
|
setSel((prev) => ({ ...prev, selected: new Set(), otherSelected: false, dontKnow: !prev.dontKnow }));
|
||||||
else {
|
else {
|
||||||
// Space on the ask's Other row (owner refinement round,
|
// Space on the ask's Other row (owner refinement round,
|
||||||
@@ -435,10 +436,7 @@ function QuestionDockForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Text-mode questions render no options list — EXCEPT explain,
|
{qd.mode !== "text" && (
|
||||||
whose single "I don't know" row is the one walkable answer
|
|
||||||
(kbd row 0). The answer itself goes in the composer below. */}
|
|
||||||
{(qd.mode !== "text" || kind === "explain") && (
|
|
||||||
<div className="flex flex-col gap-2 pb-2" role={qd.mode === "multi-select" ? "group" : "radiogroup"}>
|
<div className="flex flex-col gap-2 pb-2" role={qd.mode === "multi-select" ? "group" : "radiogroup"}>
|
||||||
{qd.options.map((opt, i) => {
|
{qd.options.map((opt, i) => {
|
||||||
const isSel = sel.selected.has(i);
|
const isSel = sel.selected.has(i);
|
||||||
@@ -484,7 +482,7 @@ function QuestionDockForm({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{(kind === "quiz" || kind === "explain") && (
|
{kind === "quiz" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ Two instruments. The quiz tool (its exact name depends on the quizzing mechanic
|
|||||||
|
|
||||||
### Quiz checks — shared doctrine
|
### Quiz checks — shared doctrine
|
||||||
|
|
||||||
- `explanation` (required) says why the correct answer is correct. It comes BEFORE `correctAnswer` in the call by design: reason it out first, then commit to the answer the reasoning supports.
|
- 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.
|
||||||
- An "I don't know" choice is always available to the learner — never author your own opt-out. A dontKnow result is a genuine gap, not a wrong answer.
|
- Option-style answers may carry a free-text `note` — read it; it says what he was thinking.
|
||||||
- Answers may carry a free-text `note` — read it; it says what he was thinking.
|
|
||||||
- ONE question per turn: ask, then end your turn — his answer determines the next.
|
- ONE question per turn: ask, then end your turn — his answer determines the next.
|
||||||
|
|
||||||
### `ask_user_question`
|
### `ask_user_question`
|
||||||
|
|||||||
+4
-5
@@ -113,12 +113,11 @@ const QUIZ_OPTION_CRAFT =
|
|||||||
* the teach variant appends the nodeId tagging paragraph. */
|
* the teach variant appends the nodeId tagging paragraph. */
|
||||||
const EXPLAIN_DOCTRINE =
|
const EXPLAIN_DOCTRINE =
|
||||||
"Ask the learner to EXPLAIN something in his own words — a graded check with NO options: he types a free-text " +
|
"Ask the learner to EXPLAIN something in his own words — a graded check with NO options: he types a free-text " +
|
||||||
"answer. Unlike ask_user_question (no right answer), explain always has one. THE COMMITMENT RULE: write " +
|
"answer. Unlike ask_user_question (no right answer), explain always has one. THE COMMITMENT RULE: write correctAnswer — the FULL " +
|
||||||
"explanation (your reasoning) first, then correctAnswer — the FULL correct answer, committed before he ever sees " +
|
"correct answer in your own words, reasoning included — committed before he ever sees the question. When his answer returns, grade it strictly against your committed answer in your reply: never " +
|
||||||
"the question. When his answer returns, grade it strictly against your committed answer in your reply: never " +
|
|
||||||
"revise the commitment after seeing his words, and never mark a wrong answer right to be kind — a near-miss is " +
|
"revise the commitment after seeing his words, and never mark a wrong answer right to be kind — a near-miss is " +
|
||||||
"partial understanding; name exactly what he got and what he missed. He sees the committed answer and the " +
|
"partial understanding; name exactly what he got and what he missed. He sees the committed answer on submit. If he genuinely doesn't know, he says so in his own words — that is a genuine " +
|
||||||
"explanation on submit. An 'I don't know' choice is always available to him — a genuine gap, not a wrong answer. " +
|
"gap, not a wrong answer. " +
|
||||||
"ONE question per turn: call a single explain, then end your turn — his answer determines the next.";
|
"ONE question per turn: call a single explain, then end your turn — his answer determines the next.";
|
||||||
|
|
||||||
export const probeQuizTool: AgentTool<typeof probeQuizInputSchema> = {
|
export const probeQuizTool: AgentTool<typeof probeQuizInputSchema> = {
|
||||||
|
|||||||
+13
-21
@@ -224,26 +224,22 @@ export const probeQuizInputSchema = quizInputBase;
|
|||||||
// correct answer BEFORE posing (the commitment rides the persisted call
|
// correct answer BEFORE posing (the commitment rides the persisted call
|
||||||
// args, is revealed to the learner on submit, and is restated in the answer
|
// args, is revealed to the learner on submit, and is restated in the answer
|
||||||
// result the model grades against; the client never grades free text).
|
// result the model grades against; the client never grades free text).
|
||||||
// Mechanics doctrine lives in the TOOL DESCRIPTIONS (owner ruling
|
// ONE committed answer, reasoning included (owner ruling 2026-09-27: the
|
||||||
// 2026-09-27: pi-native — doctrine travels with the armed tool), not in
|
// separate explanation field was quiz remnant — collapsed away).
|
||||||
// the system prompt.
|
// Mechanics doctrine lives in the TOOL DESCRIPTIONS (pi-native ruling),
|
||||||
|
// not in the system prompt.
|
||||||
|
|
||||||
export const explainInputBase = Type.Object({
|
export const explainInputBase = Type.Object({
|
||||||
question: Type.String({
|
question: Type.String({
|
||||||
description:
|
description:
|
||||||
"The single question to ask. ONE question per turn: ask, then end your turn — the answer determines the next.",
|
"The single question to ask. ONE question per turn: ask, then end your turn — the answer determines the next.",
|
||||||
}),
|
}),
|
||||||
explanation: Type.String({
|
|
||||||
description:
|
|
||||||
"REQUIRED. Explanation revealed AFTER the learner answers (shown however he did). It is written BEFORE " +
|
|
||||||
"correctAnswer by design: reason it out first, then commit to the answer the reasoning supports.",
|
|
||||||
}),
|
|
||||||
correctAnswer: Type.String({
|
correctAnswer: Type.String({
|
||||||
description:
|
description:
|
||||||
"REQUIRED. The full correct answer in your own words — COMMITTED before the learner ever sees the question. " +
|
"REQUIRED. The full correct answer in your own words, reasoning included — COMMITTED before the learner " +
|
||||||
"The commitment is immutable: when his answer returns, grade it strictly against this text — never revise the " +
|
"ever sees the question. The commitment is immutable: when his answer returns, grade it strictly against this " +
|
||||||
"commitment after seeing his words, and never mark a wrong answer right to be kind. Both this and the explanation " +
|
"text — never revise the commitment after seeing his words, and never mark a wrong answer right to be kind. " +
|
||||||
"are shown to him on submit; your next reply grades the nuance of what he wrote.",
|
"It is shown to him on submit; your next reply grades the nuance of what he wrote.",
|
||||||
}),
|
}),
|
||||||
details: Type.Optional(
|
details: Type.Optional(
|
||||||
Type.String({ description: "Optional extra context or instructions shown under the question." }),
|
Type.String({ description: "Optional extra context or instructions shown under the question." }),
|
||||||
@@ -338,6 +334,7 @@ export interface QuestionData {
|
|||||||
correctIndices?: number[];
|
correctIndices?: number[];
|
||||||
// explain only. NEVER send to the client before answering:
|
// explain only. NEVER send to the client before answering:
|
||||||
committedAnswer?: string;
|
committedAnswer?: string;
|
||||||
|
// quiz only (the MC "why"). NEVER send to the client before answering:
|
||||||
explanation?: string;
|
explanation?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -356,7 +353,7 @@ export interface QuestionAnswerData {
|
|||||||
correct?: boolean; // quiz only
|
correct?: boolean; // quiz only
|
||||||
correctIndices?: number[]; // quiz only (rendering)
|
correctIndices?: number[]; // quiz only (rendering)
|
||||||
committed?: string; // explain only (rendering): the model's committed answer
|
committed?: string; // explain only (rendering): the model's committed answer
|
||||||
explanation?: string; // quiz/explain only (rendering)
|
explanation?: string; // quiz only (rendering)
|
||||||
dismissed?: boolean;
|
dismissed?: boolean;
|
||||||
answeredAt: string;
|
answeredAt: string;
|
||||||
}
|
}
|
||||||
@@ -595,9 +592,8 @@ export function buildAnswer(qd: QuestionData, req: AnswerRequest): QuestionAnswe
|
|||||||
}
|
}
|
||||||
if (qd.kind === "explain") {
|
if (qd.kind === "explain") {
|
||||||
// No client grading — the model grades in its reply, against the
|
// No client grading — the model grades in its reply, against the
|
||||||
// commitment. Snapshots ride along for the reveal + rendering.
|
// commitment. The commitment snapshots for the reveal + rendering.
|
||||||
base.committed = qd.committedAnswer;
|
base.committed = qd.committedAnswer;
|
||||||
base.explanation = qd.explanation;
|
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
@@ -627,13 +623,9 @@ export function formatAnswerMessage(ad: QuestionAnswerData): string {
|
|||||||
}
|
}
|
||||||
lines.push(`Correct: ${correctStr}`);
|
lines.push(`Correct: ${correctStr}`);
|
||||||
} else if (ad.kind === "explain") {
|
} else if (ad.kind === "explain") {
|
||||||
if (ad.dontKnow) {
|
// No opt-out row on explain (owner ruling 2026-09-27): if he doesn't
|
||||||
lines.push(
|
// know, he says so in his own words — his text is all there is.
|
||||||
`The learner selected "I don't know" — they did NOT attempt an answer (a genuine knowledge gap, not a wrong guess).`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
lines.push(`The learner answered: ${ad.text ?? "(no answer)"}`);
|
lines.push(`The learner answered: ${ad.text ?? "(no answer)"}`);
|
||||||
}
|
|
||||||
if (ad.committed) {
|
if (ad.committed) {
|
||||||
lines.push(`Your committed correct answer (immutable — grade against it, never revise it): ${ad.committed}`);
|
lines.push(`Your committed correct answer (immutable — grade against it, never revise it): ${ad.committed}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 () => {
|
it("explain tool call persists as a pending part; the free-text answer reveals the commitment and reaches the model", async () => {
|
||||||
const EXPLAIN_INPUT = {
|
const EXPLAIN_INPUT = {
|
||||||
question: "Explain why Mercury is hard to observe.",
|
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 — it never strays far from the Sun in our sky.",
|
||||||
correctAnswer: "Its small angular separation from the Sun.",
|
|
||||||
nodeId: "planet-order",
|
nodeId: "planet-order",
|
||||||
};
|
};
|
||||||
const { engine, calls } = scriptedTeacher((cs) => [
|
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",
|
mode: "text",
|
||||||
options: [],
|
options: [],
|
||||||
committedAnswer: EXPLAIN_INPUT.correctAnswer,
|
committedAnswer: EXPLAIN_INPUT.correctAnswer,
|
||||||
explanation: EXPLAIN_INPUT.explanation,
|
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
const answer = buildAnswer(qd, { text: "It stays near the Sun." });
|
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");
|
assert.equal((part as { state?: string }).state, "output-available");
|
||||||
const out = (part as { output?: { committed?: string; explanation?: string } }).output!;
|
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.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 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.
|
// 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, /It stays near the Sun\./);
|
||||||
assert.match(seen, /committed/);
|
assert.match(seen, /committed/);
|
||||||
assert.match(seen, /small angular separation/);
|
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");
|
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) ─────────────
|
// ─── the explain tool: free-recall quizzing (owner 2026-09-27) ─────────────
|
||||||
|
|
||||||
describe("explainInputBase / explainInputProblems (the commitment contract)", () => {
|
describe("explainInputBase / explainInputProblems (the commitment contract)", () => {
|
||||||
it("commits the argument order: explanation (reasoning) BEFORE correctAnswer (the pick)", () => {
|
it("has ONE authoring field — no explanation remnant (owner ruling 2026-09-27)", () => {
|
||||||
// Same ruling as the quiz (2026-09-25): models emit tool args in
|
// The MC quiz's explanation/answer split was quiz baggage: free
|
||||||
// schema order — reasoning first, then the answer it supports.
|
// recall commits a single correctAnswer, reasoning included.
|
||||||
const keys = Object.keys(explainInputBase.properties);
|
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", () => {
|
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?",
|
question: "Why does the Michelson-Morley null result matter?",
|
||||||
mode: "text",
|
mode: "text",
|
||||||
options: [],
|
options: [],
|
||||||
committedAnswer: "It ruled out the luminiferous aether and set up special relativity.",
|
committedAnswer: "It ruled out the luminiferous aether — a stationary aether would produce a fringe shift, none was observed — and set up special relativity.",
|
||||||
explanation: "A stationary aether would produce a fringe shift; none was observed.",
|
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
@@ -587,7 +586,7 @@ describe("buildAnswer / formatAnswerMessage: explain", () => {
|
|||||||
const ad = buildAnswer(explainQD(), { text: "Something about light speed being constant?" });
|
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.correct, undefined, "no client verdict — the model grades in its reply");
|
||||||
assert.equal(ad.correctIndices, undefined);
|
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?");
|
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/);
|
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", () => {
|
describe("teachExplainToolFor: the explain variant validates nodeId like the quiz", () => {
|
||||||
const validExplain = {
|
const validExplain = {
|
||||||
question: "Explain why the null result matters.",
|
question: "Explain why the null result matters.",
|
||||||
explanation: "A stationary aether predicts a fringe shift.",
|
correctAnswer: "It ruled out the aether — a stationary aether predicts a fringe shift, and none was seen.",
|
||||||
correctAnswer: "It ruled out the aether.",
|
|
||||||
nodeId: "arith",
|
nodeId: "arith",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -374,13 +373,11 @@ describe("the prober's explain: no nodeId field at all", () => {
|
|||||||
assert.ok(!("nodeId" in probeExplainTool.parameters.properties));
|
assert.ok(!("nodeId" in probeExplainTool.parameters.properties));
|
||||||
const res = await boundaryParse(probeExplainTool, {
|
const res = await boundaryParse(probeExplainTool, {
|
||||||
question: "Explain what a derivative measures.",
|
question: "Explain what a derivative measures.",
|
||||||
explanation: "It is instantaneous rate of change.",
|
correctAnswer: "The instantaneous rate of change at a point — the limit the slopes converge to.",
|
||||||
correctAnswer: "The instantaneous rate of change.",
|
|
||||||
});
|
});
|
||||||
assert.equal(res.success, true);
|
assert.equal(res.success, true);
|
||||||
const empty = await boundaryParse(probeExplainTool, {
|
const empty = await boundaryParse(probeExplainTool, {
|
||||||
question: "q",
|
question: "q",
|
||||||
explanation: "e",
|
|
||||||
correctAnswer: " ",
|
correctAnswer: " ",
|
||||||
});
|
});
|
||||||
assert.equal(empty.success, false);
|
assert.equal(empty.success, false);
|
||||||
|
|||||||
@@ -467,17 +467,17 @@ describe("AnsweredCard: the explain variant", () => {
|
|||||||
question: "Explain why the null result matters.",
|
question: "Explain why the null result matters.",
|
||||||
mode: "text",
|
mode: "text",
|
||||||
options: [],
|
options: [],
|
||||||
committedAnswer: "It ruled out the luminiferous aether.",
|
committedAnswer: "It ruled out the luminiferous aether — a stationary aether predicts a fringe shift, and none was seen.",
|
||||||
explanation: "A stationary aether predicts a fringe shift; none was seen.",
|
|
||||||
createdAt: new Date().toISOString(),
|
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?" })} />);
|
const html = renderToStaticMarkup(<AnsweredCard answer={buildAnswer(explainQD(), { text: "Something about relativity?" })} />);
|
||||||
assert.match(html, /Explain/, "labeled as the explain kind");
|
assert.match(html, /Explain/, "labeled as the explain kind");
|
||||||
assert.match(html, /The answer:/);
|
assert.match(html, /The answer:/);
|
||||||
assert.match(html, /luminiferous aether/);
|
assert.match(html, /luminiferous aether/);
|
||||||
assert.match(html, /Why:/);
|
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("incorrect") && !html.includes("correct</"), "no client grading");
|
||||||
assert.ok(!html.includes('data-mark="x"') && !html.includes('data-mark="check"'));
|
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\?/);
|
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", () => {
|
describe("parseExplainInput: the dock's display seam for explain calls", () => {
|
||||||
const good = {
|
const good = {
|
||||||
question: "Explain why Mercury is hard to observe.",
|
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 — it never strays far from the Sun in our sky.",
|
||||||
correctAnswer: "Its small angular separation from the Sun.",
|
|
||||||
nodeId: "planet-order", // the teacher's field — STRIPPED by Clean, like quiz
|
nodeId: "planet-order", // the teacher's field — STRIPPED by Clean, like quiz
|
||||||
details: "Think about where it appears at dusk.",
|
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", () => {
|
it("parses a valid call and strips the teacher-only nodeId", () => {
|
||||||
const parsed = parseExplainInput(good)!;
|
const parsed = parseExplainInput(good)!;
|
||||||
assert.equal(parsed.question, "Explain why Mercury is hard to observe.");
|
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.equal(parsed.details, "Think about where it appears at dusk.");
|
||||||
assert.ok(!("nodeId" in parsed), "the teacher-only key is cleaned away");
|
assert.ok(!("nodeId" in parsed), "the teacher-only key is cleaned away");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user