Skip to content

Accept a restated age-band Ledger filter only where it agrees with the compiled bounds - #986

Merged
MaxGhenis merged 4 commits into
mainfrom
us-restated-age-band-filters
Sep 23, 2026
Merged

MaxGhenis merged 4 commits into
mainfrom
us-restated-age-band-filters

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #969 under Max's ruling (2), 2026-09-22: "add the age-band rule as a follow-up PR". This PR is stacked on us-labelled-filter-support (#969, head 01dc806f4), so the diff shows only this rule. GitHub retargets it to main when #969 merges.

Problem

On #969's head, the fatal guard _assert_supported_ledger_filter_metadata still refuses 939 of 32,866 compiled targets on the pinned Chronicle feed. Those are 936 census_population/population_age targets and 3 ssa/ssa_ssi_age_band_recipients targets. The refusals come from ledger_filter_age_lower_bound / ledger_filter_age_upper_bound, which ledger_targets._constraint_bound_filters stamps from each fact's age constraint rows. As a result, any build that compiles age targets stops. REPORT.md section 7 describes this.

What the materializer does (read this session at 01dc806f4)

  • _population_age_household_values (tools/build_us_fiscal_refresh_release.py:4461-4471) and the age-banded policyengine_variable branch (:4958-4995, used by the SSA SSI by-age counts) both apply lower <= age < upper. They read age_lower_bound / age_upper_bound through _as_bound.

  • The compiled bounds come from us_runtime/fiscal_targets.py::_age_bounds (:3199). It takes the value of any >/>= row as the lower edge and any </<= row as the upper edge, so it drops the operator. The SSA path rejects any operator other than >=/< at compile time. population_age has no such check.

  • ledger_targets.py::_constraint_bound_filters (:3473-3510) keeps the operator in the stamped key:

    • >= becomes age_lower_bound
    • < becomes age_upper_bound
    • > becomes age_lower_bound_exclusive
    • <= becomes age_upper_bound_inclusive

    It stamps nothing for a variable that is already a dimension.

The rule (age_band, a third entry in RESTATED_LEDGER_FILTER_CONCEPTS)

A restated ledger_filter_age_{lower,upper}_bound is accepted only when both of these hold:

  • It parses to the same number as the compiled age_{lower,upper}_bound on its own side.
  • The spec's materializer is one that applies the band: population_age or policyengine_variable (RESTATED_AGE_BAND_MATERIALIZERS).

The rule is refused, and the refusal names the values, when any of these is true:

  • the edges disagree: ledger_filter_age_upper_bound=9 disagrees with age_upper_bound=10
  • the compiled counterpart is missing
  • the key restates an exact age (ledger_filter_age=<v>), which the materializer has no filter for
  • the spec uses any other materializer, such as the irs_soi slice or a spec with no materializer
  • the spec carries ledger_filter_age at all, so age is a dimension. In that case none of the age rows were stamped, and the bound came from the dimension stamp, which carries no operator. This is the same ambiguity that keeps restated qualifying-child upper bounds refused.

The _exclusive / _inclusive keys are not restated concepts. They keep the bare-key refusal even when their value matches. They must: an age <= 9 row compiles to age_upper_bound=9, and the materializer reads that as age < 9.

Like the existing rules, this one is family-blind: it reads only metadata. The AGI and age rules now share one half-open edge comparison, _restated_band_edge_refusal. The AGI rule's behaviour does not change; its tests are untouched and pass.

