Skip to content

Place each ACS rent draw on its own household's head - #1444

Merged
MaxGhenis merged 4 commits into
mainfrom
fix-acs-rent-head-alignment
Nov 2, 2026
Merged

MaxGhenis merged 4 commits into
mainfrom
fix-acs-rent-head-alignment

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Places each ACS rent draw on its own household's head. This changes no published data: rent changes only when a base is rebuilt, and that rebuild is a release decision (d2300).

The bug

impute_us_pre_subsidy_rent draws rent in household-table order (_recipient_head_features returns all_features.reindex(household_ids)) and writes it with person_rent[head_mask] = predicted, which takes person order. The two orders agree only when heads sit in ascending household id.

Evidence

On the dense file populace-us-2024-b5db384-8b0c1950b65f-20261024T083103Z, 168,852 base households:

Pairing Rented Owned with mortgage None
Rent on the household's own head, as published 40.8% carry rent 27.1% 25.1%
The n-th head's rent with the n-th household's tenure 100% 0% 0%

Only 9.15% of households sit at the same position in both orders. The default file and ACS-local donors carry the same draws. ACS rows get rent through the donor transfer, so they learned from misplaced rent and need a rebuild, not just these draws.

Impact

PolicyEngine US 2.90.2, 51 states, published weights held fixed. A rebuild can move these further, because calibration fit those weights to misplaced rent.

Measure Dense, as published Dense, corrected Default, as published Default, corrected
Modelled housing subsidy, 2024 $7.26bn $13.88bn $4.50bn $7.51bn
SPM units with a subsidy 1.23M 2.57M 0.68M 1.34M
SNAP, 2024 $89.43bn $90.14bn $99.63bn $100.46bn
Child SPM rate, 2025 18.19 17.49 17.17 16.81

The fix

Match each draw to its head by household id. housing_inputs.py is an attested kernel, so the PR also repins seed_map, seed_protocol and spec_sha256 with the sequence in tools/repin_us_spec_engine_contracts.py, and regenerates docs/evidence/spec-engine/us-f0-coverage.json. tools/generate_us_bundle_from_constants.py needs policyengine-us 2.2.1, which this checkout lacks, so CI's engine jobs check that step.

Invariants

For every person order and household size:

  • A head's rent equals the draw made for that household.
  • Non-heads hold zero rent.
  • Total rent equals total draws.

test_each_head_holds_its_households_draw_under_any_person_order states these with Hypothesis, and test_rent_reaches_its_own_household_when_person_order_differs checks placement by tenure through with_us_housing_inputs. Both fail on the old assignment.

Not in this PR

Frames that already carry rent keep it, because with_us_housing_inputs skips re-imputation when the signal gate passes, so the rebuild must start from rent-free frames. us_housing_inputs_signal_gate still ignores positive_rent_nonrenter; tightening it would block fiscal refresh on older bases, so it lands with the rebuild. Steps 1 and 2 of #1446 cover both.

Diagnosis: #1445.

axiom: n/a: data build imputation, no policy logic.

🤖 Generated with Claude Code

impute_us_pre_subsidy_rent drew rent in household-table order and wrote it
through a head mask in person-table order. The two orders differ on every US
base, so 91% of base households carry another household's rent, and rent lands
almost independently of tenure. Match draws to heads by household id instead,
and pin the behaviour with a test that reverses the person table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
housing_inputs.py is an attested US kernel, so the rent fix moves its
seed map, seed protocol and spec digests. This commit applies the
documented repin sequence and regenerates the coverage receipt.

The new property test gives each household a distinct draw, adds
multi-person households and shuffles the person table. It fails on the
old assignment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review November 1, 2026 23:54
MaxGhenis and others added 2 commits November 1, 2026 19:47
About 91% of households hold another household's draw, but many of those draws
are zero both ways. About 47% hold the wrong rent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merging on the standing gates at head 233f459.

  • CI: gh pr checks exits 0 at this head, and GitHub reports MERGEABLE and CLEAN.
  • Review: an independent Opus 5.5 review approved this head. It also confirmed that the repin and coverage checks exit 0.
  • Trial merge onto main at 312dc4d, 92 commits past this PR's base: no conflicts, and none of those commits touches this PR's files. On the merged tree, tools/repin_us_spec_engine_contracts.py --check exits 0, tools/spec_engine_coverage.py --check reports 42305/42305 configuration fields and 41/41 inventory checks, and the housing-input and spec-engine-loader tests pass (28).
  • Not in this merge: no rebuilt data. Max's d2300 covers the release of rebuilt files.
  • Release in progress: the booked Microcosm US release pins c0513db, which predates this fix. Its owner has that in writing.

