Skip to content

Tie each paper figure to the section that states it - #14

Draft
MaxGhenis wants to merge 4 commits into
max/abstract-guard-ties-20261106from
max/section-guard-ties-20261106
Draft

MaxGhenis wants to merge 4 commits into
max/abstract-guard-ties-20261106from
max/section-guard-ties-20261106

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

This PR builds on #13, which builds on #12, so merge both first. Once they merge, we'll rebase this branch onto master.

What changes

scripts/check_paper.py searched the whole paper in 39 of its figure ties: 27 has_number and 3 has_ordered calls that fell back to QMD, and 9 literal in QMD tests. When a figure changed in the section that states it, its tie still passed if the same numeral appeared anywhere else in the paper. For example, the 0.7 that opens Appendix A's standard-error range matched the 0.7 exponent in §2.2's formula, and §6.2's 1.17, 2.58 and 0.75 matched the abstract's copies.

Each tie now reads the section that states its figure. Six new slices cover §3.2's sample sensitivities, §5.3's five-year benchmark, §6.1's committed estimate, §6.2's evaluation, §6.3's comparator check and Appendix A, and the link ties read #12's Appendix B slice. section_source() returns a slice's raw text, and section() collapses its whitespace. If either heading goes missing, repeats or falls out of order, both return an empty slice and record a failure, so every tie in that section fails.

Most ties also hold the figure beside the words that say what it measures, as #13 does for the abstract, so a swap between neighbors fails. A phrase also picks out one copy where a section repeats a numeral. For example, in §6.2 we report a mean absolute error of 1.17 percent, then a signed mean of −1.17 percent, so a bare 1.17 would survive a change to the first. stated(literal, source, *phrases) checks the figure in the slice's raw text and each phrase in its collapsed text, so rewrapping a line can't split a phrase.

