Conversation
…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
Open
5 tasks done
`_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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #650. Reviewed against the amended contract in that issue.
What this fixes
A word GPO's printer broke across a line reached
full_textbroken._merge_print_linesrequired.islower()on the continuation, soINTEL-/LIGENCEandMcKinney-/Ventowere never joined at all.Measured against each version's own XML, the defect is larger than the uppercase case it was filed for:
project-based→projectbased_merge_print_linesruns per pageWhether 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), evaluatingdevelop's parser and this branch's over the same population: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—61to 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, producingevidence-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-Dedicatedonce and neverNonDedicatedwas renderedNonDedicatedwhen 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.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.
XmlOraclenow 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_pointsschema 2.1 change and the in-browser search rewrite have been removed from this PR and preserved onissue653-join-points-wipfor a separate PR against #653. The amended #650 states search is out of scope: it reads the print-faithful DOM and normalizes independently, so repairingfull_textdoes not change it, and the two fail differently.The net diff against the base now touches no file under
schema/orsrc/deltatrack/formatters/.schema_versionstays 2.0.Verification
tests/test_pdf_word_break_recall.py, the only hyphen-sensitive check in the suite.normalize_for_cross_formatdeletes hyphens before matching and says so;normalize_for_recallrewritesChild- Rescueback at compare time. Both are right for a recall question and both make this defect invisible.d482576a:ruff check .andruff 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
d482576aTwo findings from the review of
8415084c, one commit each.1. Abbreviation compounds lost their hyphen (
e59f7963)._is_break_tailacceptedU.S.-as a break, but_BREAK_TAILrequired an alphanumeric before the hyphen, so_break_left("U.S.-")returned an empty fragment. The lookup then missed the document's ownU.S.-FSMand producedU.S.FSM. The fragment pattern now accepts the same terminal period.U.S.- / based → U.S.-basedandU.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.U.S.-FSM,U.S.-RMI,U.S.-Palau/U.S.-PALAU,U.S.-Cuba,U.S.-Israeli.118-hr-43664→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 afterU.S., with no other text edit.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_unjoinedskipped undecidable candidates without counting them._accountnow enumerates sites from the printed lines first and records each one as judged or undecided, joined or not.scripts/regen_word_break_residuals.pycalls the same function.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, andtest_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-43664→5, by text only: its change count (1,272) and summary are unchanged, so the six-pair, −32 figure stands.Known limits
government-/driven).multifamily20 times andmulti-family7; this site's XML hasmulti-family). The fixture's per-rowreasonstring still says "decided by case shape" for these 11; see follow-ups.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.H. R. 3547—61on enrolled prints,† HR 4366 EASon 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/develophas been merged into this branch and pushed. GitHub now reports thepull 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 newbase rather than hand-merged. No non-derived implementation code conflicted:
developchanged nothing undersrc/deltatrack/parsers/orsrc/deltatrack/compare/,which is the code this branch touches.
Two things to know when reading the artifact diff:
developreworkedsrc/deltatrack/formatters/in the same span, so the regeneratedexamples and canonical baseline carry those changes as well as this branch's. That
movement is not word-break drift.
parser_revisionfields move whenever the parser's byteschange, 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