Plan 74d D4 Deck Update Part 1 - SekiroKenjii/Quizzivy GitHub Wiki

Part 1 of 5 of D4 — Deck update: the fourth export and v2 (v0.11.0) · Next

D4 — Deck update: the fourth export and v2 (v0.11.0)

The phase that brings the product level with the design team's fourth export (2026-10-04, 17:31) and with the v2 export built on it (uiux/update-uiux-v2 @ 26b60795, 2026-10-10, 10:44). The fourth export rebuilds the Student take-test screen as one data-driven screen, whose layout follows the shared content of the open question's group: a passage, a cloze passage or a recording. It gives the Teacher builder's groups "Shared content", a passage with cloze gaps bound to questions or a recording with plays, pause and a transcript. It mounts the student's take-test screen inside the builder's preview, Test detail's preview and the import's preview.

v2 (Thuong, 2026-10-10: "analyse it, and implement it in the UIUX update phase planned right after this one") adds the AI assistant: seven teacher features (Generate questions, Check this test, Make a variant, Write an explanation, Suggest wrong options, Suggest a score, Explain results), "Read with AI" on the import page (the import workspace's Sources pane), a Settings section "AI assistant" with usage and preferences, and an Admin page "AI credits" with a centre pool, per-teacher allowances, requests and rules. Beside the AI it redraws four screens R4 builds (the assignment detail's stats as a progress card that filters the roster, the grading pane's heights, the join-code card, a separator in the collapsed sidebar), makes every side column sticky, and redraws screens of R5 (the Audit log's search and date range), R8 (the Attendance page and a class-detail Attendance tab) and R10 (the word list's progress card). Its inventory is under "What the v2 export changed". Thuong's four AI decisions of 2026-10-10 (D19 to D22 in 70-redesign-overview.md §1: Anthropic's API with cloud processing on, data minimisation per feature, a $200 monthly pool that is never overspent without an Admin's approval, built in D4) are folded into sections F and G below.

D4 is a phase of its own and runs after R4 (Thuong, 2026-10-04). R4 (74-r4.md) builds from the deck of record in docs/design/deck/, the third import, and none of its tasks is amended to the export. Neither export is in the repository until T-D4.1 imports v2, which contains the fourth export byte for byte in every file v2 did not touch.

What that order costs. Six R4 tasks build a drawing the export has replaced. D4 reopens each of them:

R4 task What R4 builds, from the deck of record What the export draws Reopened by
T-R4.31a The outline. A shared-context group keeps its row, restyled, with no summary line and no menu A "Shared content" menu item and a shared-content line under the group's header T-D4.2a
T-R4.34 Question groups restyled; the gap row is built from primitives, because nothing draws it The gap row: a dashed chip, a select, "Remove gap" T-D4.2b
T-R4.31b The Student preview as a 720px dialog with Previous / Next A full-window overlay: a bar, "Computer | Phone" and a frame T-D4.2c
T-R4.30 Test detail's preview as a list, changed questions outlined with "New" / "Changed" chips A frame; nothing inside it is outlined or chipped T-D4.2c
T-R4.39 The import's preview as a list with "Showing {k} of {n} questions…" The same frame, with no count line T-D4.2c
T-R4.65 "Plays" in the question media block; "Students can pause" is not built "Plays" on a group's recording too, and "Students can pause" on both T-D4.15

v2 reopens thirteen more (verdicts in "What the v2 export changed"):

R4 task What R4 builds, from the fourth export What v2 draws Reopened by
T-R4.4 A sidebar with group labels; nothing between groups when collapsed A role="separator" between groups while labels are hidden (the Admin page draws the same) T-D4.20
T-R4.25 Four stat tiles: Submitted, In progress, Average, Flagged One progress card: "{n} of {total} submitted", a two-segment bar, legend buttons with counts that filter the roster, the average T-D4.22
T-R4.28 The grading page grows with the card; the queue is capped on phones only From 768 the page fills the viewport and the queue and the card scroll inside themselves T-D4.21
T-R4.30 The version history sticky; top: 0 top: 16px, capped at calc(100vh − 88px), scrolling; "Make a variant" in the header T-D4.20, T-D4.34
T-R4.31a The outline align-self: flex-start; a title bar of Question settings, Versions, Preview, Publish The outline sticky and scrolling inside itself; "AI check" and "Generate" before Preview T-D4.20, T-D4.34
T-R4.32 New question and Share; the filter aside sticky at the top "Clean up" and "Generate" before Share; the aside at top: 16px from 1024 T-D4.34, T-D4.20
T-R4.33 The question editor's aside static The aside sticky from 900; "Write with AI" on Explanation and "Suggest wrong options" beside Add option T-D4.20, T-D4.35
T-R4.27a The wizard's summary aside sticky; top: 0 top: 16px, capped and scrolling from 800 T-D4.20
T-R4.38 The review's aside static Sticky from 1000 T-D4.20
T-R4.42 The join-code card: the full code (DG-01), link rows, the paused paragraph, QR and Download The QR as a 112px button opening a projector dialog, the code at 22px with the link under it, a "Joining paused" chip, Copy code / Copy link / Download QR in a row, two new sentences T-D4.23
T-R4.43 Five settings sections; the nav static The nav sticky from 768; a sixth section, "AI assistant" T-D4.20, T-D4.38
T-R4.36 The upload card's one accepts line A "Read with AI" toggle card and two accepts lines T-D4.39
T-R4.26 The Questions tab's item-analysis list "Explain results with AI" and the insights panel above it T-D4.37

The rework is T-D4.2a (S), T-D4.2b (XS), T-D4.2c (M) and the first bullet of T-D4.15 for the fourth export, and T-D4.20 (S), T-D4.21 (S, built on develop), T-D4.22 (M) and T-D4.23 (S) for v2; the AI entry points on R4's screens arrive with the AI tasks (section G). The builder, a high-risk area, is opened a second time and compared with the deck again at five widths in two themes. The three previews are filled twice: with R4's read-only StudentPreview, then with the engine (T-D4.16). From v0.10.0 to D4's release the teacher workspace in production shows the third import's builder and previews, and the student engine is R3's.

No fix-only release, so the id T-D4.17 is not used. The fixes merged into develop since v0.9.0 (#330, #331, #314, #321, #329, #328, #320, the units of #284, #315) reach production with v0.10.0, because work/redesign-r4 takes develop by sync. R2's contract steps (T-R3.1 to T-R3.3) run as T-R4.49 if v0.9.1 has not shipped before R4 (74-r4.md). Three things the fixes owe a release go on v0.10.0's checklist (T-R4.52), and the lead carries them to the R4 branch: release notes for the student-facing fixes (answers typed offline, the timer at 00:00, sign-out, a recording's length, the language of errors); a smoke step for #330, one answer typed in airplane mode on a phone; and the v0.9.0 items still open with the owner, closed or carried (the two sign-ins, the short test, the TEMPORARY check of docs/setup/operations.md, #194).

Deliverable: the take-test engine as the fourth export draws it: a sections rail from 900 (1180 beside shared content), a grid button and a question sheet everywhere else, a drawer for shared content below 768, a section header above every question, the recording card in the stimulus pane, a cloze answered on one screen, answer areas in the deck's rows, columns and fields. A play is spent by a start and never by a resume. A teacher can stop students pausing a recording (allowPause). In the builder, a shared-context group has its menu, its shared-content line and the Shared content dialog. The three previews are the engine in a Computer or Phone frame, with no saving, no integrity record and no play counting.

Deliverable, v2: the AI assistant behind a module port, metered in credits against a $200 monthly pool that a database constraint keeps from being overspent (section G): a teacher generates questions from a passage, checks a test, makes a variant, writes an explanation, gets wrong options, gets a score suggestion and an explanation of a class's results, and imports a scanned test; every output is a suggestion the teacher accepts or edits, and nothing the AI returns is saved as a grade, a question or a comment by itself. The teacher sees the month's usage and sets AI preferences in Settings; an Admin sets allowances, adds credits and decides requests (through the API and cmd/maintenance in D4; the Admin page is R5's T-R5.35: Q28, decided 2026-10-10). Beside it, the four R4 screens v2 redraws (section F) and every side column sticky at 16px.

Exit criteria: every engine frame matches the v2 import at 360, 768, 1024, 1280 and 1440 and at 419 / 420, 899 / 900 and 1179 / 1180, in light and dark (T-D4.12); the builder's group row, the Shared content dialog and the three previews match it at the five widths, and every difference left is a row in docs/design/gaps.md; the four redrawn R4 screens and the AI surfaces match it at the five widths; the five canaries are green and their files are as they were at v0.10.0; a draft, a flag set and an open-question id written by v0.10.0 are read unchanged; the AI operations (T-D4.31, T-D4.32) are the only operations added, permissions.golden changes in T-D4.32's pull request and nowhere else, every /teacher/ai/* operation has its isolation-suite entry, and the open list is unchanged; with ANTHROPIC_API_KEY unset every AI operation answers 501, no AI entry point is drawn, and every other criterion still passes (the key is not needed in CI); with the key set, a live smoke meters one generation, one score suggestion and one scanned page and reads them back from the ledger, and an integration test shows the pool's constraint refusing a reservation that would overspend it; every CI step of 70 §8 is green, pnpm e2e:live included; released as v0.11.0 (Q29, decided 2026-10-10).

Depends on: v0.10.0 released: tagged, and main merged back into develop. D4 uses what R4 leaves on develop: the numeric form of the viewport() test helper (T-R4.3a), useMediaUpload (T-R4.35), the content editor's document profile with "Insert gap" in its toolbar (T-R4.63), AudioPolicyPanel with "Once | Twice | Unlimited" (T-R4.65), the rebuilt builder (T-R4.31a, T-R4.31b), Test detail (T-R4.30) and ImportConfirmPage (T-R4.39).

The export is the one the three hashes under "What the export changed" name. If the design team sends a newer export before T-D4.1 runs, it is read against that inventory first and this file is corrected before a task starts.

Schema changes (T-D4.13; numbered at merge, above whatever develop holds then; none rewrites or indexes a populated table, so none is -- +goose NO TRANSACTION; two columns are nullable and two have a constant default, so the v0.10.0 binary keeps inserting during the rolling deploy without a fill trigger):

File Task
NNNNN_add_questions_audio_allow_pause.sql T-D4.13
NNNNN_add_test_version_questions_audio_allow_pause.sql T-D4.13
NNNNN_add_group_recordings_allow_pause.sql T-D4.13
NNNNN_add_test_version_group_recordings_allow_pause.sql T-D4.13
00118_create_ai_settings.sql T-D4.31
00119_create_ai_credit_periods.sql T-D4.31
00120_create_ai_teacher_credits.sql T-D4.31
00121_create_ai_reservations.sql T-D4.31
00122_create_ai_usage_ledger.sql (append-only: no UPDATE or DELETE for the app role) T-D4.31
00123_create_ai_credit_requests.sql T-D4.31
00124_add_ai_permissions.sql (two rows in app.permissions, ai.use granted to the Teacher role) T-D4.32
00125_add_word_imports_read_with_ai.sql (boolean NOT NULL DEFAULT false) T-D4.33

The AI tables are new and empty, so none is -- +goose NO TRANSACTION and the v0.10.0 binary is unaffected by them; read_with_ai has a constant default. The permission rows must be in the database before the new binary starts (core/wiring/access.go refuses a database whose app.permissions lacks a key the binary knows), which the deploy order already guarantees.

D4 contracts nothing of its own and owes no contract step to a later release. R4's contract steps stay T-R5.1 (75-r5.md), which now follows D4's release.

Branches. work/deck-d4 is cut from develop after v0.10.0 is back-merged. Every task, the import included, is feature/t-d4-<nn>-<slug> off work/deck-d4 and merges into it; develop is merged into it at least weekly and before every verification run (70 §3). So develop keeps the third import as its deck of record until D4 merges, and neither develop nor production ever holds a deck that is ahead of the engine. An engine fix made on develop during D4 is compared with the deck on develop; when a sync brings it into work/deck-d4, the engine task that is open re-checks the frame it touched.

Order of work. Two lanes, each serial, each one agent (the team's two-agent setting, 2026-10-10); the server lane stays ahead of the web lane throughout. Where the TL has a third agent, the split noted at step 6 applies, inside 70 §3's ceiling of four.

Step Server lane Web lane
1 The import (T-D4.1) with the R5 to R10 plan corrections (T-D4.19): documents, either agent T-D4.3, which changes no drawing and does not wait for the import
2 T-D4.13 allowPause, D4's first contract pull request; it unblocks T-D4.9 and T-D4.15 The engine chain: T-D4.4, T-D4.5, T-D4.6, T-D4.7. Every chain task edits Paper in TakeTestPage.tsx, so no two are open at once; T-D4.6 and T-D4.11 edit QuestionBody.tsx and run in the chain's order after T-D4.5
3 T-D4.30 the provider adapter and its configuration; T-D4.31 the credits T-D4.8, T-D4.9 (after T-D4.13), T-D4.10, T-D4.11
4 T-D4.32 the AI operations, D4's second contract pull request The builder chain: T-D4.20 (sticky columns, first, so T-D4.2a's comparison sees the scrolling outline), T-D4.2a, T-D4.2b, T-D4.14, T-D4.15 (after T-D4.13)
5 T-D4.33 "Read with AI" in the import worker The previews: T-D4.2c, then T-D4.16 once T-D4.8 (and T-D4.10, if built) are merged; then T-D4.22, T-D4.23 (T-D4.21 was built on develop, #555)
6 T-D4.40 the documents; the migration rehearsal The AI surfaces: T-D4.34, T-D4.35, T-D4.36, T-D4.37, T-D4.38, T-D4.39. With a third agent, T-D4.34 (features/ai/ alone) runs beside the other five, which touch one feature folder each
7 T-D4.12 suites, browser verification and documents (both lanes) T-D4.18 the release: work/deck-d4 → develop (--no-ff), release/<version> → main with Thuong's go

The web lane is the long one, about ten L or M tasks in series; the server lane idles after step 6 and can take T-D4.23 from the web lane if it is ahead (T-D4.21 was built on develop, #555).

Lanes.

  • Two lanes, each serial. A lane never has two pull requests open on the same file. The engine chain, the builder chain, the preview chain and the AI web tasks run one after another on the web lane; the server tasks on the server lane.
  • One contract pull request at a time. D4 has two: T-D4.13 (with the StudentSection description that T-D4.5 corrects) and T-D4.32 (the AI operations and the two permission keys; permissions.golden changes there and nowhere else). T-D4.31's /admin/ai/* operations and T-D4.33's readWithAi field ride in T-D4.32's pull request, so the contract changes twice in D4. If another release's contract lane is open on develop at the same time, the two take turns, and whoever merges second reruns make gen. Exception, recorded by T-D4.40: the contract changed a third time, in T-D4.33 (#570): createWordImport gained a documented 422 VALIDATION_FAILED with details.readWithAi, for an import that asks to be read with AI on a server with no assistant or from a caller without ai.use. It is a response only; no schema changed, and make gen was its own commit. The Tech Lead accepted it because no other contract pull request was open. D4's exit criterion that the AI operations are the only operations added still holds; where this file says the contract "changes twice", read it as twice for operations and a third time for this response.
  • Migrations are numbered at the merge into work/deck-d4 and renumbered at a develop sync that brings a higher number (70 §3).
  • Removed locale keys and make gen output are their own commits. A behavioural change never rides in a refactor: T-D4.3 and T-D4.20 hold nothing else.
  • The AI tasks never need ANTHROPIC_API_KEY to pass CI: the adapter is nil without it and every suite asserts the 501 path too. A task that calls the provider for real does so in a live smoke on the builder's machine, with the key in .env and never in a test fixture or a log.

High risk. features/take-test/, features/integrity/, features/media/, anything that touches attempts or test_versions, and the builder's autosave (AGENTS.md, "High-risk areas"). Every task that touches one runs these on its branch point and on its last commit, and puts both runs in its pull request:

  • the five canaries: publish_snapshot_test.go, audio-player.test.tsx, attempts/application/tests/events_test.go, client.refresh.test.ts, router-chunks.test.ts. None is edited by any task in this file;
  • the engine tasks: pnpm test:unit tests/units/take-test tests/units/integrity tests/units/media. The draft store's suites (draft.test.ts, draft-compat.test.ts, stranded-draft.test.ts) and integrity capture's (tests/units/integrity/**, leave-events.test.tsx) pass with no edit;
  • AudioPlayer's same-tick play: toggle() calls element.play() first, with nothing awaited or set before it. T-D4.8 and T-D4.9 keep the order, and audio-player.test.tsx passes untouched;
  • autosave's two flushes (on unmount and before publish): builder/autosave-unmount.test.tsx, builder/autosave-flush.test.tsx, tests/units/question-groups/* and builder-group-recovery.spec.ts for T-D4.2a, T-D4.2b, T-D4.2c, T-D4.14 and T-D4.15;
  • the server task: make test-api-integration, which holds the two server canaries and the two lock tests of questions and media (all four carry the integration tag, so make test-api-unit runs none of them), and the Go unit tests of questions, tests, attempts and media.

Reading the references. "S 1631" and "T 6986" are line numbers in the pretty form of the fourth export's Student and Teacher pages, the page with a newline between every two tags; the v2 inventory's "T 9696" and "A 1821" are the same form of the v2 Teacher and Admin pages. After T-D4.1 a builder makes it with perl -0pe 's/>\s*</>\n</g' "docs/design/deck/Quizzivy Student.dc.html" > student.pretty.html (and the same for the Teacher and Admin pages); the result has 1,848, 9,781 and 1,960 lines. The fourth export's Teacher references ("T n" in sections 1 to 6) were taken on its 8,675-line form: v2 inserts above most of them, so a builder finds each by the identifier beside it, or adds the offsets the v2 hunk map gives (none before T 83; +4 from T 87; +20 from T 607; +60 from T 742; +101 from T 848; +113 from T 1512; +424 from T 4609; +428 from T 5781; +633 from T 6112; +806 from T 6777; +1,106 from T 8412). Each reference also names its place by something a reformat keeps: the script function, the element's id, class or ARIA name, or the text it shows. Code paths are under web/src/ unless they start with server/, api/, migrations/ or docs/. Code and contract line numbers are those of develop at fd9c763a (v0.9.0 and the fixes after it). Two later fixes already move some: since #315's, api/openapi.yaml is one line shorter past line 492, and since #320's (49c9f0e1), TakeTestPage.tsx is up to twenty lines longer past line 45. R4 moves more, so each number is quoted beside an identifier. R4's task ids and their text are those of work/redesign-r4, where T-R4.0 corrected 74-r4.md (T-R4.1a, T-R4.2a and T-R4.3a are sub-PRs there); they reach develop with v0.10.0.

Two fields a task here may carry beyond the form of 74-r4.md. "Reopens" names the R4 task whose result the task changes. "Frozen" lists what the task leaves exactly as it is.

The gap rows. T-D4.1 opened twenty-one rows in docs/design/gaps.md on 2026-10-12: DG-182 to DG-195 for the fourth export and DG-196 to DG-202 for v2. develop held DG-181 as its highest id that day (R4 used up to it), so DG-182 was the first free id.

If Thuong allows the documents to go ahead of R4. The lead has asked whether the import and a documents-only amendment of the six R4 tasks may still happen before those tasks are built. This file is written for "no". On "yes" every build task still runs after v0.10.0, and these change:

  • The import goes to develop at once. T-D4.1 and T-D4.19 depend on nothing and their pull requests target develop. With them change T-D4.1's "Depends on" and its last bullet, the "Branches" paragraph, the decision "The import lands on work/deck-d4", section A's opening paragraph and step 1 of "Order of work". The gap rows take their ids on that day, so "The gap rows" and fact 7 go.
  • A fifteenth gap row returns, for the weeks in which the deck is ahead of the engine: an engine change is compared with the Student page at tag v0.9.0 until the engine chain ships. It returns with its bullet in AGENTS.md.
  • The six tasks in 74-r4.md are amended, with what T-D4.2a, T-D4.2b, T-D4.2c and the first bullet of T-D4.15 build, written as a first build: no bullet then names what R4 left, what a frame stands "in place of", or a key that is removed. Those three tasks and that bullet leave this file. With them go the table and the paragraph under "What that order costs"; the group's row and the previews' frame in "Deliverable" and "Exit criteria"; the verdict "reopens R4" and the "Task" entries of S-32, B-02, B-03, B-07, B-09, B-13, T-01 and T-02, and B-15's "Task" entry; the "Built by" entries of DG-186, DG-187 and DG-188; the three tasks' names in Q8, Q11 and Q26; and their entries in step 4 of "Order of work", in "Lanes", in the high-risk rules and in the release checklist.
  • T-D4.15 loses its "Reopens" line and keeps the pause switch. T-D4.14 depends on T-R4.31a and T-R4.34, and T-D4.16 on T-R4.31b, T-R4.30 and T-R4.39.
  • T-D4.12 adds no closing lines to 74-r4.md. T-R4.51 compares the group's row, the gap row and the previews' bar and frame; T-D4.12 keeps the Shared content dialog, the pause switch and the engine in the frame.
  • Outside this file: AGENTS.md's bullet on the fourth export and ordering rule 10 of 70-redesign-overview.md are reworded. Both say today that the export is imported after v0.10.0 and that no R4 task is amended.

Decisions taken in this file, worth a second look. Each is a default, with its question under "Open items".

  • A recording never locks at its play limit (Q3, DG-182). The export locks it. Spec §11.4, the contract and a canary case say the opposite, and the teacher judges; the app reports. The card is built; at the limit the button stays live.
  • "Students can pause" is one boolean, allowPause, on by default (Q4, DG-183), on a question's audio and on a group's recording. An absent field on update keeps the stored value, so an old tab cannot switch pausing back on.
  • A start spends a play; a resume spends none (Q5, DG-193). Today every press of play counts. A play in progress is remembered while the tab lives, and a place that cannot be put back costs a play.
  • A cloze stays one question per gap (Q6, DG-184). The engine shows a run of gap-bound members as one screen. Numbers, squares, flags, points, grading and the result do not move.
  • The builder keeps two levels (Q8, DG-186). The deck's group is the product's section; shared content belongs to a shared-context group inside it. Overridden 2026-10-14 by the owner, after a teacher escalation: drag in and out is built as v0.11.6, design in 74e-e1-group-move.md; the two levels stay.
  • The Shared content dialog has no writer of its own (T-D4.14). It saves through the group's one editor, so the revision, the recovery draft and the conflict message keep one owner.
  • A preview shows no time and claims no save (Q11, DG-188). A test has no time limit until it is assigned, so the deck's 45:00 has nothing to read.
  • The rail returns and R3's footer strip goes (Q12), as drawn. It reverses T-R3.9c.
  • The import lands on work/deck-d4, not on develop. The deck of record and the engine reach develop together.

Taken with v2 (2026-10-10), each under "Open items" too. Thuong confirmed Q28 to Q34 on 2026-10-10 ("I agree with the proposals"), so the eight below are decisions, not defaults; Q35 stays open for two items only (the students' sentence and the provider account's retention):

  • T-D4.1 imports v2, not the fourth export (Thuong's request). v2 holds the fourth export byte for byte in the files it did not touch, so the fourth export's inventory stands and the v2 inventory adds to it.
  • One credit is $0.001 of provider list price, so the $200 pool (D21) is 200,000 credits, the deck's AI_POOL. Credits are computed from the response's reported usage with the tier's prices from configuration, rounded up. The deck's example costs are placeholders.
  • The default allowance is 20,000 credits a month (Q30), the deck's lowest rule value: ten teachers fill the pool exactly, and the centre has fewer.
  • The pool's rule has one value: pause everyone (Q31, DG-196). The deck's "Allow 10% extra" and "billed on the next invoice" contradict D21 and the spec's non-goal on payments; "Add credits" is the approval and the product bills nobody.
  • A variant is a new draft test, never the test's draft (Q32, DG-197): a test has one draft and a variant must not overwrite unpublished edits.
  • The bank clean-up merges nothing (Q32, DG-197): "Merge into one" has no operation and crosses the publish snapshot; near-duplicates are listed, tags are applied.
  • "Let AI read student answers" stays a per-teacher switch, on by default (Q33), gating the two features that send answer text; no feature sends a name, email or id (D20).
  • Buttons stay visible and disabled when the teacher is paused or out of credits, with the reason, and are hidden only when the key is unset (Q34).
  • The Admin page waits for R5; D4 ships its operations and a maintenance command (Q28).
  • D4 is v0.11.0 (Q29): a release with a module, nine operations and twelve migrations is a minor by 70 §3's own rule.

What the export changed

Read against the deck of record (the third import) and develop at fd9c763a. Three files changed; twelve are byte-identical to MANIFEST.sha256.

File sha256 (new) Size
Quizzivy Student.dc.html 0dfd2d54b9bc874d8acd7376bb3cbd0661a8f6e701b87db992fe235dbae13575 123,460 to 173,426 bytes
Quizzivy Teacher.dc.html a4c08424f0e0b95d7989ff850f2dc03d5edb1ac2d830e2b2209064800e247cee 781,532 to 794,309 bytes
github.md bf0598f3ed26bdb73c29c1feb86dbdb0832c9bce6c2ffd3d8c3171a3d3973a62 3,547 to 3,433 bytes

The Student page has seven changed hunks, all in the take-test screen and its fixtures. The Teacher page has fifteen, all in the builder's outline group, its group dialog, the three previews and their script. The inventory is 65 rows in four tables and fifteen facts the tasks rest on.

Verdicts: rework (code that shipped in R3 changes), reopens R4 (R4 builds the deck of record's drawing and a D4 task redoes it), new (work no release has), R6 (belongs to a later release that already plans it), contradicts (the drawing goes against a decision), matches, nothing (prototype plumbing, or nothing to build). "Product today" is the code at fd9c763a; for a teacher screen it is what R4 starts from. The last column names the D4 task that builds the row, or the gap row that records why nothing is built.

Facts the tasks rest on

  1. The play-limit lock contradicts a canary, not only the spec. web/tests/units/media/audio-player.test.tsx:105-112 is the case "still plays when the hint says none are left", under the comment "Exhausted is a number, not a disabled button." That file is one of the five canaries. The lock as drawn (S 1677-1681) cannot be built without editing a canary, which AGENTS.md forbids. It needs the owner, not a default.
  2. "Shared content saved on a section turns its questions into group members" is not buildable. A member is owned by its group (questions.context_group_id, migrations/00035; "Members belong to one group in one section", api/openapi.yaml 2276-2283). A section's standalone question is a bank reference. No operation turns one into the other. Two levels stay.
  3. "Partly" is a display state only. take-test/answered.ts must keep returning a boolean with today's rule, because the server counts with the same rule in SQL (StudentAssignmentCard.liveAnsweredCount, T-R3.4, student_test.go runs answered.test.ts's cases). The deck's "one pick of two is partly" needs R6's selectCount; until then only a fill-in with some blanks typed is partly.
  4. Counting every resume as a play is a defect today, with or without the deck. features/media/components/AudioPlayer.tsx:108-110 calls onPlay on every start, and both hosts count it. With maxPlays 2, one pause and resume reads "Played 2/2 times".
  5. The rail comes and goes between questions. rail = !isMobile && w >= (stim ? 1180 : 900) (S 1631) is evaluated for the open question, so between 900 and 1179 the rail shows on a question without shared content and gives way to the grid button on one with it. It is drawn, and it is a layout change inside one paper.
  6. Three thresholds are new to the engine: 420, 900 and 1180 (and 1000 for R6's matching). At fd9c763a, web/tests/support/viewport.ts answers every min-width query alike. Engine unit tests need the numeric form T-R4.3a adds, which is on develop from v0.10.0.
  7. Gap ids were taken when T-D4.1 ran. develop ended at DG-181 on 2026-10-12, the R4 rows included, so the new rows are DG-182 to DG-202.
  8. Migration numbers are taken at merge. develop ends at 00079 today. T-R3.1 and T-R3.2 ("numbers from 00080", 73-r3.md), R4's migrations and this export's allowPause all number above it, in the order they merge.
  9. The drawing shows a third defect the designer did not mark: in the rail, "Grammar and vocabulary" wraps into its own second line and overlaps it. The export fixes it too (the title is flex:1; min-width:0; line-height:1.35 and prints the part after " · ").
  10. One open item of 76-r6.md is stale. "True / false answers already marked 0": server/internal/modules/attempts/domain/grading.go:100-110 already reads optionIds.
  11. A recording never comes without a material. The server accepts a recording only when its asset is an audio node in one of the group's materials, and refuses an audio node that has no recording (server/internal/modules/tests/domain/group_validation.go:170-177, 235, 243-246; api/openapi.yaml 2376-2381: "One explicit playback binding per audio asset used in the group's materials"). A listening group therefore reaches the student with stimuli.length >= 1 (the shape is web/tests/support/groupPreview.ts:41-64), and hasPassage is already true for it. The "recording-only group with no left pane" exists only in a unit fixture the server would refuse (tests/units/take-test/panes.test.tsx:93-100, stimuli: []). What the deck calls an audio stimulus is, in the product, a material whose content is one audio node.
  12. There is one AudioPolicy, and a group has one shape for reading and writing. It is referenced by AdminQuestion.audio (2031), StudentQuestion.audio (2141), StudentGroupRecording.policy (2175), GroupRecording.policy (2385) and QuestionInput.audio (3124); QuestionGroupBundle is both the response (StoredQuestionGroup) and the request (GroupCreateInput, GroupUpdateInput). A field cannot be "required in responses, optional in requests" without a second schema for the student shapes or a split of the bundle. And both update operations replace the whole object, so what an absent field means on update decides whether an old tab can undo a teacher's setting.
  13. A test has no time limit and no integrity policy. Both are the assignment's (AssignmentInput.durationMinutes, 3175, 3195; IntegrityPolicy, 2615-2637); previewTest returns version, questions, sections and groups only (4391-4398). The deck's preview timer (45:00) has nothing to read in any of the three hosts.
  14. Every member of a group is a full question with its own required prompt, and may carry rich prompt content, media and points (GroupQuestionInput, 2399-2413; StudentQuestion, 2112-2144). The deck's cloze has one prompt for five gaps.
  15. The deck's own English for the drawer handle is "Open the audio" (S 992, 1703), beside a tip that says "The recording is here."

1. Student, take test

ID Where Before (deck of record) After (fourth export) Product today Verdict Size Task
S-01 data model S 933-1316 markup; fixtures S 1457-1515; exam(), startExam, examVals S 1615-1719 Route take: one hard-coded reading paper (PASSAGE + 8 QS), always two panes Route exam. A paper is {title, secs:[[id,title,instructions]], groups:[{title, members, stim}], items:[[id,no,secId,type,prompt,opts,_,extra]]}; stim is {kind:'passage',title,paras}, {kind:'passage',cloze:<itemId>,title} or {kind:'audio',key,title,plays,canPause}. Layout is picked per question from the stimulus of its group (S 1630). Three fixture papers: s1 reading (8), s2 listening (two audio groups; 10), s9 "Grade 9 Mock Test 3" (31 items, four sections) take-test/pages/TakeTestPage.tsx:311-487 (Paper) reads AttemptSession.sections/groups/questions; the picker is hasPassage (panes.ts:15-17, stimuli.length > 0). A group with a recording always has a material holding its audio node (note 11), so a listening group already gets the left pane: it shows the group's title, the material's heading and the node's label (GroupContext.tsx:57-62), while the player sits in the question pane rework S T-D4.3
S-02 rail S 1006-1039; rule S 1631 not drawn (R3 removed the 256px rail in T-R3.9c) <aside aria-label="Sections">, 236px, --sidebar, right border, padding 16px 14px, gap 16px. Shown when w >= 900 on a question with no stimulus and w >= 1180 on one with a stimulus, never below 768. Per section: title (12.5px, line-height 1.35; 600 --fg for the current section, else 500 muted) and "n / m" (11.5px tabular); a 5-column grid, gap 5px, of 34px squares (radius 8, 1.5px border, 12.5px 600). Legend at the bottom: Answered, Partly, Flagged (11.5px) none: components/Navigator.tsx:29-35 deletes the old rail's stored width; tests/units/student/side-column.test.ts proves no /app route loads SideColumn rework (reverses T-R3.9c) M T-D4.4
S-03 footer S 1239-1275; flags S 1714 From 768: Previous, a wrapping strip of 34px squares, Next. Below 768: Previous (icon), "4 / 8 · 3 answered", Next The strip never renders (dotsMode: false; S 1245-1255 is dead markup). With the rail: Previous, a centred key hint (two 22px <kbd> "←" "→", then "to move between questions · A–D to choose an answer", or "to move · A–D to choose" beside a stimulus; 12.5px muted), Next. Without the rail, at every width: Previous, the grid button (flex 1, 44px, radius 10, layout-grid 16px), Next Navigator.tsx:98-102 (wide ? <Strip> : <SheetButton>), Strip 135-163 rework (reverses T-R3.9c) M T-D4.4
S-04 grid label S 1265-1270; gridLabel S 1714 "{cur} / {total} · {n} answered" `(stim && !isMobile) w < 420`: "{no} · {answered}/{total}" ("23 · 0/31"); else "Question {no} · {answered} of {total} done". In a span with ellipsis. Answers red-pen mark 2 Navigator.tsx:174-192, strings takeTest.navPosition, takeTest.navAnswered; breaks onto two lines at 320 rework
S-05 sheet S 1280-1316; geometry S 1716 Phone only: bottom sheet with "Questions", "{n} of {total} answered", one 6-column grid of 44px squares, legend Answered, Flagged Opens from the grid button at any width without the rail. Below 768: bottom sheet, radius 16px 16px 0 0. From 768: a floating card 560px wide (left/right: calc(50% - 280px)), bottom: 76px, radius 14px. Both: 1px border, max-height: 80%, padding 12px 16px 20px, gap 16px, grabber 40 by 4. No title and no total: one block per section (title, "n / m"), 6 columns, gap 5px, squares 42px high, 12.5px. Legend 11.5px with "Partly" Navigator.tsx:193-267; mounted only when !wide; squares h-11 (44px), gap-2 rework S T-D4.4
S-06 squares stIt S 1621; dot S 1640; S 1016-1019, 1294-1297 Two states; current = --accent-c border; flag dot 10px; 13px Three states: answered (primary fill), partly (--accent-soft fill, primary border), empty. Current adds box-shadow: 0 0 0 2px var(--accent-soft). Flag dot 9px with a 2px --card border. 12.5px. Names "Question 23, answered" / ", partly answered" Navigator.tsx:18-22 (DotState {answered, flagged}), toneOf 291-295, flag dot size-2.5; answered.ts:10-30 is a boolean rework S T-D4.4
S-07 phone drawer keyframes S 26-27; S 986-1005, 1041-1053; script S 1686-1710 "Passage | Question n" switcher (two 34px tabs); the passage replaces the question hasPaneTabs: false (S 1710). The stimulus is a drawer from the left: W = min(round(w * 0.88), 420), z 60, transform .32s cubic-bezier(.22,.8,.24,1), --shadow-lg; backdrop rgba(15,20,22, 0 to 0.45). Closed: a handle at left: 0; top: 42%, 30 by 84px, radius 0 12px 12px 0, icon file-text or audio-lines, a 7px --accent-c dot while the recording plays, chevrons-right; "Open the passage" / "Open the audio"; title "Swipe right or tap to open". Until first opened per group: qz-nudge 1.4s ease-in-out .4s 3 (7px) and a tip (primary, 12.5px, max 220px): "The passage is here. Swipe right or tap to read it." / "The recording is here. Swipe right or tap to open it.". Open: sticky bar "Swipe left to go back to the question" and a 34px close button. Swipe starts after 10px when ` dx > 1.2 dy ; opens past 25% of W; closes past 30%; touch-action: pan-y; a click within 60ms of a drag is swallowed. A gap tapped in the drawer closes it and opens that gap. The pane has an explicit background: var(--bg)and isaria-hidden` while the drawer is closed (S 1041, 1700), which leaves its gap buttons in the Tab order
S-08 pane surface S 1101-1103; qBg, colMax S 1709 Always --sidebar; column 600px qBg = stim && !isMobile ? --sidebar : --bg; colMax = stim ? 600px : 720px TakeTestPage.tsx:427 (bg-sidebar), 442 (max-w-150), unconditional rework XS T-D4.5
S-09 section header S 1104-1107; S 1699 not drawn On every question: the section's title (12px, 600, muted, uppercase, letter-spacing .04em) over its instructions (13.5px muted), gap 2px, 12px padding and a 1px border below TakeTestPage.tsx:388, 444-448 (part eyebrow only on a multi-part paper with no passage, .02em); SectionInstructions.tsx on a section's first question only (opensSection) rework S T-D4.5
S-10 question line S 1109; typeLabel S 1655; qWord S 1699 "Question {n} of {total} · {type}" with "Choose one", "True, false or not given", "Choose two", "Short answer" "{Question|Questions} {no} of {total} · {type}"; no may be a range ("1–5"); total counts a range as that many. Types are the builder's names: "Single choice", "Multiple choice", "True / False", "True / False / Not given", "Cloze · {n} gaps", "Fill in the blank", "Matching", "Short answer" questionType.ts:20-54; en takeTest.type.* = "Choose one", "Choose one or more", "True or false", "Fill in the blanks", "Short answer"; the number is index + 1 rework XS T-D4.5; the range and "Cloze · {n} gaps", T-D4.10
S-11 stimulus head S 1054-1056, 1063 Literal "READING PASSAGE 1" (letter-spacing .02em) over the passage title Kicker = the group's title, uppercase, 12.5px 600, letter-spacing .04em; h2 (22px) = the stimulus's own title; an audio stimulus has the kicker and no h2 PassageBody.tsx:37-44 (.02em); GroupContext.tsx:57 passes eyebrow = section title, title = group title; GroupMaterials omits a material named like its group rework XS T-D4.5
S-12 cloze passage S 1062-1073; script S 1675 not drawn (DG-80: gaps that lead to their question, from primitives) h2, then a card (padding 18px 20px, 1px border, radius 12, --card, 16px or 18px larger, line-height 2.3). A gap is an inline button: min-width 76px, height 30px, radius 7, 1.5px border, a 20px primary circle with the gap number, then the chosen word. Empty: --muted, --ring border. Filled: --accent-soft, --border. Active: --card, primary border, 0 0 0 3px --accent-soft. The card stays, filled in, beside the group's other questions GroupContext.tsx:63-77 draws a gap as .content-gap (content.css:75-80, a 1px underline) with the gap's label only; never the chosen answer, no active state rework M T-D4.10
S-13 cloze stop S 1145-1170; gapStrip S 1685; gapOpts S 1664 not drawn One navigator stop, "Question 23 of 31 · Cloze · 5 gaps". Prompt 17px; "The gaps are in the passage. Pick a gap here or in the passage." (12.5px); a strip of gap chips (32px, radius 8, 20px circle, the word or "—"; active = primary fill). A card (--sidebar, 1px border, radius 12, padding 14): "Gap {n}" (13.5px 600) and "{k} of {m} gaps filled"; options in repeat(auto-fill, minmax(132px, 1fr)), gap 8, rows 46px with the round letter marker. Choosing moves to the next unfilled gap. Answers red-pen mark 1 Each gap-bound member is its own single_choice question with its own square, drawn by Choice (QuestionBody.tsx:83-163); TakeTestPage.tsx:377-386 jumpToGap new (presentation over data that exists) L T-D4.10
S-14 recording card S 1074-1097; script S 1677-1682 not drawn (DG-80: shared recordings in the question pane) An audio stimulus fills the left pane (the drawer on a phone): the kicker, no h2 (S 1054-1056 puts the h2 inside isPassage), then the card. Card: padding 18, radius 14, 1px border, --card, --shadow. A 52px round primary button (22px icon: play, pause, rotate-ccw, lock), a 6px track on --muted filled --accent-c, under it "{m:ss} / {m:ss}" and "Play {n} of {N}" (12.5px tabular), then a line with a 15px headphones icon (13px muted): "Press play when you are ready.", "Playing", "Paused", "Finished · {k} play left", "You have used all {N} plays". Button names: Play, Pause, Resume, Play again, No plays left. No seek, no title, no transcript The player is in the question pane: GroupContext.tsx:90-199 (GroupListening, SharedAudio), mounted at TakeTestPage.tsx:462-468, one per recording, each under a printed "Recording {n}". The left pane meanwhile prints the recording's material: its heading (GroupMaterials.tsx:67-69) and, at the audio node, the node's label as a line of text (GroupContext.tsx:62). AudioPlayer.tsx:150-250: 40px button, 4px bg-primary track, hint "n plays left". Not drawn by the deck and present in the product: a material that mixes text and audio, and a group with several recordings rework (reverses T-R3.9b: "Shared recordings sit in the question pane"); the card goes to its audio node, and an audio-only material loses its heading and label M T-D4.8
S-15 pause policy canPause S 1503; S 1677-1681 not drawn Per recording. False: while playing, the button is inert at opacity .45 and the status reads "Playing · this recording can’t be paused"; before the first play "… It can’t be paused once it starts." True: Pause and Resume. The intro note still says "you can pause once" (S 1378) and the script keeps an unread pauses counter nothing: AudioPolicy is maxPlays, allowSeek, showTranscriptAfterSubmit (api/openapi.yaml 1695-1709); no "pause" in the contract; AudioPlayer always pauses contradicts DG-111's default; needs the contract M T-D4.13, T-D4.9
S-16 play-limit lock locked S 1677; S 1679-1681 not drawn; the product keeps the button live and says extra plays are recorded After the last allowed play: lock icon, button at .45 named "No plays left", "You have used all {N} plays", and toggle returns without playing GroupContext.tsx:158-183, QuestionAudio.tsx:49-53: live button, "Played 2/2 times", takeTest.extraPlaysRecorded contradicts spec §11.4, the contract (api/openapi.yaml 1701-1707, 6858) and the canary case audio-player.test.tsx:105-112 S not built (DG-182)
S-17 play counting toggle S 1681; go S 1639 not drawn A play is spent only when playback starts from 0:00 or after it finished; Resume spends none. "Play 1 of 2" before the first play. Starting one recording pauses the others. Moving to another section pauses everything; "Resume" then continues without a play AudioPlayer.tsx:104-119 fires onPlay on every start; GroupContext.tsx:175, QuestionAudio.tsx:34-38 count each; the shared player unmounts when the question leaves the group rework; changes what spec §11.4 calls a play S T-D4.8
S-18 unlimited plays T script 6904 (plays: +x.shared.plays || 99); S 1681 not drawn No student state: the builder preview reads "Play 1 of 99" takeTest.sharedAudioUnlimited ("Unlimited plays") new (undrawn) XS T-D4.8
S-19 choice rows S 1135-1142; mk S 1643; optCols S 1660 One column; chosen marker shows a check; multi marker radius 6px The chosen marker keeps its letter in --primary-fg on primary, no check. Multi radius 7px. Rows are <button aria-pressed> in role=group "Answer". Columns: every option at most 16 characters and no stimulus gives 2 columns below 768 and min(4, options) from 768; else one. Line-height 1.4 QuestionBody.tsx:114-160: flex-col gap-2, min-h-13, leading-[1.45], <Check> at 152, rounded-[6px], real radio and checkbox inputs rework S T-D4.6
S-20 fill-in field S 1171-1176 not drawn (DG-80) The sentence at 18px (20px larger), weight 500, line-height 2.1, with the blank as an inline input: width min(100%, 340px), height 38px, no box, a 2px bottom border (--ring, primary once typed), --muted fill, radius 6px 6px 0 0, padding 0 8px, placeholder "type here", autocomplete=off spellcheck=false. An optional hint (13px). No case-rule line, no worth line QuestionBody.tsx:165-254: prompt 17px; BlankInput is a bordered box h-11 w-32 (44 by 128px, 40px from 1024), centred, no placeholder rework S T-D4.6
S-21 short answer S 1227-1236; countText S 1666 52px input, "n of L words · too many" Same, plus autocomplete=off spellcheck=false and "1 word" in the singular QuestionBody.tsx:258-302: a growing <textarea>, "{n} words", the worth line; no limit in the contract matches, with R3's recorded departures XS T-D4.6
S-22 sentence gap S 1127-1129; hasSentence S 1645 not drawn A choice whose prompt holds "___": the sentence at 18px (20px larger), weight 500, line-height 1.8, with the blank as an inline box (min-width 88px, radius 7, a 2px bottom border): empty --muted with a --ring line; chosen, the option's text on --accent-soft with an --accent-c line QuestionBody.tsx:106-110 prints the prompt as written new (no field; derived from the prompt) S T-D4.11
S-23 sign card S 1115-1122; EX_SIGN S 1494 not drawn Above the prompt: a card with a 3px --fg border, radius 12, padding 18px 24px, min-width min(100%, 320px); head 20px 700 letter-spacing .06em; optional sub 15px. Data from a table keyed by question id nothing; a question image is the nearest thing new, no model; not built (default) M not built (DG-189)
S-24 word tiles EX_UL S 1493; wordMode S 1644-1646; S 1140 not drawn Pronunciation and stress items: row min-height 60px, text 20px, the tested letters underlined (2px, offset 4px, 700). Chosen by fixture ids OptionText renders StudentOption.content; ContentMark has underline; nothing marks a "word" question new, no model; the underline is authorable, the size is not built (default) S not built (DG-189)
S-25 image and caption S 1178-1202; MAP_CELLS S 1497 not drawn (DG-80: image) Above the prompt: a 13px 600 caption ("Map of the town centre") and the picture in a bordered 12px-radius frame on --sidebar QuestionSheet.tsx:64-70 draws question.media above the prompt; alt fixed; no caption answers DG-80's image position; the caption has no field XS none; the caption is not built (DG-189)
S-26 hint line S 1133-1135, 1173-1175, 1229-1231 "Choose two answers.", "Write NO MORE THAN THREE WORDS." as fixture hints The same, per question (extra.hint), 13px muted no per-question hint in the contract; multiple choice shows takeTest.chooseMultiple there new, no field; maxWords is R6 XS not built (DG-189)
S-27 multiple choice cap exPick S 1617; stIt S 1621 "Choose two", any number of picks "Multiple choice"; the third pick drops the oldest; one pick is partly. The Teacher preview feeds the same type with "Choose all the answers that are correct." (T 6907) TakeTestPage.tsx:592-603, QuestionBody.tsx:93-102: no cap; the contract has no count R6 (T-R6.5 selectCount, T-R6.10) XS R6 (T-D4.19)
S-28 TFNG S 1482-1484; typeLabel S 1655 "True, false or not given" "True / False / Not given", three options no such type; a three-option single_choice today R6 (label changes in T-R6.10) XS R6 (T-D4.19)
S-29 matching S 1177-1226; script S 1667-1670 not drawn Prompt; "{k} of {m} matched · pick the {pick} for each {what}" (12.5px). One card per item (1.5px border, primary once matched, radius 11, padding 10px 12px) with its key, text (14.5px 500) and a radiogroup of 38 by 38 chips (radius 9, 14px 600), one per option; a chip used elsewhere is at opacity .5, choosing it moves it, tapping the chosen one clears it. With option text, no stimulus and w >= 1000: a 220px reference card on the left. s2 numbers one matching "1–5" (five in the total), s9 another "31" (one) no type; UnknownType.tsx R6 (T-R6.10's matching bullets change) L R6 (T-D4.19, DG-194)
S-30 keys onExKey S 1616; go S 1639; Esc S 1530 arrows move, a–e choose Same, off in a field or under a dialog; a–e act on single, tf, tfng, multi only (not on the cloze card or matching chips, though the hint says "A–D to choose"). No F. The arrows set exCur directly and do not go through go(), so unlike a click on a square or on Next they do not close the drawer or the sheet and do not pause a recording when the section changes; Esc clears the old sheet state, not exSheet, so it does not close the new sheet TakeTestPage.tsx:164-208: arrows, A–E, F flags; every move goes through one function, and the keys are off under the sheet matches for the keys; the bypass and the Esc slip are the prototype's and are not copied; F stays a departure, now beside a drawn hint XS T-D4.4 (the hint)
S-31 submit dialog exSubmit S 1691-1697 (markup S 1317-1362 unchanged) "Submit with {n} unanswered?" counting questions; "Unanswered questions score zero. …" n counts empty gaps and match rows one each; body "Unanswered gaps, matches and questions score zero. You still have {m} minutes."; a partly answered item is listed under "Go to"; a chip may read "1–5" components/SubmitDialog.tsx:63, 89-93, 118-129 rework only if a cloze becomes one item; none under the default XS none; T-D4.10 adds one case
S-32 preview header S 950-978; S 1522, 1541, 1620, 1713 Leave, title and save line, timer, Submit Embedded with an exam prop: no leave button, no Submit, a chip "Student view" (30px, radius 999, --info-soft / --info-ink, 12.5px 600, eye 14px); Finish toasts "This is a preview. Students submit here."; no focus counting; root height 100%; the timer starts at 45:00 and runs. The save line is still drawn and saved() still runs on every pick (S 961-963, 1617, 1633), so the preview says "Saving…" and "All answers saved" components/EngineHeader.tsx:19-76; the teacher's preview is features/tests/components/StudentPreview.tsx, not the engine. No preview host has a time limit or an integrity policy to show (note 13) reopens R4 (T-R4.31b, T-R4.30, T-R4.39) and new; the timer and the save line cannot be built as drawn S T-D4.2c, T-D4.16
S-33 focus dialog S 933-947 after the sheet, inside the take screen Same markup and strings, moved above the screen; not counted in a preview features/integrity/components/StrikeDialog.tsx matches XS none
S-34 submit toasts exSubmit.go S 1694 result page r1 s1 opens the result; s2 and s9 go Home with a toast components/SubmittedScreen.tsx nothing (prototype shortcut) XS none
S-35 still undrawn S 933-1316 and examVals, checked not drawn Still not drawn: a question's own audio; a group with a passage and a recording, or several of either; group instructions; tables, lists and images in a passage; blanks in a table and the case rule; the worth line; essay; the timer at zero; the save line's failed and offline states; the strike count; the focus dialog under warn and auto_submit; the fullscreen bar; takeover, time-up and closed locks; a sealed paper's footer; the unknown-type block; the Leave dialog's unsaved states; the Submit dialog under a minute, busy and failed; the submitted screen; loading and load-failed all built from deck primitives (T-R3.9a to T-R3.9d) nothing; DG-80 keeps them XS none
S-36 dead branches S 979-985 (hasPaneTabs), 1123-1126 (hasRef), 1245-1255 (dotsMode), isSign on matching rows S 1210-1212, 1670; examBold S 1470 live tabs and strip Markup that never renders, and one fixture field nothing reads: q12 carries { examBold: 0 } — nothing; reading the template without the script would rebuild them XS T-D4.1 (README)
S-37 header save line S 952, 963 the save label could wrap; the leave button could shrink The label is in a white-space: nowrap span and the leave button is flex: none. The drawing shows what it fixes: "All answers / saved" on two lines at about 1247px, not marked by the designer components/SaveState.tsx:58 truncates the label on one line; EngineHeader.tsx:88 gives the button flex-none matches XS none

2. Student, other

ID Where Before After Product today Verdict Size Task
O-01 keyframes S 26-27 none @keyframes qz-nudge (7px) and qz-tip; the only stylesheet change in either page. No token, colour, radius, shadow or font moved index.css has qz-marquee only part of S-07 XS T-D4.7
O-02 embed props S 33 (height: {{ rootH }}), S 1522, 1527, 1541, 1615 root 100vh; no props The page takes exam (a fixture id or an exam object) and dark; root height 100% when embedded; nothing written to localStorage — nothing (how the Teacher page mounts it) XS none
O-03 fixtures TESTS S 1376-1382; Home S 1782-1783 s2 has 20 questions; three upcoming rows s2 has 10; new s9 (31 questions, 60 min, "0 of 2", a teacher note); four upcoming rows, count "4" (the nav badge stays "3"); "Grade 9 Exam Prep" is not in the Classes fixture Home reads the API nothing XS none
O-04 entry points S 1611, 1618, 1623-1624 Continue, the due row and Start open route take All three call startExam(id, resume); the old take script is dead state app/router.tsx takeTestTree nothing XS none
O-05 fixture keys EX_RAW 7th entry, ans: […] S 1485-1488 — The Student fixture holds correct indexes and accepted answers as demo data the leak test forbids four keys on /app/* nothing; a test double copied from the deck must not carry them XS T-D4.3 (its fixtures carry no key)

Unchanged on the Student page (no hunk; S 128-930 and the dialog block S 1317-1372 diffed line for line): Home, Classes, Join dialog, Test intro, Result, Settings, Learn, Course, Lesson, Flashcards, Grades, Messages, This week, the shell, tokens, the engine header's geometry (60px, timer pill, Submit 38px; its only change is S-37), the flag toggle, prompt and option sizes, Previous / Next / Finish, the copy-and-paste toast, the Leave, Start, Join and Submit dialog markup, FOCUS_ALLOWED = 2, isMobile = w < 768, larger text at 18 / 19 / 17.

3. Teacher, test builder

ID Where Before After Product today Verdict Size Task
B-01 group model T 1581-1587, 1655-1732; script T 6881-6895, 6968, 6986, 7029 The outline "group" had a title, instructions and questions; R4 maps it to the product's Section (T-R4.31a) Every outline group can carry one "Shared content": None, Passage or Audio. Body: "A passage or recording shown beside every question in this group. Students see it on the left while they answer." The deck's group is now section and shared-context group in one level Two levels: Section (api/openapi.yaml 2492-2508) holding units, each a question or a group (migrations/00037 test_section_units); QuestionGroup 2274-2313 (200 members, 16 stimuli, 16 recordings); members owned by the group. Web: features/tests/components/OutlineTree.tsx:334, OutlineGroupRow.tsx, BuilderGroupPane.tsx contradicts R4's reading, in which the deck's group is the product's section (T-R4.31a); the product's model is a superset L if built literally; S under the default T-D4.2a (DG-186)
B-02 outline line T 1581-1587; script T 6968 not drawn Under the instructions line, same geometry (margin 0 0 4px 26px, padding 4px 6px, radius 6, 12px muted, 13px icon, one line with ellipsis): file-text "Passage · {title or Untitled}" plus " · {n} gaps", or audio-lines "Audio · {file or no file yet} · {n} plays" / "· unlimited plays". Shown while the group is open. Opens the dialog. The prototype prints "1 plays" OutlineGroupRow.tsx:53-92: a Layers icon and the title, no summary reopens R4 (T-R4.31a) S T-D4.2a
B-03 menu item script T 6986 Rename, Instructions, Move up, Move down, Remove group "Shared content" (icon panel-left) in third place OutlineTree.tsx:462-488: the section menu; the group is added from the footer (334) and has no menu reopens R4 (T-R4.31a) XS T-D4.2a
B-04 dialog frame T 1650, 1655-1656; script T 7026-7030 one dialog, 440px, two variants Width per variant: 440px for Remove and Instructions, 560px for Shared content. Title "Shared content for “{group}”", a scroll area (gap 12px, max-height 56vh), Cancel and Save, toast "Shared content saved". No validation, no dirty guard, no Esc no dialog: BuilderGroupPane.tsx:150-204 renders GroupComposer in the editor pane, with autosave and device recovery new M T-D4.14
B-05 kind switch T 1657-1663; script T 6887 not drawn Segmented, full width, 30px buttons, 13px 500, 14px icons: "None" (circle-slash), "Passage" (file-text), "Audio" (audio-lines). One kind at a time; the other kind's fields stay in the draft a group holds up to 16 materials mixing text, images and audio (question-groups/components/GroupComposer.tsx:136-150) new; must not drop what the dialog cannot show S T-D4.14
B-06 passage fields T 1664-1669 not drawn "Title" (36px, "e.g. Why cities need green space"); "Passage", a plain textarea, 7 rows, 13.5px, line-height 1.55, "Paste or type the passage students read" "Material name" and ContentEditor (MaterialContent.tsx:39-58); title 1 to 200 characters, required contradicts T-R4.63 (the material is a content editor host): keep the editor, take the labels S T-D4.14
B-07 gaps T 1670-1693; script T 6889-6890 not drawn in the builder Hint "Optional: add gaps for a cloze. Each gap is answered by a single-choice question in this group, shown at the gap." and "Insert gap" (30px, text-cursor-input), which appends "[gap n] " at the end of the text. Row: a chip "gap {n}" (min-width 52px, 24px, 1.5px dashed --ring, --muted, 12px 600), a select (32px) "Choose a question…" then "Question {id} · {title}" for every question, a danger border while unbound, and "Remove gap" (28px). One question may take two gaps; Save accepts unbound gaps Exists in full: GapBindings.tsx:14-133 (targets: choice members or one blank of a fill-in; a used target is disabled), gaps inserted at the caret; rules in model.ts:113-155 and server/internal/modules/tests/domain/group_validation.go:176-212 reopens R4 (T-R4.34): the drawn row is taken; the product's rules stay S T-D4.2b, T-D4.14
B-08 audio file T 1696-1712; script T 6891 not drawn Empty: a dashed zone (1.5px --border, radius 10, padding 12), "MP3, M4A or WAV up to 25 MB", "Choose file", "From Media" (30px). Attached: a card with the file name and "Replace" (28px). No progress, failure, length or player MaterialAssetDialog.tsx, MaterialTools.tsx:107-118; T-R4.35 moves the upload into useMediaUpload; T-R4.65 builds "Choose from Media" new; DG-63's limits stand (MP3 or M4A, 50 MB, 5 minutes) M T-D4.14
B-09 plays T 1714-1719; script T 6892 not drawn for a group A select: "1 play", "2 plays", "3 plays", "Unlimited"; default 2. A third drawing of Plays (question media: "Once | Twice | Unlimited", T 2155-2159; the bank: a select) one component for both: question-bank/components/AudioPolicyPanel.tsx:13 (1, 2, 3, 5, Unlimited) reopens R4 (T-R4.65): its control serves the group too XS T-D4.15
B-10 students can pause T 1720-1724; script T 6893, 6904; also T 2161-2165 drawn once, on question media; DG-111: not built Drawn a second time, on the group's recording: a switch, on by default (34 by 20 track, 16px knob #fff). The value reaches the Student page as stim.canPause, which acts on it nothing in the contract, the schema or the player contradicts DG-111's default; needs allowPause M (server) + S (web) T-D4.13, T-D4.15
B-11 transcript T 1726-1729 not drawn for a group "Transcript", "Optional. Students see it after they submit.", 3 rows. No "Show transcript after submitting", no "Allow skipping ahead". The preview does not pass it on AudioPolicyPanel.tsx:66-108: both switches and the transcript; GroupRecording.transcript up to 100,000 contradicts DG-111 (both switches stay) XS T-D4.14
B-12 undrawn states outline rows T 1591-1623; publish checks script T 7706 not drawn Still nothing for: a question bound to a gap (in its row or its card), deleting or retyping a bound question, publishing with an unbound gap or with "no file yet", a group that holds more than the dialog shows GroupComposer.tsx:372-381 (a bound member cannot be deleted), model.ts:162, group_validation.go:52-67 nothing; product behaviour kept, listed in a gap row XS T-D4.1 (DG-187)
B-13 preview overlay T 5737-5761; script T 6896-6901 A 720px form dialog "Student preview" / "What students see. Answers here are not saved.", one question at a time, Previous / Next, four demo options A full-window overlay (role=dialog, "Student preview", z 95, --overlay, padding 14px, gap 12px). Bar (card, radius 12, --shadow-lg, padding 8px 10px 8px 14px): eye 16px, "Student preview · {title}" 14px 600, a segmented "Computer | Phone" (28px buttons, 12.5px, icons monitor, smartphone), a 34px "Close preview". Frame: min(1320px, 100%) and radius 12px, or 390px and radius 28px, full height, holding the Student take-test screen. No Esc, no focus trap features/tests/components/DraftPreviewDialog.tsx:76-90 (max-w-3xl, 85svh), StudentPreviewPane.tsx (phone 320px), StudentPreview.tsx:44-95 (panes stacked, disabled) reopens R4 (T-R4.31b: "Previous / Next" is no longer drawn) and new M frame + L engine T-D4.2c, T-D4.16
B-14 previewExam script T 6902-6914 not drawn The prototype's mapping from builder state to the Student page. It passes no gaps, no question media, no rich prompt, no per-question options, no transcript; Unlimited becomes 99 question-groups/preview.ts, DraftPreviewDialog.tsx:39-75 map the real draft, gaps and assets included, transcripts stripped nothing (demo plumbing; never the data contract) XS none
B-15 fixtures INIT_SECS T 6632 two demo groups Both carry a shared passage ("Why cities need green space", "The history of maps") and gaps: [], so the outline shows a "Passage · …" line under each open group by default — nothing; it is the fixture a deck check of the outline sees XS T-D4.2a (its deck check)
B-16 semantics T 1650-1741, 5737-5761 — role=dialog with no name on the group dialog, no Esc, no focus trap or return; outline: 0; the knob #fff; plain buttons for the kind switch R1 primitives (FormDialog, Segmented, Switch, focus rings) nothing; DG-116, DG-36, DG-34, DG-30 decide these XS none

4. Teacher, other

ID Where Before After Product today Verdict Size Task
T-01 test detail preview T 5846-5853; script T 7508 A scrolling list (max-height 680px): five questions, changed ones outlined with "New" / "Changed" chips in compare mode, footer "Showing 5 of {n} questions. Answers are hidden from students." A frame holding the Student take-test screen: padding 16px; width 100% and height 620px, radius 12px, or 390px and 720px, radius 24px. No outline, no chips, no footer. The "Changes from version {n}" list above and "Computer | Phone" are unchanged. The prototype feeds it the builder's draft, whatever the version features/tests/pages/TestDetailPage.tsx:168 mounts StudentPreviewPane reopens R4 (T-R4.30, its preview bullet) S T-D4.2c
T-02 import preview T 4975-4980; script T 7427-7428 A scrolling list (max-height 640px) built from the import, footer "Showing 7 of 34 questions. Answers and your notes are hidden from students." It drew the cloze as the product models it: the passage with gap chips, then a member question "23.1 Choose the word for gap 23.1." with its own options and underlined runs. It was the one place where the deck drew one question per gap on a student-eye surface The same frame holding Student fixture s9, not the import's own data, so the per-gap cloze is gone from every student-eye drawing (evidence for DG-184; its data stays in the script, T 7428). No footer no confirm page yet; T-R4.39 creates ImportConfirmPage.tsx reopens R4 (T-R4.39, its preview bullet) S T-D4.2c
T-03 cross-page import T 4978, 5756, 5850: <dc-import name="Quizzivy Student" exam="…" dark="…"> no page imported another The Teacher page mounts the Student page three times; support.js already implements dc-import scripts/check-design-deck.mjs:20-31 reads src, href and quoted names ending .svg, .dc.html, .js: name="Quizzivy Student" is unchecked the import teaches the check script XS T-D4.1
T-04 question count S 1380 (q: 31), 1634-1637; T script 7430 ("… of 35"); old footer "34" 34 and 35 The same paper is "31 questions" on the Student page (the cloze counts once) and 35 on the Teacher import. Inside the Student page a matching numbered "1–5" counts five and a cloze of five gaps counts one every member is a question; TestSummary.questionCount counts questions design question; no build under the default S none (DG-185)
T-05 instructions copy script T 7030 (unchanged) against S 1104-1107 "Students see this above the group’s first question." The builder still says so; the Student page shows the instructions above every question SectionInstructions on the first question reopens R4 (T-R4.31a): the dialog's line is reworded when S-09 ships XS T-D4.5
T-06 github.md "Last sync", "Sync history" 2026-10-03T17:53:01Z, four bullets on the content editor 2026-10-04T09:04:30Z, "branch read: develop (question group graph: migrations/00037, 00002)", two bullets. The Screen map is unchanged: no row for features/take-test/** or features/question-groups/** — nothing; explains the drift: pause is in no migration, allow_seek and the blank gap kind were not drawn, the contract and the engine were not read XS T-D4.1 (DG-195)
T-07 dead script values script T 7427-7428, 7508-7509 read by the two list previews wi.preview, wi.pvOptCols, td.preview, td.pvFoot ("Showing 5 of … questions. Answers are hidden from students.") and td.pvOptCols are still computed and no markup reads them — nothing; reading the script without the markup would rebuild the list previews XS T-D4.1 (README)

Unchanged on the Teacher page (all fifteen hunks are covered above): the builder's title bar, outline header, group header geometry, question rows, drag, footer, splitter, the whole editor pane (content editor, type menu, answer areas, grading note, question media with its own "Students can pause" and "Once | Twice | Unlimited", "Choose from Media", "Preview content after pasting", More options), the Publish dialog and its three checks; the bank's Question editor, Question bank, Media, Tests list; Imports history, upload and paste, processing, the Review workspace; Test detail's header, banner, changes list and history; the shell and every other screen. No token or CSS change. So T-R4.33, T-R4.35, T-R4.38, T-R4.62, T-R4.63, T-R4.64 and T-R4.66 are untouched.

5. Drawings (uploads/, eleven PNG files)

File Screen Marks In the export Verdict
draw-99b474b9-… (new) Student, take test, the cloze of "Grade 9 Mock Test…" at about 1247px, on an intermediate revision (rail, passage, question) 1: the "Gap 1" card's four options in one row, cut ("relax", "relaxin", "relaxe", "relaxes"), and its header wrapping. 2: the footer's centre button cut to "23 · 0/…" 1: options are repeat(auto-fill, minmax(132px, 1fr)) with min-height: 46px, both header spans nowrap (S 1157-1163). 2: with the rail (from 1180 beside a stimulus) the button is not drawn and the key hint takes its place; without it the label is the short form in a span with ellipsis (S 1265-1270, 1714). Unmarked, and both fixed: the rail's "Grammar and vocabulary" overlapped itself (S 1011), and the header's "All answers saved" stood on two lines (S 963, row S-37) nothing to build beyond S-03, S-04, S-13; README's "ten red-pen markups" becomes eleven with one new row
draw-8128a579-… Student, Join dialog ("6-character") already "8-character" in the deck of record unchanged already resolved
the other nine (47e9ff40, 788a17fb, 79c88341, 8ccee204, 9ac2b5fe, a3f40015, b7e91ca8, bf32bf87, ed818f3d) Teacher and Admin the rows of README's "Resolved feedback" table, one to one unchanged already resolved

screenshots/ (39 files) is Claude Design's own captures; not imported, as before.

6. Unchanged, and what the contract already holds

Byte-identical to the manifest: Admin, Landing, Sign in, Splash, System pages, support.js, qz-controls.js, google-g.svg, both mark SVGs, brand/ (two SVGs). docs/plan/75-r5.md is not touched.

The student payload already carries everything the new take-test screen draws for a passage, a cloze and a recording, with one exception; the teacher's preview lacks one more thing, which is not a missing field but the assignment's:

The deck draws The contract has Where
sections with title and instructions StudentSection {id, title, instructions} api/openapi.yaml 2098-2110
a group, its members, its passage StudentGroup {title, instructions, questionIds, stimuli[], recordings[], assets[]} 2146-2165
a gap bound to a question GroupStimulus.gaps[]: GroupQuestionGap {gapId, questionId} or GroupBlankGap 2326-2371
a recording, its length, its allowed plays StudentGroupRecording {id, assetId, policy}, MediaAsset.durationMs, AudioPolicy.maxPlays 2167-2175, 1695-1709
plays used, across reloads and devices groupAudioPlays, POST /app/attempts/{id}/group-audio-play (x-permission: learning.take_tests) 6837-6870
a question's image StudentQuestion.media 2112-2144
underlined letters in an option StudentOption.content marks (underline) ContentMark
"Students can pause" nothing no "pause" in the file
a time limit in a preview (45:00) nothing a preview can read: durationMinutes is the assignment's 3175, 3195, 2640-2645; previewTest 4391-4398

What the deck draws and no release plans a field for: a per-question hint line, an image caption, a sign or notice block, the "word" option size, matching's nouns (what, pick, colsTitle) and free key styles, a cloze as one item with sub-ids ("23.1"), display numbers and ranges. tfng, matching, selectCount and maxWords are R6's (T-R6.1a to T-R6.5). A transcript is correctly absent from every student shape and already released after submission through SharedReviewContext.transcripts.


What the v2 export changed (2026-10-10)

Read against the fourth export (uiux/update-teacher-student-view @ fd66dd6a, byte-identical to the export the six tables above inventory) and uiux/update-uiux-v2 @ 26b60795. Five pages changed; github.md, support.js, qz-controls.js, the SVGs, brand/, Splash and System pages are byte-identical to the fourth export, so with the three files above the manifest changes in six files (seven with itself) and node scripts/check-design-deck.mjs still prints "Deck OK: 15 files, 7 pages". No file is added; the three <dc-import name="Quizzivy Student"> mounts are unchanged; no <link>, font, token, colour, radius, shadow or keyframe moved (qz-pulse and qzSpin, which the AI panel uses, exist in the fourth export). Every new icon name exists in the web's lucide-react 0.548. The take-test engine, the builder's shared content and the three previews are byte-identical between the two exports, so nothing in sections 1 to 6 is contradicted.

File sha256 (v2) Size Hunks (pretty)
Quizzivy Teacher.dc.html 184536f0c6d9732ed4fae5eaf5d5e45dc188fda03d64f0ce682372364c096450 794,309 to 907,000 bytes 54
Quizzivy Admin.dc.html 8c043d26e1c4981eaee58e2b65972666a6c17e1521b9235f382cff771b44ae6a 151,390 to 171,480 bytes 14
Quizzivy Student.dc.html dc6d6077fdfe6d79de7acba73db7dce5fab4cc670d223b7be34aee3b0da4d7bb 173,426 to 173,401 bytes 10, all fixtures
Quizzivy Landing.dc.html 52196e8610429e455d619c00d4d2c13934c855c4cb98bda7dae95ccdbc5cd0db 49,717 to 49,703 bytes 3, all fixtures
Quizzivy Sign in.dc.html 15ab7e2bbcb14e96979c0cc000fdb90e464796593485ab2ccc8e886649321f6f 22,899 to 22,896 bytes 2, all fixtures

Verdicts as above, plus R5, R8 and R10 for a screen a later release owns. The last column names the D4 task, or the release and the correction T-D4.19 makes.

7. Teacher, v2

ID Where (v2 T) Fourth export v2 Product / plan today Verdict Size Task
V-01 sidebar separator T 86-89; showSep: !!g.label && !g.showLabel (T 9696); the same on the Admin page (A 79-82) Group labels only A 1px role="separator" between nav groups while labels are hidden (collapsed) layouts/shell/Sidebar.tsx (T-R4.4), shared by R5's AdminLayout reopens R4 (T-R4.4) XS T-D4.20
V-02 sticky side columns layoutVals T 9624-9630; the outline T 1633 (outPos, outMaxH, outOv), the wizard aside T 2864 (naSide), the course aside T 3596 (crsSide), the settings nav T 3853 (setNavPos), the editor aside T 4649 (qeSide), the class detail column T 4277 (cdSide), the review aside T 5405 (wiSide), the version history T 6282, the bank filters T 1100 (bfTop) sticky; top: 0 on two asides; the rest static Every side column position: sticky; top: 16px; max-height: calc(100vh − 88px); overflow-y: auto from a content width of 800 (wizard, course) or 1000 (class detail, review); the editor aside from 900; the settings nav from 768; the outline whenever the builder splits, scrolling inside itself; the bank's aside top: 16px from 1024 T-R4.31a, T-R4.27a, T-R4.33, T-R4.42, T-R4.38, T-R4.30, T-R4.32, T-R4.43; the course aside is R10's (T-R10.13) reopens R4 (geometry); R10 for the course S T-D4.20
V-03 section menu placement T 1675; openMenu T 7782 (secMenuXY) position: absolute inside the row position: fixed at the button's rect, clamped to the viewport, so the scrolling outline cannot clip it T-R4.31a ("menus are never clipped") matches; re-checked once the outline scrolls — T-D4.20
V-04 grading pane T 847, 859-875; gPane, queueH T 9629 The page grows with the card; the queue is 180px high on phones From 768 the page is calc(100vh − 120px) high (min 560px) and the queue and the card scroll inside themselves; on phones everything is static and the queue keeps 180px T-R4.28 reopens R4 (T-R4.28) S T-D4.21
V-05 assignment progress card T 607-631; asgProgVals T 9612-9623; rosterGroup, rosterCounts T 9610-9611; asgRow T 9705; roster: rosterShown T 9713, 9722 Four stat tiles One card: "{n} of {total} submitted" ("All {n} submitted", "Not assigned yet"; 13.5px 600), an 8px two-segment bar (submitted --success, in progress --info), legend buttons aria-pressed (28px, radius 7, 12.5px) with a dot or the flag icon in danger ink and a count (Submitted, In progress, Not started, Flagged) that filter the roster (rosterF; the page resets; the Students tab opens; an entry hides at 0 unless selected), "Average {avg}" (15px 600) after a 1px divider; opening the assignment clears the filter T-R4.25 built the tiles ("four columns from 640 content width", ticked); Monitor holds the counts reopens R4 (T-R4.25) M T-D4.22
V-06 "Explain results with AI" T 742-776; ins T 9529-9536; aiInsights T 9463-9469 "Questions sorted by how often students got them wrong." Beside that line an outline button (32px): "Explain results with AI", a spinner and "Reading results…" while running, "Refresh" after; under it a panel on --accent-soft (radius 10, padding 14): "What AI noticed" (sparkles, accent ink), "Based on {n} attempts", "Hide insights" (26px); three or four cards (--card, radius 9, padding 12) with a 28px icon tile (key-round warning, triangle-alert danger, trending-up success), a title under nine words (13.5px 600), a body with numbers (13px muted), and an action button "Review answers" (→ Grading) or "Make a practice set" (→ the Generate panel with types tf and short, primary "Save as practice test") T-R4.26 (Questions tab), T-R4.13 (item analysis) new (AI) server M, web S T-D4.32, T-D4.37
V-07 "Suggest a score" T 908-948; gai T 9517-9527; aiGrade T 9451-9462 Student answer, then Score Between them a box on --accent-soft "AI suggestion": idle, a sentence ("Compares the answer with the accepted answers. You decide the score." or, for a band, "Scores against the four IELTS criteria. You decide the final band.") and "Suggest a score" (30px); running, a spinner and "Reading the answer…" / "Reading the essay…"; done, a confidence pill (High / Medium / Low confidence in success / warning / danger tones), "{score} / {max}" (17px 600), bullet reasons (13px), "Comment for the student: …" in a card, "Use score and comment" (primary; fills the score and the comment; toast "Score and comment filled in. Check them, then Save & next."), "Try again", and in the demo "Sample output. AI didn’t respond." Drawn only while "Let AI read student answers" is on T-R4.28 (score buttons 0..max in 0.5 steps, comment with snippets); bands and rubrics are R6 (T-R6.7, T-R6.11) new (AI); the band form is R6 server S, web S T-D4.32, T-D4.36
V-08 bank header T 1077-1086 New question, Share "Clean up" (wand-sparkles, accent ink) and "Generate" (sparkles) before Share, outline 36px T-R4.32 reopens R4 + new (AI) XS T-D4.34
V-09 builder title bar T 1619-1631 Question settings, Versions, Preview, Publish "AI check" (scan-search) and "Generate" before Preview; the actions row is flex-wrap: wrap T-R4.31a ("keeps one row wherever the deck does") reopens R4 + new (AI); the one-row measurement is redone with two more buttons XS T-D4.34
V-10 question editor T 4560-4607, 4649-4681; qai T 9493-9515; aiExplain T 9435-9442; aiDistract T 9443-9450 Add option; the Explanation textarea in a <label> For single and multiple choice, beside Add option: "Suggest wrong options" (30px, muted; "Thinking…" with a spinner) and a panel "Suggested wrong options" (accent-soft) with up to four rows {text, why} and an "Add" button each (fills the first empty option or appends; "Added" after; the deck's toast "A question can have up to 6 options. Remove one first." is DG-63's 8 in the product), "Close suggestions", and an error line "AI didn’t respond. Try again in a minute.". Explanation: the label row gains "Write with AI" (28px; "Writing…"; "Write again" after) and the textarea takes aria-label="Explanation"; a panel "Suggested explanation" with the text (14px) and "Use this" (primary), "Shorter", "In Vietnamese" / "In English" (the other language than the preference), "Discard". Both guarded: "Write the question first", "Mark the correct answer first" T-R4.33 and T-R4.66: one QuestionEditor for the bank and the builder (DG-108), so both pages gain it new (AI) server S, web M T-D4.32, T-D4.35
V-11 "Read with AI" on the import page (the deck draws it on Upload and setup; built in the Sources pane of the import workspace, DG-207) T 5015-5033; aiImp T 9491 One accepts line, scanned PDFs refused A toggle card (1px border, radius 10, padding 12px 14px; a 32px sparkles tile; role="switch" 38 × 22): "Read with AI", "Handles scans, photos of printed tests and unusual layouts. About 400 credits per page."; on: "Accepts .docx, .doc and .pdf files up to 25 MB each, including scanned or photographed PDFs. Files with a password or macros are refused."; off: the fourth export's line. It mirrors the Settings preference "Read imports with AI" T-R4.36; the PDF_NO_TEXT refusal (17-word-import.md §1.39); 18-word-import-ux.md §4 (say where processing occurs and what is sent) new (AI) server L, web S T-D4.33, T-D4.39
V-12 "Make a variant" T 6206-6209; AI_K.variant T 7649-7661 Version history, Share, Open builder, Assign "Make a variant" (sparkles) before Share. Its panel: "A second version for another class or a retake, so students can’t share answers."; chips What to change (Reword questions, Reorder options, New wrong options; the first two on), Difficulty (Keep the same, A little easier, A little harder); an info callout "Answers, points and passages stay the same. The variant is saved as a draft version, so nothing changes for students until you publish it."; steps Reading the test / Rewriting questions / Checking that every answer still matches; items tagged "Q{n}" and Reworded / Options reordered / New wrong option with a struck-through before and an after; primary "Save {n} changes as a draft" → "Draft version saved. Review it in Version history before publishing." T-R4.30; a test has one draft (T-R4.16) new (AI); "saved as a draft version" is not taken (DG-197) server M, web S T-D4.32, T-D4.34
V-13 Settings › AI assistant T 3861-3921; aiu, aiLang, aiPrefs, setAi, setTabs T 9484-9490 Five sections A sixth section "AI assistant" (sparkles) after Appearance. Card "AI usage this month": "Your school gives each teacher a monthly allowance. It resets on 1 October."; "{used} of {limit} credits" (22px) and "{left} left"; an 8px accent bar; a bordered list of five rows with icons: Generating questions and variants, Grading suggestions, Explanations and wrong options, Reading imports, Checks, word lists and insights. Card "Preferences": "Applies to every AI feature in your workspace. AI output is always a draft until you approve it."; "Explanations and feedback in" English | Tiếng Việt ("You can still switch language on any suggestion."); switches (38 × 22) "Tag AI drafts" ("Adds an “AI draft” tag until you edit or approve the item. Only teachers see it."), "Let AI read student answers" ("Needed for grading suggestions and class insights. Student names are never sent."), "Read imports with AI" ("… About 400 credits per page."). No request control, though the Admin page lists requests (DG-200) T-R4.43 (sections, /teacher/settings/:section), T-R4.7 UserPreferences new (AI) web M T-D4.38
V-14 the AI panel T 6540-6750; aiVals T 9470-9591; AI_K T 7577-7663; aiOpen, aiStart T 9405-9429 not drawn A right-side role="dialog" named by its title, min(560px, 100%) wide, full height, --card, a left border, --shadow-lg, over a --overlay backdrop (z 75 / 76). Header (padding 16px 16px 16px 18px): a 36px accent icon tile, the title (15px 600), the sub (12.5px muted), Close (32px). Body (padding 18, gap 18) in three stages. Setup: fields by type: source chips (28px pills with an icon: "Passage 1 · Urban green space", "Start empty"), a textarea (13.5px, --bg), a segmented control (30px), toggle chips (30px pills, aria-pressed, a check when on), a toggle card (border, radius 10), an info callout (--muted, info icon). Running: three step lines (a check in --success, a spinner, or a 7px dot) advancing every 1.3s, then a pulsing skeleton (qz-pulse); a run lasts at least 3.2s. Done: in the demo a warning callout "AI didn’t respond, so this is sample output. Try again in a minute."; a summary line (13px muted) and "Select all" / "Clear selection"; a dashed empty card; item cards (border, radius 12, padding 14; --ring border when selected): a role="checkbox" "Keep this" (18px) when selectable, tone pills, a right-aligned meta, a 14px 500 title, a 13px body, lettered options (the correct one in success tones with a check), an "Answer" pill, a before / after grid (the before struck through for a variant), dashed "+ {tag}" chips, a note with a bold label, action buttons (30px; the primary filled). Footer: a credit note with coins ("About {cost} credits · {left} left this month"; after a run "Used about {cost} credits"; or the validation sentence with circle-alert in warning ink), a secondary (Cancel / Stop / Start over), a primary (the run label with sparkles; after a run the apply label, at .5 opacity until something is selected; "Done" when the kind applies nothing). No Esc, no focus trap, no focus return (DG-36) nothing in the product new (AI) web L T-D4.34
V-14a Generate questions (gen) T 7577-7605 — "Generate questions" / "From a passage, a transcript or a topic. Questions arrive as drafts for you to check.". Fields: Source (8 rows; chips "Passage 1 · …" with the test's passage, "Start empty"; placeholder "Paste a passage or transcript, or describe a topic"), Question types (Single choice, True / False / Not given, Fill in the blank, Short answer; default single and tf), How many 3 / 5 / 8 (5), Level B1 / B2 / C1 (B2), "Write an explanation for each question" / "Students see it after results are released." (on). Validation: "Add a source of at least a few sentences" under 40 characters, "Pick at least one question type". Steps: Reading the source, Writing questions, Checking every answer against the source. Items: tags the type, the level, "AI draft" (when tagging is on); the prompt; options with the key marked; an "Answer" pill for blank and short; the explanation as a note. Primary: in the builder "Add {n} to {section}" (appends the picks to the open question's section and opens the first; toast "{n} questions added to {section}. Check each one before publishing."; from a passage that a shared-context group holds ours reads "Add {n} to {group}" and adds the picks to that group as members, since v0.11.3); in the bank "Save {n} to the bank" ("… with an AI draft tag"); from insights "Save as practice test" ("Practice test with {n} questions saved as a draft in Tests"). Cost 1,800 the section and bank question writes exist; tfng is R6 new (AI); TFNG is offered once R6 ships — T-D4.32, T-D4.34
V-14b Check this test (check) T 7606-7617 — "Check this test with AI" / "Looks for answer keys that don’t match the passage, unclear wording and questions that give answers away.". Look for: Wrong or doubtful answers, Unclear wording, Answers given away, Spelling and grammar (all on); info "Checks the questions in “{title}” against their passages. Nothing changes until you apply a fix.". Steps: Reading both passages, Checking answers against the text, Looking at wording and options. Items: tags Likely error (danger) / Worth a look (warning) / Minor (info) and "Q{n}"; meta the type; title the question's; body the issue; note "Suggested fix"; actions "{fix label}" (applies and toasts), "Open question", "Dismiss". Summary "{n} things worth a look"; empty "Nothing left to look at. The test is ready to publish."; no primary. Cost 2,400 the draft read; updateQuestion for a fix; the demo's fixes name R6 types and maxWords new (AI); fixes whose write exists are one click, the rest open the question — T-D4.32, T-D4.34
V-14c Clean up the bank (dups) T 7618-7633 — "Clean up the question bank" / "Finds near-duplicates and questions with missing tags, so search and filters work better.". Look for: Near-duplicates, Missing tags; info "Scans the questions you own. Questions shared with you are skipped, and nothing changes until you confirm.". Steps: Reading 1,284 questions, Comparing similar questions, Suggesting tags. A duplicate card (not selectable): "Possible duplicate", meta "Used in 4 tests and 1 test", the reason as title, One / Other, actions "Merge into one" (toast "Merged. Tests that used either question now share one copy.") and "Keep both". A tag card: "Missing tags", the level, the prompt, dashed "+ {tag}" chips; primary "Add tags to {n} questions". Empty "All done. Your bank is tidy.". Cost 3,200 listQuestions with Own(); the bulk tag write (T-R4.32); no merge operation new (AI); no merge (DG-197): a duplicate card has "Open" for each and "Keep both". One request reads at most 2,000 questions, suggests tags for the first 200 untagged ones, finds at most 20 duplicate pairs by comparing every pair of the bank by its words and having one call judge the closest, and settles once; the response's truncated tells the panel to say "Apply these and run it again" (T-D4.32 As built, and its follow-up 1) — T-D4.32, T-D4.34
V-14d Generate words (vocab) T 7634-7648; the button T 3678-3682 — "Generate words from a text" / "Picks the words your students need to understand a passage, with meanings and new example sentences.": Text (7 rows), How many words 8 / 12 / 20, Students’ level A2 / B1 / B2, Meanings in Tiếng Việt | English; items the term, part of speech and level as tags, the meaning, the example as a note; primary "Add {n} to {list}". Cost 1,200 word lists are R10 (T-R10.4, T-R10.12) R10: the entity does not exist before it — R10 (T-D4.19: T-R10.12 gains the button and the kind)
V-15 class detail: tabs T 4145-4155; cdTabs, cdOverview, cdAttTab T 9655-9658 One screen Underline tabs Overview | Attendance under the header; Attendance carries an accent badge with the number of sessions not yet taken T-R4.42 (one screen); attendance is R8's R8 S (R8) R8 (T-D4.19)
V-16 class detail: join-code card T 4287-4345; cdCodeFull, cdLink, cdQrOp, cdShowQr, cdShare T 8328-8331; VIA T 8303; fresh T 8320 "••••-{last 4}" in a box, "The full code is shown only once, when it is created."; the paused paragraph; link rows; the New-code dialog "Copy it now. After you close this, only the last 4 characters are shown."; "Joined via" "Code ••{hint}" A 112px QR button (qr-code 88px, cursor: zoom-in, --qr-bg; "Show full size for the projector"; opacity .4 while joining is paused) opening a 420px dialog "Join {class}" ("Show this on the projector. Students scan it or type the code." or "Joining is paused, so this code won’t work until you turn it back on."); beside it the full code (22px monospace, 600, letter-spacing .08em) with the join link under it (12.5px muted, ellipsis) and a "Joining paused" chip (pause, warning tones) in place of the paragraph; a row of three outline buttons (32px, flex 1, ellipsis) Copy code (copy), Copy link (link), Download QR (download); Expires and Uses under a 1px divider; the footer "A new code makes the old code, link and QR stop working."; the New-code dialog's desc "Share the code, the link or the QR with your students."; members' "Joined via" shows the full code T-R4.42 built DG-01 (the full code, no "shown only once") in the fourth export's geometry reopens R4 (T-R4.42); DG-01 is Answered by v2 S T-D4.23
V-17 class detail: Attendance tab T 4359-4488; attNewVals T 9636-9695 not drawn A session picker (calendar-days, "{day} · {time}", chevron; a 260px menu titled "Sessions": each session with Today / Saved / Not taken), a status chip (Not taken yet / Not taken / Saved {time}); a 240px search "Search students"; "Mark all present" or "Mark the rest present". A card: a progress row "{k} of {n} marked" / "All {n} marked" with a three-segment bar (present / late / absent) and legend filters (Present, Late, Absent, Not marked); a selection bar on --primary ("{n} selected", Mark present, Mark late, Mark absent, Clear selection); a 40px header on --muted (Select all with an indeterminate minus; Student; "This term" from 640; Status; Note; the three from 760); rows (min 56px): a checkbox, avatar and name, the rate in danger ink under 80%, a radiogroup "Attendance" of Present / Late / Absent (68 × 30px, each in its tone when chosen), a Note button (message-square-text with the note, or "+ Note"; a dialog "Note for {name}" / "Only you and co-teachers see it.", placeholder "e.g. Left early for a doctor’s appointment"); empty "No student matches “{q}”" / "Nobody here yet."; footer "Students see their own attendance. Late counts as present in the attendance rate.". A sticky bar (12px from the bottom): "{n} changes not saved · {m} not marked yet", Discard, "Save attendance"; "Save with {m} not marked?" / "They count as absent until you mark them." / "Save anyway"; toast "Attendance saved · {a} absent, {l} late" R8 T-R8.6 (present / late / absent / unmarked; "No student operation exposes attendance in R8"), T-R8.12 R8; two sentences contradict R8's decisions (DG-198) M (R8) R8 (T-D4.19)
V-18 Attendance page T 3189-3235; attNewVals T 9641-9653; ATT_* T 7665-7667 One class's register at 960: "{class} · {date}", Change session, Save, a counts bar with Mark all present, a segment per row A cross-class list at 1320: "Take the register for today’s classes, or catch up on sessions you missed."; three cards "Today · {date}" ("{n} classes"), "Needs a register" ("Past sessions with no attendance taken"), "Recent" ("Last 7 days"); rows: time (13.5px 600) over the day ("Today"), the class name, a meta ("Room 2 · 24 students", or "{p} present · {l} late · {a} absent" once saved), a status chip (Not taken yet, warning; Not taken, danger; Saved, success), "Take attendance" (primary, calendar-check) or "Open"; empties "No classes today.", "All caught up.", "Nothing yet."; a row opens the class's Attendance tab on that session R8 T-R8.12 R8; T-R8.12's screen no longer exists M (R8) R8 (T-D4.19)
V-19 word list progress T 3689-3714; wlProgVals, wordCounts, WBAND T 7664, 9592-9609; paginate T 8379 Four stat tiles (Words, Students practising, Mastered on average, Hardest word) One card: "{p}% mastered on average", a three-segment bar (mastered --success at 70% or more of students, learning --warning 50–69%, hard --danger under 50%), legend filters with counts that filter the word table (wordF, the page resets), "Practising {a} / {b}" after a divider; "Generate from text" (sparkles) before Add word R10 T-R10.12 R10 S (R10) R10 (T-D4.19)
V-20 prototype plumbing AI_* T 7413-7663; aiAsk T 9399-9404 (window.claude.complete); AI_USE0, AI_LIMIT 50,000; aiSpend; AI_WAIT 3.2s — The prototype calls a browser-side model with inline prompts and falls back to demo arrays when it fails; usage is faked; a run waits 3.2s. None of it ships; the prompts are evidence of what each feature sends (section G) — nothing — T-D4.1 (README)
V-21 persona renames T 111-131, 215, 3931, 7065, 7782, 8143, 8202, 8278, 8592, 8828; the Student, Admin, Landing and Sign in pages Hoàng Thương (HT, thuong@), Trần Quang (TQ), "Ms Thương" Giàu Bùi (GB, giau.bui@), Thường Võ (TV, thuong.vo@); the honorific is gone; the Landing's testimonials name Thường Võ as "Centre manager" fixtures; DG-22, DG-71 and DG-106 quote "Ms Thương"; DG-100 wants consent for testimonials nothing; a one-word note on three rows XS T-D4.1

8. Admin, v2

ID Where (v2 A) Fourth export v2 Plan today Verdict Size Task
A-01 nav separator A 79-82; A 1803 — As V-01 T-R5.20 on R4's Sidebar built with V-01 (one component) — T-D4.20
A-02 nav "AI credits" A 1821 (item('ai', 'AI credits', 'sparkles', badge)); titles and r.ai A 1825-1826 System: Audit log, API reference, Settings System: AI credits (sparkles, a badge with the pending requests), then the three; route ai T-R5.20's nav list R5; the page is new — R5 T-R5.35 (new); T-D4.19 corrects T-R5.20
A-03 Audit log filters A 681-724; auditUiVals A 1790-1802; inRange, aFiltered, _auditEmpty A 1836-1841 A static search "Search person or entity" and a static "Last 7 days" chip A live search (36px, flex: 1 1 260px, max 320) "Search person, action or entity" with aria-label="Search audit log" and a clear button, matching actor, action and entity; a "Date range" menu button (calendar, the label, chevron, --shadow): Today, Last 7 days, Last 30 days, All time, and "Custom range…" (calendar-range) opening a dialog From / To (date fields, "Must be on or after the start date") whose label reads "{d Mon} – {d Mon}"; "Reset" while a search or a non-default range is set; the empty row "No entries match these filters."; the pager counts the filtered rows T-R5.12 (range=7d|30d|year|all), T-R5.26 ("a range select … in place of the deck's static date button"; the search is "the deck's "Search person or entity""), DG-19 R5: the contract's range set and the control change S (R5) R5 (T-D4.19 corrects T-R5.12, T-R5.26)
A-04 settings nav sticky A 894 (setNavPos) static sticky; top: 16px from 768 T-R5.28a R5 XS (R5) R5 (T-D4.19)
A-05 AI credits page A 1011-1125 (r.ai); aiAdminVals A 1666-1789; AI_POOL 200,000, AI_DAY 24, AI_DAYS 30, AI_TEACHERS, AI_REQ0 A 1550-1559 not drawn Header "AI credits" / "Monthly allowances for AI features. Usage resets on 1 October." with "Add credits" (a dialog: Amount 50,000 / 100,000 / 250,000; "Added to the centre pool right away and billed on your next invoice. Added credits don’t carry over to next month."). A pool card: "Centre pool · September" and "Centre plan 200,000 (+ {n} added) · resets 1 October"; "{used}" (28px) "of {pool} credits used"; a 10px bar (accent; warning from 75%; danger from 90%) with a 2px projection tick ("Projected by 30 September"); three facts in a grid: "Left this month" ("{d} days to go"), "On pace for ≈ {n}" ("by 30 September, within the pool", or in warning ink "More than the pool. Add credits or lower allowances."), "Teacher allowances {sum}" ("Can add up to more than the pool, since few teachers use all of theirs"). Request cards (a hand tile in warning tones): "{name} asked for {n} more credits · {used} of {allow} used", the reason and age, "Decline" (toast "{name} will be told the request was declined"), "Approve {n}" (primary; raises the allowance; toast "{name}’s allowance is now {n} this month"). A Teachers card ("{n} teachers used AI this month · 3 haven’t yet"): rows sorted by used / allowance: avatar, name, email, "{used}" and "of {allow} (· custom)" over a 6px bar, a status pill with a dot (On track, Near limit from the warn percent, At limit, Paused), "Edit allowance" (pencil; a dialog "AI allowance · {name}": "{used} credits used so far this month.", Monthly allowance Default · {n} | Custom, Credits per month ("Enter at least 1,000"; "Takes effect right away. Usage so far still counts."), "Pause AI for this teacher" / "AI buttons stay visible but can’t be used until you turn this off."). A Rules card (flex: 1 1 320px, sticky from a content width of 900): "For every teacher without a custom allowance."; Monthly allowance per teacher 20,000 | 50,000 | 100,000 ("Teachers see their balance in Settings → AI assistant."); Warn the teacher at 80% | 90% ("They get a notification and an email."); When a teacher runs out: Pause until next month | Let them ask for more ("Requests appear at the top of this page."); When the centre pool runs out: Pause AI for everyone | Allow 10% extra ("Extra use is billed on the next invoice.") nothing; no Admin console before R5 (T-R4.48 deletes the old AdminLayout, T-R5.20 builds the new one) new (AI); "Allow 10% extra" and the two "billed" sentences contradict D21 and the spec's non-goals (DG-196) server L (D4), web M (R5) T-D4.31 (operations and the maintenance command); R5 T-R5.35 (the page)
A-06 Overview rows A 1770-1773 — An "AI credits" row ("{used} of {pool} this month", accent) appended to the Storage meters and to Plan & usage; an attention item (sparkles, accent tones) "{n} teacher(s) asked for more AI credits" → ai while requests are pending T-R5.18, T-R5.21, T-R5.28b R5 + new XS (R5) R5 (T-D4.19)
A-07 fixtures A 101-124, 1506-1507, 1521-1545, 1779 — Persona renames; the export-all email — nothing — —

9. Student, Landing and Sign in, v2

Fixtures only (S 300-306, 335-341, 596, 855, 865, 1397-1412, 1439, 1748-1752, 1774; Landing 367-401; Sign in 91, 220-221): the teacher is "Giàu Bùi" with no honorific, the admin "Thường Võ", the demo accounts giau.bui@ and thuong.vo@. Nothing to build: the product says "Your teacher" (DG-106) or the display name a teacher sets (T-R4.7).

10. Counts and what they mean for D4

Class Rows Where they go
Reopens R4 V-01, V-02 (nine R4 tasks), V-03 (a check), V-04, V-05, V-08, V-09, V-16; A-01 Section F: T-D4.20 to T-D4.23; the four header buttons with their AI tasks
Inside a screen D4 already reopens V-02's outline and V-09's title bar (T-R4.31a, which T-D4.2a reopens) T-D4.20 runs first on the builder chain; T-D4.2a's comparison includes the scrolling outline
A later release's screen V-14d, V-15, V-17, V-18, V-19; A-02, A-03, A-04, A-06 T-D4.19 corrects 75-r5.md, 78-r8.md, 80-r10.md; nothing is built early (R8's register needs sessions, R10's word list its schema, the Admin page R5's shell)
New capability V-06, V-07, V-08, V-09, V-10, V-11, V-12, V-13, V-14 (five kinds); A-05 Section G: T-D4.30 to T-D4.40
Nothing V-20, V-21, A-07 T-D4.1's README notes

Deck ahead of the engine. Every later-release row is a screen production lacks and will lack when D4 ships; that is the standing state of the deck for R5 to R11, not the case the branch rule guards (a shipped screen redrawn in the next revision). v2 adds thirteen such cases, the R4 rows, all handled as before: the import lands on work/deck-d4 and develop keeps the third import until D4 merges.

11. Contradictions with recorded decisions, and their resolutions

v2 draws Recorded decision Resolution
Cloud AI processing Spec "Changes since v0.7": cloud and private processing "remain candidates … not enabled by default"; 17-word-import.md §8; 18-word-import-ux.md §4 D19 supersedes it for the v2 features; the spec is edited by T-D4.40, in the release that ships the behaviour
"Allow 10% extra"; "Extra use is billed on the next invoice."; "billed on your next invoice" D21 (never overspent without an approval); spec §1.3's non-goal on payments One pool rule, "Pause AI for everyone"; "Add credits" is the approval and records the approver; the copy says the extra is paid for outside Quizzivy (DG-196)
"They get a notification and an email." Email is R7 (D9) In-app in D4; the email channel joins in R7 (T-R7.23 gains ai.allowance_warning)
"A question can have up to 6 options." DG-63: 8 options 8 (a DG-63 note)
"The variant is saved as a draft version" One draft per test (T-R4.16) A new draft test, "{title} · Variant", through the duplicate path (DG-197)
"Merge into one. Tests that used either question now share one copy." No merge operation; frozen copies in published versions (publish_snapshot_test.go); questions.LockForDraftUse Not built; "Open" and "Keep both" (DG-197)
"Add {n} to {section}", "Save as practice test", "Use score and comment" The AI never writes a grade or a question on its own (D19 to D22, AGENTS.md) The primary is the acceptance: the browser then calls the existing writes with the accepted content; the AI module has no content write path
"Scores against the four IELTS criteria." Bands and rubrics are R6 (DG-61, DG-86) D4 suggests a score in the card's set; the band form joins in R6 (T-R6.11 note)
"True / False / Not given" in Generate; "Change to Not given", "Add word limit", a matching finding in Check tfng, maxWords and matching are R6 D4 offers the five stored types and the fixes whose write exists; the rest joins with R6 (T-R6.10 note)
"Students see their own attendance." T-R8.6: no student operation in R8 R8 decides (DG-198)
"They count as absent until you mark them." DG-92 / T-R8.6: unmarked is outside the rate R8 keeps unmarked (DG-198)
No AI row in the roles matrix 70 §4.1: every grantable key is a matrix row ai.use and ai.credits.manage join the catalogue and the matrix; the rows are asked for (DG-199)
No "ask for more credits" control on the teacher side — A request form under the usage card, extrapolated from the Admin card (DG-200)
The fourth export's class fixtures DG-01 Answered (the card shows the full code, link, QR and the share buttons)

Part 1 of 5 of D4 — Deck update: the fourth export and v2 (v0.11.0) · Next


Generated from docs/plan/74d-d4-deck-update.md. Edit that file in the repository; changes made on this page are overwritten.

⚠️ **GitHub.com Fallback** ⚠️