Skip to content

Bind infinite parameter values in reform digests - #588

Draft
MaxGhenis wants to merge 6 commits into
w4/receipt-compositionfrom
w4/receipt-nonfinite-reforms
Draft

MaxGhenis wants to merge 6 commits into
w4/receipt-compositionfrom
w4/receipt-nonfinite-reforms

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

Fixes #587

Stacked on #579, which is stacked on #571. Review only git diff b840fdf6..HEAD.

Why

The UK Budget dashboard's salary_sacrifice_cap measure sets gov.hmrc.pensions.salary_sacrifice_pension_cap to inf, which is how PolicyEngine writes an uncapped threshold. Reform digests are RFC 8785 over strict JSON, which has no infinities, so the measure could not be receipted (W5 reported it).

What changes

  • Encoding: reform payload digests encode inf as {"$nonfinite": "Infinity"} and -inf as {"$nonfinite": "-Infinity"}. NumPy infinities encode the same way.
  • One-to-one: a payload that uses the key $nonfinite itself is refused, so no two payloads share a digest.
  • NaN: still refused. It has no single value to bind.
  • Scenarios: parameter changes are converted once, at the digest, so a Scenario's infinite parameter change binds too.
  • Docs: the digest rule, schema description and docs say so.

Invariants (tested)

  • Unchanged digests: a payload without infinities digests exactly as before. A Hypothesis property test checks the digest against the previous definition (canonical_sha256(strict_json_value(...))).
  • Distinct values: inf, -inf and every finite cap get distinct digests.
  • Refusals: the reserved key is refused at any depth, and NaN is refused.

414 receipt, wheel and canonical tests pass, and the schema check passes.

axiom: n/a: receipt digest encoding, no policy encoding

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis force-pushed the w4/receipt-composition branch from b840fdf to e0052bc Compare October 12, 2026 17:05
@MaxGhenis
MaxGhenis force-pushed the w4/receipt-nonfinite-reforms branch from c8e8537 to 1ec7c76 Compare October 12, 2026 17:05
MaxGhenis and others added 2 commits October 13, 2026 09:49
PolicyEngine parameters take infinite values (an uncapped threshold is
.inf), and the Budget dashboard's salary_sacrifice_cap measure carries
one, but JSON cannot hold them, so its reform could not be receipted.

Reform payload digests now encode inf as {"$nonfinite": "Infinity"} and
-inf as {"$nonfinite": "-Infinity"}; a payload using the key $nonfinite
itself is refused, so no two payloads share a digest. NaN stays refused.
Scenario parameter changes are converted once, at the digest. A payload
without infinities digests exactly as before (differential property test
against the previous definition).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the w4/receipt-composition branch 2 times, most recently from e0052bc to b5d43d0 Compare October 13, 2026 13:50
@MaxGhenis
MaxGhenis force-pushed the w4/receipt-nonfinite-reforms branch from 1ec7c76 to cab895b Compare October 13, 2026 13:50
MaxGhenis and others added 4 commits October 13, 2026 11:01
Round-1 review of #588:
- M1: rebuilding mappings before strict conversion dropped one of two
  equal keys, so payloads refused before (colliding keys) were accepted.
  Infinities are now encoded inside _strict_json_value, after the
  collision check, and the reserved key is checked on the converted text.
- M2: the -Infinity spelling is pinned by exact canonical bytes.
- L1: a tuple case for the reserved key and for inf.
- L3: keys convert to an exact str, so a str subclass's __eq__ cannot
  hide the reserved key or a collision.
- L4: docs say what the encoding guarantees and what a JavaScript reform
  verifier must do.
- L5: an infinite modifier default binds by float.hex(); NaN is refused.
- L6: the differential test now generates nested payloads with int and
  colliding keys, tuples, nulls, strings, dates, numpy scalars and large
  integers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The composition docstring and docs gave "a single simulation modifier" as
an example of how to run a composition. On policyengine-uk 2.126.12 that
is not the same run: the engine applies a Scenario's own parameter changes
before the construction steps that read parameters (the Universal Credit
rebalancing inputs and the parameter-switched structural reforms) and runs
its modifier after them, so a change made inside a modifier reaches
neither. The text now says the changes apply where the engine applies a
Scenario's own, and how to run a composition on 2.126.12. Reported by the
dashboard workstream's review of its combiner; no digest changes.

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

Round-2 review of #588:
- The 2.126.12 composition recipe merged reform dictionaries into
  parameter_changes, but that version reads a scalar in a reform
  dictionary as applying from 2023 and in parameter_changes from 2000.
  The recipe now assembles one Scenario from each member's own Scenario
  (Scenario.from_reform for a dictionary): the scenario members' merged
  parameter_changes, every member's dated_reforms and every member's
  modifier, in member order. The modifier-ordering caveat is qualified
  to applied_before_data_load=False.
- Dates render with datetime.date.isoformat, so a subclass cannot choose
  its text or forge the reserved infinity object.
- Docs and docstring state the one change to finite digests: keys or
  dates that are subclasses overriding how str or date render now digest
  by their characters and their date.
- Tests pin the typed infinity spellings, a str subclass overriding
  __str__, key normalization in content digests, and the date rendering;
  each of the review's three surviving mutants, and a date mutant, is now
  killed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-3 review of #588: a str subclass whose iterator yields characters
with hostile equality escaped its quotes in the canonical writer and
could spell the reserved infinity object, so a payload with no infinity
digested like one with an infinity. String values now convert to an exact
str, as keys already do. The docs and docstring extend the one
documented change to finite digests to string values.

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

This branch has not been deployed

No deployments
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