Repository navigation
feat(search): report the scope choices each answer settled, and count by the OECD's conventions - #163
Merged
Merged
Conversation
Search can now report how each part was counted. ResolvedSpec gains an optional scope_choices record: the part's base identity, the explicit and carried entries, one choice per settled axis with its options and their availability, and the compiled identity. A pure compile derives every default after the stated entries, never moves an explicit or carried entry, and refuses a selection it cannot serve. The dataset_resubmit door accepts scope_entries on an identity entry, compiles them over the posted identity and returns the compiled spec, refusing with requested_option_unavailable or conflicting_requested_options (422). The sector-grouping and measure-basis declaration tables join the declarations hash. No axis is live yet, so every response is unchanged. The spec-kit pins move to the commit that registers the two codes.
Parsed requests may carry the user's own message and the conversation's carried scope entries. A deterministic, negation-aware scanner turns stated provider and measure-basis words into explicit entries and notes "total" intent; a single-part request reads the message, a multi-part request reads each part's own text. Entries reach every part before the binders run and apply only to an axis the part settled (a sector entry only to its concept). The raw-text cache key includes the stated scope, so "bilateral" and plain queries never share an entry.
A part that sums over a recipient region binds the region's members plus the regional and unspecified buckets the served dataset publishes, and reports a region scope choice (members and residuals, or members only). Listings, rankings and "African countries" bind members only. Residuals are left out of every member count, ranking and unavailable entity by party kind, and the read-as-members departure names what the data allocates to no single country. A switch through dataset_resubmit round-trips to the first spec_id.
# Conflicts: # src/search/README.md # src/search/engine/region/pipeline.py
…oice A declared sector grouping turns its concept words into the grouping's code set with declared_grouping provenance: "health" binds CRS 120 + 130 and "humanitarian aid" binds 720, 730 and 740. A code the text names keeps its binding and states an explicit choice. The sector rule reads the base identity alone, so search and dataset_resubmit offer the same options: the grouping and each member code, with option_not_covered marking an option no covered member declares.
# Conflicts: # src/search/README.md # src/search/scope/build.py # src/search/scope/rules.py # tests/scope/test_build.py
…esh search's scope Both live axes register in rules.AXIS_RULES. build_part_scope applies the entry a binder read (AxisRecord.entry) by one rule: it joins the posted entries when the part's table on its axis holds the option the record settled, which covers an axis the answer does not reach and a base that no longer holds the binder's option. ScopeFacts keeps one shape, with the part's operations, groups and datasets in PartFacts. Carried entries reach read_sector and region_selection through ScopeState.entries_on; parsed-door tests pin a carried sector:120 on a "health aid" part and a carried region:members_only on a summed region. On the parsed and raw_text doors build_part_scope runs with refuse false: an entry compile refuses is dropped into dropped_entries (carried_choice_not_applied for a carried entry, the new stated_choice_not_applied for a stated one, with the refusal's cause), and the defaults derive without it. The 422s stay on dataset_resubmit scope_entries, so the request pointers ScopeIntake carried for them are gone. Option labels are standalone noun phrases: "health incl. population and reproductive health (CRS 120 + 130)", "health, CRS 120 alone", "Africa incl. regional and unspecified amounts", "Africa's countries only". The templates sit in code_index.scope_conventions.OPTION_LABELS and feed the rule version; the region is named from the part's group, else its total. The capability pins in the first-answer snapshot and the render fixtures are re-recorded.
The basis follows the question's shape and window: grant equivalents for a donor-only total from 2018 (the OECD headline, flows 10 and 60), net flows before 2018 and for a window crossing it, net disbursements for a recipient's all-sector total (DAC2a receipts), and gross disbursements once a sector, instrument or modality is bound. A stated or carried basis wins and drives dataset election, so a stated grant equivalent for a recipient reads the CRS. Each part reports a basis scope choice, and alternative_measures no longer lists siblings of another basis, so a settled basis raises no count question.
… the parsed cache on stated scope A fresh search compiles only the entries its binders applied and reports the rest as not applied, so the record never claims an option the served answer does not hold; a basis served off the convention records its own rule. Same-shape merges state the merged scope. A switch replaces the entry on its axis, and a posted entry without a concept takes its axis's concept. Grant equivalents read flows 10 and 60 however the basis was elected, so switches round-trip. The parsed cache keys on the stated scope, not the raw message. "commitment" in the pledge sense states no basis, region labels come from a declared name table, and the axis rules scan each family once per call.
…components A declared composite provenance names how its components derive from the composite identity: bilateral CRS plus imputed multilateral stacked over the years both cover, or the OECD headline spliced from DAC1 net before 2018 and grant equivalents from 2018. The mint materialises each component through the single-provenance path under its own pins, keeps only the composite window, writes a zero row where the imputed table leaves a covered cell empty, and stamps the composite on every row. Citations and a re-render from the version record name the component tables. The admission estimate counts the zero rows, a frame mixing raw provenances is still refused, and every reader of facets.provenance either expands the composite or refuses it.
…ed stated words and label prices as served Recall that finds only a dataset's amount family now carries the sibling families that measure a basis the conventions count it on, so a donor-only "ODA" from 2018 is counted in grant equivalents like "aid". A stated word on an axis no rule serves, such as "bilateral", is reported as not applied. Basis labels state the prices and currency the part is served in. A word naming a resolved entity is read as that entity and not rated as a qualifier, and judges read a declared grouping by its label.
A declared table maps graph regions to the DAC regions the OECD publishes totals for: sub-Saharan Africa, Asia, Europe, the Middle East, the African subregions and South America. On a part summing over such a region on DAC data, Search binds the DAC members and the residual amounts the served dataset publishes, so figures match the OECD; the region choice states the composition and offers the graph's member countries as the other option. Rankings, listings and every other dataset keep the graph's place.
…ds it A word from a resolved entity's name, such as "Gates", is read as the entity only when no qualifier or measure vocabulary reads it, so "women" beside UN Women or "malaria" beside the Global Fund is still rated against the series. Merged parts and resubmits report the basis rule the search records, and region totals skip the reading match for a single member.
A CRS part gains a provider choice: bilateral flows, or bilateral plus imputed multilateral over the years the imputed table covers, offered shortened with its window when it covers only part of the request. A total from a named donor defaults to the inclusive reading when it covers the request. A donor-only DAC1 window crossing 2018 is counted as the OECD headline, net to 2017 and grant equivalents from 2018. Both answer as one composite spec with its components, switch and switch back to the same spec_id, and read through the component tables wherever Search reads a provenance.
# Conflicts: # packages/dfo-data-tools/src/code_index/scope_conventions.py # packages/dfo-data-tools/src/graph_release/declarations.py # src/search/scope/basis.py
…d provider the part lacks A composite answer states its own headline year and series, and counts the StatVars of every component. A typed provider word on a part with no provider axis, such as a DAC1 donor total, is reported as not applied.
# Conflicts: # src/search/README.md # src/search/engine/region/pipeline.py # src/search/models/scope.py # src/search/scope/basis.py # src/search/scope/compile.py
…p its latest year out of the base A headline or inclusive reading becomes the default only when every component is available; otherwise the served shape stands with the rule it was served under. An inclusive latest window keeps the base on latest and elects the latest year both tables cover after compile, so a switch to bilateral answers bilateral's own latest year. The worker estimates the composed frame, and a composite is labelled from its declaration.
# Conflicts: # packages/dfo-data-tools/pyproject.toml # pyproject.toml # uv.lock
# Conflicts: # packages/dfo-data-tools/pyproject.toml # pyproject.toml # src/search/engine/departures.py # tests/fixtures/first_answer_place_scenarios.json # uv.lock
The restated composite takes its date range, its when slot and the data years its departures read from the years the components serve, so a latest-year answer names the elected year.
…ite both of its tables A composite mint passes each component's unserved members through to its coverage warning and writes no zero row for a member its facet group left out. A composite answer cites each component table and names its measure from the declaration.
… declarations The scope conventions read the dataset provenances and the CRS codes their groupings and basis tables name from code_index.declarations, the one module where dataset literals live.
…, over a year both tables hold The inclusive provider scope is offered for a part whose named providers the imputed table carries, and for a pool only when none of them is a multilateral whose own outflows the table attributes to its funders (REDISTRIBUTED_PROVIDERS), so a pool of the US and IDA, or a part naming no provider, keeps the bilateral count. A latest read elects its year among the years both tables hold. A composite zero row states the unit its cell states, so a member absent from one component takes its own currency.
…egion names, and build scope off the loop A carried sector entry applies to a part when the grouping offers it and it was made for the concept, or names codes the part's words matched. A part keeps alternative measures on other bases unless its measure-basis choice offers them. A region switch restates the recipient group for the members it reads, so the group name and departure survive. Every text origin indexes the part's normalised scope text, scope choices build in a worker thread, one elector holds the convention-dataset gate, and the declarations hash covers every scope table.
# Conflicts: # packages/dfo-data-tools/src/graph_release/declarations.py # src/search/engine/assemble.py # src/search/engine/entry/dataset_resubmit.py # src/search/engine/region/phase_election.py # src/search/scope/state.py
…es the part A convention row is a dataset and a basis: it applies when the elected shape reads that dataset, and otherwise the part takes the row of the dataset it reads, so a CRS-only recipient part binds gross. A convention basis with no series at the places falls back to the shape's default and records the departure. On grant equivalents ODA binds flows 10 and 60 and its label names both, and an echoed assumption holds only when it names the declared default the slot states.
…d compose each frame once A stacked composite serves the years every component table covers, so a year the imputed table covers keeps its bilateral figure with imputed zeros. Every path that restates a composite elects its latest window, explicit entries attach only where their concept holds, a composite provenance names its component datasets to unit conversion and skills, and the ceiling counts only the rows a composite keeps. The mint builds the composed frame once and folds the digest, statistics and output profile in that pass, cutting a 43,680-row composite mint from 7.4 s to 3.8 s with byte-identical output.
# Conflicts: # src/search/engine/region/phase_election.py
jm-rivera
marked this pull request as ready for review
October 27, 2026 10:46
jm-rivera
marked this pull request as draft
October 27, 2026 10:50
jm-rivera
marked this pull request as ready for review
October 27, 2026 10:50
…r's composite check The worker asks the stored definition whether it binds a composite before estimating, so the live over-ceiling tests stand in a definition whose identity binds none, and their estimate stand-in takes the composite it is given.
… and pin spec-kit 455e958 A place named without a direction has no part shape until its direction settles, so basis election leaves it to the house policy. Test fixtures model the net members DAC2a publishes, the pass-end test holds only its listing on the held pool, model registries list the new fields, and every dfo-spec-kit pin moves to 455e958 on spec-kit dev.
This was referenced Oct 29, 2026
Merged
jm-rivera
added a commit
that referenced
this pull request
Oct 29, 2026
…e answers by component (#186) #163 made Search answer by the OECD's conventions: a recipient's total in net flows, a donor's total from 2018 in grant equivalents, a sector, instrument or modality part in gross disbursements, and "health" as CRS 120 and 130. The corpus still expected the earlier answers, so 67 cases regressed against main. Each now expects the convention's answer, with a `why` that names the rule, and `dac1.disbursement_net` joins the DAC families. The materialisation check counted no observations for an answer served by the OECD headline composite, because no graph series carries a composite's provenance. It now derives the components with `component_identities` and sums each component's count under its own table's provenance and inside its own years.
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.
Search now reports, for each part it answers, the scope choices it settled on the sector, region and measure-basis axes, with every option the part could have been counted on, each option's availability and reason, and the rule or user words that chose the active one. The record is
ResolvedSpec.scope_choices(src/search/models/scope.py), built after dataset election bypipeline._with_scope_choicesfrom thesrc/search/scope/package: per-axis rules insector.py,region.pyandbasis.py, a purecompilethat derives defaults after the explicit and carried entries, and declared tables incode_index/scope_conventions.pyregistered in the graph release declarations.The defaults change what several questions return. "Health" binds CRS 120 and 130 together. A part summing over a recipient region binds the region's members plus the regional and unspecified buckets the served dataset publishes, and a graph region with a DAC regional total (sub-Saharan Africa, Asia, Europe, the Middle East, the African subregions, South America) reads as the OECD counts it on DAC data; rankings, listings and other datasets keep the graph place. The measure basis follows one convention table: a donor total from 2018 in grant equivalents, before 2018 in net flows, a recipient total in net flows, and a sector, instrument or modality part in gross disbursements. These figures match the OECD's published cells to within 0.02% on the rows checked, and the gross-or-net question the harness used to ask drops from 62.6% to 3.3% of definite aid answers.
A client switches an option by posting the stored base identity with
scope_entriesthroughdataset_resubmit; one entry per axis, a switch replaces the entry on its axis, and switching back returns the original spec_id. Only a resubmit refuses, with the two codes the spec-kit PR registers. A fresh search never refuses on scope: a stated option nothing serves is reported underdropped_entries. The parsed request acceptsuser_messageandcarried_entries, and the parsed cache keys on the scope they state.Reviewers should challenge the convention table in
scope_conventions.py, the region reading inphase_intake.oecd_readingandplace_roles._with_region_residuals, and the entity-word rule inphase_rank_bind._resolved_name_words.Merge order: the spec-kit registry PR first, then re-pin
dfo-spec-kithere to its merge commit; this PR and the dfo-ai scope-choices PR merge together, since the harness sendsscope_entriesand reads the new record.Testing: targeted suites (
tests/scope, stated scope, place settling, qualifier guard, departures, first-answer scenarios, render,test_corpus_offline) pass locally; the full suite runs on CI. Live against the dev graph on a local server: OECD comparisons for US 2019, sub-Saharan Africa and Asia, switch round trips, eight Gates-to-Senegal trials, and cold latency over 25 aid probes at +26 ms p50 and +124 ms p90.