Repository navigation
Conversation
…ws to year A product binding now reads exactly two concepts: one usual rate held as a float and one annual flow counted in weeks, in either order. Before, the only check was the count of two, so these validated: - Product(liquid_financial_assets, weeks_worked), although the first is a stock; - usual weekly hours times a year's income. policyengine-uk's hours_worked, usual weekly hours times weeks worked, is the one committed product, and it still validates. A concept declared an annual flow must also have period year. All 14 committed annual flows already do, so concept_schema_sha256 does not move. The tie runs one way: usual weekly hours, a usual rate, keeps period year. Tests: - Hypothesis: either operand of the hours product is swapped for any concept and its basis redeclared; for a numeric quantity that is neither an amount nor a pointer, its dtype and unit are redeclared too. In either order, the binding validates exactly when the rule's text says it should, and a refusal names only operands at fault. - Exhaustive: of all ordered pairs of committed concepts, only usual weekly hours and weeks worked multiply. - Each of 13 ways to break the rule is refused with its whole message, and validates once the field at fault is redeclared. A product of one concept or of three is refused by name. - Exhaustive: redeclaring either operand of the committed product under any other basis refuses it. - The complement test now excludes products and redeclares the reads of every other committed binding under all four bases. - Exhaustive over policyengine-uk's JSON form: any other person concept in either operand's place is refused. - Exhaustive over every committed concept, basis and period: exactly an annual flow outside period year is refused. The _redeclared test helper now sets period year when it redeclares a concept as an annual flow. ADR invariant 11 states both rules. No pinned hash moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
product-rate-times-flow
branch
from
October 7, 2026 15:41
ae69246 to
8713319
Compare
The product property drew both operands independently and rejected collisions with assume(), which discarded up to a quarter of the examples. It now draws the second operand from the concepts the first did not take, and records whether each example validates or is refused as a Hypothesis event (the valid branch runs in about 4-8% of examples). The ADR says "any other concept" to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main (89660a3) added the ScaledSum and SignedPart transforms and ADR invariants 12 and 13 since this branch's base. Three files conflicted: - concept_mapping.py: _FLOW_TRANSFORMS keeps main's six transforms, and its comment keeps this branch's pointer to the product rule. - docs/concept-schema-transport-adr.md: main's wording for the stock and the scaled sum, this branch's product and annual-flow text in invariant 11, then main's invariants 12 and 13. - test_concept_mapping.py: imports both example and event, and the docstring names the scaled sum beside this branch's product paragraph. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
… amendment The annual-flow rule covers six transforms, and its prose named five: a signed part computes from annual flows too, so "other transforms are outside both rules" contradicted the code. The ADR's lists, the test class docstring and the _FLOW_TRANSFORMS comment now name it. The ADR also gives the product and period rules their own dated amendment, says the product rule refuses a concept of any other basis, and says the rule leaves the usual rate's unit unchecked. No code changes. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A reviewer of the previous commit found four places in test_concept_mapping.py that still listed five of the six transforms under the annual-flow rule: two docstrings and two comments. They now name the signed part. One test loops over every committed arithmetic binding, the UK mapping's two signed parts included, and its comment now says so. Also rewraps the _FLOW_TRANSFORMS comment and gives an ADR parenthesis a verb. No code changes: both Python files parse to the same tree as before once docstrings are set aside. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
November 11, 2026 14:11
This branch has not been 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.
Follow-up to #1128. That PR left
Productoutside its annual-flow rule, and its merge note named the two rules below as follow-ups. This PR adds both.What this does
A product reads a usual rate times weeks. A
productbinding now reads exactly two concepts, in either order: one usual rate held as a float, and one annual flow counted in weeks (unitweeks, an int or a float). Before, the only check counted two concepts, so these all validated:Product(liquid_financial_assets, weeks_worked), although the first is a stock;Product(take_up_seed, weeks_worked), a persistent draw;Product(usual_weekly_hours, employment_income), hours a week times a year's income;Product(employment_income, weeks_worked), two annual flows.Construction now refuses each one, with a message that names the concepts at fault:
Of the four committed mappings, only policyengine-uk's binds a product:
hours_worked, usual weekly hours times weeks worked. It still validates.The weeks rule goes one step past #1128's note. #1128 described the follow-up as a usual rate times an annual flow. This PR also requires the annual flow to count weeks. The
TemporalBasis.USUAL_RATEdocstring calls a usual rate "a usual intensity during the reference year's weeks of activity", so only a number of weeks multiplies it to a year's total. The product branch of_applymultiplies the two columns as float64 and tracks no unit, so nothing downstream would catch a mismatch. Of the 14 committed annual flows, 13 hold currency amounts. Without the weeks check, 28 ordered pairs of committed concepts would validate (usual weekly hours times each annual flow, in either order),usual_weekly_hours x employment_incomeamong them; with it, 2 do. The rule leaves the rate's own unit free, since a usual weekly amount times weeks also gives a year's total.An annual flow has period
year.Concept.__post_init__refusestemporal_basis=annual_flowwith any other period. All 14 committed annual flows already have periodyear.concept_schema_sha256hashes the declarations, and no declaration changes, so the schema digest does not move. The tie runs one way: other bases take any period, andusual_weekly_hours, a usual rate, has periodyear.Invariants
floatand one annual flow with unitweeksand dtypeintorfloat, in either order._check_product, which_check_transform_aritycalls fromInputBinding.__post_init__. These construction paths run it:bind(), direct construction,InputBinding.from_dict,ConceptMapping.from_dictanddataclasses.replace.year. For everyConcept,temporal_basis is ANNUAL_FLOWimpliesperiod == "year". Periodyeardoes not imply an annual flow:usual_weekly_hours, a usual rate, has periodyear.Concept.__post_init__, so it covers direct construction anddataclasses.replace.ADR invariant 11 (
docs/concept-schema-transport-adr.md) states both rules.Tests
In
TestArithmeticReadsAnnualFlows(engine_free/shared/test_concept_mapping.py):_faults) says it should. A refusal names only operands at fault.(usual_weekly_hours, weeks_worked)and its reverse validate. Construction refuses every other pair with a product message that names only concepts at fault.TestConstructionRules, construction also refuses a product of one concept and of three, and the test matches the whole message.ConceptMapping.from_dictrefuseshours_workedwith any of the other 21 person concepts in either operand's place, and the refusal names the newcomer. A second test matches the stock times weeks worked, the case that prompted the rule, in full, and shows that it validates once the test declares the stock a usual rate._redeclarednow sets periodyearwhenever it redeclares a concept as an annual flow. Without that, Refuse scale, sum, share and fraction on concepts that are not annual flows #1128's tests that redeclare the stock (periodpoint) as an annual flow would trip the new period tie.In
TestConceptValidation(engine_free/shared/test_concepts.py):yeardeclarations, and the test matches the whole message. Every other declaration constructs.test_bad_declarations_are_refusedadds age redeclared as an annual flow, which keeps its periodpoint.Hashes
concept_schema_sha256(5a55669d…, the value the coverage goldens and the NZ build files commit) anddocs/graph-interface.lock, which hashesdecl.pyandkernel.py;source_hashcovers each kernel's defining module, and no parity kernel lives inframe/concepts.pyorframe/concept_mapping.py;coverage_reportreads only the mapping, the engine's input surface and the concept registry and its digest, and none of them changes, so no coverage golden can move;seed_protocol,seed_map, USspec_sha256, loader golden). This PR edits none of their files, and no spec-engine module namesframe/concepts.pyorframe/concept_mapping.py.concepts.py. Cite NZ spec sources by symbol or stable record, never by a drifting line #1282 replaced the NZ benefit-unit rule's line-number citations ofconcepts.pywith symbol citations, so no committed line citation moves.Mutation testing
At
38c313157, before the merge, a pytest plugin swapped_check_productorConcept.__post_init__for 25 mutants at run time, with no tracked file edited and the Hypothesis database off. The two shared test files killed 24. The merge leaves_check_productbyte-identical and the period tie unchanged.year;monthallowed for annual flows;==instead ofison enum members. It behaves identically.At
91de7f67a, an independent reviewer removed or weakened the two rules in nine ways in a scratch copy, and a test failed for each.Tests run
91de7f67a: one pytest process with-p no:cacheproviderand a fresh Hypothesis directory. Exit 0, 474 passed in 105 s. At the head, the two shared files pass again (440 tests).engine_free/shared/test_concept_mapping.pyengine_free/shared/test_concepts.pyengine_free/shared/test_axiom_concept_mapping.pyengine_free/us/test_concept_mapping.pyengine_free/uk/test_concept_mapping.pyruff checkandruff format --check(ruff 0.15.16) pass on the four touched Python files.Merge with main
The branch sat 2,464 commits behind main. I merged main
89660a3b6into it as91de7f67a. Since the branch's base, main had added theScaledSumandSignedParttransforms and ADR invariants 12 and 13. Three files conflicted:concept_mapping.py:_FLOW_TRANSFORMSkeeps main's six transforms, and its comment keeps this branch's pointer to the product rule.docs/concept-schema-transport-adr.md: main's wording for the stock and the scaled sum, this branch's product and annual-flow text in invariant 11, then main's invariants 12 and 13.test_concept_mapping.py: imports bothexampleandevent, and the class docstring names the scaled sum beside this branch's product paragraph.In
concept_mapping.pyandconcepts.py, the linesgit diff 89660a3b6 91de7f67aadds and removes equal the branch's own.Two later commits change prose only. The annual-flow rule covers six transforms and its text named five, so the ADR, the test file's docstrings and comments and the
_FLOW_TRANSFORMScomment now name the signed part. The ADR also gives the product and period rules their own dated amendment and says the product rule leaves the usual rate's unit unchecked.axiom: n/a: a validation rule in Microcosm's concept layer; it encodes no policy.
🤖 Generated with Claude Code