Skip to content

Call the October upgrade's fixed outputs fixed, and build the table's split from a helper - #209

Merged
MaxGhenis merged 7 commits into
mainfrom
engine-upgrade-fixed-wording
Nov 3, 2026
Merged

MaxGhenis merged 7 commits into
mainfrom
engine-upgrade-fixed-wording

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

We fix the two non-blocking nits from the round-2 final review of release dashboard-data-20261010 (#208, which we merged as 5a8164a). In faf2f5c we merged main at 875fe1f, after we had moved the references to policyengine-us 2.41.0 (2026-10-13) and 2.60.0 (2026-10-23). In 860520e we merged main again at 8ac64ac, which brought in #253's parallel test job.

Changes

  1. A helper writes each October move's split in the upgrades table. On main, october_upgrade_row in paper/index.qmd builds one row per October move (2.38.6, 2.41.0, 2.60.0) and writes its parenthetical inline from restored - ruled and ruled. With one ruled output it would print "the 1 ruled on … stay scored", with one returned output "1 that earlier releases excluded return", and with one fixed output "behind 1 outputs".
    • restored_split_clause(restored, ruled, ruling_date) in policybench/paper_results.py, beside restored_split_sentence, builds the verb phrase after "The move from policyengine-us X". It handles nothing fixed, all returned, all ruled, a mix, and singular and plural ("one", "returns", "stays", "output", "defect").
    • With nothing fixed it returns "returns no excluded output to scoring", the words main's row already printed for the 2.60.0 move.
    • The row calls it through each view's engine_upgrade_restored_split_clause.
    • For this release's three moves, (18, 4), (9, 0) and (0, 0), the clause reproduces the text main's row printed.
  2. We call a move's outputs "fixed" when some of them never left scoring. engine_upgrade_restored_sentence reads "The fixed outputs take the upstream fixes for …" when we ruled on some of the move's fixed outputs on 2026-10-06 while they still counted in scoring. It reads "The restored outputs take …" when every one returns to scoring.
    • We moved the references to version 2.38.6 on 2026-10-10. It fixes the engine defects behind 18 outputs, and we ruled on 4 of them while they still counted. Its sentence, in the paragraph and in the table row, now says "fixed".
    • We moved the references to version 2.41.0 on 2026-10-13. It fixes the defects behind 9 outputs, all of which we had excluded from scoring, and we ruled on none of them. Its sentence keeps "restored".
    • When we moved the references to version 2.60.0 on 2026-10-23, we returned no excluded output to scoring, so engine_upgrade_restored_sentence writes no sentence for that move.
    • fixed_or_restored(ruled) picks the word: "fixed" when we ruled on any of a move's fixed outputs while they still counted, and "restored" otherwise. It refuses a negative count.
    • The docstrings that cover all of a move's fixed outputs say "fixed": EngineUpgrade, partition_engine_upgrade_changes, engine_upgrade_restored_count, _fixes, _fix_phrase, _targets and _target_sentence. So do the per-entry refusal messages ("is fixed off its target", "is fixed on a tolerance of") and restored_split_sentence's refusal ("ruled of … fixed outputs").
    • We kept the accessor names.

The rendered paper changes two words

We re-rendered the paper with uv run python paper/render_paper.py (Quarto 1.9.36, TinyTeX 2026.10, LuaTeX 1.24.0, xurl.sty v0.10) and re-pinned it with uv run python scripts/freeze_snapshot.py --rendered-only. Against main's committed renders:

  • In the HTML with tags stripped, "The restored outputs take" becomes "The fixed outputs take" twice, in the 2.38.6 paragraph and its table row. The raw HTML also differs in two Quarto cell ids (cells 3 and 8), which change between renders of the same source.
  • In the PDF's pdftotext output, the same two words change. The table cell loses a line and rewraps, so pdftotext lists the repeated table header "Review outcome" and the page number 35 at different points. The paper keeps its 54 pages, and no number changes.
  • app/public/paper/web/figures/positive_zero_scatter.png keeps main's bytes, and the PDF embeds it. The first render host had no Arial and drew the figure's labels in Liberation Sans. So in 5de92ca we copied main's PNG over the one generate_figures.py draws and rendered the PDF again. The image that pdfimages extracts from page 45 now equals the PNG pixel for pixel, as on main.
  • In 0e02069 we rebuilt the PDF the same way, after we updated TinyTeX 2026.10's KOMA-Script from 3.49.2 to 3.49.3 with tlmgr update --self and tlmgr update koma-script. KOMA-Script 3.49.2 sets no anchor for four unnumbered headings: the table of contents, Abstract, Appendix A and Competing interests. Every PDF we committed on this branch before 0e02069 lacked those anchors, as main's did until Disclose why 330 frozen households record near-zero latency and two models record none #228. From faf2f5c to 5de92ca, the bookmarks and table-of-contents links for the last three each pointed one page early, while main's pointed to the right pages. The outline and the 128 named destinations now equal main's, and the text equals 5de92ca's byte for byte.
  • The LaTeX log for 0e02069 reads LaTeX2e <2026-06-01>, scrartcl 2026/10/19 v3.49.3 and hyperref 2026-06-17 v7.01r. Nothing in the repository pins KOMA-Script, so anyone who rebuilds the PDF on the stock TinyTeX 2026.10 bundle brings the three wrong targets back. The hand-off comment lists the checks and the TeX setup.
  • In the manifest and app/src/paperSnapshot.json, we changed only the PDF and index.html hashes.

Invariants

For every 0 ≤ ruled ≤ restored, restored_split_clause:

  • with restored = 0, returns "returns no excluded output to scoring";
  • otherwise names the total in numerals with an agreeing noun and "defect"/"defects";
  • says "return" iff restored − ruled > 0, and "stay" and the ruling date iff ruled > 0;
  • agrees with restored_split_sentence on both;
  • agrees each verb with its count;
  • never prints "the 0", "the 1", "0 that", "1 that" or a lone "1" inside the split;
  • has exactly one balanced parenthetical;
  • refuses ruled > restored and ruled < 0.

fixed_or_restored returns "fixed" iff the ruled count exceeds zero, and engine_upgrade_restored_sentence takes its word from it.

Tests

  • test_the_split_clause_counts_both_kinds runs a Hypothesis property test of the invariants above, over restored ∈ 0..40, ruled ∈ 0..restored and a ruling date drawn from 2026-10-06 and 2027-01-02. It also checks fixed_or_restored at each drawn count, and that fixed_or_restored(-1) raises.
  • test_one_ruled_output_makes_the_fix_sentence_say_fixed gives the 2.41.0 view (nine fixed, none ruled) a MOCK ruled count of one and requires "The fixed outputs take".
  • test_the_fixed_outputs_split_into_returned_and_still_scored pins each October move's clause. It also requires october_upgrade_row to call engine_upgrade_restored_split_clause, and refuses the old inline arithmetic.
  • test_the_upgrade_sentences_name_what_the_sidecar_records requires "fixed" or "restored" in each view's sentence by its ruled count, and pins 2.38.6 to "fixed", 2.41.0 to "restored" and 2.60.0 to no sentence.
  • test_rendered_html_calls_the_upgrade_fixes_fixed_not_restored checks the rendered HTML: each move's clause, each move's sentence twice, and two "fixed" and two "restored" sentences in all. test_rendered_pdf_calls_the_upgrade_fixes_fixed_not_restored checks the counts in the PDF.
  • Each of these mutants fails at least one test:
    • the sentence always says "restored";
    • the sentence always says "fixed";
    • "the 1 … stay";
    • "1 that … return";
    • "1 outputs";
    • main's inline october_upgrade_row;
    • main's HTML render;
    • main's PDF render.
  • At 0e02069, the reviewer who approved that head ran the three mutants that survived the tests at 4988b0c, and each now fails at least one test:
    • the sentence says "fixed" only from two ruled outputs;
    • the sentence says "fixed" only above nine fixed outputs;
    • the split prints 2026-10-06 in place of its ruling date.
  • On the first render host, tests/test_paper_results.py, tests/test_disclosures.py and tests/test_snapshot_artifacts.py give 213 passed and 1 skipped, and uv run ruff check . and uv run ruff format --check . report no problems. CI on 4988b0c passes test, lint, app and paper.
  • On a Mac at 942dba4, tests/test_paper_results.py, tests/test_disclosures.py, tests/test_snapshot_artifacts.py and tests/test_notes.py give 258 passed and 1 skipped, and the changed files pass ruff check and ruff format --check. GitHub Actions cancelled 942dba4's test job at its 30-minute limit.
  • After we merged Run the CI test job in parallel with pytest-xdist #253's parallel test job in 860520e, CI passed at 5de92ca and again at 0e02069: test (7,081 passed, 91 skipped), lint, app, paper and Vercel. On the host where we rebuilt the PDF at 0e02069, tests/test_snapshot_artifacts.py and tests/test_disclosures.py give 137 passed and none skipped. That host has pdftotext, so the seven tests in tests/test_disclosures.py that read the PDF's text, which CI skips, ran there.

Reviews

axiom: n/a: paper wording and its render, no policy logic

🤖 Generated with Claude Code

… split from a helper

Follow-up to release dashboard-data-20261010 (#208), fixing the two
non-blocking nits from its round-2 final review.

1. The upgrades table cell built its 14 returned / 4 ruled parenthetical
   inline in paper/index.qmd. With zero ruled outputs it would print "the 0
   ruled on ... stay scored", and with one, "the 1 ... stay". The new
   restored_split_clause in policybench/paper_results.py, beside
   restored_split_sentence, builds the whole verb phrase. It handles no
   fixed outputs, all returned, all ruled, a mix, and singular and plural
   ("one", "returns", "stays"). The cell calls it through
   engine_upgrade_restored_split_clause. For this release's (18, 4) it gives
   the same text the cell printed before.

2. engine_upgrade_restored_sentence called all 18 fixed outputs "restored",
   though the four ruled on 2026-10-06 while still scored never left
   scoring. It now says "The fixed outputs take the upstream fixes for ...".
   The docstrings and the per-entry refusal messages that cover all of them
   say "fixed" too.

The rendered paper changes in two words only: "restored" becomes "fixed" in
the paragraph and in the table, in both the HTML and the PDF. No number
changes. Re-rendered with paper/render_paper.py and re-pinned with
scripts/freeze_snapshot.py --rendered-only.

Tests:
- A Hypothesis property test over (restored, ruled) for the clause checks
  the total and its noun, that it says "return" and "stay" exactly when the
  split sentence does, verb agreement, and no "the 0", "the 1" or lone "1".
- The clause is pinned for this release, and the paper must call it.
- The fixed sentence is pinned.
- Rendered HTML and PDF checks require "fixed outputs take" and refuse
  "restored outputs take".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policybench-site Ready Ready Preview Nov 3, 2026 7:20am UTC

Request Review

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the PolicyBench session to bring it up to date with main. Reply here if you're mid-change and I'll stop.


Generated by Claude Code

Bring #209 up to date with main, which has since moved the references
to policyengine-us 2.41.0 and 2.60.0 and builds each October move's
upgrades-table row in october_upgrade_row.

- The row's split now comes from restored_split_clause through each
  view's engine_upgrade_restored_split_clause. A move that fixes
  nothing reads "returns no excluded output to scoring", as main's row
  already did.
- engine_upgrade_restored_sentence says "fixed" for a move whose fixed
  outputs include ones the 2026-10-06 rulings decided while still
  scored (2.38.6), and "restored" when every one returns (2.41.0).
- The tests cover each October view; the rendered checks count two
  "fixed" and two "restored" sentences.
- Re-rendered the paper and re-pinned the PDF and index.html hashes.
  The scatter PNG keeps main's bytes: this VM's raster differs only in
  its fonts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6GJ8QcoBhPtNJDGFLb
Its docstring, from main, said the outputs return to scoring; the
2.38.6 move's four ruled outputs never left scoring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6GJ8QcoBhPtNJDGFLb
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 4988b0c0ade6245d0e4038c410f6c94ac54a7807. It holds the merge of main at 875fe1f (faf2f5c, a merge commit, with no rebase or force-push), then one docstring commit.

Changes and how I resolved them

  • Generated files: the PDF, web/index.html, the manifest and app/src/paperSnapshot.json take main's version, then I re-rendered and re-pinned them.
  • Split clause (nit 1): main has no helper for it. Its new october_upgrade_row builds every October move's row with inline arithmetic, so I kept the PR's helper and pointed main's function at it.
    • paper/index.qmd:905-911: october_upgrade_row calls view.engine_upgrade_restored_split_clause.
    • policybench/paper_results.py:435: restored_split_clause. With nothing fixed it now returns "returns no excluded output to scoring", the text main's row prints for the 2.60.0 move. The PR's version returned "fixes none of the engine defects behind excluded outputs".
    • policybench/paper_results.py:2167: engine_upgrade_restored_split_clause.
  • "Fixed" wording (nit 2): policybench/paper_results.py:2339-2349. engine_upgrade_restored_sentence says "fixed" when the move's ruled count is above zero, and "restored" otherwise. My call for each sentence:
    • 2.38.6 move (paragraph and table row): 18 fixed, 4 ruled on 2026-10-06 while still scored. It becomes fixed.
    • 2.41.0 move (paragraph and table row): 9 fixed, 0 ruled. All nine were excluded before and return to scoring, per the view's engine_upgrade_restored_ruled_count and split sentence. It stays restored.
    • 2.60.0 move: fixes nothing, so there is no sentence.
  • paper_results.py conflict in engine_upgrade_restored_fixes: I took main's body (fix groups). In 4988b0c I changed its docstring to say "fixed", as its sibling accessors do.
  • tests/test_disclosures.py:1046-1052 (card test): I took main's v10 view and kept the PR's fixed naming.
  • tests/test_disclosures.py:744-770: the rendered HTML and PDF checks now cover each October view. Each move's clause appears, its sentence appears twice, and there are two "fixed" and two "restored" sentences in all.
  • tests/test_paper_results.py:1648 (the per-view test from main): asserts "fixed" or "restored" by each view's ruled count, and pins ruled counts [4, 0, 0].
  • tests/test_paper_results.py:1790: pins all three moves' clauses and requires october_upgrade_row to call the helper, with no restored - ruled or {ruled} ruled on.
  • tests/test_paper_results.py:1885: the Hypothesis property test, with the zero case updated.

Render diff against main's committed renders

The render used Quarto 1.9.36, TinyTeX 2026.10 (LuaTeX 1.24.0, the version main's PDF records) and xurl.sty v0.10.

  • HTML, tags stripped: only "The restored outputs take" becomes "The fixed outputs take", twice (the 2.38.6 paragraph and its row). The raw HTML also differs in the Quarto cell ids of cells 3 and 8. These ids changed between two of my renders of identical source.
  • PDF (pdftotext): the same two words. The shorter cell rewraps, and the repeated table header "Review outcome" and page number 35 move in the extraction order. It is still 54 pages, and no number changed.
  • Manifest and app/src/paperSnapshot.json: only the PDF and index.html hashes change.

Tests

  • uv run pytest tests/test_paper_results.py tests/test_disclosures.py tests/test_snapshot_artifacts.py -q: 213 passed, 1 skipped.

    • Before the merge commit existed, test_the_published_upgrades_pins_are_their_committed_bytes failed: it reads git show HEAD: and HEAD lacked main's evidence files. It passes once the merge is committed.
  • uv run ruff check . and uv run ruff format --check .: clean.

  • CI on 4988b0c: test, lint, app and paper all succeed. The test run on faf2f5c was cancelled because the newer push superseded it.

  • Mutation checks, each made by hand and reverted:

    Mutant Fails
    Sentence always "restored" test_the_upgrade_sentences_name_what_the_sidecar_records
    Sentence always "fixed" (the PR's original) test_the_upgrade_sentences_name_what_the_sidecar_records
    "the 1 … stay" test_the_split_clause_counts_both_kinds
    "1 that … return" test_the_split_clause_counts_both_kinds
    "1 outputs" test_the_split_clause_counts_both_kinds
    main's inline october_upgrade_row test_the_fixed_outputs_split_into_returned_and_still_scored
    main's HTML render test_rendered_html_calls_the_upgrade_fixes_fixed_not_restored
    main's PDF render test_rendered_pdf_calls_the_upgrade_fixes_fixed_not_restored

Judgment calls

  1. "Fixed" is conditional on the ruled count, not fixed text. A constant "fixed" would mislabel the 2.41.0 move, whose nine outputs all return to scoring.
  2. The zero case of restored_split_clause reuses main's wording, so the 2.60.0 row renders unchanged.
  3. Scatter figure: app/public/paper/web/figures/positive_zero_scatter.png keeps main's bytes. This VM rasterizes its SVG with Liberation Sans in place of Arial. I compared the two images: the plot is the same, only the glyphs differ, so I didn't commit a fonts-only change or a new pin for it. The re-rendered PDF embeds this VM's raster, so the figure's labels in the PDF use Liberation Sans.
  4. xurl: the TinyTeX bundle lacks xurl.sty, which the template loads only if present. Without it, three bibliography URLs broke at different points than in main's PDF. I installed xurl.sty v0.10 from Ubuntu's texlive-latex-extra, and those differences went away.
  5. I pushed to engine-upgrade-fixed-wording, the PR's head, as the task said, rather than to this session's default claude/merge-pr-209-main-qz1nx4.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

This is an independent review by a Claude Opus 5.5 cloud session, written for the PolicyBench owner session.

Verdict: APPROVE at 4988b0c0ade6245d0e4038c410f6c94ac54a7807 (the PR's head on GitHub is still this SHA).

I found nothing blocking. The code, the rendered words, the merge resolution and the hashes match what the description says. Below are three non-blocking findings and one addition to the plan for the PDF re-render.

Findings (none blocking)

1. Two tests pin the "fixed"/"restored" rule only at ruled counts 4 and 0, so an off-by-one at ruled = 1 survives

policybench/paper_results.py:2349 chooses the word with "fixed" if self.engine_upgrade_restored_ruled_count else "restored". The tests check this rule only against the three frozen views, whose ruled counts are (4, 0, 0): tests/test_paper_results.py:1648 (test_the_upgrade_sentences_name_what_the_sidecar_records, which also pins :1712) and tests/test_disclosures.py:744. Three hand-made mutants survive the full tests/test_paper_results.py tests/test_disclosures.py tests/test_snapshot_artifacts.py tests/test_notes.py run on the scratch merge (257 passed, 3 skipped for each):

Mutant at paper_results.py:2349 or :474 Result
kind = "fixed" if self.engine_upgrade_restored_ruled_count > 1 else "restored" survived
kind = "fixed" if self.engine_upgrade_restored_count > 9 else "restored" survived
restored_split_clause writes the literal 2026-10-06 in place of {ruling_date} in the plural "kept" branch survived (equivalent in practice, because every caller passes RULING_DATE and the property test always passes "2026-10-06")

The first mutant matters only if a later move has exactly one ruled output, and no such move exists in the frozen data. If you want this rule pinned, one way is to move the choice into a pure function next to restored_split_clause, for example fixed_or_restored(ruled: int) -> str, and cover it in test_the_split_clause_counts_both_kinds with assert fixed_or_restored(ruled) == ("fixed" if ruled else "restored"). A MOCK view with one ruled entry would also work. For the third mutant, the property test could draw the ruling date as well (for example st.sampled_from(["2026-10-06", "2027-01-02"])) and assert that the date appears in the split.

2. The description's PDF diff leaves out one change that the hand-off comment includes

The description says the pdftotext output "differs in the same two words plus the position of the page number 35". A word-level diff of main's PDF against this head's PDF shows one more move. The repeated table header "Review outcome" also moves in the extraction order:

$ pdftotext <pdf> (875fe1f6 and 4988b0c), then difflib over the words
('replace', 8942, 8943, ...)  ['restored'] -> ['fixed']
('insert',  12168, ...)       []           -> ['Review', 'outcome']
('delete',  12176, ...)       ['Review', 'outcome'] -> []
('replace', 12246, ...)       ['restored'] -> ['fixed']
('delete',  12402, ...)       ['35'] -> []
('insert',  12408, ...)       [] -> ['35']

The hand-off comment states this move correctly. Replacement text for the description's PDF bullet: "PDF (pdftotext): the same two words. The table cell loses a line and rewraps, so pdftotext lists the repeated table header "Review outcome" and the page number 35 at different points. The paper keeps its 54 pages, and no number changes."

3. One error message still says "restored", and one older view still raises KeyError

  • policybench/paper_results.py:399: restored_split_sentence raises f"{ruled} ruled of {restored} restored outputs", while its new sibling restored_split_clause raises "... fixed outputs". The description says only the per-entry refusal messages changed, so the description is accurate. This is a consistency nit only.
  • r.upgrade_view("2.15.17") (the September move, whose sidecar has no regenerated_exclusions) raises KeyError: 'regenerated_exclusions' from engine_upgrade_restored_ruled_count (:2152), and so from _split_sentence, _split_clause and _sentence. main at 875fe1f6 raises the same KeyError for _ruled_count, _split_sentence and _sentence, so this is not a regression. The paper never calls these accessors on that view. If a later change does, last.revision.get("regenerated_exclusions", []) at :2152 would make the September view return 0.

Addition to the plan for the PDF figure raster (item 6)

The plan as stated covers the text, the page count and the scope of the commit. It needs one more check, and it has one step that paper/render_paper.py would undo without warning:

  1. Check the raster's pixels, not only its font. pdffonts can't see the figure's labels, because the PDF embeds the figure as a 1800×1220 RGB image (pdfimages -list, page 45). "The labels use Arial" can't be checked from the PDF except by eye. A pixel comparison can be checked. On main, the PDF's embedded image is identical pixel for pixel to the committed web PNG (sha256 85851be2…). At this head, 19,918 pixels differ, all of them in text glyphs (title, subtitle, tick labels, axis titles, legend). The dots and grid don't change (I rendered a diff mask to confirm this). The commit should land only if pdfimages -png policybench.pdf img gives an image identical in pixels to app/public/paper/web/figures/positive_zero_scatter.png at 85851be224bcceddc3ee15776db2b1917babcfc58e8dc7fbf0f8c26f57b2b9a6. For example: python -c "from PIL import Image, ImageChops as C; a=Image.open('img-000.png').convert('RGB'); b=Image.open('positive_zero_scatter.png').convert('RGB'); print(C.difference(a,b).getbbox())" must print None.
  2. The render rewrites the web PNG. paper/generate_figures.py:29-44 rasterizes the SVG with rsvg-convert -w 1800, and render_paper.py copies the output into app/public/paper/web/. If the Mac's rsvg/pango produces different bytes, the render changes app/public/paper/web/figures/positive_zero_scatter.png, and freeze_snapshot.py --rendered-only re-pins its hash in the manifest. The commit must keep that PNG at 85851be2… and keep the manifest's figures/positive_zero_scatter.png entry unchanged. If the Mac's PNG differs from main's in more than encoding, then the Mac's raster is not the raster main shipped, and check 1 fails on purpose.
  3. index.html. The Quarto cell ids (cells 3 and 8) change on every render, so the Mac render will change index.html's bytes even when its text is the same. Either keep this head's index.html and its hash, or check the tag-stripped text against this head's and re-pin both webVersion and the manifest hash. Keeping this head's file is simpler.
  4. Re-run uv run pytest tests/test_snapshot_artifacts.py tests/test_disclosures.py -q on the new commit. That run checks the manifest hashes against the files and re-runs the two rendered-PDF count tests. Also check that git diff --stat 4988b0c HEAD lists only app/public/paper/policybench.pdf, paper/snapshot/20260501/manifest.json and app/src/paperSnapshot.json (plus index.html if you take step 3's second option), and that pdfinfo still reports Producer: LuaTeX-1.24.0 and 54 pages.

The plan's other conditions (pdftotext output equal to this head's, 54 pages, no other file changes) are the right ones. Equal pdftotext output also covers the xurl line breaks in the bibliography that the hand-off mentions.

Checked and sound

  • Wording rule (item 1). engine_upgrade_restored_ruled_count (paper_results.py:2143-2153) counts the move's regenerated_exclusions entries whose key is among ruled_records (:2735-2754): current exclusion records with decided_on == 2026-10-06, plus removed records kept in any upgrade's regenerated_exclusions. Running the views gave (restored, ruled) = (18, 4) for 2.38.6, (9, 0) for 2.41.0 and (0, 0) for 2.60.0. I cross-checked against the raw sidecar: the 2.38.6 entries' record.decided_on values are {2026-09-22: 14, 2026-10-06: 4}, the 2.41.0 values are {2026-09-22: 9}, and 2.60.0 has no entries. The only views that render the sentence are r10, r12 and r (paper/index.qmd:50-51, 919, 939, 1136, 1138). No other module, app note or test fixture calls engine_upgrade_restored_sentence (grep -rn over the repo). The rendered sentences say "fixed" for 2.38.6, whose 4 ruled outputs never left scoring, and "restored" for 2.41.0, whose 9 outputs were all excluded and return. Both words are right for their moves. 2.60.0 prints no sentence.
  • restored_split_clause (item 2). I called it for every 0 ≤ ruled ≤ restored < 300 and checked these properties. Each result has exactly one balanced parenthetical (none when restored = 0). Nothing matches \bthe [01]\b, \b[01] that, \b1 outputs or \b1 defects. No standalone 1 appears inside the parentheses. "return" appears if and only if restored − ruled > 0, and "stay" and the date appear if and only if ruled > 0. The singular verbs "returns" and "stays" go with counts of one, and no plural count takes a singular verb. restored_split_sentence doesn't raise for any of these pairs. Result: 0 violations. Both (1, 2) and (3, -1) raise ValueError. Sample outputs: (2, 1) → "fixes the engine defects behind 2 outputs (one that earlier releases excluded returns to scoring at the new version's values; the one ruled on 2026-10-06 while still scored stays scored)"; (1, 1) → "fixes the engine defect behind 1 output (the 2026-10-06 rulings decided it while it was still scored, and it stays scored at the new version's values)". A count above 40, beyond the property test's range, takes no other branch, because the code compares only against 0 and 1.
  • Merge resolution (item 3). In git show --remerge-diff faf2f5c, paper/index.qmd takes main's october_upgrade_row and changes only its fixes expression. The PR's earlier single-row version is dropped in favour of main's three rows. engine_upgrade_restored_fixes takes main's body and engine_upgrade_restored_fix_groups unchanged. The PR's diff against the merge base touches only that docstring. The PR's zero-case text became main's "returns no excluded output to scoring". git diff 875fe1f6 origin/main (now ec5413c) touches no paper, render or paper_results.py file, so the renders are not stale against current main. The 2.60.0 row, tag-stripped, is the same on main and at the head.
  • Rendered HTML (item 5). Tag-stripped diff of index.html at 875fe1f6 and 4988b0c shows two changed lines, each "The restored outputs take" → "The fixed outputs take" (the 2.38.6 paragraph and its table row). The raw diff adds only the cell ids at lines 302 and 1113.
  • Rendered PDF (item 5). pdfinfo: both have 54 pages and LuaTeX-1.24.0. The pdftotext word diff is shown in finding 2, and no number other than the page number 35 moves. pdffonts lists the same six Latin Modern fonts in both.
  • Hashes (item 5). git diff 875fe1f6 4988b0c on the manifest and app/src/paperSnapshot.json shows changes only to the PDF sha256 / pdfVersion and the index.html sha256 / webVersion. sha256sum of the committed files gives 24034abf10c9aa26…eb0c (PDF) and af0ecbe96fac2eb5…3b5c (index.html), which match. The scatter PNG at the head is 85851be2…, the same as main.
  • Tests and mutants (item 4), on a scratch merge of the head into origin/main: uv run pytest tests/test_paper_results.py tests/test_disclosures.py tests/test_snapshot_artifacts.py -q gave 213 passed, 1 skipped (tests/test_paper_results.py:1557: needs the local 2.37.2 rehearsal build). uv run ruff check . and ruff format --check . were clean. CI at the head: test, lint, app and paper all succeed. All eight mutants from the description and hand-off are killed. "Always restored" and "always fixed" are killed by test_the_upgrade_sentences_name_what_the_sidecar_records. "the 1 … stay", "1 that … return" and "1 outputs" are killed by test_the_split_clause_counts_both_kinds. main's inline row is killed by test_the_fixed_outputs_split_into_returned_and_still_scored. main's index.html is killed by test_rendered_html_calls_the_upgrade_fixes_fixed_not_restored, and main's PDF by test_rendered_pdf_calls_the_upgrade_fixes_fixed_not_restored. These mutants of my own are also killed: "1 defects", the old zero-case text, "they stay" for one ruled output, "they return" for one returned output, a dropped ruled < 0 check, and october_upgrade_row reading r. in place of view.. The survivors are listed in finding 1.
  • The description (item 7). These claims hold: main's inline row would print "the 1 ruled on … stay", "1 that … return" and "behind 1 outputs" (read from the removed code); the clause reproduces main's row text for all three moves; the invariant list; the test descriptions; the renamed docstrings and refusal messages; and "the accessor names stay". The one gap is noted in finding 2.

What I couldn't check

  • I didn't re-render the paper. I have no Quarto/TinyTeX here, and the brief asks for a review only. The render claims rest on comparing the committed files.
  • Whether the new PDF raster's font is Liberation Sans in particular is UNVERIFIED. I confirmed that only the glyph pixels differ from main's raster, not which font drew them. Rasterizing the head's SVG with fc-match Arial resolving to Liberation Sans, and comparing it to the PDF's image, would settle it.
  • The skipped test (tests/test_paper_results.py:1557) needs a local 2.37.2 rehearsal build that this container doesn't have.

Generated by Claude Code

Review r2 found that the tests pinned the "fixed"/"restored" word only at
ruled counts of 4 and 0, and the split clause's date only at 2026-10-06.
fixed_or_restored now makes the choice. The property test checks it at every
drawn count and draws the ruling date from two dates. A MOCK count of one
ruled output turns the frozen view's sentence to "fixed".
restored_split_sentence's refusal now says "fixed outputs", like its sibling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Response to review r2, at 942dba4.

  • Finding 1: fixed_or_restored now picks the word. The property test checks it at each drawn count and draws the ruling date from 2026-10-06 and 2027-01-02, and a MOCK ruled count of one turns the 2.41.0 view's sentence to "fixed". We'll run your three surviving mutants against these tests in the same round as the re-render.
  • Finding 2: the description's PDF bullet now names the "Review outcome" move.
  • Finding 3a: restored_split_sentence's refusal says "fixed outputs".
  • Finding 3b: we keep the KeyError. If the September view returned 0, the paper would state that rulings decided none of that move's outputs while they still counted, and its sidecar doesn't record that. The KeyError stops a later caller from printing the guess, and the paper never asks.
  • Re-render: your four checks stand, with one change. git diff --stat 942dba4 HEAD must list only the PDF, the manifest and app/src/paperSnapshot.json, since 942dba4 changes no word the paper renders.

The PR merges after the re-render and the mutant run.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the PolicyBench session to rebuild the PDF's figure raster from the committed PNG. Reply here if you're mid-change and I'll stop.


Generated by Claude Code

The PDF's raster equals app/public/paper/web/figures/positive_zero_scatter.png
pixel for pixel, as it does on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 5de92ca5d8d3e4f63349d00c093a9eb5b3c63639. It's one commit on 860520e4d2f6a485374c74e1978f6c034ba892fa, the head the owner handed over, with no rebase or force-push. The commit changes app/public/paper/policybench.pdf, paper/snapshot/20260501/manifest.json and app/src/paperSnapshot.json. The PDF's figure raster now equals app/public/paper/web/figures/positive_zero_scatter.png (sha256 85851be2…) pixel for pixel, and its text equals 860520e's byte for byte.

Step 5 checks

I ran these before the commit, with /tmp/head.pdf taken from git show HEAD:app/public/paper/policybench.pdf at 860520e.

Raster

$ pdfimages -list app/public/paper/policybench.pdf
page   num  type   width height color comp bpc  enc interp  object ID x-ppi y-ppi size ratio
--------------------------------------------------------------------------------------------
  45     0 image    1800  1220  rgb     3   8  image  no       319  0   291   291 79.8K 1.2%

$ pdfimages -list /tmp/head.pdf
page   num  type   width height color comp bpc  enc interp  object ID x-ppi y-ppi size ratio
--------------------------------------------------------------------------------------------
  45     0 image    1800  1220  rgb     3   8  image  no       319  0   291   291 79.3K 1.2%

$ pdfimages -png app/public/paper/policybench.pdf /tmp/img
$ uv run --no-project --with pillow python -c "
from PIL import Image, ImageChops as C
a = Image.open('/tmp/img-000.png').convert('RGB')
b = Image.open('app/public/paper/web/figures/positive_zero_scatter.png').convert('RGB')
print(C.difference(a, b).getbbox())"
None

PDF metadata

$ pdfinfo app/public/paper/policybench.pdf
Title:           PolicyBench
Subject:         
Keywords:        
Author:          Max Ghenis
Creator:         LaTeX via pandoc
Producer:        LuaTeX-1.24.0
CreationDate:    Tue Nov  3 04:12:17 2026 UTC
ModDate:         Tue Nov  3 04:12:17 2026 UTC
Custom Metadata: yes
Metadata Stream: no
Tagged:          no
UserProperties:  no
Suspects:        no
Form:            none
JavaScript:      no
Pages:           54
Encrypted:       no
Page size:       612 x 792 pts (letter)
Page rot:        0
File size:       265550 bytes
Optimized:       no
PDF version:     1.7

pdfinfo /tmp/head.pdf gives the same fields, except CreationDate/ModDate (Mon Nov 2 15:05:50 2026 UTC) and File size: 265025 bytes.

Text (the .txt and .fonts files sit in this session's scratchpad)

$ pdftotext app/public/paper/policybench.pdf new.txt
$ pdftotext /tmp/head.pdf head.txt
$ sha256sum new.txt head.txt
a1b701196a75d795a22563c033b7795b79d00b20368cc5f2211d2cafeb15f08c  new.txt
a1b701196a75d795a22563c033b7795b79d00b20368cc5f2211d2cafeb15f08c  head.txt
$ cmp new.txt head.txt
[exit 0]
$ pdffonts app/public/paper/policybench.pdf > new.fonts; pdffonts /tmp/head.pdf > head.fonts
$ cat new.fonts
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
BXQVBW+LMRoman17-Regular             CID Type 0C       Identity-H       yes yes yes     30  0
GVSSFD+LMRoman12-Regular             CID Type 0C       Identity-H       yes yes yes     31  0
UULGZQ+LMRoman10-Regular             CID Type 0C       Identity-H       yes yes yes     32  0
TMHZDF+LMRoman6-Regular              CID Type 0C       Identity-H       yes yes yes     33  0
CZOBCD+LMRoman9-Regular              CID Type 0C       Identity-H       yes yes yes     34  0
LCTEHD+LMRoman8-Regular              CID Type 0C       Identity-H       yes yes yes     67  0
$ diff new.fonts head.fonts
[exit 0, no output]

Scope

$ git status --porcelain
 M app/public/paper/policybench.pdf
 M app/src/paperSnapshot.json
 M paper/snapshot/20260501/manifest.json

Pins

$ git diff -- paper/snapshot/20260501/manifest.json app/src/paperSnapshot.json
diff --git a/app/src/paperSnapshot.json b/app/src/paperSnapshot.json
index 813de58..944be88 100644
--- a/app/src/paperSnapshot.json
+++ b/app/src/paperSnapshot.json
@@ -1,5 +1,5 @@
 {
-  "pdfVersion": "24034abf10c9",
+  "pdfVersion": "b8fe0592f43a",
   "responseWindow": "June 12 and October 26, 2026",
   "snapshotDate": "2026-10-27",
   "webVersion": "af0ecbe96fac"
diff --git a/paper/snapshot/20260501/manifest.json b/paper/snapshot/20260501/manifest.json
index d9256df..164e6c9 100644
--- a/paper/snapshot/20260501/manifest.json
+++ b/paper/snapshot/20260501/manifest.json
@@ -165,7 +165,7 @@
   "rendered_paper_artifacts": {
     "pdf": {
       "path": "app/public/paper/policybench.pdf",
-      "sha256": "24034abf10c9aa26c90c7577e874a1282543c723080f8a5d7c55fe62f054eb0c"
+      "sha256": "b8fe0592f43a368d4eddde5a5a790595f4faaba76989b590c8acafff2cfaa0f8"
     },
     "web": {
       "files": {

$ sha256sum app/public/paper/policybench.pdf app/public/paper/web/index.html app/public/paper/web/figures/positive_zero_scatter.png
b8fe0592f43a368d4eddde5a5a790595f4faaba76989b590c8acafff2cfaa0f8  app/public/paper/policybench.pdf
af0ecbe96fac2eb5c1f80fead5e122575e5f7024efcac80d0342e640019e3b5c  app/public/paper/web/index.html
85851be224bcceddc3ee15776db2b1917babcfc58e8dc7fbf0f8c26f57b2b9a6  app/public/paper/web/figures/positive_zero_scatter.png

Independent checks

Five subagents re-ran the checks with methods of their own, read-only on the repo, and a sixth looked for gaps. All five passed, and the sixth returned "ready".

  • Raster. The PDF's one image XObject (object 319, page 45, 1800×1220, /DeviceRGB, 8 bits, FlateDecode) has no /SMask, /Decode, /DecodeParms or /Intent. It equals the PNG's pixels by six methods: getbbox(), numpy.array_equal without conversion, pikepdf, pypdf, a stdlib zlib/PNG decoder, and ImageMagick compare -metric AE (0). Its compressed stream is byte-identical to the one in main's PDF (sha256 1c4a3183…). The PNG has only IHDR, bKGD, IDAT and IEND chunks, with no gAMA, iCCP, sRGB or cHRM. At 860520e, 19,918 pixels differed (x 27–1676, y 49–1137). They are all label glyphs, and the labels read the same 16 strings in both rasters.

  • Pages. I rendered every page at 100 and 300 dpi. Only page 45 differs from 860520e's PDF, and every differing pixel lies inside the figure's rectangle, (83.459, 290.91)–(528.545, 592.579) pt. Page 45 is identical to main's page 45 under both pdftoppm and pdftocairo. pdftotext also matches 860520e's output in -layout and -raw modes, and pypdf's text matches too.

  • Structure. I compared both PDFs object by object (pikepdf, qpdf QDF, and a byte segmentation that covers both files). Three objects differ:

    • the image (319);
    • the Info dictionary (585), only in /CreationDate and /ModDate;
    • the XRef stream (586): its /ID, and offsets shifted by the image's 523 extra bytes.

    These are identical in both files: the 124 link annotations, the 26 outline entries, the 124 named destinations, the 54 page content streams and the 6 font programs. qpdf --check finds no errors, and PTEX.FullBanner reads "This is LuaHBTeX, Version 1.24.0 (TeX Live 2026)".

  • Pins. A JSON deep comparison finds one changed path per file: rendered_paper_artifacts.pdf.sha256 and pdfVersion. The PDF pin and all 17 web-file pins match recomputed hashes. Both files equal json.dumps(indent=2, sort_keys=True) + "\n", and no other tracked file differs from 860520e. app/src/app/paper/page.tsx builds the PDF's ?v= key from pdfVersion. The app tests read only snapshotDate.

  • Pipeline.

    • index.qmd's 16 code cells write no files, and none of the 15 policybench modules it imports writes to paper/figures/ or app/public/paper/. paper/ holds no filters, extensions or pre- or post-render hooks.
    • Recompiling the captured .tex twice with this TinyTeX and the committed PNG reproduces the committed PDF byte for byte, once the /ID is spliced in. LuaTeX derives /ID from the start time and the path.
    • LuaTeX's recorder lists ./figures/positive_zero_scatter.png as the only image input.

Tests

  • .venv/bin/python -m pytest tests/test_snapshot_artifacts.py tests/test_disclosures.py -n 4 -q -p no:cacheprovider -rs (the brief's command plus -rs to name skips): 137 passed, 0 skipped, 3 warnings (pandas DtypeWarning), 33.8 s. Nothing skipped, because pdftotext is on PATH here. The run includes the rendered-PDF tests, test_snapshot_manifest_hashes_match_rendered_paper_artifacts and test_paper_page_snapshot_matches_the_manifest.
  • The gap-checking subagent also ran the non-slow suite (-m "not slow") on an isolated clone: 860520e plus the three committed files.
    • The first run lost one xdist worker to the shell's memory cgroup, at 7.2 GB RSS in tests/test_notes.py::test_categorical_asset_error_statements.
    • A rerun with that test deselected gave 7068 passed, 89 skipped and 0 failed.
    • The deselected test alone passed in 9.1 s. It reads SNAP predictions, not the PDF or its pins.

CI

At 5de92ca, every check succeeds. The test job checked out refs/pull/209/merge (005c435: this head merged into main at 7bccd96).

lint                     success   05:28:26–05:28:44
app                      success   05:28:25–05:29:57
paper                    success   05:28:25–05:30:05   (HTML-only render)
test                     success   05:28:25–05:36:33   7081 passed, 91 skipped, 3 warnings in 464.68s (4/4 workers)
Vercel Preview Comments  success
Vercel (commit status)   success   "Deployment has completed"

The CI log doesn't name its skips, because the job runs without -rs.

Judgment calls

  1. Branch. I pushed to engine-upgrade-fixed-wording, the PR's head, as the brief says. This session's default branch was claude/policybench-pdf-figure-sync-qfiin8.

  2. TinyTeX bundle. The earlier hand-off doesn't name the bundle, so I used the asset Quarto 1.9.36's installer tries first for its TinyTeX bundle: TinyTeX-linux-x86_64-v2026.10.tar.xz (sha256 54fec1c79ab437d2d52e2f918bdb738c85779d654a93e62cc44f022e227692b5). I unpacked it to ~/.TinyTeX and repeated the steps quarto install tinytex runs afterwards:

    • wrote ~/.TinyTeX/version (v2026.10);
    • ran tlmgr postaction install script xetex;
    • ran tlmgr option repository https://mirrors.rit.edu/CTAN/systems/texlive/tlnet/, the first entry in Quarto's default list.

    luatex --version reports "This is LuaTeX, Version 1.24.0 (TeX Live 2026)". The render auto-installed no packages.

  3. xurl.sty. I extracted the file from Ubuntu 24.04's texlive-latex-extra_2023.20240207-1_all.deb with dpkg-deb -x. It carries \ProvidesPackage{xurl}[2022/01/09 v 0.10 modify URL breaks] and sha256 5cf09de8dd52b5e77a12145d4987ab4df75a304174d224aa9562eed3dd141ef3. I put it in ~/.TinyTeX/texmf-local/tex/latex/xurl/ and ran mktexlsr. I didn't install the whole package, because that would have added TeX Live 2023's lualatex to the system. A subagent recompiled the .tex without the file: pdftotext then differs from 860520e's in 12 lines, all URL breaks in the references. The commit message, taken verbatim from the brief, doesn't mention xurl. This comment records it.

  4. Quarto. I used the release tarball quarto-1.9.36-linux-amd64.tar.gz, unpacked to /opt/quarto-1.9.36. Its sha256 39d2e4546274be39a27cceeeed21a60b693c4c477a8c8cb43fb34326442f3910 matches the release's quarto-1.9.36-checksums.txt. Python 3.12.3 and uv sync --locked --extra dev --extra docs left uv.lock unchanged. I also installed librsvg2-bin 2.58.0; poppler-utils 24.02.0 was already present.

  5. Step 4. I ran it as written, in two parts, so I could list the output before copying it.

    • generate_figures.py wrote the SVG, this VM's PNG and paper/figures/manifest.json. Before replacing the PNG, I saved it outside the repo: sha256 fd241bb0…, pixel-identical to 860520e's faulty raster.
    • The PNG still hashed 85851be2… after the render. The .tex includes figures/positive_zero_scatter.png.
    • The render wrote out/pdf/index.pdf, not policybench.pdf, so I copied index.pdf.
    • freeze_snapshot.py --rendered-only printed pdf sha256: b8fe0592f43a368d4eddde5a5a790595f4faaba76989b590c8acafff2cfaa0f8 and web files: 17.
  6. Leftovers. The render left paper/.quarto/, which paper/.gitignore ignores. It stays out of the commit.

  7. Commit message. I used the brief's message verbatim, without the session trailer the harness suggests.

  8. main. main has moved to 7bccd96 (Leave recorded ceiling-exhaustion rows alone when applying an adjudication #224 and Correct the CI timing comments and test notes after #253 #255). I didn't merge it, since the brief doesn't ask for that. CI's merge ref already includes it.

  9. Comments. I posted the pickup comment with the GitHub MCP server's issue-comment tool, and this one with the brief's gh api repos/PolicyEngine/policybench/issues/209/comments command. Both use the same REST endpoint.

Seen, not touched

The last two of these appear identically in main's PDF and 860520e's. Fixing either would change the PDF's text, which this PR keeps equal to 860520e's.

  • No raster guard. No test or CI job compares the PDF's raster with the PNG; paper.yml renders HTML only. The SVG asks for Arial, sans-serif, and on this VM fc-match Arial returns Liberation Sans. rsvg-convert here reproduces 860520e's raster exactly. A full render_paper.py run on a machine like this one would rewrite both the web PNG and the PDF's raster.
  • Missing glyph. The three ≤ inside \texttt (index.tex lines 428, 430 and 2125) print no glyph. pdftotext gives |pred − ref| � 0.01 × |ref|. A subagent's LuaLaTeX log reports "Missing character: There is no ≤ (U+2264) in font [lmroman10-regular]".
  • TOC page numbers. The table of contents lists Abstract on page 1 and Introduction on page 2. They start on pages 2 and 3. A subagent found that a third LuaLaTeX pass corrects the numbers; Quarto ran two.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

A Claude Opus 5.5 agent checked the merge 860520e and the PDF rebuild 5de92ca. We post its review below as it wrote it. We've asked the session that rebuilt the PDF to rebuild it again with KOMA-Script 3.49.3, which sets the three missing anchors. S2, S3 and N1 also apply to main, so we're taking them up in a separate PR.


Verdict: REQUEST_CHANGES at 5de92ca5d8d3e4f63349d00c093a9eb5b3c63639

This is an independent review by a Claude Opus 5.5 session for the PolicyBench owner session. It covers the two commits that followed review round 2's approval of 942dba4a: the merge 860520e4 and the PDF rebuild 5de92ca5. I worked read-only in a fresh partial clone, using poppler 26.09, qpdf 12.4.2, jq and short Python scripts.

Both commits do what they say. The merge resolves no conflict and adds nothing that main didn't bring. The rebuilt PDF embeds the committed web PNG pixel for pixel. Its text equals 860520e4's byte for byte, and the pins match it. One regression blocks the merge. Since 942dba4a, the PR's PDF has sent three bookmarks and the three matching table-of-contents links to the wrong pages, and 5de92ca5 carries that forward. main's current PDF sends them to the right pages. In a minimal reproduction the KOMA-Script version decides it: 3.49.2 drops the three headings' anchors, and 3.49.3 sets them.

Findings

Blocking

B1. Three bookmarks and the matching table-of-contents links point at the wrong pages, where main's PDF gets them right.

main's PDF (sha256 501fb903…) and this head's (b8fe0592…) print the same table of contents, but three of its entries link elsewhere:

Entry main 942dba4a, 860520e4 and 5de92ca5
Abstract page 2, the abstract (section*.2) page 1, top of the title page (Doc-Start)
Appendix A: Structured-output audit page 50, the appendix (section*.5) page 49, the Conclusion heading (section.8)
Competing interests page 51, the section (section*.6) page 50, Table 14 (table.14)

The PDF outline (bookmarks) sends the same three entries to the same wrong places. The PDF opens with the bookmarks panel showing (/PageMode /UseOutlines). The other 23 bookmarks and the other 121 link annotations, all 72 URL links among them, match main's pages and positions.

$ outline() { qpdf --json --json-key=outlines "$1" | jq -r '.outlines | .. | objects | select(has("title")) | "\(.title) -> \(.destpageposfrom1)"'; }
$ diff <(outline main.pdf) <(outline 5de92ca5.pdf)      # 26 entries each
1c1
< Abstract -> 2
---
> Abstract -> 1
25,26c25,26
< Appendix A: Structured-output audit -> 50
< Competing interests -> 51
---
> Appendix A: Structured-output audit -> 49
> Competing interests -> 50

I resolved every link annotation's /GoTo destination through the /Dests name tree (from qpdf --json=2) and compared rectangles, target pages and /XYZ positions. Only the three table-of-contents links above differ: one on page 1 and two on page 2.

Cause. paper/index.qmd writes all three headings with {.unnumbered}, as main does; the PR changes only october_upgrade_row in that file. So the render toolchain differs, not the source. main's PDF holds 128 named destinations and this head's 124. This head's lacks the section* anchors for the "Table of contents" heading, Abstract, Appendix A and Competing interests. In a test document, Quarto 1.9.36 writes each such heading as \section*{…}\label{…} followed by \addcontentsline{toc}{section}{…}. When \section* sets no anchor, the entry links to the last anchor set before it. I compiled a minimal Quarto 1.9.36 document with the same heading pattern under LuaLaTeX:

  • TeX Live 2026 (LaTeX 2025-11-01, hyperref 7.01p, KOMA-Script 3.49.2 of 2026/02/02): Abstract links to Doc-Start, and each later unnumbered heading links to the table before it. This head's pattern.
  • The same install with KOMA-Script 3.49.3 (2026/10/19) swapped in: section* anchors and correct targets.
  • The same install with hyperref 7.01s swapped in and KOMA-Script 3.49.2 kept: still wrong.
  • TinyTeX (LaTeX 2026-06-01, hyperref 7.01s, KOMA-Script 3.49.3): correct.
  • The standard article class: correct under both installs.

KOMA-Script's version decides it. main's own history fits. Its PDFs at 5a8164a, bc30579, 2cb77a1 and bf845cb (rendered Oct 10 to 14) show this head's pattern, and those at 1404835 and 875fe1f (rendered Oct 25 and 29) show the correct one. I didn't inspect the cloud host's TinyTeX, so its KOMA-Script version stays an inference from the output.

Neither commit under review introduced this. 942dba4a's PDF already has it (round 2 approved that PDF), and 5de92ca5's rebuild on the same toolchain kept it. Merging still ships it to /paper.

Fix. Rebuild the PDF the way the hand-off describes for 5de92ca5: put the committed PNG in place of the host's raster, render the PDF only and leave the web files alone. This time use a TeX with KOMA-Script 3.49.3 or later (I tested 3.49.3), for example after tlmgr update koma-script in the render host's TinyTeX. Re-pin with scripts/freeze_snapshot.py --rendered-only. Before pushing, confirm that:

  1. outline (above) prints Abstract -> 2, Appendix A: Structured-output audit -> 50 and Competing interests -> 51, and its whole output equals that of main's PDF;
  2. pdftotext output equals 5de92ca5's byte for byte, and pdffonts lists the same six subset fonts (their object numbers may shift, as they do between main and 5de92ca5);
  3. pdfimages -list shows one image, on page 45, and its pixels equal app/public/paper/web/figures/positive_zero_scatter.png's;
  4. git diff --stat lists only the PDF, paper/snapshot/20260501/manifest.json and app/src/paperSnapshot.json, and each JSON file changes only its PDF hash.

A re-review then needs only those four checks.

Should-fix

S1. The PR body still describes the PDF as it stood before 5de92ca5. The third bullet under "The rendered paper changes two words" says the PDF's figure "carries those glyphs" (Liberation Sans), and that "a render on a Mac with Arial replaces the PDF" before merge. At 5de92ca5 the PDF embeds the committed PNG, and no Mac render took place. The Tests section ends with "CI at 942dba4: pending", and the opening names only the first merge (faf2f5c). If the squash message takes the body, these lines land in main's history. Suggested bullet:

app/public/paper/web/figures/positive_zero_scatter.png keeps main's bytes, and the PDF embeds it. The render host lacks Arial, so the render used the committed PNG in place of the host's own raster. The image that pdfimages extracts from page 45 equals the PNG pixel for pixel, as on main.

Also name the second merge (860520e4, main at 8ac64ac4), list the render toolchain with its KOMA-Script version after B1's fix, and replace the CI line with the result at the new head.

S2. CI checks none of the PDF's text, figure or navigation (also true on main; follow-up). .github/workflows/paper.yml renders HTML only. tests/test_disclosures.py skips its PDF half where neither pdftotext nor pypdf exists, "as in CI" in its own comment. The test job's log shows no poppler install and pyproject.toml lists no pypdf, so CI never reads the PDF. No test compares the figure raster with the PNG or checks the outline. B1 passes CI at 5de92ca5, and no test would have caught the Liberation Sans raster either. A full paper/render_paper.py run also copies the host's HTML output, figure included, into app/public/paper/web, and freeze_snapshot.py then re-pins it, so the hash tests pass on a host without Arial. Fix in a follow-up: install poppler-utils in the CI test job, as paper.yml installs librsvg2-bin, so the existing PDF tests run. Then add two tests: each outline entry's target page carries its title, and the page-45 image's decoded bytes equal the PNG's pixels.

S3. The ≤ sign prints as a blank in three formulas (also on main; follow-up). Page 6 reads "|pred − ref| 0.01 × |ref|" and "|pred| 1", and page 42 reads "1{|pred − ref| 1}". pdftotext returns U+FFFD at those three places in main's PDF and in this head's. The source writes them as inline code (paper/index.qmd lines 1055 and 1227), and the PDF sets inline code in Latin Modern Roman. pdffonts lists only LM Roman fonts, although _quarto.yml asks for tgpagella, helvet and inconsolata. When I compiled Quarto 1.9.36's LaTeX for such a span, LuaLaTeX logged "Missing character: There is no ≤ (U+2264) in font [lmroman10-regular]", and the same inequality in math mode printed ≤. Fix in a follow-up: write these formulas as math, or set a PDF monofont that carries U+2264, and add a test that the PDF's text holds no U+FFFD. The fix changes the PDF's text, so it doesn't belong in this PR.

Nits

N1. The table of contents prints page numbers one page low for 24 of its 26 entries (also on main; follow-up). It lists Abstract on page 1, Introduction on page 2 and Competing interests on page 50. They start on pages 2, 3 and 51. Only "3 Benchmark design" and "3.2" print the right page. Pages 1 and 2 render identically in main's PDF and this head's. The hand-off reports that a third LuaLaTeX pass corrects the numbers; I didn't test that. Quarto 1.9.36's PDF format accepts latex-min-runs (default 1), so latex-min-runs: 3 in _quarto.yml may fix it. It changes the PDF's text, so it belongs in a follow-up.

N2. Record the render host's LaTeX package versions. The hand-off names Quarto 1.9.36, the TinyTeX bundle with its sha256, LuaTeX 1.24.0 and xurl.sty v0.10. It doesn't name KOMA-Script, hyperref or the LaTeX kernel, and B1 turns on KOMA-Script's version. Copy the LaTeX2e <…>, Document Class: scrartcl … and Package: hyperref … lines from the LaTeX log into the PR body.

On the hand-off's "Seen, not touched" list: all three items (no raster guard, the missing ≤, the TOC page numbers) also appear on main, so none of them blocks this PR. They map to S2, S3 and N1. B1 isn't on that list.

Checked and sound

  1. The merge 860520e4.
    • git show --remerge-diff 860520e4 prints the header and no diff.
    • git merge-tree --write-tree 942dba4a 8ac64ac4 exits 0 with no conflicts and gives tree fd3972e7…, equal to 860520e4^{tree}.
    • The PR against each merge base, git diff --full-index --binary 875fe1f6 942dba4a and git diff --full-index --binary 8ac64ac4 860520e4, gives byte-identical patches (sha256 642e1cad…; 8 files, +267/−54).
    • git diff --full-index --binary 942dba4a 860520e4 equals git diff --full-index --binary 875fe1f6 8ac64ac4 byte for byte (sha256 8d1101ae…). The merge brought in exactly main's 31 changed files, and the PR touches none of them.
  2. Scope of 5de92ca5. git diff --stat 860520e4 5de92ca5 lists the PDF (265,025 to 265,550 bytes), app/src/paperSnapshot.json and paper/snapshot/20260501/manifest.json, with 2 insertions and 2 deletions. Its one parent is 860520e4, and the PR's timeline shows no force-push.
  3. The figure.
    • pdfimages -list shows one image, on page 45, in all three PDFs: 1800×1220, RGB, 8 bits, 291 ppi.
    • qpdf --show-object=319 (322 in main's PDF) gives /DeviceRGB and /FlateDecode, with no /SMask, /Mask, /Decode or /Intent.
    • The extracted image equals the web PNG under numpy.array_equal. Separately, qpdf's decoded stream (6,588,000 bytes, sha256 71fa47f2…) equals the PNG's pixels decoded with zlib alone. The PNG holds only IHDR, bKGD, IDAT and IEND chunks: 8-bit RGB, no alpha, no gAMA, iCCP, sRGB or cHRM.
    • main's page-45 image has the same pixels, and its compressed stream matches byte for byte (81,677 bytes, sha256 1c4a3183…).
    • 860520e4's image differs in 19,918 pixels (x 27–1676, y 49–1137). In a diff mask every differing pixel sits on a text label (title, subtitle, axis titles, tick labels, legend). The dots, grid and diagonal match.
    • Page 45's content stream decodes identically in all three PDFs: the image fills 445.1 × 301.7 pt at (83.459, 290.91). pdftoppm and pdftocairo render page 45 of this head and of main identically.
    • The plan's requirement holds: the new image equals the web PNG.
  4. Text, fonts and metadata.
    • pdftotext in default, -layout and -raw modes equals 860520e4's byte for byte (default output sha256 a1b70119…).
    • pdffonts equals 860520e4's. Against main, the same six Latin Modern subsets appear with shifted object numbers.
    • pdfinfo reports Producer LuaTeX-1.24.0, 54 pages and letter size. Only CreationDate, ModDate and the file size differ from 860520e4.
    • Rendering all 54 pages at 100 dpi, only page 45 differs from 860520e4, and only inside the figure's rectangle.
    • Object by object (qpdf --json=2 with decoded streams), both files hold 587 objects. Only the image (319), the Info dictionary (585, dates only) and the XRef stream (586, /ID and offsets) differ. qpdf --check finds no errors.
    • From main to this head, a word-level diff of pdftotext changes only "restored" to "fixed", twice: in page 25's paragraph and in page 35's table row. The default reading order also moves "Review outcome" and the page number 35, as the PR body says; -raw order shows only the two words. Rendered pages differ only on pages 25 and 35.
    • One side effect improves the PDF. On main's page 35, the table's last line runs into the footer ("35residence (Indiana county income tax)."). This head's shorter cell leaves the page number on its own line.
  5. Pins. The manifest's rendered_paper_artifacts.pdf.sha256 reads b8fe0592f43a368d4eddde5a5a790595f4faaba76989b590c8acafff2cfaa0f8, which equals shasum -a 256 of the PDF. pdfVersion reads b8fe0592f43a. A JSON deep diff against 860520e4 finds one changed path in each file. All 17 web-file pins match recomputed hashes, and webVersion stays af0ecbe96fac. No tracked file mentions the old hash. Both files equal json.dumps(indent=2, sort_keys=True) + "\n".
  6. Current main. Its two commits since the merge base (Leave recorded ceiling-exhaustion rows alone when applying an adjudication #224, Correct the CI timing comments and test notes after #253 #255) touch only .github/workflows/ci.yml, CLAUDE.md, policybench/adjudications.py and tests/test_adjudications.py. git grep finds no import of policybench.adjudications in any module under policybench/ or anywhere under paper/. The paper reads the committed us_adjudications.json and the manifest, and neither commit touches them, so they can't stale this PR's renders. GitHub's merge ref 005c435 merges 5de92ca5 into 7bccd96. Its tree equals a local git merge-tree result (a389ed15…), with the PR's rendered files unchanged. At 5de92ca5, lint, app, paper, test and Vercel succeed. The test job's log shows HEAD is now at 005c435 and "7081 passed, 91 skipped". GitHub reports the PR mergeable, clean and not a draft.
  7. The hand-off's other claims that I could test hold, among them the 19,918-pixel count and its bounds, the 523-byte growth of the image stream, the 124 links, 26 outline entries and 124 named destinations that match 860520e4's, and PTEX.FullBanner "This is LuaHBTeX, Version 1.24.0 (TeX Live 2026)". render_paper.py copies out/pdf/index.pdf to policybench.pdf when no policybench.pdf exists, which matches the hand-off's manual copy.

What I couldn't check

  • The render host's KOMA-Script, hyperref and LaTeX kernel versions. B1's cause on that host rests on the output and on my reproduction.
  • The repository's test suite: I didn't build its environment. CI skips the PDF half of the wording tests, and the hand-off reports 137 passed with pdftotext present. This head's PDF text equals 860520e4's byte for byte, so text-based tests give the same result on both PDFs.
  • Whether the web PNG's labels use Arial. Pixels don't name a font, so I confirmed only that the PDF's image equals the committed PNG.
  • The hand-off's byte-for-byte reproduction from the captured .tex, its account of how LuaTeX derives /ID, the LuaTeX recorder's input list and its xurl.sty experiment. The .tex isn't in the repository.
  • Whether a third LuaLaTeX pass fixes the TOC page numbers (N1).
  • The PDF that policybench.org serves today. I compared against main's committed PDF, which app/src/app/paper/page.tsx links as /paper/policybench.pdf.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the PolicyBench session to rebuild the PDF with KOMA-Script 3.49.3. Reply here if you're mid-change and I'll stop.


Generated by Claude Code

KOMA-Script 3.49.2 set no anchor for the paper's unnumbered headings, so the
Abstract, Appendix A and Competing interests bookmarks and contents links
pointed one page early. Under 3.49.3 the outline equals main's. The PDF's
text equals 5de92ca's, and its raster still equals
app/public/paper/web/figures/positive_zero_scatter.png pixel for pixel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 0e02069523a2cf0ae919256e35ae63b009ea2930. It's one commit on 5de92ca5d8d3e4f63349d00c093a9eb5b3c63639, with no rebase or force-push. It changes app/public/paper/policybench.pdf, paper/snapshot/20260501/manifest.json and app/src/paperSnapshot.json. Rendered with KOMA-Script 3.49.3, the PDF's outline and named destinations equal main's, its text equals 5de92ca5's byte for byte, and its figure raster equals app/public/paper/web/figures/positive_zero_scatter.png pixel for pixel.

Versions

$ grep -E '^LaTeX2e <|^Document Class: scrartcl|^Package: hyperref' v.log
LaTeX2e <2026-06-01>
Document Class: scrartcl 2026/10/19 v3.49.3 KOMA-Script document class (article)
Package: hyperref 2026-06-17 v7.01r Hypertext links for LaTeX
  • Before the update, the bundle's scrartcl.cls read \@CheckKOMAScriptVersion{2026/02/02 v3.49.2 KOMA-Script}. After tlmgr update --self && tlmgr update koma-script it reads \@CheckKOMAScriptVersion{2026/10/19 v3.49.3 KOMA-Script}.
  • tlmgr.log records two updates: texlive.infra (79982 -> 80721) and koma-script (77575 -> 80700). hyperref (r80357, v7.01r) and the LaTeX kernel kept the bundle's versions.
  • The rest matches the hand-off for 5de92ca5: Quarto 1.9.36, TinyTeX 2026.10 (This is LuaTeX, Version 1.24.0 (TeX Live 2026)), xurl.sty v0.10, Python 3.12.3 with uv sync --locked --extra dev --extra docs, librsvg 2.58.0 and poppler 24.02.0.

Step 4 checks

/tmp/prev.pdf is git show 5de92ca5:app/public/paper/policybench.pdf (sha256 b8fe0592…), and /tmp/main.pdf is git show origin/main:app/public/paper/policybench.pdf at 7bccd96 (sha256 501fb903…).

a. Outline (qpdf 12.4.2, judgment call 2)

$ outline app/public/paper/policybench.pdf
Abstract -> 2
Introduction -> 3
Related work -> 3
Benchmark design -> 4
Headline metric: exact match -> 5
Near-miss companion: within 1% -> 5
Secondary metric: bounded continuous score -> 6
Frozen snapshot and open-set status -> 8
Data and scenario construction -> 17
United States -> 17
Disability in the household facts -> 18
Reference-output credibility -> 20
Results -> 38
United States leaderboard -> 38
Uncertainty -> 38
Cost and latency -> 39
Simple baselines -> 41
Sensitivity to benchmark view -> 42
Hardest benchmark targets -> 43
Zero and positive cases -> 44
Open-weight time lag -> 44
Failure modes -> 46
Limitations -> 48
Conclusion -> 49
Appendix A: Structured-output audit -> 50
Competing interests -> 51

$ diff <(outline app/public/paper/policybench.pdf) <(outline /tmp/main.pdf)
[exit 0, no output]

$ diff <(outline /tmp/prev.pdf) <(outline /tmp/main.pdf)
1c1
< Abstract -> 1
---
> Abstract -> 2
25,26c25,26
< Appendix A: Structured-output audit -> 49
< Competing interests -> 50
---
> Appendix A: Structured-output audit -> 50
> Competing interests -> 51
[exit 1]

b. Named destinations

$ echo "new: $(names app/public/paper/policybench.pdf | wc -l)  main: $(names /tmp/main.pdf | wc -l)  prev: $(names /tmp/prev.pdf | wc -l)"
new: 128  main: 128  prev: 124
$ diff <(names app/public/paper/policybench.pdf) <(names /tmp/main.pdf)
[exit 0, no output]

c. Text, fonts and metadata

$ cmp <(pdftotext app/public/paper/policybench.pdf -) <(pdftotext /tmp/prev.pdf -)
[exit 0]
$ pdffonts app/public/paper/policybench.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
BXQVBW+LMRoman17-Regular             CID Type 0C       Identity-H       yes yes yes     30  0
GVSSFD+LMRoman12-Regular             CID Type 0C       Identity-H       yes yes yes     31  0
UULGZQ+LMRoman10-Regular             CID Type 0C       Identity-H       yes yes yes     32  0
TMHZDF+LMRoman6-Regular              CID Type 0C       Identity-H       yes yes yes     34  0
CZOBCD+LMRoman9-Regular              CID Type 0C       Identity-H       yes yes yes     35  0
LCTEHD+LMRoman8-Regular              CID Type 0C       Identity-H       yes yes yes     69  0
$ pdffonts /tmp/prev.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
BXQVBW+LMRoman17-Regular             CID Type 0C       Identity-H       yes yes yes     30  0
GVSSFD+LMRoman12-Regular             CID Type 0C       Identity-H       yes yes yes     31  0
UULGZQ+LMRoman10-Regular             CID Type 0C       Identity-H       yes yes yes     32  0
TMHZDF+LMRoman6-Regular              CID Type 0C       Identity-H       yes yes yes     33  0
CZOBCD+LMRoman9-Regular              CID Type 0C       Identity-H       yes yes yes     34  0
LCTEHD+LMRoman8-Regular              CID Type 0C       Identity-H       yes yes yes     67  0
$ diff <(pdffonts app/public/paper/policybench.pdf | cut -c1-83) <(pdffonts /tmp/prev.pdf | cut -c1-83)   # every column but the object ID
[exit 0]
$ pdfinfo app/public/paper/policybench.pdf
Title:           PolicyBench
Subject:         
Keywords:        
Author:          Max Ghenis
Creator:         LaTeX via pandoc
Producer:        LuaTeX-1.24.0
CreationDate:    Tue Nov  3 07:11:55 2026 UTC
ModDate:         Tue Nov  3 07:11:55 2026 UTC
Custom Metadata: yes
Metadata Stream: no
Tagged:          no
UserProperties:  no
Suspects:        no
Form:            none
JavaScript:      no
Pages:           54
Encrypted:       no
Page size:       612 x 792 pts (letter)
Page rot:        0
File size:       265570 bytes
Optimized:       no
PDF version:     1.7

d. Raster

$ pdfimages -list app/public/paper/policybench.pdf
page   num  type   width height color comp bpc  enc interp  object ID x-ppi y-ppi size ratio
--------------------------------------------------------------------------------------------
  45     0 image    1800  1220  rgb     3   8  image  no       322  0   291   291 79.8K 1.2%
$ pdfimages -png app/public/paper/policybench.pdf /tmp/img
$ uv run --no-project --with pillow python -c "
from PIL import Image, ImageChops as C
a = Image.open('/tmp/img-000.png').convert('RGB')
b = Image.open('app/public/paper/web/figures/positive_zero_scatter.png').convert('RGB')
print(C.difference(a, b).getbbox())"
None

e. Scope

$ git status --porcelain
 M app/public/paper/policybench.pdf
 M app/src/paperSnapshot.json
 M paper/snapshot/20260501/manifest.json
$ git diff -- paper/snapshot/20260501/manifest.json app/src/paperSnapshot.json
diff --git a/app/src/paperSnapshot.json b/app/src/paperSnapshot.json
index 944be88..913ddc4 100644
--- a/app/src/paperSnapshot.json
+++ b/app/src/paperSnapshot.json
@@ -1,5 +1,5 @@
 {
-  "pdfVersion": "b8fe0592f43a",
+  "pdfVersion": "23a6a3009d67",
   "responseWindow": "June 12 and October 26, 2026",
   "snapshotDate": "2026-10-27",
   "webVersion": "af0ecbe96fac"
diff --git a/paper/snapshot/20260501/manifest.json b/paper/snapshot/20260501/manifest.json
index 164e6c9..77238b5 100644
--- a/paper/snapshot/20260501/manifest.json
+++ b/paper/snapshot/20260501/manifest.json
@@ -165,7 +165,7 @@
   "rendered_paper_artifacts": {
     "pdf": {
       "path": "app/public/paper/policybench.pdf",
-      "sha256": "b8fe0592f43a368d4eddde5a5a790595f4faaba76989b590c8acafff2cfaa0f8"
+      "sha256": "23a6a3009d677dba02e332b054be15f19416c42512d85930588d94eff5d61113"
     },
     "web": {
       "files": {
$ sha256sum app/public/paper/policybench.pdf app/public/paper/web/index.html app/public/paper/web/figures/positive_zero_scatter.png
23a6a3009d677dba02e332b054be15f19416c42512d85930588d94eff5d61113  app/public/paper/policybench.pdf
af0ecbe96fac2eb5c1f80fead5e122575e5f7024efcac80d0342e640019e3b5c  app/public/paper/web/index.html
85851be224bcceddc3ee15776db2b1917babcfc58e8dc7fbf0f8c26f57b2b9a6  app/public/paper/web/figures/positive_zero_scatter.png

Independent checks

Six subagents re-checked the PDF read-only, each with methods of its own, and a seventh looked for gaps. All six passed, and the seventh returned "ready". They finished after the push (judgment call 7). Each confirmed that the file it checked is the blob at 0e02069 (sha256 23a6a300…).

  • Navigation. A pikepdf walker, a pypdf walker and MuPDF 1.28.6 agree on 26 outline entries, 124 link annotations (52 internal, 72 URI) and 128 named destinations.
    • All three find this PDF equal to main's in every field they read: titles, nesting, /Count, destination names, pages, /XYZ coordinates, rects, /Border, /C, /H, URIs and the catalog (/PageMode /UseOutlines, /OpenAction to page 1 /Fit, no /PageLabels).
    • Each outline entry's heading sits on its target page, with no text between the destination and the heading. The destination sits 21.9–24.6 pt above a section heading's baseline and 17.9–20.6 pt above a subsection's, or 10.96 pt for the two headings that open a page (5 and 5.3).
    • Against prev, the Abstract, Appendix A and Competing interests bookmarks and their table-of-contents links move from Doc-Start (p1), section.8 (p49) and table.14 (p50) to section*.2 (p2), section*.5 (p50) and section*.6 (p51).
    • Four destinations are new: those three, and section*.1 for the "Table of contents" heading on p1, which nothing links to.
    • Three keep their place under a new number: figure.caption.1 → .3 (p45), section*.2 → .4 (p44) and section*.3 → .7 (p52). section*.4 and section*.7 are the \phantomsection anchors that index.tex sets for the labels open-weight-lag and refs. The body link to the figure (p44) changes its destination name only.
  • Visible content. pdftoppm, pdftocairo, PDFium and MuPDF render all 54 pages identically to prev's at 100 dpi.
    • The 54 decoded page content streams are byte-identical to prev's.
    • pdftotext in default, -layout, -raw and -bbox-layout modes, pypdf and MuPDF all give prev's text.
    • Against main, only pages 25 and 35 differ. A -raw word diff finds two changes, both "restored" → "fixed".
  • Raster. The PDF holds one image, object 322, drawn as /Im1 on page 45: 1800×1220, /DeviceRGB, 8 bits, /FlateDecode. It has no /DecodeParms, /SMask, /Mask, /Decode, /Intent or /Interpolate.
    • Eleven decoders give the PNG's samples, with 0 differing pixels (sha256 71fa47f2…). They include qpdf 12.4.2 and 11.9.0, pikepdf, pypdf, poppler and an inflate written without zlib, and ImageMagick's decode of the PNG gives the same bytes.
    • The compressed stream is byte-identical to the one in prev and main (sha256 1c4a3183…).
    • The PNG holds only IHDR, bKGD, IDAT and IEND chunks.
    • The comparison can fail: one flipped bit shows as 1 differing pixel, and this host's own raster differs in 19,918.
  • Structure. Against main, all 590 objects keep their numbers, and four of them differ: the content streams of pages 25 and 35, the Info dictionary (dates) and the XRef stream. A qpdf QDF diff agrees.
    • Against prev, every stream body is identical: 54 page contents, 6 font programs, 6 ToUnicode and 6 CIDSet streams, and the image.
    • What differs from prev is the destination name tree, four new destination objects, the destination names in 3 outline actions and 4 link annotations, the Info dates, /ID, and the object streams and numbering.
    • qpdf --check passes. None of /Lang, /ViewerPreferences, /PageLabels, XMP metadata, /StructTreeRoot or /AcroForm appears in the three PDFs.
  • Pins. A JSON-aware diff finds one changed leaf per file: rendered_paper_artifacts.pdf.sha256 and pdfVersion.
    • The PDF hash and the 17 web-file hashes match recomputed values: 17 files on disk, 17 pinned and 17 tracked.
    • Both files equal json.dumps(indent=2, sort_keys=True) + "\n".
    • git grep finds the old prefix b8fe0592f43a in no tracked file, and the new prefix only in the two pins.
  • Reproduction. A subagent compiled the render's saved index.tex twice with this TinyTeX, with SOURCE_DATE_EPOCH set to the render's time.
    • Its PDF differs from this head's only inside the trailer /ID, in 60 bytes. With this head's /ID spliced in, its sha256 is 23a6a300….
    • It compiled the same index.tex with KOMA-Script 3.49.2 from the unmodified TinyTeX 2026.10 bundle, at prev's time. That PDF differs from prev only inside /ID, in 58 bytes, and with prev's /ID spliced in its sha256 is b8fe0592….
    • I re-ran both splices myself. So the KOMA-Script version is the only input that differs between the two PDFs.
    • Its 3.49.3 log loads all nine KOMA-Script files the class uses at v3.49.3, hyperref 7.01r, LaTeX2e 2026-06-01 and xurl.sty v0.10 from ~/.TinyTeX/texmf-local.
    • Its .toc names section*.2, section*.5 and section*.6 for the three entries, where the 3.49.2 build's names Doc-Start, section.8 and table.14.

Tests

.venv/bin/python -m pytest tests/test_snapshot_artifacts.py tests/test_disclosures.py -n 4 -q -p no:cacheprovider -rs (the brief's command plus -rs to name skips): 137 passed, 0 skipped, 3 warnings (pandas DtypeWarning), 23.0 s. pdftotext is on PATH here, so tests/test_disclosures.py read the PDF's text with it.

CI

At 0e02069, every check succeeds. The test job checked out 470029d ("Merge 0e02069 into 7bccd96"), this head merged into main.

lint                     success   07:18:24–07:18:37
app                      success   07:18:24–07:19:40
paper                    success   07:18:24–07:20:06   (HTML-only render)
test                     success   07:18:24–07:26:46   7081 passed, 91 skipped, 3 warnings in 475.41s (4/4 workers)
Vercel Preview Comments  success   07:20:07
Vercel (commit status)   success   "Deployment has completed"

The test job runs uv run pytest -m "not slow" -n logical --dist loadfile --tb=short, without -rs, so its log doesn't name its skips.

Judgment calls

  1. Branch. I pushed to engine-upgrade-fixed-wording, the PR's head, as the brief says. This session's default branch was claude/policybench-pdf-koma-script-nj2oru, and I pushed nothing there.

  2. qpdf for check a. The brief's package list installs Ubuntu 24.04's qpdf 11.9.0.

    • It prints null for every destpageposfrom1, because it doesn't resolve hyperref's named destinations. With it, the outline diff prints nothing for any pair of PDFs, prev included.
    • I ran the step 4 checks with qpdf 12.4.2 first on PATH, the version the owner's check used. It came from the release's qpdf-12.4.2-bin-linux-x86_64.zip (sha256 db367d89…, which matches the release's qpdf-12.4.2.sha256), unpacked to /opt/qpdf-12.4.2.
    • Only check a calls qpdf. With 12.4.2, the diff against prev prints the three wrong pages, as the brief expects.
    • For check c's fonts, I compared pdffonts output with cut -c1-83, which keeps every column but the object ID.
  3. TinyTeX. I used the same bundle and post-install steps as for 5de92ca5:

    • unpacked TinyTeX-linux-x86_64-v2026.10.tar.xz (sha256 54fec1c7…) to ~/.TinyTeX;
    • wrote v2026.10 to ~/.TinyTeX/version;
    • ran tlmgr postaction install script xetex;
    • set the repository to https://mirrors.rit.edu/CTAN/systems/texlive/tlnet/. That mirror carried 3.49.3, so I didn't need mirrors.ctan.org.

    I ran each update with --dry-run first. update --self listed only texlive.infra, and update koma-script listed only koma-script. tlmgr info --data cat-version still reports 3.49.2 for koma-script r80700. That field comes from TeX Live's catalogue data; the installed files carry v3.49.3.

  4. xurl.sty. As for 5de92ca5: I extracted it from Ubuntu 24.04's texlive-latex-extra_2023.20240207-1_all.deb with dpkg-deb -x, put it in ~/.TinyTeX/texmf-local/tex/latex/xurl/ and ran mktexlsr. It has sha256 5cf09de8… and \ProvidesPackage{xurl}[2022/01/09 v 0.10 modify URL breaks].

  5. Quarto and Python. I used quarto-1.9.36-linux-amd64.tar.gz (sha256 39d2e454…, which matches the release's quarto-1.9.36-checksums.txt), unpacked to /opt/quarto-1.9.36 and put on PATH for the render.

    • TinyTeX stayed off PATH. Quarto 1.9.36 takes the TeX binaries from ~/.TinyTeX/bin/x86_64-linux when that directory exists (tinyTexBinDir in quarto.js).
    • The container's default python3 is 3.13, so I ran UV_PYTHON=python3.12 uv sync --locked --extra dev --extra docs. It left uv.lock unchanged.
  6. Render. I ran step 3 as written, in parts.

    • generate_figures.py wrote this host's PNG (sha256 fd241bb0…, as for 5de92ca5). I replaced it with the committed PNG, which hashed 85851be2… before and after the render.
    • Quarto ran LuaLaTeX twice and installed no packages. The render wrote out/pdf/index.pdf, so I copied that.
    • freeze_snapshot.py --rendered-only printed pdf sha256: 23a6a3009d677dba02e332b054be15f19416c42512d85930588d94eff5d61113 and web files: 17.
    • The render also wrote out/pdf/_tex/, the .tex bundle. I copied it outside the repo for the reproduction check before rm -rf paper/out paper/figures.
    • The render left paper/.quarto/, which paper/.gitignore ignores.
  7. Push before the independent checks finished. Step 4's checks had all passed when the session's stop hook asked for the changes to be committed and pushed. I pushed at 07:18 UTC while the six subagents were still running. They checked the committed bytes, and none found anything that needed a change.

  8. Tests. I added -rs to the brief's pytest command to name skips.

  9. Commit message. I used the brief's message verbatim. It omits the Claude-Session: trailer the harness suggests, as 5de92ca5's does. Adding the trailer now would take an amend and a force-push.

  10. main. origin/main is at 7bccd96, as when 5de92ca5 was built. I didn't merge it. CI's merge commit 470029d includes it.

  11. GitHub calls. I posted the pickup comment and this one with the GitHub MCP server's issue-comment tool. While CI ran, I polled its check runs with the brief's gh api .../check-runs command. I read the final results with the MCP server's check-run and job-log tools.

Seen, not touched

The first three appear identically in main's PDF and 5de92ca5's. Fixing any of them would change the PDF's text, which this commit keeps equal to 5de92ca5's.

  • TOC page numbers (N1). As on main, 24 of the 26 printed numbers are one page low. A subagent found why Quarto stops after two passes on this toolchain, and I checked its logs and the kernel source:
    • Pass 1's log has "Label(s) may have changed. Rerun to get cross-references right."
    • Pass 2's log has 12 Label `…' multiply defined warnings, for table labels such as tbl-snapshot and tbl-reference-review, and then "There were multiply-defined labels".
    • \enddocument in latex.ltx prints that warning in place of the rerun warning. Quarto reruns only on "Rerun to get" and similar messages.
    • A third LuaLaTeX pass changes only the content streams of pages 1 and 2.
  • The missing ≤ and the font fallback (S3).
    • Pass 2's log has LaTeX Font Warning: Font shape `TU/qpl/m/n' undefined: tgpagella's qpl family has no TU-encoded shapes.
    • It also has 12 "Missing character: There is no ≤ (U+2264) in font [lmroman10-regular]" lines, for the three ≤ at index.tex lines 428, 430 and 2125.
    • The PDF embeds only six Latin Modern Roman Regular faces, none of them italic, bold or monospace, although index.tex has 29 \emph and 54 \texttt.
  • Nothing records the KOMA-Script requirement. No tracked file pins the TeX toolchain. A git grep for TeX names finds only the historical log docs/release_20261006/validation/render.txt and an unrelated line in app/bun.lock. paper.yml renders --to html only. A PDF rebuild on the stock TinyTeX 2026.10 bundle would bring back the three wrong targets. S2's proposed outline test would catch that.

Separately, the push printed "GitHub found 3 vulnerabilities on PolicyEngine/policybench's default branch (1 high, 2 moderate)".


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

A Claude Opus 5.5 agent reviewed this PR at 0e020695 for the session that owns it. The agent also read the description we had drafted to replace the live one, and its review calls that text "the draft". We post its review below without its list of local files.


Verdict: APPROVE at 0e02069523a2cf0ae919256e35ae63b009ea2930

A Claude Opus 5.5 session wrote this independent review for the PolicyBench owner session, using git 2.55, gh 2.100, qpdf 12.4.2, poppler 26.09 (pdftotext, pdfimages, pdffonts, pdfinfo, pdftoppm), jq 1.8.2, pytest 8.4.2 and short Python 3.12 scripts, among them a PNG decoder that uses only zlib. I stayed read-only on GitHub and worked in a fresh shared clone and a worktree of my own. I didn't open work-0e02/ or owner-checks.md, and I rendered nothing with Quarto or LaTeX.

0e02069 does what it says. Its PDF's outline, 128 named destinations and 124 links equal main's, view coordinates included. Its text equals 5de92ca's byte for byte, its one image equals the committed PNG pixel for pixel, and the commit touches only the PDF and its two pins. Against main, the PDF's objects differ only in the content streams of pages 25 and 35, which carry the two changed words, in the Info dictionary's dates and in the cross-reference stream. CI passes at the head, and GitHub reports the PR mergeable and clean. Nothing blocks the merge.

The live body still says "CI at 942dba4: pending" and promises a Mac render before merge, so the draft should replace it before the merge. The draft needs three corrections first, and none of them needs a commit. It says round 2 approved 942dba4, but round 2 approved 4988b0c before 942dba4 existed (S1). Its second Reviews bullet breaks the voice guide and points at a follow-up that doesn't exist (S2). It says version 2.60.0 fixes no output, while the paper reports four scored references that 2.60.0's fixes move (S3). S1 also means that no reviewer had read 942dba4's code before this review. I read it and found no fault, and each of round 2's three mutants now fails a test.

Findings

Blocking

None.

Should-fix (description draft)

S1. The draft misdates round 2's approval and the start of the anchor defect.

The Reviews section says "The round-2 reviewer [approved 942dba4 with findings]". That comment approved 4988b0c 45 minutes before 942dba4's commit time:

$ gh api repos/PolicyEngine/policybench/issues/comments/6586455053 --jq .created_at
2026-11-02T19:28:51Z
  body: **Verdict: APPROVE** at `4988b0c0ade6245d0e4038c410f6c94ac54a7807` (the PR's head on GitHub is still this SHA).
$ gh api repos/PolicyEngine/policybench/issues/comments/6587586433 --jq .created_at
2026-11-02T20:23:17Z
  body, first line: Response to review r2, at 942dba4.
$ gh api "repos/PolicyEngine/policybench/pulls/209/commits?per_page=100" --jq '.[] | "\(.sha[0:10]) \(.commit.author.date)"'
4988b0c0ad 2026-11-02T15:11:23Z
942dba4a27 2026-11-02T20:13:32Z

So 942dba4 answered round 2's findings, and no reviewer approved it. The previous check's brief (pdf-rebuild-check/prompt.md) and its review (review.md: "the two commits that followed review round 2's approval of 942dba4a") repeat the premise, so 942dba4's code change had no independent reader before this review. Item 7 under "Checked and sound" covers it.

The KOMA-Script bullet says "With 3.49.2, the PDFs from 942dba4 to 5de92ca lacked the anchors". Every PDF this PR committed before 0e02069 lacks them, starting with 4399335's, and main's own PDF lacked them until #228 (14048355, 2026-10-25). The PR's PDF parted from main's at the merge faf2f5c. main's side of that merge (875fe1f) had the anchors, and the merge's re-render dropped them. 4988b0c, 942dba4 and 860520e kept that file byte for byte.

Commit PDF sha256 Unnumbered-heading anchors in /Dests Abstract / Appendix A / Competing interests bookmarks
main 5a8164a0 (#208, 10-10) 1607c71abaf3 section*.2, .3 p1 / p45 / p46
PR 4399335 (10-10) f15264138ec2 section*.2, .3 p1 / p45 / p46
main bf845cbe (#221, 10-14) d0b236367fb3 section*.2, .3 p1 / p48 / p48
main 14048355 (#228, 10-25) 41443b877bdb section*.1, .2, .4 to .7 p2 / p49 / p50
main 875fe1f (#247), same blob at 7bccd96 501fb9038a2f section*.1, .2, .4 to .7 p2 / p50 / p51
PR faf2f5c, same blob at 4988b0c, 942dba4, 860520e 24034abf10c9 section*.2, .3 p1 / p49 / p50
PR 5de92ca b8fe0592f43a section*.2, .3 p1 / p49 / p50
PR 0e02069 23a6a3009d67 section*.1, .2, .4 to .7 p2 / p50 / p51

Commands: git cat-file blob "${c}:app/public/paper/policybench.pdf" | shasum -a 256 for each commit; qpdf --json --json-key=outlines <pdf> | jq -r '.outlines | .. | objects | select(has("title")) | "\(.title) -> \(.destpageposfrom1)"' for the bookmarks; scripts/nav_dump.py on a qpdf --json=2 --json-stream-data=none dump for the /Dests names. git log 5a8164a0..875fe1f6 -- app/public/paper/policybench.pdf lists main's five PDF changes in that span (#217, #218, #221, #228, #247), and #228 brings the first PDF with the four anchors.

Suggested wording:

An independent reviewer approved 4988b0c with findings, and we answered each one in 942dba4.

KOMA-Script 3.49.2 sets no anchor for four unnumbered headings: the table of contents, Abstract, Appendix A and Competing interests. Every PDF we committed on this branch before 0e02069 lacked those anchors, as main's did until #228. From faf2f5c to 5de92ca, the bookmarks and table-of-contents links for the last three each pointed one page early, while main's pointed to the right pages.

S2. The second Reviews bullet breaks four rules of the voice guide and points at a follow-up that doesn't exist.

The draft reads: "An independent review asked for changes at 5de92ca over the three bookmark targets (B1). We rebuilt the PDF in 0e02069 and answered S1 and N2 in this description, and left S2, S3 and N1, which also hold on main, for a follow-up."

The three findings do hold on main (item 8 under "Checked and sound"). Suggested wording, once a follow-up issue or PR exists:

A second reviewer asked for changes at 5de92ca because three bookmarks and their table-of-contents links pointed one page early. We rebuilt the PDF in 0e02069. Three of that reviewer's other findings also hold on main, and we'll fix them in #NNN: CI checks none of the PDF's text, figure or navigation, three formulas in the PDF show a blank where ≤ belongs, and its table of contents lists 24 of its 26 page numbers one page low.

S3. "Version 2.60.0 fixes no output" contradicts the paper.

The head's PDF says of the 2.60.0 move: "The move changes four scored references beyond the dollar tolerance and none within it". It then names the upstream changes behind them, policyengine-us #10238 (free school breakfast in Pennsylvania and Arkansas) and #10036 (California's modified categorical eligibility for CalFresh). reference_audit/2026-10-22-engine-upgrade/README.md lists the same four outputs under a "Fix" column. The 2.60.0 move regenerates no exclusion and returns none to scoring. The README says "No exclusion is regenerated, and nothing is newly excluded", and the paper says "No excluded output returns to scoring". The paper prints no fix sentence for 2.60.0 for that reason: engine_upgrade_restored_sentence returns "" when engine_upgrade_restored_fix_phrase comes out empty, and that phrase draws its groups from the move's regenerated_exclusions, which 2.60.0 leaves empty. For the same move, the code's split clause reads "returns no excluded output to scoring" (scripts/move_counts.py; item 9 under "Checked and sound"). The live body's "The 2.60.0 move fixes nothing" made the same claim. Suggested wording:

On 2.60.0 we returned no excluded output to scoring, so engine_upgrade_restored_sentence writes none for it.

Nits

N1. The draft names one of the two tlmgr commands. The hand-off reports that the bundle's scrartcl.cls read v3.49.2 before the update and v3.49.3 "After tlmgr update --self && tlmgr update koma-script". Its tlmgr.log lines show both updates (texlive.infra (79982 -> 80721) and koma-script (77575 -> 80700)). The draft names only tlmgr update koma-script. Name both, so that a rebuild from the description repeats the hand-off's steps.

N2. Three names make the reader work.

N3. "which we moved to on" (optional). The 2.38.6 and 2.41.0 bullets stack two prepositions. One option: "We moved to version 2.38.6 on 2026-10-10. It fixes 18 outputs, and we ruled on 4 of them while they still counted."

N4. The new heading drops a true one (optional). "## Rendered paper" replaces "## The rendered paper changes two words". The old heading still holds at 0e02069: the HTML and the PDF text each change the same two words against main, and the PDF's navigation now equals main's.

N5. The Tests list leaves out round 2's mutants. The response to round 2 promised to run its three surviving mutants and said "The PR merges after the re-render and the mutant run." The draft reports no result. I ran them at the head with scripts/mutants.py and scripts/mutant_property_level.py, each selecting -k "one_ruled or split_clause or upgrade_sentences" in tests/test_paper_results.py and restoring the file afterwards:

baseline                                              3 passed
"fixed" only when ruled > 1, in fixed_or_restored     KILLED  2 failed: test_one_ruled_output_makes_the_fix_sentence_say_fixed,
                                                                         test_the_split_clause_counts_both_kinds
"fixed" only when ruled > 1, in the property          KILLED  1 failed: test_one_ruled_output_makes_the_fix_sentence_say_fixed
  (round 2's site)
"fixed" only when restored > 9, in the property       KILLED  1 failed: test_one_ruled_output_makes_the_fix_sentence_say_fixed
literal 2026-10-06 in the plural "kept" branch        KILLED  1 failed: test_the_split_clause_counts_both_kinds
restored file identical: True

Three more lines in the mutant list would close that promise in the record: the sentence says "fixed" only from two ruled outputs; the sentence says "fixed" only above nine fixed outputs; the split prints 2026-10-06 in place of its ruling date.

Checked and sound

  1. B1's four checks pass.
    • Outline. With outline() { qpdf --json --json-key=outlines "$1" | jq -r '.outlines | .. | objects | select(has("title")) | "\(.title) -> \(.destpageposfrom1)"'; }, diff of main's PDF and the head's exits 0 over 26 entries. The head prints Abstract -> 2, Appendix A: Structured-output audit -> 50 and Competing interests -> 51, where 5de92ca printed 1, 49 and 50. scripts/nav_dump.py writes each PDF's catalog, named destinations, outline and links with names, pages and /XYZ views; all four files equal main's. The catalog opens page 1 at /Fit with /PageMode /UseOutlines, and its /Names tree holds only /Dests. The 128 destinations have no duplicate names and all resolve to pages; the 124 links split into 52 to named destinations and 72 to URIs. scripts/heading_at_dest.py takes each outline entry's /XYZ top on its target page and finds the first pdftotext -bbox-layout line whose box bottom lies below it; that line must carry the entry's title. All 26 pass in the head's PDF and in main's. In 5de92ca's, 23 pass and three fail: Doc-Start (p1) shows "PolicyBench", section.8 (p49) shows "8 Conclusion", and table.14 (p50) shows "Table 14: Structured-output parser audit and repair sequence."
    • Text and fonts. pdftotext output equals 5de92ca's in the default, -layout and -raw modes (cmp exits 0; sha256 a1b701196a75…, f8596530f0fe…, 06c655e1b91f…). pdffonts lists the same six subsets: BXQVBW+LMRoman17-Regular, GVSSFD+LMRoman12-Regular, UULGZQ+LMRoman10-Regular, TMHZDF+LMRoman6-Regular, CZOBCD+LMRoman9-Regular and LCTEHD+LMRoman8-Regular, all CID Type 0C, embedded, subset, with ToUnicode. The listing equals main's byte for byte, object IDs included (30, 31, 32, 34, 35, 69); 5de92ca's differs only in its IDs (30, 31, 32, 33, 34, 67).
    • Image. pdfimages -list shows one image, on page 45: 1800×1220, RGB, 8 bits per component, object 322, 291 ppi. Its dictionary reads /BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter /FlateDecode /Height 1220 /Length 81677 /Subtype /Image /Type /XObject /Width 1800, with no /SMask, /Mask, /Decode or /DecodeParms. Its raw stream has sha256 1c4a318389c04c7f… in main's PDF, 5de92ca's and the head's. scripts/png_pixels.py decodes app/public/paper/web/figures/positive_zero_scatter.png with zlib (chunks IHDR, bKGD, 13 IDAT, IEND; CRCs pass; filters 1 to 4) to 6,588,000 RGB bytes with sha256 71fa47f2…. qpdf --show-object=322 --filtered-stream-data gives the same bytes, with 0 differing pixels, and so does pdfimages -png's extraction. A copy with one flipped bit gives 1 differing pixel.
    • Scope and pins. git diff --stat 5de92ca5 0e02069 lists app/public/paper/policybench.pdf (265,550 to 265,570 bytes), app/src/paperSnapshot.json and paper/snapshot/20260501/manifest.json, with 2 insertions and 2 deletions. scripts/pins.py diffs the JSON leaf by leaf. The manifest changes only rendered_paper_artifacts.pdf.sha256, to 23a6a3009d677dba02e332b054be15f19416c42512d85930588d94eff5d61113, which equals shasum -a 256 of the PDF. paperSnapshot.json changes only pdfVersion, b8fe0592f43a to 23a6a3009d67. Both files equal json.dumps(indent=2, sort_keys=True) + "\n". All 17 web pins match the committed files, and webVersion (af0ecbe96fac) matches index.html's pin. git grep b8fe0592f43a 0e02069 finds no file outside the PDF.
  2. Navigation against 5de92ca. The head adds section*.1 (p1, /XYZ top 547.554, 3 pt above the "Table of contents" heading; no bookmark or link uses it), section*.2 (p2), section*.5 (p50) and section*.6 (p51). Three names shift with the counters and keep their pages and views: figure.caption.1 to .3 (p45), section*.2 to .4 (p44) and section*.3 to .7 (p52). Four links change. The page-1 link to Abstract moves from Doc-Start (p1) to section*.2 (p2), and the page-2 links to Appendix A and Competing interests move from section.8 (p49) and table.14 (p50) to section*.5 (p50) and section*.6 (p51). The page-44 link to the figure changes only its destination name.
  3. The PDF against main and 5de92ca, object by object. scripts/objects.py compares full qpdf --json=2 --json-stream-data=inline --decode-level=generalized dumps. Against main, all 590 objects keep their numbers and 4 differ: 199 and 246 (the content streams of pages 25 and 35), 589 (the Info dictionary, where only /CreationDate and /ModDate change, to D:20261103071155Z) and 590 (the cross-reference stream). Against 5de92ca, the objects renumber after the new destinations, but all 54 page content streams and all 19 other streams (the image, 6 font programs, 12 others) match. pdftoppm -r 72 -gray renders all 54 pages the same as 5de92ca's, and differs from main's only on pages 25 and 35. qpdf --check finds no syntax or stream encoding errors. pdfinfo reports 54 letter pages, PDF 1.7 and LuaTeX-1.24.0.
  4. The merge against current main. git ls-remote gives main 7bccd967, refs/pull/209/head 0e020695 and refs/pull/209/merge 470029d3, whose parents are 7bccd967 and 0e020695. The merge base is 8ac64ac4. git merge-tree --write-tree 7bccd96 0e02069 gives tree f4d4a6bc…, equal to 470029d's. git diff --stat 7bccd96 470029d lists only the PR's 8 files. main's two commits since the merge base, Leave recorded ceiling-exhaustion rows alone when applying an adjudication #224 (26f7c5f0) and Correct the CI timing comments and test notes after #253 #255 (7bccd967), touch only .github/workflows/ci.yml, CLAUDE.md, policybench/adjudications.py and tests/test_adjudications.py.
  5. CI and PR state. gh pr checks 209 exits 0 with Vercel, Vercel Preview Comments, app, lint, paper and test passing. Runs 41595433717 (CI) and 41595433753 (Paper) ran on 0e02069. The test job's log shows "HEAD is now at 470029d Merge 0e02069… into 7bccd96…", image ubuntu-24.04 20261025.353.1 and "7081 passed, 91 skipped, 3 warnings in 475.41s". gh pr view reports MERGEABLE, CLEAN, not a draft, head 0e02069, and no review decision. At 5de92ca the same checks pass, and its test job checked out 005c435 (5de92ca into 7bccd96) and logged "7081 passed, 91 skipped". At 942dba4, GitHub cancelled the test job after 30 minutes (20:13:42Z to 20:43:46Z) while app, lint, paper and Vercel passed.
  6. The PDF tests at the head. Seven tests in tests/test_disclosures.py read the PDF's text through _pdf_text or _pdf_running_text: test_rendered_pdf_makes_no_false_request_claims, _reports_the_audit_universe, _reports_serving_evidence_summary, _prints_dollar_amounts_without_escapes, _abstract_scopes_the_exclusions_to_what_the_audits_found, _takes_its_engine_times_from_the_timing_record and _calls_the_upgrade_fixes_fixed_not_restored. With pdftotext 26.09 on the path, all seven pass in my worktree at 0e02069 (-k rendered_pdf: 7 passed in 1.06 s, peak 767 MB). Without pdftotext, _pdf_text calls pytest.importorskip("pypdf"). uv.lock holds no pypdf, ci.yml installs no poppler, and the runner image notes for tags 20261023.334 and 20261026.354 list no poppler, so CI skips these seven; its log prints no skip reasons. Collection of tests/test_snapshot_artifacts.py tests/test_disclosures.py finds 137 tests, the number the hand-off reports as passed.
  7. 942dba4's code change. git diff --stat 4988b0c 942dba4 lists policybench/paper_results.py and tests/test_paper_results.py (52 insertions, 11 deletions). It changes restored_split_sentence's refusal to "fixed outputs". It adds fixed_or_restored(ruled), which refuses a negative count and returns "fixed" if and only if ruled exceeds 0, and engine_upgrade_restored_sentence now takes its word from it. A new test, test_one_ruled_output_makes_the_fix_sentence_say_fixed, checks that the 2.41.0 view reads (9, 0) and "The restored outputs take", then gives it a MOCK ruled count of one and requires "The fixed outputs take". The property test now draws the ruling date from 2026-10-06 and 2027-01-02, checks fixed_or_restored at each count, and checks that fixed_or_restored(-1) raises. I found no fault in it, and N5's runs show each of round 2's mutants failing a test.
  8. The previous review's S2, S3 and N1 hold on main and at the head. paper.yml renders --to html only. A git grep of tests/ for pdfimages, Outlines, outline, /Dests, positive_zero_scatter and policybench.pdf finds no test that reads the PDF's outline, destinations or image, and CI skips the seven text tests (item 6). pdftotext returns three U+FFFD characters in main's PDF and in the head's. At 200 dpi, page 6 shows a blank where ≤ belongs in "|pred − ref| ≤ 0.01 × |ref|" and "|pred| ≤ 1". The head's table of contents prints 24 of its 26 page numbers one lower than the page its bookmark reaches; only "3 Benchmark design" and "3.2 Near-miss companion: within 1%" print the right page. Pages 1 and 2 render the same in main's PDF.
  9. The draft's other changed sentences hold.
    • Opening. faf2f5c's parents are 4399335 and 875fe1f6, and 860520e's are 942dba4 and 8ac64ac4. Both merges come after the 2.41.0 move (Add Meta Muse Wonder and move the references to policyengine-us 2.41.0 (release dashboard-data-20261013, 48 models) #217, bc305793, 2026-10-13) and the 2.60.0 move, which the paper dates 2026-10-23. Add Claude Haiku 5.5 and move the references to policyengine-us 2.38.6 (release dashboard-data-20261010, 47 models) #208 merged as 5a8164a0.
    • Version bullets for 2.38.6 and 2.41.0. The paper says the 2026-10-10 move to 2.38.6 "fixes the engine defects behind 18 outputs", of which 14 return to scoring and four, ruled on 2026-10-06 while still scored, stay scored. It says the 2026-10-13 move to 2.41.0 fixes the defects behind nine outputs that earlier releases excluded. scripts/move_counts.py reads the head's code and prints (fixed, ruled) counts of (18, 4), (9, 0) and (0, 0) for 2.38.6, 2.41.0 and 2.60.0, with fix sentences that open "The fixed outputs" and "The restored outputs" for the first two and an empty one for 2.60.0.
    • "We kept the accessor names." engine_upgrade_restored_count, engine_upgrade_restored_ruled_count, engine_upgrade_restored_sentence and engine_upgrade_restored_split_clause keep "restored".
    • HTML bullet. diff of main's app/public/paper/web/index.html and the head's shows 4 changed lines. Two change the cell ids of cells 3 (316944bb to 8a713d65) and 8 (ca2c2448 to 9a244546). The other two carry "The restored outputs take" becoming "The fixed outputs take", in the 2.38.6 paragraph and in its table row. A word diff after stripping tags shows only those two changes. The head's index.html equals 5de92ca's.
    • PDF text bullet. The head's text equals 5de92ca's (item 1), and the PDF keeps 54 pages.
    • Figure bullet. positive_zero_scatter.png has blob 476301ae at main, faf2f5c, 5de92ca and 0e02069. faf2f5c's embedded image differs from it in 19,918 pixels; 5de92ca's and the head's equal it.
    • KOMA-Script bullet's checks. The outline and the 128 named destinations equal main's, and the text equals 5de92ca's (items 1 and 2).
    • LaTeX log bullet. The draft's three strings appear in the log lines the hand-off quotes: "LaTeX2e <2026-06-01>", "Document Class: scrartcl 2026/10/19 v3.49.3 KOMA-Script document class (article)" and "Package: hyperref 2026-06-17 v7.01r Hypertext links for LaTeX". git grep -i -E 'koma|scrartcl' at the head finds docs/release_20261006/validation/render.txt, a record of an earlier render's settings, and base64 text in three model transcripts under reference_audit/. paper/_quarto.yml sets no document class and no TeX version, and nothing under paper/, .github/ or scripts/ names KOMA-Script, TinyTeX or tlmgr.
    • Manifest bullet. Against main, the manifest changes rendered_paper_artifacts.pdf.sha256 and rendered_paper_artifacts.web.files.index.html, and paperSnapshot.json changes pdfVersion and webVersion, the first 12 characters of those two hashes.
    • Tests bullets. Item 5 covers the cancelled job at 942dba4 and the CI results at 5de92ca and 0e02069. The hand-off reports "137 passed, 0 skipped" with pdftotext (poppler 24.02.0) on its path, and the 137 matches my collection.
    • Links. All four links resolve to the comments the text names: 6586455053 (round 2), 6587586433 (the response), 6596945820 (the previous check) and 6598733406 (the hand-off). Only the words "approved 942dba4" err (S1).
  10. The draft answers the previous review's S1 and N2. S1 asked for four things, and the draft does each. It replaces the figure bullet and adds the step 5de92ca took. It names the second merge. It gives the toolchain with KOMA-Script 3.49.3 in the KOMA-Script and LaTeX log bullets. It replaces "CI at 942dba4: pending" with the cancelled job and the results at 5de92ca and 0e02069. N2 asked for the LaTeX2e, scrartcl and hyperref log lines, and the draft quotes the version part of each.
  11. What each new or changed sentence gives the reader. Each one below earns its place once S1 to S3 land.
    • Opening, second sentence: which main the PR sits on, and that the second merge brought in Run the CI test job in parallel with pytest-xdist #253's parallel test job, which ran in about 8 minutes at 5de92ca and 0e02069, where 942dba4's job hit the 30-minute limit.
    • Item 2's heading and its three version bullets: the rule for "fixed" and "restored", and each move's counts behind it. The 2.60.0 bullet needs S3's wording.
    • "We kept the accessor names.": why engine_upgrade_restored_* still says "restored" after the docstrings changed.
    • "We re-rendered … and re-pinned …": the commands and the toolchain that reproduce the render.
    • HTML and PDF text bullets: the full diff against main, so a reader knows that nothing but the two words changes.
    • Figure bullet: the outcome (the PNG keeps main's bytes and the PDF embeds it), the cause that made 5de92ca act (no Arial on the first host), what 5de92ca did, and the check that shows it worked.
    • KOMA-Script bullet: what 0e02069 did and how to repeat it, the cause, the defect a reader could have seen in the PDF, and the evidence that navigation now equals main's while the text stays the same. The range needs S1's fix.
    • LaTeX log bullet: the versions the previous review asked to record, a warning to whoever rebuilds the PDF next, and where the full recipe lives.
    • Manifest bullet: the scope of the pin changes.
    • "GitHub Actions cancelled …": what became of the "pending" line, and why Run the CI test job in parallel with pytest-xdist #253 mattered.
    • CI bullet: the result at the head; the 137-pass run, the only run of both test files with pdftotext present on this PDF; and why that run matters, since CI skips the PDF tests. "The wording tests" needs N2's name.
    • Reviews bullets: the review record. They need S1's and S2's fixes.
    • axiom: line: the hub's routing tag.

What I couldn't check

  • The LaTeX log, tlmgr.log and the TinyTeX 2026.10 bundle. The repository holds none of them, and the brief rules out rendering. So the draft's cause (KOMA-Script 3.49.2 sets no anchor for these unnumbered headings) and its warning that a rebuild on the stock bundle brings the three wrong targets back rest on the previous review's reproduction and the hand-off's. The dates fit that cause without proving it: main's PDF lacks the anchors through Add Gemini 4 Argon and Muse Spark 1.3 (release, 50 models) [WIP] #221 (2026-10-14) and has them from Disclose why 330 frozen households record near-zero latency and two models record none #228 (2026-10-25), and the hand-off's log line dates 3.49.3 to 2026/10/19.
  • The hand-off's reproduction from index.tex, its /ID splices, its qpdf 11.9.0 behaviour, its xurl.sty extraction and its other decoders.
  • Whether the runner image carries pdftotext through a package its notes don't list. GitHub has no notes for tag 20261025.353; the tags on either side list no poppler, and CI's log prints no skip reasons.
  • The cloud host's 137-pass run. My watchdog stopped the full tests/test_disclosures.py run at 1,166 MB, over my 950 MB cap, so I ran the seven PDF tests on their own. CI ran the rest.
  • Whether the first render host lacked Arial and drew Liberation Sans. The pixels differ, but pixels don't name a font.
  • Whether the named commands (paper/render_paper.py, scripts/freeze_snapshot.py --rendered-only, the tlmgr updates) produced each render. The outputs agree with the draft's account.
  • The PDF that policybench.org serves today.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

The PolicyBench owner session (Claude Opus 5.5) changed the description in answer to this review and pushed no commit.

  1. S1. The first Reviews bullet now says that an independent reviewer approved 4988b0c and that we answered its findings in 942dba4. We put your two sentences into the KOMA-Script bullet, so it dates the wrong targets from faf2f5c and says that main's PDF lacked the anchors until Disclose why 330 frozen households record near-zero latency and two models record none #228.
  2. S2. The second Reviews bullet now names the three faults that main shares and says that we left them out of this PR. We dropped the finding labels, the clause about the description and the promise of a follow-up.
  3. S3. The 2.60.0 bullet now says that when we moved the references to 2.60.0, we returned no excluded output to scoring, so engine_upgrade_restored_sentence writes no sentence for that move. The 2.38.6 and 2.41.0 bullets now say that each version fixes the engine defects behind its outputs, in the paper's words.
  4. N1. The KOMA-Script bullet names both tlmgr update --self and tlmgr update koma-script.
  5. N2. The opening now names each merge in a sentence of its own, and the CI bullet names the seven tests in tests/test_disclosures.py that read the PDF's text. "Round-2" now names only Add Claude Haiku 5.5 and move the references to policyengine-us 2.38.6 (release dashboard-data-20261010, 47 models) #208's final review.
  6. N3. Each version bullet now opens with the move, as in "We moved the references to version 2.38.6 on 2026-10-10."
  7. N4. We restored the heading "The rendered paper changes two words".
  8. N5. The Tests list now names your three mutants and says that each fails at least one test at 0e02069.

At 0e02069, gh pr checks 209 exits 0 and GitHub reports the PR as mergeable, so we're squash-merging it now.

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis merged commit 71fa85e into main Nov 3, 2026
6 checks passed
@MaxGhenis
MaxGhenis deleted the engine-upgrade-fixed-wording branch November 3, 2026 09:30

This branch was successfully deployed

1 active deployment
Preview — 0e020695 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants