Skip to content

Take jupyter-book out of the paper render environment and uv.lock - #227

Merged
MaxGhenis merged 12 commits into
mainfrom
docs-site-extra
Oct 25, 2026
Merged

MaxGhenis merged 12 commits into
mainfrom
docs-site-extra

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

The docs extra did two jobs: it was the environment for the Quarto paper render, and it held the Jupyter Book 2 builder for the operational-docs site in docs/. The render needs only a Jupyter kernel and notebook runner. jupyter-book brought jupyter-server, nbconvert, bleach, mistune, soupsieve and 29 more packages into uv.lock, and the render uses none of them. This PR takes jupyter-book out of pyproject.toml. docs/requirements.txt already declared it and is now its only declaration. The relock removes 34 packages and changes no version.

Why not a separate docs-site extra

Dependabot scans every package in uv.lock, whichever extra pulls it in. bleach, mistune, soupsieve and jupyter-server are reachable only through the docs extra (uv tree --invert: jupyter-book → jupyter-server → nbconvert). Yet all four had open alerts against uv.lock in the 2026-10-14 inventory (alerts_before.tsv), listed as transitive runtime dependencies, until #225's bumps closed them. A docs-site extra would keep them in the lock, and their future alerts with them. Taking jupyter-book out of the lock is what removes them. GitHub's dependency graph records docs/requirements.txt only as jupyter-book >= 2.0, with no resolved version and no transitive packages.

Dependabot alerts

#225 (merged as 555a0f2c) bumped bleach, jupyter-server, mistune and soupsieve to patched versions, and their 18 alerts are now fixed: bleach #42–#44, jupyter-server #45, mistune #49–#58, soupsieve #47, #48, #69 and #70. Bleach #42 (GHSA-g75f-g53v-794x) closed only because the advisory's range is = 6.3.0. 6.4.0 keeps the same email regex, and no release fixes it. This PR removes all four packages from uv.lock, so that code is no longer installed at all.

Five alerts are open on main after #225:

Alert Package Severity After this PR
#92, GHSA-pjvx-prjx-h659 nbconvert 7.17.1 high Closes: nbconvert leaves the lock.
#1, GHSA-w8v5-vhqr-4h9v diskcache 5.6.3 medium Stays: no patched release (#226 guards the cache directory).
#93, #94, #95 filelock 3.24.3 high, low, low Stay: advisories published 2026-10-23, patched in 4.2.1; filelock comes in through huggingface-hub, not jupyter-book.

tornado's alerts (#66–#68, #77–#79) were fixed by #225, and tornado stays in the lock because ipykernel and jupyter-client need it.

What leaves uv.lock

Field-by-field comparison with main's lock (555a0f2c, after #225): 171 → 137 packages; 34 removed, 0 added, 0 version changes. Relocking on #225's lock removes the same 34 packages as relocking on the lock before it.

argon2-cffi 25.1.0, argon2-cffi-bindings 25.1.0, arrow 1.4.0, beautifulsoup4 4.14.3, bleach 6.4.0, defusedxml 0.7.1, fqdn 1.5.1, isoduration 20.11.0, jsonpointer 3.0.0, jupyter-book 2.1.2, jupyter-events 0.12.0, jupyter-server 2.21.1, jupyter-server-terminals 0.5.4, jupyterlab-pygments 0.3.0, lark 1.3.1, mistune 3.3.4, nbconvert 7.17.1, nodeenv 1.10.0, overrides 7.7.0, pandocfilters 1.5.1, prometheus-client 0.24.1, python-json-logger 4.0.0, pywinpty 3.0.3, rfc3339-validator 0.1.4, rfc3986-validator 0.1.1, rfc3987-syntax 1.1.0, send2trash 2.1.0, soupsieve 2.10, terminado 0.18.1, tinycss2 1.4.0, uri-template 1.3.0, webcolors 25.10.0, webencodings 0.5.1, websocket-client 1.9.0.

Two retained entries change in place:

  • policybench: its docs extra loses jupyter-book.
  • jsonschema 4.26.0: it loses its format-nongpl extra entry, which only jupyter-events requested.

