UK wave 1: ONS population source packages — mid-year estimates 2023+2024 and 2024-based projections (#132) - #137
Conversation
|
Source-review outcomes and CI triage (commit Source decisions (reviewed):
CI triage: the red check was not the R2/storage gate — it was the Remaining red, by design: the bundle test's consumer export requires (Counts were verified under locally injected and reverted 🤖 Generated with Claude Code |
|
Re the failing Causal chain: the bundle now includes this PR's four packages (the alias registration doing its job), their consumer-contract export requires (4 artifacts here; #139 has the same designed red with 9.) Current blocker on running it: María's Cloudflare membership role doesn't include R2 — user API tokens are intersected with membership roles, so her 🤖 Generated with Claude Code |
|
Adversarial review complete — all findings addressed in Fidelity core: all 3,676 pins independently re-derived from the committed artifact bytes with zero mismatches — row headers, guard cells, column letters, header strings, cell types, filters↔constraints shape. Best cross-check in the review: the NPP 2024 sex×age total equals the mye24 UK all-ages estimate to the person (69,281,437). All shas, manifests (no premature R2 blocks), aliases, and boundary checks clean. Fixed from findings:
🤖 Generated with Claude Code |
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Read the registry diff and the bundle-constant updates; the four ONS population packages are pure data plus four SOURCE_PACKAGE_ALIASES lines, so there's no logic here to get wrong. Nothing blocking from me.
Two questions rather than findings:
1. Are the ~87k lines of source_package.yaml harness-generated or hand-authored? packages/ons/mye_2024_uk alone is 29,836 lines. If they're generated from the committed artifact bytes by validate-package, then review is genuinely "the gates passed" and that's fine — but it'd help to say so in the description, because the alternative reading (hand-transcribed) implies a very different review burden and a very different regeneration story when ONS reissues a table.
2. NPP 2024 is a projection, MYE 2023/2024 are estimates. They'll sit side by side in the bundle under calendar_year periods with no type-level distinction. Is the estimate-vs-projection boundary carried by provenance_class, or is it left to the consumer profile to know that ons/npp_2024_uk facts past 2024 are modelled? Worth being explicit somewhere, since a target profile that resolves "latest not after base period" would happily pick up a projection without noticing.
The description's task list shows 1 of 3 checked — is the remainder in-scope for this PR or tracked into #132's later waves?
4b456ec to
81999b0
Compare
…kages (PolicyEngine#133) Measured under the disclosed-and-reverted local storage.r2 sandbox (same protocol as PolicyEngine#137/PolicyEngine#139), where the merged-bundle test passes: 40,401 facts, 75 packages, 34 sources, 9 entities (dwelling is new), 84 periods, 1,067 geographies. All deep blocks (by_period, by_source, by_geography spot keys, by_entity, table count) re-measured together so the test converges with zero further commits once `ledger publish-raw --root db/data` runs on the branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Answering the review's three questions: 1. The ~87k YAML lines are machine-generated, not hand-transcribed. Row/measure blocks were emitted by generator scripts running over the parsed artifact bytes (the same parse the harness performs), then validated by 2. Estimate-vs-projection is carried by 3. The unchecked remainder is deliberately in sibling PRs, per the one-family cadence. SLC was gated on the 🤖 Generated with Claude Code |
…England regions (PolicyEngine#132) Two ledger-source-ingestor packages backing the 108 region-x-age rows of the wave-1 production surface (PolicyEngine#132), built from publisher truth instead of policyengine-uk-data's provenance-unrecorded demographics.csv: - ons-mye-2023-uk-countries: mye23tablesuk.xlsx (MYE2 persons), rows UK/Wales/Scotland/Northern Ireland, 368 facts. - ons-mye-2023-england-regions: mye23tablesew.xlsx (MYE2 persons, 2023 LA boundaries edition), publisher-emitted E12 region rows, 828 facts. Shape: one record set per single year of age (groupby age; all-ages sets are age-totals), rows carry per-row geography overrides, age enters fact identity via row filters mirrored by explicit constraints. Single-age facts at publisher granularity; the fixture's 10-year bands (whose 80_89 is actually 80+) are a populace-side mapping concern. Gates: validate-package clean for both; build-suite green end to end (source cells, records, facts, DB, agent acceptance) with facts spot-verified against the workbooks. Consumer-contract export requires manifest storage.r2 (raw_r2_uri), so the final consumer gate awaits 'ledger publish-raw --root db/data' by a credentialed maintainer; the committed manifests record checksums and retrieval metadata only. The MYE2 digit column headers are text-typed cells the guard contract cannot express (render int-coerces digit expectations, guard compare is raw); the age span is pinned by the guarded 'All ages' and '90+' endpoints instead. Follow-up contract issue to be filed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ns in one artifact (PolicyEngine#132) The mid-2024 UK edition publishes the E12 region rows directly in MYE2, so a single package covers all 13 geographies (1,196 facts at single year of age). Per-edition packages rather than year-templating because ONS restructured the workbook between editions (Wales moved from row 13 to row 345); the vintage-suffixed package-ID convention expects this. Same gates as the mid-2023 pair: validate-package clean, build-suite and agent acceptance green (proven under a disclosed, reverted local storage.r2 sandbox), facts spot-verified against the workbook. Consumer-contract export still awaits 'ledger publish-raw' by a credentialed maintainer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-1 aliases (PolicyEngine#132) ons-uk-population-projections-2024: the NPP principal projection (uk_ppp) from the 2024-based z1 uk.zip, one record set per year 2024-2029, rows = sex x single year of age (214 per year), 1,284 source_projection facts at calendar-year periods. The national NPP was never blocked by the demographics.csv lineage problem (that concerned the regional family only); policyengine-uk-data itself consumes the 2024-based national file. Later projection years land via the vintage backfill lane. Registers all four packages in SOURCE_PACKAGE_ALIASES (the drift guard that failed CI - unregistered packages/* fail loudly rather than being silently dropped from bundles, per PolicyEngine#78) and updates the year-2023 bundle expectations to the post-publish-raw truth measured by a full local bundle build: 42,895 facts (+3,676), 64 packages, 19 periods, ons by-source 4,922, 59 source tables, geography 1,065. Digit-text guard gap note: NPP year headers and single-age labels are text-typed digit cells the guard contract cannot express; anchored by the guarded Sex headers, the '110 and over' block ends, and the sha-pinned artifact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…PP age tokens (PolicyEngine#132) Review verdict was needs-fixes on one major: the earlier constants pass updated the headline bundle expectations but left four deeper assertion blocks at pre-PR values (by_period map, by_geography length, the country:K02000001 count, by_entity), making the test self-contradictory - it could never pass even after publish-raw. All four are now set to the post-publication truth and PROVEN by running the bundle test itself under the disclosed, reverted R2 sandbox: 1 passed (by_period gains calendar 2027-2029 at 214 each; K02000001 1,439 -> 2,907; geography 1,053 -> 1,065; person entity 3,688 -> 7,364). Also from review: the twelve NPP '105 - 109' rows are normalized to value_id {female,male}_105_109 with matching filters/constraints (the guard cells keep the publisher's raw '105 - 109' label); geography_name cosmetics (Yorkshire and the Humber; East of England) - guards pin the raw all-caps workbook strings and are untouched. Review confirmations worth recording: all 3,676 pins independently re-derived from artifact bytes with zero mismatches, and the NPP y2024 sex-by-age total equals the mye24 UK all-ages estimate to the person (69,281,437). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-raw (PolicyEngine#132) Uploaded via chronicle publish-raw with the account pinned by PolicyEngine#148; keys are content-addressed and byte-verified. Flips the wave-1 bundle contract from designed-red to green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
81999b0 to
7a57882
Compare
|
R2 publish is done for this branch. Sequence since the last push:
Same publish step will follow on #139/#141/#155 once their bases settle. |
…kages (PolicyEngine#133) Measured under the disclosed-and-reverted local storage.r2 sandbox (same protocol as PolicyEngine#137/PolicyEngine#139), where the merged-bundle test passes: 40,401 facts, 75 packages, 34 sources, 9 entities (dwelling is new), 84 periods, 1,067 geographies. All deep blocks (by_period, by_source, by_geography spot keys, by_entity, table count) re-measured together so the test converges with zero further commits once `ledger publish-raw --root db/data` runs on the branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Verified this against the artifacts rather than just reading the YAML: re-hashed both committed workbooks and uk.zip against the manifests (match), then opened them with openpyxl and checked the pinned cells.
Everything I checked is exact:
- MYE column span — header row 8 is
All agesat D,0at E …89at CP,90+at CQ, and CR is empty. The 92 record sets map 1:1 onto D..CQ with the guarded non-digit endpoints where you said they are. - MYE geography rows — per-row overrides are right, including E12000001–9 correctly carrying
geography_level: regioninside a record set whose default iscountry. - NPP —
Populationsheet, Females rows 2–108 / Males 109–215, C..H = 2024..2029,B215 = '110 and over'andB107/B214 = '105 - 109'guarded on both sexes. Ages are 0–104 contiguous plus105_109and110_plus, no gaps. - NPP year truncation to 2029 is documented in the header note with the backfill lane called out, which is the right call.
The digit-text header finding and the db/data/** lane-path gap are both real; agree they're contract-lane follow-ups, not blockers here.
England (E92000001) is missing from all three packages
mye23tablesuk.xlsx row 12 and mye24tablesuk.xlsx row 12 both publish E92000001 / ENGLAND / Country directly — 57,690,323 for mid-2023 and 58,620,101 for mid-2024. Neither MYE package ports it, and mye_2023_england_regions only carries the nine E12 rows, so England-level population isn't in the bundle at any level.
Skipping K03000001 (Great Britain) and K04000001 (England and Wales) is obviously right — they're roll-ups. But England is a constituent country and it's the one nation a UK build is most likely to ask for, and it sits in the same row block you're already reading. It also makes the mye_2023_uk_countries label — "UK and constituent countries" — inaccurate, since it's UK + three of four.
Is the omission deliberate (recoverable by summing the nine regions, so not worth the rows) or an oversight? If deliberate I'd still add it: Chronicle's job is publisher truth, and a consumer summing nine regions to get England is doing exactly the arithmetic this repo exists to avoid. Four extra rows across the two UK-countries packages.
provenance_class: census for the mid-year estimates
You justify it as census-anchored, mirroring census_pep, which is a fair precedent. But #154's own doc draws the line as "a national-balance-sheet estimate is model_output + observation" — and a mid-year estimate is the same shape: a cohort-component roll-forward of the last census with births, deaths and migration, asserted by ONS as an estimate. Under the new two-axes framing that reads as model_output + observation, with census reserved for enumerated counts.
Not blocking, and I'd rather not churn 92 record sets over it — but now that #154 makes the axes explicit it's worth settling the convention once, because it's about to be copied into every UK package in wave 2.
Coverage asymmetry worth knowing before the profile work
MYE is persons-only (MYE2 - Persons); NPP is sex × age. So observed UK population by sex isn't available at any period, while projected population by sex is. If the Microcosm UK dataset wants sex-split observed targets, the MYE2 - Females / MYE2 - Males sheets are sitting in the same workbooks — worth a note in #132 either way so it isn't discovered downstream.
Merge order with #139
Both PRs rewrite the same absolute constants in test_chronicle_bundle.py (fact_count 39219 → 42895 here, → 40131 there, plus geography_count / period_count / by_source). They'll conflict textually, and the second one through needs a genuine re-measure, not a hand-merge of the two numbers. Both are MERGEABLE / CLEAN right now. Let's merge this one first since #139's OBR lines are the ones that actually need #154's policy, then you re-run the bundle for #139.
Nice work — the cross-artifact coherence check (NPP 2024 sex×age total equalling the mid-2024 UK all-ages estimate to the person) is the kind of thing that makes this reviewable. England is the one I'd like resolved before merge; the rest can follow.
…target profiles Facts have always carried the axis (assertion: observation vs source_projection) through to every consumer row, but profile resolution ignored it - under latest_not_after_build_base_period a selector matching both MYE estimates and NPP projections would resolve a projection without anyone noticing (PolicyEngine#137 review). Profiles (and individual targets) now declare an assertion_policy: - observed_only (default): projections are invisible; a projection-only family fails loudly with only_projection_facts instead of resolving silently, and an older observation at a requested later period still demands an explicit period alignment. - prefer_observed: observed facts win even against a projection sitting at the requested period; projections fill only periods with no observation. - allow_source_projection: both compete under the period policy - for forecast families such as the OBR EFO lines. A target whose chronicle_selector names assertion explicitly bypasses the policy (the selector is already maximal intent - preserves the existing cbo.receipts selector pattern). Every projection resolution emits a resolved_from_projection warning and ResolvedTarget already exposes the fact's assertion, so consumers see the basis they got rather than guessing. Harness doc gains the two-axes section (provenance_class = how the publisher measured; assertion = whether the period had happened). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Four ONS population packages with a four-line registry change and no parser work — the harness earning its keep. The MYE/NPP split (2023 countries + England regions, 2024 UK, 2024-based projections) is a sensible slicing.
1. This will collide with the other three open UK waves on the golden-count test.
#137, #139, #141 and #155 all branch from the same base and all rewrite the absolute totals in tests/test_chronicle_bundle.py to different values:
| PR | fact_count |
source_package_count |
period_count |
|---|---|---|---|
| #137 | 42895 | 64 | 19 |
| #139 | 40131 | 69 | 61 |
| #155 | 39418 | 66 | 33 |
Each is right on its own branch and stale the moment a sibling merges. So this is four rebases with hand-recomputed constants, and the tempting way to resolve the conflict — take one side's number — produces a green test that has stopped checking anything.
No change needed here, but it's worth filing: per-package or per-source delta assertions would compose across parallel work, where one whole-bundle snapshot can't. Given four UK waves are in flight simultaneously this is costing real time right now.
2. The projection years are exactly the case #154 is about.
This PR adds calendar_year:2027/2028/2029 at 214 facts each, from ons/npp_2024_uk — national population projections. #154 (open) introduces the assertion: source_projection axis and makes observed_only the default resolution policy, calling out "NPP projection years" by name as the motivating example.
If #154 lands first, these facts should carry assertion: source_projection or every consumer profile will silently treat 2024-based projections as observations. If this lands first, they'll need a follow-up pass. Either way the two PRs should probably be sequenced deliberately rather than merged in whatever order review finishes — the NPP package is the one that most needs the new field.
Worth checking whether the 2026 jump (21 → 235) is also projection-side.
3. Minor
persondoubles (3688 → 7364) andby_source["ons"]goes 1246 → 4922 — a ~3700-fact increase from population tables looks right for MYE age×sex×geography detail, but since the test is the only thing pinning it, a sentence in the PR description about the expected magnitude would help the next reviewer.geography_countmoves 1053 → 1065, i.e. 12 new geographies for UK countries + English regions. That's 4 countries + 9 regions = 13, so one is presumably already present (K02000001?). Cheap to confirm.
Part of #132 — deliberately not
Closes: this PR delivers one of wave 1's three families. The issue's completion map:academic_year); successor PR#132 closes when the two remaining families land.
Lane:
ledger-source-ingestor(.github/ledger-agents.yml) — judgesledger-source-fidelity+ledger-boundary. Paths:packages/**plus thedb/data/**writes thefetch-artifactharness flow prescribes (note for maintainers: the ingestor lane'sallowed_pathslists neitherdb/data/**— where the canonicalfetch-artifactflow writes — norledger/source_package.py, which every package PR must touch for theSOURCE_PACKAGE_ALIASESdrift guard; both are candidates for the lane canon).What
Four source packages, 3,676 facts total, at publisher granularity (single year of age; the fixture's 10-year bands are a populace-side mapping concern):
ons-mye-2023-uk-countriesmye23tablesuk.xlsx(MYE2 persons)ons-mye-2023-england-regionsmye23tablesew.xlsx(MYE2 persons, 2023 LA-boundaries edition)ons-mye-2024-ukmye24tablesuk.xlsx(MYE2 persons; this edition carries the E12 rows directly) — sha256f78d50e8d77fe5ed9d04667d6875aefcf62b56380af7aceb2e90dce067981526ons-uk-population-projections-2024uk.zip→uk_ppp_machine_readable.xlsx(2024-based NPP principal) — sha256dea79703b9caddd560f1958b5a8ca12aa817b2396d244e6a2c775b1af7f48ab1source_projectionCross-artifact coherence check (from adversarial review): the NPP 2024 sex×age total equals the mye24 UK all-ages estimate to the person (69,281,437).
Shape: one record set per single year of age (
groupby_dimension: age; the all-ages sets are age-totals), rows carry per-row geography overrides, and age enters fact identity via row filters mirrored verbatim by explicit constraints (the consumer-contract comparator matchesunit/labelexactly). The YAMLs are machine-generated and uniform (10.9k + 21.4k lines); if a compact templated form is preferred, that belongs with the contract follow-up below.Sources (for review)
mye23tablesuk.xlsx— sha2567ee5724ecf71abc3d811c89174cdddf1bce7f07d3eefc8b69b15ee8371414b16mye23tablesew.xlsx— sha25696dcbb73d01c2965a0e8173bdf736f04a16d91dc189c4a52220919792e5e2c94Both artifacts were fetched twice by independent code paths (manual download, then
uv run ledger fetch-artifact) with byte-identical checksums;provenance_class: census(census-anchored official estimates, mirroring thecensus_pepprecedent).Source decision this PR asks reviewers to ratify
Issue #132 originally framed this family as projections (SNPP/NPP), inheriting policyengine-uk-data's claimed lineage for its committed
demographics.csv. Kickoff findings changed that:_age_80_89band is actually 80+ (a systematic −18% otherwise): median 0.76%, p90 2.14%, max 2.94% — the expected estimates-vs-projections gap, enumerated per-target in the populace consumption gate (Consume Ledger UK facts: uk_target_references, mapping module, and a compile-parity gate microcosm#622). Top deltas:south_east_age_10_19+2.94%,east_age_10_19+2.86%,west_midlands_age_10_19+2.46%.Projection packages (2022-based NPP
uk.zip, England 2022-based SNPP) remain queued for future-year calibrations, where actuals cannot exist — porting both sources is consistent with the exhaustive-vintage policy; populace picks the active source per calibration year.Gates
validate-package: clean for both packages, zero warnings.build-suite --year 2023: green end to end — source cells, source records, facts, DB build,agent_acceptance.jsonvalid with zero errors (only the standard non-fatalno_concept_alignmentswarning; Axiom alignment is a later step).test_source_files.py,test_ledger_source_cells.py,test_ledger_facts_only.py) pass.Draft because: the consumer-contract export requires
storage.r2(raw_r2_uri) in the manifests. The committed manifests honestly record checksums and retrieval metadata only; the suites were proven green under a locally injected, disclosed, and reverted R2 block. After source review, a credentialed maintainer runsuv run ledger publish-raw --root db/dataand the last gate closes with no package changes.Contract findings for maintainers
_render_valueint-coerces digit-string expectations while_resolve_guard_cellcompares raw — MYE2's text-typed0..89headers can't be expressed asexpected_column_header. The age span is pinned by the guarded non-digit endpoints (All ages,90+) plus the sha-pinned artifact. Follow-up contract-lane issue to be filed.ledger-agents.yml's ingestor lane omitsdb/data/**although the canonicalfetch-artifactflow writes there.🤖 Generated with Claude Code