Conversation
…s the partner The UK builder puts everyone in one benefit unit and sent only ages, so policyengine-uk inferred the couple from ages: a lone parent aged 50 with a dependant aged 25 was assessed as a couple (PolicyEngine/policyengine-uk#2039). The builder made the same mistake itself: getBuilderPartnerKey fell back to any other adult, so setting a dependant's age to 25 flipped the composition to married, and choosing "single" then deleted the dependant. - getBuilderPartnerKey now returns only an explicit partner: the US marital unit, "your partner", or a member flagged is_claimant_or_partner for the year. Age never makes a partner. This applies to US and UK builders. - Household.getBuilderClaimantRoles / withBuilderClaimantRoles give a UK builder household ("you" present) explicit roles: "you" and the explicit partner true, every other member false. Values already set are kept. - withSupportedBuilderClaimantRoles adds those roles before every save (standalone builder, report-builder modal create and update, report year copies) only when the loaded UK model information defines the variable. policyengine-core rejects a situation naming an unknown variable, and the live API (policyengine-uk 2.90.2) does not have it yet, so saves are unchanged until it ships. - New children are numbered after existing dependants, so adding a child next to an adult dependant gives "your second dependent", not "your first dependent 2". Tests: composition cases for UK and US single parents and couples with a dependant aged 25, role and codec round trips (v1 payload, Python package, saved household), the metadata gate, every save site, and fast-check properties over random builder action sequences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Independent review of 8c57518 found two reproduced bugs and three smaller issues; all are fixed here. - A claimant-or-partner flag names the partner only inside the primary person's benefit unit. Before, a flagged claimant in another unit could become "the partner", and choosing "single" deleted them. - Roles are generated only for the builder's structure: "you" and one benefit unit holding everyone. policyengine-core gives anyone without an input the variable's default (false), not its formula, so roles for one unit would make another unit's own claimant a non-claimant. Multi-unit and partly-outside households now get no roles, and the model infers them as before. - When the loaded UK model information lacks the variable, roles a household already carries are removed before saving, so a stored flag can never reach a model that rejects it. - New children are numbered after the highest dependant ordinal in use, so a gap no longer gives "your second dependent 2". - The integration test's API response moved to fixtures. Tests: regression cases for each, and fast-check properties over saved multi-unit households, households already carrying roles, and roles recorded before and after further builder edits. Undoing each fix fails 4, 5, 2 and 1 tests respectively. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xes) The second independent review (of 0f4234b) confirmed the round-1 fixes and found two more cases; both are fixed here, with three smaller points. - The partner and child controls manage only the primary person's own family unit (UK benefit unit, US tax unit). A 17-year-old claiming in another benefit unit was counted as a child, so setting the child count to zero deleted them and their unit. Main had the same behaviour. - A claimant-or-partner flag names a partner only when "you" are flagged as a claimant too. Before, "you" flagged false (or unflagged, which policyengine-core reads as false once anyone has the input) still acquired a flagged member as partner, and "single" deleted them. - New partners and children join the groups that hold "you", not whichever group is listed first, so a saved household whose other unit comes first no longer gets the new partner in the wrong unit. - The role helper treats model information as loaded only when it has finished loading without error (the app's getModelMetadataError), so roles are never stripped while it is still loading. Tests: regression cases for each, a US tax-unit case, and a fast-check property that builder edits on a multi-unit household never touch the other unit and that new members join the unit of "you". Undoing each fix fails 5, 2, 2 and 1 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The UK household builder puts everyone in one benefit unit and sends only ages and incomes. policyengine-uk then infers who the claimant and partner are from ages, so a lone parent aged 50 with a dependant aged 25 is assessed as a couple (PolicyEngine/policyengine-uk#2039).
The builder made the same mistake in its own composition logic.
getBuilderPartnerKeyfell back to any other adult aged 18+, so:The US builder had the same fallback.
What changes
The partner must be explicit (
Household.getBuilderPartnerKey, both countries). A partner is one of:is_claimant_or_partneris true for the year, when the primary person is flagged true too. A flagged member of another unit is that unit's own claimant. If the primary person is flagged false, or not flagged at all, nobody is their partner by flag. policyengine-core reads an unflagged person as false once anyone has the input.Age never makes a partner.
The builder's controls manage only the family unit of "you". "Marital status" and "Number of children" read and change only the primary person's own unit (UK benefit unit, US tax unit). New partners and children join the groups that hold "you". Members of another unit belong to their own claimant, so the builder never counts them as children and never deletes them. A saved household whose other unit is listed first no longer gets the new partner in the wrong unit. Main counted and deleted them too.
UK builder households carry explicit roles.
Household.getBuilderClaimantRoles(year)givesis_claimant_or_partnerper person: true for "you" and the explicit partner, false for every other member.withBuilderClaimantRoles(year)records them, keeping any value a person already has.Roles are generated only for the builder's structure: a person named "you" and one benefit unit holding everyone. They are all or nothing. policyengine-core gives anyone without an input the variable's default (false), not its formula, so roles for one unit would make another unit's own claimant a non-claimant. Other households get no roles, and policyengine-uk keeps inferring them as before. That covers saved multi-unit households and households made outside the builder.
The roles are sent only when the model accepts them.
withSupportedBuilderClaimantRoles(inutils/builderClaimantRoles.ts) runs before every save:getModelMetadataError), it leaves the household alone. Saving is blocked then anyway.It runs at each place the app creates a household:
HouseholdBuilderView);createReportSimulations).The roles are stored in the household itself, the way the US builder stores
is_tax_unit_dependent. The v1 and Python-package codecs already pass person fields through unchanged, and both the saved-household calculation and the earnings-variation chart read the stored household, so the two always agree. The in-memory household handed on after a save is the one that was stored.New children are numbered after existing dependants. Numbering starts after every dependant and after the highest ordinal in use. Adding a child next to an adult dependant now gives "your second dependent" instead of "your first dependent 2", and a gap left by removed children no longer causes a collision either.
Why the gate, and what happens before policyengine-uk ships
is_claimant_or_partnerwas added to policyengine-uk by PolicyEngine/policyengine-uk#1896, which merged on 2 October 2026 but is not yet in the API's model. I checked the live API on 2 October 2026, and again on 3 October:POST /uk/calculatewith the variable returnsUnrecognized household variable is_claimant_or_partner;SituationParsingErrorfor a situation that names an unknown variable (simulation_builder.py,init_variable_values).So sending the roles unconditionally would break every UK household calculation. With the gate, this PR is safe to merge now:
is_coupletrue, UC standard allowance £8,003.64 for 2026).Households saved earlier have no roles. Builder households get them the next time they are saved through the builder.
The earnings-variation chart sends the stored household as it is. A household stored with roles would fail there only if the API's model later lost the variable, and the saved-household calculation would fail the same way.
Invariants
Each is a fast-check property. Most run over random sequences of builder actions (marital status, child count 0-5, any person's age 0-100) on UK and US starter households. The multi-unit properties add a separate claimant in a benefit unit of their own, with any age and with or without a role, and optionally record roles first.
toV1CreationPayloadthenfromV1CreationPayload).Tests
fromV1Metadataround trip.fetch, the modal's create and update paths, andcreateReportSimulations.tsc, eslint and prettier are clean.fast-checkis added to the app's devDependencies. The website workspace already uses the same version.Browser verification
The live API cannot show the single-claimant result yet, so I ran the calculator against a local API:
0ebfc086, on a local SQLite database (the same engine swap the API's own legacy test suite uses), never Cloud SQL;72d70ddb(2.104.2). policyengine-us was upgraded to 2.21.0 in that environment so it loads under the newer policyengine-core;BASE_URLpointed at it (local edit, not committed).In the UK report builder I created a household with one child, set "you" to 50 and the dependant to 25, and ran the report for 2026:
is_claimant_or_partneris_couplefalse, UC standard allowance £5,098.80is_coupletrue, UC standard allowance £8,003.64The report page showed household benefits of £5,099 for the lone parent, and the earnings-variation chart loaded from the same stored household. The same lone-parent household without the roles, on the same local model, is a couple with £8,003.64. These are single-household checks on an unmerged model branch, not published figures.
Review
An independent review (GPT-6.1 Sol, via Subfleet) of the first commit requested changes. It reproduced two bugs:
It also noted three smaller points:
All five are fixed in
0f4234ba, with the regression tests and properties above.A second review (GPT-6.1 Sol) of
0f4234baconfirmed all five fixes, and CI on that head ran 4,090 app tests with no failure. It requested changes for two more cases:It also noted three smaller points:
All are fixed in
3768c7f7. Its stale "#1896 unmerged" claim and the overstated "byte-for-byte" claim are corrected above.Notes for review
is_tax_unit_dependentdoes for US households.Refs PolicyEngine/policyengine-uk#2039 and PolicyEngine/policyengine-uk#2040 (which lists this as a follow-up).
axiom: n/a: app change to how households are entered and sent; no policy rule changes.
🤖 Generated with Claude Code