Skip to content

fix(pdf): rejoin every printed word break, deciding the hyphen from the document (#650) - #682

Draft
willhea wants to merge 11 commits into
developfrom
worktree-issue650-word-breaks
Draft

willhea wants to merge 11 commits into
developfrom
worktree-issue650-word-breaks

Conversation

@willhea

@willhea willhea commented Aug 24, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #650. Reviewed against the amended contract in that issue.

What this fixes

A word GPO's printer broke across a line reached full_text broken. _merge_print_lines required .islower() on the continuation, so INTEL- / LIGENCE and McKinney- / Vento were never joined at all.

Measured against each version's own XML, the defect is larger than the uppercase case it was filed for:

sites today
Uppercase continuation 1,031 never joined, the filed defect
Lowercase continuation, compound hyphen 423 joined but welded: project-based → projectbased
Break at a page seam 1,542 never joined; _merge_print_lines runs per page

Whether the hyphen belongs to the word is not decidable at the break, but it is decidable from how the rest of the document spells that word.

Measured with the shipped aligned oracle and the gate's own accounting (_account), evaluating develop's parser and this branch's over the same population:

judged undecided wrong of which left unjoined
today 50,785 224 3,027 2,611
this branch 50,785 224 165 0
fixed 2,862

Split sites are enumerated from the printed lines, upstream of any join, using #650's definition (a line ending in an alphanumeric, or the period of an abbreviation compound, then a hyphen, including page-seam breaks continued by running-header chrome). The oracle then judges each site or counts it undecided, whether or not the merger joined it. So the population and the judged/undecided split depend only on the printed lines and the XML, not on the parser, and the two rows are measured over the identical 50,785 judged sites.

The 165 decomposes exactly: 165 occurrences over 159 distinct per-version residual keys (version, left, right), which is the residual fixture row-for-row, and zero judged sites left unjoined. Those 159 keys cover 96 globally distinct fragment pairs, since the same pair recurs across versions.

An earlier revision of this PR claimed 2,980 → 118 with 1,336 fixed. Those came from the vocabulary oracle the contract rejects and are withdrawn, not restated: the numbers above are re-derived on the population every other figure here is now stated over.

All six named examples reflow correctly, each against its own version's XML: INTELLIGENCE, McKinney-Vento, E-Verify, Non-Dedicated, Climate-Related, Child-Rescue.

Three false greens found in review, now fixed

The first version of this PR passed its own gate while being wrong in three ways. Each fix is pinned by a negative control, and each control was confirmed to go red when the mistake is reintroduced.

1. The repair joined broken words to running-header chrome. PDFium floats H. R. 3547—61 to the top of the next page, where it lands between a page-seam break and its continuation. It starts with an alphanumeric, so the guard let it through, producing evidence-H., project-H., Hart-Scott-H. — and all 16 were then blessed in the residual fixture, so the gate certified them. In the corpus this shape outnumbers the genuine cross-page uppercase breaks 16 to 5, so the dominant cross-page case was corrupted while the named examples passed.

2. Pooling let one version overrule the other about its own text. Both sides were merged into one index and the majority won. A version writing Non-Dedicated once and never NonDedicated was rendered NonDedicated when the other version wrote the closed form more often — corrupting a version that was never ambiguous, and erasing a real spelling change so the diff reports no change where there is one. Evidence is now ordered tiers: own text first, sibling only where silent, then the fallback.

The earlier "0 cross-version disagreements after pooling" measurement was vacuous. Pooling applies identical evidence to both sides, so agreement was guaranteed by construction. It measured a tautology and was reported as a safety property.

3. The gate used the vocabulary oracle the contract rejects. It asked whether a produced form appears anywhere in the version's XML, which certifies a reconstruction from an unrelated occurrence and cannot choose at all when a bill uses both spellings. XmlOracle now matches against PDF neighbours, trigram first then bigram, comparing both candidates within a tier before falling to the next. Writing that control caught a further bug in my own oracle: mixing tiers reads a bigram for one candidate as competing with a trigram for the other. Sites the oracle cannot decide are now counted per version and asserted, so silent growth of the uncovered set is itself a failure.

Scope

The join_points schema 2.1 change and the in-browser search rewrite have been removed from this PR and preserved on issue653-join-points-wip for a separate PR against #653. The amended #650 states search is out of scope: it reads the print-faithful DOM and normalizes independently, so repairing full_text does not change it, and the two fail differently.

The net diff against the base now touches no file under schema/ or src/deltatrack/formatters/. schema_version stays 2.0.

Verification

  • New gate tests/test_pdf_word_break_recall.py, the only hyphen-sensitive check in the suite. normalize_for_cross_format deletes hyphens before matching and says so; normalize_for_recall rewrites Child- Rescue back at compare time. Both are right for a recall question and both make this defect invisible.
  • Clause 1 is oracle-driven, asked as "would joining these fragments make a word the bill has in that position", so it cannot pass by restating the merger's own rule.
  • Residuals asserted by set equality, not a ceiling: 159 per-version keys, plus 224 undecided sites asserted per version across 52 versions. A missing budget entry fails rather than disabling the check for that version, and a companion test pins the budget's key set to the collected versions. A join at a printed line outside the site population also fails, so no join can escape the count.
  • Red→green proven by mutation on the original defect, and five controls proven red on theirs: chrome join, pooled majority, vocabulary oracle, deleted budget key, and an undecidable unjoined site added to a passing input.
  • All five gates run locally on d482576a: ruff check . and ruff format --check . (255 files), fast tier 2,011 passed, browser tier 41 passed, slow tier (--deselect tests/test_govinfo_corpus_parity.py) 1,807 passed. See Base below for hosted status.

Review repairs at d482576a

Two findings from the review of 8415084c, one commit each.

1. Abbreviation compounds lost their hyphen (e59f7963). _is_break_tail accepted U.S.- as a break, but _BREAK_TAIL required an alphanumeric before the hyphen, so _break_left("U.S.-") returned an empty fragment. The lookup then missed the document's own U.S.-FSM and produced U.S.FSM. The fragment pattern now accepts the same terminal period.

  • A new parameterized test, U.S.- / based → U.S.-based and U.S.- / FSM → U.S.-FSM, supplies the unbroken spelling as own-document evidence. It was red on the pre-fix pattern (U.S.based, U.S.FSM) and is green now.
  • 8 residual keys resolve. Each was checked against its version's XML, which writes the hyphenated form: U.S.-FSM, U.S.-RMI, U.S.-Palau / U.S.-PALAU, U.S.-Cuba, U.S.-Israeli.
  • One canonical baseline pair moves, 118-hr-4366 4→5: identical change count and summary, +27 bytes. Its canonical output was diffed with and without the fix, and the whole difference is 27 hyphens restored after U.S., with no other text edit.
  • The round-1 sentinel moves only parser_revision. No pairing-stream digest changed on any of the 27 pairs.

2. Undecidable sites the merger did not join sat outside the budget (d482576a). The budget counted only among _joins, and _unjoined skipped undecidable candidates without counting them. _account now enumerates sites from the printed lines first and records each one as judged or undecided, joined or not. scripts/regen_word_break_residuals.py calls the same function.

  • Judged sites are unchanged at 50,785. Undecided goes from 53 to 224, and each of the 171 added sites is an unjoined candidate: 165 running-header chrome, 5 non-alphanumeric continuations (enumeration dash, suspended hyphen), 1 end of document. No residual moved as a result of this change.
  • New negative control: adding an undecidable, unjoined chrome boundary to a passing input fails the unchanged budget. It went red under the mutation that counts undecided sites only among joins.

ADR 0020 evidence

Of 23 pinned pairs, 6 changed and all 6 emit fewer changes (−32 total, almost entirely spurious added); none emits more. That is the #31 defect class clearing. One round-1 revocation moved with it, 115-hr-5895/3→4 (72 → 71); the other 34 pairs are identical under both extractors, and test_pdf_round1_revocation's pinned figure is updated 224 → 223 with that attribution recorded.

The U.S.- repair in this round moves one of those six pairs again, 118-hr-4366 4→5, by text only: its change count (1,272) and summary are unchanged, so the six-pair, −32 figure stands.

Known limits

  • 159 per-version residual keys still reconstruct wrongly (165 occurrences of 50,785 judged, 0.32%; 96 globally distinct fragment pairs). These counts come from the gate, which runs the single-document path and does not consult sibling evidence, so they are an upper bound on what a comparison's reader sees. Two causes, and an earlier revision of this text wrongly said all were the first:
    • 148 keys fall to the case-shape rule: a lowercase-continuation compound with no evidence in the document (government- / driven).
    • 11 keys are decided by the document's own evidence, where the bill uses both spellings and its majority form differs from the one at this position (118-hr-4366 writes multifamily 20 times and multi-family 7; this site's XML has multi-family). The fixture's per-row reason string still says "decided by case shape" for these 11; see follow-ups.
  • The case-shape rule's observed error of 5.9% (2,001 sites, 118 wrong) is carried over from the earlier measurement and was not re-derived in this round. A probe with this round's site definition does not reproduce its totals, so the population definition behind 2,001 differs. What the probe does show is the effect of the U.S.- repair on it: 2 fewer fallback sites, no change in case-shape errors. It fails single-directionally, never wrongly keeping a hyphen. Draft PDFs are unmeasured; 5.9% is not a prediction for them.
  • Page-seam breaks continued by running-header chrome stay split: H. R. 3547—61 on enrolled prints, † HR 4366 EAS on Senate engrossed amendments. That is Running page header/footer is read as bill text and splices into the middle of a word #535, untouched here. The oracle cannot judge them because the chrome token is not a word, and they are now counted in the undecided budget rather than skipped: 165 of the 224 undecided sites. That is why they sit outside the "zero left unjoined" result above rather than contradicting it.

