Bug
When any reform is applied via the API, all NJ tax variables return null. NJ is the only state affected — all other 50 states + DC work correctly. NJ baseline (no reform) also works fine.
Root Cause
Two issues in country.py:
1. clone() breaks ParameterNode iteration
The API applies reforms via system.clone() + direct parameter.update() (lines 366-383) rather than Reform.from_dict(). This breaks ParameterNodeAtInstant.__iter__, which NJ's nj_gross_income formula relies on:
cats = p.loss_eligible_categories
for cat in cats: # iterates ParameterNodeAtInstant — fails after clone()
total += max_(add(person, period, cats[cat]), 0)
This pattern is valid policyengine-core usage but breaks under the API's clone path. It cannot be reproduced locally with Reform.from_dict().
2. Silent error swallowing (lines 447-449)
except Exception as e:
if "axes" in household:
pass # Exception silently discarded
This hides the actual exception, making the bug invisible. The variable silently returns null instead of surfacing the error.
Evidence
- API trace confirms
nj_gross_income (person-level) is the root failure — all downstream NJ variables cascade
- Federal variables (
adjusted_gross_income, income_tax) compute correctly under the same reform
- Every other state works fine
- Cannot reproduce locally even when mimicking the
clone() path
- API version: 1.592.4
Suggested Fixes
- Investigate why
clone() breaks ParameterNodeAtInstant iteration and fix the deep copy
- At minimum, log the exception instead of silently swallowing it — the
pass on line 449 should be logging.exception(e)
Workaround
PolicyEngine/policyengine-us#7743 works around this by replacing the iteration with explicit category references, but this is a band-aid — the API should support valid core patterns.
Bug
When any reform is applied via the API, all NJ tax variables return
null. NJ is the only state affected — all other 50 states + DC work correctly. NJ baseline (no reform) also works fine.Root Cause
Two issues in
country.py:1.
clone()breaksParameterNodeiterationThe API applies reforms via
system.clone()+ directparameter.update()(lines 366-383) rather thanReform.from_dict(). This breaksParameterNodeAtInstant.__iter__, which NJ'snj_gross_incomeformula relies on:This pattern is valid
policyengine-coreusage but breaks under the API's clone path. It cannot be reproduced locally withReform.from_dict().2. Silent error swallowing (lines 447-449)
This hides the actual exception, making the bug invisible. The variable silently returns
nullinstead of surfacing the error.Evidence
nj_gross_income(person-level) is the root failure — all downstream NJ variables cascadeadjusted_gross_income,income_tax) compute correctly under the same reformclone()pathSuggested Fixes
clone()breaksParameterNodeAtInstantiteration and fix the deep copypasson line 449 should belogging.exception(e)Workaround
PolicyEngine/policyengine-us#7743 works around this by replacing the iteration with explicit category references, but this is a band-aid — the API should support valid core patterns.