two questions per teaching unit: predict → teach → check (owner 2026-10-03)

The teaching cycle gains its motivation question. The teach quiz schema
now carries a REQUIRED type field — predict (posed before the teaching,
arrives open, the learner predicts the next step) or check (posed after,
arrives collapsed behind the reveal). The prober's quiz stays role-less.

- core: quizInputObject.type union + normalizeQuizType + the reveal law
  as pure quizNeedsReveal; QuestionData.questionType; note → thinking
  rename (legacy reads fold back); readingThinking channel on answers;
  compactAnswerForModel emits thinking / thinkingWhileReading
- tools: descriptions stripped to mechanics — QUIZ_OPTION_CRAFT and
  EXPLAIN_DOCTRINE deleted, doctrine moved to the prompt layer
  (reverses the 2026-09-27 placement, owner 2026-10-03); shared
  quizDescription/explainDescription builders differ only in deltas
- prompts: shared/20-question-tools.md rewritten (option craft,
  transcribed-speech leniency, the two thinking channels); new
  teacher/28-predictions.md (cycle, roles, skip rule, resolution law);
  20-teaching + 30-question-principles scoped to the new taxonomy
- UI: predictions render open with a Prediction eyebrow, verdict shows
  but the explanation tucks behind a toggle; the reading-thinking field
  mounts while a check waits (dictation claims it until reveal);
  answered card separates thinking vs thinking-while-reading blocks
This commit is contained in:
amosblomqvist
2026-10-04 00:53:29 +03:00
parent 571cc94053
commit 360d564a6c
19 changed files with 664 additions and 106 deletions
+80 -2
View File
@@ -23,7 +23,14 @@ import { JobsContext } from "../components/jobs-context.ts";
import { ChatActionsContext } from "../components/chat.tsx";
import type { JobRecord } from "../src/core/contracts.ts";
import { AnsweredCard } from "../components/answered-card.tsx";
import { buildAnswer, coerceCorrectAnswer, normalizeOptions, resolveCorrect, type QuestionData } from "../src/core/question-core.ts";
import {
buildAnswer,
coerceCorrectAnswer,
normalizeOptions,
resolveCorrect,
quizNeedsReveal,
type QuestionData,
} from "../src/core/question-core.ts";
const quizInput = {
question: "Which planet is closest to the sun?",
@@ -276,7 +283,7 @@ describe("render smoke tests", () => {
it("AnsweredCard promotes the learner's thinking to a first-class block", () => {
const answer = buildAnswer(quizQD(), {
values: ["Venus"],
note: "I picked Venus because it looks brightest — but now I realize brightness isn't distance.",
thinking: "I picked Venus because it looks brightest — but now I realize brightness isn't distance.",
});
const html = renderToStaticMarkup(<AnsweredCard answer={answer} />);
assert.ok(html.includes('data-testid="your-thinking"'), "the thinking block exists");
@@ -509,3 +516,74 @@ describe("AnsweredCard: the explain variant", () => {
});
});
// ─── the two roles in the UI (predict vs check, owner 2026-10-03) ─────────
// Predictions arrive OPEN before the teaching; checks arrive collapsed after
// it. The answered card tells them apart (eyebrow) and tucks a prediction's
// explanation (the teaching that follows IS the explanation) while a check's
// stays inline.
describe("predict vs check rendering", () => {
it("a predict quiz renders the 'Prediction' eyebrow in the dock", () => {
const html = renderToStaticMarkup(
<QuestionDock
{...{
toolName: "quiz" as const,
toolCallId: "call-p1",
input: { ...quizInput, type: "predict", nodeId: "planet-order" },
}}
expanded={true}
onExpand={() => {}}
onSubmit={() => {}}
/>,
);
assert.ok(html.includes("Prediction"), `eyebrow missing: ${html.slice(0, 200)}`);
});
it("a check quiz keeps the 'Quiz' eyebrow (old records unchanged)", () => {
const html = renderToStaticMarkup(
<QuestionDock
{...{
toolName: "quiz" as const,
toolCallId: "call-c1",
input: { ...quizInput, type: "check", nodeId: "planet-order" },
}}
expanded={true}
onExpand={() => {}}
onSubmit={() => {}}
/>,
);
assert.ok(html.includes("Quiz"));
});
it("a prediction answer SHOWS the verdict but TUCKS the explanation behind a toggle", () => {
const qd: QuestionData = { ...quizQD(), questionType: "predict" };
const answer = buildAnswer(qd, { values: ["Venus"] }); // wrong on purpose
const html = renderToStaticMarkup(<AnsweredCard answer={answer} />);
assert.ok(html.includes("Prediction"), "the eyebrow distinguishes the card");
assert.ok(/incorrect|wrong|&times;|✗/i.test(html), "the verdict still shows (owner: predictions are graded)");
assert.ok(!html.includes("Why:"), "the explanation is not center stage");
assert.ok(html.includes("tucked-explanation"), "it sits behind the toggle");
});
it("a check answer keeps the inline explanation", () => {
const answer = buildAnswer(quizQD(), { values: ["Venus"] });
const html = renderToStaticMarkup(<AnsweredCard answer={answer} />);
assert.ok(html.includes("Why:"), "checks explain inline (current behavior)");
assert.ok(!html.includes("tucked-explanation"));
});
it("the reading-thinking channel renders as its own block; legacy `note` still renders", () => {
const withReading = buildAnswer(quizQD(), {
values: ["Mercury"],
thinking: "answering thought",
readingThinking: "was unsure mid-paragraph",
});
const html = renderToStaticMarkup(<AnsweredCard answer={withReading} />);
assert.ok(html.includes("Your thinking while reading"), "the reading channel is labeled distinctly");
assert.ok(html.includes("answering thought"));
assert.ok(html.includes("was unsure mid-paragraph"));
const legacy = { ...buildAnswer(quizQD(), { values: ["Mercury"] }), note: "a 2026-09 answer" };
const legacyHtml = renderToStaticMarkup(<AnsweredCard answer={legacy} />);
assert.ok(legacyHtml.includes("a 2026-09 answer"), "records from before the rename still render");
});
});