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

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

Open items

Every question below has the default this file builds, what the other answer costs, and who answers. "Owner" is Thuong; "design" is the design team. Nothing waits for an answer, with two exceptions: the play-limit lock (Q3) is not built without the owner, and T-D4.10 gives the design team two working days on Q6, counted from the day the question is sent, which need not wait for T-D4.1.

Where each item stands (read by T-D4.12 on 2026-10-13). "Built" is the default, or the answer, as the named task shipped it; "left" says who still owes what. The items' text below is unchanged.

Item Stands
Q1, Q2 Decided; nothing to build
Q3 Default built (T-D4.8, DG-182); left with the owner, who may still change §11.4
Q4 Default built (T-D4.13, T-D4.9, T-D4.15, DG-183)
Q5 Default built (T-D4.8, DG-193)
Q6 Answered (#581) and built (T-D4.10a, T-D4.10b, DG-184)
Q7 Default built (T-D4.10a, DG-185); no answer from the design team
Q8 Default built (T-D4.2a, T-D4.14, DG-186); the drag overridden by E1 (v0.11.6, 74e-e1-group-move.md)
Q9, Q10 Default built (T-D4.14, T-D4.2b; the card at its node, T-D4.8; DG-187)
Q11 Default built (T-D4.2c, T-D4.16, DG-188); a running timer would need a field on the test
Q12 Default built (T-D4.4, DG-191)
Q13 Default built (T-D4.7, DG-190)
Q14 Default built (T-D4.5, DG-192)
Q15 Default built (T-D4.5); left with Thuong for the Vietnamese ("Tự luận"), T-D4.12's last box
Q16, Q19 Default built (T-D4.6)
Q17 Default built (T-D4.11), widened to a plain rich paragraph (T-D4.11b)
Q18 Default: not built (DG-189)
Q20, Q21 Default built (T-D4.4, DG-191; the letters on a cloze card, T-D4.10a)
Q22 Default: no cap; R6's selectCount (T-R6.5) decides
Q23, Q24 Default built (T-D4.2c)
Q25 Default built (T-D4.15's one control; T-D4.14's limits and both switches)
Q26 Moot: T-D4.1 imported after v0.10.0, and T-D4.12 recorded each reopened R4 task in 74-r4.md
Q27 Left with the design team's next sync (DG-195)
Q28 Built for D4 (T-D4.31's operations and cmd/maintenance ai-credits); the page is R5's T-R5.35
Q29 T-D4.18 takes v0.11.0
Q30, Q31 Built (T-D4.31: 20,000 credits in 00118_create_ai_settings.sql; one pool rule, DG-196)
Q32 Built (T-D4.34, DG-197)
Q33 Built (T-D4.38's switch, gating T-D4.36 and T-D4.37)
Q34 Built (T-D4.34 to T-D4.39, DG-201)
Q35 Default built (T-D4.33, T-D4.39); two items left with Thuong (70 §10)
D19 to D22 Built (T-D4.30 to T-D4.40)
The documents ahead of R4 Moot: R4 shipped first (Q26)
A newer export, before and after v2 None came before T-D4.1; the design team's later answers are in deck/page-width-proposal/
Q6 and Q7 decided the largest task Built (T-D4.10a, T-D4.10b)
The preview's 45:00 timer Left: needs a time limit on the test (Q11, DG-188)
"Students can pause" in the other documents Done by T-D4.13
Migration numbers Done: assigned at merge (T-D4.13's renumbering to 00114 to 00117)
Line numbers of code A reading note; nothing to build
The design team's next sync and the AI module Left with the design team (DG-199 to DG-201)
The clean-up's word model has no unaccent Left for the live smoke (follow-up 2)
Builders A process note; nothing to build

Decided

Q1. Which phase is "this phase"? Decided by Thuong, 2026-10-04: R4. The deck update is D4, a phase of its own after v0.10.0, and R4 is not amended to the export while it is in flight.

Q2. Do the fixes already on develop wait for the deck update? Decided by Thuong, 2026-10-04: no, and they get no release of their own. They ship with v0.10.0; R2's contract steps ship as T-R4.49 if v0.9.1 has not shipped before R4.

Q6. Is a cloze one question with n gaps, or n questions? Answered by the design team, 2026-10-13 (#581): n questions, one per gap, shown as one screen (T-D4.10). The question and its default stay below, under "Blocking, or close to it", with the answer.

D19 to D22 (D-AI-1 to D-AI-4). The AI assistant. Decided by Thuong, 2026-10-10, recorded in 70-redesign-overview.md §1: Anthropic's API for every AI feature, cloud processing approved and on, the adapter behind a module port that answers 501 with the key unset (D19); data minimisation per feature, no personal identifier to the provider unless a feature cannot work without it, and none does (D20); a $200 monthly pool, exceeded only when an Admin approves extra credit that Thuong pays for, a hard stop, a credit unit metered from reported usage (D21); built in D4, the release after R4 (D22). The spec records them in the task that ships the behaviour (T-D4.40, which wrote it as spec v0.99), not on the planning branch.

Blocking, or close to it

Q3. May a recording lock at its play limit? (owner, then design)

  • The deck: lock icon, "No plays left", "You have used all {N} plays", and the button does nothing (S 1677-1681).
  • Against it: spec §11.4 ("Over-limit plays are reported to the teacher, not enforced"), the contract (AudioPolicy.maxPlays: "never blocked"; recordGroupAudioPlay: "never block playback or submission"), AGENTS.md ("the teacher judges; the app reports"), and the canary case "still plays when the hint says none are left" (audio-player.test.tsx:105-112).
  • Default: the decision stands. The new card is built; at the limit the button stays live and the status adds the product's "You can keep listening. Additional plays are recorded for your teacher to review." (DG-182).
  • Cost of the other answer: the owner changes §11.4 and that canary case himself; a student whose play was counted twice, or counted while offline, is locked out of a recording they did not hear, and only a teacher's intervention reopens it. The lock in the browser alone is S; a limit the server enforces is a new refusal on two operations.

Q4. "Students can pause": build it, and as what? (owner, then design)

  • Now drawn on the builder's question media, on Shared content, and acted on by the Student page. The intro fixture says "you can pause once"; the script keeps an unread pauses count.
  • Default: one boolean, allowPause, on by default, on a question's audio and on a group's recording (T-D4.13, T-D4.9, T-D4.15). When off, the button is inert while playing; if playback stops anyway (a headset button, leaving the group, a lock) the student resumes without spending a play, and nothing new is recorded. A save that does not send the field (a tab loaded before the release, or a draft recovered from the device) keeps the stored value, so it cannot switch pausing back on.
  • When: T-D4.13 is D4's server task. It builds the default if no answer has come when it starts, and T-D4.15 adds the switch R4 left out (T-R4.65).
  • Cost of "do not build": T-D4.13 and T-D4.9 go, T-D4.15 keeps only its "Plays" bullet and its comparison, DG-111's default stands, and the deck's switch is left off two teacher surfaces. Cost of "a number of pauses": a counter the server must own as it owns plays (a column, a ledger, an operation with its permission and isolation entry), L, and a student rule nobody has drawn.

Q5. What spends a play? (owner)

  • Today every press of play counts, a resume included, so one pause costs a play (AudioPlayer.tsx:108-110). The deck spends one only on a start from 0:00.
  • Default: a start with no play in progress spends one; a resume spends none; a play in progress and its position are remembered while the tab lives, so coming back to a group resumes; a reload forgets, and the next start counts. If the place cannot be put back (the browser refuses, or the file's length never loads), that start counts as a play: with seeking off the player undoes any jump (AudioPlayer.tsx:240-247), and a restore that failed silently would be a replay from 0:00 that costs nothing. Proven in a browser, not in jsdom (T-D4.8). With no limit the counter reads "Play {n}" (the deck's preview reads "Play 1 of 99"). Spec §11.3 and §11.4 change with it.
  • Cost of the other answer: keeping today's rule makes the drawn "Paused · Play 1 of 2" false and keeps the defect. Surviving a reload needs the server to know a play is in progress: a contract change, M, in a do-not-guess area.

Q6. Is a cloze one question with n gaps, or n questions? (design, then owner)

  • Answered by the design team on 2026-10-13 (#581): the default, one question per gap, shown as one screen. The drawing is docs/design/deck/page-width-proposal/Cloze Decision.dc.html; its eight additions are listed under T-D4.10. No data, contract or Frozen item changes.
  • Student page: one item, "Question 23 of 31 · Cloze · 5 gaps", one square, sub-ids 23.1 to 23.5. Teacher page: "Each gap is answered by a single-choice question in this group", which is the product's model. The builder's own preview never passes a gap to the Student page. The deck of record drew the product's model once on a student-eye surface, in the import preview (a passage with gap chips, then "23.1 Choose the word for gap 23.1." with its own options); this export removed that preview (T 4975-4980).
  • Default: one question per gap, each with its number, square, flag, points and result row. The engine shows a run of them as one screen, "Questions 23–27 of 34 · Cloze · 5 gaps" on the fixture paper (35 once R6 adds the matching), with the gap strip and the gap card as drawn (T-D4.10). No contract change.
  • Every member is a full question with its own required prompt, and may have media and its own points. So: a prompt all members share is printed once, as the deck draws its one prompt; otherwise the open gap's card shows that member's own prompt; the card always ends with that member's worth; a member that carries an image or audio is not part of the run and is shown as a question of its own.
  • Cost of the other answer: a new question type with its own answer shape, marking, result, authoring and import mapping, across every high-risk area: an R6-sized track. Tests already published with gap-bound members would keep the old shape beside it.

Q7. How is a multi-part question numbered and counted? (design)

  • One matching is "Questions 1–5" and counts five (s2); another is "31" and counts one (s9); the cloze counts one; the Teacher import counts the same paper as 35.
  • Default: every question has one number and counts once. A matching (R6) is one question, "Partly" until every item is matched.
  • Cost of the other answer: a display label per question in the contract, and the count changing on Home, the intro, the Submit dialog, the roster and the result together.

Q8. Is the builder's "group" now the shared-context group? (owner, then design)

  • The deck puts "Shared content" on every outline group, which R4's plan mapped to the product's Section.
  • Default: two levels stay. A Section keeps Rename, Instructions, Move, Remove. "Shared content" and its outline line belong to a shared-context group's row inside a section (T-D4.2a, T-D4.14). A bank question cannot be dragged into a shared group. 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.
  • Cost of the literal answer: a section-wide group and two new operations that turn a bank question into a group member and back, through the draft lock, the publish snapshot and the deal order: L, high risk, with permissions and isolation cases. A middle answer, "Shared content" on a section's menu creating a group in it, is S more in T-D4.14, but its sentence "shown beside every question in this group" would be untrue of the section's other questions.

Design decisions with a default

Q9. One passage or one recording per group? (design)

  • In the product a recording is always an audio node inside a material (the server refuses one without the other), so "Audio" in the dialog stores a material holding that one node, named after the file; the student never sees that name.
  • Default: the dialog edits a group with at most one material and one recording of one kind. A group that holds more (imports, bank copies: up to 16 of each, or a material that mixes text and audio) opens the full group pane, and nothing is dropped. On the student side a recording's card stands where its audio node is: a material that is only a recording shows the kicker and the card, with no heading, as drawn; a material with text and audio keeps its heading and has the card in reading order; several recordings each stand at their own node, and only then is each labelled. None of the last two is drawn.
  • Cost of the other answer: a dialog that is the only editor would lose content on save, or needs a drawing for several materials.

Q10. The passage: a plain textarea, or the content editor? (design)

  • Default: the content editor (T-R4.63's document profile) with the deck's labels and placeholders, a gap inserted at the caret, targets limited to choice questions and blanks, each used once, and no save with an unbound gap. The editor's toolbar already has "Insert gap", so the deck's separate button is not built and the dialog has one, not two. The dialog saves through the group's own editor, so its autosave, its recovery draft and its conflict message stay one mechanism.
  • One English sentence is reworded: the deck's "Students see it on the left while they answer." becomes "Students see it beside the questions while they answer.", because on a phone it is a drawer (DG-187).
  • Cost of the other answer: stored passages hold tables, images and gaps a textarea cannot show; the deck's looser rules ("[gap n]" at the end, any question, unbound gaps saved) are refused by the server today (group_validation.go:176-212).

Q11. Is the preview meant to be answered, with a running timer? (owner, then design)

  • A test has no time limit: durationMinutes belongs to the assignment, and neither previewTest, the builder draft nor the import's review draft carries one. The same holds for the integrity policy. So the deck's 45:00 has nothing to read, running or not.
  • Default: answerable, kept only while it is open; no saving, no integrity record, no play counting, pasting allowed; "Student view" chip; Finish says "This is a preview. Students submit here." In the timer's place, a pill of the same size reading "––:––", named "The time limit is set when the test is assigned"; in the save line's place, "Answers here are not saved." (the deck's preview still says "All answers saved"). T-D4.2c builds the bar and the frame around R4's read-only preview first (DG-188).
  • Cost of the other answer: read-only saves little, because the engine must run from a source other than the store either way. A timer with a number needs an invented limit, or a "typical time" field on the test, which nothing else uses.

Q12. The sections rail returns and the footer strip goes: intended? (design; owner informed, since it reverses what shipped today)

  • Default: as drawn. The rail from 900 with no shared content and from 1180 with it, so it comes and goes between questions from 900 to 1179; the grid button and its sheet everywhere else.
  • Cost of the other answer: keeping R3's strip turns T-D4.4 from L to S (the third state and the label), and stays a recorded departure.

Q13. The phone drawer: is swipe required, and may the recording be out of sight while a student answers? (design)

  • Default: the handle and the close button are the controls and swipe is an extra; focus, Esc, inert (the deck marks the closed drawer aria-hidden, which leaves its gap buttons reachable by Tab), reduced motion and a once-per-group tip are added (DG-190). The handle is named "Open the audio" in English, as the deck has it. The handle's dot is the only sign of a playing recording on the question.
  • Cost of the other answer: keeping the switcher drops T-D4.7 (L) and leaves a recording on a phone with no drawing. A small player on the question needs one.

Q14. Section instructions above every question? (design)

  • Default: yes, with the section's title, except on a one-section paper with no instructions. A group's own instructions stay under the stimulus title. The builder's dialog sentence becomes "Students see this above every question in the section."
  • Cost of the other answer: none; today's first-question note stays and S-09 is a departure.

Q15. The engine's type labels become the builder's names? (owner, for the Vietnamese)

  • Default: "Single choice", "Multiple choice", "True / False", "Fill in the blank", "Short answer", with the same Vietnamese names as the builder's type menu.
  • Cost of the other answer: none; the labels stay instructions ("Choose one").

Q16. Options in columns: what counts as short? (design)

  • Default: every option plain text of at most 16 characters, and no shared content; two columns on a phone, up to four from 768. Rich option content forces one column.
  • Cost of the other answer: one column always, as built.

Q17. A choice sentence with a gap that fills with the chosen option: what marks the gap? (design)

  • Default: a plain one-line prompt with exactly one run of three or more underscores, on a single-choice or true/false question (T-D4.11), or a rich prompt that is one paragraph of unmarked text meeting the same test (T-D4.11b). Anything else prints as written.
  • Cost of the other answer: T-D4.11 is dropped, or a field marks the gap (a contract change).

Q18. Sign cards, 60px word tiles, the hint line and the image caption: what in the data triggers each? (design)

  • All four are keyed to fixture ids or fixture fields with no model.
  • Default: none is built (DG-189). A notice is an image or bold prompt content; an underline comes from the option's rich text.
  • Cost of the other answer: a notice block in the content model (contract, editor, engine, result), an option-size rule, a hint field and a caption field: R6 work.

Q19. The fill-in field: 340px and 38px everywhere? (design)

  • Default: the underline style everywhere; 340px wide only when the question has one blank, 128px otherwise and in tables; 38px from 1024 and 44px below. The case-rule and worth lines stay.

Q20. The key hint names "← →" and "A–D" only. (design)

  • Default: printed as drawn, without "A–D" on a question that has no options; A to E and F keep working; on the cloze card the letters choose for the open gap (in the deck they do nothing there).

Q21. The sheet has no title, and four targets are under 44px on a phone. (design)

  • Default: the name "Questions" and the total stay for a screen reader only; the sheet's squares and a blank are 44px below 1024; the 34px rail squares (never below 900) and the 30px drawer handle are as drawn.

Q22. Multiple choice: capped at two, or "Choose all the answers that are correct"? (design)

  • The Student script caps it; the Teacher preview's hint says the opposite (T 6907).
  • Default: no cap until R6's selectCount; then R6's rule.

Q23. Test detail's compare mode lost its outlines and "New" / "Changed" chips. (design)

  • Default: followed. The "Changes from version {n}" list stays above the frame; nothing is marked inside it.

Q24. The import's preview lost "Showing 7 of 34 questions. Answers and your notes are hidden from students." (design)

  • Default: the whole draft is previewed and the sentence goes. The deck shows a Student fixture there, not the import.

Q25. "Plays" is drawn three ways; the audio line reads "MP3, M4A or WAV up to 25 MB" a third time; the dialog has no "Show transcript after submitting" and no "Allow skipping ahead". (design)

  • Default: one control everywhere, T-R4.65's "Once | Twice | Unlimited" with a stored other value as a fourth option; DG-63's limits (MP3 or M4A, 50 MB, 5 minutes); both switches kept, and "Students see it after they submit." shown only while the first is on.

Process

Q26. Import now, and amend R4's six tasks now? (owner)

  • Decided for the import and for every build task: not while R4 is in flight. The export is imported by T-D4.1, after v0.10.0.
  • Still asked: whether the documents alone (the import, and the amendment of T-R4.31a, 31b, 30, 39, 34 and 65) may go ahead before those six are built. This file is written for "no", and its head says what changes on a "yes".
  • Cost of "no": those six are built to drawings the design team has replaced (the Previous / Next preview, the "New" / "Changed" chips, "Showing k of n", an outline without the shared-content line), then redone by T-D4.2a, T-D4.2b, T-D4.2c and T-D4.15.

Q27. For the design team: what the next sync should read.

  • This export's sync read migrations/00037 and 00002 only (github.md). Pause is in no migration; allow_seek, show_transcript_after_submit and the blank gap kind are in 00037 and were not drawn; the contract and the engine were not read.
  • Request: read api/openapi.yaml (QuestionGroup, StudentGroup, AudioPolicy), web/src/features/take-test/** and web/src/features/question-groups/**, and add them to the Screen map. Recorded as DG-195.
  • Inconsistencies inside the export to send back with it: the cloze counts one and the listening matching five; the builder binds a question per gap and the Student fixture is one item; multiple choice is capped at two under a hint that says "all"; "you can pause once" beside a boolean; the hint says "A–D to choose" on a card where the letters do nothing; previewExam passes no gaps, media or rich content; hasRef, dotsMode, hasPaneTabs and isSign are dead branches, examBold is a dead field, and wi.preview, td.preview, td.pvFoot and the two pvOptCols are dead values on the Teacher page; the arrow keys bypass go(), so they do not close the drawer or the sheet and do not pause a recording; Esc does not close the new sheet; the preview's header says "All answers saved"; the preview runs a 45:00 timer for a test that has no time limit; the handle says "Open the audio" beside a tip that says "The recording is here."; the dialog's "on the left" is not true on a phone. From v2: the pool rule "Allow 10% extra" and the two "billed" sentences; a variant "saved as a draft version"; "Merge into one"; "up to 6 options"; "Students see their own attendance" and "They count as absent until you mark them"; a matrix with no AI row; a request inbox with no request form; the AI panel without Esc, a focus trap or an off, paused or failed state; the demo output shown as if it were a result.

v2 (2026-10-10): Thuong's decisions

Asked on 2026-10-10 with the Principal's recommendations; Thuong answered the same day ("good catch; I agree with the proposals above"). Q28 to Q34 are Answered 2026-10-10, each as recommended; Q35 is answered for its default and stays open for two items.

Q28. Where does "AI credits" live in D4, when the Admin console is R5's? (owner)

  • T-R4.48 deletes the old AdminLayout; T-R5.20 builds the new one in R5.
  • Answered 2026-10-10: (a). D4 ships the server side in full (/admin/ai/* under ai.credits.manage: the pool, the rules, the allowances, the requests) and cmd/maintenance ai-credits for Thuong, in the manner of DG-42's maintenance windows; the page is R5's new T-R5.35, drawn against those operations.
  • Not taken: pulling T-R5.20 (M) into D4 and building T-R5.35 (M) there (2M in D4 and a one-item console for a release).

Q29. Is D4 v0.11.0? (owner)

  • Answered 2026-10-10: (a), v0.11.0. A release with a new module, nine operations, two permission keys and twelve migrations is a minor by 70 §3's own rule; R5 to R10 move one minor each and R11 stays v1.0.0. 70 §2 carries the new versions; the release files' titles take them in T-D4.18.
  • Not taken: v0.10.1, the first minor-sized release labelled a patch.

Q30. The default per-teacher allowance. (owner)

  • Answered 2026-10-10: (a), 20,000 credits ($20) a month, the deck's lowest rule value; ten teachers fill the pool exactly, and a month of ordinary use is about 2,300 credits a teacher (section G). The rule still offers 50,000 and 100,000; changing the default is one row in ai_settings.

Q31. The pool's rule. (owner; follows D21)

  • Answered 2026-10-10: the default. One value, "Pause AI for everyone"; "Add credits" is the approval (DG-196). The deck's "Allow 10% extra" contradicts D21 and is not offered.

Q32. A variant and a merge. (owner, then design)

  • Answered 2026-10-10: (a). A variant is a new draft test; the clean-up merges nothing (DG-197). Not taken: a variant that overwrites the test's draft, or a merge operation (L, high risk: the publish snapshot and the draft lock).

Q33. "Let AI read student answers": a per-teacher switch, on by default? (owner)

  • Answered 2026-10-10: (a), as drawn. D20 is satisfied either way (no identifier leaves); the switch gates Suggest a score and Explain results.

Q34. Paused or out of credits: visible and disabled, or hidden? (owner)

  • Answered 2026-10-10: (a). Visible and disabled with the reason, as the deck says ("AI buttons stay visible but can’t be used"); hidden only when the provider is off (DG-201).

Q35. What students and parents read about cloud processing. (owner; product copy about minors' data)

  • Answered 2026-10-10: (a) for its default. The import page's privacy callout says what left the system (T-D4.33), and the centre's privacy notice (R11, D18) names the provider.
  • Still open, two items: the student-facing sentence on the Test intro waits for Thuong's wording and is not built in D4 until he gives it; and the provider account's data-retention setting is his to confirm. 70 §10 keeps both.

The gap rows T-D4.1 opens

Twenty-one new rows in docs/design/gaps.md, all Open: DG-182 to DG-195 for the fourth export and DG-196 to DG-202 for v2. DG-182 moves to "Decisions that override the current drawings" only if the owner confirms Q3's default. T-D4.1 took the ids on 2026-10-12, the first one being the next free id after develop's DG-181. If a develop sync later brings a row with the same id, the D4 row is renumbered at that sync. The full "Meanwhile" texts are in gaps.md.

ID Where What the row records Question Built by
DG-182 Student › Take test, a recording at its play limit The deck locks the recording; the product keeps it live and reports extra plays Q3 T-D4.8
DG-183 Teacher › Question media and Shared content; Student › the recording card "Students can pause" as one boolean, allowPause; "you can pause once" is read as a note Q4 T-D4.13, T-D4.9, T-D4.15
DG-184 Student › Take test and Result, a cloze One item with n gaps on the Student page, one question per gap in the builder; the product keeps one question per gap (Q6 answered, #581) Q6 T-D4.10a, T-D4.10b
DG-185 Student › Take test, numbers and counts "Question 23 of 31" against 35; a matching counts five or one; every question has one number Q7 T-D4.10a
DG-186 Teacher › Test builder, the group The deck's group carries shared content; the product keeps a section and a shared-context group. A question is dragged into and out of a group since E1 (v0.11.6, 74e-e1-group-move.md) Q8 T-D4.2a, T-D4.14
DG-187 Teacher › Test builder, the Shared content dialog A textarea, one passage or one recording, loose gap rules, "MP3, M4A or WAV up to 25 MB", two switches missing; "on the left" reworded Q9, Q10, Q25 T-D4.2b, T-D4.14
DG-188 Teacher › the three previews An answerable take-test screen with a running 45:00 timer and "All answers saved"; the product shows no time and claims no save Q11 T-D4.2c, T-D4.16
DG-189 Student › Take test, frames with no model The sign card, the 60px word tiles, the image caption, the per-question hint, paragraph letters Q18 not built
DG-190 Student › Take test, the phone drawer No Esc, focus move, inert, reduced-motion form or tip rule; the recording is out of sight while answering Q13 T-D4.7
DG-191 Student › Take test, navigation The rail comes and goes from 900 to 1179; the key hint; a sheet with no title; four targets under 44px; the arrow keys bypass go(); Esc does not close the sheet Q12, Q20, Q21 T-D4.4
DG-192 Student › Take test, section instructions Shown above every question, while the builder says "above the group’s first question"; a group's own instructions are not drawn Q14 T-D4.5
DG-193 Student › Take test, what spends a play A start spends one and a resume none; unlimited plays read "Play 1 of 99" Q5 T-D4.8
DG-194 Student › Take test, matching Chips per row, three key styles, the nouns "pick the {pick} for each {what}", 38px chips Q7 R6 (T-D4.19)
DG-195 github.md, the sync behind the export The sync read two migrations; the contract, the engine and the group editor were not read Q27 the design team's next sync
DG-196 Admin › AI credits, the pool's rules "Allow 10% extra" and "billed on your next invoice" against D21 and the non-goal on payments; one rule, pause everyone; "Add credits" is the approval Q31 T-D4.31, R5 T-R5.35
DG-197 Teacher › Make a variant; Clean up the bank A variant "saved as a draft version"; "Merge into one"; the product makes a new draft test and merges nothing Q32 T-D4.34
DG-198 Teacher › Class detail › Attendance (R8) "Students see their own attendance"; "They count as absent until you mark them"; R8's decisions stand — (R8) R8
DG-199 Admin › Roles & permissions No AI row; two keys join the matrix — T-D4.32, R5 T-R5.24
DG-200 Teacher › Settings › AI assistant No request form while the Admin page lists requests; a form under the usage card — T-D4.38
DG-201 Teacher › the AI panel's states Off, paused, exhausted, failed and invalid states; Esc, focus trap and return Q34 T-D4.34
DG-202 Teacher › the AI against R6 The band form, TFNG, the word-limit fix and the matching finding wait for R6 — T-D4.34, T-D4.36, R6

T-D4.1 also edits sixteen existing rows: DG-80 (what the export now draws, and which frames stay listed), DG-60, DG-63, DG-68, DG-69, DG-111, DG-113, DG-115, DG-116 and DG-36 for the fourth export; DG-01 (Answered), DG-22, DG-71, DG-106, DG-100 and DG-63 (a second note) for v2.

Other open items

  • The documents ahead of R4. The lead has asked Thuong whether the import and the amendment of six R4 tasks may go ahead before those tasks are built. This file's head says what changes on a yes.
  • A newer export. D4 starts weeks after the export it describes. If the design team sends another before T-D4.1 runs, the inventory is redone against it and this file is corrected before any task starts.
  • Q6 and Q7 decided the largest task. The design team answered Q6 with the default on 2026-10-13 (#581), so T-D4.10 builds it (T-D4.10a, T-D4.10b) and no R6-sized cloze track exists. Q7 has no answer; its default stands.
  • The preview's 45:00 timer. A test has no time limit until it is assigned, so no answer from the design team alone makes the deck's timer buildable; it needs a field on the test (Q11).
  • "Students can pause" in the other documents. 70-redesign-overview.md §10 said it was unbuilt until D4, and 74-r4.md's open items said R4 does not build it. T-D4.13 corrected both (they now name allowPause, the task and DG-183). If the owner declines Q4, 70 §10 records the D1 exception instead.
  • Migration numbers. T-R4.49 (R2's contract steps), R4's lane and T-D4.13 all number above develop's last file. Numbers are assigned at merge.
  • Line numbers of code. They are those of develop at fd9c763a. The fixes for #315 and #320 have moved the contract and TakeTestPage.tsx since, and R4 rewrites the builder, StudentPreview and the media block, so a D4 builder finds each place by the identifier quoted beside the number.
  • A newer export after v2. This file now describes v2 (26b60795). If the design team sends another before T-D4.1 runs, it is read against sections 1 to 11 first and this file is corrected before a task starts, as before.
  • The design team's next sync should read server/internal/modules/ai/ and web/src/features/ai/ once they exist, and draw the states DG-201 lists and the rows DG-199 and DG-200 ask for.
  • The bank clean-up's word model has no unaccent. Duplicate candidates come from the words of the prompts as typed (T-D4.32 follow-up 1), so a prompt without its diacritics is not paired with its accented twin, and a rewording that shares few words is not found either. Watch it in the live smoke (follow-up 2) before building anything.
  • Builders. The engine chain takes one builder for its whole length and, at times, a second for T-D4.6 or T-D4.11; the server task, the builder chain and the preview chain take one each. Never more than four work at once (70 §3).

Follow-ups from the built tasks

Recorded by T-D4.40 on 2026-10-13 from the "As built" notes and the reviews of T-D4.7 to T-D4.11 and T-D4.30 to T-D4.33. None blocks a task or the release; each says where it would be done. None is built here. T-D4.12 read every item on 2026-10-13: 8 is built, and 2, 3 and 6 are left open, each with its reason. Item 8 came from T-D4.10a, items 9 and 10 from the Tech Lead's reviews of T-D4.36 and T-D4.39, and item 11 from T-D4.12's browser verification.

  1. The chunked bank clean-up. T-D4.32 built cleanUpQuestionBank as one bounded call on the fast tier: at most 2,000 of the caller's own questions and 300,000 characters, at most 20 duplicate pairs, tags for the first 60 untagged, and truncated when the bank held more (the panel says "Apply these and run it again"). The plan sketched chunks, with duplicates found across chunks, inside a 60-second budget. That pass is not built: it needs a way to compare questions that are never in the same chunk, and an answer that continues. Default: the one call stands. Revisit after the owner has seen a real bank run (item 2).

    As built (T-D4.32 follow-up 1): the contract's shapes and truncated's meaning did not change; two descriptions in api/openapi.yaml that stated the old bounds were reworded (its own commit and its own make gen commit, and the generated diff is comments and the embedded spec). The answer "continues" the way it did, by truncated and a second run; what changed is how much one run reaches.

    The pass. CleanupHandler reads the bank (Own, at most 2,000), plans its calls from what it reads, reserves FeatureCleanup.Ceiling() (200, unchanged) once, makes every call at the same time (at most five: up to four tagging calls and one that judges duplicates) under a 45-second context (Service.CleanupBudget), and ends with one Finish. The settlement and the read of what is left share one 10-second deadline of their own, so a pass ends inside 55 seconds, under the 60-second idle timeout of Fly's proxy, which would answer 502 for a request that was already settled. Tags are asked of the first 200 questions that have none (the contract's own maxItems), 50 to a call so each answer fits MaxTokens 3,000, and each call carries the 30 tags the bank uses most so a suggestion reuses them. A tagging call sends only its 50 questions.

    Duplicates across chunks. Chunks never meet, so no chunk is asked for duplicates. Every pair of the bank is compared locally by the distinct words of its prompts (domain.SimilarQuestions: Jaccard at least 0.3 over word ids, through an inverted index of the words that are in few prompts; only the first 200 distinct words of a prompt count; a prompt of fewer than three words and a stem repeated more than eight times are left out; a question is in at most three pairs; a work limit of 100 million merge steps stops the search, and then truncated is true: 0.7 s on a bank built to exhaust it, 0.4 s for 2,000 prompts of forty words from a skewed vocabulary, 0.1 s for fifteen words), and one call judges the closest 100 pairs, sending only the questions in them, each under its reference in the whole bank, and keeping at most 20. That call is cut to the best pairs whose text fits MaxCleanupBytes (100,000 bytes, a token being at least a byte, so the price step holds in any language): prompts.FitDuplicates keeps the longest prefix of the ranked pairs, and truncated is true when it dropped any. The prompt clips at 160 runes and a tag at 40, so a bank of 3-byte letters at those lengths fills 200 questions with more than 200,000 bytes and keeps about half the pairs; the pass's own choice of pairs is never a 422. A tagging call (50 lines and a vocabulary of 30 tags) stays under 40,000 bytes whatever the letters. Chosen over a per-chunk model pass because that pass sees only the pairs inside one chunk, so a pair whose halves are in two chunks needs a further round over what every chunk passes on, costs a call for every chunk, and sends the whole bank; this one sends the candidates only, costs one call, finds a pair wherever its halves sit, and costs nothing when no pair looks alike. Its limit is the price: a rewording that shares few words with the original (the deck's "Summarise the chart…" against "Write a summary of the chart…", about 0.27) is not a candidate; the deck's first pair (about 0.46) is. The word model has no unaccent: a prompt typed without its diacritics ("Tai sao bau troi…") is never paired with its accented twin, which a bank imported from several sources may well hold; the live smoke (item 2) is where to see whether it matters. Embeddings, or app.immutable_unaccent in the words, are the next step if a real bank shows misses; the port has none.

    One reservation, one settlement. session (in query/ask.go) reserves, runs, and ends from a defer with Finish(Tally.Response()). command.Tally sums the usage of the calls that command.Billed says were billed (the rule Finish also applies: a completion, a refusal, an answer that was not an object), joins their request ids (the ledger keeps 200 characters, so ids that do not fit whole are left out of the row; the pass logs them all), and a call that billed nothing adds nothing. So a failed call in the middle settles what the others cost and releases the rest, a cancelled caller (Finish runs on context.WithoutCancel) and a panic in any call do the same (the panic is recovered in its goroutine, the other calls finish, and it is raised again once, so the reservation is ended once and the process's recoverer still sees it). ask is a session of one call, so the seven single-call features reserve and finish through the same code. The ledger gets one row for a pass. The tier has one source, prompts.CleanupTier, which the prompts carry, the fit reads from each prompt and the session takes from the plan. A call is planned only if its worst case at the tier's configured prices (Price.Bound: its bytes as tokens, a token being at least a byte, plus 1,000 for the provider's framing, plus MaxTokens) fits what is left of the reservation; calls that do not are dropped, last first, and truncated is true. A pass whose first call does not fit answers unavailable before it reserves, logged at error, since the prices are configuration.

    What the answer does. A call that fails, is cut by the time or does not fit leaves what the other calls found in a 200 with truncated true and a charge for what was billed; it answers 503 only when no call gave a usable answer (a pass whose refusals were billed still settles them). A tidy bank (nothing to tag, no candidate pair) makes no call but reserves and releases, so a paused or exhausted teacher is refused as for any AI operation. Scanned is the questions read.

    A deploy while a pass runs (the server waits 15 seconds to shut down, a pass can take 45) cuts the request before Finish; the 200 credits stay held until the sweep returns them once the reservation is ten minutes old, which it does at start-up and hourly, so for at most an hour and ten minutes (docs/setup/operations.md).

    Not done: the live smoke on a real bank (item 2), which also tells whether 0.3 and 100 candidates are right and whether the missing unaccent matters. An estimate, not a metering: a full pass at the plan's indicative fast prices is about 6 credits, against the ceiling of 200 and the sketch's ≈ 13.

  2. The live smokes of T-D4.32 and T-D4.33. Both need the owner's key, so both "Done when" boxes stay open: one generation, one check, one score suggestion, and one two-page scan read for real, with the metered credits and the ledger rows pasted into the release pull request (never the key or a response). The procedure is docs/setup/operations.md, "What the assistant logs, and the live smoke". It runs once, at the release, with the exit criteria's ledger read-back.

    Left open (T-D4.12): it needs the owner's key, which no branch and no CI run holds; it runs once at the release (T-D4.18), and T-D4.12's live-smoke box stays open with it.

  3. T-D4.8's "Finished" copy after a reload in the middle of a play. A play in progress at a reload costs one play and the release notes say so. The card that comes back then has a count above zero and no play in progress, which phaseOf reads as a finished play: a full track, the button "Play again" and the status "Finished" ("Finished · {n} plays left" under a limit), though the student may not have heard the end. The number is right; the word may not be. Default: as built. Ask the design team whether a recording with a count and no remembered place should read "Played" instead.

    Left open (T-D4.12): a design question, so nothing changes until the design team answers. Brief for the design team. A recording with a play limit is reloaded while it plays. The play counts (spec §11.4: a play is spent when it starts, and the server is told then), and the place it reached is kept in memory only, so after the reload the card knows the count and nothing else. It then looks exactly like a recording that played to its end: the track full, "Play again", and "Finished · 1 play left" (or "Finished" with no limit, "You have used all 2 plays." at the limit). The student may not have heard the end, so "Finished" may say more than the product knows. Please draw the card for "played, end not seen": for example "Played · 1 play left" with an empty track and "Play again", or confirm that "Finished" stands for both. Whatever is drawn, the button stays live and a new start counts as a play (DG-182, DG-193); telling the two apart in the browser (an end remembered in the tab's session storage) needs no contract change.

  4. The spec still describes the pane switcher T-D4.7 replaced with the drawer. Three places say "Ngữ liệu | Câu n" or "the pane switcher": §7.3's paragraph on learner material, §9's "Panes" bullet and §12's "One breakpoint, 768px" bullet. T-D4.12 already rewrites §9 and §12 for the rail and the sheet (its box on S-05, S-06 and S-08); it replaces these sentences too, and §7.3's, and the engine paragraph of AGENTS.md, which says "a switcher shows one pane at a time". T-D4.7 did not touch the spec.

    As built (D4 follow-ups): spec v0.100 rewrites the three sentences to T-D4.7's drawer; AGENTS.md's engine paragraph stays with T-D4.12.

  5. An E2E for T-D4.9's inert Pause button. T-D4.9 is proved in jsdom (audio-player-pause, group-context, question-audio-events) and by the comparison with the deck; no Playwright case presses the inert button in a real browser. Add one in audio-events.spec.ts or the phone spec: start s2 part 2's recording, click the button with force: true (Playwright counts an aria-disabled button as not enabled), and assert the recording is still playing and the play count has not moved. S, web; T-D4.12.

    As built (D4 follow-ups): audio-events.spec.ts presses a question recording's inert Pause with Space and Enter (lint refuses force: true) and finds it playing on, its line shown, one play.

  6. A real-Android check of T-D4.7's tap after a swipe. The drawer swallows the click a swipe leaves behind for 60 ms and never swallows a tap that follows a scroll. That is proved with real touches from CDP in mobile-chromium and in jsdom, not on a real Android phone, whose touch timing differs from the emulator's. Add to T-D4.12's real-phone box: swipe the drawer open and closed, then tap an option at once, and see that the option is chosen and the drawer does not move.

    Left open (T-D4.12): it needs a real Android phone, which neither the documents half nor CI has; T-D4.12's As built lists it with the real-phone box, which the tester runs.

  7. T-D4.11's gap rule does not reach a prompt made in the product. The sentence gap fired only for a prompt with no promptContent, and the builder's prompt field and the import both write promptContent, so on content made today it fired only for a prompt kept in Markdown (T-D4.11 "As built", #571). The Tech Lead widens the rule to a rich prompt that is one plain paragraph (with the run of underscores in its text and no mark, gap or media): T-D4.11b. It changes Q17's default, the refusal "a rich prompt" in sentence-gap.test.ts, and the comparison with s9 questions 5 and 12.

    As built (T-D4.11b): sentenceGap accepts promptContent when it is valid question prompt content (isQuestionPromptContent), its plain projection (contentPlainText) is exactly the stored prompt, and it is one paragraph block whose children are all text nodes with no marks; adjacent unmarked text nodes count as one text. The prompt then goes through T-D4.11's test unchanged (trim, NFC, one line, one run, no other marker), so a * or _ typed as plain rich text still refuses the gap. Anything else prints as today: a mark, a link, a line break, a gap node, a heading, a second block, an image, legacy Markdown content, content that does not validate, and content whose projection is not the prompt, which QuestionProse reports as invalid and still does. T-D4.11's "a rich prompt" refusal was content without marks, which does not validate, so it still refuses and is renamed "rich content that does not validate"; no other case changed. GapSentence and the answer shape are untouched. E2E 1 (author-and-publish.live.spec.ts) authors its choice prompt in the builder, so the test's preview now draws it as the sentence; the spec finds it as a screen reader reads it ("They chỗ trống to the museum last weekend."). In a production build of the engine, the rich prompt "If I ___ you, I would study harder." measures exactly as the same prompt in Markdown at 360 and 1280, light and dark, empty and chosen (the sentence 500 18px/32.4px, the box 88 × 30.8px empty), which is the drawing T-D4.11 matched to s9 questions 5 and 12; no horizontal scroll.

  8. Spec §9 on the cloze run. §9 says "The engine shows one question at a time"; since T-D4.10a a run of a cloze's gaps is one screen. T-D4.12 rewrites §9 and §12 for the rail and the sheet, and adds the run, its line, the ties and the move by stop there. T-D4.10a left the spec's version alone, to keep it out of the lanes running beside it.

    As built (T-D4.12): spec v0.104. §9 opens "one question at a time, except that a run of a cloze's gaps is one screen", and its new "A cloze" bullet gives the run (consecutive single-choice members bound to gaps of one material, none with media of its own), its line ("Câu 23–27 trên 34 · Điền từ vào đoạn văn · 5 chỗ trống"), the shared prompt or each member's own, the chips and the gap card, the choice that opens the next empty gap, the ties and rings of its squares, and a gap on its own. "Keys" says the arrows move by screen and the letters choose for the open gap; §12's density rule says a cloze's run is one screen and its option rows are 48px.

  9. The grading leave guard after "Use score and comment" (T-D4.36, the Tech Lead's review). A score filled from the suggestion and not yet saved was dropped on a route change without a word when the suggested comment was empty or matched the saved one.

    As built (D4 follow-ups): a score laid by fill is unsaved work until a Grade saves it, and the dialog then reads "Rời trang khi điểm chưa lưu?"; every way of saving is unchanged.

  10. The credit figure in Settings (T-D4.38 against T-D4.39). Settings' "Read imports with AI" hint carried the deck's "About 400 credits per page", which the import card leaves out (§12: no figure the product has not metered).

As built (D4 follow-ups): the hint ends after its first sentence in vi and en, as the import card's does. 11. A settled-state step for geometry in e2e specs (QA-12b-4, T-D4.12). A spec that measures a box with boundingBox() or getBoundingClientRect reads it at once, and one read the drawer handle while qz-nudge was still moving it (fixed for that spec in #596 by reading its layout). Proposal: a helper in web/tests/support/ that waits until no animation that can move the element is running (document.getAnimations() filtered to those whose target is the element, an ancestor or a descendant, until each has finished, polled with expect.poll), called before a measurement, and adopted spec by spec as each is next touched, not in one sweep. Not built in D4. S, web.

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


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

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