Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
35 changes: 35 additions & 0 deletions docs/design/exam-tex-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 25 additions & 2 deletions references/reference_exam.tex
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Loading