ADR-070 — Group sets: a classroom's reusable groups, which a project follows until its deadline¶
Status¶
Accepted (2026-10-04, product owner); amended by the product owner on 2026-10-05 (decisions 4, 5, 6 and 8: four amendments) and 2026-10-06 (decision 2, ADR-077). Amended 2026-10-09: ADR-048 is folded into this record (decision 12, and the correspondence table), and the five amendments are folded into the decisions they change, each marked with its date and task (the table of old references maps them).
Scope: where a classroom's groups of students live, who forms them and how, and how a group project uses them, including the GitHub side of a group repository (decision 12, folded from ADR-048's lot 2).
Relations: amends ADR-048 decisions 1, 2, 4
and 5 and its rejected alternatives 1, 2 and 5 (the scope of a group, who
forms it, the rename lock, where the maximum size lives, the reconciliation
of a membership change). Amends in docs/spec/02-exigences-fonctionnelles.md
F-PROJ-01 (the form names a set), F-PROJ-03 (a draft's set, not its groups,
freezes at publication), F-PROJ-06 (rewritten on sets), F-PROJ-13 (the
"access to revoke" flag, the drift of a stopped copy) and F-PROJ-17 (decision
5 below), and adds F-PROJ-22 (decision 8); N-SEC-20 in docs/spec/03-exigences-non-fonctionnelles.md (the
set's student view, lot 2); the glossary's Project group
(docs/spec/01-glossaire-et-domaine.md); the cards M3-15 and M3-16
(docs/merge/history/09-tasks-delivered.md). The import of F-PROJ-20 (M8-01) maps onto it.
Context¶
ADR-048 came from heig-classroom with three choices: a group belongs to one
assignment, only the staff forms it, and its members are frozen once the
group has a repository. The port to Quiz has not begun (M3-15, M3-16), and its
tables (project_groups, project_group_members, migration 0054) hold no
row, so the model can still change at the price of a migration.
The product owner describes a wider practice:
- some teachers form groups once for the whole semester, others per piece of work, and groups break up or change during the semester;
- groups are formed by the teacher, at random by the platform, or by the students themselves, who like to name their group;
- groups have from two to N members, and an odd remainder goes either to a smaller group or to larger ones, at the teacher's choice;
- a teacher moves a student from one group to another by dragging them, and a mistaken drop must not cost anything;
- the project form chooses the groups a group project uses, or leads to forming new ones.
heig-classroom rejected classroom-wide groups because "teams change from one lab to the next". That holds against ONE list of groups per classroom; it does not hold against several named lists, of which a project picks one. heig-classroom's split is not random either: it cuts the roster in alphabetical order and leaves a remainder of one (7 by 3 gives 3, 3, 1).
The word matters too. "Team" collides twice in Quiz's interface: French
Équipe already names a course's staff (courses.staff), and English
Teams already names Microsoft Teams (ADR-030). GitHub teams are a third
meaning, which ADR-048 deliberately does not use.
The four forks of the design (how a project is tied to its set, the UI words, the place of student self-formation, the guard of a drag and drop) were settled by the product owner on 2026-10-04.
Decision¶
-
Words. The unit is a group (fr groupe); a named list of groups of one classroom is a group set (fr répartition). Code says
group_set,group; "team" is not used, in the interface or in the code. A group's name is free text; the default is "Group k" (fr "Groupe k"), the first free k. -
A group set belongs to a classroom, in a module of its own (
apps/api/src/modules/group/), not to the roster: the roster says who is in the classroom; a set says who works with whom. A classroom holds any number of sets. A new set is named "Groups of group_sets—classroom_id(cascade),name,max_size(null: none),open_until(lot 2),created_by,created_at;student_groups—set_id(cascade),name,position, unique (set, name) (groupsis a reserved word in PostgreSQL);-
student_group_members—set_id,group_id(cascade),enrollment_id(cascade), unique (set, enrollment): a student is in at most one group of a set, and adding them to another group moves them. Membership is by roster line, as in ADR-048 decision 3: a student is placed before they ever sign in. The students of a set are the classroom's roster lines, claimed or not, except staff seats (ADR-018), which are never placed. The set'smax_sizereplacesprojects.group_max_size, which the migration drops. An archived classroom's sets are read-only (409 classroom_archived) and cannot be opened to its students. Amended 2026-10-06 (product owner, ADR-077), staff seats stay unplaced: a teacher's staff seat may now accept an individual project to test it (ADR-077), but it is never placed in a group: on a group project it answers409 no_group, whatever group membership it kept from before it became a staff seat, and it is never invited on a group's repository. (End of the amendment.) -
Three ways to form groups, on the same set.
- By hand: create, rename, delete a group; move a student into a group or out of every group.
- At random: the students of the set who are in no group are shuffled
(
cryptorandomness) and cut into groups of the chosen sizes, from 1 to their number. The remainder rule is the staff's choice, and in both the sizes differ by at most one: smaller makes ⌈n/s⌉ groups (some of size s − 1), larger makes ⌊n/s⌋ groups (some of size s + 1). 23 by 3: smaller gives seven of 3 and one of 2, larger gives five of 3 and two of 4; 10 by 4: smaller gives 4, 3, 3 (never 4, 4, 2), larger gives 5, 5. At the edges a size lies further than one froms, the sizes still balanced: 5 by 3, larger, gives one group of 5; 7 by 5, smaller, gives 4 and 3 (M3-15a). Groups already formed are never touched. The rule is pure, in@quiz/domain, with its tests; the whole formation is one transaction. -
By the students (lot 2): see decision 8. A group has one member or more, with no upper bound but the set's
max_size: a group of one is how a student works alone in a group project.max_sizeis advisory for the staff (a warning, never a refusal, as ADR-048 decision 5) and binding for the students (decision 8). -
A group project follows its set until its deadline (product owner).
- A project in group mode names one set of its classroom
(
projects.group_set_id), chosen or changed while it is a draft, likegroup_mode. A set named by a project that is not archived cannot be deleted (409 set_in_use, the projects named). - The project keeps its own copy of the set: ADR-048's
project_groupsandproject_group_membersstay, andproject_groupsgainssource_group_id(set null). The copy is made when the project names the set and replaced when it names another. It is what the publish guard already reads (409 unassigned_students, M3-02, unchanged) and what Accept, the hints, the project page and the student view will read (M3-15, M3-09): the project's fact of who is in which of its groups, hence whom GitHub has been asked to let in. - What follows. A group of the copy follows the set while the project is not archived and its deadline has not been applied, and, once the group has a repository, while that repository's own deadline (ADR-064) has not been applied either. A change of the set reaches the copy where it touches only groups that follow: a move reaches a project's copy only when both the group the student leaves and the group they join follow there; otherwise that copy keeps the student where they were. A rename follows; in the copy a name or slug that clashes is disambiguated like a repository name (ADR-048).
- What stops. After the deadline the copy no longer moves: a move in
December never changes who was graded in October. A copy that has
stopped does not follow again by itself — not when its deadline is moved
later (F-PROJ-09), nor when the project is unarchived. The project page
then shows that the set has drifted, and Resync with the set applies
the whole difference through the confirmation of decision 6.
Amended 2026-10-05 (product owner, M3-15b-1), the first deadline stops a
group: a group of the copy stops following at the FIRST of its
deadlines: the project's (already
groups_stopped_at) or, earlier, its repository's own (ADR-064). A repository whose deadline is extended past the project's does not keep its group following. (The per-group stop is stored by M3-15b-2.) (End of the amendment.) How it is built (orchestrator, 2026-10-05, M3-15b-2a): each copy group's stop is stored (project_groups.stopped_at). - Resync with the set (amended 2026-10-05, product owner, M3-15b-2;
delivered by M3-15b-2b) is allowed after the deadline — its confirmation
names every frozen repository it touches — and refused once the project is
released (
409 released). R1 and R2 below: product owner, 2026-10-05, M3-15b-2b.- R1, a deleted set group. A stopped group with a repository whose
set group was since deleted is KEPT by Resync: its repository and its
frozen score stay; its members follow the set, each named as a
loseconsequence; the repository may end with no member. - R2, the release waits. The release is refused
(
409 group_sync_pending) while a confirmed resync is not fully applied. - How it is built (orchestrator): the resync is a durable confirmed
target, not a fence — the keys of the consequences confirmed are stored
on the project (
projects.group_resync, withgroup_resync_atand_by); thegroup.syncjob applies only those keys, computing the plan with every stop lifted, and drops a key once done or no longer asked for by the set: a set's write after the confirmation never extends it. What touches no repository (renames, new groups, moves between groups without one) is applied in the request's transaction. The resync is one-shot:stopped_atandgroups_stopped_atare never cleared, so the copy stays stopped and drifts again at the next write of the set (alternative 5). The drift is a non-empty plan with the stops lifted (the project's and each group's) beyond what the job already owes; a following copy with a group stopped on its repository's deadline drifts too, and is resynced the same way. A confirmed resync's departure out of a frozen group into another ends moved, never left, as far as the arrival was confirmed; its departure marks survive a stop and a set's write. Its GitHub writes are auditedvia: "group.resync", the requestproject.group_resync. A draft has nothing to resync (its copy follows). Refused: an archived project (project_archived, the sync's own refusal), a released one (released), an archived classroom (classroom_archived).
- R1, a deleted set group. A stopped group with a repository whose
set group was since deleted is KEPT by Resync: its repository and its
frozen score stay; its members follow the set, each named as a
- How it is applied. A change that touches no repository is applied to
the copy in the set's own transaction. A change that touches a
repository is applied by a job (
group.sync, pg-boss, one lease per project likedeadline_job_at, ADR-064), which re-reads the deadlines in its transaction and works from a pure diff of the set and the copy (groupSyncPlan,@quiz/domain). A departure waits for GitHub: the collaborator seat is revoked and a pending invitation cancelled, and only then does the student leave the copy's group. A revocation GitHub refuses leaves them in it, flags the repository's row access to revoke on the project page (F-PROJ-13), and is retried by the job and by the reconciliation (M3-06). An arrival waits only for the same student's pending departure: it is written to the copy, then invited, or invited when they link their GitHub account (ADR-048). Each GitHub write is audited. How it is built (orchestrator, 2026-10-05, M3-15b-2a): a move touching a group with a repository is always the job's; the set's transaction applies the rest, marks the departures on the copy (departing_at, which no invitation passes) and the project due (group_sync_due_at, a third leasegroup_sync_job_at). A failed pass retries after a backoff doubling from 30 s up to an hour. An access is revoked only once GitHub confirmed it (project_repo_access.revoking_atwhile asking): a roster write meanwhile is refused (502 revoke_failed), a crashed job's next pass asks again, and an access GitHub kept on a group repository whose line is no longer its member (a stray grant) is the job's to revoke and flags the repository access to revoke. - Whose repository. A group repository is a student's only through
the copy's membership, never through
project_repos.user_id, which only records who created it (N-SEC-20; tested with the creator moved out). The work does not follow a moved student: what they pushed stays in their former group's repository, which they no longer reach. - A group's display name follows the set; its slug, hence its repository's
name
<project slug>-<group slug>, is fixed once the repository exists. ADR-048 decision 4's rename lock is lifted: renaming a group never renames a repository. -
Deleting a group of the set is refused while a following copy of it has a repository (
409 has_repo); emptying it is allowed, with the confirmation of decision 6. -
Leaving the roster (F-PROJ-17) does not wait for the job: removing a student calls the group module first, which revokes their access to every repository of the classroom's projects, a group's or their own, before the roster line is deleted, and refuses with
502 revoke_failed, the roster unchanged, when GitHub refuses (as ADR-048). The cascade then takes them out of every set and every copy, frozen ones included: like the rest of the gradebook, a student who left the roster leaves the project's grades; the repositories, runs and scores stay (F-PROJ-17). A seat whose account changes (unclaim, adoption, ADR-061) is a departure of the former account's GitHub login followed by an arrival of the new one. Amended 2026-10-05 (product owner, M3-15b-1), nothing to revoke: when GitHub cannot be asked — the App not installed or uninstalled, the organization or the repository deleted on GitHub (deleted_at), the member never had an account invited — there is nothing to revoke: the removal proceeds, and the audit records the revocation as skipped with its reason. Only a refusal by a repository GitHub can reach refuses the removal (502 revoke_failed: roster, sets and copies unchanged). The account revoked is the one Quiz INVITED, recorded at each invitation (project_repo_access), never the user's link of today. The same revocation runs before an unclaim, an e-mail change that detaches the seat's account, and a self-enroll that turns the seat into a staff seat. (End of the amendment.) -
A membership write is guarded by its consequences, decided by the server (product owner). Every membership write (add, move, remove, delete or empty a group, Resync with the set; a random formation only fills new groups, which have no repository yet, so it never needs it) that has GitHub consequences answers
409 needs_confirmationwith them — which repositories each student loses, which invite them — and a digest of them. The request is applied when it sends that digest back (confirm: <digest>); a digest that no longer matches answers a fresh409 needs_confirmation. A write with no GitHub consequence takes effect at once, and the page offers Undo (the reverse write, itself a write that may meet the 409) for a few seconds; a write with consequences opens a confirmation that names them. The client never guesses the consequences. Amended 2026-10-05 (product owner, M3-15b-2), what needs a confirmation: every place or unplace touching a following group that has a repository needs the confirmation, even when GitHub will have nothing to do (no account invited, the App gone): it changes whose repository and grade it is. Thegroup.syncjob then audits the revocation skipped (decision 5). (End of the amendment.) How it is built (orchestrator, 2026-10-05, M3-15b-2a): the consequences confirmed are those the write ADDS to the ones already waiting, by a SHA-256 digest of their sorted (project, group, roster line, kind). Amended 2026-10-05 (product owner, M3-15b-2b), R3, an arrival without a repository: a resync may move a student into a group without a repository after the deadline: allowed, and its confirmation flags it (the student will have no repository, Accept being closed). (End of the amendment.) -
The project form offers, under group work, the classroom's sets (name, number of groups, students placed / not placed, date) and Create new groups, which opens a new set and comes back to the draft. Publishing a group project without a set answers
409 no_group_set; at publication the set chosen freezes with the groups mode (F-PROJ-03,409 not_draft), while the groups themselves keep following (decision 4). The project page shows the set it follows and a link to it. -
Student self-formation (lot 2). The staff opens a set to its students until a date (
open_until, the server's clock; amax_sizeis then required). While it is open, and as long as no group of the set has a repository in any project, a student holding a claimed seat of the classroom may create a group and name it, join a group belowmax_size, leave their group, and rename the group they are in; the staff keeps every right. When it closes, nothing happens by itself: the staff places the remaining students (by hand or at random), and a project's publish guard still refuses anyone left out. These are the first student writes of a classroom besides Accept: they go through the student routes of the module onreadableClassroom's student branch, refuse an impersonation session and thesebandkiosksessions, and each is audited. What a student reads goes through the module's student view, a new exit listed in N-SEC-20: while the set is open, its groups' names, sizes and members' first and last names; once closed, only their own group of it (name and members). Never a set that is not open nor used by a project they see, never another classroom's, never an e-mail, a GitHub login or a staff note. Tested like the other student views: a second classroom's set, a closed set, and another group's members after closing, searched for in every student response. Amended 2026-10-05 (product owner, M3-17), the students' side, settled before it was built: -
S1, where. The student's classroom page gets a Groups tab (fr Groupes), at the same address as the staff's,
/classrooms/:id/groups, role-dispatched like/classrooms/:id(F-ORG-15). It is drawn only while a set reaches the classroom's students (the page'shasGroups, likehasJournal). - S2, which sets. A student sees a set that is OPEN (its
open_untilahead by the server's clock, the classroom not archived), or one a published project of the classroom that is not archived names. A closed set no such project names is invisible. - S3, how they learn of it. An open set is a row in Open now of the student's Activities, on the classroom page and the home: "Form your group until …" (fr "Formez votre groupe jusqu'au …"), leading to the tab. It never takes the urgent accent, and nothing is notified.
-
S4, the unplaced. While the set is open, the students also see the students of the set in no group, by first and last name only. How it is built (orchestrator, M3-17):
-
The student view is one service of the module (
studentGroupSets), each field picked by hand:serverNow, and per set the set (name,maxSize,openUntil,open),writable,myGroupId, the groups (name,size, members' names) and, while open,unplaced; closed, the reader's own group alone. Never a roster line's id, a claim, an e-mail, a GitHub login, the projects naming the set.writableis the server's: open, no group of the set with a repository, the classroom not archived, the reader's own portal session on a claimed student seat. - Routes.
GET /app/api/classrooms/:id/group-sets/studentthroughreadableClassroom's student branch, the student payload forced (a teacher in the student view and an impersonation read it,writablefalse); the writesPOST /app/api/group-sets/:id/student/groups(create and name, the creator moved in; the default name in the student's language),PUT|DELETE …/student/membership(join by group id, leave),PATCH …/student/groups/:gid(rename their own), each answering the classroom's sets as its writer reads them ({ serverNow, sets }, the read's own shape). A write takes the caller's own portal session on a claimed student seat of a set that reaches them (selfFormingSeat, the one rule the loaderstudentGroupSetand the read'swritableshare,guards.ts): a teacher in the student view, a staff seat, aseborkiosksession, a token get the 404 of a missing set; an impersonation the 404 in development, and the auth plugin's403 impersonation_read_onlyelsewhere (ADR-034). Group ids only, never a roster line's. - The checks, under the set's lock (
writeSet), BEFORE anything is written or stepped: open by the server's clock (409 set_closed, no grace); no group of the set with a repository in any project naming it, stopped copies and archived projects included (409 set_frozen, the same repository predicate as the copy's slug), so a student's write never reachesneeds_confirmation;409 group_fullat the maximum or above (a group the staff filled past it is full to the students), the lock serialising the last seat. An emptied group stays; a student never deletes one. The audit reusesgroup.create,group.rename,group.member_movewithself: trueand the student's line. - The staff.
GroupSetPatch.openUntil: a date opens the set until then (a date already past leaves it closed), null closes it, reopening is allowed, an archived classroom refuses it (409 classroom_archived); the set open after the write needs a maximum size (422 max_size_required, also for clearing it while open). The summary and the detail sayopenUntilandopen; a duplicate is born closed. The set's page opens it from its menu (a date and the maximum, the risk said: a published project on an open set can be accepted once everyone is placed, and its first repository freezes the set) and says until when it is open, with Close to students. -
Hints. A write on a set that reaches the students, before or after it (an opening, a closing), hints
groupsto theuser:topic of every claimed student of the classroom besides the course's staff; neverclassroom:. Nothing ticks at the closing: the client reads the set again whenopenUntilpasses, byserverNow. -
Who and what. Every route of the staff is loaded through
staffAccess(invariant 6): a set of a classroom one does not reach is a -
Every input and answer has its schema in
packages/contracts(group.ts). Every write is audited under closed names (group_set.*,group.*,project_group.repo_invite|repo_revoke, invariant 9). Its refresh hints go to the course's staff and to theuser:topic of each student whose group changed — in lot 2, while a set is open, to theuser:topics of the classroom's claimed students — and never to the classroom's topic (N-SEC-20). -
The import (F-PROJ-20, M8-01): each heig-classroom group assignment becomes one set of its Quiz classroom, named after the project, with its groups and members; the project names it, and its copy is the imported groups,
source_group_idset. An imported project past its deadline has a stopped copy (decision 4). -
Lots. Lot 1 is sets, by hand, at random, a project's set and its copy (M3-15a), the follow's job, ADR-048 lot 2's group repositories, the revocation on leaving the roster (M3-15b) and the web screens — the classroom's Groups tab, a set's page with drag and drop and a keyboard equivalent, the project form's choice (M3-16). Lot 2 is self-formation (decision 8, F-PROJ-22), the student's screen of it (M3-17). ADR-048 lot 3 (per-member invitation follow-up, a per-member adjustment of the group's score) is unchanged and still later. Delivery is tracked in
docs/merge/PROGRESS.md. -
The GitHub side, and the rules kept from ADR-048 (heig-classroom's ADR-014, imported as ADR-048 and folded here on 2026-10-09).
- Group mode requires the students' own tools (
work_mode = 'free',409 work_mode_group, ADR-047) and changes only while the project is a draft (409 not_draft). - Publishing refuses to leave anyone out: a group project with a
student in no group of its copy, or with no group at all, answers
409 unassigned_studentswith the students ({ students }) before any state change; the scheduled publication applies the same guard in the ticker, so such a draft stays a draft past its start, and goes live on the tick that follows the fix. - One
project_reposrow per group repository,group_idset (ON DELETE SET NULL: deleting a group never deletes a repository),user_idthe member whose Accept created it and nothing more (§4, N-SEC-20). The members are read from the copy, never copied onto the row, so the deadline, the freeze, the CI score and the review are per repository, hence per group, by construction. Grades and exports stay one line per student, each member reading the group repository's score. "Which repository is this student's?" is answered in one place (modules/project/groupRepos.ts,pickStudentRepoof@quiz/domain). Who reads a repository is one rule too (repoMembers): the refresh hints and the final-grade notification (on each member's channels) of a group repository go to every member. - Accept. The first member to accept creates
<project slug>-<group slug>(groupRepoName, capped at GitHub's 100 characters), and every member with a linked GitHub account is invited withpush; a later Accept joins the repository and invites its member. A student in no group gets409 no_group; a member with no linked account is invited when they link it, or when they accept. GitHub teams are not used: students are outside collaborators, and a team only grants access to members of the organization. - One provisioning at a time: a partial unique index on
(
project_id,group_id) makes two simultaneous Accepts insert one row, and an atomic claim (provision_claimed_at) lets one of them provision it; the other answers409 provision_in_progress. A claim older than five minutes (PROVISION_CLAIM_STALE_MS) is taken over: the request that held it died. A late failure never overwrites a provisioned row (markProvisionFailed). - No adoption. A name another tracked repository bears is
disambiguated with the group's id, and the provisioning adopts an
existing repository of that name only when this row recorded its id
(
409 repo_name_takenotherwise). - An individual repository inside a group project (imported from
heig-classroom's lot 1, M8-01) keeps its student while it is live
(
isLiveIndividualRepo): their Accept returns it unchanged, and they are neither shown, invited nor notified about their group's repository. Quiz's own Accept never makes one. - Later (heig-classroom's lot 3, unchanged): a per-member follow-up of the invitations, and the staff's per-member adjustment of a group's score.
- Group mode requires the students' own tools (
Consequences¶
- One teacher action serves the whole semester: a set formed once is named by every project of the semester, and a student who drops out is removed once; every open project follows, closed ones keep their history.
- A single drag can now reach several repositories. The cost is a job, a
pure diff, a lease and a flag on the project page instead of ADR-048's
synchronous
502 revoke_failed: with N projects on one set, an all-or-nothing call to GitHub is no longer possible. A student whose revocation failed keeps their access until the retry succeeds, and the staff sees it. Leaving the roster keeps the synchronous refusal. - The copy's unique constraints stay true by construction: a move reaches a copy only where both its ends follow, and a departure completes before the arrival.
- Downstream code keyed on
project_repos(deadline, freeze, review, scores, release) does not change; the questions "who is in this group" and "whose repository is this" read the copy. - A migration adds three tables,
projects.group_set_idandproject_groups.source_group_id, and dropsprojects.group_max_size; nothing holds a row yet. - The spec follows with the record (see Relations), with a new card for lot 2.
- Lot 2 opens a student write surface on a classroom, and a student view of its own.
Alternatives considered¶
- Groups per project only (ADR-048 as written, with Copy from…): correct for one lab, wrong for a semester — a student who drops out is removed project by project. Group sets keep its economy (a new set, or a duplicated one, per lab) without that cost.
- One list of groups per classroom: the false invariant heig-classroom rightly rejected; several named sets are its fix.
- A frozen copy at publication (the set is only a template): simpler, and the semester case again needs one edit per project; rejected by the product owner.
- Projects that always read the set live: a late move would rewrite who was graded on a closed project.
- A stopped copy that follows again on its own after a reopen: the next change would replay, without a word, every move made since the freeze; the explicit Resync shows them first.
- A synchronous, all-or-nothing membership change (ADR-048 alternative 5 kept): impossible to keep atomic across several repositories; the copy that loses a member only when GitHub has revoked them keeps its honesty instead — the copy never claims a revocation that did not happen.
- A confirmation on every drop: safe and slow when forming ten groups by hand; Undo covers a mistaken drop with no consequence, the confirmation stays for a drop that reaches GitHub.
- Unbalanced remainders (10 by 4 as 4, 4, 2): one group carries the whole shortfall; balanced sizes spread it.
- "Team" (fr équipe) as the word: the students' word, and a collision with the course's staff in French and with Microsoft Teams in English.
- GitHub teams for group repositories: students are outside collaborators, and a team only grants access to members of the organization (ADR-048, lot 2).
Correspondence with ADR-048¶
ADR-048 (heig-classroom's group assignments, imported 2026-09-30) was folded here on 2026-10-09. Code and documents that cite it resolve as follows.
| ADR-048 | This record |
|---|---|
| Decision 1 (a group belongs to one assignment, Copy from…) | replaced by decisions 2 and 4 (group sets, a project's copy) |
| Decision 2 (the staff forms the groups) | replaced by decisions 3 and 8 |
| Decision 3 (membership by roster line) | decision 2 |
| Decision 4 (a group with a repository is locked) | replaced by decisions 4 and 6 (the rename lock lifted; departures through the job, confirmed) |
| Decision 5 (an advisory maximum size) | decision 3 (the set's max_size) |
| Decision 6 (group mode requires the free work mode, draft only) | decision 12; its 400 group_mode_requires_free is 409 work_mode_group in Quiz, and 409 group_mode_off does not exist |
| Decision 7 (publishing refuses anyone left out) | decision 12 |
| Decision 8 (three lots) | decision 11; lot 3 in decision 12, Later |
| Lot 2 (one repository per group: data model, Accept, one provisioning at a time, no adoption, leftover individual repositories, audit) | decision 12; membership changes on GitHub, decisions 4 to 6; the roster's revocation, decision 5; the audit, decision 9 |
Consequences (additive migration, group_id set null, staff routes, audit) |
decisions 9 and 12 |
| Rejected alternatives 1, 2 and 5 | replaced by decisions 2, 3, 4 and 8; Alternatives 1, 2 and 6 |
| Rejected alternatives 3 (a hard maximum) and 4 (a repository at the group's creation) | still rejected: decision 3 (advisory for the staff), decision 12 (created at the first Accept) |
Correspondence of old references¶
Decisions 1–12 keep their numbers. The five amendments are folded into them:
| Old reference | Now |
|---|---|
| Amendment of 2026-10-05 (M3-15b-1): §4, the first deadline stops a group | decision 4, What stops |
| Amendment of 2026-10-05 (M3-15b-1): §5, nothing to revoke | decision 5 |
| Amendment of 2026-10-05 (M3-15b-1): delivery split | removed: an interim rule (until M3-15b-2, a set's write reaching a copy group with a repository refused whole, 409 has_repo, a rename still following), superseded since M3-15b-2a and M3-15b-2b are done (docs/merge/PROGRESS.md) by decisions 4 and 6 |
| Second amendment of 2026-10-05 (M3-15b-2): §4 Resync; §6 what needs a confirmation; how it is built | decision 4 (Resync; How it is applied), decision 6 |
| Third amendment of 2026-10-05 (M3-17): S1–S4; the student view, routes, checks, staff, hints | decision 8 |
| Fourth amendment of 2026-10-05 (M3-15b-2b): R1, R2, how it is built | decision 4, Resync |
| Fourth amendment: R3 | decision 6 |
| Fifth amendment of 2026-10-06 (ADR-077): staff seats stay unplaced | decision 2 |
| Decision 11, Delivery | decision 11, Lots |