ADR-108 — How an item counts: normal, bonus or neutralized, changeable at grading time¶
Status¶
Accepted (2026-10-12, decided by the product owner).
Scope: the stored counting of an evaluation item, the sums that read it, and the grading-time act that changes it after an evaluation has run.
Relations: amends ADR-052 §2 (the lock: the
builder is unchanged, the grading-time act is allowed once frozen), §3 and §4
(negative marking floored at sum time), §5 (the total), §7 (copies) and §9
(CSV); depends on ADR-030 §h
(results_updated) and ADR-068 §3
(after the release, the owner's).
Context¶
After an evaluation has run, a teacher may discover that a question was incoherent — the real case: question 7 of a quiz must become a bonus. ADR-052 locks the bonus flag with the points once an attempt exists, so the only way out was to re-grade every student by hand. The teacher needs to change how the item counts from the grading panel: normal, bonus (ADR-052: out of the total, its points still lift the student) or neutralized (out of the total AND out of every student's points), with a confirmation, and reversibly.
Decision¶
- One state per item, not two flags.
evaluation_items.counting('normal' | 'bonus' | 'neutralized', not null, default'normal') replacesevaluation_items.bonus; migration0112_item_countingcopiesbonus = trueto'bonus'and drops the column. Every item shape of@quiz/contracts(ItemRow,AttemptItem,StudentResultItem,ResultsItem,TemplatePullItem) and the MCPget_evaluationcarrycounting. The rule names areITEM_COUNTINGS/ItemCountingin@quiz/domain/grade. - The builder is unchanged (ADR-052 §2): its toggle offers normal ↔
bonus only (
ItemPatch.countingisBuilderCounting), locked byassertItemListEditable. - The grading-time act is
PUT /evaluations/:id/items/:itemId/counting(ItemCountingBody, any of the three states), the opposite gate of the builder's: allowed only once the item list is frozen (itemListLocknot null), else409 counting_not_frozen; never on a poll (422 counting_poll); never down to a total of 0 (409 illegal_transition, reasonno_graded_points,lacksGradedPoints). It is loaded through the staff access loader of the grading routes; after the release it is the course owner's (403 owner_required, ADR-068 §3). Allowed as soon as the list is frozen, including while an exam or an exercise is still open: deliberate, since an exercise runs for weeks and takes (partial) retakes. Reversible any number of times; choosing the current state changes nothing. Audited asevaluation.item_countingwith the item id, before and after. - ONE floor, at sum time; never rewrite a grading. Stored gradings are
never touched by the act, and an automatic grade is stored RAW whatever
the counting:
itemPoints(raw)only rounds (amends ADR-052 §3, which floored a bonus item at write time). ONE pure function,countedPoints(points, counting)— neutralized → 0, bonus →max(0, p), normal →p— is applied byattemptTotal, which now takes{ points, counting }cells, so every student sum goes through it: the results grade table, the kept attempt,pointsByAttempt, the dashboard, the preview, the CSV total, the gradebook, the live grades, the release snapshot. The cell a student's copy, the preview and the results grid SHOW isshownPoints: a bonus floored (never "−4 / 2"), a neutralized one as stored, struck through. The grading panel shows the stored points, which the teacher edits; a manual correction of a bonus item stays in[0, max](scoresNegatively). Because the floor is never written, a change of counting is reversible: a bonus made normal again counts its negative points. Limit: bonus gradings written before this decision were floored at write time and stay so; made normal, such a cell counts 0, not its lost negative score. - The total (
evaluationTotal, andtotalPointsByEvaluationin SQL) leaves out both bonus and neutralized items. - After the release: the act marks the evaluation modified after
publication in the same transaction (
setModifiedAfterRelease), so the frozen snapshot stops being served, andresults_updatedtells only the students whose shown grade changed —watchReleasedGradesbefore, itsannounceafter the commit, reading the points as they stand (pointsByAttempt). - The student: a neutralized item stays on the feedback page with the
answer, labelled "Not counted", its points struck through; a bonus keeps
"Bonus question". A neutralized item's ungraded answers hold nothing back
(they are not pending,
countsForStudent), nor on the teacher's grading progress and queue counts. It creates no drill card (ADR-041 §1) from then on; limit: the cards an exam's release or an exercise's hand-in already created are kept, not retired, when the item is neutralized later. Item statistics are unchanged. - CSV: a neutralized item's header ends with
(not counted), beside the(bonus)rule (headers stay English, liketotal); its points are shown and left out oftotal. - Copies:
copyEvaluation(duplicate, save as template, instantiate) and a template pull copyneutralizedasnormalandbonusasbonus(copiedCounting). The grading act never modifies a template. - The screen: the grading panel's question bar has a secondary ⋯ menu
("Count normally / Count as bonus / Do not count", the current one
ticked, a choice that would leave no total greyed out). A choice asks
through
useConfirm()with the new total ("5 → 3 pts") and, once released, that students whose grade changes are told. The item's "Bonus question" or "Not counted" label sits beside the points badge.
Consequences¶
- Correcting an incoherent question costs one menu choice instead of a re-grade of every copy, and stays undoable.
- Grades can move after the release without any grading write; the
existing "modified after publication" flag and
results_updatedcover it. attemptTotalreads per-cell rows with their item's counting instead of a SQLsum, a little more data for the same number of queries.
Alternatives considered¶
- A second boolean
neutralizedbesidebonus. Four combinations for three states, one of them meaningless; the product owner chose one state. - Rewrite the stored gradings (floor or zero them) when the counting changes. Not reversible, and it would lose what the student actually scored.
- Remove the item. Destroys the answers and the grading history, and is refused once an attempt exists (F-EVAL-03).