slide-review
Render the deck, gate it on a headless-Chrome fit check, screenshot every slide, then hand those PNGs to
reviewer subagents that judge from pixels. The predecessor skill read .tex source and guessed at
overflow by counting characters, which is how a deck reaches the podium with its last bullets below the
bottom edge. Nothing here infers layout from source. A browser measures it and the reviewers look at it.
Same loop as compile-latex --figures: render, rasterize, let an agent with eyes judge the image,
report exact fixes. This skill reports and does not edit, because a deck revision is the author's call.
Descends from the slide-excellence orchestrator, the create-lecture workflow, and its four review
agents in pedrohcgs/claude-code-my-workflow.
Where things live
The stages stay here; the detail sits in files this skill reads on demand. Paths are relative to
this skill's directory. $ASSETS is ~/.claude/assets/quarto-yale, and its README.md is the
toolchain's own manual: gate internals, filter behaviour, theme classes, the offline recipe. Facts
that README owns are cited below, never restated.
| File | What it holds |
|---|---|
scripts/capture.mjs |
Stages 4 and 5 in one run: launches a headless Chrome of its own over CDP, opens the deck over file://, runs both probes, and writes the PNGs. |
scripts/probe.js |
The stage 4 probe. capture.mjs reads it and runs it in the page. |
scripts/figure-ground.js |
The stage 4 figure-ground pass. Same. |
references/probe-reading.md |
The theme ink tables with measured ratios, the font-size and contrast thresholds, the four math-failure kinds, the dark-deck defect classes. |
references/reviewer-prompts.md |
The four reviewer prompts, the settled-decisions paragraph reviewers receive, and the reply contract. |
references/failure-taxonomy.md |
The full failure table. The eight commonest rows are inline below. |
style/house.md |
This author's expectations: the closing-slide contact block, density calibration by deck type, settled design decisions, the author line. A public fork swaps this file. |
Every browser here is headless and private to the run
The capture path never touches Claude in Chrome. The gate scripts and capture.mjs each launch their
own headless Chrome over CDP, open the deck over file://, and exit, so runs cannot collide, no server
is started, and no permission prompt fires. Claude in Chrome is the user's visible browser at whatever
window size they have, with no headless mode, so it cannot deliver the fixed 2334x1556 viewport the
screenshots and the reviewers' arithmetic depend on. Fan out reviewers only after the PNGs are on
disk; they read image files and touch no browser.
The measurements are split by what each pass can see. deck-check.mjs owns geometry (stage 3).
capture.mjs owns what needs a rendered page at review size: the deck's ground colour, contrast
against composited backgrounds, the math-engine signals, and the screenshots.
Inputs
| Argument | Default | Meaning |
|---|---|---|
| positional | required | A .qmd to render, or an already-rendered .html to review as-is. |
--preflight |
off | Stages 0 to 4 only (type, render, fit, probe, and the offline check if the deck asked for it), no reviewer fanout. Use in the hour before a talk. |
--type=talk|lecture |
detected | Which lenses to run and which contrast expectations apply. Detection in stage 0. |
--slides=1-12 |
all | Capture and review a range only. 1-based, matches the on-slide number. |
--goal="..." |
none | What the deck should accomplish. Passed to the argument reviewer verbatim. |
If the positional is a bare filename, Glob for it under the current project before asking. No config
file is read.
Setup
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
ASSETS="$HOME/.claude/assets/quarto-yale"
RUN="$HOME/.claude/state/slide-review/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$RUN"
Preflight quarto, python3, node (22 or later), and a Chrome binary (/Applications, or
CHROME_BIN). Missing quarto or python3, surface SETUP_MISSING:<tool> and stop. node and Chrome
are needed for the stage 3 gate and the stage 4 capture, neither of which is optional; a missing
Chrome is SETUP_MISSING:chrome, never a reason to read the source, which is the failure this
skill replaces.
There is no Homebrew on this machine, so no pdftoppm, no pdftotext, no ImageMagick. Do not reach for
them. Every PDF goes through ~/.claude/assets/bin/pdfread.py (see stage 5), including any
PDF you need to look at, because Read cannot open one here.
Stage 0: deck type and ground
Settle this before running anything, because the whole review branches on it. Deck type picks
the lens list (a lecture gets the pedagogy reviewer, a talk does not), the text-density standard
(deliberately opposite targets per type; style/house.md), and the closing-slide expectation
(stage 3c). The ground branches the contrast arithmetic: the shipped starter theme is dark ink on a
paper ground, and a deck on a designed dark theme flips the ground every contrast number is taken
against, so ink that is correct on one is invisible on the other.
Read the theme and the other front matter facts in one pass. In a Quarto website the format block
usually sits in a _metadata.yml beside the .qmd, so search both:
grep -nE '^\s*format:|^\s*theme:|html-math-method|embed-resources|self-contained-math|highlight-style|stage-slide' \
deck.qmd _metadata.yml "$(dirname deck.qmd)/_metadata.yml" 2>/dev/null
For a rendered .html with no source, fingerprint the class names, since the two deck types use
mostly distinct vocabularies:
grep -coE 'class="[^"]*\b(assumption|proposition|lemma|takeaway|result|thanks-slide|ymid)\b' deck.html # talk
grep -coE 'class="[^"]*\b(keyidea|definition|question|prompt|agenda|steps|demo-tag|hero|ypale)\b' deck.html # lecture
Whichever count is higher wins. A tie, or zero both ways, means ask. The root font size is a second signal when the themes in play differ, since a lecture theme typically runs a larger root than a talk theme (the starter theme serves both types at 30px). The stage 4 probe reports the root size and the deck's ground colour, so confirm the answer there before writing the report. A deck reporting pure white when its theme sets a tinted or dark ground means the theme did not load, which is CRITICAL on its own.
What the deck's ink should measure, hex by hex, comes from the theme's own palette table;
references/probe-reading.md opens with the starter theme's. The class vocabulary is the README's
Theme classes section; a cheap single fingerprint is that talk sources use .ymid where
lecture sources use .ypale (the starter theme styles both).
.section-break styling is per theme, so old advice about it goes stale, and
the README's Things that will silently break the deck section has the current rule: no
background-color attribute on a section divider, whatever the theme. On a dark deck the attribute
is also a defect
the probe can see, because reveal's has-dark-background then forces that slide's body text to pure
white (verification in references/probe-reading.md, dark defects). Do not carry an old
hard-coded background-color fix forward from memory.
Stage 1: render
cd "$(dirname deck.qmd)" && quarto render deck.qmd --to revealjs 2>&1 | tee "$RUN/render.log"
On non-zero exit, extract the error from the log, surface RENDER_FAILED:<message>, and stop. Never
review a stale HTML from a previous render.
Then mine the log, because pandoc reports two classes of defect that no amount of looking at pixels will reveal:
grep -nE '^\[WARNING\]|^ERROR|Could not fetch|not found' "$RUN/render.log"
Verified output on a deck citing a key absent from the .bib and embedding a missing figure:
[WARNING] Citeproc: citation ghostcite2019 not found
[WARNING] Could not fetch resource figures/does-not-exist.png
Both are CRITICAL. An unresolved citation prints the raw bibtex key on the wall.
There is no post-processing step. A correctly configured deck comes out of quarto render ready to
review, and any offline claim gets confirmed in stage 2. Skip stage 1 entirely when handed an .html.
Stage 2: offline check, only when the deck asked for it
This used to be a mandatory gate and is not one any more. Decks now default to MathJax loaded from a
CDN, so an external MathJax reference is the expected state of a correct deck, and reporting it as a
defect sends the author after a bug that is not there. Offline is an opt-in variant, declared in
front matter as embed-resources: true together with html-math-method: katex. Run the check only
in that case, using the facts stage 0 already read:
python3 "$ASSETS/check-offline.py" deck.html; echo "exit=$?" # opt-in decks only
Say which branch ran, in the report, on its own line. A default deck gets
Offline-safe: not checked (CDN MathJax variant, offline not requested). An opt-in deck gets the
output quoted verbatim near the top, because a deck that loses its math on a conference room
projector with no wifi is the worst outcome this skill exists to prevent. A pass lists external
hosts and both runtime loader fields as none and ends OFFLINE-SAFE : YES (the field is
padded, so grep the label and read the verdict, per the README).
On an opt-in deck, exit 1 with OFFLINE-SAFE : NO is CRITICAL, and the report names the specific
cause. Two causes account for nearly all of them, and the README's Things that will silently break the deck section owns the mechanics of both:
- Any
cdn.jsdelivrhit or leftover katex runtime loader meanshtml-math-methodwas written in the object form, which Quarto's katexPostProcessor silently ignores. The fix is the README's offline recipe: the bare stringkatexplusself-contained-math: trueunderembed-resources: true. With that, Quarto embeds KaTeX and its fonts itself; verified at 4.95 MB with no external hosts, no runtime loaders, and no post-processing of any kind. - The base theme was swapped off
defaultonto one of the eight built-in reveal themes that@importGoogle Fonts, which no Quarto setting can make offline-safe; the README names the eight and the four clean ones. CRITICAL, with the fix being a return to[default, <your-theme>.scss](the starter extension layersstarter-theme.scssondefault).
Run this grep on every deck, offline variant or not, because a swapped base theme also changes the variables the deck's own SCSS layers onto, and it can quietly undo the ground the rest of this review measures against:
grep -nE '^\s*theme:|fonts\.googleapis|fonts\.gstatic' deck.qmd deck.html
Ignore the unconverted math count. Quarto's katex mode leaves <span class="math"> in the file on
purpose and converts it in the browser, so a non-zero count there is normal and not a finding.
Stage 3: fit gate
Run this on every deck, before touching the browser. It is the one mandatory gate, whatever the theme and whatever the math engine. It owns every geometric finding in the report.
node "$ASSETS/deck-check.mjs" fit deck.html; echo "exit=$?"
node "$ASSETS/deck-check.mjs" fit deck.html --json > "$RUN/fit.json"
node "$ASSETS/stage-check.mjs" deck.html; echo "exit=$?"
The second gate is the step-0 check: every content slide must open as its heading alone, and every
press must change visible ink. A clean deck prints STEP-0-CLEAN: YES and exits 0. It fails with
LEAKS AT STEP 0 on content visible before the first press, and with
DEAD STEP: press 2 changes nothing or DEAD STEPS: presses 2, 4 change nothing on a press the
room cannot see, presses counted from 1. Both are CRITICAL. Name every slide it lists, and quote the
press numbers: the fix is per press, not per slide. Nothing else in this review can find either, and
the walk adds about two seconds. Mechanics, slide-kind classification, and what produces a dead
step are the README's staging section, under Checking it.
deck-check.mjs fit prints the canvas, one line per slide, then a verdict, and exits 1 whenever the
verdict is NO, so the gate composes: node "$ASSETS/deck-check.mjs" fit deck.html || <report>.
Verified output on a deck whose third slide is overstuffed, where quarto render had exited 0 in
silence:
canvas 1050x700, 5 slides
1 ok Deliberately Broken Test Deck
2 ok Motivation
3 OVERFLOW +547px Results
4 ok A formal result
5 ok Conclusion
DECK-FITS: NO
The status tokens, and what each one is worth in the report:
| Token | Meaning | Severity |
|---|---|---|
ok |
Slide fits. | none |
OVERFLOW +Npx |
Content past the bottom edge. Gone from the screen. | CRITICAL |
TOO WIDE +Npx |
Content past the right edge. | CRITICAL |
N MISSING IMG |
An <img> that resolved to nothing. |
CRITICAL |
N CRUSHED FIG |
A figure scaled down far enough to be unreadable. | MAJOR |
FIG SHRUNK TO Npx |
A figure authored at 1200px or wider that auto-stretch scaled under 600px. |
MAJOR |
N UNRENDERED MATH |
A span.math the engine never touched. Raw TeX on the wall. |
CRITICAL |
NEARLY EMPTY |
Effectively blank slide. Prints "nearly empty (warning, not a failure)" and does not fail the gate. | MAJOR |
DANGLING TARGET #id |
A .jump button whose target id resolves to no slide. Reveal lands on the title slide instead, silently. Fails the gate. |
CRITICAL |
UNPAGINATED BIBLIOGRAPHY |
div#refs holds entries, which means the deck set a bibliography: and forgot citeproc: false, so pandoc rendered a second copy of the whole reference list onto one slide. Fails the gate. |
CRITICAL |
no [.jump-back] on #id |
A jump target with no way back. Warning; leaves the exit code alone. | MAJOR |
The exact measurement condition behind each token is the README's fit-gate table. UNRENDERED MATH
works under either engine and is what catches broken math delimiters, which under MathJax leave the
literal \(...\) in the paragraph and produce no container at all for the stage 4 probe to find.
The tokens are separate and a slide can carry more than one (4 1 MISSING IMG and
5 TOO WIDE +308px on the same run, verified). Carry each token and its number into the report
verbatim; never re-derive either.
NEARLY EMPTY is the one token that leaves the exit code at 0. A blank slide is still a defect the
audience sees, so report it, but do not describe the gate as failing on it. The common cause is a
bare macro block before the first ##; the fix is ## Notation {visibility="hidden"}, per the
README's Things that will silently break the deck.
deck-check.mjs drives its own headless Chrome over CDP on node 22, needs no npm install, honours
CHROME_BIN, and navigates slide by slide with fragments forced visible, which is the only way the
numbers come out right (README, fit-gate section). Do not write your own overflow probe: the obvious
one-pass snippet reads scrollHeight 0 off every non-present slide and reports a broken deck as
clean, as the README explains. deck-check.mjs handout deck.html out.pdf exports a PDF; offer it
when the user wants a handout, and use it as the capture fallback in stage 4.
Stage 3b: jump buttons and the progress bar
The staging filter gives every deck two behavioural features no static screenshot can check, and both fail silently.
Run this only on a deck that has an appendix or .jump spans. Stage 3 covers the static half from
its own headless Chrome; the interactive half runs in a browser you can script, below:
grep -c 'class="jump-btn' deck.html # 0 means skip the jump half
grep -c 'appendix-break\|section .*appendix' deck.html
What has to be true, and what each failure means:
| Check | Failure means |
|---|---|
.progress carries metered when the deck has an appendix |
The filter's script did not run, so the appendix is back in the denominator. MAJOR. |
.progress carries segmented when the deck has two or more .section-break dividers |
Same cause. MINOR on its own. |
--section-cut-N values equal 100 * i / (main - 1) for each divider at flattened index i, where main is the index of the first appendix slide |
A cut in the wrong place, which is worse than no cuts. MAJOR. |
The fill span's scaleX is 1 on the last main slide and on every appendix and references slide |
The exclusion is not working. MAJOR, and the symptom the feature exists to prevent. |
Every a.jump-btn[data-jump] target resolves to a slide |
A dangling target lands the presenter on the title slide mid-question. CRITICAL. |
Every appendix slide reachable by a jump carries a .jump-back |
A one-way trip. MAJOR. |
A jump then a back returns to the origin's {indexh, indexv, indexf} exactly |
The handler is not capturing the origin before reveal navigates. CRITICAL. |
Stage 3 already ran the dangling-target check: deck-check.mjs fit fails on
DANGLING TARGET #id "label" and warns on a jump target with no .jump-back, so read its
jump-button block before doing this by hand, and carry its label and id into the report verbatim.
The equivalent by hand, on an HTML you were handed: open the deck in Claude in Chrome (file:// is
fine there) and run this with its JavaScript tool:
() => [...document.querySelectorAll('a.jump-btn[data-jump]')].map(a => {
const id = a.getAttribute('data-jump'), el = document.getElementById(id);
const sec = el && el.closest('.slides section');
return { label: a.textContent.trim(), target: id,
resolves: !!sec, isAppendix: !!(sec && sec.classList.contains('appendix')),
hasBack: !!(sec && sec.querySelector('a.jump-btn.jump-back')) };
})
Report resolves: false as CRITICAL with the label and the target id. hasBack: false is MAJOR:
the presenter can get there and not back. isAppendix: false is not a finding; a jump to a main
slide is legitimate.
The round-trip check needs interaction, so it belongs in one script that drives reveal itself, run
the same way:
put the origin slide at a known fragment step, read Reveal.getState(), dispatch a bubbling click
on the button, dispatch one on the .jump-back it lands next to, and compare Reveal.getState() to
what you stored. A synthetic bubbling click is the right tool here: the handler is delegated, and a
real mouse click cannot reach a button whose fragment is still hidden.
Four behaviours are correct and must not be reported as defects. A jump lands the target with
all its fragments visible, on purpose. The bar stays full while you are inside the appendix or
the references, also on purpose, because the main deck really is over. A references slide has zero
fragments, because a reference list is not an argument being built. And an empty <div id="refs">
on the references divider is the marker that keeps citation links alive, not leftover markup: what
fails the gate is a #refs with entries in it.
Stage 3c: the closing slide
How a deck ends is settled per type, so this check branches the way stage 0 does. Run it on every
deck. A research talk ends on a closing slide carrying the contact block; a teaching lecture ends on
content, with no closing slide. The expected contents, their position relative to the appendix and
references dividers, and the concrete presence greps are in style/house.md; state each finding
against that file's values. Handed a rendered .html with no source, take the ordered title list
from fit.json and the per-slide classes from probe.json after stage 4 has run, and read the
same positions off those.
| Deck type | State | Severity |
|---|---|---|
| talk | No closing slide anywhere | MAJOR. The talk stops instead of closing, and the room leaves with no address to write to. |
| talk | Closing slide sits inside the appendix, or after it, or after the references | MAJOR. The audience meets the appendix before the invitation. The fix is to move it ahead of the .appendix-break divider. |
| talk | Closing slide with no contact line and no invitation to get in touch | MINOR. Name the line that is missing. |
| talk | No QR slot on the closing slide | MINOR. |
| talk | QR slot present, image resolves to nothing | CRITICAL, and stage 3 has already printed MISSING IMG for that slide. Carry its token. |
| lecture | A thank-you slide is present | MINOR. It belongs on a talk; the lecture's own closing slide is the one idea to leave with. |
The bar reads full on the closing slide, since it is the last main-body slide. That is stage 3b's fill check and not a second finding here.
Stage 4: probe and capture
The rendered page supplies what stage 3 cannot: the deck's real ground colour, contrast against composited backgrounds, the math-engine failure signals, unresolved citations, and the PNGs the reviewers look at. Geometry is already settled by stage 3, so the probe does not measure it.
One command does the probe and the screenshots. It launches its own headless Chrome, opens the deck
over file://, sizes the viewport to 2334x1556, waits for Reveal, the math engine, and the fonts,
switches fragments and transitions off, runs probe.js and figure-ground.js, and shoots every
slide:
SKILL="$HOME/.claude/skills/slide-review"
node "$SKILL/scripts/capture.mjs" deck.html "$RUN" # probe.json + shots/
node "$SKILL/scripts/capture.mjs" deck.html "$RUN" --slides=1,3-5 --no-probe # a subset, shots only
Verified output on the public example talk deck, 26 slides in six seconds:
canvas 1050x700, scale 2.000, 26 slides
probe: $RUN/probe.json (26 slides, 2 figures)
shots: 26 -> $RUN/shots/slide-01.png .. slide-26.png
Reveal computes scale = min(0.9 * innerW / 1050, 0.9 * innerH / 700) at the Quarto default margin of
0.1, so 2334x1556 gives scale 2.000 against the 1050x700 logical canvas. Verified. One screenshot
pixel is half a deck pixel, and the reviewers are told so. The script prints a WARNING when the
scale comes out anything else, which happens on a deck that overrides width, height, or
margin; carry the printed scale into the reviewer prompts in that case.
The script sets fragments: false, transition: 'none', and autoAnimate: false before it measures
or shoots anything, so one capture per slide is the whole slide.
fragments: false is what makes staged content measurable, and it has to stay. Decks run
stage-slide.lua, which wraps nearly every top-level block on a content slide in a .fragment so
the slide opens as its heading alone. A staged block is present in the DOM from the start and only
hidden, so it is content, not missing content. Never report a slide as empty or a point as absent
because it arrives on a later keypress, and tell the reviewers the same, since the screenshots are
taken with fragments off and show the fully revealed slide.
The probe (scripts/probe.js, run in the page by capture.mjs, its JSON at $RUN/probe.json)
forces each section visible
in turn and restores the inline style afterwards, because Reveal sets display: none on off-screen
sections and a hidden element reports no computed geometry and no client rects. It measures font
size and contrast against the composited background, reads the deck's own ground colour instead of
assuming white, and re-bases per slide on a data-background-color, because reveal paints that
backdrop on a separate .slide-background element that is nowhere in the text's ancestor chain;
without the re-base, the probe measures a divider slide's text against the deck ground while the
audience sees it on the attribute's colour.
Images cannot be measured in that pass: reveal lazy-loads them from data-src, so off-screen
<img> elements report naturalWidth 0 and a zero-size box, and forcing the section visible does
not change that (verification in references/probe-reading.md). So capture.mjs runs
scripts/figure-ground.js as a second pass: it loads each image into a detached Image and samples
the border band, and the result sits in $RUN/probe.json as figures. Under file:// that read
needs Chrome's --allow-file-access-from-files, which the script passes; without it every figure
comes back unmeasurable.
Read both against references/probe-reading.md: the font-size and contrast thresholds, the
figures[].lightbox rule with its verified calibrations, the four math-failure kinds and how each
engine shows them, the unresolved-citation shape, and the four dark-deck defect classes, which apply
only when deck.dark is true and are all skipped on a talk deck.
Stage 5: the PNGs
capture.mjs has already written them. It navigates with Reveal.slide(h, v) from
Reveal.getIndices, which is the only navigation that works reliably (URL fragments do not, and
arrow keys drift on decks with vertical stacks), and names each file by its flat index, zero-padded,
so slide-07.png is slide 7 in fit.json and on the slide number.
The capture is a viewport screenshot, so content past the canvas edge is cut off in the image exactly as the projector will cut it. A bullet sliced through its x-height in the PNG is the defect, not an artifact of the capture.
A full deck costs a few seconds, so --slides= scopes the review and never the capture cost. In
--preflight mode, capture only the slides the probe flagged, plus the title slide: run the probe
pass first, read probe.json, then run again with --slides= and --no-probe.
Fallback when capture.mjs cannot run
If Chrome cannot be launched at all (no binary, or a sandbox that blocks --remote-debugging-port),
export the deck to PDF and rasterize from there. Stage 3 has already run, so the geometry is in hand
either way; what this route gives up is stage 4's contrast and math-engine signals, and the report
says so.
node "$ASSETS/deck-check.mjs" handout deck.html "$RUN/deck.pdf"
That reuses the same headless Chrome the gate uses and needs nothing installed. decktape is the second
option if the handout export disagrees with what you saw on screen; the README's PDF sections carry
the exact npx -y decktape@latest reveal invocation. Both drive their own browser over file://.
Chrome's --headless --print-to-pdf is not an alternative; it produces a blank PDF on reveal decks.
Reading any PDF
The Read tool cannot open PDFs on this machine. There is no poppler and so no pdftoppm, which is
what Read needs, and it fails on every .pdf. Route through pdfread.py, then Read the PNG:
~/.claude/assets/bin/pdfread.py png "$RUN/deck.pdf" --pages 3 --dpi 110 --out "$RUN/shots/s"
That writes $RUN/shots/s-3.png. --pages takes 3, 1-5, or 1,4,9, 1-based. Raise --dpi to 150
when a reviewer needs to judge small type in a figure. This applies anywhere a PDF shows up, including
a deck the user hands over directly.
If handed a PDF instead of a deck
Legacy Beamer, or a deck someone exported. No probe, no offline check, no browser, so the image reviewers run alone and the report says which lenses were unavailable. Rasterize every page:
~/.claude/assets/bin/pdfread.py pages deck.pdf # page count first
~/.claude/assets/bin/pdfread.py png deck.pdf --pages 1-24 --dpi 110 --out "$RUN/shots/slide"
Check head -c 5 deck.pdf is %PDF- first. A download that was actually an HTML error page will die
with Unrecoverable error, exit code 1, which reads like a rasterizer bug and is not one. Confirm the
pixel size with sips -g pixelWidth -g pixelHeight; under 1400 wide, re-render at --dpi 150 so the
reviewers can judge small type. Correct once, do not loop. gs at /usr/local/bin/gs does the same job
if PyMuPDF is unavailable, and pdfread.py --help prints the equivalent invocation.
Stage 6: fan out
Now the PNGs are on disk, so this part parallelizes safely. One Agent call
per lens, all in one message, subagent_type: "general-purpose". Every reviewer gets the deck facts
(canvas 1050x700, screenshots at scale 2.0, root font size, deck type, and the ground colour from
deck.ground) and absolute PNG paths, and every reviewer is told to read the images.
Two facts every reviewer needs, or they will report the theme as a bug. A lecture deck is dark on
purpose, so light text on a near-black ground is the design and not a rendering failure. And the deck
is staged by stage-slide.lua, so the screenshots show the fully revealed slide even though the
audience meets it one block at a time; a point that arrives on a later keypress is present, not
missing.
Every reviewer gets one more instruction. Before ruling on a candidate defect, crop that region out of the full-resolution PNG and look at the crop enlarged. This setup assumes no ImageMagick, so the crop goes through PIL; adjust to your machine. One crop per suspect region, written beside the screenshot it came from:
python3 -c "from PIL import Image; im=Image.open('$RUN/shots/slide-07.png').crop((x0,y0,x1,y1)); im.resize((im.width*3,im.height*3), Image.LANCZOS).save('$RUN/shots/slide-07-crop1.png')"
A coordinate read off the crop gets the crop origin added back before the halving to deck px, so the arithmetic in a finding is always full-image. A defect that resolves at full resolution in the crop is not reported, and a finding that survives names its crop file next to its evidence line.
The lenses: layout, argument, and copy on every deck, pedagogy on a lecture only. The prompts, the
settled-decisions paragraph that opens each of them, and the reply contract are in
references/reviewer-prompts.md; send them as written. The density calibration and the settled
decisions they encode are style/house.md. Inputs per lens: layout gets all PNGs plus fit.json
and probe.json; argument gets all PNGs plus the ordered title list and --goal if provided;
pedagogy gets all PNGs; copy gets all PNGs plus the .qmd source path so fixes can be exact. Every
reviewer replies in the numbered contract, or NONE at its severity floor.
Stage 7: synthesize
Do this in the main thread. Merge, drop duplicates (layout and copy will both find raw TeX in an
equation; keep the one with better evidence), and rank on two keys: severity first, then whether the
audience sees it. An unresolved citation printing (smith2020?) on the wall outranks an inconsistent
symbol in the appendix at the same severity. Drop reviewer findings that contradict the settled
decisions or the density calibration in style/house.md; the full drop rules are rows in
references/failure-taxonomy.md.
# Slide review: deck.qmd
Type: research talk (light, ground #ffffff) | Slides: 24 | Captured: $RUN/shots/
Render: clean | 2 pandoc warnings
Math: MathJax from CDN | Offline-safe: not checked (offline not requested)
DECK-FITS: NO (2 slides)
Closing: thank-you slide at 22, ahead of the appendix divider
## Blocking (fix before presenting)
1. [CRITICAL] Slide 3 "Results" - OVERFLOW +547px.
Bullets 9 to 14 and the specification table are off screen. Split the slide,
or move the table to its own.
2. [CRITICAL] Slide 5 "A wide table" - TOO WIDE +308px.
The rightmost three columns are off screen. Drop columns or mark the table `.spec`.
3. [CRITICAL] Slide 4 "A formal result" - `\frobenius` renders as red literal text.
KaTeX has no such macro; it was a Beamer preamble definition. Replace with `\|X\|_F`.
## Worth fixing
4. [MAJOR] Slide 1 is blank (NEARLY EMPTY), counted in c/t and printed in the handout.
A macro block sits before the first `##`. Retitle it `## Notation {visibility="hidden"}`.
The gate passes this one, so it will not block a release.
## Minor
## Clean
Slides 2, 5 through 24 pass all mechanical checks.
Lead with the fit verdict and the count of blocking issues. If nothing blocks, say the deck is ready to present in the first line, then list the rest. The offline line still appears, saying either the verdict or that the deck did not ask for the check, so nobody reads its absence as a pass.
On a lecture deck the header line carries the deck type and the measured ground the same way, the
closing
line reads Closing: no thank-you slide (correct for a lecture), and on a dark deck the report
says which dark checks ran and what each found, including the ones that found nothing. A figure whose
pixels could not be read is reported as sent for human review, with the slide numbers.
Finish with the run directory, the PNG paths, and the agent count.
Failure taxonomy
Every symptom this skill has met, with the response to each, is the table in
references/failure-taxonomy.md. Read it when a stage exits non-zero or a tool behaves oddly, and
before reporting a tool failure as a finding.
Out of scope
Editing the deck. This produces a report; the author decides.
Prescribing how the author should write. Legibility, correctness, and whether the argument is
followable are in bounds. Sentence length, tone, and diction are not. Text density is in bounds on
the terms style/house.md sets, which are per deck type and about what the audience is asked to
absorb.
Judging whether the research is right. That is /review-paper.
Authoring a deck from scratch. That is /research-talk for a seminar deck and /teaching-lecture for
classroom material. This skill audits a deck that already exists.
Beamer .tex source. A PDF exported from Beamer goes through the rasterize path for image review only.