ADR-024 — One locked editor, three student tools, a visible cooldown¶
Status¶
Accepted (2026-09-24, with packages/qt-code/src/LockedEditor.tsx,
ArgsInput.tsx, lockEdit.ts and @quiz/domain/cooldown). Amended
2026-09-25 by the addendum at the end (issue #129).
Context¶
A review of the code question by the product owner, first as the author and
then as a student, found the screens explaining themselves instead of being
understood:
- the template editor carried a paragraph about
@@lock/@@endlock, and an author who naturally wrote@@unlockgot, without a word, ONE locked region running to the end of the file; - the player stacked one read-only block and one editor per region, so the compiler's "line 12" matched no line number the student could see;
- the reference solution asked the author to type
@@nextseparators by hand; - the arguments of a case were "one per line, blank lines ignored";
- the student had a Run, a free stdin box and — in the teacher's rehearsal —
a "Grade my answer" whose result, for
code, is the tests again; - the runtime choice (ADR-015) was worded by mechanism ("browser", "runner") and needed a sentence under it to be understood.
Decision¶
1. One editor, locked lines read-only, markers hidden¶
The player and the reference-solution field render the WHOLE program in one Monaco model. Locked lines are greyed and refuse edits; the marker lines are hidden but keep their place, so the line numbers are the source's and a compiler diagnostic lands on its line. The textarea fallback (no Monaco: a blocked CDN, jsdom) keeps the stack.
This reverses the "split, not decorate" choice of MonacoHost.tsx. The split
protected nothing: the server rebuilds the file from the stored template and
the regions (invariant 14), so a locked range undone from a devtools console
changes a display, never what is compiled or graded.
2. Locking by selection; @@unlock accepted; unknown markers flagged¶
The author selects lines and presses the lock button; the markers are written
for them, and stay visible in the author's editor, which is how the syntax is
learnt. @@unlock is a synonym of @@endlock. A marker-looking comment the
split does not know, or an unlock with no open lock, is reported with its line.
A lock left open to the end of the file stays legitimate.
3. The reference is written in the student's editor¶
The reference field shows the template with one editable region per template
region, prefilled with the template's own text. The stored format is unchanged
(@@next between pieces); the author no longer types it.
4. Three student tools, each with a cooldown and a reason to press it¶
Compile, Run the tests (primary), Free try. Each button is disabled while the
code — and for the free try, its input — is identical to its last completed
use: running the same program twice teaches nothing, and the last result stays
on screen. After each use the button refills before it is ready again:
fixed (3 s) or progressive (3 s, +30 % per use, capped at 30 s, forgiven
while the student does not press it). For a server run the cooldown is never
shorter than 60 s / runsPerMinute, so the button never offers what the
budget of N-SEC-07 would refuse; the budget itself stays the server's guard.
cooldown is a new field of the program config and the student view, with a
default, so stored configs parse unchanged and the config version stays.
A compile-only run (RunBody.compileOnly) goes through the same route, the
same budget and the same attempt log, with action: "check" and no case.
(Reversed on 2026-09-25: no "unchanged" rule, no cooldown on Compile, and a
budget of its own for it — see the addendum.)
5. Runtime by outcome, new C/Python questions in the browser¶
The runtime moves to "Advanced options" as "Instant" / "Same as grading".
New C and Python questions start with the browser runtime; stored configs keep
theirs (the zod default stays backend). When the reference passes on the
server but the browser disagrees on a case, the editor says so — that is where
"the browser runs, the server grades" is learnt, instead of a sentence.
6. The teacher's rehearsal of a code question is one action¶
"Run all the tests" grades the whole answer, hidden cases included, and shows the score. The student's own view never merges tests and grade: the hidden cases and the weights make "every visible test passes" differ from "every point".
Consequences¶
ProgramRegionsandReferenceSectiondepend on a Monaco integration that guards edits itself; its tests in jsdom cover the fallback only, and the Monaco path needs the browser check ofapps/web/scripts/screenshots.mjs.- A student's cooldown is a client courtesy; the server budget is unchanged
and still answers
rate_limited. docs/spec/04-types-de-questions.md§4.7 describes the new buttons, the markers and the reference editor.
Addendum — 2026-09-25: no "unchanged" rule, and Compile without a cooldown¶
Source: issue #129, from a teacher walking the student player.
What changed¶
- The "unchanged" rule of §4 is dropped. A button used to stay disabled
while the code (and, for the free try, its input) was what it last ran.
In practice the button looked dead once its cooldown ended, and the hint
under it ("Change your code to run the tests again") went unnoticed. A
button is now ready again as soon as its cooldown ends, whatever the code;
the last result of each tool still stays on screen until the next run.
Re-running the same program is harmless, and the cooldown already bounds
the load. The three
hints (
qt.code.p.unchangedTests,unchangedRun,unchangedManual) are gone. - Compile has no visible cooldown. It is the quick look a student takes
after every edit, and holding it back with the tests' clock made it the
slow tool. It is still one at a time (disabled while any run is in
flight). Run the tests and Free try keep their shared cooldown, and so
does
codeimage's Run. - Compile spends a budget of its own on the server. §4 said a
compile-only run spent "the same budget" as a run. It now counts against
compilesPerMinute(runsPerMinute) = min(60, max(20, 3 × runsPerMinute))(@quiz/domain/cooldown), and the test runs count without it: compiling never consumes a test run, and a spent test budget still lets the student compile. The attempt log is unchanged — a compilation is arunevent withcompileOnly: true— and that flag is what splits the two counts (countRecentEvents). The teacher's preview keeps the same rule under two in-memory keys. The runner priority of a compilation staysinteractive.
Why these numbers¶
Without a visible cooldown a student's compilations are bounded by the compiler's own latency, about a second on the runner. Three compilations per test run — one every 2 s sustained at the default 10 runs a minute — is above what a person editing code does and well below a held-down key or a script. The floor of 20 keeps a question with a tight run budget (1 or 2 a minute) compilable; the cap of 60 keeps the most generous question at one compilation a second per student, so the budget still guards the runner. It is derived, not configured: no new config field, no config version, nothing for the author to tune.
Consequences¶
- A host now reports a 429 of
POST /attempts/:id/run(and of the preview's/run) asrate_limitedinstead of folding it intounavailable, and thecodeplayer words it ("Too many runs in a minute…",qt.code.p.rateLimited, formerlyqt.codeimage.p.rateLimited). With Compile uncapped in the UI, its budget is the one a student can actually meet. - The cooldown floor
60 s / runsPerMinutestill holds for the test runs, so the test budget is never what a student runs into by clicking.