diff --git a/CHANGELOG.md b/CHANGELOG.md index e1d1d81..c370612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to lectern are documented here. - **Sanitization lint (`lectern.feedback_sanitize`).** Deterministic guard that withholds any student-facing comment leaking internal jargon (triage verdicts, honor-gate, advisory framing) or another student's name. Deliberately excludes crypto-colliding words (`oracle`, `digest`) so legitimate feedback isn't censored. ### Changed +- **Exam questions no longer split across page boundaries** (issue #29). `references/reference_exam.tex` now reserves `\qreserve` (default `5\baselineskip`) at the top of every top-level question `\item` via an `enumitem` `before`-hook (choices are exempt), glues each stem to its choice list (`\@beginparpenalty`), damps widows/orphans, and sets `\raggedbottom`. A stem stranded at the page bottom with its choices pushed to the next page is both a readability and an appeal problem ("I never saw part (d)") on paper exams. Template-only, automatic (no authoring change), and **parser-safe** — the `\item ... \textit{(N pts)}` pattern is untouched, so `exam_pack` form-splitting is unaffected. `\qreserve` is tunable per exam. A 40-question stress build splits 3 questions before and 0 after, same page count. Documented in `docs/design/exam-tex-format.md`; applies to exams authored from the template going forward (existing exam `.tex` are not backfilled). No Python change. - **`reg-lab-digest` results now carry a `student_comment`** — a sanitized, student-facing comment alongside the internal terse `comment`. One grading pass yields both; `merge` runs the sanitize lint and withholds the student comment on low-confidence/abstain or any lint hit. Additive, backward-compatible schema change to the Layer-2 contract (the grader-prompt doc documents the new field). - **True/False exam questions now use stacked `(a) True / (b) False` choices** — the house standard documented in `references/reference_exam.tex` and `docs/design/exam-tex-format.md`. The previous inline `\textsc{T~/~F.}` form listed no answer options on their own lines, so Gradescope's region detection could not find them. The `\textsc{T~/~F.}` label and inline `Answer:` reveal are retained, so questions stay typed `tf` and `parse_outline_from_tex` still emits `True`/`False` in `_outline.csv` — no code change, purely an authoring-convention fix. - **Refreshed the `examples/cecs-378-demo` worked example** to cover the current command surface. The demo exam gains a stacked-T/F section and a `gradescope: region` build (emitting `gradescope/` + `GRADING_NOTE.md`), and the README adds runnable stages for `reg-syllabus` (stamp + build), `reg-qbank` (validate + emit), `reg-exam-readinglist` (Lectern→Scriptorium seam), and `reg-gradescope-stats` (item analysis), plus a documented "requires live infrastructure" section for the Classroom / ISA-publish / triage / term-finalize verbs. diff --git a/docs/design/exam-tex-format.md b/docs/design/exam-tex-format.md index 47cfbf7..8e76a0b 100644 --- a/docs/design/exam-tex-format.md +++ b/docs/design/exam-tex-format.md @@ -261,6 +261,41 @@ answered with a specific slide and date. --- +## Page-break discipline — questions stay whole + +*(Added for issue #29.)* By default the template keeps each question from +splitting across a page boundary — a stem stranded at the bottom of a page with +its choices pushed to the next page is both harder to read and an appeal vector +("I never saw part (d)") on a proctored paper exam. + +How it works, entirely in the template preamble (no authoring change, no build +change): + +- **`\qreserve` reserve.** A `before` hook on the level-1 (question) `enumerate` + wraps each `\item` with `\needspace{\qreserve}` (default `5\baselineskip`). If + that much vertical space isn't left on the page, the whole question moves to + the next page instead of splitting. The level-2 (choice) `enumerate` restores + the plain `\item`, so individual choices are never space-reserved. +- **Stem↔choices glue.** `\@beginparpenalty=9999` discourages a page break + between a stem paragraph and its choice list. +- **Widow/orphan damping.** `\clubpenalty`/`\widowpenalty` at `10000`. +- **`\raggedbottom`.** Pages are allowed to run short rather than stretch to + force-fit a question. + +**Tuning.** `\qreserve` is a normal macro — raise it for choice-heavy or +code-heavy forms (`\renewcommand{\qreserve}{7\baselineskip}` after the preamble), +lower it to pack more per page. Very tall items (long code listings, `\writelines` +short-answer blocks) can still break across pages by design; the reserve only +guarantees the question *starts* with room for its opening lines. + +**Parser-safe.** The authored `\item ... \textit{(N pts)}` question pattern is +unchanged, so `exam_pack` form-splitting (`_QSTART_RE` / `_PTS_RE`) and outline +parsing are unaffected — the break control lives only in list metrics. + +**Verification.** A 40-question stress build (four choices each) splits 3 +questions with the pre-#29 template and **0** with the current template, at the +same page count. + ## Compile discipline Delete derived files before regenerating — synced binary files have a race diff --git a/references/reference_exam.tex b/references/reference_exam.tex index 9d32ae9..33493d7 100644 --- a/references/reference_exam.tex +++ b/references/reference_exam.tex @@ -148,9 +148,32 @@ \repeat \par\vspace{4pt}} +% --- Keep each question whole across page breaks (Lectern issue #29) --- +% A question that starts near the page bottom otherwise splits from its choices. +% Reserve \qreserve of vertical space at the top of every top-level (question) +% \item: if that much room isn't left, the whole question is pushed to the next +% page. Level-2 (choice) items are exempt via the enumerate,2 before-hook, which +% restores the unwrapped \item. \qreserve is tunable per exam (raise for +% choice-heavy forms, lower to pack more per page). Complemented by widow/club +% penalties, a stem<->choices no-break penalty, and \raggedbottom so pages run +% short rather than stretch to force-fit a question. Parser-safe: the authored +% "\item ... \textit{(N pts)}" pattern is unchanged, so exam_pack form-splitting +% (_QSTART_RE / _PTS_RE) is unaffected. +\newcommand{\qreserve}{5\baselineskip} +\clubpenalty=10000 +\widowpenalty=10000 +\makeatletter +\@beginparpenalty=9999 % discourage a page break between a stem and its choice list +\makeatother +\raggedbottom + % --- List metrics: editorial proportions, tightened --- -\setlist[enumerate,1]{leftmargin=2.4em, itemsep=0.45em, topsep=0.45em, parsep=0pt, label=\textbf{\arabic*.}} -\setlist[enumerate,2]{leftmargin=1.8em, itemsep=0pt, topsep=0.1em, parsep=0pt, label=(\alph*)} +% before-hook on level 1 wraps \item with \needspace (see issue #29 note above); +% level 2 restores the plain \item so choices are never space-reserved. +\setlist[enumerate,1]{leftmargin=2.4em, itemsep=0.45em, topsep=0.45em, parsep=0pt, label=\textbf{\arabic*.}, + before={\let\qsaveitem\item \renewcommand\item{\needspace{\qreserve}\qsaveitem}}} +\setlist[enumerate,2]{leftmargin=1.8em, itemsep=0pt, topsep=0.1em, parsep=0pt, label=(\alph*), + before={\ifdefined\qsaveitem\let\item\qsaveitem\fi}} % --- Code block style --- \lstset{