Where two sections state a figure, each gets a tie:

  • §6.1's footnote compares the superseded blend's 0.98 percent with the committed blend's 1.17. A new tie, "nowcast evaluation amended MAE 1.17", checks that copy, and "evaluation amended MAE 1.17" checks §6.2's.
  • The forecast-record link has ties in §6.1 and Appendix B. Each link tie now looks for the Markdown link target, ](https://…), and §6.1's tie uses section_source() in place of two str.split calls that widened the search when a heading went missing.

A second new tie, "2025 SE triple in tenure order", holds §6.2's standard errors of $327, $560 and $393 in tenure order, so a swap between two of them fails. The four 2020–2025 retrospective errors appear in prose only in the abstract, so their "current projection prose" ties now read the abstract. has_number() and has_ordered() now require the text they search, so no tie can fall back to the whole paper.

The guard keeps all 170 of #13's check labels and adds the two new ties. The PR changes no paper text or number.

Tests

tests/test_paper_guard.py adds SECTION_PROSE_MUTATIONS, 67 cases that each edit one section through #12's mutate_article, across §3.2, §5.3, §6.1, §6.2, §6.3 and both appendices. Of these, 49 move one figure, 10 swap two figures, 4 point links at main instead of the pinned commit, 3 change a spelled-out count and 1 flips a sign. Each of the 62 ties that read a section slice, including #13's abstract ties, fails in at least one case.

Three tests run the cases, and a fourth reads the guard's source:

  • test_section_figures_drift_alone runs each case and asserts that the guard fails and names the case's ties.
  • test_section_pins_reject_a_figure_restated_elsewhere reruns each case after restating the original sentence under the Discussion heading, and asserts that the guard's failures match the case's ties. A tie that read the whole paper would pass these papers.
  • test_section_slices_fail_closed_without_unique_markers renames five headings and repeats two. Each time, it asserts that the guard reports the heading and fails ties in that section.
  • GuardSourceTests.test_no_tie_reads_the_whole_paper parses the guard and fails if a has_number or has_ordered call omits its text or passes QMD, or if an in test reads QMD.

#13's abstract cases now also expect their "current projection prose" ties to fail. The full suite of 80 tests passes locally in 342 seconds, and CI's drift check runs the guard and the suite on Python 3.12 and passes at the head.

#13's guard (0fd4223) accepts 84 of the 141 mutated papers: 18 of the 67 single-section edits, 63 of the same 67 with a copy of the original sentence under Discussion, and 3 of the 7 heading edits. All four new tests fail against it, and this PR's guard rejects all 141.

Invariants

These hold for every paper the guard reads:

  1. No tie searches the whole paper. has_number() and has_ordered() take no default text, and GuardSourceTests fails on any call or in test that reads QMD.
  2. If a section's start or end heading goes missing, repeats or falls out of order, its slice comes back empty and the guard records a failure. The guard then exits 1, and every tie in that section fails.
  3. When a registered figure changes in the section that states it, the guard fails, even if the original sentence survives under another heading. Ties match at numeric-token boundaries, so the tests' figure moves stand for any edit to the digits.
  4. The guard rejects every paper that Tie the abstract's error rates to the abstract #13's guard rejects. It runs every check Tie the abstract's error rates to the abstract #13's guard runs, and each scoped tie implies the whole-paper test it replaced:
    • No start heading ends in a digit, comma or period, and every end heading starts with "#", so the characters around a slice can't extend a figure. Every figure the old has_number() and has_ordered() ties searched for contains a digit, and each ordered list starts with one. So a match at token boundaries inside a slice also matches in the full paper and passes the old in tests.
    • stated() checks the figure in the slice's raw text, and its phrases only add conditions.
    • The three has_ordered() calls that searched the whole paper now search raw slices, so their windows count the same characters.
    • Two searches moved into whitespace-collapsed slices: the abstract's four retrospective errors and Appendix B's links. Both look for strings without whitespace, and such a string appears in a collapsed slice only where it appears in the paper.
    • When both of §6.1's headings appear once and in order, section_source() returns the text the old str.split calls returned, and otherwise it fails.

We also tested invariant 4 with a probe that wrote up to three papers for each occurrence of every literal #13's guard searched the whole paper for. One changes a digit or count word, one splits the occurrence across two lines at its first space, and one deletes it. The probe also renamed and repeated each of the 22 slice headings. Across the 254 distinct papers, both guards reject 176, this guard alone rejects 25, and none passes this guard after failing #13's. Both accept the other 53: in 47, the edit changes or deletes a copy that no tie needs, and 2 repeat a §7 subheading outside §7, where no tie reads. The last 4 change one of Appendix B's two links to data/PROVENANCE.md.

Like the rest of the suite, the tests use only the standard library, and they enumerate every figure the scoped ties pin instead of drawing random edits. The probe needs #13's guard beside this one, so it ran once, outside the suite.

Follow-up

  • Appendix B links data/PROVENANCE.md twice, and its tie passes while either link keeps the pinned commit. Tie the abstract's error rates to the abstract #13's guard has the same gap, and a check that every link into this repository's blob/ tree carries the pinned commit would close it.
  • section_source() names the start heading in its failure message even when the end heading went missing or repeated.
  • APPENDIX_B_TEXT and REPRODUCIBILITY both slice Appendix B, one with the {.unnumbered} suffix on its headings and one without, and one slice would do.

Plan

axiom: n/a: guard and tests only; no policy rule changes

🤖 Generated with Claude Code

MaxGhenis and others added 4 commits November 6, 2026 21:10
The guard checked the 2020–2025 retrospective errors (2.29, 1.65, 0.51 and
0.90 percent) and the 2025 evaluation errors (1.17, 2.58 and 0.75 percent)
against the whole document. The body restates 1.17, 2.58 and 0.75, so the
guard passed when one of them changed in the abstract alone.

The guard now also reads each of these figures from the abstract alone,
beside the rule it scores, so a swap between rules fails too. It also ties
the abstract's count of four projection rules to the retrospective
artifact. The abstract slice now comes from section(): when "abstract: |"
or "# Introduction" goes missing or repeats, the slice comes back empty and
every abstract tie fails instead of reading the whole paper.

New tests change each figure in the abstract alone, swap two pairs, and
rerun every case with the original wording restated in the body. Another
test renames, repeats and restyles the slice's markers. No paper text or
number changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check_paper.py read the whole paper in 39 figure ties: 27 has_number
and 3 has_ordered calls that fell back to QMD, and 9 `literal in QMD`
tests. Each tie now reads the section that states its figure, through
slices that fail closed when a heading goes missing or repeats.
stated() keeps each old literal as the whole-paper search matched it,
line breaks included, and adds phrases that hold the figure beside
what it measures, so the guard still rejects every paper it rejected
before. has_number and has_ordered now require their text.

The abstract alone states the four 2020-2025 error rates, so its decoy
cases now list the "current projection prose" ties as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SECTION_PROSE_MUTATIONS holds 67 cases. Each changes one figure, or
swaps two, in the section that states it.
test_section_figures_drift_alone expects every listed tie to fail.
test_section_pins_reject_a_figure_restated_elsewhere restates the
original sentence under the discussion heading and expects exactly the
listed ties to fail. test_section_slices_fail_closed_without_unique_markers
renames or repeats seven slice headings. GuardSourceTests parses the
guard and fails on any has_number or has_ordered call that names no
text or reads QMD, and on any `in QMD` test; it finds 39 in the guard
at 0fd4223.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The module docstring, the comment on block (d) and stated() now say
what the guard does instead of what it once did: which slices keep line
breaks, which normalize whitespace, and why a stated() pin fails
wherever a whole-paper search for its figure would.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis mentioned this pull request Nov 7, 2026
11 of 16 tasks
@MaxGhenis
MaxGhenis force-pushed the max/abstract-guard-ties-20261106 branch from 0fd4223 to f95d8d7 Compare November 7, 2026 17:34
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