Skip to content

Add the frs_relationships UK spine stage and rebind the ONS household-composition cells (#791) - #903

Merged
juaristi22 merged 4 commits into
mainfrom
uk-household-composition-791
Sep 11, 2026
Merged

juaristi22 merged 4 commits into
mainfrom
uk-household-composition-791

Conversation

@juaristi22

@juaristi22 juaristi22 commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #791. Plan of record: María's local repos/uk-791-relationship-to-head-plan.md (approved 2026-09-11; plans are kept outside the tree by her convention); receipts R1–R11 in experiments/791-household-composition-receipts.md.

What changes

Spine. A new stage frs_relationships (after age_tail; manifest 30 → 31 entries, executed roster 28 → 29) re-reads the pinned FRS 2024-25 adult, child and househol tabs and derives four frame columns from the household grid (relhrp, R01–R14, hrpid, hrpnum): person.relationship_to_head, person.ons_family_role, person.ons_family_index, household.ons_household_type. The household column is valued with Chronicle's ons-families-households-2025 ons.household_type value ids verbatim, so the frame value, the manifest declaration, the contract row and the published fact are one string. The derivation follows the ONS Families and households definitions (family = couple, or lone parent with a child; a child of any age with no partner or own child belongs to the parent's family; dependent = under 16 or 16–18 in full-time education; one family plus other individuals is one-family; no family present = two or more unrelated adults; two or more families = multi-family). Codes and rules are declared as a schema-validated derive_ons_household_composition manifest operation whose parameters the runtime lockstep-asserts (the age_tail pattern), with the FRS SN 9563 / ONS / Chronicle provenance recorded beside each list.

Gates. uk_stage_frs_relationships_composition (stage health at the assembled boundary: one HEAD per household agreeing with hrpid and hrpnum, contiguous person numbers, no double partners, no unmapped codes, family-index and enum consistency, the ten-cell partition covering every household exactly once, grid reciprocity within the reviewed tolerance of 3 ordered pairs on the 2024-25 tape) and uk_ons_household_type_enum_domain; both in the spine scope and mirrored in the data contract. The four columns are on the export allowlist.

Contract and registers. All ten ons.household_composition.* rows bind on household.ons_household_type == <value id> (one reduce-less condition each; notes rewritten with the translation record); the three microcosm#791 measure exclusions are retired (register 51 → 48; the 2026-08-26 tranche 39 → 36); a compile-time check refuses any ons_household_type condition outside the declared domain or a partition that does not cover it once; the census-households bridge is bound and the local adjudication wording, the uk-data parity register and the cross-grain doctrine note say so.

Executor fix (found by this work). A new dense column on a filtered population (a sampled rung) was widened to float64 because the executor's placeholder was built on a fresh RangeIndex and inserted label-aligned; the placeholder now binds to the table's index, with a regression test.

Re-adjudication of the register (the three retired entries)

  • Tape probe (R2): every invariant clean on all 16,288 households; the ten cells within 25 % of the published 2025 values at design weights before any calibration (multi-family 0.87, unrelated adults 0.73, the rest 0.91–1.10).
  • Full licensed build spine-r (R4): 29 stages, 17/17 spine gates, both new gates passed with the R2 counts.
  • Twin diff vs spine-q (R5): only the four candidate-only columns; no value, row, index, weight or attribute moves.
  • Parity instrument (R6): verdict and register usage identical to spine-q (the pinned reference's pre-existing 20 unsigned differences, Make the UK eFRS parity re-pin one reviewed command; hold every identity mirror in lockstep (companion to #747) #749); the four columns are outside the instrument.
  • Derivation delta (R7): the old bindings were not a partition (6,563 households in two or more cells); multi-family 18.8× → 0.74, unrelated adults 4.0× → 0.62, lone parent non-dependent 2.7× → 0.88 of published, at design weights.
  • National calibration (R8): 367 targets, final loss 0.0114, 0 targets outside the 25 % fence, 6/6 terminal gates; the ten cells within ±0.9 % (the three retired cells within ±0.13 %); the +21.9 % collateral on lone_parent_dependent_children seen on the Q50f run is +0.21 % here.
  • Rowwise dry run (R9): unbound_bridges == []; the composition bridge binds both lower grains with declared factor 1.0 and zero shift, so the 1,011 census cells are numerically unchanged.

Review round 1 (Vahid, at 7779747)

Should-fix 1 is in (dated re-wording trailer on the census_disclosure_control_noise adjudication). Should-fix 2: the plan of record lives outside the tree by María's convention (her local repos/uk-791-relationship-to-head-plan.md); the receipts carry what the tree needs of it. The skip-generation age switch and the ONS source for "two or more unrelated adults" are now on the contract notes and in R1/R11. age NaN refuses instead of reading as 0; stage_health.py carries only the composition hunks. The cached-reference regeneration test in test_uk_parity_reference.py is retired (María's call): it regenerated the reference under the installed engine, was skipped in CI, and has failed on main since the 2.97.0 lock; the other thirteen reference integrity tests stay.