Downstream listings. The two places that list unsupported ledger_filter_* keys, _unsupported_ledger_filter_metadata (the fatal guard) and _unsupported_soi_ledger_filters (the SOI loop's silent skip), both go through _restated_ledger_filter_refusal. So an accepted key is dropped from neither, and a refused key is listed by both. I grepped packages/*/src and tools/ and found no other US listing. target_materialization.py is UK-only.

Measured

experiments/us-labelled-filter-support/age_band_rule_receipt.json (aggregate counts only) was written by the committed measure_age_band_rule.py at b43aec644, with a clean tree apart from the receipt. It covers the whole compiled registry, compile_us_fiscal_target_registry(..., age_targets=True), on 32,866 targets:

Arm Refused targets Refusal entries SOI silent skips
this PR 0 0 0
#969 alone (age removed from the concepts) 939 1,825 0
main (rule reverted) 3,208 5,116 2,269
  • Both pinned exports give identical counts: consumer_facts_us_c5e5bf8.jsonl (b8543739…) and chronicle_us_b571381/artifact/consumer_facts.jsonl (4d1dba8c…).
  • All 939 targets carrying a restated age key use population_age (936) or policyengine_variable (3).
  • The only age keys present are _lower_bound (939) and _upper_bound (886).
  • A separate scan of both feeds, not committed, found 1,493 facts with an age constraint. Every row is role: filter, >= or <, with an integer value, at most one per side. No fact has an age-like dimension.

Tests

packages/microcosm-build/tests/test_us_fiscal_refresh_builder.py:

  • test_restated_age_bounds_pass_the_guard_only_where_they_agree covers both materializers, each side alone, an open top, and float spellings. Neither listing names these keys. As a non-vacuity check, removing the concept brings back the bare-key refusal on every spec.
  • test_disagreeing_restated_age_bounds_are_refused_by_value covers each disagreement with both values named, a missing counterpart, an exact age, an age dimension, the SOI and absent materializers, and an unknown-key control.
  • test_operator_changed_age_bounds_stay_refused_by_bare_key is the negative control: _inclusive/_exclusive keys are still refused when their values match.
  • test_restated_age_bounds_are_judged_on_the_real_compile runs real facts through _dynamic_us_fiscal_target_references + compile_ledger_target_references.
  • test_population_age_targets_materialize_person_age_counts is now parametrized over none, agreeing and disagreeing restatements, and gains two SSA age-band policyengine_variable specs. With agreeing restatements the materialized columns are identical and no target is dropped. With disagreeing ones the build stops at the guard.
  • test_pinned_chronicle_feed_whole_registry_compiles_no_unsupported_filters is gated on MICROCOSM_US_CHRONICLE_FACTS.

Local runs are listed in the PR thread's final report. Note that .github/workflows/test.yml runs only on PRs into main, so CI will not run here until this PR is retargeted.

Operator attestation at compile (added 2026-09-22, 622486abb)

A ledger_filter_age_{lower,upper}_bound key carries the mask's operator only when _constraint_bound_filters stamped it from a >= / < row; _ledger_metadata stamps dimensions first and constraint edges through setdefault, so a dimension named age, or age plus a bound suffix, can write an operator-less value under the same key. The compile now records age_bound_stamp_source on every compiled population_age and SSA SSI age-band spec (constraint_rows, or dimensions:<names> when ledger_targets.constraint_bound_shadowing_dimensions finds a shadowing dimension), and the rule accepts a restatement only with constraint_rows. Re-measured on both pinned feeds at the committed tree (age_band_rule_receipt.json, clean worktree): all 939 targets read constraint_rows, so the rule arm stays at 0 refused. Tests: test_ledger_targets.py, test_us_fiscal_targets.py and test_us_fiscal_refresh_builder.py (pinned feed set, no skips) — 794 passed; ruff and ci_test_groups.py --verify clean.

Not changed / follow-ups

  • population_age compile still drops the operator in _age_bounds and does not reject <=/> rows the way the SSA path does. This guard catches those rows through the _inclusive/_exclusive keys, and the operator-attestation layer below now also refuses the case where age is a dimension whose value is None or "" (the compile's age_bound_stamp_source checks dimension keys whatever their value). A compile-side operator check for population_age would still be the cleaner fix.

🤖 Generated with Claude Code

MaxGhenis and others added 4 commits September 22, 2026 19:39
…e compiled bounds

Add "age" to RESTATED_LEDGER_FILTER_CONCEPTS under a third rule,
age_band. ledger_filter_age_lower_bound / _upper_bound (stamped by
ledger_targets._constraint_bound_filters from a fact's age >= / age <
rows) are accepted only when the edge parses to the same number as the
compiled age_lower_bound / age_upper_bound, and only on a spec whose
materializer applies the band (population_age, or the age-banded
policyengine_variable branch the SSA SSI by-age counts use). Refused,
naming the values: a disagreeing edge, a missing compiled counterpart,
an exact-age restatement, any other materializer, and a fact whose
dimensions include age (the bound then came from the operator-less
dimension stamp). >/<= rows keep their bare-key refusal
(_lower_bound_exclusive / _upper_bound_inclusive), since
fiscal_targets._age_bounds drops the operator when compiling the band.

The AGI and age rules now share one half-open edge comparison,
_restated_band_edge_refusal; the AGI rule's behaviour is unchanged.

Tests: agreement accepted on both materializers, each disagreement
refused with both values named, missing counterpart refused,
operator-changed keys still refused by bare key, the real compile path
(_dynamic_us_fiscal_target_references + compile_ledger_target_references)
judged end to end, the population_age materialize test parametrized over
no/agreeing/disagreeing restatements with an SSA age-band spec added, and
an env-gated whole-registry test over the pinned feed.
experiments/us-labelled-filter-support/measure_age_band_rule.py counts the
whole-registry refusals under rule / without_age_band / reverted arms.

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

age_band_rule_receipt.json (aggregate counts only), written by the
committed measure_age_band_rule.py at b43aec6: over the whole compiled
registry (32,866 targets) both pinned Chronicle exports go from 939
refused targets without the age-band rule to 0 with it; main's reverted
behaviour refuses 3,208. REPORT.md section 9 records the mechanism read
and the counts.

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

A ledger_filter_age_{lower,upper}_bound key keeps the operator only when
_constraint_bound_filters stamped it from a >= / < row; _ledger_metadata
stamps dimensions first and constraint edges through setdefault, so a
dimension named age, or age plus a bound suffix, can put an operator-less
value under the same key. The compile now records, on every age-banded
population_age and SSA SSI spec, age_bound_stamp_source: "constraint_rows"
unless such a dimension is present (ledger_targets.
constraint_bound_shadowing_dimensions), and the builder's restated age-band
rule accepts a restatement only with that attestation. age < 10 and
age <= 10 select different people, so an unattested bound is refused
rather than compared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis changed the base branch from us-labelled-filter-support to main September 23, 2026 02:09
@MaxGhenis
MaxGhenis marked this pull request as ready for review September 23, 2026 02:09
@MaxGhenis MaxGhenis closed this Sep 23, 2026
@MaxGhenis MaxGhenis reopened this Sep 23, 2026
@MaxGhenis
MaxGhenis merged commit 5a9655b into main Sep 23, 2026
24 checks passed
MaxGhenis added a commit that referenced this pull request Sep 23, 2026
No textual conflicts. build_us_fiscal_refresh_release.py (restated
age-band ledger filters, #986) and test_us_spine_blindness.py
(spm_universe_source.py, #977) auto-merged alongside this branch's
spm_independence_role stage and module registrations.

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