ADR-012 — Grade freezing: receipt time written synchronously, two-step freeze¶
Status¶
Accepted (2026-07-03, phase 3).
Addendum (2026-09-30, ADR-035, the classroom merge): two readings of the same principle. Until the merge, Quiz applied this record by analogy only. With projects it has two readings:
- Literal, for projects (classroom's assignments): exactly as
written —
push_receiptswritten synchronously by the webhook handler,bot_commits, the provisional pointer at the deadline, the definitive freeze atdeadline + grace_minutesby the ticker (ADR-006), GR-14.3 for a SHA with no known receipt. What is frozen is a score (points out of a maximum), not a grade. - Analogical, for evaluations: the same principle — a result is fixed
on facts the server recorded (its own receipt time of every write,
invariant 5), in two steps — but the second step is the teacher's
release, not a clock: grades are proposed, validated, then released
(
released_at,released_grades), and a later change is visible asmodified_after_release.
How a frozen project score reaches students and the gradebook was
settled on 2026-10-01 (D05, D06): only through a teacher release,
indicative before it, converted to a grade by a per-project
grading_scale (F-PROJ-14, F-GBOOK). A frozen score is never a released
one by itself.
Amended 2026-10-09: decision 2 names bot_commits.repo_id and decision 3.3 sets project_repos.frozen_at only; student_repo_id and frozen_final do not exist in Quiz.
Context¶
The grade frozen at the deadline is the most disputable piece of data in the system. The reference for the freeze is the server receipt time of the push webhook, persisted per SHA (GR-14, H6) — never the git timestamp, which can be forged. A run on a commit received before the deadline but finished after it counts towards the frozen grade, within the limit of a grace period (GR-14.4, default 30 min). Webhooks are processed asynchronously through a queue (GH-60): if the receipt time depended on the processing, a queue delay would change grades.
Decision¶
- Synchronous write of
push_receiptsin the webhook's HTTP handler, before the job is enqueued: the receipt time (the legally decisive data of the freeze) never depends on the queue lag. Acknowledgement stays under 5 s (two INSERTs). - A
bot_commitstable (repo_id,sha,kind) fed on every bot push (revert, deadline, sync): a deterministic GR-05/GH-44 eligibility filter, more reliable than inferring from the actor at run time. - A two-step freeze (a literal reading of GR-12 and GR-14.4, borrowed from the productivity proposal):
- When the deadline is applied,
frozen_grade_run_idis set provisionally (the current GR-09 grade at that instant). - During the grace period, only runs on commits received before the deadline (present in
push_receipts) can still improve that pointer. - At
deadline + grace_minutes, the ticker sets the repository'sfrozen_at(project_repos): the frozen grade becomes definitive and immutable, and later runs never change it. - A SHA with no known receipt time (lost webhook, reconciled after the fact) is treated as
after_deadline = trueas soon as the deadline has passed — the conservative GR-14.3 choice, open to a teacher's arbitration in the light of the history. - The grace period is configurable per assignment; the portal recommends 60 min when the class size makes runner capacity the limiting factor (ADR-007).
Consequences¶
- The freeze is insensitive to processing delay: a deadline burst only produces display lag, never a wrong grade.
- Disputes are settled on persisted facts:
push_receipts.received_atper SHA,bot_commitsfor excluding bot commits, the full history of the GradeRuns. - The grading pipeline and the freeze are decoupled from runner availability: a failed runner delays the grades, and the freeze waits for the grace period and then locks in.
Rejected alternatives¶
- Receipt time written by the asynchronous worker (productivity proposal, not spelled out): a queue delay would shift the reference time towards the processing time — unacceptable for a piece of data that separates submissions to the second.
- A single-step freeze at
deadline + graceonly: simpler, but it offers no provisional grade to display during the grace period, and the literal reading of GR-12 (freeze at the deadline) would be lost. - A bot filter based on the run's
github.actoronly: it depends on the workflow's execution context; the per-SHAbot_commitstable can be verified after the fact and replayed.