Repository navigation
fix(submission): compare a share's whole to its part by graph ref at admission - #80
Merged
Merged
Conversation
…admission The planner's share check compared entity names as the plan wrote them, so a whole naming "United States of America" over a part naming "United States" divided the member by itself unrefused. admit now runs the check again with each name resolved against its requirement's pinned source, matching an entity by ref where both sides resolve one and by name where either does not.
Each G7 donor's share of total G7 aid names G7 on one side and Group of Seven on the other; both resolve to the same seven country refs. The pool exemption counted that ref set as one member and refused the plan as whole_is_its_member. It now counts the union of resolved refs, plus the names of entities that resolve none.
This was referenced Oct 25, 2026
This was referenced Oct 29, 2026
Closed
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.
The planner refuses a share whose denominator, on a party its ask totals across, names only entities its numerator names, such as the United States' ODA to Kenya divided by a total naming only the United States and Kenya (
whole_is_its_member, from #75). It compared names as the plan wrote them, so a whole naming "United States of America" over a part naming "United States" passed, and the answer divided the member by itself, the 99.7% Kenya figure #75 was written to stop. Translator checks such as_divides_its_member_by_itselffrom #70 already compare graph refs.Planvalidation cannot see refs. A search reading stored in state carries none, and the refs live in each pinned search block (PinSource.spec_block, through_bound_refs), which only admission reads. Passing a ref map intoPlanvalidation would make a stored plan validate differently on reload than when it was admitted, and storing refs onEntitywould add a plan field and a migration for stored plans. Socheck_wholes_span_their_totalsinplanner/plan.pytakes arefsfunction.Planvalidation runs it withno_refs, which refuses exactly what it refused before.submission/service.pyadmitruns it a second time on the stated plan with_entity_refs, which resolves each name against the source its requirement pins throughcompiler.evidence.named_refs, as #70 does, and returns no refs when none resolve._among_membersmatches each entity of the whole to the part. It matches by ref when every ref it resolves to is one the part's entities resolve to, so a group named in the part covers its members. Where either side resolves no ref, it matches by equal name. The whole is refused when every entity in it is matched. A name resolving on one pin but not the other still matches its twin, and a whole naming G7 beside a part naming Germany is accepted as before, since G7 holds refs the part does not. The pool exemption counts the whole's members as the union of the refs its entities resolve to, plus the names of entities that resolve none, so each G7 donor's share of total G7 aid, with "G7" on one side and "Group of Seven" on the other, counts seven members and is accepted, while a pool of one is still refused.The second pass can only add refusals, and it runs before
split_of,_window_pinsand_unrecordable, so a refused plan leaves no state behind;record_admissionis the only write._invalid_datasends a typed rule error raised outside validation through_rule_refused, so the model gets the samewhole_is_its_memberproblem and message and can retry. Every other bareValueErrorstill gets the generic invalid-plan answer. A share read through an aggregate is compared by the pin of the dataset underneath, and rolling copies are left out because the check reads the plan as the model wrote it.A name only the graph would resolve still falls back to its name at admission, because
SelectedSource.entity_refsis not filled yet at that point.Testing:
test_a_share_compares_its_whole_to_its_part_by_ref_where_the_pins_resolve_oneintests/planner/test_agent.pyhas six rows, andtest_a_pooled_share_counts_the_members_its_whole_resolves_its_group_tocovers the G7 pool of seven and a pool of one. An alias pair resolving to one ref is refused, and so is the same pair beside a name only the part's pin resolves, both directly and through an aggregate. "Congo" against "Congo, Dem. Rep." is accepted, unpinned equal names are refused, and an unpinned alias pair is accepted. The refusing rows fail without the change. The full harness suite passes (7171 passed, 1 skipped, 1 xfailed). ruff and ty are clean.