Follow-ups (not in this PR)

🤖 Generated with Claude Code

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Review at 7779747b (Claude Code, high effort; worktree checkout; without the engine, the tape or the pinned feed: 709 passed, 9 skipped, 0 failed across the sixteen touched and neighbouring suites; ruff check and ruff format --check clean on all 29 changed files; both census tools, the coverage manifest, the spec-engine coverage check and ci_test_groups --verify current; CI green on every lane that has finished. Draft, so a comment rather than a review.)

The derivation is right. I ran derive_frs_relationships directly on seventeen synthetic households built from the FRS grid codes — single under and over 65, married, cohabiting, civil-partner and same-sex couples, couples with dependent, non-dependent and three-plus children, lone parents with dependent and adult children, two three-generation households (grandparent head with a lone-parent daughter and grandchild → one family with the grandparent as other individual; grandparent couple with a partnered child and grandchild → multi-family), grandparent with grandchild and no parent, unrelated adults, siblings, couple plus lodger, a step-child through the cohabitee, a child whose parent is not the head, a one-sided parent link, and a 17-year-old in education with her own baby. Every one lands in the cell the ONS Families and households definitions give, and I checked those definitions against the 2025 bulletin glossary. Partners come from the R-grid only with relhrp used solely for the head relationship, own-child detection is column-any so the parent need not be the head, and 16–18 in full-time education is child-tab membership or the adult fted/educft flag with age 19+ non-dependent regardless.

Registration is lockstep: sources.yaml and source_stages.json both 30 → 31 with the stage in the same position, _STAGE_CONSUMES and _STAGE_CELLS declared, the spine fixture 40 → 41 nodes with only downstream input keys moving, and the policy, gates, spine and release-cut digests and the UK spec sha re-pinned. The ten value ids are one string across runtime tuple, manifest, contract conditions and ledger selectors. The three #791 exclusions are gone (51 → 48, tranche 39 → 36), the ten rows bind on one condition each with the compile-time partition check wired in, and the #900 fence is replaced honestly — a partial bridge stays unbound with reviewed records, a full one reconciles with unbound_bridges == []. The published 2025 values in the receipts match the parity fixture exactly. The gate fails closed on every malformed grid I could think of and the transform refuses first. The executor dtype fix is real and tested.

Should-fix

1. The adjudication register is re-worded under an unchanged signature. local_binding_adjudications.json:85 changes the census_disclosure_control_noise reason from "today it is reviewed-unbound" to "bound from microcosm#791 (2026-09-11)…", but approved_by, approved_on: 2026-08-31 and the "re-worded 2026-09-10 (#887 round 1)" trailer are untouched. You did the right thing on #900 by dating the re-wording; do the same here. The census JSON and local_target_census.py mirrors regenerate from it.

2. The plan of record is not in the tree. Body and receipts cite repos/uk-791-relationship-to-head-plan.md (approved 2026-09-11); there is no repos/ directory at this head. Commit it or point at where it lives.

