7. Incompatibilities and risks¶
Every known point where classroom and Quiz disagree, with its resolution and the task that carries it. A task that finds a new one adds a row here in its PR.
7.1 Model and rules¶
| # | Incompatibility | Resolution | Task |
|---|---|---|---|
| I01 | "classroom" = top unit bound to an org with an owner (classroom) vs a period instance of a course with course staff (Quiz) | a classroom-classroom becomes a Quiz classroom under a course chosen in the mapping file; the org becomes an optional link | M2-01, M1-06 |
| I02 | Staff per classroom, invited by e-mail before sign-in, teacher/assistant label vs course_staff requiring an account, no label |
D04; the migration reports pending seats and labels | M0-01, M8-01 |
| I03 | Owner-only actions in classroom | become staff actions (no owner in Quiz) | M3-02 |
| I04 | oidc_sub may differ per edu-ID client; Keycloak-era subs in classroom |
match on swiss_edu_id, then verified e-mail; login adoption if needed |
M0-02, M1-06 |
| I05 | Role computation differs (staff affiliation scope, pending seats) | recompute after import, report differences | M8-01 |
| I06 | Grades: forgeable CI points/max (doubles) vs Swiss 1–6 frozen at release | ported tables keep doubles; the gradebook converts; a project score counts only after a teacher release | M3-08, M5-03, D05 |
| I07 | Freeze: clock-driven (deadline + grace, push receipts) vs teacher-driven (release) | ADR-012 addendum: same principle, literal for projects, analogical for evaluations | M0-03 |
| I08 | Deadlines: 20-s ticker, 30-min grace, late flagged vs 1-s ticker, 3-s grace, late refused (410) | projects keep their grace and late flag; project sweeps run on Quiz's ticker with everyMs |
M3-05 |
| I09 | Accommodations (time bonus) exist only in Quiz | D13 (suggested: not applied to projects; manual unlock covers it) | M3-05 |
| I10 | scheduled_tasks (admin-configurable, restart-safe) vs in-memory everyMs |
port the table (D10) | M2-05 |
| I11 | Classroom e-mail kinds and unsubscribe links vs ADR-030 notification kinds and channels | new kinds with per-kind defaults; unsubscribe ⇒ settings redirect | M3-09, M8-02 |
| I12 | Two closed audit unions; ~70 classroom actions; imported history | project/github actions added to Quiz's union; history into legacy_classroom_audit_log (D11) |
M3-*, M8-01 |
| I13 | Poll = an evaluation mode, anonymous polls have no classroom | not an activity kind of its own; a poll is listed as kind: "evaluation"; the future classroom list (M1-03 "As delivered") never returns anonymous polls |
M1-03 |
| I14 | Roster status column vs user_id IS NULL |
derived | M1-06 |
| I15 | Classroom domain/roster.ts duplicates Quiz's |
keep Quiz's (classroom's + time bonus), port classroom's tests if they add cases | M1-01 |
| I16 | GitHub wire names carry classroom's words: the GRADE annotation title, the grade-final / grade-milestone dispatch events and their client_payload keys (assignment_id, milestone_id, milestone), read by the grading.yml already in student repositories |
kept verbatim on the wire; Quiz's identifiers use §7.4's words (extractScore, planFinalReviewDispatch, planCheckpointReviewDispatch, checkpointDueAt) — renaming the wire would break every live repository |
M1-01, M3-04, M3-05 |
| I17 | Pure rules that live inside ported adapter files: parseStudentIgnore (C:github/studentize.ts), zurichIso (C:github/commit.ts), repoName (under the journal's journalRepoName) |
in @quiz/domain since M1-01 (studentIgnore.ts, zone.ts as zonedIso, repoName.ts): apps/api/src/github/ imports the first two instead of keeping copies; packages/docrender builds journalRepoName on @quiz/domain's repoName. rateLimitReset (C:github/metrics.ts) reads HTTP headers: transport knowledge, it stays in github/metrics.ts |
M1-01, M1-02, M4-01 |
| I58 | activity_available is kind-neutral since M1-03, but its sentences say "exercise" and its Teams template parameter is still {evaluationTitle}, declared by the app every user installed (found by M1-03) |
kept as is; a project reusing the kind (rather than D18's project_published) needs kind-aware sentences, a renamed parameter and a TEAMS_APP_VERSION bump |
M3-09, D18 |
| I59 | activity_scheduled folds per (user, classroom) and its sentences count "exercises"; deadline_approaching, results_released and results_updated stay evaluation-shaped (evaluationId, found by M1-03) |
a project gets kinds of its own (D18's suggestion) — or, if it shares these, the fold key gains the activity kind (a migration of the partial unique index) and the payloads become kind-neutral like activity_available |
M3-09, D18 |
| I65 | The journal's stored copy is not portable: classroom's journal_pages.html links assets at /app/api/journals/<jid>/assets/…, its warnings and sync_error are English sentences, and its renderer never decoded a percent-escaped relative link (handout%201.pdf resolved to nothing, where github.com finds the file) |
the cutover never copies html, toc, warnings nor sync_error: every classroom row is re-rendered by @quiz/docrender (classroom-scoped asset URLs, warning codes, escapes decoded); heading ids are unchanged (same slug, same 60-character cap) except for headings holding HTML entities, now decoded before the slug, so deep links survive; html becomes html_staff and html_student (§4.2 of 04-journal.md) |
M4-01, M8-01 |
7.2 Invariants of CLAUDE.md¶
| # | Invariant | What the merge stresses | Resolution | Task |
|---|---|---|---|---|
| I20 | 1. i18n | classroom teacher surfaces are literal English; server-built English toasts, e-mails, 409 messages, journal warnings | every port writes en + fr keys; structured AppNotice variants and codes; decide the language of text written into GitHub (D12) |
all web and API tasks |
| I21 | 2. one primary action | AssignmentDetail (57 kB), AssignmentForm (41 kB) |
redesign, not copy | M3-11, M3-12 |
| I22 | 3. no dev login in production | new secrets | production refusals for GITHUB_* and CODESPACE_* in config.ts |
M1-02, M6-06 |
| I23 | 4. toStudent single exit |
projects (statements, review text, CI logs), journal (drafts, visible_from), gradebook |
generalise: "activity content reaches a student only through its kind's student view"; leak tests per kind | M0-05, M3-09, M4-02, M5-03 |
| I24 | 5. server clock | classroom uses Date.now() and SQL now() |
app.clock.now() everywhere, TestClock tests; no GitHub call inside a tick |
M3-05 |
| I25 | 6. staffAccess |
staff keyed on classroom; webhooks have no user | loaders on the course; HMAC for webhooks, App JWT for setup; student loaders by own enrollment/group, 404 otherwise | M2-02, M3-02 |
| I26 | 7. contracts | classroom declares TS interfaces, inline zod | everything in packages/contracts; webhook fields read by the server validated with zod |
all API tasks |
| I27 | 8. pure rules in domain | small, pure; a markdown renderer does not belong in domain |
domain ports in M1-01; renderer in packages/docrender |
M1-01, M4-01 |
| I28 | 9. audit union | see I12 | ||
| I29 | 10–14. runner | codespace mounts a volume and opens a git channel; invariant 14 is meaningless for a repository | scope 11–12 to apps/runner; codespace's own CLAUDE.md; add "a project's source is fetched by the server at the frozen sha, never uploaded by a client" |
M0-05, M6-03 |
| I30 | A table belongs to one module | webhook handlers touch project rows; group repos | handler registry (project → github); receipts written by github synchronously; gradebook writes only its own table |
M2-04, M3-04 |
7.3 Security and operations¶
| # | Risk | Resolution | Task |
|---|---|---|---|
| I40 | Staging restores production dumps (ADR-028): with the production App key, staging's ticker would act on real student repositories | a separate staging App on a test org; tasks no-op without an App; the refresh nulls installation_id |
M2-06 |
| I41 | SSE classroom: topic reaches students unfiltered in Quiz: per-repo hints there reintroduce classroom #38 (DoS, grade leak) |
per-repo hints to course: + user: topics only; a test that a student never receives another student's hint |
M3-04 (the hints, tested), M3-09 |
| I42 | Forged CI grades (classroom H5) | indicative until a teacher release; the gradebook shows the source | M3-08, M5-03 |
| I43 | Journal assets of hidden pages readable (J1) | visibility-aware asset route | M4-02 |
| I44 | Concurrent journal ingestions (J2); J3 dropped (D03) | queue or advisory lock per classroom journal row + transaction | M4-02 |
| I45 | One webhook URL per App: no parallel run | resolved by D23: Quiz has its own App, both run in parallel; organizations install Quiz's before the cutover | M2-06, M8-05 |
| I46 | Launch tokens logged by the codespace's Caddy | mask the query string | M6-05 |
| I47 | Engine VM: no backups, 2 sessions of capacity, runner and codespace compete | resize, cgroup slices, off-VM backups | M6-05 |
| I48 | Seccomp profiles diverged | port the runner's tightenings to codespace, keep ptrace |
M6-05 |
| I49 | ADR-027 rejected self-contained signed tokens | cross-VM HS256 kept as a recorded exception (the portal enforces single use) | M0-03, M6-01 |
| I50 | App permission Secrets:read used but undocumented |
granted to Quiz's App at registration (D23); the probe reports "unknown" on an installation that has not approved it. Dropped 2026-10-14, M3-14f: no probe, no permission | M2-06 |
| I51 | Rate limits: 5 000 req/h per installation; anonymous org checks share the VM's 60/h | keep noRateLimitWait, SWR cache, backoff, hint coalescing; Quiz's 1-s live streams never trigger a GitHub fetch |
M3-08 |
| I52 | node:24-slim has no git |
add git + ca-certificates |
M1-02 |
| I53 | App VM memory (1 vCPU / 2 GB) grows before classroom goes away | watch it through M3–M8; net gain after cutover | M8-05 |
| I54 | Dev in-process queue has no retries; no webhooks in dev | tests call handlers directly; Refresh / mock paths | M2-04, M4-02 |
| I55 | The audit table is append-only in production | dry-run until clean | M8-06 |
| I56 | Classroom keeps changing until the cutover | sync point in PROGRESS.md; forward fixes |
every phase |
| I57 | Rulesets hgc-protect and hgc-deadline-lock may name classroom's App as a bypass actor: Quiz's App (D23) could not then lock, revert nor dispatch |
the import writes nothing on GitHub, so it rewrites no bypass list (M8-01 as delivered). Staging's App reaches no production organization, so M8-06 cannot check this either. It is checked at the cutover on one imported repository (runbook C8, row 8), and the owner adds Quiz's App where needed | M8-07 |
| I60 | The deploy of M1-03 changes a stored payload shape: delivery jobs already queued carry the old activity_available, and a rollback past migration 0037 meets rewritten rows |
deliver parses the payload and drops a stale job with a warning; after a rollback the old reader drops the rewritten bells from the list (no failure) until the next deploy |
M1-03 |
| I61 | Classroom's verifySignature compares the hex STRINGS' lengths, so a 64-character header with a non-hex character decodes short and timingSafeEqual throws (a 500 instead of a 401); an empty secret is not refused |
fixed in the port (github/signature.ts: decoded lengths compared, empty secret accepts nothing, never throws); classroom should get the same fix |
M1-02 |
| I62 | Newer octokit types declare GitHub ids number \| bigint |
Number() at the five places the adapters keep an id (provision ×2, squash ×2, collaborators); ids stay below 2^53 |
M1-02 |
| I63 | The git bot identity of classroom is hgc <bot@hgc.local> |
Quiz's commits are heig-quiz <bot@heig-quiz.local>; harmless, bot pushes are recognised by the sender login <slug>[bot], not the committer; the ruleset names keep hgc-* (I57) |
M1-02 |
| I64 | Classroom clones with https://x-access-token:<token>@github.com/…: the installation token lands in the clone's .git/config under /tmp (outliving a crash) and in git's argv |
fixed in the port: plain remotes, the token reaches git only through the environment (GIT_CONFIG_* → http.https://github.com/.extraheader), tested on a clone; classroom should get the same fix |
M1-02 |
| I66 | Classroom's lazy healing re-points an organization row by LOGIN (C:modules/classrooms.ts:270-280, resolveOrgInstallation(org.login) then installationId/githubOrgId overwritten): a login freed by a deleted or renamed organization and taken by another one hands every classroom of the old row to the newcomer |
fixed in the port: a row is matched by github_org_id, by login only while it has none (M8-01 imports); a row holding the login under another id is marked deleted, its installation cleared, its login moved aside (<login>~<row id>), its links kept (recordInstallation, tested through the healing and the setup return); forward the fix to classroom |
M2-02 |
| I67 | Classroom builds the distribution repository before inserting the assignment, deletes it when the insert loses the slug race, deletes it with a draft, and builds it with execFileSync, which stops the whole process for seconds |
Quiz inserts the draft first (the UNIQUE decides the slug), deletes only the row on a failed build, never a repository (an empty leftover adopted, a non-empty one stepped over up to -squashed-20), and runs git asynchronously (ADR-062) |
M3-02 |
| I68 | Classroom lets a published assignment change its grace and grading mode, freezes only the deadline strategy (at publication) and the publication mode; its source, branches and source strategy are not editable at all | Quiz follows F-PROJ-03: the deadline strategy changes until the deadline; grace, grading mode and scale, start and groups freeze at publication (not_draft); the source, branches and source strategy are fixed at creation, as in classroom (projectFieldRefusal) |
M3-02 |
| I69 | Two leftovers of Accept no reconciliation repairs (reviewers of M3-03): a repository GitHub created whose id never reached the row (the process died between the create and the update) leaves that student on repo_name_taken; a student account renamed before success leaves a stray private repository, possibly still inviting them |
Reported only (product owner, 2026-10-05): a repository is the row's only if the row made it (rule of 2026-10-02), so reconcile.repos adopts nothing by name and looks for no stray repository; the staff recover the first through the organization's repository browser and the student's own resend, the second by hand on GitHub |
M3-06 |
7.4 Words¶
| Word | Quiz | Classroom | Resolution |
|---|---|---|---|
| classroom | period instance of a course | top-level, org-bound, owned | Quiz's meaning; classroom's maps to course + classroom |
| assignment | forbidden | the GitHub work item | Project everywhere; "assignment" only in migration code |
| activity | #190 section, activity_* kinds |
commit-graph panel | Quiz's; classroom's panel becomes "repository history" |
| journal | the attempt event log (spec 05 §5.5, ADR-018) | course documentation | classroom's; Quiz's becomes attempt log (D16) |
| milestone | evaluation navigation checkpoint | intermediate LLM review date | classroom's becomes review checkpoint |
| runner | apps/runner |
self-hosted Actions runner | ours; theirs are "CI runners" |
| template | evaluation template (ADR-031) | source/template repository | never call a source repository a template |
| grade | Swiss 1–6 | points/max | score (points/max) vs grade (1–6) |
| release | results published to students | validate grades | one verb: release |
| session | forbidden for evaluations | codespace session | workspace |
| staff | course seat | classroom seat with label | D04 |
7.5 Spec and ADR amendments (carried by M0-03 and M0-04)¶
- 00: main use, comparison table (GitHub Classroom column), objectives, constraints (GitHub linking ≠ sign-in; 100 repos at a deadline in < 5 min), phases, out of scope (Monaco-only applies to question editors; import classroom's exclusions), risks, decision log.
- 01: drop "no quiz, assignment, activity or session"; Activity, Project, Group, Student repository, Source repository, Grade run, Push receipt, Frozen score, Score vs Grade, Gradebook, Gradebook column, Journal, Online workspace, GitHub organization; ER diagram; Project lifecycle.
- 02: F-PROJ (from classroom's US/GH/GR), F-JRN (JN-01…43), F-GBOOK (F-RES-05 promoted); F-NOTIF-04/05/06 widened; F-ORG-01 (pending invites?), F-ORG-09 (deletion leaves repositories).
- 03: N-DATA-02 (GitHub identity, repositories), N-DATA-03 vs classroom H11 (never delete repositories), N-DATA-05 (commits carry names).
- 05: ADR list; stack (Octokit, code-server); layout (
apps/codespace, modulesgithub,project,journal,gradebook); tables; runner at a frozen sha (phase L); grading; §5.7 generalised; deployment (public webhook route, GitHub secrets, codespace on the engine VM). - 06: the open decisions of
08-decisions.mdthat are not settled. - 07: frozen as history, pointing to ADR-035 and
docs/merge/. - 08: a novice path for projects.
- ADRs: 029 superseded; 006, 007, 010, 012, 016, 027, 030 amended; classroom's 011 imported as 011, 013/014/015 as 047/048/049 (M0-03).