ADR-088 — The exam integrity journal: leaving the page and pasting from outside, recorded lightly, never proof¶
Status¶
Accepted (2026-10-09, decided by the product owner). Migration
0094_integrity_journal_off_purge.
Scope: the attempt journal's student-behaviour kinds (INTEGRITY_EVENT_KINDS,
packages/contracts/src/live.ts), the logVisibility setting and its default
(logVisibilityDefault, integrityJournalOn, @quiz/domain), what
POST /app/api/attempts/:id/events stores (storesIntegrityEvent,
apps/api/src/modules/live/integrity.ts), the journal's retention (the
release, the integrity.purge scheduled task), the player's signals and toast
(apps/web/src/attempt/signals.ts), and the teacher's list.
Relations: amends F-EVAL-13, F-EVAL-33 (the platform's line), N-SEC-10,
N-DATA-02, N-DATA-03 and N-DATA-07; refines ADR-079
§2 (a poll never shows the derived line visibility_logged, which names
leaving the page and pasting from outside it); relies on
ADR-034 (delegated sessions) and
ADR-051 (confined sessions).
Amended 2026-10-15 (§6 and §8, what the student is told, issue #868): the
"Data and privacy" page of N-DATA-07 (/privacy) says, beside the condition
line and the toast, that leaving the page and pasting from outside are
recorded (the length, never the content), that the teacher sees it, and when
it is deleted.
Context¶
F-EVAL-13 and N-SEC-10 already journal tab visibility and window focus during
an attempt: the player posts visibility and focus to
POST /attempts/:id/events, the server writes them to attempt_events, and
the teacher reads them in the attempt inspector. Three gaps:
- the server stored those events whatever
logVisibilitysaid; the switch only drove the condition line the student reads; - the most common shortcut in a written exam — pasting a text prepared elsewhere — is not seen at all;
- the journal was kept as long as the evaluation, although it only serves the correction, and N-DATA-03 said nothing about it.
The product owner asked (2026-10-09) for a light detection of focus loss and of pasting from outside the page, a toast telling the student it is recorded, and a discreet list for the teacher (name, time, kind), never blocking and never presented as proof.
Decision¶
-
Not proof, by construction. The journal is a hint for the teacher's attention, never evidence. Its limits are stated wherever a teacher reads it: the listeners run in the student's browser and can be disabled, so the absence of an event proves nothing; a paste inside an embedded frame (a question type's own iframe) is not seen; copying from another Quiz tab looks external, since the copy fingerprint is per tab. Nothing is ever blocked, and no grade rule reads the journal.
-
One setting, a default by mode.
logVisibility(the field keeps its name) covers leaving the page AND pasting from outside it. At creation it is on for anexamand off for anexercise, where the teacher may switch it on (logVisibilityDefault, applied once increateEvaluation, whatever the preset); a copy (duplicate, template, instance) keeps the source's value, and an evaluation already stored keeps its own. Apollnever journals, whatever its row says (integrityJournalOn). -
The server honours it. An event of an integrity kind is stored only while the evaluation keeps the journal and the session is the student's own. Otherwise it is answered
204like a stored one, and dropped: never from a delegated session (impersonation, ADR-034, which is read-only in production anyway). The teacher's preview is stateless and has no attempt to write to. Under a confined session (seb,kiosk, ADR-051) the player journals and the server stores exactly as in the portal — the conditions line announces the journal there too, so it must stay true; only the student's toast is suppressed there (§6), since leaving the page is barely possible and a toast would be noise.reconnectis not an integrity kind and keeps its behaviour (stored, and a sign of presence). The one-off migration deletes thevisibilityandfocusrows already stored for an evaluation whose switch is off, and for a poll. -
Pasting: a length and a flag, never the content. The player sends
paste {length}— the number of characters, nothing else; the strict schema refuses any other key with a400. The server stores it as{ length, afterFocusLoss }(recordPaste), the flag computed by the SERVER, never sent by the client, by one rule on its own times: the attempt's latestvisibilityorfocusentry is an absence still open (hidden,focused:false), or a return to the page received within the last 60 s. No such entry, or a return older than that, isfalse. At most 30 pastes per attempt are stored a minute; the rest are answered204and dropped, like an event the switch does not keep. The player reports a paste or a drop (paste,dropin the capture phase, never prevented, so an editor still receives it) only when its text, whitespace collapsed, is at least 20 characters long and none of the tab's last five copies (copy,cut,dragstart: the selection, and what the page's own handlers put on the clipboard) holds it. Those fingerprints stay in the tab's memory. The pasted text is never sent nor stored. -
The client filters its own noise. An absence shorter than one second is dropped, and a blur into an in-page iframe (a question's own editor) is not a focus loss. The durations the teacher reads are derived by the server from consecutive events, indicative only, and capped at the attempt's end.
-
The toast. When the student comes back to the page, or pastes from outside, a toast says: "This event is recorded and visible to your teacher." At most one per kind every five minutes, shown on focus return (never during the absence) or at the paste, and never under a
seborkiosksession. The conditions linevisibility_loggedreads "Page exits and outside pastes recorded", and says that the pasted content itself is never recorded. -
The teacher's view. A discreet list in the attempt inspector and the dashboard: student, time, kind, the paste's length and flag. The live badge travels on staff topics only. There is no student view of the journal: the student knows the rule (the condition line, the toast), not the record.
-
Retention. The integrity rows (
INTEGRITY_EVENT_KINDS:visibility,focus,paste) are deleted at the release of the grades, in the release's transaction (releaseResults), their count recorded in the release's audit entry (journalPurged); a re-release finds nothing left. An evaluation never released loses them six months after it closed (the dailyintegrity.purgescheduled task). No other kind is touched:runcarries the Run rate limit, andreconnect,ip_change,time_added,pausedandresumedare the sitting's own history. -
Delivery in four pull requests. (1) the server: the setting honoured, the default by mode, the migration, the retention, this record and the spec; (2) the player: the signals' filtering and the toast; (3) the
pastekind, its server-side flag and the client's fingerprint; (4) the teacher's list and live badge.
Consequences¶
- An exercise created from now on records nothing unless its teacher asks; the exams' behaviour is unchanged.
- A teacher who reads the journal after the release finds it empty: the release is the end of the correction. A grade dispute after the release cannot rest on it — which it never could, since it is not proof.
- The admin's task list gains
integrity.purge. - Adding
pasteis a one-line change toINTEGRITY_EVENT_KINDS: the storage rule, the release purge, the backstop and the inspector follow it.
Alternatives considered¶
- Blocking or warning before leaving. Refused: an exam must never stop a student, and a browser cannot forbid another window anyway; only a trusted client can (ADR-051).
- Storing the pasted text, or a hash of it. Refused: it is the student's data with no use the length does not serve, and a hash of a short text reveals it.
- A separate switch for pasting. Refused: two switches for one idea — "your behaviour on the page is recorded" — would need two condition lines and two defaults for no teacher need.
- Keeping the journal as long as the evaluation. Refused: it serves the correction only, and a hint kept for years starts to read like a record.
- Refusing the events with a 4xx when the switch is off. Refused: the client would have to know the setting to stay quiet, and a refusal storm is what the route was designed never to cause.