Skip to content

Fix Reform.from_dict: apply bare date keys from that instant onward (#509)#510

Merged
anth-volk merged 3 commits into
masterfrom
fix/reform-from-dict-dates-onward
Jul 8, 2026
Merged

Fix Reform.from_dict: apply bare date keys from that instant onward (#509)#510
anth-volk merged 3 commits into
masterfrom
fix/reform-from-dict-dates-onward

Conversation

@anth-volk

Copy link
Copy Markdown
Collaborator

Summary

Fixes #509. Reform.from_dict applied bare date period keys (e.g. "2026-01-01") for a single instant, so the reform value reverted to baseline in every later period — silently — and a bare except: swallowed real parameter.update errors, misrouting them to the range branch.

Changes

  • from_dict — explicit period-format detection instead of try/except-as-control-flow:
    • Bare ISO instant ("2026" / "2026-01" / "2026-01-01") → applies from that instant onward.
    • Range "start.stop" and compound "year:2026:5"bounded (unchanged).
    • "ETERNITY" → all time.
    • Entries applied in chronological order, so multi-entry dicts are order-independent (a later-starting key no longer clobbers an earlier one).
    • parameter.update errors now propagate instead of being swallowed.
  • from_api — emits explicit start.stop ranges so a single-civil-year policy stays bounded under the new from-onward rule (it would otherwise stringify to a bare "YYYY").

Why

A dated policy change is naturally read as "from this date onward." The single-instant behavior produced silently-wrong multi-year results downstream — e.g. PolicyEngine/policyengine.py#453 (a 10-year window scored the start year and exactly $0.00 for the nine later years), and in the PolicyEngine simulation API a reform dated 2023-01-01 scored exactly $0 for a 2026 simulation while the identical reform dated 2026 scored −$16.9M.

Verification

Compatibility

Semantic change for existing bare-key callers (they previously applied for a single instant) → shipped as a changed (minor) towncrier fragment. API round-trips stay bounded via the from_api hardening.

🤖 Generated with Claude Code

anth-volk and others added 3 commits July 8, 2026 00:18
…509)

Bare period keys like "2026-01-01" parsed to a single-day Period, so the
reform value applied for one day only and every later year silently
reverted to baseline. The bare `except:` also swallowed real
parameter.update errors and misrouted them to the range branch.

from_dict now detects period formats explicitly: bare ISO date/month/year
apply from that instant onward, while ranges ("start.stop") and compound
periods ("year:X:N") stay bounded. Entries are applied in chronological
order so multi-entry reforms are order-independent, and update errors
propagate. from_api emits explicit ranges so single-civil-year API
policies stay bounded under the new rule.

Fixes #509.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Extract per-format handlers (_eternity/_range/_bounded_period/_from_instant
  _update_kwargs) + dispatcher (_period_key_update_kwargs) + sort key
  (_update_start_instant) into module-level pure functions called by
  from_dict. Extract from_api range normalization into _api_period_range
  with an out-of-window guard.
- Coerce non-string period keys to str, so an int year key no longer raises.
- Build all update kwargs before applying, so a malformed key raises with no
  partially-applied reform.
- Document and lock intended edge behaviors: entries sharing a start instant
  apply in dict order (last wins); bare dates are literal instants; invalid
  input is atomic.
- Add per-helper unit tests (tests/core/test_reform_period_keys.py) plus
  behavior tests for ETERNITY, scalar shorthand, int key, bracket-indexed
  path, mixed range+bare, mid-year literal, same-start last-wins, and atomic
  failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@anth-volk anth-volk marked this pull request as ready for review July 8, 2026 18:13
@anth-volk anth-volk merged commit 7cba2c8 into master Jul 8, 2026
22 checks passed
@anth-volk anth-volk deleted the fix/reform-from-dict-dates-onward branch July 8, 2026 18:13
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.

Reform.from_dict applies bare-date period keys for a single instant, silently (and its bare except swallows update errors)

1 participant