The lock's top-level metadata is unchanged.

Why the render doesn't need jupyter-book

Quarto 1.9.36's Jupyter engine (share/jupyter/notebook.py, jupyter.py) imports jupyter_client, nbclient, nbformat and yaml. Its two other imports, papermill (for --execute-params) and jupyter-cache (for cache: true), sit in try/except ImportError, and neither was ever locked. The kernel is ipykernel's python3. All of these remain in the docs extra or come in with it. In the synced venv, jupyter_server, nbconvert, bleach, mistune, soupsieve, bs4, jupyter_book and terminado are all absent, and quarto check jupyter passes.

Changes

  • pyproject.toml: docs = ["ipykernel>=6.0", "nbformat>=5.0", "nbclient>=0.10"], with a comment saying what the extra is for and where jupyter-book went.

  • uv.lock: relocked with uv lock (uv 0.13.4) on main's lock after Patch 50 of 53 Dependabot alerts in uv.lock; document diskcache and bleach, which have no fix #225. Only the removals and the two in-place entries above change.

  • docs/requirements.txt: unchanged requirement (jupyter-book>=2.0), plus a comment with the build command.

  • README.md: a new section, "Paper and documentation site", documents both environments and the docs build command.

  • paper/README.md: says what the docs extra holds and that it excludes jupyter-book.

  • tests/test_render_environment.py: guard tests.

    • The docs extra holds ipykernel, nbclient and nbformat.

    • No extra declares jupyter-book, jupyter-server or nbconvert.

    • None of those three is in uv.lock.

    • docs/requirements.txt declares jupyter-book. The test's reader accepts only line forms that pip and uv (which builds the docs site) read the same way. It fails on every other line rather than guessing (reviews r3 to r7). The accepted forms:

      • blank lines and comments (a # at a line's start or after whitespace), except a PEP 263 encoding line among a file's first two;
      • includes spelled -r FILE, --requirement FILE or --requirement=FILE, where FILE is a relative path, resolved against the including file's directory, that names no symlink (checked both with .. collapsed, as pip opens it, and as the OS resolves it), has no .. after a symlinked directory, doesn't end in / or /., and closes no cycle;
      • requirements made of a name, optional extras and version specifiers, with no empty parentheses or trailing comma.

      Every file must be UTF-8 with no byte-order mark. Each refusal has its own message, and r5/differential.log records what pip and uv do with each refused line:

      • Continuation lines. pip joins jupyter-book \ onto a following >=2.0 and installs it, while uv can't parse the pair. And a comment that doesn't start its line but ends in \ makes pip swallow the next line: foo # see below \ followed by jupyter-book installs only foo with pip, and both with uv.
      • Whitespace other than spaces and tabs. pip splits lines with str.splitlines. A form feed inside a requirement makes pip fail while uv installs it, and a no-break space before a comment is a parse error for uv while pip installs. The file is read as bytes, so a lone carriage return is caught too.
      • Encoding declarations, byte-order marks and non-UTF-8 bytes. pip decodes a file by its byte-order mark, or else by a coding[:=] comment among its first two lines (_decode_req_file in req_file.py); uv ignores such a comment. So # coding: utf-7 followed by foo # x+AFw and jupyter-book installs only foo with pip (in UTF-7, +AFw is a backslash), and both with uv. # Docs site; no coding=needed makes pip fail on the unknown codec needed. A UTF-8 byte-order mark is read the same way by both, but it is refused anyway, since only the plain UTF-8 path is modelled.
      • Include cycles. pip reports one as an error. uv reports no error and installs what the files hold: a file whose only line includes itself installs nothing, foo before a self-include installs foo, and a two-file cycle installs both files' requirements.
      • An include whose .. comes after a symlinked directory, which pip collapses lexically while uv follows the link, so the two read different files.
      • An include path ending in / or /., which pip (like pathlib) reads as the file and uv rejects.
      • Absolute include paths and included symlinks, each with its own message. pip and uv read every such case run alike: an absolute path; a symlinked file; that file reached both directly and through the link; and a symlinked file that pip opens once it collapses sub/.., while the OS, following sub, reaches a regular file. They are refused anyway: no file here needs them, and refusing them keeps the test independent of the machine's paths and of how uv recognizes a file it has already read.
      • Every other option:
        • -r=FILE (pip opens =FILE, uv opens FILE);
        • -rFILE and --requirem FILE (pip follows them, uv rejects them);
        • two includes on one line (pip reads the first, uv rejects the line);
        • quoted, $-expanded or URL paths, and includes with trailing options;
        • -c and -e in any spelling (--pre -e ./local makes pip fail and uv install nothing);
        • index options.
      • Per-requirement options such as --hash.
      • URLs and paths.
      • Markers. The reader doesn't evaluate them, and with a false marker pip and uv both install nothing and exit 0.
      • ===, string equality, which the reader doesn't model. Both tools read ===2.0 alike, but ===2.0.zip is a file path to pip and a parse error to uv.
      • A trailing comma in the specifiers. uv 0.11.7 rejects it; pip, uv 0.12.13 and uv 0.13.4 accept it.
      • Empty parentheses (jupyter-book ()), which pip accepts and uv rejects.
      • Archive names, judged as pip judges them: case-insensitively, with extras (and any whitespace before them) stripped, against pip's full extension list (utils/filetypes.py). So jupyter-book==2.0+local.zip is refused: pip reads it as a file path, uv as a version.
    • Five more tests pin that reader:

      • one reads a tree with every accepted form, including CRLF endings, tabs and includes at two depths;
      • one checks 57 refused lines, each against the reason it must fail for;
      • one builds a symlinked directory and checks that a .. through it is refused;
      • one checks that an include of a symlinked file is refused, whether named directly or reached through sub/..;
      • one includes a real file by its absolute path and checks that it is refused.

      A differential against real pip and three uv versions, and 30 mutants, are under Verification.

    Run against main's pyproject and lock, the two lock and extra checks fail as intended.

Verification

Local runs used CPython 3.14.7 (GIL build) on macOS arm64. Evidence is in ~/reviews/policybench-docs-site-extra-2026-10-14/ (r3/ for the render and lock, r5/ for the requirements reader).

  • Render environment: uv sync --locked --extra dev --extra docs (uv 0.13.4) succeeds, and uv pip check finds the 134 installed packages compatible. jupyter_server, nbconvert, bleach, mistune, soupsieve, bs4, jupyter_book and terminado don't import.

  • Paper, with and without the 34 packages. The same source was rendered twice: main at 555a0f2c, and this branch at b6f6f778, which changes only paper/README.md under paper/, policybench/ and app/ (render_paper.py renders index.qmd alone). It was rendered with paper/render_paper.py (HTML and PDF, Quarto 1.9.36, LuaHBTeX), main in its full locked environment in a separate worktree, this branch in its own. Both exit 0.

    • 17 of 19 files are byte-identical.
    • web/index.html differs on one line, a Quarto per-render cell id (id="965529f8" → id="e2561682"). Its visible text is identical (0 diff lines).
    • policybench.pdf has identical pdftotext -layout text and the same size (261,164 bytes). Its 66 differing bytes all fall inside /CreationDate, /ModDate and the trailer /ID; with those masked, the files are byte-identical.
  • Paper at the merged head (d9f862d1, which brings in Disclose why 330 frozen households record near-zero latency and two models record none #228's paper changes): rendered in this branch's environment and compared with the render Disclose why 330 frozen households record near-zero latency and two models record none #228 committed. 17 of 19 files are byte-identical. web/index.html differs on two lines, both per-render cell ids, with identical visible text. policybench.pdf has identical text and size (263,255 bytes), and its 70 differing bytes all fall inside /CreationDate, /ModDate and /ID. I restored the committed render, so the rendered_paper_artifacts pins are untouched.

  • Docs site (round 1, at dd09f15e; docs/requirements.txt hasn't changed since): cd docs && uv run --no-project --with-requirements requirements.txt jupyter book build --html exits 0 with jupyter-book 2.1.7 and builds the 4 pages. For comparison I built the same site in main's locked docs environment (uv export --frozen --extra docs from main's lock: jupyter-book 2.1.2).

    • Both builds report the same 4 warnings (toc extension inference) and the same 3 errors. The errors are pre-existing math-parse errors on # inside $…$ in benchmark_card.md.
    • All four pages have identical visible text.
    • Each page's MyST AST is identical apart from per-build node key ids.
  • Lock: uv lock --check (uv 0.13.4) passes at d9f862d1. uv.lock and pyproject.toml are unchanged by the merge of Disclose why 330 frozen households record near-zero latency and two models record none #228.

  • Lint: at 37c7bfbc, ruff check . passes, and ruff format --check . reports 181 files already formatted. uv lock --check (uv 0.13.4) passed at 31571187; nothing since touches uv.lock or pyproject.toml.

  • Tests: OPENBLAS_NUM_THREADS=1 pytest -m "not slow" -q, run serially at b6f6f778, gives 5459 passed, 58 skipped, 36 deselected (r3/pytest-not-slow.log). At 37c7bfbc, tests/test_render_environment.py passes (65 tests); CI runs the not-slow suite there.

  • The requirements reader against real pip and uv (r5/differential.py, differential.log, at 37c7bfbc; the log starts with the command line and tool versions). Hypothesis (derandomized) generated 300 requirements trees in the accepted forms.

    • Each tree spreads up to 12 stub projects (two with digits in their names) over six files in four directories: the root, docs, docs/sub and other.
    • The files are joined by includes in eight spellings of the three accepted forms; the log counts each. Paths include ./ and sub/.. forms and go up and across directories. The entry file includes another file in 138 trees, and includes nest up to four deep (in 11 trees).
    • Lines mix random-case names with separator runs, extras (including [] and [ docs , x ]), specifiers from a grammar (parentheses, tabs, wildcards, local and epoch versions), comments, CRLF endings and missing final newlines.

    Every tool ran from an empty directory outside the tree, given the entry file's absolute path. On all 300 trees, the reader's names equal four things:

    • the projects in the files the entry reaches;
    • what pip 26.2.2 would install (pip install --dry-run --report, from stub wheels without dependencies);
    • what uv 0.13.4, 0.12.13 and 0.11.7 resolve (uv pip compile);
    • and what each installs with the docs build's own command, uv run --no-project --with-requirements.

    The log prints how often each feature occurred, what the generator filtered out (lines packaging rejects or no stub version satisfies), and one example tree per nesting depth.

    It then checks 20 fixed accepted rows on every tool, where each must return the reader's names:

    • round 4's six forms;
    • pre, post and dev releases (each met only by its own stub), >=v2.0, ==2.0-1 and ==2.0_post1;
    • names with digits;
    • non-ASCII and NUL bytes in a comment;
    • ./ and tab includes;
    • a file inside a symlinked directory, whose include must resolve inside the linked directory (docs/sub), not against docs/, where a decoy sits beside the link;
    • the real docs/requirements.txt.

    Finally it records what pip and each uv do with 64 refused inputs: the 57 refused lines; a .. after a symlinked directory; an absolute include of a real file; an include of a symlinked file; that file reached both directly and through the link, in both orders; a symlinked file that pip opens once it collapses sub/.., while the OS, following sub, reaches a regular file; and a two-file cycle. r5/pip-errors.txt has pip's last error line for three of them.

  • Mutants (r5/mutants.py, mutants.log, run on the committed head 37c7bfbc with a clean tree through r5/run_mutants.sh, which writes the log's first line; every test runs, and each mutant's failing tests are listed under it): 30 of 30 are killed. Each removes or loosens one rule:

    • one per refusal;
    • round 2's include regex and archive list, and round 3's extras pattern;
    • the case-insensitive and extras-stripped archive checks;
    • reading with newline translation, or replacing undecodable bytes;
    • a naive comment rule;
    • includes resolved from the working directory;
    • a symlink check made only on the path as the OS resolves it;
    • CRLF endings refused;
    • an encoding check that looks only at line 1, or only for Emacs-style lines.
  • CI:

    • At d9f862d1: CI run 40176708922 (lint, app, test) and Paper run 40176708957 pass.
    • At 60f76fa8: CI run 40178391332 and Paper run 40178391363 pass.
    • At 31571187: CI run 40181871961 and Paper run 40181871759 pass.
    • At 919b9887: CI run 40184706164 and Paper run 40184706155 pass.
    • At d3a430ec: CI run 40187540436 and Paper run 40187540420 pass.
    • At a381226a: CI run 40189939694 and Paper run 40189939603 pass.
    • At 37c7bfbc: CI run 40192194064 and Paper run 40192194090 pass.

    The Paper workflow re-renders HTML from a clean uv sync --locked --extra dev --extra docs on Python 3.12.

Reviews

  • Round 1 (GPT-6.1 Sol, at dd09f15e): APPROVE, with one nit: the guard test's requirements reader would fail on an indented comment. Fixed in 269d79f3, which handles comments and options as pip does and adds a test of the parser.

  • Round 2 (GPT-6.1 Sol, at d9f862d1): APPROVE, with three non-blocking corrections, made in 60f76fa8 and this body:

    • The reader didn't handle continuations or -r includes, and named bare URLs wrongly. 60f76fa8 modelled more of pip's req_file.py. Round 3 showed that this still missed forms pip names differently, so the reader is now narrowed (round 3, below).
    • "Have open alerts" is now past tense, citing the 2026-10-14 inventory.
    • CI runs the not-slow suite, not the full one.
  • Round 3 (Opus 5.5 in-session, at 60f76fa8, the reader only). The Sol run was cancelled before launch because every Subfleet lane was on hold. Verdict: REQUEST_CHANGES.

    • It found four families of unusual lines on which the test would pass while pip installs no jupyter-book from the index: unevaluated markers, PEP 508 strings pip reads as archive paths, -r spellings pip reads differently, and a ; inside a named URL.
    • It also found the archive list incomplete and case-sensitive.
    • And it found the body overclaiming: naming is constructors.py's job, not req_file.py's.

    The current one-line file was never affected. c5744ceb takes the review's second route: it narrows the reader to forms pip and uv agree on and refuses the rest in code. 31571187 merges main (Record whether each prediction row's responses were live calls or cache replays #231).

  • Round 4 (Opus 5.5 in-session, at 31571187, the reader only; lanes still on hold). Verdict: REQUEST_CHANGES.

    • Blocking: pip decodes a requirements file by a PEP 263 line among its first two, so an accepted comment such as # coding: utf-7 could make pip join jupyter-book into a comment while the test passed.
    • Non-blocking:
      • whitespace before extras hid an archive name;
      • an include through a symlinked directory;
      • the differential never nested includes or moved the working directory, and sampled only part of the accepted forms;
      • four claims lacked a source or log: that the tools differ on markers; uv's continuation behaviour; that every round-3 input was among the refused lines; and the pending CI line.

    919b9887 fixes all of them. It refuses encoding declarations, a UTF-8 byte-order mark and non-UTF-8 bytes, include cycles, includes whose .. follows a symlinked directory, and trailing commas, which the widened differential found uv 0.11.7 rejecting. The archive check now strips whitespace before extras. Every input the reviews said should be refused is now among the refused lines, some with different file names. The differential above was widened as asked. Its log now backs the continuation claim, which c5744ceb's message had made without one; 919b9887's message corrects that.

  • Round 5 (Opus 5.5 in-session, at 919b9887, the reader only). Verdict: APPROVE, with nothing blocking. It found no accepted line or tree on which pip or any of the three uv versions quietly installs something else, and checked the reader's packaging 26.0 against pip's vendored 26.2 on 135,456 fuzzed lines with identical results. Non-blocking:

    • N1: -r base.txt/ and -r base.txt/. are accepted, but uv probably opens them as written;
    • N2: an include inside a file reached through a symlink was never run;
    • N3: few generated trees followed an include from the entry file, several accepted spellings were never run on uv, and the body misstated the nesting depth and the include spellings;
    • N4: wording, including an over-broad cycle claim and the stale CI line.

    d3a430ec answers it. Running the unrun forms showed pip installing -r base.txt/, --requirement=base.txt/. and jupyter-book () while all three uv versions reject them, so the reader now refuses all three. Includes through a symlinked file, absolute include paths, --requirement<TAB>FILE, pre, post and dev releases, >=v2.0, ==2.0-1, names with digits, and non-ASCII and NUL bytes in comments are read alike, so they stay accepted and are fixed rows in the differential (a381226a later refuses the symlinked-file and absolute forms; see round 6). The generator now draws an include from the entry file in about half the trees and logs the nesting depth and each include spelling. The wording is fixed, and d3a430ec's message corrects 919b9887's cycle claim.

  • Round 6 (Opus 5.5 in-session, at d3a430ec, the delta since round 5). Verdict: APPROVE, with nothing blocking. It found every round-5 finding answered with evidence, and the two new refusals reached for the right reason and no broader than the log supports. Non-blocking:

    • a symlinked file reached under two names in different directories had never run on uv;
    • two mechanism claims rested on too few rows;
    • the description of accepted includes was incomplete;
    • the pre-release row could be met by a final release;
    • the reader test's absolute include depended on the temp directory's path, so a space, @ or + there would fail the test falsely;
    • the example trees were near-duplicates;
    • the CI line would go stale.

    a381226a answers it. The new rows show pip and uv reading the symlinked file alike in both include orders, an absolute include alike, and a two-file cycle as described above. The reader now refuses absolute include paths and included symlinks anyway, which removes the temp-path dependence. The claims and descriptions are reworded to what the log shows. The pre-release row is ==2.1rc1. The log prints one example tree per nesting depth. The mutants were rerun on the committed head.

  • Round 7 (Opus 5.5 in-session, at a381226a, the delta since round 6). Verdict: APPROVE, with nothing blocking. The delta only narrows what the reader accepts. Non-blocking:

    • The symlink check tested only the path as the OS resolves it. With -r sub/../x.txt, where docs/sub links to a directory and docs/x.txt is a symlink, pip opens the symlink, while the OS reaches a regular file. The names still agreed, but "names no symlink" overstated the check.
    • Absolute includes were refused only as "an option", and the mutant allowing them died of a missing file.
    • Two body sentences claimed more than the rows show.
    • The differential's docstring was stale, and a file inside a symlinked directory, the one symlink form still accepted, had never run on pip or uv.

    37c7bfbc answers it. The symlink check now tests both paths, and the reader test builds that tree. Absolute includes have their own message, and a new test includes a real file by its absolute path, which the absolute-path mutant now fails (see round 8). The differential has rows for both trees: every tool reads them alike. The wording is fixed. The mutants ran on the committed head through r5/run_mutants.sh, which writes the log's first line.

  • Round 8 (Opus 5.5 in-session, at 37c7bfbc, the delta since round 7). Verdict: APPROVE, with nothing blocking. The delta only narrows what the reader accepts. In the reviewer's in-memory fuzz of 60,000 trees with directory and file symlinks, the trees 37c7bfbc accepts are a subset of those a381226a accepts, and on none of them do its models of pip and uv disagree. Non-blocking:

    • The mutants log showed the absolute-path mutant failing only on a row whose file doesn't exist, so it didn't evidence that the new test fails it. r5/mutants.py now runs every test and lists each mutant's failures. Under "absolute include paths allowed", mutants.log lists the new test among three failures, and r5/mutant-reason-absolute.log shows why: it fails with "DID NOT RAISE", because the mutant reads the absolute include instead of refusing it.
    • The body still said CI at 37c7bfbc was running; it has passed.
    • "The link's directory" meant the linked directory in one place and the link's parent in another; the fixed-row line now names docs/sub.
    • "A symlink that pip opens through sub/.." read as if pip follows sub; it now says pip collapses sub/.. first.
    • Two code comments could be worded more clearly. Neither is wrong, so they stay as they are; from round 8 on, non-blocking wording changes go into this body only.

Invariants

This PR changes no logic. It must preserve three properties:

  • The published paper does not move. A re-render in the slimmed environment matches the committed HTML text and PDF bytes, apart from per-render ids and timestamps.
  • The lock only shrinks. No locked package changes version, and nothing is added. The field-by-field diff shows this.
  • jupyter-book's server stack stays out of the render environment and the lock. tests/test_render_environment.py enforces this.

axiom: n/a: dependency/environment change, no policy logic

🤖 Generated with Claude Code

The docs extra was both the Quarto paper render environment and the
Jupyter Book 2 docs-site builder. Quarto's Jupyter engine needs only the
kernel and notebook runner (ipykernel, nbclient, nbformat); jupyter-book
pulled in jupyter-server, nbconvert, bleach, mistune, soupsieve and 29
other packages that the render never imports.

Drop jupyter-book from the docs extra and let docs/requirements.txt,
which already declared it, be its only declaration. A separate extra
would not help: Dependabot scans every package in uv.lock whichever
extra pulls it in. The relock removes 34 packages and changes no
version.

The paper re-renders identically (HTML text and PDF bytes, apart from
Quarto's per-render cell id and the PDF dates and /ID), and the docs
site builds through docs/requirements.txt with the same page text as
main's locked jupyter-book. README and paper/README document both
environments; tests/test_render_environment.py keeps the stack out.

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

vercel Bot commented Oct 14, 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 Oct 25, 2026 12:15pm UTC

Request Review

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Taking this PR over from session local_a36e319c, which has been idle since 2026-10-14; I've messaged it. Review r1 (Sol) approved at dd09f15 with one nit. This rebases on #225 once it merges (main's lock, then uv lock), then gets a fresh review. I'll merge it on green CI and an agreeing independent review, then remove its worktree. — session local_a8447b8d ("Finish five stale PolicyBench draft PRs")

MaxGhenis and others added 3 commits October 24, 2026 22:10
Review r1's nit: an indented comment would have reached _name() and failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uv.lock conflicted with #225's bumps. Took main's lock and ran
`uv lock` (uv 0.13.4): the same 34 packages leave, none is added and
no retained version changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (review r2)

The guard test's reader now joins backslash continuations and strips
comments as pip's req_file.py does, follows -r includes relative to the
including file, skips -c constraints and other options, and drops
per-requirement options such as --hash. Requirements it can't name
without fetching them (bare URLs, paths, -e) fail the test instead of
yielding a wrong name. Names come from packaging's PEP 508 parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 25, 2026 05:43
… (review r3)

The reader of docs/requirements.txt tried to follow pip, and review r3 found
four families of lines on which the test would pass while pip installs no
jupyter-book: unevaluated markers, PEP 508 strings pip reads as archive paths
(==2.0+local.zip), -r spellings pip reads differently (-r=FILE, -rFILE), and
a ";" inside a named URL. uv, which builds the docs site, differs from pip on
some of these too: it rejects a backslash continuation inside a requirement
and reads -r=FILE as FILE.

The reader now accepts only blank lines, comments, -r FILE /
--requirement FILE / --requirement=FILE includes, and requirements made of a
name, extras and version specifiers. Every other line fails with a message
naming why: continuations, whitespace other than spaces and tabs (the file is
read as bytes, so a lone CR isn't a line break), other options, per-requirement
options, URLs and paths, markers, ===, and archive names as pip judges them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew r4)

Review r4 found that pip decodes a requirements file by a PEP 263 line among
its first two (_decode_req_file in req_file.py), so an accepted comment such
as "# coding: utf-7" could make pip join a later line into a comment and
install no jupyter-book; uv ignores the line. The reader now refuses an
encoding declaration in a file's first two lines, a UTF-8 byte-order mark,
and bytes that aren't UTF-8.

It also refuses:
- an include cycle, which pip reports as an error and uv reads as nothing;
- an include whose .. comes after a symlinked directory, which pip collapses
  lexically while uv follows the link, so they read different files;
- a trailing comma in the version specifiers, which uv 0.11.7 rejects and
  pip and uv 0.13.4 accept.
And it strips extras together with any whitespace before them before the
archive-name check, as pip's strip_extras does, so "jupyter-book.zip [docs]"
is refused.

A correction to c5744ce's message: it said uv rejects a backslash
continuation inside a requirement, with no logged run behind it. Review r5's
differential now records one: uv 0.13.4 can't parse "jupyter-book \"
followed by ">=2.0", which pip joins and installs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r5 approved 919b988 and listed forms the differential had never
run. Run now: pip installs "jupyter-book ()" and "-r base.txt/" (or
"base.txt/."), which pathlib also reads as base.txt, while uv 0.11.7,
0.12.13 and 0.13.4 all reject them. The reader now refuses both, so it again
accepts only forms the two read alike. An include inside a file reached
through a symlink, an absolute include path, a tab after --requirement, and
pre, post and dev releases are read alike and stay accepted; the reader
test now covers the absolute path and the tab.

A correction to 919b988's message: it said uv reads an include cycle as
nothing. That holds for a file whose only line includes itself; with "foo"
before the self-include, uv installs foo while pip reports the cycle. The
reader refuses both, and both are now among the refused lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r6 approved d3a430e and noted that the reader test's absolute
include depended on the temp directory's path matching the include
pattern, so a temp root or user name with a space, @ or + would fail the
test falsely. It also asked whether uv reads a symlinked file reached
under two names as pip does. Run now, pip and uv 0.11.7, 0.12.13 and 0.13.4
read an absolute include, an include of a symlinked file, and that file
reached both directly and through the link alike. The reader refuses both
forms anyway: no file here needs them, and refusing them keeps the test
independent of the machine's paths and of how uv recognizes a file it has
already read.

Also from the review: the comment on paths ending in / now says uv rejects
them, which is what the log shows, rather than why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view r7)

Review r7 approved a381226 with two non-blocking findings in the reader.

The symlink check looked only at the path as the OS resolves it. With
"-r sub/../x.txt", where docs/sub links to a directory and docs/x.txt is a
symlink, pip opens docs/x.txt (it collapses .. first) while the OS follows
sub and finds a regular file, so the include was accepted. pip and uv
0.11.7, 0.12.13 and 0.13.4 read that tree alike, so no name was wrong, but
the comment said "names no symlink". The check now looks at both paths, and
the reader test builds that tree.

An absolute include was refused only as "an option", and its one test line
named a file that doesn't exist, so the mutant allowing absolute paths died
of a missing file rather than an accepted include. Absolute includes now
have their own message, and a new test includes a real file by its
absolute path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 25, 2026 12:40
@MaxGhenis
MaxGhenis merged commit 370caed into main Oct 25, 2026
6 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merge audit. Squash-merged at head 37c7bfbc (pinned with --match-head-commit), merge commit 370caed9.

Gates at that head:

  • gh pr checks 227 exited 0, rechecked just before the merge: CI run 40192194064 (lint, app, test) and Paper run 40192194090 passed.
  • mergeable was MERGEABLE and mergeStateStatus CLEAN. The PR was marked ready first, and it had no CHANGES_REQUESTED review.
  • No published number moves. The paper renders the same with and without the 34 removed packages; see Verification in the body.

Independent reviews, saved verbatim in ~/reviews/policybench-docs-site-extra-2026-10-14/:

  • r1 (GPT-6.1 Sol, at dd09f15e): APPROVE.
  • r2 (GPT-6.1 Sol, at d9f862d1): APPROVE, with three non-blocking corrections.
  • r3 and r4 (Opus 5.5 in-session, at 60f76fa8 and 31571187): REQUEST_CHANGES on the requirements reader. The Subfleet lanes were on operator hold.
  • r5 to r8 (Opus 5.5 in-session, at 919b9887, d3a430ec, a381226a and 37c7bfbc): APPROVE, with nothing blocking.

Round 8 approved the merged head. Its non-blocking notes are answered in the body.

🤖 Generated with Claude Code

This branch was successfully deployed

1 active deployment
Preview — 37c7bfbc Deployed Oct 25, 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.

1 participant