Repository navigation
fix(harness): divide a share by its own dataset's total, keep a switch's plan valid and send the plan card's text - #90
Merged
Conversation
…mmary, answer step and failed switch Contract only: wire.py gains PlanApproved, PlanSwitchFailed, PlanProposed.summary and switch_failed, and PlanStep.gives_answer, with fixtures and the delta in docs/studio-contract-v1.md. Nothing emits them yet.
…ason on the card and shorten the plan's text A share whose whole comes from another dataset is realigned, with no model call, to the part's own dataset with its sector unbound; a stated source keeps its whole and a plan decision states the providers differ. Provider scope aligns for shares, quotients and sums only. A switch clears the stated source on the requirements it re-pins, a translation referral in revise refuses as plan_not_ready with its reason, and every refusal code has a sentence that names inputs in reader words. A failed switch re-offers the card with switch_failed. Approving emits plan.approved. The plan card carries a title, a summary and short step labels with the answer step marked, and "Counted as" names each input once. The harness reads the provider:contributions binding and counts_core_contributions reason Search will send for DAC1 totals.
…e, and name every input in reader words A whole's source counts as stated only when the user's own words carry a short name of its dataset (DAC1, CRS) that the part's dataset lacks; a planner-written source no longer keeps a cross-dataset whole. A whole read by another calculation refuses for repair with share_whole_underivable. part_name never returns a spec_id, so the Counted as line and every reader sentence name inputs by concept, measure or dataset title. The recorded share fixture keeps only what the tests read.
…is row blocked only by another choice A quotient of a summed sector part over a total of the same parties from another dataset is a share, so its whole is realigned to the part's own total. A scope choice whose other options are unavailable only because of another choice (conflicts_with) stays on the plan card.
… a re-pinned whole's stated source A share or quotient of a summed sector part over an unpinned total of the same parties compiles its whole from the part's own dataset with no search, so a total Search cannot settle no longer makes the planner decline the share. A whole compiled from its part drops the stated source text left from the old pin, with the same repinned helper a switch uses, so discovery no longer refuses it with pinned_source_not_found. A realigned whole replaces the planner's decision on its source with one written from the scope records.
refusal_sentence is model-facing: the planner repairs a plan by requirement id, so it names each requirement by id. refusal_reason, the user-facing switch notice, names inputs by what they count.
…d keep Studio choices stable A dataset requirement's whole_of names the part it is the whole of and the axis left open; the harness validates it against the part's scope records and compiles the whole from the part's own dataset with no search. Undeclared shares are validated and never rewritten, and accept_provider_difference exempts one calculation from provider alignment. Decision.caveat marks the decisions plan.approved carries as caveats. choice_id is built from the requirement and axis, a Stop during a switch re-offers the unchanged plan, switch_failed carries retryable, and changed_from holds across switches. data-mcp learns contributions_counted.
…und across instances and deploys plan.proposed carries the same caveats as plan.approved. A provider mismatch is refused as providers_unaligned for repair. A switch on an id from a plan paused before this release still finds its row, a second Stop on another instance ends the stream, and switch_stop is armed inside the stream. A Search outage during a switch is retryable, a stopped switch carries a neutral reason, and the turn's choice baseline resets on a redraft. Scope decisions carry their provenance, so the default reason is hidden structurally; card summaries key facts by role; contributions options are read by what they bind.
… read slot-bound provider options accept_provider_difference skips only the provider-mismatch refusal. An unknown provider option drops that option alone. Caveat decisions are exempt from the decision cap, and providers_differ is written only when the inputs' provider scopes differ. A declared whole keeps its own switched choices. plan.approved goes out once, after the approve node accepts the answer. Provider options bound by slot, as on DAC1 totals, read with their scope. Card summaries keep the share clause under a rank or aggregate, card and step rows share one core, and a switch snapshots the drafting state once.
…oved exactly once A providers_differ caveat is written unless every input has a provider record and they match, and every flagged calculation's caveat is recomputed on each revision, carried or restated. Each declared whole states its own derived_whole caveat. An unknown active provider scope drops that choice alone. A cancelled switch node re-raises its own cancellation, the re-offer deadline is checked before a ready frame, and a retried approval sends plan.approved if the first attempt did not. Refusal reasons name an earlier turn's pin from its own block; switch failures carry their ids once, and dataset names come from BlockDataset.name.
A decision with a caveat reaches the card only through caveats, never as a row or a requirement's citation. Two rows on one axis name their inputs. A providers_differ caveat states one plain sentence per input. A whitespace ask falls back to the plan title, losing the segment after plan.approved ends the stream, a restated decision a carried requirement cites is kept, and a whole that cannot be read names its own cause through unread_part.
A comparison subtracts its inputs, so it is aligned on provider like a share, quotient or sum, with the same accept_provider_difference flag and caveat. A carried decision cited only by a whole restated with whole_of is dropped, row questions name inputs as the card does, a caveat with no dataset name drops the "from" clause, and the card and caveats share one naming rule (concept_name) that never returns a fragment such as "for official development assistance".
A whole the planner restates by requirement id keeps the options the user switched on its row, and alignment never moves a declared whole. A calculation whose inputs the user set to different provider scopes carries the providers_differ caveat. Three refusals name their requirement, and a Stop ends as stopped however the revision unwinds.
Inputs other than declared wholes align first, then each whole compiles
from its part's final pin with its own switched options, so a part moved
by another calculation takes its whole with it. The derived-whole
caveat leaves out axes switched on the whole's row, a re-offered plan
clears the switch deadline, a move with no card row is listed once, a
whole is named by the axis it opens ("all sectors"), and a side read
from a shared retrieval names its entity.
A revision whose translation ends on an unanswered service refuses with DISCOVERY_UNAVAILABLE or SURFACE_UNAVAILABLE, both retryable. A declared whole offers no row on the axis it leaves open and a switch on it is refused. A caveat whose rationale passes the limit falls back to a shorter form, a segment lost at the re-offer deadline keeps its reason, and the digest words a declared conflict as a conflict.
…ixed target A PROVIDERS_UNALIGNED refusal records an optional decline, so the planner can decline when no repair applies. A declared whole is a fixed target for any other calculation that reads it, removes only the planner decision that fixes its measure, and takes its part's scope choices with their provenance, writing rows only for axes switched on its own row.
…ties once Which pins are wholes comes from the requirements that declare whole_of, so a pin an earlier plan declared a whole behaves as a plain requirement once the planner drops whole_of. A whole takes its part's scope and has no row, so the code for switching a whole's own row is removed. A retrieval named apart by its parties names them once, a chip failure keeps its code and retry mode, and the digest reads a declared conflict's cause only from a mapping.
…reading by the whole A Stop landing in the same wait as a finishing revision ends the switch as stopped. A failed or stopped switch restores the plan, the turn's readings and blocks along with the drafting state, so a whole compiled during it leaves no reading behind. A declared whole's reading is named by the whole (whole_name), never by its part's words.
A whole_of requirement, alone or summed, may be read only as the denominator of a share or quotient over its part; any other read, or none, is refused at admit with INVALID_PLAN and text telling the planner to fetch a plain total. The fixed-target alignment for other calculations is removed. The switch node restores state on any exception, and compile_whole keeps the part's source fields.
…set labels on every redraft Only a pin that declared wholes alone hold loses its scope rows, so a plain requirement on the same spec keeps them. The turn's switched labels reset whenever the planner drafts, which covers an execute replan as well as an amendment.
basis_free matched each basis phrase between word boundaries, so a label ending in a bracket, such as "grant equivalents (OECD headline)", never matched and the card kept the old basis words after a basis switch.
…share test card_summary named only each ratio's numerator for a comparison or sum of shares, so a difference of health shares read as health aid amounts. It now returns None there and the card shows its title. The seam test for a share whose total has no pin runs again for a share as well as a quotient, and the slot length limit comes from planner.records.
…by its wider figure A quotient that divides by its whole through a sum had its caveat worded as a share, and the double-count refusal blamed imputed multilateral flows when the wider input was a DAC1 total counting core contributions.
…ir slots A planner decision on a derived_whole or providers_differ slot replaced the code's decision and its caveat vanished; code's now holds the slot. A sum whose inputs sit on two known provider scopes states the difference even when a third input has no provider record.
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.
Deploy after DevelopmentFinanceObservatory/website#74, which reads the new plan fields, and after DevelopmentFinanceObservatory/apis#165, and before DevelopmentFinanceObservatory/apis#166, whose
provider:contributionsoption this harness is the first to read."What share of US aid went to health in 2023?" divided CRS bilateral health aid ($8.48bn) by DAC1 total ODA ($65.0bn), which also counts core contributions to multilaterals, and gave 13.05%. A dataset requirement can now declare
whole_of: {part, axis: "sector"}, the whole of another requirement with that axis left open._aligned_basesinsubmission/service.pyaligns every other input first, andcompile_wholeinplanner/agent.pythen compiles the whole from the part's final pin through Search's resubmit, with no search. The share reads 14.2% ($8.48bn of $59.7bn). A whole takes its part's scope and has no card rows of its own, so the part's row moves both. The plan validator (whole_of_faultinplanner/plan.py) admits a whole only as the denominator of its part's share or quotient, read directly or summed, and refuses any other read as an invalid plan.A share, quotient, sum or comparison is aligned on one provider scope. An input its dataset cannot serve on the other inputs' scope is refused as
providers_unalignedfor the planner to repair or decline.accept_provider_differenceexempts one calculation, for a user who asked for another source's total. A code-setDecision.caveatstates a derived whole (derived_whole) and inputs on different provider scopes (providers_differ), whether the planner flagged them or the user's switches set them. On dev Gemini the planner declaredwhole_ofin 9 of 9 runs where it applies, in none of 6 where it does not, and set the flag only when asked for the DAC1 total.Switching a plan's provider failed with "No plan was made for this request", because the planner's stated source text survived the re-pin and discovery refused it.
repinnedinplanner/plan.pyclears the stated source wherever a requirement is re-pinned. A failed switch re-offers the card withswitch_failed(choice, code, reason,retryable) and sends no chat message.plan_display.pywrites the card's text: a title, a one-sentence summary (left off for a calculation over ratios), step labels, the answer step flaggedgives_answer, andcaveatsonplan.proposedandplan.approved.choice_idis built from the requirement id and axis, so it holds across switches and merged rows. A Stop during a switch cancels the revision and re-offers the unchanged plan with a live pause, withinSWITCH_REOFFER_SECONDS. Approving emitsplan.approved, whosechanged_fromholds the label from the turn's first plan. These frames are deltas 22 and 23 indocs/studio-contract-v1.md. data-mcp learnscontributions_counted.Left out: provider records for DAC1's other aid types and DAC2a, and a
whole_ofon any axis but sector.Testing: the harness, planner, translator and data-mcp scope and digest tests pass, and the full CI suite runs on this PR. Local harness at this branch with the website branch, against dev Search: the US health share divided by the CRS total and gave 14.19%, the DAC1 variant kept DAC1 with its caveat at 13.05%, a stopped switch left the plan approvable, and approving after a provider and a sector switch sent
changed_fromon both choices. ruff, ty and pre-commit are clean.