Repository navigation
Conversation
The OBR targets came from whatever obr.uk served, falling back to committed workbooks matched by URL substring, with the vintage label taken from sources.yaml rather than the bytes, year columns fixed by letter, and missing rows logged and dropped. A manifest per EFO vintage (targets/source_manifests/) now names the two workbooks with their SHA-256, size and own title, the unit and financial years every parsed table must head, and the targets they must yield. Builds read only the verified committed bytes; each table's years come from its header row; a missing target or year, or an undeclared one, fails the build. `python -m policyengine_uk_data.targets.obr_sources ingest` adopts a new vintage from browser-downloaded workbooks and prints each target's change. On the March 2026 bytes all 29 OBR targets are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n unit Sol review of #592: - The manifest's vintage, its label and both workbook titles must agree (march_2026, "March 2026", "March 2026 Economic and fiscal outlook ... receipts"), at load and at ingest, so verified March bytes cannot be labelled October. - A row whose label states its own unit ("Standard colour licence fee (£)") is refused under a "£ billion unless otherwise stated" table, and the TV licence target reads the "BBC licence fee receipts" row only. - Ingest checks both workbooks and parses them in a staging folder before copying anything, and never replaces an ingested vintage. - Coverage checks every workbook target, including one without the vintage. - The NICs docstring no longer claims Class 3 or that partial results pass. All 29 OBR targets are still identical to main's on the March bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
obr.uk refuses scripted downloads, so on Budget day a person downloads the two EFO workbooks in a browser. The manifest now records each workbook's download time with its SHA-256 and size: the file's modification time, which a browser sets on saving, unless --downloaded-at is given. `ingest --dry-run` checks everything and prints titles, hashes, sizes, download times and every target's change, writing nothing; on the active vintage it says whether the bytes match the committed workbooks, so a March 2026 download rehearses the Budget-day path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 15, 2026
- Refuse a row whose label gives its own unit in any spelling, or that sits under a unit sub-heading, rather than listing exact forms. - Accept plausible EFO title formats (reordered, a closing note, trailing punctuation, Unicode spaces and dashes) while refusing any title that names another vintage, another report or another kind. - Write the workbooks and the manifest under temporary names and rename them into place; a partial ingest is named on the next try. - Fail coverage when a target is built twice. - Require the download pages on a real ingest. None of the 25 March 2026 target rows is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Refuse a row whose label gives its own unit in any spelling, or that sits under a unit sub-heading, rather than listing exact forms. - Accept plausible EFO title formats (reordered, a closing note, trailing punctuation, Unicode spaces and dashes) while refusing any title that names another vintage, another report or another kind. - Write the workbooks and the manifest under temporary names and rename them into place; a partial ingest is named on the next try. - Fail coverage when a target is built twice. - Require the download pages on a real ingest. None of the 26 rows the build reads from the March 2026 workbooks (25 targets) is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
w3-obr-source-manifest
branch
from
October 15, 2026 15:10
90b26bc to
f1faff2
Compare
Sol delta review of #592 at f1faff2 (four P3 findings), plus one unit fix: - A workbook title must also say "detailed forecast tables", so "Economic and fiscal outlook – October 2026: supplementary fiscal tables – receipts" is refused. - A dry run reports what the real ingest would refuse (an ingested vintage, an interrupted ingest's leftovers, a download page missing or off obr.uk) as "would refuse: ..." lines, returns them in IngestResult.refusals and exits 1, still writing nothing. A real ingest raises exactly those refusals. A dry run of the active vintage on the bytes its manifest names is a rehearsal, not a refusal. - Each manifest file records downloaded_at_basis: mtime or given, or unrecorded when there is no downloaded_at. The March 2026 workbooks entered git in 11e41f9 with no download time, so theirs is unrecorded. - A download page must be https on obr.uk or a subdomain, with nothing in the address but the host and port 443. - A sub-heading restating the table's own unit ("£ billion", "£bn") is no longer refused. - The manifest text is parsed back before anything is written. - The CLI rehearsal tests run on a copy of the committed manifest and workbooks made the active one, so no test can touch the committed files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…trings The test that a "£ billion" sub-heading under table 4.19's "£ billion unless otherwise stated" is not refused now also puts another unit in the same cell and expects a refusal, so it cannot pass by the heading going unread. Two docstring lines over the line length are rewrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dry run now exits 1 when the real ingest would refuse, and a missing --receipts-url or --expenditure-url is a refusal, so the module's example command names both obr.uk pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51 of 76 tasks
This was referenced Oct 17, 2026
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.
Do not merge on its own. Once approved on its gates, this lands with the d833 uk-data batch on Max's go. It is part of workstream W3 of the Autumn Budget 2026 CI plan: Budget-day OBR source ingestion with hashes, vintage, units, geography and year coverage.
Why
The OBR targets came from whatever
obr.ukserved, falling back to two committed workbooks matched by URL substring. Four things could go wrong without anything failing:sources.yaml, not from the bytes. Pointingsources.yamlat the October 2026 tables while the download failed would have labelled the March 2026 bytes as October 2026. obr.uk refuses scripted downloads (403 to Actions runners per 11e41f9, and to a developer machine on 2026-10-15), so the fallback is the normal path.Cmeant 2024-25 on four tables andDdid on the current-receipts table. An EFO whose tables start at 2025-26 would have been read one year off in every target.get_targetscaught every receipts or expenditure exception and continued, so a renamed row, a formula in a value cell or a failed parse calibrated without those targets.What changes
targets/source_manifests/obr_march_2026.yamlnames the two committed workbooks with:£ billion) and financial years (2024-25 to 2030-31);sources.yamlnames the manifest in use.The manifest is bound to the bytes.
march_2026, "March 2026", "March 2026 Economic and fiscal outlook … receipts".targets/obr_sources.py.workbook_targetsfails unless:forecast_vintageis the manifest's vintage, so it always names the bytes it came from.get_targetsno longer catches. A calibration without the OBR targets is never correct.python -m policyengine_uk_data.targets.obr_sources ingestadopts a vintage on Budget day:--receipts-urland--expenditure-url, the pages they came from. Every target cites those pages, so each must be https onobr.ukor a subdomain of it, with nothing else in the address but port 443.storage/obr_efo/<vintage>/and write the manifest, each under a temporary name that is renamed into place. An interrupted ingest leaves a.partialfile, which the next attempt names, never a half-written file under the real name. An ingested vintage is never replaced.obr_<vintage>.yamland prints every target's old and new value.The manifest records each workbook's SHA-256, size and download time, with
downloaded_at_basissaying how that time is known:mtime: the file's modification time at ingest. The default. Copying a file without keeping its times, or unzipping it, changes that time, so pass--downloaded-atfor such a file.given: stated with--downloaded-at.A person's browser download is therefore hash-bound and timed, and the manifest says how sure the time is. The March 2026 manifest records
unrecordedand no time. Its workbooks entered git in 11e41f9, so they were downloaded no later than 2026-07-21 21:06:50 +01:00, but nothing recorded when. Git keeps no file times, so the time is neither anmtimenor agivenone. The parser requiresmtimeorgivenwhen there is adownloaded_at, andunrecordedwhen there isn't.--dry-runwrites nothing. It checks everything and prints the titles, hashes, sizes, download times with their basis, and every target's change. It also checks what the real ingest would refuse: an ingested vintage, an earlier ingest's leftovers, or a download page missing or not on obr.uk. It prints each as "would refuse: …", returns them, and exits 1. A real ingest raises exactly those refusals. Any other problem (a title, a year, a missing target) raises in a dry run as in a real ingest.On the active vintage it says whether the bytes match the committed workbooks. A dry run of the active vintage on the bytes its manifest names is a rehearsal of the Budget-day path, so that manifest is not a refusal:
Run on copies of the committed March workbooks, it:
Without the two URLs it prints "would refuse: give the obr.uk https page each workbook was downloaded from (--receipts-url, --expenditure-url)" and exits 1. With other bytes under the March name it would refuse the ingested vintage.
Adopting is then a one-line
sources.yamlchange.verifychecks the active manifest.Removed: the download, retry and fallback path (
_download_workbook,_fallback_workbook) and its tests.Effect on the data
None. On the committed March 2026 bytes, all 29 OBR targets are identical before and after the change: names, variables, every value, vintage and reference URL. The 25 workbook targets plus 4 static ones were compared directly, and
test_header_read_years_match_the_fixed_columns_on_march_2026pins the year mapping against the old column letters. Calibration inputs are unchanged, so no release numbers move. This round's changes leave the 25 workbook targets (175 target-years) identical, field by field, to those built at the previous head,f1faff2f.Invariants (tested)
"ingest: " + "; ".join(refusals).obr.ukor ends in.obr.uk.obr.uk.example.com,notobr.uk,obr.uk@example.com,example.com\.obr.uk,example.com\@obr.uk,user:password@obr.uk,obr.uk:8443,-.obr.ukandhttp://obr.uk.mtime,given,unrecorded) and offset-aware times: a manifest's YAML parses back to the same files and targets.Interactions with open PRs
Sol's review checked these with
git merge-tree:obr/nionni_employee) conflicts in a test, and removes a target this manifest requires. Whichever lands second must edittargets.required; coverage fails until it does, which is the intended check.corporate_sdlt_receipts()defaults to_FY_COL_TO_YEAR.values(), which this PR removes. Useactive_manifest().yearsinstead (noted on Calibrate households' SDLT to the OBR total less corporate SDLT #569).Tests
Run serially:
test_obr_sources.pyat this head (133632af): 102 passed.test_obr_council_tax.py,test_obr_nic_signal.py,test_obr_nics.py,test_obr_receipts_sheet.py,test_obr_universal_credit_target.py) and the two OBR fixture filestest_esa_jsa_targets.pyandtest_dwp_housing_benefit_targets.py: 42 passed, 6 skipped. The skipped tests need the enhanced FRS dataset, which isn't built locally.test_target_registry.py, which builds every target, andtest_changelog_fragment_check.py: 105 passed.The last two runs were at
159db44e, which differs from this head only in a docstring.The full suite (
pytest policyengine_uk_data/tests, no built datasets locally) last ran at the previous head,f1faff2f: 2244 passed, 131 skipped, 1 xfailed, 1 failed. The failure wastest_target_registry.py::test_savings_interest, a live ONS download that failed during a network outage. It passes on re-run, and it passed in the registry run above. The full suite has not been re-run at this head.axiom: n/a: calibration-target ingestion infrastructure; no policy change
🤖 Generated with Claude Code