Skip to content

fix(submission): compare a share's whole to its part by graph ref at admission - #80

Merged
jm-rivera merged 3 commits into
devfrom
fix/share-whole-refs
Oct 26, 2026
Merged

jm-rivera merged 3 commits into
devfrom
fix/share-whole-refs

Conversation

@jm-rivera

@jm-rivera jm-rivera commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

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_itself from #70 already compare graph refs.

Plan validation 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 into Plan validation would make a stored plan validate differently on reload than when it was admitted, and storing refs on Entity would add a plan field and a migration for stored plans. So check_wholes_span_their_totals in planner/plan.py takes a refs function. Plan validation runs it with no_refs, which refuses exactly what it refused before. submission/service.py admit runs it a second time on the stated plan with _entity_refs, which resolves each name against the source its requirement pins through compiler.evidence.named_refs, as #70 does, and returns no refs when none resolve.

_among_members matches 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_pins and _unrecordable, so a refused plan leaves no state behind; record_admission is the only write. _invalid_data sends a typed rule error raised outside validation through _rule_refused, so the model gets the same whole_is_its_member problem and message and can retry. Every other bare ValueError still 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_refs is not filled yet at that point.

Testing: test_a_share_compares_its_whole_to_its_part_by_ref_where_the_pins_resolve_one in tests/planner/test_agent.py has six rows, and test_a_pooled_share_counts_the_members_its_whole_resolves_its_group_to covers 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.

…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.
@jm-rivera
jm-rivera merged commit dddfe1c into dev Oct 26, 2026
3 checks passed
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