Skip to content

Tie the abstract's error rates to the abstract - #13

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

MaxGhenis wants to merge 5 commits into
max/ce-inkind-section-20261106from
max/abstract-guard-ties-20261106

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

WIP. This PR builds on #12, so merge #12 first. Once #12 merges and we delete its branch, GitHub retargets this PR to master.

What changes

scripts/check_paper.py checked the abstract's seven error rates against the whole document:

  • For 2020–2025, the abstract gives retrospective errors of 2.29 percent with CPI-U, 1.65 with composite prices, 0.51 with replicated consumption growth and 0.90 with the equal blend. The rendered paper repeats them in the mean row of the generated tables/current_projection.md, and no other line of paper/index.qmd states them.
  • For the 2025 evaluation, it gives 1.17 percent for the committed blend, 2.58 for CPI-U adjustment and 0.75 for the replication ratio. §6.2 restates all three.

The whole-document ties read paper/index.qmd without its included tables. Because §6.2 restates the evaluation figures, the guard passed when 1.17, 2.58 or 0.75 changed in the abstract alone (the known gap #12 lists). It caught a lone change to the four retrospective figures only because no other line of the file states them.

The guard keeps its whole-document ties and now also checks each figure inside the abstract, beside the rule it scores. scripts/paper_text.py finds the front matter's one top-level abstract: | key and returns the lines under it. Like Quarto and pandoc, it ends that block at the next line with fewer leading spaces than the block's first line of text. Other metadata fields, a comment after the block and the body fall outside it. Each tie collects every number the abstract gives in its rule's wording and passes only if each one equals the registered value. For example, every number the abstract puts before "percent with the Consumer Price Index" must read 2.29. The guard now fails if someone changes a figure, flips its sign or swaps two figures, and if someone adds a different figure in a rule's wording, even beside a correct copy.

Where Quarto could render other text as the abstract, or where Quarto and pandoc could disagree, the parser returns nothing. The guard then reports "paper must set abstract only in one abstract: | block of its front matter" and fails every abstract tie. The parser returns nothing when:

  • the front matter names the key more than once, or on a line other than abstract: |, as in abstract: >, "abstract": | or ? abstract;
  • a line after the front matter names the key at its start or in a flow mapping such as {abstract: …}, since a later metadata block, even in a list item or block quote, replaces the front matter's abstract;
  • a tab falls within the block's indentation, where Quarto rejects the line or ends the block and pandoc reads it as text;
  • a carriage return stands alone in the front matter, where Quarto and pandoc read it differently;
  • the front matter never closes, or the block holds no text.

The guard also ties "compare four projection rules" to the retrospective artifact, whose four rules must match the four the abstract names. #12's guard took the abstract as the text before the first "# Introduction", so renaming that heading widened its slice to the whole paper. The parser reads no headings, and #12's four in-kind ties that read the abstract now read the block and still pass.

The PR changes no paper text or number.

Tests

tests/test_paper_guard.py gains ABSTRACT_PROSE_MUTATIONS: eleven cases that edit only the abstract through #12's mutate_article, which now finds the abstract with the guard's own parser. Seven move one figure by 0.01, one flips the sign of 2.29, one changes "four projection rules" to "five", and two swap 2.29 with 1.65 and 1.17 with 0.75.

The PR adds five tests to that file:

  • test_abstract_figures_drift_alone runs each case and asserts that the guard fails and names the case's ties.
  • test_abstract_pins_reject_a_copy_restated_elsewhere reruns each case after restating the original wording in four places: under the Discussion heading, in a note: field after the abstract, in body text before the introduction, and on an extra line inside the abstract. Each time, it asserts that only the case's ties fail.
  • test_abstract_ties_ignore_the_introduction_heading renames "# Introduction", demotes it, repeats it, and moves it after a sentence that restates 1.17. Each time, the guard passes the paper, and changing the abstract's 1.17 to 1.18 fails the 1.17 tie alone.
  • test_abstract_ties_skip_a_comment_after_the_block rewords the CPI-U rule, then Add a section on imputed in-kind benefits #12's "less than one point" bound, and each time keeps the old wording in a comment line after the block, which Quarto leaves out of the abstract. The guard fails that wording's tie and no other.
  • test_abstract_block_fails_closed_unless_one_key_sets_the_abstract folds the key to abstract: >, repeats it, repeats it as "abstract": |, renames it to summary: | with an abstract: | copy in the body, and sets the abstract again from a metadata block in the body. Each time, it asserts that the guard reports "paper must set abstract only in one abstract: | block of its front matter" and fails all eight abstract ties and the in-kind lift-range tie.

Across these tests, the guard at 9ba8f61, the #12 commit this PR builds on, passes 62 of the 66 papers it should reject. Its whole-document ties catch only a lone change to 2.29, 1.65, 0.51 or 0.90. This PR's guard rejects all 66 and passes the four papers that only move the heading.

tests/test_paper_text.py adds 44 tests of the parser and the slot reader: 31 unit cases, 7 property tests that each draw 300 seeded random documents or statements, and 6 differential tests. The property tests check that:

  • the abstract's words round-trip through any wrapping, indentation and field order;
  • decoys in other fields and in the body never change what the guard reads;
  • a second or restyled key, or a key after the front matter, fails closed;
  • a rate the abstract states reads back unchanged, and a longer number never reads as a value it contains.

The differential tests render the fixture, 47 edge cases and random documents with decoys: 100 through pandoc and 12 through Quarto. Wherever the parser reads an abstract, each tool must show the same words or reject the document. Quarto reads the front matter with its own YAML parser before pandoc runs, and it rejects a ... terminator, a line holding only a tab in the abstract's block and a list under notes. We edit the Quarto tests' random documents to avoid all three. CI installs neither tool, so it skips these tests. When we break paper_text.py in any of 31 ways, one at a time, at least one test in that file fails, even without either tool.

Beyond the suite, we rendered 333 documents with Quarto 1.9.36, adding 86 cases and 200 more random documents to the edge cases. Quarto showed the parser's words in 207 of them. The parser failed closed on 41 that Quarto renders, returned text for 48 that Quarto rejects and returned nothing for 36 that Quarto rejects or renders without an abstract. The last document sets the abstract through an alias in a body metadata block, a spelling the parser does not resolve.

Like the rest of the suite, the tests use only the standard library.

Invariants

These hold for every paper the guard reads:

  1. Each abstract tie reads only the block under the front matter's single top-level abstract: | key, which ends at the next line with fewer leading spaces than its first line of text. No text in another field, in a comment after the block or in the body can satisfy a tie.
  2. Every abstract tie fails unless the paper opens with front matter that closes and names the abstract once, on a line that reads abstract: | with text under it. The ties also fail if a later line names the key at its start or in a flow mapping.
  3. Each tie passes only when every number the abstract states in its rule's wording equals the registered value, and at least one does. So the guard fails if someone changes the digits or sign of one of the seven rates, rewords its rule, swaps two rates, adds a different rate in a rule's wording or changes the rule count. The ties read numbers at numeric-token boundaries, so "12.29" never reads as "2.29".
  4. Wherever the front matter ends before the first "# Introduction", the guard rejects every paper that the guard at 9ba8f61 rejects. This PR adds ties, narrows the slice that Add a section on imputed in-kind benefits #12's four in-kind abstract ties read and removes no check. The unchanged paper passes both guards.

The parser recognizes the key spelled plainly, quoted or as ? abstract, but it resolves no YAML tags, anchors, aliases, escapes in quoted keys or flow keys without values. Quarto 1.9.36 rejects every such spelling in our comparison but one: in a body metadata block, k: &k abstract followed by *k : … sets the abstract while the ties still read the front matter's block.

Follow-up

§6.2 restates 1.17, 2.58 and 0.75, and the guard still checks those restatements against the whole document, where the abstract's copies satisfy them. If someone changes one of them in §6.2 alone, the guard at this head still passes; it fails on 2.02, which only §6.2 states. In all, 39 call sites in the guard read the whole document, some of them in loops over several figures. #14 ties each of those figures to the section that states it.

Plan

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

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits November 7, 2026 12:07
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>
…d rate to match

The abstract ties read section("abstract: |", "# Introduction"), which also
took in the fields after the abstract, the closing delimiter and any body
text before the introduction, so a copy there could rescue a drifted figure.
scripts/paper_text.py now finds the one top-level `abstract: |` key in the
front matter and returns only its indented block. A missing, repeated or
restyled key leaves the slice empty and fails every abstract tie.

Each tie now collects every rate the abstract gives in its rule's wording
and requires them all to equal the registered value, so a correct copy
elsewhere in the abstract cannot hide a changed one, and a sign flip fails.

The mutation tests place decoys in another metadata field, in body text
before the introduction, inside the abstract and under the discussion
heading, and check that renaming, demoting, repeating or moving the
introduction heading changes nothing. tests/test_paper_text.py adds unit
and seeded property tests for the parser and the slot reader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the max/abstract-guard-ties-20261106 branch from 0fd4223 to f95d8d7 Compare November 7, 2026 17:34
MaxGhenis and others added 3 commits November 7, 2026 18:15
block_scalar_span now follows pandoc's reading of the front matter:

- a comment indented less than the block ends the block;
- the block takes its indentation from its first line with text;
- a quoted key or an explicit "? abstract" also names the key;
- a line after the front matter that sets the key, at its start or in flow
  style, fails closed, since pandoc lets a later metadata block, even
  inside a list item or block quote, replace the abstract;
- a tab in a block line's indentation fails closed, since Quarto rejects
  it, and so does a lone carriage return in the front matter, which pandoc
  drops;
- lines end only at line feeds, and front matter needs text on the line
  after its opening dashes.

The guard's label for a missing or ambiguous block becomes "paper must set
abstract only in one abstract: | block of its front matter". A new guard
test shows that a comment after the block cannot satisfy a tie, and the
fails-closed test adds a quoted repeated key and a body metadata block.
tests/test_paper_text.py grows to 40 tests, among them a differential test
that compares the parser with pandoc wherever pandoc is installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quarto 1.9.36 renders a tab that follows the block's indentation as text,
as pandoc does, so the parser now reads such a line instead of failing
closed. A tab within the indentation still fails closed: there Quarto
rejects the line, or ends the block, where pandoc reads text. The parser
counts only leading spaces as the block's indentation.

The parser tests now separate the tabs that fail closed from the tabs
that read as text, and the edge cases add both kinds.

A comparison with Quarto's rendered abstract over 333 documents (47 edge
cases, 86 shared cases, 40 random documents and 160 random documents
edited so Quarto renders them) found 207 agreements, 41 documents where
the parser fails closed, 36 that both refuse and 48 that Quarto cannot
render. One document diverges: an aliased key in a body metadata block,
which the pull request lists as out of scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quarto renders the paper, and it reads the front matter with its own YAML
parser before pandoc runs, so the two can disagree. Quarto rejects a `...`
terminator, a line holding only a tab and a repeated key, and it reads a
tab-led `#` line after the block as a comment where pandoc reads text.

Wherever Quarto is installed, three new tests render the fixture, the 47
edge cases and 12 random documents with decoys, and require Quarto's
abstract to show the parser's words, or Quarto to reject the document. The
random documents close with `---`, empty their tab-only lines and rename
the `notes` list, all of which Quarto would reject. CI installs no Quarto,
so it skips them, as it skips the pandoc tests.

The guard's and parser's docstrings now say they read the abstract as
Quarto and pandoc read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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