Run the rebuild from a rent-free base, because with_us_housing_inputs returns early when the signal gate passes.

@MaxGhenis
MaxGhenis merged commit 9434c50 into main Nov 2, 2026
10 checks passed
MaxGhenis pushed a commit that referenced this pull request Nov 3, 2026
#1444 changes the attested kernel us_runtime/housing_inputs.py. Running the
documented sequence locally (repin --legacy-only, the bundle generator, the
repinner, the coverage tool) moved:

- seed_map and seed_protocol in inventory_coverage.py EXPECTED_HASHES, to
  main's values (07fe9086..., 4ff99232...), which the merge had already
  carried into the coverage evidence's map/protocol/implementation digests;
- the bundle spec_sha256, be81ebbc... -> 7d2aaa4d..., in
  test_us_multispine_pool_tool.py and both spec_sha256 fields of
  docs/evidence/spec-engine/us-f0-coverage.json.

Nothing else moved: source_stages.json, the bundle YAML, the field-usage
counts (42,489 configuration fields, 32,704 authored, 9,785 resolved), the
source_manifest, late_resource_semantics, tail, full_checkpoint and take_up
digests, and the resolved-reference and tail-control counts (337, 942) are
unchanged. The coverage tool reports 42489/42489 configuration fields and
41/41 inventory checks.