Questions

  • Skip-generation households are age-dependent by inheritance from FRS coding. Codes 15/16 never form a family, so a grandparent raising a grandchild under 16 with no parent present is unrelated_adult_households — consistent with Families and households, where family means couple or lone parent with child (the "grandparent plus grandchild is a family" rule is Census 2021's). But receipts R1 records that the FRS codes a 16–19 student grandchild's grandparents as guardian (7/3), so the same household becomes lone_parent_dependent_children once the child turns 16. The unrelated_adult_households contract note lists skip-generation there unconditionally; one sentence on the age switch would save the next reader.
  • The note also says ONS counts siblings, foster and skip-generation under "two or more unrelated adults". The bulletin does not define that category and the methodology page 404s, so the R1 quote is the only support I can reach. Not disputing it; flagging it as unverifiable from here.

Nits

  • _evaluate_enum_domain falls back to the engine variable when the enum-domain artifact is absent; only the spine driver supplies it, and the spine scope is evaluated only there, so it is latent rather than live.
  • The H2 parity fixture's synthetic tabs emit only head plus optional child, so the couple and multi-family paths are covered by unit tests, not by the parity oracle.
  • age NaN coerces to 0 → dependent at frs_relationships.py:398; unreachable today because age is validated upstream, but a fillna(0) that silently biases is worth a refusal instead.
  • About 60% of the stage_health.py diff is reformatting of unrelated gates, which buries the composition-gate hunk.

#904 overlap

Both drafts sit on the same base. A trial merge auto-resolves five of the seven shared files and conflicts on two: test_uk_measure_simulation.py (48/36 here vs 50/38 there, different census markers removed) and the UK spec sha in test_spec_engine_country_bundles.py. Whichever lands second needs count 47, tranche 35, both markers dropped and a third sha — the auto-merge would hide that. This one is self-contained and small; I would land it first and rebase #904.

Could not verify

The R2–R9 tape, build and calibration numbers, the educft versus fted tape column, the three-pair reciprocity count, and the Chronicle value ids against Chronicle itself (checked against the ledger selectors generated from the pinned feed).


Nothing blocks. 1 and 2 before this leaves draft; the rest at your discretion. The derivation itself I am satisfied with.

juaristi22 added a commit that referenced this pull request Sep 11, 2026
…regeneration test (#791)

Should-fixes: the census_disclosure_control_noise adjudication reason carries a
dated re-wording trailer for the bound bridge beside the #887 one; the plan of
record is committed as docs/uk-relationship-to-head-plan-791.md and the
receipts and PR body point at it. Questions: the unrelated-adults and
lone-parent contract notes record the skip-generation age switch (under 16
with no parent present: two unrelated adults; 16-19 in full-time education:
the FRS codes the grandparents as legal guardian, so a family with a dependent
child) and cite the ONS "Families and households statistics explained"
definition by URL. Nits: a non-numeric age refuses instead of reading as 0;
stage_health.py is the main text plus the composition hunks only.

Retire test_cached_reference_regeneration_matches_committed_surface (Maria's
call): it regenerated the pinned eFRS parity reference under the installed
engine, was skipped in CI and has failed on main since the 2.97.0 lock; the
other thirteen reference integrity tests stay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Thanks, all addressed at b4ab81cf (fixes at 12462fb0, plan pointer at b4ab81cf).

Should-fix 1: the census_disclosure_control_noise adjudication reason now carries a dated re-wording trailer, "re-worded 2026-09-11 (microcosm#791 …)", beside the #887 one, under the unchanged 2026-08-31 approval.

Should-fix 2: the plan of record lives outside the tree by María's convention (her local repos/uk-791-relationship-to-head-plan.md); the receipts carry what the tree needs of it, and the PR body and experiments/791-household-composition-receipts.md now say so rather than citing a path that is not in the repo.

Your skip-generation question is on the unrelated_adult_households and lone_parent_dependent_children_households contract notes: under 16 with no parent present the household is two unrelated adults (codes 15/16 never form a family); a 16–19 grandchild in full-time education is coded by the FRS as the grandparents' legal dependent (7/3), so the same household becomes a family with a dependent child. The "two or more unrelated adults" definition is cited by URL from ONS's "Families and households statistics explained" (2021), which is the source R1 quotes.

Nits: a non-numeric age now refuses (FRSRelationshipsError, test added) instead of reading as 0; stage_health.py is the main text plus the two composition hunks only. The enum-domain artifact fallback and the head-plus-child-only H2 fixture stay as you noted; the couple and multi-family paths are held by the unit tests.

The cached-reference regeneration test in test_uk_parity_reference.py is retired on María's call: it regenerated the reference under the installed engine, was skipped in CI, and has failed on main since the 2.97.0 lock; the other thirteen reference integrity tests stay, including the cached-artifact sha check. Landing order as you suggest: this one first, #904 re-pins the counts and the spec sha.

🤖 Posted with Claude Code

juaristi22 and others added 4 commits September 11, 2026 18:04
…-composition cells (#791)

The FRS household grid (relhrp, R01-R14, hrpid, hrpnum) becomes four exported
frame columns - person.relationship_to_head, person.ons_family_role,
person.ons_family_index and household.ons_household_type - derived per the
ONS Families and households definitions in a new stage after age_tail. The
household type carries Chronicle's ons.household_type value ids verbatim, so
the frame, the manifest, the contract rows and the published facts share one
string. The derivation is declared as a schema-validated
derive_ons_household_composition manifest operation that the runtime
lockstep-asserts, gated at the assembled boundary by a stage-health check and
an enum-domain gate, and mirrored into the data contract's part scopes and
digests.

All ten ons.household_composition.* rows bind on the new column and the three
microcosm#791 measure exclusions are retired (register 51 -> 48), which binds
the national_household_composition_partition_vs_census_households bridge. A
compile-time check refuses any ons_household_type condition outside the
declared domain or a partition that does not cover it once.

Found and fixed on the way: the graph executor's placeholder for a new dense
column on a filtered population (a sampled rung) was built on a fresh
RangeIndex and inserted label-aligned, widening int64 to float64; it now binds
to the table's index, with a regression test.

Receipts in experiments/791-household-composition-receipts.md: tape probe
clean on all 16,288 households; full licensed build 17/17 spine gates; twin
diff vs spine-q = only the four columns; calibration 367 targets, loss 0.0114,
0 outside the 25% fence, the ten cells within +/-0.9%; rowwise dry run binds
the bridge with declared factor 1.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 7779747)
…regeneration test (#791)

Should-fixes: the census_disclosure_control_noise adjudication reason carries a
dated re-wording trailer for the bound bridge beside the #887 one; the plan of
record is committed as docs/uk-relationship-to-head-plan-791.md and the
receipts and PR body point at it. Questions: the unrelated-adults and
lone-parent contract notes record the skip-generation age switch (under 16
with no parent present: two unrelated adults; 16-19 in full-time education:
the FRS codes the grandparents as legal guardian, so a family with a dependent
child) and cite the ONS "Families and households statistics explained"
definition by URL. Nits: a non-numeric age refuses instead of reading as 0;
stage_health.py is the main text plus the composition hunks only.

Retire test_cached_reference_regeneration_matches_committed_surface (Maria's
call): it regenerated the pinned eFRS parity reference under the installed
engine, was skipped in CI and has failed on main since the 2.97.0 lock; the
other thirteen reference integrity tests stay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 12462fb)
…eceipts point at her local copy

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit b4ab81c)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22
juaristi22 force-pushed the uk-household-composition-791 branch from b4ab81c to 48b1566 Compare September 11, 2026 16:22
@juaristi22
juaristi22 marked this pull request as ready for review September 11, 2026 16:27
@juaristi22
juaristi22 merged commit 3094bfe into main Sep 11, 2026
23 checks passed
MaxGhenis added a commit that referenced this pull request Sep 11, 2026
…mark the stamp as in-process

Two attested identities moved with the merge ref, both re-pinned to the
values the merged tree computes: the UK country bundle (main's #903 added
the frs_relationships spine stage on top of this branch's attested kernel
changes, so neither side's pin survives the merge), and the multispine
pool tool's constants-adapter country_spec digest, which had already moved
to the branch's attested value recorded in docs/evidence/spec-engine/
us-f0-coverage.json when the seed identities were re-pinned but was still
asserted at main's value in test_us_multispine_pool_tool.py.

survey_atomic_geography._population_stamp now says it is an in-process
seal that must never be persisted or compared across reconstructions
(microcosm#907 / #909), naming what a cross-call pin has to carry instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Sep 14, 2026
…egenerate the surfaces (#904)

María merged chronicle #263 (closing #261): consumer facts now carry
dimension_labels and dimension_value_labels as chronicle.consumer_fact.v2,
the schema-8 hierarchy's missing input. The UK artifact rebuilt at 474a0ae
carries 141,400 rows (the c6f9361 rows plus chronicle #259/#260's nine
Universal Credit packages), every row labelled.

- uk/chronicle_feed.json pins the new artifact (facts bb12d77a, manifest
  c649b7fe, consumer-fact schema 6a42e4a5, rows chronicle.consumer_fact.v2).
- The national generator runs again on main's hierarchy code: references
  and membership regenerate byte-identical to round 2 apart from the feed
  label (424 active / 7 deferred / 7 signed out, no value or period moves)
  and the generator's hierarchy lookups equal the hand-completed ones.
- The nine vendored resources regenerate (rows unchanged, headers move);
  the local census restates the pin; the compile-parity receipts are
  unchanged; compile_uk_target_registry compiles 424 / 0 unsupported.
- All 109,693 local-level facts are identical between the two artifacts,
  so the local surface moves only by its membership feed label. The local
  generator and the local runtime compile stay refused on main and here:
  Chronicle emits no geography.name and Microcosm's catalog has no
  constituency or local-authority labels (receipts Part I); that is the
  local grain's own follow-up.
- The seam-driver test reads the pin's facts digest instead of a literal;
  the local validation-level register restates the new pin.
- Baseline (receipts Part J): spine-p cannot host the post-#903 composition
  rows, so round 3 ran on #903's spine-r twin; every #890 row inside the
  25% bound, the battery blocked on the HMRC self-employment 20-30k band
  cell (+25.8%, +24.1% on #903's own spine-r run), María's ruling.
- Filed chronicle#266 (geography.name) and microcosm#920 (local grain
  blocked since #855) on María's go.

Rebased onto main 15ebde8 first (only the spec-digest conflict).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Sep 14, 2026
Merge origin/uk-full-build-graph-registration (051fb97) into the #893
integration head (2300e56) after taking origin/uk-context-framing-fix-20260913
(072c2e8, amendments 25 and 26).

Resolution rules: the graph package, its tests, the charter and the interface
lock are #918's; #901's own draft of WeightUpdate and the frame-context fields,
its test_weight_update.py and its charter section are dropped. US-owned tests
and tools/spec_seed_identity_diagnostics.py are #893's; #901's 10 September
review fixes to them (and the us_spine_stage_contracts.json fixture and the
VerifiedParameterAssets test) are superseded. UK files #901 rewrote into shims
or cut down are #901's. Stage roster is 29: #901's 28 plus frs_relationships
from #903; the retained-leaves / SPI pair stays retired.

Ports of HEAD-side UK changes into #901's modules: #903's frs_relationships
transform in uk_runtime/spine_build.py and its ons_household_type enum-domain
gate artifact through one shared uk_spine_gate_artifacts() used by the
spine-phase gate kernel, the full-build final gates and the driver test; #855's
hierarchy decode in graph_targets.registry_from_payload and on the local
target surface (_surface_records encodes it as TargetRegistry.to_json does,
_local_specs decodes it and keeps it out of metadata), so the schema-8
hierarchy the shared uk_local_target_surface now emits survives the surface
artifact and reaches diagnostics. Invented UK test registries take their
hierarchy from one shared tests/uk_hierarchy_fixtures.py helper.

Dropped run_graph(population_retention="lazy") from full_build_cli: the shared
executor on main has no such mode (see the #893 reconciliation record).

Pins regenerated on the merged tree: calibrate H1 parity, the H2 uk_spine.json
fixture, the UK country-bundle spec digest, the UK release-input coverage
manifest, APPROVED_UV_LOCK_SHA256 / LOCK_SHA256 for the merged uv.lock, and the
microcosm.graph/kernel.py contract in the US implementation inventory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Sep 14, 2026
The calibration-comparison evidence discloses its register vintage (the
a256c88 register: 47 current entries plus the three #903 retired, no
band-H, hence 374 and 545 targets; 377 and 542 under HEAD) and drops the
stale fuel-duty sentence (#904 retired that exclusion; the row sits at
-23.9% inside the bound). The runbook names the constants' home in
microcosm.calibrate.geography_constants. A geography predicate must project
to the reference's own entity, refused otherwise, so a hand-edited register
cannot misalign a mask silently. The three 2026-09-14 exclusions are
asserted by name, date, tracking and window. ruff format is clean on every
touched file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Sep 16, 2026
Receipts Part B records the measurement design: the four refusals of the
baseline attempts (stale ladder, pre-#903 spine, the cross-grain
collision fixed in 97ee7d5, the unset gate signing key) and the ruling
to drop the separate baseline and measure the family once at 1,500
epochs against the D2 figures of the 10 September report.

Part D records the run on 96e6204 (dense K=15, seed 42, spine-r, feed
df35af7): council tax authority cells within 10 % 92.2 % -> 99.6 %, cells
past 25 % 33 -> 5 (three band-H micro-cells, one Western Isles band A,
one Derbyshire Dales band G residue), England bias by band -0.2 % to
+0.1 % where every band was negative, the four band-G micro-cells and
the inner-London A-D misses resolved, Wales 99.5 % and Scotland 98.0 %
within 10 %, the 97 council-tax controls of the joint solve within
3.7 %, the other local families unchanged; uk_local_target_fit failing
targets 53 -> 24 (council tax 33 -> 5), uk_local_weight_ratio fails as
before; obr.council_tax -11.0 % -> -12.8 % (the CT-C level gap, open on
#929); the frozen-vs-recomputed VOA artifact is gone.

The vendored incumbent registry fixture relabels its 90
voa/council_tax/<REGION>/<band> rows onto the mhclg and welshgov ids
that registry_parity.mapped declares (values untouched; the compile
parity registers are byte-identical), so the incumbent-surface
evaluation names the family that now owns the rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UK spine: relationship-to-head frame column, so the ONS household-composition split is measurable

2 participants