Problem
microunit currently hardcodes / snapshots the federal policy values used by the dependency rules, instead of sourcing them from the canonical parameter tree. They happen to match PolicyEngine today, but nothing keeps them in sync — they will silently drift when PE (or the law) updates.
Inventory of what's baked in and its canonical PE parameter:
| microunit (current) |
Where |
Canonical source in PE-US |
non_student_age_limit = 19, student_age_limit = 24 (literal default args) |
rule_helpers.py → qualifying_child_age_test() |
gov.irs.dependent.ineligible_age.non_student (19, 2018-, §152) and .student (24, 2018-, §152) |
| Qualifying-relative gross-income limit (§152(d)(1)(B) = the §151(d) exemption amount), as a bundled year-keyed YAML |
data/dependent_gross_income_limit.yaml |
gov.irs.income.exemption.amount |
The bundled gross-income YAML is already a faithful structural copy of PE's parameter (year-keyed from 2013, uprating: gov.irs.uprating, Rev. Proc. references) — it just isn't generated from PE, so it's hand-maintained and can go stale.
These are currently in agreement, so this is drift-prevention, not a bug fix, and is not urgent.
What we want
The policy values should come from a single source of truth: PolicyEngine's parameters now, and the Axiom / Rules-Foundation rules-as-code source later — with PE as the nearer-term source. microunit should stop being the place a federal constant is independently authored.
Hard design constraint — keep microunit runtime-independent of policyengine-us
rule_helpers.py is explicit that microunit "does not depend on policyengine-us being installed," and that independence matters: the one-off household-request path and microplex's tax-unit construction must not be forced to drag a full PE install into the runtime just to know "19."
So the fix should not add a hard PE runtime dependency. Preferred shape:
- Build-time codegen — generate microunit's bundled parameter data from PE's parameters at package-build/release time (a small script that reads
gov.irs.dependent.ineligible_age.* and gov.irs.income.exemption.amount and writes microunit's data/*.yaml). The shipped data is then provably PE-derived and dated/uprated, and a CI check can fail if the committed snapshot diverges from PE.
- Optional runtime override — if
policyengine-us is importable, allow reading parameters live; otherwise fall back to the bundled snapshot. Keeps the dependency optional.
- Later, swap the build-time source from PE to Axiom / Atlas (rules-as-code) without changing microunit's internals — the codegen seam is the same.
Scope boundary (important)
Only the parameter values move to PE/Axiom. The partition logic stays in microunit — PE has no "given these N people, return the tax-unit partition" callable; the partition is PE's input, not an output, which is microunit's whole reason to exist. This issue does not propose delegating the qualifying-child/relative logic to PE, only the dated values it parameterizes.
Why it's worth doing
- Removes the only places microunit can silently disagree with the law / with PE.
- Strengthens the entity-convergence story: microunit is eCPS's tax-unit engine, so sharing PE's exact dated parameters makes the partition provably track PE rather than approximate it.
- Year-aware already:
construct_tax_units(person, year, mode) takes a year and the sourced parameters are dated/uprated, so a year-keyed build is a natural fit.
Acceptance criteria
Related: the microplex-side adapter gap that prevents the 24-yo student branch from firing is tracked separately at PolicyEngine/microplex-us#122 (that's about feeding microunit the enrollment columns; this issue is about where the 24 comes from).
Problem
microunit currently hardcodes / snapshots the federal policy values used by the dependency rules, instead of sourcing them from the canonical parameter tree. They happen to match PolicyEngine today, but nothing keeps them in sync — they will silently drift when PE (or the law) updates.
Inventory of what's baked in and its canonical PE parameter:
non_student_age_limit = 19,student_age_limit = 24(literal default args)rule_helpers.py→qualifying_child_age_test()gov.irs.dependent.ineligible_age.non_student(19, 2018-, §152) and.student(24, 2018-, §152)data/dependent_gross_income_limit.yamlgov.irs.income.exemption.amountThe bundled gross-income YAML is already a faithful structural copy of PE's parameter (year-keyed from 2013,
uprating: gov.irs.uprating, Rev. Proc. references) — it just isn't generated from PE, so it's hand-maintained and can go stale.These are currently in agreement, so this is drift-prevention, not a bug fix, and is not urgent.
What we want
The policy values should come from a single source of truth: PolicyEngine's parameters now, and the Axiom / Rules-Foundation rules-as-code source later — with PE as the nearer-term source. microunit should stop being the place a federal constant is independently authored.
Hard design constraint — keep microunit runtime-independent of
policyengine-usrule_helpers.pyis explicit that microunit "does not depend onpolicyengine-usbeing installed," and that independence matters: the one-off household-request path and microplex's tax-unit construction must not be forced to drag a full PE install into the runtime just to know "19."So the fix should not add a hard PE runtime dependency. Preferred shape:
gov.irs.dependent.ineligible_age.*andgov.irs.income.exemption.amountand writes microunit'sdata/*.yaml). The shipped data is then provably PE-derived and dated/uprated, and a CI check can fail if the committed snapshot diverges from PE.policyengine-usis importable, allow reading parameters live; otherwise fall back to the bundled snapshot. Keeps the dependency optional.Scope boundary (important)
Only the parameter values move to PE/Axiom. The partition logic stays in microunit — PE has no "given these N people, return the tax-unit partition" callable; the partition is PE's input, not an output, which is microunit's whole reason to exist. This issue does not propose delegating the qualifying-child/relative logic to PE, only the dated values it parameterizes.
Why it's worth doing
construct_tax_units(person, year, mode)takes a year and the sourced parameters are dated/uprated, so a year-keyed build is a natural fit.Acceptance criteria
19/24literals as the source of truth; no hand-maintained value YAML).policyengine-us(optional live-read is fine).Related: the microplex-side adapter gap that prevents the 24-yo student branch from firing is tracked separately at PolicyEngine/microplex-us#122 (that's about feeding microunit the enrollment columns; this issue is about where the 24 comes from).