ADR-109 — Each question type draws its own answer key for the debrief¶
Status¶
Accepted (2026-10-13, issue #828; its shape decided by the product owner). Amended the same day by the amendment (the program and the essay draw their key; §7 reversed for them), then by the second amendment (diagram, circuit and blocks draw theirs; every type that counts test cases now has a key).
Scope: the optional Key surface of the question-type client contract
(packages/core/src/client.ts) and its host, the correction projection and
the Results "Questions" tab (apps/web/src/results/CorrectionQuestion.tsx).
Relations: amends ADR-033 (who draws the key on the wall and on the page); depends on ADR-056 §9 (the example caption stays the host's).
Context¶
The debrief draws each question's key: an mcq's choices, a cloze's blanks in
its text, a short or math answer's expected answers, a categorize board, a
program. The web host did it with a table keyed by type id, importing the
solution types of six qt-* packages and copying pieces the types already
own (the categorize board, the math answers). diagram, circuit and blocks
still have no key on the wall, because their renderer is in their package
and out of the host's reach. A type's look should live with the type.
The host's copies of the categorize board and the math answers move into
their packages as they are, not merged with the types' own pieces, which
render differently: that duplication is deferred, not solved.
Decision¶
- An optional
Keysurface.QuestionTypeClient.Keyis aReact.lazycomponent, wired through@quiz/registry/clientlikeReview.drawsKey(type)is "the type's client has aKey"; the host's table by type id is gone. A type without one gets the host's prompt and answer groups, as before. - Staff only. It reads the solution the per-question view serves the
course's staff (
GET /evaluations/:id/results/by-question), which never goes throughtoStudent. It must never be mounted on a student surface;apps/web/src/results/keyGuard.test.tsholds one reader of.Keyin the app, two staff screens importing it, and no student directory importing anything ofresults/. - Structural props.
KeyPropsnames the student view, the solution,revealed, thedensity(wallorpage) andstats: the outcomes, the answer groups and the parameterized flag — the part ofByQuestionthe keys read, written structurally because@quiz/contractsdepends on@quiz/core. The host passes itsByQuestionasstats, so a contract change a key would not survive fails to compile there. - Slots from the host. The host draws the generic pieces and lends
them:
prompt(the statement, inside the host's frame) androws(the answer groups), andstatement(body), the frame itself — the example caption of a parameterized question (ADR-056 §9) above it and, on the wall, the explanation after it. A key places, replaces or omits them: an mcq and a categorize board keep the prompt and draw instead of the rows; a cloze draws its text throughstatementand no rows; a short and a math answer put "Expected answer" above the rows. - Renderers lent by the host, as
renderMarkdownalready is: the thin segmented bar (Bar,SegmentedBarofapps/web/src/ui/bar.tsx), the inline markdown, and a cloze'srenderText. A package never importsapps/web. The Suspense fallback is the statement itself, so nothing above the key moves when its chunk arrives. - Density split. The sizes the host and several keys share live in
CORRECTION_SCALEof@quiz/uiwith theDensitytype; those only the host draws stay in itsSCALE; those only one type draws live in itsKey.tsx. The words move with them, underqt.<type>.k.*; a cloze's "correct" and "wrong" stay the head'scorrection.outcome.*, mapped. codeandrichwait (reversed by the amendment forcode,codeimageandrich; ended by the second amendment for the three canvases). The program (template, reference, test cases) and an essay's model answer stay in the host for now: they have their own treatment on the Results tab beside the type's review, and moving them is a separate change. diagram, circuit and blocks gain aKeynext.
Consequences¶
- The host no longer imports a type's solution shape for mcq, cloze, short, math or categorize; a type's key is tested in its package.
- The first projection of a type fetches one more chunk; the statement stands meanwhile.
- A short or math key wraps the host's answer groups in one more flex column of the same gap: the same pixels, one more element.
Alternatives considered¶
- Keep the host's table and import more
qt-*internals for diagram, circuit and blocks: the duplication the issue names, growing by type. - Two surfaces, a statement and an answers piece: the host would still decide which type replaces what. One component with slots lets the type decide and keeps the host type-blind.
- Move
SegmentedBarinto@quiz/ui: it reads the app's i18n and tooltip layer; lending it keeps one bar and no new package dependency.
Amendment 2026-10-13: the program and the essay draw their key¶
Settled with the product owner on 2026-10-13 (#828, second part). It
reverses §7 for code, codeimage and rich; diagram, circuit and blocks
still wait (ended by the
second amendment).
The rest stands.
- A third slot,
cases.KeyProps.casesis the host's bars of the test cases (ByQuestion.casePassRate), besidepromptandrows, andnullwhen the type counts no case. The host keeps drawing them, so the rules of ADR-033 stay in one place: a case reads by itslabel, never itsname, and the papers it never ran on are "not run".KeyStatsdoes not carry the pass rates. A type without aKeystill gets them from the host, in the program's two columns (blocks, until it draws its own key; ended by the second amendment, §6). - One program key for
codeandcodeimage, inqt-code(ProgramKey): the reference solution, or hidden the code the class started from, with the cases beside it and the answer groups under it (a code image's accuracy buckets). It takes over what the host drew; the target image of a code image stays out of the debrief. Its words areqt.code.k.*, read by both types. - The essay's key,
rich: its model answer, once revealed, after the statement. Several paragraphs, lists and formulas need a block renderer:KeyProps.renderBlock, lent by the host asrenderMarkdownis, at each density's size. - No key draws a rubric, the grading criteria, at either density. They
are the grader's (ADR-037) and stay on the grading screen; the wall is
read by a class. The
Keydoc comment in@quiz/core/clientsays so, and the essay's key is tested for it. - The Results "Questions" tab changes for these three types. Drawing a
key, they leave the type's own review (
QuestionReviewHost): the card shows the statement, the key and the cases, no longer the empty student copy ("no answer", a score of "—") nor an essay's criteria. The wall is unchanged, markup for markup.
Amendment 2026-10-13: the three canvases draw their key¶
Settled with the product owner on 2026-10-13 (#828, last part). diagram,
circuit and blocks gain a Key, which ends §7; the rest stands.
- The reference, drawn by the type's read-only renderer: a diagram's
(
DiagramView, ADR-046), a schematic (SchematicView), a program of blocks (ADR-103). The host lends the canvas's words (canvasStrings) as it lendsstrings. - Hidden, nothing hints at the reference. A diagram shows its starter,
or a hatched block — never the counts of elements and links its review
prints beside the reference. A circuit, whose students start from an
empty box, shows a hatched block. A program of blocks shows its starter,
the warehouses the class could see and the host's bars of their passes.
A hidden warehouse's map (
solution.cases) never appears, revealed or not: the key reads the student view's visible ones only. - One frame per density (
KeyFrameof@quiz/ui): one element of a fixed height, hatched while hidden, measured for the renderer's height in pixels. The reveal changes what is inside it, never its size; on the wall it follows the screen, so a diagram is read across a room. No Expand. - A program of blocks is text on the wall, in the canvas's words
(
pseudoCode): a Blockly workspace would take the focus, and with it the arrows and the space bar the projection's stepper answers to. The page keeps the read-only workspace. Each key stays a chunk of its own; the engine and Blockly load with it, never before. - No rubric, a diagram's included, at either density; nor a diagram's text forms (ADR-046 §4). They stay on the grading screen.
- The Results "Questions" tab changes for these three types, as the first amendment's §5 did for its own: their review leaves the card, and a circuit gains a key there (its review with no answer showed none). The host's bars of test cases for a type without a key (first amendment, §1) go: every type that counts cases now draws a key and places them.