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
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
StudentSectiondescription that T-D4.5 corrects) and T-D4.32 (the AI operations and the two permission keys;permissions.goldenchanges there and nowhere else). T-D4.31's/admin/ai/*operations and T-D4.33'sreadWithAifield ride in T-D4.32's pull request, so the contract changes twice in D4. If another release's contract lane is open ondevelopat the same time, the two take turns, and whoever merges second rerunsmake gen. Exception, recorded by T-D4.40: the contract changed a third time, in T-D4.33 (#570):createWordImportgained a documented422 VALIDATION_FAILEDwithdetails.readWithAi, for an import that asks to be read with AI on a server with no assistant or from a caller withoutai.use. It is a response only; no schema changed, andmake genwas 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-d4and renumbered at adevelopsync that brings a higher number (70 §3). - Removed locale keys and
make genoutput 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_KEYto 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.envand 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()callselement.play()first, with nothing awaited or set before it. T-D4.8 and T-D4.9 keep the order, andaudio-player.test.tsxpasses untouched; - autosave's two flushes (on unmount and before publish):
builder/autosave-unmount.test.tsx,builder/autosave-flush.test.tsx,tests/units/question-groups/*andbuilder-group-recovery.spec.tsfor 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 ofquestionsandmedia(all four carry theintegrationtag, somake test-api-unitruns none of them), and the Go unit tests ofquestions,tests,attemptsandmedia.
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
developat once. T-D4.1 and T-D4.19 depend on nothing and their pull requests targetdevelop. With them change T-D4.1's "Depends on" and its last bullet, the "Branches" paragraph, the decision "The import lands onwork/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.0until the engine chain ships. It returns with its bullet in AGENTS.md. -
The six tasks in
74-r4.mdare 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.mdare 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 ondevelop. The deck of record and the engine reachdeveloptogether.
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.
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.
-
The play-limit lock contradicts a canary, not only the spec.
web/tests/units/media/audio-player.test.tsx:105-112is 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. -
"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.yaml2276-2283). A section's standalone question is a bank reference. No operation turns one into the other. Two levels stay. -
"Partly" is a display state only.
take-test/answered.tsmust 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.gorunsanswered.test.ts's cases). The deck's "one pick of two is partly" needs R6'sselectCount; until then only a fill-in with some blanks typed is partly. -
Counting every resume as a play is a defect today, with or without the deck.
features/media/components/AudioPlayer.tsx:108-110callsonPlayon every start, and both hosts count it. WithmaxPlays2, one pause and resume reads "Played 2/2 times". -
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. -
Three thresholds are new to the engine: 420, 900 and 1180 (and 1000 for R6's matching).
At
fd9c763a,web/tests/support/viewport.tsanswers everymin-widthquery alike. Engine unit tests need the numeric form T-R4.3a adds, which is ondevelopfrom v0.10.0. -
Gap ids were taken when T-D4.1 ran.
developended at DG-181 on 2026-10-12, the R4 rows included, so the new rows are DG-182 to DG-202. -
Migration numbers are taken at merge.
developends at 00079 today. T-R3.1 and T-R3.2 ("numbers from 00080",73-r3.md), R4's migrations and this export'sallowPauseall number above it, in the order they merge. -
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.35and prints the part after " · "). -
One open item of
76-r6.mdis stale. "True / false answers already marked 0":server/internal/modules/attempts/domain/grading.go:100-110already readsoptionIds. -
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.yaml2376-2381: "One explicit playback binding per audio asset used in the group's materials"). A listening group therefore reaches the student withstimuli.length >= 1(the shape isweb/tests/support/groupPreview.ts:41-64), andhasPassageis 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. -
There is one
AudioPolicy, and a group has one shape for reading and writing. It is referenced byAdminQuestion.audio(2031),StudentQuestion.audio(2141),StudentGroupRecording.policy(2175),GroupRecording.policy(2385) andQuestionInput.audio(3124);QuestionGroupBundleis 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. -
A test has no time limit and no integrity policy. Both are the assignment's
(
AssignmentInput.durationMinutes, 3175, 3195;IntegrityPolicy, 2615-2637);previewTestreturnsversion,questions,sectionsandgroupsonly (4391-4398). The deck's preview timer (45:00) has nothing to read in any of the three hosts. -
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. - 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."
| 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 |
| 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.
| 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 |
| 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.
| 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.
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.
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.
| 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 |
| 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 | — | — |
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).
| 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.
| 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.