ADR-105 — The training course: one per teacher, four missions on real screens, a progress card derived from the data¶
Status¶
Accepted (2026-10-11, issue #776: the product owner approved the proposal in session
and left its open points to the implementation; the values taken are the
Assumptions section, each marked as such until he confirms or changes it). The
spec challenge of the same day corrected three claims of the first draft (no YAML
import exists; a staff attempt earns no drill card and no statistic; a seat is per
classroom). Delivery progress lives in docs/merge/PROGRESS.md and changes/.
Scope: how a new teacher learns the platform by using it — what is provisioned for them, what content it holds, how their progress is shown — and nothing about what a student sees.
Relations: builds on ADR-018 (the staff seat and
the student view are the way to the student side; a staff attempt counts nowhere),
ADR-095 (the training content is one
public pool, linked read), ADR-013 §5 (a pool with
no member keeps its owner), ADR-080 (the guide
chapter is what the assistant reads; a palette command with an effect is never run
by it, §10), ADR-022 and
ADR-023 (the MCP mission),
ADR-087 §9 (What's new and the coach
marks come first on a first visit), spec 08 §8.2 (the novice path). Adds F-ORG-17
(docs/spec/02) and settles open question 58 (docs/spec/06). Amends apps/web/DESIGN.md
("Empty state: one action") for the Courses home only: a text link may follow the
one action. Changes no rule of ADR-018, ADR-025, ADR-034, ADR-041, ADR-068 or ADR-095.
Context¶
A teacher signs in for the first time and lands on an empty Courses home that says
"Create a course". The guide (docs/guide/getting-started.md) explains the frame;
nothing makes the teacher do the loop the platform exists for — write a question,
launch it, watch the class, grade, release — nor shows them the student side of it,
nor the tools around it (the assistant, MCP). Reading is not doing.
Three facts shape the answer. First, the teacher's default is the teacher view, and
crossing to the student side is two existing gestures: a staff seat through Join
as student on the classroom page, then Switch to student view (ADR-018). The
seat is badged, counted in no statistic, resettable (DELETE
/evaluations/:id/attempt), and it earns neither drill cards nor question statistics
(drill/lifecycle.ts, stats/service.ts). A teacher needs both gestures later to
rehearse their own exams, so the onboarding must make them do both, not do them for
them. Second, a public pool linked read (ADR-095 §4) makes a course's staff readers
of a colleague's questions without letting them edit: one central copy of the
training content, never a copy per teacher. Third, there is no import or export of
questions today: canonical is planned, not present (spec 05, CLAUDE.md); spec 08
§8.3's "YAML export and import" describes that plan. The training content therefore
ships as code, installed through the ordinary services, as apps/api/src/seed/ does.
Constraints: invariant 2 (one primary action per screen; the classroom page's is per
tab, F-ORG-13, and New ▾ on Activities), invariant 4 (the training questions reach
the teacher's student view through toStudent like any other), invariant 6 (the
course is an ordinary course reached through staffAccess), invariant 9 (new audit
actions), the seed rule of CLAUDE.md (never raw inserts), N-I18N-01 (UI strings in
both languages), DESIGN.md (an empty state has one action; coach marks play on a
screen's first visit), issue #722 (a roster line admits a sign-in, so fake students
are not a training material).
Decision¶
1. One training course per teacher, provisioned by one action¶
POST /app/api/training (module training; a portal session of a teacher or an
admin, never a student, an API token or a delegated session) creates, with the
ordinary services of org:
- a course whose
training_forcolumn names the caller (courses.training_for, nullable, referencesuserswithset null, unique: the constraint is the idempotence, aspools_personal_uqis for the personal pool; a second call returns the existing course,created: false). The course is the caller's only while they hold a staff seat on it (invariant 6): theorgmodule clearstraining_forwhen that seat is removed, the course lives on as an ordinary course of its remaining staff, and the teacher may provision a new one. A deleted classroom is rebuilt by the next call, which keeps the course's other pool links and audits the rebuild. The course is named and coded by the service — name in the request's UI language, codeINIT-<n>free at creation; both editable later as any course's, the column alone marks it. The caller is its only owner; - one classroom, the current period (its
periodEndthe end of the current semester), no roster, no seat; - a
readlink to the public training pool (§2); - the two evaluations of §3, from the content definitions, in the states §3 names.
Nothing else: no seat (mission 1 is the seat), no private pool (mission 3 is a
pool). Refused 409 training_not_installed while the content is not installed.
Audited training.provision. A training course is an ordinary course everywhere
else: staffAccess, F-ORG-12, its deletion by its owner (F-ORG-09), which is how
"Restart the training" works (delete, then provision again; a pool the teacher made in
mission 3 is theirs and stays). An assistant (ADR-068) is a teacher here and gets
their own. An admin's course list under Super Powers shows every training course like
any course (Assumption 4).
2. The content: one public pool, shipped as code, installed by an admin task¶
The training content is ONE public pool, owned by the super admin's account
(Assumption 1), marked pools.is_training (a partial unique index: one per
instance; the install finds it by that mark, never by name). The questions and the
two evaluation definitions are a TypeScript content module,
apps/api/src/modules/training/content.ts, typed by content-types.ts and modelled on
seed/content.ts: French statements, a stable internalName per question, a config
validated by the type's schema at publish, no concept (ADR-081's vocabulary is the
courses', not the platform's). It is installed by the admin task
training.install (the task registry of the administration page, POST
/app/api/admin/tasks/:key/run, which also runs it daily so a deployed content
update reaches the pool without a click): a missing question is created and
published; a question whose config or explanation differs from its latest published
version gets a new published version; an unchanged one is left alone. A run with no
question in the content installs nothing, and a run with no super admin account
skips with a message. The pool is made public when it is created and never
re-published by the task: an unpublication (ADR-095 §5) is deliberate, and
"installed" means the pool exists AND is public, so the entries of §5 disappear
and provisioning refuses until an admin publishes it again. Evaluations already
instantiated keep their frozen versions and show "Update" (F-EVAL-03), so nothing
moves under a teacher's feet. Audited training.install. Teachers are readers of the
pool; it is listed in the catalogue like any public pool, under its owner's real name
(ADR-095 §6), and reached by find_similar_questions; its statements are about the
platform, so they collide with no course's content. This module gives way to
canonical when that package lands; the content does not change, its carrier does.
3. Four missions, each on a real screen, none examined¶
The teacher is never graded: a staff attempt counts nowhere (ADR-018), neither in a
statistic nor in the drill, and the record promises neither. The missions are the
text of a guide chapter, The training course (docs/guide/training.md), and of the
training classroom's help topic; the card of §4 links them. "Quiz" names what the
teacher takes as a student; "mission" names what they do as a teacher.
- Be a student — "The welcome exercise" (« L'exercice d'accueil », until 2026-10-20 « Le quiz d'accueil »; courses provisioned before keep their title): an exercise,
Scheduled, open from the provisioning until the end of the classroom's period
(the ticker closes it then, F-EVAL-04; a restart provisions a fresh one), free
navigation, immediate correction, retakes allowed (ADR-025, which a staff seat
gets like a student). One question per automatically graded type present on
main:mcq,short,cloze,categorize,math,circuit,code. A statement teaches a gesture of the platform and its answer is discoverable on the screen around it; it points at the gesture, never at its wording, so a key is never a UI label (Assumption 5). The explanation, visible under the exercise's feedback policy, says what the teacher would configure to obtain that behaviour. Two clicks before it: Join as student on the classroom page (the coach anchorclassroom.joinalready points at it), then Switch to student view. - Run a class — "The live exam" (« L'examen en direct », until 2026-10-20 « Le quiz en direct »), a draft in Live mode:
the three screens and the start checklist with the waiting-room preview
(F-EVAL-23); the teacher opens the room, opens a second tab in the student view
(the mode is per tab, ADR-018), starts, answers, closes; grades the
richquestion and thediagramquestion by hand (with the LLM proposal when the gateway is configured, ADR-063); releases; reads the released grade in the student view (the staff feedback page shows it through the live tally,releasedGradesOf; pinned by a DB test). - Make it yours — New pool on the pools page, Link a pool on the training course's page (F-ORG-12), then in that pool: create a question with the wand (ADR-059), try it, publish it. Save the welcome quiz as a template (F-EVAL-24), instantiate it, add the question, edit the question to meet the "newer version" badge (F-EVAL-03), preview it (stateless), take it from the seat of mission 1, reset one's own attempt.
- The tools around — ask the assistant "how do I give a student extra time?" and let it open the roster (ADR-080 §10); create a personal token or connect Claude through MCP (ADR-022, ADR-023), create a question from there and find it in the pool. Journal, projects and groups are links to the guide, not missions.
Out of the training: no roster (the only seat is the teacher's), no SEB or kiosk
rehearsal (ADR-027, ADR-051), no impersonation (ADR-034), no GitHub project (an
organisation is needed, ADR-100), no poll type (brainstorm is a poll's), no blocks
until that type merges (a content update then adds it).
4. A progress card derived from the data, never stored¶
GET /app/api/training returns whether the content is installed, the caller's
training course (id, classroom id, and the id of the live quiz of mission 2 — the
classroom's oldest evaluation in Live mode, resolved by the server so the card never
matches a title the teacher may rename, null once deleted) or null, and the state
of each mission —
todo, done or unavailable — computed from existing tables and never stored:
| Mission | done when |
unavailable when |
|---|---|---|
| 1 | a handed-in staff attempt of the caller on an exercise of the course | never |
| 2 | a released evaluation of the course | never |
| 3 | a published question in a pool linked edit to the course AND a template in the course |
never |
| 4 | an api_tokens or oauth_grants row of the caller, ever (revoked rows are kept), OR an assistant conversation of the caller (purged after 30 days, ADR-080 §6: the mission then reads todo again, which is harmless) |
the LLM gateway is off AND OAuth has no client |
Mission 4 is optional: "all done" is missions 1 to 3. The card shows on the training
classroom's Activities tab only, to the teacher the course was provisioned for
(training_for), above the activities. Its one accent is the next todo mission,
and it leads to the screen or the control where the mission happens — it never
performs the gesture (ADR-018: the switch stays secondary chrome, and doing it from
the card would not teach where it is). While a todo mission remains, New ▾ is
secondary on that tab; the other tabs keep their own primary action (F-ORG-13). All
done: the card says so and links the guide. The coach tour of the classroom plays as
on any first visit, after What's new (ADR-087 §9); the card is content, not chrome,
and competes with neither.
5. Where it is offered¶
The empty state of the Courses home keeps its one action, New course, and gains
a text line under it: "First time here? Start with the training course"
(Assumption 2; DESIGN.md amended for this screen). A teacher whose home is not
empty (invited on a colleague's course) finds Training course in the account
menu and in the palette's Help group; it is a global palette command, which the
assistant is never offered (it reads the mounted screen's commands only, ADR-080
§10). Each provisions or opens the same course. Neither is shown while the content is
not installed. "All done" and the demotion of New ▾ follow one pure rule shared by
the card and the page, so the two can never disagree; when mission 1 is next and the
teacher already holds a seat, the card shows a hint in words and New ▾ keeps its
accent; likewise when mission 2 is next and the live quiz was deleted, the row keeps
its text and no button.
Assumptions (the product owner's open points, values taken on 2026-10-11)¶
- The public training pool is owned by the super admin's account
(
SUPER_ADMIN_EMAIL). Nothing forbids it (ADR-013 §5, ADR-054); if that address changes, the pool stays with the old account and the new admin edits it under Super Powers. Alternative left open: a platform account of its own. - The empty state gains a text link, not a second button; the account menu and the palette carry the entry elsewhere.
- Statements are about the platform, not a neutral subject.
- The admin's course list shows every training course like any course. If they pile up, a filter off by default may follow; it is not built now.
- French content only, with the English UI showing French statements; no key is a UI label, so the language of the interface changes nothing to the answer.
- The welcome quiz holds the automatically graded types only;
richanddiagramare graded by hand in the live quiz; blocks joins by a content update.
Consequences¶
- Benefits: the first success in two gestures and one click, on the real screens; the two gestures to the student side are learned where they will be needed; the content is maintained once and reaches every training course.
- Costs: a migration (
courses.training_for,pools.is_training), thetrainingmodule (provision, status, install task) and its DB tests, two contracts, two audit actions, the card and the two entries, a guide chapter and a help topic, the content itself, aDESIGN.mdamendment. - Risks: the live quiz's hand grading consumes the shared LLM cap (ADR-058 §5) when the teacher asks for a proposal, one small call per teacher; the nightly review (ADR-060) reviews the training questions like any other, which is wanted. A teacher who deletes the super admin's pool cannot: only its owner can.
- Nothing here touches a student: no student payload changes, no seat is created for anyone.
Alternatives considered¶
- One shared training classroom for every teacher. Rejected: attempts and edits of one teacher would land in another's screen, and the course's staff would be every teacher of the instance.
- A course template instantiated per teacher. Rejected: templates belong to a course (F-EVAL-19), so the content would need a course of its own to live in.
- A YAML folder installed by an import. Rejected for now: no import exists, and
canonicalis planned afterui-kit(CLAUDE.md); building it here would jump that order. The content module is the same data in code. - A copy of the questions per teacher. Rejected: copies diverge, and an improved statement never reaches a teacher already started (ADR-095 context).
- A seat and a private pool at provisioning. Rejected after the spec challenge: a seat is per classroom and the button disappears once it exists, so a provisioned seat removes the very gesture to teach; a private pool survives the course's deletion and piles up at each restart, while creating and linking a pool is a gesture worth teaching.
- A
courses.kindenum. Rejected: it marks a course but names no teacher, so it gives neither idempotence nor "whose card is this";training_fordoes both. - Stored progress (a checklist table). Rejected: every line is a fact the tables already hold; a stored flag would drift from them and need its own writes, audit and reset.
- A guided tour overlay. Rejected as the vehicle: the coach marks already play a tour per screen and teach the chrome; the missions teach the loop.
- Automatic provisioning at first sign-in. Rejected: a teacher invited as an assistant arrives with a course already; an unasked course on everyone's home is noise, and the empty state is the moment the need is felt.