Repository navigation
Re-pin the US Chronicle feed to add Federal Student Aid's loan portfolio tables - #1506
Conversation
…ables Chronicle 41b2fb2b (tag microcosm-us-feed-1074-v1) is a9026cc8f, the #1345 pin, plus a cherry-pick of chronicle#347's 4448a7c96, which adds the six packages/ed_fsa Federal Student Aid loan portfolio packages. - us/chronicle_feed_scope.json: the cut session's scope, byte for byte (706 pairs: #1345's 640 plus 66 ed_fsa pairs at build year 2026; sha256 73a39c16...). - us/chronicle_feed.json: source_commit, build text, fact_row_count 40,642, facts_sha256 1d45a89c..., scope_sha256. consumer_fact_schema_versions stays ["chronicle.consumer_fact.v3"]: every row of the feed carries v3. consumer_fact_schema_sha256 stays bdb51e2a...: the v3 schema file has the same bytes at 41b2fb2b as at a9026cc. - Parity resources regenerated: the 11 ed_fsa feed families are off_by_default reviewed exclusions (a default compile binds none of the 461 facts; #1357's --scf-education-debt opt-in binds them). Still 32 compiled families; 64 reviewed exclusions; 93 feed families. - experiments/us-feed-ed-fsa/compare.json (#1345's compare_feeds.py, baseline 4d89e4a4...): every old row kept unchanged, 461 rows added, all ed_fsa; the default compile's 33,250 targets are unchanged. - The B4923C report chain test now accepts a later re-pin that adds rows from one declared source (added_rows.json), and nothing else. Part of #1074. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6KfC1gXpoY2QTYwvvy
A section for the re-pin in docs/us-chronicle-feed-repin.md (the cut, the 66 pairs, the comparison, why the stamp stays v3 and the schema digest stays bdb51e2a), the pin table and the feed's home path. Part of #1074. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6KfC1gXpoY2QTYwvvy
|
Ready for hub review Head: Inputs. All checks passed: the five notes files fetched at The re-pin.
Stamp readers on #1345's branch. All five checks read as the brief says:
I found no other code that reads the stamp. #1357's FSA code has no read either: its only Compare.
Report chain test. It used to refuse added rows. It now accepts rows added from one declared source, checked against a committed listing ( Default compile. No #1357 scratch merge (head
Tests on this branch, with the feed at
The only skips are 6 in Judgment calls. The body has the full list:
Generated by Claude Code |
…space The listing labels the row with the declared source, ed_fsa, but its record set is irs_soi.ty2023.table_1_4. _follow_later_repin_report takes the source from the listing alone, so it accepts the row; the case fails with "DID NOT RAISE" until the declaration also names a namespace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6FbAZXafYbq34jLAg3
Each entry of _LATER_ROW_KEEPING_REPIN_REPORTS now names the record-set
namespace its added rows must sit in ("ed_fsa." for the FSA re-pin), and
_follow_later_repin_report refuses a listed row whose record_set_id does
not start with it, after the anchor check. The test declares the namespace
rather than reading it from source_name, because a source's record sets
need not sit in a namespace of its name.
The outside-namespace case now passes; a no-namespace case covers a
declaration that leaves the namespace out. The listing-extra case's rows
now sit in ed_fsa. record sets, so it refuses only because the keys differ.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FbAZXafYbq34jLAg3
_follow_later_repin_report already refuses a listing made from another feed and a listing that repeats a key, but no case reached either check. Two cases now do; each refuses at its own assert, so they need no failing commit first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6FbAZXafYbq34jLAg3
…ports - The ed_fsa parity test's docstring said the families bind in a build with the balance columns. Only four of the 11 do (#1357's FSA_BOUND_TABLES); the portfolio summary and the six repayment-plan and IDR tables stay diagnostic in every build, and so do the loan-type table's recipient counts. The single namespace fence stays; the seven diagnostic families' own exclusions wait for #1357. - The re-pin doc said #1357's opt-in binds the ed_fsa facts; it binds 136 of the 461. Its paragraph on the anchor test now names the declared record-set namespace. - compare_feeds.py's docstring said both feeds must hold the same keys; the script reports keys_only_in_old and keys_only_in_new instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6FbAZXafYbq34jLAg3
|
Ready for hub review Head: Setup. I fetched the feed from Chronicle 1. The listing's record sets (finding 1): done.
2. The parity test's docstring (finding 2): done ( 3. The doc (finding 3): done ( 4. 5. The runs. Each file ran in its own process, with
The four new tests are
6. The PR body and the changelog. I added a "Review follow-ups" section at the end of the body and changed nothing else in it. The changelog fragment says nothing these fixes change, so it is unchanged. Judgment calls
Generated by Claude Code |
An independent review of 8982b8f found two code changes that left every test in TestAnchorFeedCheck passing: matching the namespace anywhere in a record set id instead of at its start, and moving the namespace check ahead of the anchor check. A mutation run over every check in _follow_later_repin_report found six more that left every later-report test passing: moving the namespace check ahead of the source check, and deleting the old-row-count check, the changed-field anchor check, the declared-source check, the source check or the added-row anchor check. In each, another check refused the case first, or no case reached the check. Every check in the helper now names itself in its assertion message, and each refusing case names the check that must refuse it, through pytest.raises(..., match=...). Three cases join them: a record set that holds ed_fsa. after its first component, an old feed of another size, and a label change in a B4923C record set. The helper's logic stays the same, so the new cases pass at once. Against the eight code changes above, each change now fails at least one case, both under pytest's assertion rewriting and with --assert=plain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of 8982b8f found the sentence awkward: microcosm#1357's student_loan_targets=True binds 136 of the 461 ed_fsa facts, and nothing else binds any. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I merged |
Three PRs reached main after this PR's last CI run, which tested 17e2643 on main 0f0e777: #1506, #1485 (H2) and #1501. None of them changes a file this PR changes, and git merge-tree writes tree 79ee630 with no conflict. H2 adds an NZ coresident binding and teaches test_support's reference concepts the coresident_adult transform, which the concept-mapping differential beside this PR's encode change reads, so this merge has CI test the two together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the merge base 875506e, main gained #1473, #1506, H2 (#1485), #1501, #1480 and #1488. The merge is clean. Two files changed on both sides, in separate hunks: compose.py (#1488 on main's side) and test_transport_evidence_kernels.py (H2 on main's side). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…losure (S2b PR 1) (#1482) * Add binding-local views to the Axiom adapter AxiomEngine takes an optional views sequence of AxiomView declarations: a logical engine entity no frame entity maps to (rulespec-be's Child), one bool person selector column, and an outside value per output. The adapter gathers the selected person rows in table order, runs the module's program on the logical entity, checks the engine's array against the selected count, and scatters it back through the same mask; every other row holds the outside value, cast through the output's graph dtype. An empty selection makes no engine call, and a null or non-bool selector refuses. View outputs resolve to the person entity, so callers that key on (binding entity, variable) read them unedited. Each view's program compiles under its own cache key, re-checks the pinned RuleSpec tree first, refuses dense relations, and contributes its derived metadata, so a Child-only module has metadata. The selector joins the person entity's relation_dependencies; view_root_inputs() reports each view's roots. axiom_engine_ref writes views only when present, so every existing reference is unchanged. The view code stays in axiom.py, because source_hash hashes defining files and does not follow imports. * Partition binding-local view roots in the input closure closure_partition no longer refuses a binding that declares views, and BINDING_LOCAL_VIEW_MESSAGE is gone. Per binding, each view's logical entity (Child) joins the live entities and maps to the person table, so a view's roots partition against the closure's Child entries and the existing frame check reads them on the person slice. Roots on that entity stay unmapped on every other binding, and unreached_roots still reads only the binding's engine entity, so it cannot excuse a view root. engine_root_inputs adds each view's roots under its logical entity through AxiomEngine.view_root_inputs(), and refuses a view binding on an injected adapter. binding_view_entities reads a binding's logical entities for the closure and compose. * Parse binding-local views in the registry and check them in compose The registry parses a binding's views block into AxiomView declarations (logical_entity, selector {column}, outside {name: value}) and builds the adapter with them. It refuses an empty list, views beside relations, a binding off the person entity, a logical entity that names a mapped engine entity, a repeated logical entity, a selector with any field but column, a structural selector, a selector that names a binding output, and an outside key that names no binding variable. Once the adapter is pinned, it checks each view's program for relations in place of the binding entity's program, requires each view's outside keys to equal the binding's variables on its logical entity, and refuses a view input named like a root input of the binding entity's program. _check_engine compares an injected adapter's views with the binding's and refuses views on an injected Python adapter. binding_view_dependencies reports each selector as a whole-table person slice, the same columns AxiomEngine.relation_dependencies reports to the rules kernels. Compose unions them with the relation dependencies for every rules node, refuses a view node whose person slice omits an input the closure encodes (other than through a graph node) or defaults on the view's logical entity, and requires each selector on the closure producer's person slice. * Pin NZ-shaped and relation engine references to goldens from the base No NZ CI test builds an axiom_engine_ref for NZ's rows: NZ's engine-free graph injects Python toy engines. The new golden holds references generated at main 4a36edc for NZ's six binding rows (unreached_roots included) over a synthetic tree, and for the registry and adapter relation fixtures, all built engine-free through build_transport_registry and axiom_engine_ref. The test compares them byte for byte, so a reference that gains "views": [] or "views": null without a declared view fails. A second test, gated on POPULACE_RULESPEC_NZ, builds NZ's six real references from the tree alone and compares them with a frozen copy the NZ hub writes at the base. * Add the changelog fragment for binding-local views * Test that every rules kernel requires a view's whole selector slice SimulateRulesKernel._require_relation_inputs reads the adapter's relation_dependencies, which now carry each view's selector, so a node whose person slice omits or masks the selector refuses before the engine runs. The test pins that path for a view adapter. * Pin view code to axiom.py and the no-view outputs to the base path The placement lint now walks every import node in axiom.py, relative and function-body imports included, and a second test requires AxiomView and every view helper to carry axiom.py's module name. A helper moved into a module axiom.py already imports (materialize, concept_mapping) would leave the simulate.* hashes unmoved by an edit to it; the module-name test refuses that move, which the import pin alone passes. P4 gains an output clause: with views=None, materialize returns the same names, order, dtypes and values, through the same engine calls, as a reference copy of main 4a36edc's no-view path kept in the test. NZ's CI graph injects toy engines and never runs AxiomEngine.materialize, so this is the executable guard on view-free outputs. * Class a view's defaulted roots as person columns in the support roster _support_roster mapped each defaulted closure entry to a frame entity through entity_names alone, so a defaulted Child root, which PR 2 writes on the person table, mapped to no entity and missed the closure_default class. _closure_defaults now maps a binding-local view's logical entity to the person entity for entries of that binding's module. Without views the mapping is unchanged. * Freeze NZ's real-tree engine references at the base test_nz_real_refs_match_their_frozen_copy compares NZ's six real references with a frozen copy, and wrote none without a rulespec-nz tree. This adds the copy, generated at PR 1's base (main 4a36edc) from a git archive of rulespec-nz 8dc2507d, the commit NZ's bindings pin. The base run froze the file, and a second base run passed against it. At this branch's head the test passes too, so the six references keep their bytes with views in place. The CI golden test passed at the base as well, which checks that axiom_engine_refs.json came from the base. * Document binding-local views in the declared-units guide The guide said binding-local views remained separate work. It now describes the declaration, the gather and scatter, the outside values, the registry's refusals, the selector slice, the closure's treatment of view roots, and the row-locality assumption that the skip-gated real-engine differential tests. * Union shared-path closure roots to remove binding-order dependence * Cover both logical views to catch dropped input roots * Validate complete outside declarations before pinning bindings * Preserve outside value types in view equality and hashing * Exercise view property edges across every graph dtype * Type and stabilize real Child differential inputs * Freeze CI reference bindings to isolate NZ edits * Clarify view evidence, serialization and selector refusals * Accept overrides of a view selector that a program reads The first review of the fix round (P2) found that 0d6201a's selector refusal also refused an override of a selector that a Person program reads as a root input. The registry accepts that shape, variables() lists the selector, and before 0d6201a the counterfactual bridge applied such an override. validate_input_override now names a selector only when no program reads it, which is exactly when the bridge refuses the override as outside the engine's inputs, so the check changes no override the bridge applies. test_a_selector_that_a_program_reads_stays_overridable drives the counterfactual bridge with a Person program that reads is_child: the override reaches that program's input and the rows the view selects. It fails on 0d6201a's check. The refusal test now also runs through the bridge, which calls no program. The guide states the rule. * Correct the two docstrings that review B flagged `_real_inputs` said the pinned engine refuses a wrong-typed input by name. At axiom-rules-engine 753ce7d, `src/dense.rs` reports a bool, text or date column as a type mismatch when a program needs a number from it ("expected decimal-compatible dense column" for an arithmetic operand, "{side} side of comparison is not numeric" for a comparison), and neither message names the input. The docstring now says that. `axiom_ref_goldens.py`'s module docstring wrote `"\n"` in a non-raw string, so it rendered a line break where the regeneration recipe needs the two characters. It now escapes the backslash. Two merges of main bring this branch up to date: `02265bb5` merges main `875506e8`, and `36800b14` merges main `3e7a7045` (#1473, #1506, H2 #1485, #1501, #1480 and #1488). git merged both without conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main re-pinned the US feed to Chronicle 41b2fb2b (#1506) and the NZ feed onto chronicle.consumer_fact.v5 (#1507). The one conflict is in docs/us-chronicle-feed-repin.md, where #1506's new re-pin section and this branch's section on re-pinning onto a newer row contract sit in the same place: both are kept, the general section first. #1506's sentence saying the US loader carries a row's stamp unchecked now adds that the release and target-parity tools check the stamps against the pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…folio (#1074) (#1357) * 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> * Calibrate the SCF-imputed student loan balances to Federal Student Aid portfolio facts (#1074) A new runtime module, fsa_student_loan_targets, binds Federal Student Aid's portfolio tables (the six packages/ed_fsa packages of chronicle#347) to the SCF education-debt stage's person columns, federal_student_loan_balance and federal_parent_plus_loan_balance: the balance and borrower count of each state and DC, of five borrower age bands and of nine debt-size bands, and the Parent PLUS balance beside the balance of every other loan type. Every family binds at FY2026 Q2 (31 March 2026), the latest quarter all four bound tables publish, as a stock unaged at the 2024 build period (target aging's new declared stock_at_source_reference_date alignment). Not Reported is allocated to the reported locations, Puerto Rico and Other leave with their share, and the age, debt-size and loan-type rows bind as shares of the one in-scope control ($1,679.1B, 44.31M borrowers). Repayment plans, the portfolio summary and loan-type recipient counts stay diagnostic, each with its reason in the receipt. compile_us_fiscal_target_registry binds the facts only with student_loan_targets, which the fiscal-refresh release tool sets with --scf-education-debt; the release materializes the rows from the stored person columns, guards their restated age and balance bands, records every fact's disposition in us_source_coverage.json and keeps the opt-in family off the target-parity contract. Default compiles and releases are unchanged. The ED Federal Student Aid provider joins the calibration label registries. experiments/fit_1074_fsa_calibration.py refits the published default with and without the family (plus L2-anchored arms) on Modal through experiments/modal_1074.py, against the pinned feed plus chronicle#347's FSA rows, now built at its merge commit on Chronicle main. Squashes #1357's earlier history onto #1246's review-round head. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Refit the #1074 receipt on #1246's child-loan routing and the merged chronicle#347 (#1074) Modal run 20261023T191527Z-4b6bfce9a at 4b6bfce. chronicle#347's export at its merge commit (acbedb5) is byte-identical to the PR head's (sha256 58774e11...), so the candidate feed and the test fixture are unchanged. With loans for a child's education on co-resident adult children, the host gains young borrowers (1,246 borrower records under 25, from 561) and its Parent PLUS share falls from 16.8% to 10.7%, but holds the same borrowers (21.2M) and borrower households (16.4%) at the published weights. Arm B now meets 95% of the FSA targets within 5% (92% before) and every age band; B + L2 78% (76%). Borrower households' share of weight after calibration is 33.7% (B) and 32.3% (B + L2), against 33.0% and 31.5% before and 18.2% of SCF families: the routing moves balances within households, so it does not shrink the doubling. Non-FSA capped error: B 3.62% (3.66%), B + L2 3.85% (3.83%), A unchanged at 3.30%. 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> * Add down-weighted borrower-count arms to the #1074 fit (#1074) Six arms bind the 67 FSA balance targets at full loss weight and the 65 borrower-count targets (state, age band, debt-size band) at 0, 0.1 and 0.25 of their weight, renormalized to mean 1, with and without the L2 anchor. At 0 the counts only validate. The existing arms are unchanged and the receipt keeps their tables; a new section compares the arms on the FSA balance and count fit, the non-FSA surface, the share of households with debt, total borrowers, the Parent PLUS share and the borrower age profile against FSA. The receipt's reference-date sentence now says what the stored balances are: the March 2026 stock written at the 2024 build period, which policyengine-us#10415 (merged 2026-10-30) holds at its stored value in every year. The runner and fit docstrings say the candidate keeps the 51aa40f feed, the pin before main's cf9ece2 re-pin, which adds only the B4923C facts that bind no target. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Refit the #1074 receipt with the down-weighted borrower-count arms (#1074) Modal run 20261102T174003Z-88d7a950d at 88d7a95. The merge with #1246's updated head moved nothing: all 4,363 numbers of the existing arms are identical to the 4b6bfce receipt. Binding the balances with the borrower counts at 0.25, 0.1 and 0 of their weight lowers the share of households with debt from 33.7% to 32.7%, 31.7% and 30.2% (B + L2: 32.3% to 28.2%, 26.7% and 25.7%; SCF 18.2%), and the borrowers from 44.26M to 42.98M, 41.71M and 39.75M (B + L2: 42.67M to 34.11M at 0). Balance targets within 5%: 99% in B, 96% in each down-weighted arm (B + L2 94%, 88% to 87%); count targets within 5%: 92% to 43%, 31% and 17%. The solver reaches the balances with larger balances per borrower, so the bands under $10K fall short. Non-FSA capped error 3.45% to 3.50% (B 3.62%, A 3.30%); Parent PLUS 6.8% to 6.9% throughout. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Restamp the FSA fixture v3 and refuse a debt build with no FSA facts (#1074) The fixture's 244 rows now carry chronicle.consumer_fact.v3, as every row of the feed microcosm#1506 pins (Chronicle 41b2fb2b) does, so each matches one of that feed's FSA rows byte for byte; the stamp is the only change. The release tool refuses a --scf-education-debt build on a feed with no ed_fsa facts before the compile (_check_us_feed_carries_fsa_facts): the plan was empty there, so the build compiled no FSA target and raised nothing. The us_source_coverage.json receipt reuses the check's plan. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Keep FSA loan-type rows diagnostic and refuse changed FSA tables Both loan-type dollar rows leave the loss, so the family binds 130 targets. Originating a Direct Consolidation Loan discharges the loans it consolidates (34 CFR 685.220(a)), so FSA's Parent PLUS row covers only unconsolidated loans, while the stage's column holds every loan for a child's education. Every other family pins the total, so a target on either side of FSA's split would set the same split. The plan refuses a location, age or debt-size table whose rows differ from the set FSA publishes at the reference quarter. The feed-refusal test reads _main's syntax tree, and two Hypothesis properties pin the family totals and the band partition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse repeated FSA location rows and restate the Parent PLUS reason _require_reviewed_rows compared sets of row IDs, so a repeated Puerto Rico or Other row passed and changed the location control. It now compares sorted lists and names any repeat, and a new test repeats each of those two rows. FSA's notes don't say whether its Parent PLUS row leaves out loans the Secretary paid off through consolidation (34 CFR 685.220(f)), so the docstring and the loan-type reason no longer say it does. Every other family pins the total, so either loan-type row would still constrain the split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Count the FSA targets module in the transport bindings' identity us_runtime.fiscal_targets imports the new fsa_student_loan_targets module, so the transport bindings' identity reaches one more module. test_the_batterys_binding_identity_adds_only_the_leaf_and_its_imports names it in BINDING_MODULES_ADDED_AFTER_BASE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Ready the FSA fit for the 130-target rerun: L2 grid, income tables, release fit table (#1074) Max's review of 2026-11-05, on top of #1525's diagnostic loan-type rows: - The fit runs the review's grid: A and B at lambda 0, record L2 0.03 and chi-square L2 0.03 (calibrate's l2_basis), and B with the borrower counts at x0, at lambda 0 and record 0.03. --extra-chi-square-lambda adds A and B at another lambda. Every receipt table covers every arm. - It reads FSA's Parent PLUS figure from the diagnostic loan-type facts, writes strict JSON (null, allow_nan=False) and refuses a feed that binds no FSA target. - The receipt adds every household-income quintile for every arm, at each arm's own cut points and at the release arm's applied to every arm. - A --scf-education-debt release writes us_scf_education_debt_calibrated_fit.json, the stage's fit table on the exported frame at its shipped weights, before post-export scoring; a test pins the write and its place in _main. - A missing age counts in the youngest band (FSA_MISSING_AGE), now named, documented and tested. - The doc and changelog cite 34 CFR 685.220(f) and give FSA's filing of a consolidated Parent PLUS loan as an inference, say the materializer groups the stored, unaged age, and drop the re-pin as a future step (#1506). - The Modal runner builds the feed the commit pins by default and gains fit_smoke and lint steps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Tidy the FSA receipt's signs and the missing-age note (#1074) The receipt writes a flat Q5 - Q1 as +0.0, not -0.0, and a negative income as -$945,807. The FSA_MISSING_AGE comment says the plan, not FSA, spreads the "not reported" ages, and the doc's Parent PLUS share reads as a share of the loan-type dollar rows. The fit_smoke run 20261105T214517Z-66b37d37c showed both signs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Limit the 1074 runner's format check to this PR's files (#1074) Run 20261105T215640Z-f83241311 passed ruff check and every test, and its format check flagged only eleven us_runtime modules this PR leaves untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say the FSA loan-type table is diagnostic in the parity manifest (#1074) families' reviewed-exclusion reason no longer lists the loan-type table among the tables a --scf-education-debt build binds. The reason changes in the builder and in the eleven manifest entries alike. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Refit the #1074 receipt on main: 130 targets and the L2 grid (#1074) Modal run 20261106T142002Z-8a9d335a7 at 8a9d335, on the feed main pins (Chronicle 41b2fb2b, sha256 1d45a89c..., 40,642 rows, rebuilt as pinned): 6,102 non-FSA and 130 FSA targets, none dropped, every arm of the review's grid solved. B binds 95% of the FSA targets within 5% at ESS 2,932 and 33.9% of households with debt; B + record L2 76% at 4,484 and 32.4%; B + chi-square L2 59% at 6,222 and 28.0%. Chi-square 0.03 moves the fit, so no second lambda. The doc's measured limits take the new levels. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Write the calibrated SCF fit after the export cleanup (#1074) test_us_post_export_scoring executes _main's export and cleanup statements on their own, without args, so the calibrated fit-table write inside that span failed it with a NameError. The write now follows the ladder cleanup, still before any scoring or terminal gate, and the builder test pins that order. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Describe the Parent PLUS column as the stage's classification and drop the "moved" clause (#1074) Since #1246 the stage gives a loan for a child's education to a fitting co-resident adult child, keeps it as the head's Parent PLUS only past the dependent limit or when no child fits an undergraduate loan, and makes a graduate loan with no fitting child the head's own. The module docstring, the loan-type reason and the doc now say the column holds the balances the stage classifies as Parent PLUS. The calibrated fit table reports shares at the shipped weights, not the FSA family's effect, so the doc and the release tool's comment no longer say it reports what the family moved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Max Ghenis <mghenis@gmail.com>
Part of #1074. This branch started from #1345's head (
ea4606d6b9), and #1345 merged into main on November 4, so this PR now targets main.Re-pins the US Chronicle feed so it carries Federal Student Aid's loan portfolio tables, which #1357 binds as opt-in calibration targets. Every row of #1345's feed stays the same, the re-pin adds 461
ed_fsarows, and the default compile's 33,250 targets don't change.The Chronicle cut
41b2fb2b864a28c0eda03322b77a97ef147dfa3c, on Chronicle branchfeed/us-a9026cc-ed-fsa-1103, tagmicrocosm-us-feed-1074-v1. It cherry-picks chronicle#347's commit4448a7c96onto #1345's pina9026cc8f(tagmicrocosm-us-feed-1343-v1) and changes nothing else. Chronicle main doesn't contain the cut. Chronicle#347's commit also brings a Census ACS S1501 state package, which the scope leaves out. A separate session cut the commit and built the feed (Chronicle branchhub-notes/ed-fsa-feed-cut-1103atbf3456f9,notes/ed-fsa-feed-cut-1103/). This PR uses that session's feed and scope, after these checks:1d45a89c28177a95371b4cc4068d3657c7bf12bd8edad7c82fe25bf453f9ea07schema_versionchronicle.consumer_fact.v3on all 40,642 rows73a39c166a8f78c8a8a1ae711515e5d2e6b76a74952e67d7783cf974a46156334d89e4a498ffd73a407821d9ce2c45837072901e0a29acf3206160d1fafd7c14(#1345's feed)What changes
us/chronicle_feed_scope.json: the notes' scope, byte for byte. It has 706 pairs: Re-pin the US Chronicle feed so three dimension values carry one label each #1345's 640 plus 66ed_fsapairs, all at build year 2026, over 72 package runs. No microcosm test needed a different order or format.us/chronicle_feed.json:source_commit;buildtext;fact_row_count40,642;facts_sha2561d45a89c…;scope_sha25673a39c16….consumer_fact_schema_versionsstays["chronicle.consumer_fact.v3"]andconsumer_fact_schema_sha256staysbdb51e2a…(see below).target_parity_manifest.jsonandtarget_parity_feed_families.json, regenerated:consumer_facts_us_41b2fb2.jsonl) and its digest;ed_fsa.*families;tools/build_us_target_parity_manifest.pydeclares those 11off_by_defaultreviewed exclusions, under one fence for theed_fsanamespace;Tests:
test_us_chronicle_feed.py:_CHRONICLE_COMMIT,_FACTS_SHA256, the row count, the scope's pair and run counts, and the exclusion count._SCHEMA_SHA256and the v3 schema-versions assertion stay the same and still hold;test_release_target_parity.py: the exclusion count, plus a test that the manifest lists all 11ed_fsafamilies asoff_by_defaultreviewed exclusions behind the microcosm#1074 fence;test_us_esi_premiums.py: the report chain, below.experiments/us-feed-ed-fsa/:compare.json, plusadded_rows.jsonand the script that writes it (list_added_rows.py).docs/us-chronicle-feed-repin.md: a new section and the pin table. Changelog fragment:changelog.d/1074-us-feed-fsa-loan-portfolio.added.md.Compare
experiments/us-feed-agi-age-labels/compare_feeds.py(#1345's script; follow-up 4 corrects its docstring) ran with #1345's feed as the old feed and the notes' feed as the new one, and wroteexperiments/us-feed-ed-fsa/compare.json. The script compiled both feeds at 2024, as the release does.Rows kept: all 40,181.
compare.jsonlists nokeys_only_in_old, counts 0changed_rowsand lists nochanged_fields, so every old row keeps its key, values and labels.Rows added: 461, all from source
ed_fsa. They match theed_fsarows of the notes'ed_fsa_rows.jsonlone for one:dl_portfolio_by_repayment_plan_fy2026_q3portfolio_by_age_fy2026_q2portfolio_by_debt_size_fy2026_q2portfolio_by_loan_type_fy2026_q3portfolio_by_location_fy2026_q2portfolio_summary_fy2026_q3Targets:
b8d571e91c22on both;national_statesurface: 6,102 targets,3aa2a0830e7d, on both;Feed label variants:
age_20andage_21, as before.Memory: the compile step finished without running out of memory, and the whole script took about 2 minutes.
Why the schema versions stay v3
chronicle.consumer_fact.v3. At the cut, the exporter writes every row's stamp from one constant,CONSUMER_FACT_SCHEMA_VERSION. That constant takes its value from the last entry ofCONSUMER_FACT_ROW_CONTRACTS(chronicle/consumer_contract.py:35,chronicle/epoch.py:171-176), and the tuple ends at v3. Both files match byte for byte ata9026ccand41b2fb2b; I read them through raw.githubusercontent.com.consumer_fact_schema_sha256as the sha256 ofpolicyengine_chronicle/schemas/consumer_fact.v3.schema.json(policyengine_chronicle/schema.py:59-70). That file hashes tobdb51e2a…at both commits, so the digest doesn't change from Re-pin the US Chronicle feed so three dimension values carry one label each #1345's._load_fact_rowscarries a row's stamp without gating on it (ledger_artifact.py:464-471);fact_schema_versionsonly reports the stamps (:207-219);consumer_fact_schema_versionsholds a list of distinct, non-empty strings (us_runtime/chronicle_feed.py:136-143);expected_facts_sha256, which checks only the file's hash;validate_chronicle_feed_pin(ledger_artifact.py:257-291) would compare the list with the rows' stamps (:276,:287). It runs only on the transport kernels and the BE and NZ reference tools, never on the US release path.fsa_student_loan_targets.py,ledger_targets.py,target_aging.py, Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357's diff tofiscal_targets.pyand the release tool, and its FSA test file, all at0ff293b1a. The grep findsschema_versiononly in the FSA receipt's own version (fsa_student_loan_targets.py:441) and a target profile's (ledger_targets.py:756-765).So the pin lists only v3. #1357's body expected v4 FSA rows and a pin listing both versions; that would need Chronicle's
0ec21e5in the cut, which would restamp every row v4.The report chain test
test_the_committed_feed_pin_banks_the_anchorfollows the committed re-pin reports from #454's feed to the pin. Before this PR it refused any report that added rows (keys_only_in_new == [],old_rows == new_rows), so this re-pin would have failed it. This PR moves the per-report check into a helper,_follow_later_repin_report:_LATER_ROW_KEEPING_REPIN_REPORTSnames a committed listing of the added rows (added_rows.json), one source and one record-set namespace ("ed_fsa."here). The listing must name the same keys the report adds, each from that source, outside the B4923C anchor record sets and in a record set whose id starts with the namespace.new_rowsmust equalold_rowsplus the number of added keys.record_set_spec_hash, and leave the anchor record sets alone.added_rows.json.compare.jsondoesn't record where an added row comes from, solist_added_rows.pyreads the added keys back out of the new feed, with theirsource.source_name, record set and period.ed_fsa.after its first component;Default compile
On this branch, which doesn't have #1357's code, the default compile turns no
ed_fsafact into a target. I ran:compile_us_fiscal_target_registry(age_targets=True, packaged CD crosswalk)andapply_us_medicaid_enrollment_substitutionson the new feed;_check_committed_us_ledger_feed_pin,_check_us_esi_anchor_against_feed(no mismatch),assert_target_parity_manifest_current(registry=…)andus_release_target_parity_gate.It gives 33,250 targets in 32 families, none of them from
ed_fsa, and the parity checks pass against the regenerated resources. The pinned-feed parity tests (test_committed_artifacts_match_regeneration,test_gate_passes_on_real_compiled_registry) ran rather than skipped and pass. So the compiled families stay at 32.Checking against #1357 (scratch branch, not pushed)
I merged #1357's head,
0ff293b1a335a29d46f1598f2039beffa242e632, into a local scratch branch off this PR.Merge. It conflicts in seven files:
docs/evidence/spec-engine/us-f0-coverage.json;spec_engine/field_usage.pyandspec_engine/inventory_coverage.py;test_spec_engine_coverage_tool.py,test_spec_engine_field_usage.pyandtest_us_multispine_pool_tool.py;tools/spec_engine_coverage.py.git merge-treegives the same seven conflicts for Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357 against main alone, and against Re-pin the US Chronicle feed so three dimension values carry one label each #1345, so they come from Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357's stack against main, not from this re-pin. Neither this PR nor Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357's FSA code touches any of the seven. In the scratch branch I resolved all seven to our side.Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357's FSA tests. I ran its FSA test file,
test_us_fsa_student_loan_targets.py: 25 passed. The selector-k 'fsa or student_loan or ed_fsa'matches no test names in Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357's other edited test files, because its edits sit inside existing tests. So I ran those files whole in scratch:test_provider_labels4 passed;test_variable_labels3 passed;test_us_spine_blindness504 passed;test_us_fiscal_targets174 passed;test_us_fiscal_refresh_builder,test_restated_concept_rules_all_have_a_comparison: 1 passed.This PR's three changed test files also pass in scratch: 28, 39 and 159.
Compile with
student_loan_targets=Trueagainst this pin and the notes' feed, which I loaded with the pin's digest and checked with_check_committed_us_ledger_feed_pin:33,382 targets, 132 FSA: 67 balance (65
total_balance, 1parent_plus_balance, 1balance_excluding_parent_plus) and 65borrowers;the FSA receipt reads status
bound, reference period 2026-03, over 461 FSA facts;its dispositions match Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357's table, and the compile binds the other 136 facts:
_without_opt_in_student_loan_targetstakes the registry back to 33,250 targets, andassert_target_parity_manifest_currentand the parity gate pass on it;with the flag off, the scratch compile also gives 33,250 targets and no FSA target.
Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357's fixture (
us_fsa_portfolio_chronicle_347_facts.jsonl.gz, sha2569faed0e3…, 244 rows, all v4):aggregate_fact_keywith a notes row, differ from it only inschema_version, and match it byte for byte once the notes row reads v4.In scratch I restamped the fixture's 244 rows v3, and they then match the notes' rows byte for byte. On the restamped fixture:
Tests at
c624e6f(with the feed in place)I put the feed at
~/PolicyEngine/_buildh-runtime/inputs/consumer_facts_us_41b2fb2.jsonl, wheretools/build_us_target_parity_manifest.pyand the pinned-feed fixtures look for it, and pointedMICROCOSM_US_CHRONICLE_FACTSat it. Each file ran in its own process (-n 4,-p no:cacheprovider). Onlytest_us_fiscal_refresh_builder.pyskipped any tests, and its 6 skips need thepolicyengine-usextra (:55), not the feed:test_us_chronicle_feed.pytest_release_target_parity.pytest_us_esi_premiums.pytest_us_fiscal_targets.pytest_us_soi_congressional_district_vintage.pytest_us_soi_state_agi_bands.pytest_us_medicare_irmaa_holdout.pytest_us_acs_local_sparse_cd.pytest_us_release_gate_preflight.pytest_us_spec_bundle.pytest_us_acs_local_release_tool.pytest_us_fiscal_refresh_builder.pyruff check .passes andtools/ci_test_plan.py verifyreportsok. No test I ran says it peaks near 15 GB. CI covers the full suites.Merge order
--scf-education-debt(student_loan_targets=True); a default build compiles none of them.#1357 has to resolve the seven spec-engine conflicts against main whatever the order. This PR already takes the step #1357's body lists for the re-pin: the generator declares the FSA families reviewed exclusions, and
_without_opt_in_student_loan_targetskeeps them off the parity contract.Judgment calls
I regenerated the parity resources. Since no
ed_fsafact compiles by default, the resources could have stayed the same, but then CI would fail:test_parity_resources_and_generator_restate_the_pin(test_us_chronicle_feed.py:120-132) requires both files to restate the pin'sfacts_sha256;_exclusion_for), so it can't run on this feed without one.No family's status changed: 32 compiled before and after. The generator classifies the 11 new families
off_by_default, the classificationTargetFamilydocuments. This PR takes the third re-pin step in Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357's body. Without the fence this PR can't pass CI, so the fence can't wait for Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357.The
ed_fsafence's origin says this re-pin did not search the retired us-data pipeline for a student loan target.DEFAULT_FEED_NAMEreadsconsumer_facts_us_41b2fb2.jsonl, following Re-pin the US Chronicle feed so three dimension values carry one label each #1345's naming.The chain test. I left the logic of Re-pin the US Chronicle feed so three dimension values carry one label each #1345's
compare_feeds.pyalone; follow-up 4 corrects only its docstring. The added rows' sources come from a separate listing,added_rows.json.The pin's
buildtext still sayschronicle build-consumer-artifactrefuses the bare feed "at this commit". I didn't run that command at41b2fb2b. The v3 schema file hasn't changed, and the feed holds the same 994 rows the doc gives as the reason (theconcept_alignmentrows with noauthority: 515 fromcms_medicaid, 468 fromcensus_acs, 11 fromjct).What the doc takes from the cut session's notes. It attributes three things to them, which I did not rerun: the
acbedb5export,0ec21e5, and the 217 FSA rows the fixture lacks.Branch. No microcosm branch
us-feed-ed-fsaorus-feed-ed-fsa-1103existed, so I createdus-feed-ed-fsa-1103.Review follow-ups
An independent review of
c624e6fapproved this PR with four follow-ups. Commits395d1cc..8982b8fmake them:_LATER_ROW_KEEPING_REPIN_REPORTSnow names the namespace its added rows must sit in ("ed_fsa."here)._follow_later_repin_reportrefuses a listed row whoserecord_set_iddoesn't start with it, after the anchor check. Before this, a listing could label anirs_soirowed_fsaand pass.395d1cccommits that case failing, andaf45400makes it pass. Ano-namespacecase covers a declaration that leaves the namespace out.8353e34adds cases for two checks that no case reached: a listing of another feed, and a listing that repeats a key. Both already refused.ed_fsafamilies bind under Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357's opt-in: location, age, debt size and loan type. The portfolio summary and the six repayment-plan and IDR tables stay diagnostic in every build, and so do the loan-type table's recipient counts. The single namespace fence stays. Giving the seven diagnostic families their own exclusions waits for Calibrate federal student loan balances to Federal Student Aid's portfolio (#1074) #1357, which owns their reasons.student_loan_targets=Truebinds 136 of the 461ed_fsafacts. The doc's paragraph on the anchor test now names the namespace too.compare_feeds.py's docstring says what the script does. It reportskeys_only_in_oldandkeys_only_in_new; it doesn't require both feeds to hold the same keys.With the feed in place at
8982b8f,test_us_esi_premiumspasses 163 (159 before, plus four new cases),test_release_target_parity39 andtest_us_chronicle_feed28. The follow-ups don't touch the pin, the scope, the feed, the parity resources or the changelog fragment.A second independent review approved
8982b8fand found two code changes that left the later-report tests passing: matching the namespace anywhere in a record set id instead of at its start, and moving the namespace check ahead of the anchor check. A mutation run over every check in_follow_later_repin_reportfound six more:1f25420gives every check in the helper a name in its assertion message and has each refusing case name the check that must refuse it, throughpytest.raises(..., match=...). It adds three cases: a record set that holdsed_fsa.after its first component, an old feed of another size, and a label change in an anchor record set. The helper's logic stays the same, so the new cases pass at once. Each of the eight changes now fails at least one case, under pytest's assertion rewriting and with--assert=plain.212dd72rewords the doc's sentence on the 136 facts.A third independent review approved
212dd72and found two more gaps in the cases. Each anchor case holds one record set, so a check that read only the first would still refuse it. Each refusing count exceeds the expected count and each refusing digest sorts after the expected one, so a check that compared with<=would still refuse them all. The helper reads every record set and compares with==; #1513 adds cases for both gaps.🤖 Generated with Claude Code