The repinner's first pass builds its inventory report against the stale seed
pins and wrote covered_item_count == 40 into test_spec_engine_coverage_tool.py;
a second pass restored 41, so that file is unchanged here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jmpqc2fSZAM219btF
MaxGhenis pushed a commit that referenced this pull request Nov 6, 2026
* Impute federal student loan balances from the 2025 SCF (#1073)

Add the scf_education_debt runtime module. It carries federal education
debt from the 2025 SCF through two weighted QRFs and allocates it to the
SCF-style reference person and spouse as four person inputs:
federal_student_loan_balance and federal_parent_plus_loan_balance
(policyengine-us#9721), is_income_based_repayment_new_borrower and
federal_student_loan_graduate_share (policyengine-us#9724).

- Only loans flagged federal count; X7179 is spread over loans 1-6 in
  proportion to their balances; whose-education code 3 is Parent PLUS
  on the reference person, codes 6, 7 and -7 the reference person's own.
- scf_wealth declares the two pinned 2025 files as artifacts 3 and 4
  with a new scf_2025 vintage. Net worth, liquid assets and auto loans
  stay on 2022.
- The fiscal-refresh tool runs the stage only with --scf-education-debt.
  The four columns join the stored-input register with the pending-
  engine reason the parent ids carry; the coverage manifest is left
  until the engine pin defines them.
- Regenerate sources, vintages and calibration YAML and re-pin the
  frozen source_stages.json and calibration projection digests.

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

* Add SCF 2025 education-debt fit receipts (#1073)

Weighted fit of the stage against SCF 2025: source checks against the
Fed's Alternate Table 13 (19.92%, $25,500, matched exactly), a five-fold
family hold-out with a predictor ablation, and the stage imputed onto
the published default populace-us-2024-spm-20260915 at its weights.

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

* WIP: SCF-matching recipient definitions and #1073 host-fit diagnostics

Codes the CPS recipient side the way the SCF defines it: married includes
cohabiting partners, children count the partner's children, and the
reference person follows the householder-couple rule. These move Parent
PLUS from 21.5% to about 16% of imputed federal balances (SCF 13.8%) but
leave the borrower-household share at about 14.2% (SCF 18.2%).

Adds diagnosis and experiment scripts and their first receipts. The
predictor-shift and remedy runs still need to run (on Modal; local memory
is too low); the fit receipt and PR body are not updated yet.

Part of #1073.

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

* Condition the education-debt QRFs on SCF-comparable predictors (#1073)

The first host fit drew federal education debt for 14.4% of CPS
households against 18.2% in SCF 2025, and gave Parent PLUS 21.5% of
balances against 13.8%, while the SCF hold-out reproduced the SCF. The
cause is covariate shift: the CPS host carries tax-record imputations of
interest and dividends and of Social Security and pensions, which put
42% and 18% of 25-34 households above zero against 16% and 6% of SCF
families. Mapping each to its SCF distribution at equal rank moves the
host's borrower share most of any predictor; all together reach the
hold-out level (receipts/1073-predictor-shift-diagnosis.json).

- SCF_EDUCATION_DEBT_CANDIDATE_PREDICTORS keeps every predictor both
  sides build; SCF_EDUCATION_DEBT_PREDICTORS, used by both QRFs, leaves
  out the two shifted income components, each with its reason in
  SCF_EDUCATION_DEBT_EXCLUDED_PREDICTORS.
- The recipient definition sums income over the reference person,
  spouse or partner and their children, the CPS counterpart of the SCF
  primary economic unit, on top of the SCF-matching codings.
- The stage spec drift check now covers both QRFs; the stage notes,
  module docstring and changelog say why.
- The receipt builder reports the build, the first receipt and the
  all-candidates setting side by side and records commit and run id.
- experiments/modal_1073.py runs the heavy steps on Modal, adds generate
  and spec steps, and collects every tracked file a run changed.

Re-pins the frozen source_stages.json digest; the generated mirrors are
regenerated on Modal in a follow-up commit.

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

* Regenerate the US stage spec for the SCF-comparable education-debt predictors (#1073)

Output of tools/generate_us_bundle_from_constants.py (run on Modal at
b9f3d67, run 20261020T154855Z-b9f3d6719): both education-debt QRFs drop
interest_dividend_income and social_security_pension_income, the stage
notes follow the module, and the stage-asset digest moves.

The Modal experiment runner now strips the Modal runtime's PYTHONPATH from
step environments; inheriting it put Modal's own packages beside the venv's
and the worker-identity check refused the duplicate idna.

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

* Make the spouse-routing test independent of draw chance (#1073)

test_a_spouse_draw_without_a_spouse_moves_to_the_reference_person checks
where a spouse's draw lands. With the synthetic donor's random incidence
and a 30-household frame, the SCF-comparable predictor set draws no debt
for any couple on 3 of 20 seeds (seed 11 among them), while the routing
assertion holds on all 20 for both predictor sets (Modal probe run
20261020T161732Z-5d047f687). Every donor family now borrows in this test.

The Modal runner gains --script for an uncommitted local probe, recorded
by sha256 in the run log.

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

* Collect the e35c4b0 receipts, add a Modal launch entrypoint, bind the loan register entries (#1073)

- experiments/1073-scf-education-debt-receipts.md and its JSON are the
  receipt of Modal run 20261021T133110Z-e35c4b0f9 at e35c4b0 (build,
  first-receipt and all-candidates settings on the published default).
- experiments/modal_1073.py gains `launch`, which spawns the run and
  exits, so a stopped local client cannot cancel it, and `--script` for
  an uncommitted probe.
- test_support/microcosm_data/stored_inputs.py lists the four federal
  student loan inputs among the producer-bound register entries, next to
  the parent ids: no file examined for #1026 stores them, and
  test_us_stored_input_register.py binds them to their live producer.

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

* Re-derive the spec-engine identity values the education-debt stage moves (#1073)

The scf_wealth education-debt declaration adds 145 configuration fields to
the US bundle (141 authored, 4 resolved). Modal probe run
20261021T145336Z-b1a461f19 printed every value below from the code; none
is derived by hand. It also diffed the field universe against origin/main:
all 159 added and 14 removed pointers belong to exactly one claim, and no
field is unclaimed or claimed twice.

- field_usage: 42,329 configuration / 32,545 authored / 9,784 resolved
  (was 42,184 / 32,404 / 9,780); claim pins source_stages 1,845 (the two
  2025 artifacts and six operations of scf_wealth), vintage_records 120
  and resolved_vintage_authorities 67 (scf_2025), calibration 316 (the four
  person outputs in the QRF tail-concentration list), each with its
  observed digest; ledger mode and generation-0 effect counts in the
  field-usage and coverage-tool tests.
- inventory_coverage: source_manifest, late_resource_semantics, tail and
  full_checkpoint digests; tail control fields 938 and resolved
  references 336.
- The pool tool's bundle spec_sha256 (a6f254a8...).
- experiments/modal_1073.py: the spec step first runs the bundle generator
  and the coverage attestation in --check mode.

The coverage tool's pointer-inventory digest and the regenerated
docs/evidence/spec-engine/us-f0-coverage.json follow in the next commit.

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

* Pin the coverage pointer inventory and regenerate the F0 attestation (#1073)

Modal probe run 20261021T150509Z-fcbd704c9, with the previous commit's
field-usage and inventory pins in place, printed the coverage tool's
pointer-inventory digest (37ce78d6...) and regenerated
docs/evidence/spec-engine/us-f0-coverage.json with tools/spec_engine_coverage.py;
--check then reported 42329/42329 configuration fields and 41/41 inventory
checks. The bundle spec_sha256 stays a6f254a8....

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

* Re-pin the inventory test's resolved-reference and tail-control counts (#1073)

test_support/microcosm_build/spec_engine_inventory_coverage.py keeps its own
copy of the reviewed inventory counts. Modal run 20261021T151444Z-05856e6ca
reported the two the stage moves, matching inventory_coverage.py and probe
run 20261021T145336Z-b1a461f19: resolved references 336 (the scf_2025
vintage) and tail control fields 938 (the four outputs in the QRF
tail-concentration list). Everything else in that run passed.

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

* Re-derive the merged tree's bundle digest and F0 attestation (#1073)

Modal run 20261021T183247Z-97bb3af92 regenerated the bundle mirrors on the
merge commit (no change) and re-derived every identity value there: the
field-usage counts and claim pins, the ledger counts, the inventory
digests and counts, and the pointer-inventory digest all match the
committed values. The only moved value is the bundle spec_sha256, now
90fa3260... (main's kernel changes plus this branch's stage), which the
pool tool pins. tools/spec_engine_coverage.py regenerated the attestation
and --check reported 42329/42329 configuration fields and 41/41 inventory
checks.

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

* Pin the #1073 fit receipt to the merged commit

Reran build_1073_scf_education_debt_receipt.py on Modal at 2a6e65a (run
20261022T153020Z-2a6e65a73), after the merge with main brought in #1332's
QRF change. Every fit figure is unchanged; only the receipt's commit, run
and date move.

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

* Describe #1073 as the student debt pilot in the changelog and module docstring

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

* Route loans for a child's education to co-resident adult children and swap X8000 codes on loans 4 and 6 (#1073)

Parent PLUS is drawn for itself before the own shares, the IBR new-borrower
share is labelled an upper bound, and the receipt benchmarks Parent PLUS and
borrowers against Federal Student Aid.

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

* Keep the education-debt stage operations within the sources schema (#1073)

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

* Re-derive the spec identity values for the child-loan routing (#1073)

Regenerated on Modal (run 20261023T143414Z-b93cc3ec0): sources.yaml, the
field-usage counts and source_stages claim pin, three inventory digests, the
coverage pointer-inventory digest, the bundle spec_sha256 and the F0
attestation.

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

* Rerun the #1073 receipts at the merged head and benchmark Parent PLUS against FSA

Modal run 20261023T152015Z-60255dd98 at 60255dd (tests, spec and receipt
steps all passed). Parent PLUS falls from 13.8% to 9.1% of SCF federal
balances and from 16.8% to 10.7% on the host; FSA's Direct Parent PLUS share
is 6.7%, a floor.

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

* Re-derive the spec identity values on the merge with main (#1073)

Regenerated on Modal (run 20261102T162227Z-3c4cfe441): sources.yaml, the
field-usage counts (42,489 configuration fields, 32,704 authored and 9,785
resolved), the source_stages and calibration claim pins, four inventory
digests (source_manifest, late_resource_semantics, tail, full_checkpoint),
the resolved-reference and tail-control counts (337, 942), the coverage
pointer-inventory digest, the calibration projection digest and the bundle
spec_sha256. The source_stages.json byte digest is the merged file's. The F0
attestation is from run 20261102T163748Z-c012b92c9 (write, then --check and
the generator's --check: 42,489/42,489 configuration fields, 41/41
inventory checks).

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

* Rerun the #1073 receipts on the merge with main

Modal run 20261102T172355Z-3297aefd8 at 3297aef. No reported number moved:
the fit JSON differs from the 60255dd receipt only in its provenance and in
the last digit or two of 14 floats (floating-point summation order).

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

* Re-derive the spec identity values on the merge with main (#1073)

#1444 changes the attested kernel us_runtime/housing_inputs.py. Running the
documented sequence locally (repin --legacy-only, the bundle generator, the
repinner, the coverage tool) moved:

- seed_map and seed_protocol in inventory_coverage.py EXPECTED_HASHES, to
  main's values (07fe9086..., 4ff99232...), which the merge had already
  carried into the coverage evidence's map/protocol/implementation digests;
- the bundle spec_sha256, be81ebbc... -> 7d2aaa4d..., in
  test_us_multispine_pool_tool.py and both spec_sha256 fields of
  docs/evidence/spec-engine/us-f0-coverage.json.

Nothing else moved: source_stages.json, the bundle YAML, the field-usage
counts (42,489 configuration fields, 32,704 authored, 9,785 resolved), the
source_manifest, late_resource_semantics, tail, full_checkpoint and take_up
digests, and the resolved-reference and tail-control counts (337, 942) are
unchanged. The coverage tool reports 42489/42489 configuration fields and
41/41 inventory checks.

The repinner's first pass builds its inventory report against the stale seed
pins and wrote covered_item_count == 40 into test_spec_engine_coverage_tool.py;
a second pass restored 41, so that file is unchanged here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jmpqc2fSZAM219btF

* Count #1073's module in the transport battery's binding identity (#1073)

engine-free failed on the merge ref with test_the_batterys_binding_identity_
adds_only_the_leaf_and_its_imports: assert 221 == (197 + 23). The battery's
binding closure reaches the us_runtime package, which imports
us_runtime.scf_education_debt (us_runtime/__init__.py:786), so the merged
tree holds one module more than main's base of 197. Diffing the closure on
main (220) and on the merge (221) shows that module as the only addition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jmpqc2fSZAM219btF

* Hold graduate child loans and the dependent limit to the loan rules (#1073)

A loan for a child's education that no listed child fits is Parent PLUS only
when it financed undergraduate study (34 CFR 685.200(c)(2)(i)); a graduate
one is now the head's own loan, as codes 6, 7 and -7 are. On the host, a
child share drawn for a household without an adult child splits the same
way: its graduate part joins the head's own balance, the rest is Parent PLUS.

The dependent undergraduate aggregate limit follows the loan year: $17,250
before July 1, 1993, $23,000 from then (Pub. L. 102-325) and $31,000 from July
1, 2008 (Pub. L. 110-227), with 1993 and 2008 loans counted as on or after
July 1. The docstring, stage notes and changelog say the over-the-limit rule
leans toward Parent PLUS, and the receipt sizes a $57,500 reading.

The loader checks local SCF files against the pinned member SHA-256s. The
receipt counts the loans each rule moves, the X8000 swap by loan position,
the host's moved child shares and the host's children-in-law. Citations:
20 U.S.C. 1087vv(d)(1), 34 CFR 685.209(b)(13)(ii), program 16.

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

* Rerun the #1073 receipts for the graduate-loan and dependent-limit rules

Modal run 20261103T172225Z-78928589e at 7892858. The receipt's loans-moved
table header is relabelled ("Rule") in the builder and in the committed note,
which otherwise is the run's output.

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

* Update the #1073 changelog figures to the new receipt

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

* Re-derive the spec identity values for the new stage notes (#1073)

The scf_wealth stage notes changed in 7892858, so the documented repin
sequence ran on Modal at that commit (run 20261103T172238Z-78928589e):
repin --legacy-only, the bundle generator, the repinner (twice), the
coverage tool, then each tool's --check, all passing. Moved: the frozen
source_stages.json digest (de488b42...), sources.yaml, the source_manifest,
late_resource_semantics and full_checkpoint inventory digests, the bundle
spec_sha256 (4fd75d58...) and the F0 attestation. The field-usage counts,
claim pins and inventory counts did not move. ruff format turns the
repinner's single-quoted literals, and 0caf490's two seed pins, into
double quotes; the values are unchanged.

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

* Re-derive the spec identity values on the merge with main (#1073)

Ran the documented sequence locally on the merge with main 31c8939:
repin --legacy-only, the bundle generator, the repinner, the coverage tool,
then the repinner once more (the second pass changed nothing). Moved:
- the field-usage counts: 42,394 configuration fields (was 42,489),
  32,609 authored (was 32,704); compiler_semantic 27,673,
  front_end_validation 349, no_generation0_effect 3,614. Resolved bindings
  (9,785), legacy_behavior (14,269 / 38,780) and identity_only (103) did not
  move. The counts are the merge base's (42,305) plus this branch's 184
  minus the 95 authored fields main dropped;
- the coverage pointer-inventory digest (f6832e8d...);
- the bundle spec_sha256 (ce8b1865...) in test_us_multispine_pool_tool.py and
  both spec_sha256 fields of us-f0-coverage.json;
- the F0 attestation's documentation_sha256 and claim_count (48: main's
  #1484 retired the catalog unit waiver and its claim).
Nothing else moved: source_stages.json and its frozen digest, sources.yaml,
and the inventory_coverage.py digests are unchanged. The merge commit kept
main's seed_map and seed_protocol, which the coverage tool accepts. The
coverage tool reports 42394/42394 configuration fields and 41/41 inventory
checks. ruff format turns the repinner's single-quoted literals into double
quotes; the values are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count the two modules #1342 adds to the transport bindings' identity (#1073)

The merged head's binding identity holds 223 modules: 200 without the
leaf's 23. This branch added us_runtime.scf_education_debt (198, b1218b0),
and main's #1342 adds microcosm.calibrate.hierarchy_labels and
microcosm.diagnostics.catalogs, which the bindings reach through
microcosm.calibrate's package __init__. Main fails the same assertion at
31c8939 (222 == 197 + 23); #1495, still open, counts them there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Sum an only child's loans against the dependent limit, and pin the boundaries (#1073)

Review of 2026-11-03 (second round):

- The dependent undergraduate limit caps a student's total. When exactly
  one listed child fits the family's federal undergraduate loans for a
  child, those loans are read year by year, oldest first: a year's loans
  within the limit on their own are the parent's once the child's total
  through that year would pass that year's limit, and the parent's loans
  do not count toward it. With two or more fitting children each loan is
  still compared with the limit alone, which leans toward the child; the
  docstring, stage notes and changelog say so, and the receipt counts
  those loans. _DEPENDENT_LIMIT_SUMS_AN_ONLY_CHILDS_LOANS switches the
  build reading off for the receipt's per-loan reading, and
  _scf_education_loan_holders returns what the rule read (_LoanHolders).
- Undergraduate means programs 10-12 and 16 (_UNDERGRADUATE_PROGRAM_CODES,
  checked against the 2025 codebook, which lists 10-16 and 0), so a loan
  with any other code is never parents-only.
- The moved-dollar columns compute part / sums * total
  (_part_of_drawn_total), so a subnormal share sum gives no NaN.
- The $46,000 pre-July-2008 limit for a dependent student whose parent
  cannot get PLUS joins the docstring, notes and changelog.

Tests: routing cases on the age-16 boundary (code 25, 2014, $9,000) and
the age-24 boundary (code 30, 2021, $40,000), and an unlisted program
code; test_the_dependent_limit_sums_an_only_fitting_childs_loans (two
$20,000 2016 loans against one $40,000 loan, oldest-first years, two
fitting children, the per-loan reading); the split test now draws shares
that sum to 0.6 and a non-new child loan for a new head, and asserts the
head's flag comes out False; a direct test of the subnormal guard.

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

* Count the summed-limit and pre-2008 loans in the #1073 receipt

- child_education_loan_moves counts and sizes the code-3 undergraduate
  loans, each within its year's limit, that the child's total through
  their year cannot take, by holder under the build reading and under a
  new per-loan reading (per_loan_limit, also in the readings table), and
  the code-3 undergraduate loans borrowed before 2008 that fit a listed
  child who could still have been dependent, by where they fall against
  their year's limit and $31,000.
- The reading for a parent who cannot get PLUS uses $46,000 before July
  2008 and $57,500 from then (limit_when_a_parent_cannot_get_plus).
- host_children_in_law also counts a PECOHAB partner of an adult child.
- The host draw check compares the four balance columns, placed on the
  build's people, as well as the weighted total.

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

* Rerun the #1073 receipts for the summed dependent limit

Modal run 20261104T190152Z-63776f2ce at 63776f2. Summing an only fitting
child's loans moves 27 pooled code-3 loans (5 in implicate 1; $3.79B,
0.38% of SCF federal balances) to Parent PLUS: the SCF's Parent PLUS share
goes from 8.99% to 9.36%. Of the 49 loans each within its year's limit but
over the child's remaining room, 22 stay the child's in families with two
or more fitting children. No code-3 undergraduate loan before 2008 fits a
dependent-age child. The refit second QRF puts the host's Parent PLUS share
at 10.7% (was 9.7%) and the hold-out's at 6.8% (was 6.5%); household
totals are unchanged. The repeated host draw matches the build in its
total and in all four balance columns. With PECOHAB partners, 0.97% of
host households have an adult child's spouse or partner present. The
changelog's figures follow the receipt.

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

* Re-derive the spec identity values for the summed-limit stage notes (#1073)

The scf_wealth stage notes changed in b4f9260, so the documented repin
sequence ran on Modal at 63776f2 (run 20261104T190206Z-63776f2ce): repin
--legacy-only, the bundle generator, the repinner (twice), the coverage
tool, ruff format on the changed files, then each tool's --check, all
passing (42,394/42,394 configuration fields, 41/41 inventory checks).
Moved: the frozen source_stages.json digest (098c90d5...), sources.yaml,
the source_manifest, late_resource_semantics and full_checkpoint inventory
digests, the bundle spec_sha256 (cd762197...) and the F0 attestation. The
field-usage counts, claim pins and inventory counts did not move. ruff
format turns the seed_map and seed_protocol pins' single quotes into double
quotes; the values are unchanged.

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

* Pin the summing rule's parts and note the Perkins lean (#1073)

Third review of #1246. test_the_summing_rule_for_an_only_fitting_child
pins nine parts of the summed dependent limit (oldest first, the parent's
years out of the total, every kept year in it, a loan over the limit out
of its year's group, a loan at the limit counted, private loans out,
pooled loans deciding how many children fit, and the 1993-2007 and
pre-1993 limits), and
test_over_the_total_skips_a_year_the_child_could_not_be_dependent pins
the dependency check in the running comparison. Both are the reviewer's.

The module docstring's within-limit line gains the exception for an only
fitting child whose total through that year exceeds that year's limit,
and its lean bullet, the stage notes and the changelog note that the SCF
lists Perkins loans among federal student loans while 34 CFR 685.203(e)
caps only Direct, Stafford and SLS loans. The receipt builder's docstring
says federal for the pre-2008 count and describes over_the_total as the
family's running total. The stage notes' spec identity values are
re-derived after the merge with main that follows.

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

* Re-derive the spec identity values after the merge with main (#1073)

The documented sequence in tools/repin_us_spec_engine_contracts.py, run
on Modal (20261105T212737Z-525506fef): repin --legacy-only, the bundle
generator, the repinner twice, the coverage tool, ruff format, then every
tool's --check. What moved:

- the frozen source_stages.json SHA-256 (the stage notes' Perkins line):
  sources.yaml, the generator, the test-support bundle and the core
  contract test;
- the inventory digests full_checkpoint, late_resource_semantics and
  source_manifest; seed_map and seed_protocol keep main's values,
  requoted;
- the bundle spec_sha256 in the multispine pool test;
- the F0 coverage attestation (42,394/42,394 fields, 41/41 checks).

The field counts and claim pins did not move.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@MaxGhenis
MaxGhenis deleted the fix-acs-rent-head-alignment branch November 6, 2026 10:56
MaxGhenis added a commit that referenced this pull request Nov 6, 2026
Populace US means to differ from Census where it fixes CPS errors, so the note
now measures differences from Table 18 and no longer treats them as misses:

- The title and opening say so, and name the question: separating intended
  corrections from noise and bugs. "Systematic error" becomes a systematic
  shift, and "state-specific error" becomes state-specific variation.
- "What would fix it" becomes "What we would change", and says a smaller gap
  alone does not make a better file.
- The rent sections now record what landed. #1444 fixed the assignment, #1529
  made the gate fail on rent outside renter households (the test the note named
  no longer exists), and no published file carries the fix yet. PolicyEngine/child-poverty-impact-dashboard#127
  has left draft.

No figure changes: a token comparison of the old and new text finds only
punctuation differences. ACS-local figures stay on hold for the fill-0 rerun.

Co-Authored-By: Claude Opus 5.5 <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.

1 participant