Base and hosted status

origin/develop has been merged into this branch and pushed. GitHub now reports the
pull request mergeable; the three conflicts were confined to derived artifacts
(examples/hr8752_pdf_diff.html, tests/data/pdf_canonical_baseline.json,
web/webapp/sample/example.html) and were resolved by regenerating them on the new
base rather than hand-merged. No non-derived implementation code conflicted:
develop changed nothing under src/deltatrack/parsers/ or src/deltatrack/compare/,
which is the code this branch touches.

Two things to know when reading the artifact diff:

  • develop reworked src/deltatrack/formatters/ in the same span, so the regenerated
    examples and canonical baseline carry those changes as well as this branch's. That
    movement is not word-break drift.
  • The round-1 sentinel's parser_revision fields move whenever the parser's bytes
    change, including for comments and formatting. Every regeneration on this branch has
    moved those fields and no pairing stream, which is what makes those edits
    demonstrably behaviour-neutral.

Hosted CI is green on d482576a: all 27 checks, including corpus-gates and remaining-slow on 3.12.0, 3.12, 3.13 and 3.14. The per-tier test counts quoted above are from the local run on the same commit.

🤖 Generated with Claude Code

willhea and others added 9 commits August 21, 2026 17:31
…he document (#650)

A word GPO's printer broke across a line reached `full_text` broken whenever the
continuation was uppercase, so the Title I heading of 118-hr-8752 exported as
`INTEL-` / `LIGENCE` and its governing statute as `McKinney-` / `Vento`. The
merge required `.islower()` on the continuation, so it declined to join at all.

Swept over the fixture corpus with GPO's XML as an oracle, the defect is larger
than the uppercase case it was filed for, and in two directions the issue did not
name:

    1,031  uppercase continuation, never joined      -- the filed defect
      423  lowercase continuation, joined but WELDED -- `project-based` -> `projectbased`
    1,542  break at a PAGE SEAM, never joined        -- `_merge_print_lines` runs per page

Whether the hyphen belongs to the word is not decidable at the break: GPO breaks a
compound at its own hyphen and PDFium marks that identically to a syllable break
(`McKinney<FFFE>22 Vento`). It IS decidable from the rest of the document, which
almost always spells the word out somewhere it did not have to break. `BreakEvidence`
indexes those spellings and decides 47,875 of 50,679 breaks with zero errors against
the XML; pooling the compared pair's two documents adds 826 more, also with zero;
the remainder falls to letter case.

Measured paired on the 50,679 breaks the XML can decide: 2,980 wrong -> 118 wrong,
1,336 fixed, no site regressed. Through the pipeline's own output the corpus goes
from 3,201 defective sites to 213, and the 180 that remain are enumerated in
tests/data/pdf/word_break_residuals.json with the reason each could not be decided.

Not gated on PDFium's U+FFFE marker, though it names the same breaks where it
appears. Swept corpus-wide, 1,390 of 52,044 line-final hyphens carry no marker --
every one in an enrolled print, plus a 0-4% tail of ordinary syllable breaks in
numbered prints -- so gating on it would stop joining all of them.

ADR 0020 evidence for the correspondence that moved: of 23 pinned pairs, 6 changed
and all 6 emit FEWER changes (-32 total, almost entirely spurious `added`), none
more. That is the #31 defect class -- a word split in one version and whole in the
other reading as a change -- clearing. One round-1 revocation moved with it, in
115-hr-5895/3->4 (72 -> 71); the other 34 pairs are identical under both extractors.

The gate is hyphen-sensitive, which nothing else in the suite is:
`normalize_for_cross_format` deletes hyphens before matching and says so, and
`normalize_for_recall` rewrites `Child- Rescue` back into `Child-Rescue` at compare
time. Both are right for a recall question and both make this defect invisible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e-deriving them (#650)

Schema 2.1 adds optional `join_points: {v1, v2}`: where the printer broke a word
across a line inside `full_text`, and whether reflowing that break drops the hyphen.
Additive; `null` wherever the text has no printed line breaks to reflow, which is the
XML pipeline always and the PDF pipeline's reflowed rendering.

The field exists because the answer is not recoverable from the text it describes.
GPO prints a syllable break and a compound broken at its own hyphen identically, so a
consumer looking at `INTEL-` / `LIGENCE` and `McKinney-` / `Vento` cannot tell which
closes up and which keeps its hyphen. The extractor decides it from evidence the
consumer does not have -- how the rest of the document spells that word -- and now
carries the decision instead of discarding it. That is #653's rule: a consumer may
derive by applying facts the document carries, and may not re-infer facts it omits.

In-browser search was the third implementation of the rule and the one that disagreed
with the parser in both directions. It now applies a `data-join` attribute the
renderer stamps on the row from the carried points. Its own test asserted agreement
with the parser's merged text and passes by construction rather than coincidence.

Encoding: `at` is delta-encoded and `drop` is a bitstring, not an array of objects.
Measured on 118-hr-8752, 2.7 kB against 14.4 kB -- 2% of the rendered text rather
than 11%. Both were built and compared rather than assumed.

Verified by reconstruction: applying every carried point to the print-faithful text
reproduces `pdf_full_text` exactly, and `pdf_full_text` is computed without reference
to the points. A wrong offset, a wrong delta or a flipped bit diverges.

Baseline churn is the version string alone -- 17 digests move, no pair's change count
does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… measurement (#650)

`pdf_blocks._rejoin_cross_page_hyphens` and `pdf_anchors._join_major_run`'s
de-hyphenating branch each still describe how to reflow a printed word break, and each
describes the pre-#650 answer: lowercase-continuation-only in the first, drop the
hyphen unconditionally in the second. Neither runs any more -- `extract_clean_pages`
resolves every break before either sees a `Page` -- but a reader has no way to tell a
dormant rule from a live one, and four copies of this rule quietly disagreeing is the
whole of #650.

Measured rather than asserted, over the fixture corpus:

    pdf_blocks._rejoin_cross_page_hyphens   0 joins performed
    pdf_anchors._join_major_run (hyphen)    0 de-hyphenations performed

Kept rather than deleted: both remain reachable for a `Page` assembled by hand (tests,
and the anchor parser's single-page `parse_lines`), neither of which can carry a page
seam. Each docstring now says it is dormant, that it is not the project's rule, and
that `pdf_text.BreakEvidence` is. Deleting them is a separate change with its own test
surface, and is not bundled here.

Comment-only, and shown to be: regenerating the round-1 sentinel moves 27
`parser_revision` fields -- the digest covers module bytes by design, so a comment
moves it -- and 0 pairing streams.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review against the amended contract found the repair passing its own gate while
being wrong in three ways. Each is fixed and pinned by a negative control that was
confirmed to go red when the mistake is reintroduced.

1. The repair joined broken words to running-header chrome. PDFium floats
   `H. R. 3547—61` to the top of the next page's reading order, where it lands
   between a page-seam break and its continuation. It starts with an alphanumeric,
   so the "continuation starts with a letter" guard let it through, producing
   `evidence-H.`, `project-H.`, `Hart-Scott-H.` -- and all 16 were then blessed in
   the residual fixture, so the gate certified them. In the corpus this shape
   outnumbers the genuine cross-page uppercase breaks 16 to 5, so the dominant
   cross-page case was corrupted while the named examples passed. `_SEAM_CHROME`
   now declines the join; the 16 entries are gone from the fixture.

2. Pooling let one version overrule the other about its own text. Both sides were
   merged into one index and the majority won, so a version that writes
   `Non-Dedicated` once and never `NonDedicated` is rendered `NonDedicated` when the
   other version writes the closed form more often. That corrupts a version that was
   never ambiguous AND erases a real spelling change, so the diff stops reporting a
   difference the documents have. Evidence is now ordered tiers: a document's own
   text first, the sibling consulted only where it is silent, then the case-shape
   fallback.

   The earlier "0 cross-version disagreements after pooling" measurement was
   vacuous. Pooling applies identical evidence to both sides, so agreement was
   guaranteed by construction; it measured a tautology.

3. The gate used the vocabulary oracle the contract rejects. It asked whether a
   produced form appears anywhere in the version's XML, which certifies a
   reconstruction from an unrelated occurrence and cannot choose at all when a bill
   uses both spellings. `XmlOracle` now matches the reconstruction against its PDF
   neighbours, trigram first and bigram either side as a fallback, comparing both
   candidates WITHIN a tier before falling to the next -- the negative control caught
   that mixing tiers reads a bigram for one candidate as competing with a trigram for
   the other. Sites the oracle cannot decide are now counted per version and asserted,
   so silent growth of the uncovered set is itself a failure.

Residuals 180 -> 167 and 53 sites recorded as undecided.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…650

The amended #650 states that in-browser search is out of scope: search reads the
print-faithful DOM and normalizes it independently, so repairing `full_text` does not
change it, and the two fail differently. `full_text` leaves all six named examples
split, while search is already correct for the compounds and wrong only for
`INTEL-` / `LIGENCE`. Shipping the search rewrite here would contradict the contract
this PR is meant to satisfy.

Removed from this branch, and preserved on `issue653-join-points-wip` for a separate
pull request against #653:

  - `join_points` on the canonical, and the schema 2.1 bump that carries it
  - `pdf_print_join_points` / `print_join_points`
  - the `data-join` row attribute and the in-browser search rewrite that reads it
  - the fixtures and version pins those required

What stays is what #650 asks for: `full_text` reconstructs a word the printer broke,
the print-faithful rendering is untouched, and nothing here changes the public
contract. `schema_version` returns to 2.0 and the regenerated baselines lose the churn
the version string caused, so the remaining diff in those artifacts is the word-break
repair alone.

Verified: full suite green, and the canonical baselines regenerate with no change to
any pair's change count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pooling claims (#650)

Three corrections from review.

The undecided-site budget failed OPEN. It was read with a default, so deleting a
version's entry silently disabled the control for that version -- the one edit most
likely to be made to quiet it, and precisely the weakening the count exists to
prevent. A missing entry is now a failure, and a companion test asserts the budget
covers exactly the collected versions, so a stale entry is caught alongside a missing
one. Confirmed red by deleting a key.

`BreakEvidence` still advertised the pooling method and its result -- "pooling a
compared pair's two documents adds 826 more, also with zero errors" -- for a method
that no longer exists and a measurement this branch has already acknowledged was
vacuous. `PrintPages` and the regeneration script described pooling too. All three now
describe own evidence first with the sibling as fallback, and the docstring records
why pooling was withdrawn rather than leaving the discredited claim in place for the
next reader to trust.

The result quantities are re-derived. The figures previously quoted came from the
vocabulary oracle the contract rejects, so they could not stand once the oracle
changed. Measured with the shipped `XmlOracle`, evaluating `develop`'s parser and this
branch's with identical code over the same population of 50,785 judged breaks:

    today   3,027 wrong      repair   193 wrong      fixed 2,834
    undecided 47 -> 53

The 193 decomposes exactly: 193 occurrences over 167 distinct fragment pairs, which is
the residual fixture, and ZERO splits left unjoined. The fallback figures were also
re-derived and are unchanged (2,001 sites, case shape wrong on 118, 5.9%).

#650 is amended with the corrected population and an account of the 74 -> 53 movement.

Sentinel regeneration moves 27 parser_revision fields and no pairing stream, so the
docstring edits are behaviour-neutral.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… can (#650)

Docstring-only. Audited after the same failure was found in a sibling branch's test:
a description claiming more than the code does. This file had four instances.

The oracle paragraph still described VOCABULARY membership -- "every word form the
reflowed PDF produces at a break should be a word form the XML has" -- which is the
framing #650 explicitly rejects and which the code stopped doing when `XmlOracle`
became positional. A reader checking the test against the contract would have found
the file arguing for the contract's discarded alternative.

Neither clause stated its boundary. Both are scoped to sites the oracle can judge, and
a break attested in neither form or in both equally is EXCLUDED and counted, not
passed. That set is where the #535 chrome seams sit, so "no word left split" was
never the guarantee: it is "none left split where the XML can say so". Written down
explicitly, because a reader who took the stronger reading would stop looking for #535,
which is still open.

`_unjoined_words` and `_RESIDUALS` were both renamed and the prose was not updated.

The class's comparative claim was carried over from a standalone probe whose tiering
differed from the shipped class, and was quoted after that tiering was corrected. A
caveat is binding on the person who wrote it, so it is re-derived here rather than
trusted: measured with `XmlOracle` over the sites the repair produces, aligned matching
decides 50,785 against vocabulary's 50,716, contradicting it at zero sites. The claim
holds; it just had no current measurement behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#650)

The residual paragraph said both candidate forms are absent from the document's own
text "and from the other version compared with it". This gate runs the SINGLE-DOCUMENT
path (`cached_pages` -> `extract_clean_pages`) and never consults a sibling, so it
established no such thing. The sibling-borrowing path exists, in `compare/pdf.py`, but
it is not what these counts measure, and reading them as though it were would overstate
what a comparison's reader is exposed to in the wrong direction.

Now says what the gate exercises, and notes separately that the production comparison
can borrow the other version after the document's own evidence is silent, so the
residual set here is an upper bound. `_residuals` carried the same claim and is fixed
with it.

Also runs `ruff format`, which is why `lint-format` failed the last hosted CI run on
this branch (`e171b36f`); every test job in that run passed. Three files this branch
introduced were unformatted. The diff is line rejoining and trailing blank lines, with
no semantic change, and it is included here rather than left because the branch cannot
go green without it.

Regenerating the round-1 sentinel afterwards moves 27 parser_revision fields and no
pairing stream: the revision digest covers module bytes by design, so reformatting
moves it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…word-breaks

# Conflicts:
#	examples/hr8752_pdf_diff.html
#	tests/data/pdf_canonical_baseline.json
#	web/webapp/sample/example.html
willhea and others added 2 commits September 29, 2026 22:04
`_is_break_tail` accepts a period before the break hyphen, so `U.S.-` / `FSM`
is joined, but `_BREAK_TAIL` required an alphanumeric there and extracted an
empty fragment. The evidence lookup then missed the document's own `U.S.-FSM`
and the case-shape fallback produced `U.S.FSM`. The fragment pattern now
accepts the same terminal period as the break detector.

Eight residual keys resolve, each against its version's XML, which writes the
hyphenated form (`U.S.-FSM`, `-RMI`, `-Palau`, `-Cuba`, `-Israeli`). One
canonical baseline pair moves, 118-hr-4366 4->5: same change count and summary,
+27 bytes, exactly 27 hyphens restored after `U.S.` and no other text edit.
The round-1 sentinel moves only `parser_revision`; no pairing stream changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#650)

The budget counted undecided sites only among the merger's joins, and the
unjoined check skipped undecidable candidates silently. So a split site the
merger declined to join and the oracle could not judge, such as a page-seam
break continued by running-header chrome, sat outside the gate: adding one
passed without moving the budget.

`_account` now enumerates split sites from the printed lines first, using
#650's population (a line ending in an alphanumeric, or the period of an
abbreviation compound, then a hyphen, including chrome boundaries). It then
looks up each site's reconstruction and records it as judged or undecided,
joined or not. A join at a line outside the population fails as well, so no
join escapes the count. The regen script calls the same function, so the
fixture and the gate cannot disagree about which sites exist.

Judged sites are unchanged at 50,785. Undecided goes from 53 to 224, and each
of the 171 added sites is an unjoined candidate: 165 running-header chrome
(#535), 5 non-alphanumeric continuations (enumeration dash, suspended hyphen),
and 1 end of document. No residual moved.

Negative control: adding an undecidable, unjoined chrome boundary to a passing
input fails the unchanged budget. It went red under the mutation that counts
undecided sites only among joins.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Words split across a printed line stay broken in the exported bill text when the continuation is uppercase

1 participant