Repository navigation
feat(search): state the provider scope of a DAC1 total ODA part - #166
Merged
Merged
Conversation
A DAC1 total counts core contributions to multilaterals as the donor's ODA, so it now carries a provider choice whose one available option is provider:contributions ("bilateral plus core contributions to multilaterals"). Bilateral and inclusive are unavailable with reason counts_core_contributions, and DAC1's own bilateral line is never offered as bilateral. Spec_ids do not change; the scope rule tables' hash moves the capability version.
…her dataset codes The provider convention reads DAC1_TOTAL_ODA from declarations.py, so no election module names a declared dataset's codes.
…ateral donors PUBLISHED_PROVIDER_SCOPES maps DAC1 total ODA to contributions and DAC1 bilateral ODA to bilateral, for donors outside the declared multilateral providers. A stated inclusive provider on a DAC1 total drops with contributions_counted, since the total already counts core contributions. ProviderScope is declared once, contributions is an explicit no-op edit, and the settled record names the rule that set its option. Search's whole-request test and README paragraph live here.
A convention whose scope the part does not hold no longer names the record; it reads bilateral_flows, so a fresh search and a resubmit agree.
…ep each record consistent A DAC1 total or bilateral ODA part gets a provider record only for a country, the EU institutions, or a declared group of countries, and never for a pool of an EU member with the EU institutions. On a DAC1 total, bilateral binds DAC1 bilateral ODA and is available to a switch; inclusive is unavailable with contributions_counted. Each option names its own rule, so a fresh search and a resubmit record the same rule, and the record never holds an option it marks unavailable. The provider axis has one option builder, and the published table is read once per identity.
…es a request matched A DAC1 total's bilateral option and bilateral ODA's contributions option are available only where a series covers the part's donor, basis and window. A provider record falls back to an available option on every table. A record names a convention's rule only when the request matched it in this call; otherwise each option names its own scope rule from PROVIDER_SCOPE_RULES, bilateral_flows or bilateral_plus_imputed. Availability objects are per option, the convention lookup is memoised, and AxisTable checks its held rule.
…lot options to published tables A sibling DAC1 option serves the years every donor in the part holds, read per basis segment on the headline, and is partial or unavailable otherwise. When a convention matched, a record falls back to the held option before bilateral. Slot-bound provider options are allowed only on published tables, published tables skip the CRS convention lookup, and the pass and the build share one composite predicate.
…erves A recorded bilateral on a base holding the inclusive composite now strips the composite, and a fallback names the served option's rule. A partial sibling total binds its covered run of years as the window (SlotBinding.window), a latest window counts a donor holding any year, and sibling and held years come from one coverage scan per family. A pool loses its record only when it pairs the EU institutions with an EU member, read from declared membership. AxisTable.rule derives from rules.
… holds A pool's sibling option compared its coverage with the years every donor held, so one donor holding none made the option fully available and a switch understated the pool. It now compares with the union of the donors' years, and a partial sibling states its covered run in its label.
…year A year counts as covered when every donor holding the part's own total that year holds the sibling too, so a donor with no own total places no demand. On a latest window the sibling elects the year its pool shares and pins it: available when it matches the total's year, partial with that year otherwise, unavailable when the donors share none. donor_years reads only the window's years.
A latest sibling whose elected year no donor demands is unavailable rather than raising, and a part whose own total holds no year judges the sibling per donor. The EU-institutions double-count guard reads membership over the part's window (policy_members_between), so a pool with the United Kingdom before 2020 gets no record. The SlotBinding.window description covers the latest case.
…quested edits produce A resubmit switching measure basis and provider together judged the DAC1 sibling on the old basis and refused it as option_not_covered, though the same switches one after the other compiled. Each explicit entry is now judged on the axis table read with the other explicit entries' edits applied; options that conflict with another requested option make no edit, so the conflict rule still decides that pair.
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.
Merge and deploy this only after DevelopmentFinanceObservatory/ai#90, the harness change that reads
provider:contributions, and DevelopmentFinanceObservatory/ai#91, which keeps a ratio's accepted DAC1 total from moving when a comparison aligns it, are live on dev. A harness without it reads the new option as unknown and words thecontributions_countedreason as a conflict.A DAC1 total ODA part carried no provider record, because
provider_ruleinsrc/search/scope/provider.pyserved only the CRS bilateral table. A CRS bilateral health figure was divided by DAC1 total ODA, which also counts core contributions to multilaterals.PUBLISHED_PROVIDER_SCOPESincode_index/scope_conventions.pynow declares a provider scope for two DAC1 aid types. Total ODA (1010) holdsprovider:contributionsand bilateral ODA (1015) holdsprovider:bilateral. Each offers the other as a switch through a slot binding.provider:inclusiveis unavailable on the total with reasoncontributions_counted, since the total already counts core contributions. A stated "bilateral" in a fresh search keeps the total and reports the entrynot_served, and a switch reaches bilateral ODA.The sibling option is judged per donor and per year. A year is covered when every donor that holds the part's own total that year also holds the sibling. When the sibling covers only part of the years the answer holds it is
partial, its label states the covered run ("bilateral flows only, 2016 to 2017"), and its binding carries that run asSlotBinding.window, a new optional field the harness ignores as unknown. On alatestwindow the sibling pins the year its donors share: available when that is the total's year, partial with that year otherwise, and unavailable when the donors share none.A part gets the record only when its provider counts as a donor: a country, the EU institutions (
DONOR_ORGANISATIONS), or a declared group made only of countries. Multilateral organisations and totals that include them get none, and so does a pool of the EU institutions with a state that was a member during the part's window (_pools_funder), which would count the member's EU contribution twice.A record names a convention's rule only when the request matched that convention in this call and the part holds its scope. Otherwise it names the served option's scope rule from
PROVIDER_SCOPE_RULES(bilateral_flows,bilateral_plus_imputed). A resubmit matches no convention, so several CRS expectations intests/scope/test_provider.pymove fromnamed_donor_total. No dfo-ai code readsrule. A record never holds an option it marks unavailable.A resubmit that switches several axes at once judges each requested option on the identity the other requested edits produce (
_requested_tablesinsrc/search/scope/build.py), so switching measure basis and provider together compiles as the two switches one after the other. Before, the DAC1 sibling was judged on the old basis and refused asoption_not_covered. An option that conflicts with another requested option makes no edit, so the conflict rule still decides that pair.Spec_ids do not change. The scope rule tables are hashed into the declarations, so the capability version moves to
cap_9594f5ce0957914a, and 82capability_versionpins intests/fixtures/first_answer_place_scenarios.jsonmove with it. Nothing else in those snapshots changed.Testing: tests/scope/test_provider.py (127), test_compile.py, test_parsed_door.py, tests/engine/test_dataset_resubmit.py and test_first_answer_place_scenarios.py pass, as do the rest of tests/scope and the graph_release format and publish tests.
test_a_dac1_record_never_holds_an_option_it_marks_unavailablechecks every table. ruff, ty and pre-commit are clean.