Skip to content

release: ai 0f949ec to production - #121

Merged
jm-rivera merged 29 commits into
mainfrom
release/2026-10-29
Oct 30, 2026
Merged

jm-rivera merged 29 commits into
mainfrom
release/2026-10-29

Conversation

@jm-rivera

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

Copy link
Copy Markdown
Contributor

Releases ai at 0f949ec to production, cut on release/2026-10-29 at b9ad157 so the dashboard work in dev ships next release, plus #122: 28 fixes and features, each deployed and green on the dev service.

Scope choices: #88 states, carries and switches the scope choices Search settles, reading resolved.scope_choices from apis #163, #95 switches planner decisions from the plan card, #106 names each approved choice by its choice_id, and #103 and #104 hold the user's price choice and word decision options for the reader. Shares and plans: #79, #80, #85, #90, #91, #93, #109 and #110 fix how a share names and divides by its whole, keep its sides apart and refuse a share of one series by itself; #108 derives input alignment per requirement from the planner's statement, and #122 (in dev as #123) switches a row of a compile the planner pins directly, from the block the card shows, and clears a plan's alignments with the plan; #78 and #83 leave a pinned source's flow count to propose_plan, #81 and #102 state a plan's counts and basis lines, and #82, #84, #86, #87 and #94 keep pins, entities and named donors bound to what the user said. Performance: #92 reuses resolves, reads and listings between plan, switch and translation, and #96 mints a card's data before approval, sending those mints as background (X-DFO-Mint-Admission: background) for apis #169 to admit.

Stored bound refs: spec blocks store _bound_refs as BoundEntity rows, and this release reads only that form. #107 adds harness.bound_refs_migration and #112 builds its rows with bound_rows, the function the prod hotfix (#111) writes with. Before merging: deploy #111 to production, wait 15 minutes, then run the migration's dry run and --apply against production as deploy/README.md ("Bound refs migration") describes, on this release's image. #111's merge into main conflicts with this branch around bound_rows in compiler/sources.py and in the new scope modules; the conflicts resolve to this branch's side.

The deploy adds no env var, secret, workflow or dependency change, and the spec-kit pin stays at 708f293. Merge after the apis release has deployed: on today's apis main the harness shows no scope choices, its background mints compete with users' mints, and prod Search states one entity-refs entry per distinct text, which this release's bound-row writer pairs per row.

jm-rivera and others added 28 commits October 25, 2026 19:14
data-mcp's SearchEntity takes an optional name or dcid, never both, and
sends them, so an agent can post "Congo" twice and settle an
entity_ambiguous refusal by resending the row with the candidate's
dcid. The digest lists each ambiguous entity group by its entity_choice
index, with its words, role and candidate dcids.

The harness keeps Search's entity_refs per restatement row: BoundEntity
carries the row's identity (dcid, else name, else text), text, role and
refs, and SelectedSource.bound_refs is a tuple of them. Evidence
resolution matches each requirement entity to its own row, by identity
then text, fitting the role first, so two Congos in one role column or
in two both resolve. A saved plan with the old dict form fails
validation and is planned again. The search capability's input hash is
re-pinned for the new fields.

Co-authored-by: Claude <noreply@anthropic.com>
Discovery sends a requirement's entities to Search as bare text, so
"aid from Congo to Congo" gets no_data with one ambiguous_entities
group. Discovery dropped that group and referred the requirement as
plain no_data, so the planner had no way to learn or settle which
Congo each side meant.

outcome_from_envelope now reads each group into one
AmbiguousEntityReading per entity it pins, mapped by the rows its
options change. discover_source refers them as entity_name_ambiguous,
whose repair carries the settle-or-ask guidance. Entity.dcid lets the
planner state the node, admitted only when a pending referral offered
it for that entity or the previous plan held it. Discovery sends it to
Search, and evidence matching keeps two entities written alike apart.

Co-authored-by: Claude <noreply@anthropic.com>
…s plan (#79)

* fix(harness): name the total a share divides by on its card and in its plan

"What share of ODA to Kenya came from the United States?" divides by
official donors (DAC) in some runs and by all providers in others, and
neither the card nor the approved plan said which. make_charts now
writes the whole into a share card's title from the denominator's
selected source, as it writes the period, and the plan's fetch label
and share sentence name the group or member count that source holds in
place of "every donor".

* fix(harness): state a share's whole only where its rows reach the total

A denominator filtered by value or cut to its top members before its
total still read "of the total from all 140 donors"; a per-period rank
writing positions carried the share's whole; and the whole a numerator
also holds was matched by wording. share_wholes now names no whole past
a narrowing step, stops at a step whose output is not a ratio, and
compares populations by group ref or axis and member count. The plan's
share sentence names the narrowing step in place of the parties.

* fix(harness): name a share's whole only where the plan shows what it is

Two open populations are the same only when their sources are one dataset,
since sources record counts and no members; a different dataset of as many
donors keeps its clause. A denominator reduced by a mean, maximum or minimum
names no total, and neither does one narrowed to one entity. A card title
keeps its whole only where the whole subject fits beside it in 120
characters; otherwise the whole goes in the card's note.
The planner instruction asked for a decision on every measure, so the
model wrote its own gross/net decision beside the "How the flow is
counted" decision propose_plan records for each pinned source, and the
plan listed the flow twice. The instruction now asks for a decision on
a measure no pinned source settles, and says a pinned source fixes its
flow count, with another count reached by pinning that count's spec_id.
* fix(harness): name the total a share divides by on its card and in its plan

"What share of ODA to Kenya came from the United States?" divides by
official donors (DAC) in some runs and by all providers in others, and
neither the card nor the approved plan said which. make_charts now
writes the whole into a share card's title from the denominator's
selected source, as it writes the period, and the plan's fetch label
and share sentence name the group or member count that source holds in
place of "every donor".

* fix(harness): state a share's whole only where its rows reach the total

A denominator filtered by value or cut to its top members before its
total still read "of the total from all 140 donors"; a per-period rank
writing positions carried the share's whole; and the whole a numerator
also holds was matched by wording. share_wholes now names no whole past
a narrowing step, stops at a step whose output is not a ratio, and
compares populations by group ref or axis and member count. The plan's
share sentence names the narrowing step in place of the parties.

* fix(harness): say what a plan's member and indicator counts count

A plan said "Uganda and all 108 donors" and "summed over its 288
indicators" while the answer said 92 providers with data in 2022 and 13
indicators, with nothing to tie the two. The plan now says "the 108
donors the source holds in any year" and "summed over whichever of its
288 indicators hold data", a share card's whole names an open axis with
no count, and the orchestrator instruction quotes the new phrases.

* fix(harness): state the counts a plan's fetch reads, not an any-year guess

The plan's donor count is already the providers with data in the plan's
years (and, with dfo-apis counting parties only, the donors the frame
holds), so "the N donors the source holds in any year" was false; it
reads "all N donors" again. The indicator clause states Search's exact
count of indicators with data where every entity axis is named
("summed over its 13 indicators"), and the family size otherwise. A
flow side's count comes from role_axis_facts over the scope list's
length, and the orchestrator instruction says what each count counts.

* test(harness): pin count-free share wholes after the population identity rule

* fix(harness): name a family's size only from Search's served count

* fix(harness): name a share's whole only where the plan shows what it is

Two open populations are the same only when their sources are one dataset,
since sources record counts and no members; a different dataset of as many
donors keeps its clause. A denominator reduced by a mean, maximum or minimum
names no total, and neither does one narrowed to one entity. A card title
keeps its whole only where the whole subject fits beside it in 120
characters; otherwise the whole goes in the card's note.
…ount (#83)

* fix(planner): leave a pinned source's flow count to propose_plan

The planner instruction asked for a decision on every measure, so the
model wrote its own gross/net decision beside the "How the flow is
counted" decision propose_plan records for each pinned source, and the
plan listed the flow twice. The instruction now asks for a decision on
a measure no pinned source settles, and says a pinned source fixes its
flow count, with another count reached by pinning that count's spec_id.

* fix(submission): refuse a decision restating a pinned source's flow count

Under the planner instruction that leaves a pin's flow count to
propose_plan, 14 of 131 pinned plans still listed the flow twice: the
model wrote its own decision fixing the measure beside the code's
"How the flow is counted" decision for the same pin. propose_plan now
refuses such a draft as measure_fixed_by_pin when a requirement citing
it also cites the code's decision for its own pin.
…erator holds others (#85)

* fix(harness): name the total a share divides by on its card and in its plan

"What share of ODA to Kenya came from the United States?" divides by
official donors (DAC) in some runs and by all providers in others, and
neither the card nor the approved plan said which. make_charts now
writes the whole into a share card's title from the denominator's
selected source, as it writes the period, and the plan's fetch label
and share sentence name the group or member count that source holds in
place of "every donor".

* fix(harness): state a share's whole only where its rows reach the total

A denominator filtered by value or cut to its top members before its
total still read "of the total from all 140 donors"; a per-period rank
writing positions carried the share's whole; and the whole a numerator
also holds was matched by wording. share_wholes now names no whole past
a narrowing step, stops at a step whose output is not a ratio, and
compares populations by group ref or axis and member count. The plan's
share sentence names the narrowing step in place of the parties.

* fix(harness): say what a plan's member and indicator counts count

A plan said "Uganda and all 108 donors" and "summed over its 288
indicators" while the answer said 92 providers with data in 2022 and 13
indicators, with nothing to tie the two. The plan now says "the 108
donors the source holds in any year" and "summed over whichever of its
288 indicators hold data", a share card's whole names an open axis with
no count, and the orchestrator instruction quotes the new phrases.

* fix(harness): state the counts a plan's fetch reads, not an any-year guess

The plan's donor count is already the providers with data in the plan's
years (and, with dfo-apis counting parties only, the donors the frame
holds), so "the N donors the source holds in any year" was false; it
reads "all N donors" again. The indicator clause states Search's exact
count of indicators with data where every entity axis is named
("summed over its 13 indicators"), and the family size otherwise. A
flow side's count comes from role_axis_facts over the scope list's
length, and the orchestrator instruction says what each count counts.

* test(harness): pin count-free share wholes after the population identity rule

* fix(harness): name a family's size only from Search's served count

* fix(harness): name a share's named members as its whole where the numerator holds others

* fix(harness): read a narrowed numerator's own frame when naming a share's whole

A numerator narrowed to one entity now holds what its own input's sources
hold, with that entity on the column its requirement names it under
(side_entities), in place of assuming the denominator's frame. A share of
the US out of UK and US ODA to Kenya, divided by the two donors' ODA to
Kenya and Uganda, now names the recipients too.

* fix(harness): name a share's whole only where the plan shows what it is

Two open populations are the same only when their sources are one dataset,
since sources record counts and no members; a different dataset of as many
donors keeps its clause. A denominator reduced by a mean, maximum or minimum
names no total, and neither does one narrowed to one entity. A card title
keeps its whole only where the whole subject fits beside it in 120
characters; otherwise the whole goes in the card's note.
…admission (#80)

* fix(submission): compare a share's whole to its part by graph ref at 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.

* fix(planner): count a pooled whole's members as the union of their refs

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.
…ntity (#82)

"What share of ODA to Kenya came from the United States?" could ask the
same single-option dataset question twice and rework its draft four
times. The tokenizer split Search's label "OECD DAC2A" into "dac2, a",
so a requirement stating "OECD DAC2a" re-searched a pin it named, and
the discovery log answered identities only for a logged source's
alternatives, so a candidate the user accepted had none on record and
its open period was refused. A digit now splits from a capital only
where the capital starts a word, and the log's recorded_definition
answers each logged source's own identity for admission, windowing and
the first-year read.
Bind entity answers to the requirement and reading they settle. Use the
authored requirement ID for history-copy choices across referrals,
questions, and submission checks.

Preserve replayable group-member candidates under Search's registered
names.
…es (#88)

* feat(harness): store scope choices and carry them into later searches

SearchReading keeps each part's resolved.scope_choices, persisted with
the turn and rendered as one ledger line per choice for the planner and
recall. The model-facing part summary is unchanged. Parsed searches send
the user's own message (from a contextvar set at turn start and on
resume) and the conversation's stated choices as carried entries, which
data-mcp forwards to the Search API only while
search_sends_conversation_context is on.

* fix(data-mcp): bound the conversation context sent to search

Send the newest 4,000 characters of the user message, so appended
amendments survive, and the 16 newest carried entries, matching the
Search API limits.

* feat(data-mcp): list scope choices in the search digest

The digest prints one line per scope choice (axis, active option,
provenance and the other options a client can ask for), and the
structured answer carries the same record. The region eval accepts a
summed region with its own regional and unspecified buckets, and the
count-and-prices eval no longer expects a gross or net question where
the convention table settles the basis.

* feat(harness): end each answer with how its figures were counted

fetch and the plan execute node keep a compact record of what was
served (members and residuals per role, measure basis, components) on
the reading. finish() appends one "Counted as:" line built from the
stored scope choices and their search labels, corrected where the
served figures depart, beside the loose-match notes, so it reaches the
stored turn and the next turn's history. The planner's ledger text is
unchanged.

* feat(planner): put each switchable scope choice on the plan card

propose_plan writes, in code, one decision per scope choice on each
pinned source that has another available or partial option, citing it
from the requirements that pin the source, user-resolved for explicit or
carried choices and planner default otherwise. Decisions past the plan
cap are left to the basis line. The decision keeps each option's
availability, so the card and a later switch read from the plan alone,
and the Studio wire carries the choice id, the active option and the
options with reader reasons.

* feat(harness): switch a scope choice on the plan card without the planner model

A switch answer on a plan pause is checked against the pending card (a
stale revision is refused 409, an option not offered or unavailable
422) and routed to a deterministic node. The node posts each affected
source's base identity with its scope entries and the switch to the
Search API through data-mcp, which registers the compiled definition,
then revises the plan through propose_plan's own chain with no model
call and offers it again with what moved. A switch writes no approval,
and a refusal re-offers the unchanged plan with a note.

* fix(harness): state the basis line only for what was served and send search the user's own words

A fetch narrowed to some of a region's places drops the region clause;
a plan reading a windowed copy states the line of the reading it was
copied from. A switch label stays in the turn record but never reaches
search as the user's message. The scope record is read leniently, so a
field the Search API adds never drops the choices. data-mcp types each
carried entry and logs whether it forwards the conversation context.

* fix(harness): answer a stale plan action with the current card

A switch or an approve naming a plan revision the user no longer sees is
refused 409 plan_revision_stale with the pending plan card, stored with
the pause exactly as the stream sent it, so a client replaces its card
instead of acting on a plan it has not seen. Approve without a revision
behaves as before.

* feat(planner): put the inputs of a calculation on one measure basis

A share, ratio, sum, comparison or join reading several sources has its
inputs put on one basis before the plan is recorded, unless the user
stated different bases on them. The target is the basis the user stated,
else the narrowest input's; other inputs are recompiled through the
same Search resubmit a plan-card switch uses, with no model call, and
stored as readings. A basis the planner chose this way is marked on its
spec block, so it never carries into later searches as the user's
choice. An input that cannot be counted on the target refuses the plan
as operation_unsupported, naming the mismatch.

* fix(harness): honour a pin compiled in code, keep the basis line out of history and label switched inputs by their source

The translator reads a pinned identity from the pin's own spec block,
so a source a switch or basis alignment compiled during the request is
fetched as pinned. The turn record stores the basis line apart from the
answer, so the next turn's model never sees it to copy. A step reading
a dataset governed by a basis or sector choice names its input as
Search counts the served source.

* fix(planner): pin every input of a calculation before putting it on one basis

An input of a multi-input calculation that the planner left unpinned is
searched in code with the words the translator would send, pinned to
what Search serves and stored as a reading, so a basis alignment or a
switch moves it with the rest. Code-appended reply notes are stored
apart from the answer, like the basis line, so the next turn's model
never reads them.

* fix(orchestrator): state open prices in code so a multi-year answer asks them

A definite search part whose window spans several years, whose unit is
money in current prices and whose prices the user left unstated carries
open_prices, the options of its prices question with search's reading
first. The orchestrator asks prices from that field, so the question no
longer depends on another counting choice being open beside it.

* feat(harness,data-mcp,analysis-mcp): read, state and align composite frames

get_data states each component of a composite frame by counts and year
spans, and the change of measure at 2018 in a spliced one; component
member lists are bounded like resolved ones. The basis line names an
inclusive frame as bilateral plus imputed multilateral with its years,
and a headline frame as net to 2017 and grant equivalents from 2018. A
calculation puts its inputs on one provider scope as it does one basis,
and refuses a sum of inputs the user stated on different providers. An
analysis verb given a composite frame points to the summed get_data
read, where dfo-apis adds the components once.

* feat(harness): offer one-click chips that switch a reading after an answer

After a fast-path answer, Studio suggests up to four chips, one for
each available option of the scope choices the answer read. A chip
turn resubmits the stored base with the chosen option through the same
path a plan-card switch uses, stores the reading, fetches the compiled
spec and answers with the basis line, with no search or plan call. A
stale or unavailable chip is refused before the turn starts. Chips
shown, applied, kept and reversed are recorded as telemetry events.

* fix(harness): a chip refetches the answer's own places and years

The fetch arguments that shape a frame are stored on the reading, and a
chip turn fetches the switched spec with them, so a chip changes only
the chosen reading. An answer that named its places offers no chip on a
choice that binds places, since the refetch would not change them.

* fix(planner): treat every scope the planner's words settled as the planner's

A spec block marks the scope axes the planner's own words or alignment
settled. Those choices never carry into later searches as the user's,
never count as a basis the user stated, and show on the plan card as
the planner's. A calculation input that search cannot settle to one
dataset refuses the plan, since one basis cannot be guaranteed.

* docs(harness): state the scope choice rules as they hold

* fix(harness): a chip repeats every fetch of its reading, and a turn names a message or a chip

A reading keeps the arguments of each fetch that returned data, and a
chip fetches the switched spec once per entry, so an answer that read
Ghana and then Kenya is switched for both. A turn request carrying
neither a message nor a chip is refused at the boundary.

* docs(harness,analysis-mcp): state the chip and composite rules as they hold

* fix(harness): name a re-pinned part by Search's label for what it serves

After a switch or a basis alignment, the basis line names the part by
the label Search gives the served pin, so the name never states a basis
the clause contradicts. A part still on its first pin keeps the words it
was asked in.

* fix(data-mcp,planner): state the scope Search could not honour and compile each alignment once

The search digest and its structured content state each entry Search
dropped: the option asked for, its provenance, the option served and the
cause in words, so an MCP client learns why an explicit request was not
applied. The planner compiles each source and option set once, however
many requirements ask for it.

* fix(harness): apply a chip only on the answer that offered it

A chip names the turn whose answer offered it and the reading it
switches, and is refused as stale once a later turn has answered or a
later reading holds its choice, so an old chip never switches a reading
the conversation has moved past.

* fix(planner): compile an input on every axis it moves on in one resubmit

A realignment names one option per axis an input moves on, so an input
counted gross and inclusive can become net and bilateral in one
resolve_definition resubmit carrying both entries. The compiled pin is
keyed by the origin and that option set, and inputs sharing both compile
once.

* fix(harness,data-mcp): retry what Search may answer later and align nested calculations as one group

A retryable Search refusal for an unpinned input is answered as
retryable and never held. Calculations sharing an input are aligned as
one connected group, a basis the user chose in an earlier turn counts as
stated, the decision cap counts only decisions that survive a re-pin,
and the input search and compile take the time the entry has left. A
scope record from a newer Search keeps every part it can read. The plan
switch resubmits through resolve_definition with the groups every
resubmit posts, and data_mcp.scope holds the one definition of scope
entries, axes and reason wording.

* fix(harness,mcp): bind a chip to the reading it names and serve sums over disjoint donors

A chip resubmits with the groups its reading was resolved with, is
refused stale when seed finds its choice on another reading, is offered
only on the newest reading holding its choice, and fetches through the
failure handling every agent fetch gets. A sum over different stated
provider scopes is refused only when its inputs share a donor or one
counts every provider. A composite refusal names the side whose
components hold the refused StatVars, and common.composite holds the one
reader of trace components and year spans.

* fix(evals): send the search context the harness sends and declare the scope decision fields

The planner-entry eval takes the Resolver protocol from
harness.orchestrator_tools, so its recorded and recording resolvers
accept and replay the search context the real one sends, and its probe
passes revise through. The revision base format lists DecisionScope and
ScopeOptionState.
…h's plan valid and send the plan card's text (#90)

* feat(harness): declare the plan.approved frame and the plan card's summary, 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.

* fix(harness): align a share's whole with its part, keep a switch's reason 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.

* fix(harness): keep a share's whole only when the user names its source, 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.

* fix(harness): realign a quotient's cross-dataset whole and keep a basis 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.

* docs(harness): tighten the comments and reader sentences this branch added

* fix(harness): build an unpinned share's whole from its part and clear 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.

* style: apply the JSON and Markdown formatters to this branch's fixtures and docs

* fix(harness): keep requirement ids in the refusal text the planner reads

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.

* fix(harness): derive a share's whole only from a declared whole_of and 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.

* fix(harness): send caveats with the plan, keep Stop and switch ids sound 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.

* style: apply mdformat to the frontend interface doc

* fix(harness): keep double-count checks under a provider exemption and 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.

* fix(harness): keep caveats current across switches and send plan.approved 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.

* fix(harness): show each caveat once and tell same-axis rows apart

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.

* fix(harness): align a comparison on provider and name inputs once

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".

* fix(harness): keep a declared whole as the user switched it

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.

* fix(harness): compile a declared whole from its part's final pin

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.

* fix(harness): make a transient switch failure retryable and keep caveats

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.

* fix(harness): let a declared whole take its part's scope and stay a fixed 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.

* fix(harness): read declared wholes from the current plan and name parties 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.

* fix(harness): restore a stopped switch's readings and name a whole's 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.

* fix(harness): read a declared whole only as its share's denominator

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.

* fix(harness): keep a plain requirement's rows on a whole's pin and reset 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.

* fix(harness): drop a bracketed basis label whole from a measure

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.

* fix(harness): leave the summary off a plan over ratios and restore a 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.

* docs(harness): tighten the wording of the plan card and switch comments

* fix(harness): word a whole's caveat by its reader and a double count 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.

* fix(harness): keep code-written caveats when the planner restates their 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.

* fix(harness): drop brackets a basis phrase leaves empty in a measure
…ly where flows meet (#91)

An outer calculation over shares or quotients reached through them to
their datasets, so comparing two shares that each kept a DAC1 total could
move both totals to another provider scope, or refuse. Ratios now align
with each other numerator to numerator and denominator to denominator, a
ratio's accepted provider difference stands, and the outer step states any
remaining difference. The double-count guard refused sums of disjoint
flows; it now refuses only when donors, recipients, sectors and years all
overlap.
#96)

* perf(harness): reuse resolves, reads and listings between plan, switch and translation

A proposed plan that the planner's own rules refuse returns
invalid_plan before any Search call. A switch posts its moved parts
and every declared whole on them in one concurrent round, over the
window the plan reads, so translation needs no further call.
with_source_reads runs its searches and compiles together and applies
their writes in order up to the first refusal.

Resolves are held per part under translator:resolved:<sha256> in the
turn's session state, with a per-invocation single-flight layer for
in-flight calls and refusals. Anchor reads are held per requirement
group, tools are listed once per translation, and data-mcp keeps each
verified series-statistics table in a 64 MiB LRU by artifact digest.

* fix(harness): stage admission, keep switch coverage and hold only safe refusals

admit refuses on rules that read no pin first (planner.plan
check_references), then reads Search, then runs the decision chain
and Plan validation on aligned pins, so no pin-dependent rule judges a
pin a pending read will move. An uncited decision is refused with
detail.uncited.

A plan-card switch posts each part over the question's window and the
plan's window together; the reading and pin source keep the dataset's
full coverage, and the windowed answer is held for admission's copy.
The compile-ahead of declared wholes is removed.

translator.discovery.answered_once serves resolves and searches within
an invocation; successful searches survive a refused proposal, and a
retryable refusal is held only inside a translation. Tools are listed
once per translation and passed to each proposal round, which also
stops on_progress=None reaching the eval surface. The statistics LRU
fetches each artifact once and sizes tables by the buffers they hold.

* perf(harness,data-mcp): mint a card's data before the user approves it

When the approve node offers a plan card, `harness.early_mint.EarlyMints`
sends one `get_data` per retrieve step with the step's own arguments, once
per plan revision. Each call carries `org.one.dfo/mint_admission` in its
`_meta`, so data-mcp marks the mint POST `X-DFO-Mint-Admission: background`
and apis admits it only with room left for foreground callers.

data-mcp answers a background call once its POST is written, keeps the
POST open in a referenced task until apis answers, since Cloud Run gives
the request CPU only while it is open, and logs apis's verdict (`minted`,
`declined` and the rest) with caller and elapsed time. The card's response
waits up to `EARLY_MINT_WAIT_S` for its calls to be sent, after the stream
has released the conversation lock. `EARLY_MINT=0` turns the feature off.

`_sse_stream` clears the conversation's cancel event only when it is still
the one that stream armed, so a stream ending cannot clear a later
stream's Stop.

* fix(harness,data-mcp): share a failed statistics fetch, check the surface at read time

Readers missing one statistics artifact share one fetch and its
outcome, a failure included, each waiting up to its own deadline; the
fetch runs under its hops' own timeouts. The anchor read lists the
surface when it checks the workflow against the tools it was compiled
with, so a tool changed after the compile stops the run.

The switch no longer posts each part a second time over the plan's
window: no request discovery makes for the copy matches what that post
asked, so its held answer was served for another request.

* fix(data-mcp,harness): bound the shared statistics fetch, key it by its link

A statistics fetch readers share is keyed by the link it reads
(subject, content version, transport) as well as the table, runs under
a 60 s bound of its own, and `clear()` cancels it. The planner-entry
eval's stand-in anchor read takes the arguments
`stated_from_the_data` passes. The switch posts its entries without
rebuilding choices nothing reads.

* fix(harness,data-mcp): keep a user decision's own refusal, answer the first refused read

`_draft_unreferenced` leaves a draft restating a user decision to
`_user_decision_refused` until the user speaks, so it still draws
USER_DECISION_DROPPED. `_held_in_order` applies source reads in order
as each finishes and cancels later ones at the first refusal.
`read_anchors` lists the surface only when it reads a group. A shared
statistics fetch that fails after its callers gave up logs a warning
in place of an unretrieved task exception.

* fix(harness,data-mcp): settle source reads in order, read a shared stop on held groups

`_held_in_order` awaits reads strictly in order, so a later read's
crash never replaces an earlier refusal, and `_cancel_and_drain`
retrieves every task's exception. A held anchor group whose root
stopped in this read reads as that stop. The switch posts its entries
to `_resubmit` directly. A statistics read past its deadline starts no
fetch.

* fix(harness): read a malformed anchor-reads value as none held, state what is kept

`held_anchor_reads` reads a stored value that is no mapping as no held
reads and logs it. The `build_translate` and `resolving_once`
docstrings state that only a retryable refusal is kept for the
translation and only a definite resolve part for the turn.

* fix(harness,data-mcp): cap and attribute early mints, honour background admission from the harness alone

Early calls per card run at most the executor's step concurrency, each
logs one line with its own elapsed time, and telemetry flushes again
after a settle that waited. data-mcp honours the background marker only
from a conversation caller inside the harness (not under the external
call budget), holds no progress reporter for a background mint, and
makes a foreground mint wait for an open background POST of the same
request in this process, since dfo-apis runs identical in-flight mints
twice. `background_outcome` reads a background call's answer for both.

* fix(harness,data-mcp): join an open mint POST, withdraw unsent early calls

Every mint POST registers its request; a background call for a request
already being posted answers `joined` and sends nothing, and a
foreground call waits while leaving itself time to post. Background
admission is honoured only on a loopback request from the harness.
Approving a card or offering a newer revision withdraws early calls not
yet sent, and a new stream drops a cancel reason left for an earlier
one.

* test(harness): watch the response's outermost stream when a client leaves at the pause

A Studio route serves `_settling` around `_sse_stream`, so the loop
takes over `_settling` and `_settling` closes the stream itself;
`_abandoned_streams` matches either generator.

* fix(data-mcp): join an open mint POST in place of posting a duplicate

A foreground mint that outwaited an open early POST for the same
request replaced it and posted again with seconds left, so apis minted
twice and the caller could fail while the first POST succeeded. The
open POST now owns its entry and resolves it with its outcome. A
foreground caller takes that outcome (response, refusal or upstream
failure), posts its own only when the open POST never reached apis or
was declined as background, and gets a retryable mint_unanswered when
its own deadline runs out first.

* fix(data-mcp): give a joined mint its own refusal and pin one repost

A foreground call that joined an open mint POST and ran out of time got
mint_unanswered, whose text claims a ledger hold and advises narrowing
the scope; neither applies to a joiner, whose repeat call joins the
open POST again. It now gets mint_joined_unanswered (service_unavailable,
retry_after_delay, fix.waited_s measured from the join). A test pins
exactly one new POST when several joiners wake after an owner that
never sent, and the early_mint docstring states when a run posts its
own request.

* fix(harness): withdraw an evicted conversation's queued early mints

EarlyMints dropped the oldest conversation's record once MAX_CALLERS
newer conversations had offers, leaving its unsent calls queued, so
approving that card could no longer cancel them and a queued call could
start after approval. Eviction now withdraws the record's unsent calls
first, as withdraw() does.

* docs(harness): stop promising nothing is retrieved before approval

Offering a plan now starts its retrievals, but the reject text said
nothing was retrieved and the approval prompt asked to approve before
anything is retrieved. The reject text reads "Plan rejected.", the
approval prompt says the data may already be loading, and the
PlanProposed docstring, the Studio contract and the two READMEs say
nothing is computed until approval.
basis_line merged parts only when all their clauses matched, so a share
answer (all ODA and its health part) repeated the flow, currency,
perspective and year for each part. The clauses every part holds now
come first, once, and each part follows its name with only its own.
* perf(harness): reuse resolves, reads and listings between plan, switch and translation

A proposed plan that the planner's own rules refuse returns
invalid_plan before any Search call. A switch posts its moved parts
and every declared whole on them in one concurrent round, over the
window the plan reads, so translation needs no further call.
with_source_reads runs its searches and compiles together and applies
their writes in order up to the first refusal.

Resolves are held per part under translator:resolved:<sha256> in the
turn's session state, with a per-invocation single-flight layer for
in-flight calls and refusals. Anchor reads are held per requirement
group, tools are listed once per translation, and data-mcp keeps each
verified series-statistics table in a 64 MiB LRU by artifact digest.

* fix(harness): stage admission, keep switch coverage and hold only safe refusals

admit refuses on rules that read no pin first (planner.plan
check_references), then reads Search, then runs the decision chain
and Plan validation on aligned pins, so no pin-dependent rule judges a
pin a pending read will move. An uncited decision is refused with
detail.uncited.

A plan-card switch posts each part over the question's window and the
plan's window together; the reading and pin source keep the dataset's
full coverage, and the windowed answer is held for admission's copy.
The compile-ahead of declared wholes is removed.

translator.discovery.answered_once serves resolves and searches within
an invocation; successful searches survive a refused proposal, and a
retryable refusal is held only inside a translation. Tools are listed
once per translation and passed to each proposal round, which also
stops on_progress=None reaching the eval surface. The statistics LRU
fetches each artifact once and sizes tables by the buffers they hold.

* feat(harness): switch planner decisions from the plan card

A planner-authored decision that sets requirement fields (not a search
pin, a scope choice or a caveat) appears on the card as a switchable
row, decision:<slot> with one option per label. Switching it replans
through the planner node with the user's pick held: the pick lives in
planner:decision_pick, is shown to the planner as its own line, and
admit refuses a plan whose decision for that slot is not the picked
label (user_pick_not_held), recording it as the user's. A pick
releases another user decision only when the new plan drops every
requirement citing it.

The replan runs under the switch's Stop handling, also after a
planner-side question, and a stop or refusal restores the state from
before the pick and answers switch_failed.

* fix(harness,data-mcp): share a failed statistics fetch, check the surface at read time

Readers missing one statistics artifact share one fetch and its
outcome, a failure included, each waiting up to its own deadline; the
fetch runs under its hops' own timeouts. The anchor read lists the
surface when it checks the workflow against the tools it was compiled
with, so a tool changed after the compile stops the run.

The switch no longer posts each part a second time over the plan's
window: no request discovery makes for the copy matches what that post
asked, so its held answer was served for another request.

* fix(data-mcp,harness): bound the shared statistics fetch, key it by its link

A statistics fetch readers share is keyed by the link it reads
(subject, content version, transport) as well as the table, runs under
a 60 s bound of its own, and `clear()` cancels it. The planner-entry
eval's stand-in anchor read takes the arguments
`stated_from_the_data` passes. The switch posts its entries without
rebuilding choices nothing reads.

* fix(harness,data-mcp): keep a user decision's own refusal, answer the first refused read

`_draft_unreferenced` leaves a draft restating a user decision to
`_user_decision_refused` until the user speaks, so it still draws
USER_DECISION_DROPPED. `_held_in_order` applies source reads in order
as each finishes and cancels later ones at the first refusal.
`read_anchors` lists the surface only when it reads a group. A shared
statistics fetch that fails after its callers gave up logs a warning
in place of an unretrieved task exception.

* fix(harness,data-mcp): settle source reads in order, read a shared stop on held groups

`_held_in_order` awaits reads strictly in order, so a later read's
crash never replaces an earlier refusal, and `_cancel_and_drain`
retrieves every task's exception. A held anchor group whose root
stopped in this read reads as that stop. The switch posts its entries
to `_resubmit` directly. A statistics read past its deadline starts no
fetch.

* fix(harness): read a malformed anchor-reads value as none held, state what is kept

`held_anchor_reads` reads a stored value that is no mapping as no held
reads and logs it. The `build_translate` and `resolving_once`
docstrings state that only a retryable refusal is kept for the
translation and only a definite resolve part for the turn.

* fix(harness): hold a pick only where its requirements move, keep decision ids apart

`_picked_decision` holds a pick only where what the decision fixes
reads the picked option, and `_released_by_pick` releases a user
decision only where every requirement citing it was one the pick
changed. Planner decision rows take ids `decision/<slot>`, which no
scope choice id (`<requirement>:<axis>`) can equal. A refused pick
follows the switch's retry rule and reports `plan_not_ready` when the
seam names no code.

* fix(harness): keep a pick's pause a pause, clear its state when it ends

A revision that paused on a planner question in the same wait as Stop
stays paused, so a node run ends as one outcome. A pick never releases
a user decision no requirement cited, and a restated pick whose citing
requirements fix none of what it names is refused saying so. A pick
entry clears its pick and snapshot on every exit but a pause, which is
the only time the snapshot is persisted, and an answer to a pick
replan's question is routed by the pending interrupt's own flag.

* fix(harness): hold a pick against what the user picked on

A pick released any user decision whose requirements also cited the
picked slot, so switching a donor scope could move the user's year. It
now releases one only when the picked decision's fixes set every field
that decision fixed. The pick state records the decision as it stood
when picked, and every proposal of the replan is held against it with
its fixes set. Planner decision option ids derive from the label, so a
replan that reorders options keeps them. A pick replan leaves the
turn's switch baseline until a drafted plan lands, so a failed or
stopped replan keeps earlier switches' changed_from.

* fix(harness): release only the picked slot and hold a pick by what it reads

A pick released any user decision whose fields the picked decision
listed, and the planner writes that list, so listing period released
the user's year. A pick now releases only its own slot; a proposal that
drops another user decision is refused and the switch fails with the
plan unchanged. The pick records a digest of what the requirements
citing its slot read (content without ids or citations, each input by
its own digest), and a proposal holds the pick only when that reading
changes. A landed pick replan keeps the planner-decision rows'
changed_from.

* fix(harness): lock a pick replan to the requirements citing the pick

Admission checked a pick replan's whole re-authored plan after the
fact, and each check left another way through: an unrecorded card
version let the user's year go, and moving the picked citation onto an
unchanged requirement read as a change. The pick now records the
card's other requirements and the decisions they or the user hold, by
content digest. A proposal must carry those unchanged, may replace only
the requirements that cited the picked slot, and the replacing set
must read differently. A pick entry counts as the user not having
spoken, so the rule does not depend on the session's events.

* fix(harness): tell the planner what a pick replan may change

The pick entry named the decision to restate but not the scope
admission now enforces, so a first proposal could be refused for
editing requirements it was never told to carry. It now says to change
only the picked decision and the requirements citing it, to carry every
other requirement and decision as the card has them, and to keep the id
of a replaced requirement another one reads.

* fix(harness): replace what a pick governs and carry the user's decisions

A pick replan could keep every requirement citing the pick and add one,
leave the user's year uncited on a replacing requirement, strand
requirements it added so a pick back was refused, and trip on
admission's own derived citations. The scope check now reads what the
planner wrote, before alignment and derived rewrites; it refuses when
every reading of the replaced requirements survives, when a replacing
requirement does not cite the pick and every user decision they cited,
or when a kept requirement or decision changed, naming the item broken.
A switch to the option already chosen builds no pick.

* fix(harness): judge a pick by the card's fixes and its root readings

A pick could not land on another pin (code-written scope choices were
required citations), a top-5 pick that still kept 10 was admitted, a
pick over figures citing different user years could never land, and a
share keeping a replaced total as its numerator was refused. The pick
now records the card's fixes and is refused when the replan settles to
them; it asks every replacing requirement to cite the user decisions
all replaced ones cited and one of them to cite each of the rest,
leaving code-written slots out; it compares root readings only; and a
pick replan drops code-written decisions nothing cites. A stored pick
this release cannot read ends the switch with switch_failed, and a
switch to the active option says it is already on the plan.

* fix(harness): let a pick land when only a shared field keeps its value

_fixes_kept refused a pick whenever any field the card's decision fixed
settled to the card's value, so a shape pick could never land when the
decision also fixed a field both options share, such as the unit. It
now refuses only when every field the card fixed still settles to the
card's value. The unreadable-pick handler restores only a snapshot the
current entry wrote.

* fix(harness): judge a shape pick by the scope rules alone

After a share pick landed, the decision kept only the fields both
options share, so picking the total back always settled them to the
card's values and _fixes_kept refused it. The pick records the kinds
of the replaced roots, and _fixes_kept runs only when the replacing
roots keep them; a shape change is judged by the scope and citation
rules alone, and top 5 that keeps a limit of 10 stays refused.
)

* fix(harness): hold the prices the user chose on a clarify question

A user who answered "constant prices" got a plan in current prices: the
pick carried no spec_id, so nothing bound it, and the planner dropped
the unit when Unit refused constant prices without a base year. A
prices pick now reaches admission as the user's decision (user_prices,
fixes.unit), so a plan that leaves the unit out or states other prices
is refused. The base_year refusal names the base year the source's
block states, and with none known the planner asks the user. A
constant-prices pick with no spec_id tells the orchestrator to search
once in constant prices.

* fix(harness): hold each prices choice on its own part, basis only

One plan-wide user_prices decision fixed the first money requirement's
whole Unit, so plans mixing currencies or base years failed, a second
requirement was told to take the first's base year and spec_id, and
several prices answers collapsed into the last. Each prices question
now binds its own part (user_prices_<spec_id>) and fixes only the price
basis (DecisionFixes.prices, read as unit.prices); each requirement's
base year comes from its own pin. The ask node marks prices options in
code from the turn's open_prices, so no model-copied field carries the
choice. Only a typed message, an amendment or a new answer to the same
question releases it, and turns with no prices choice behave as before.

* fix(harness): bind a prices question by its part and check the written pins

A part realigned to its aligned compile escaped the prices check, a
prices refusal's pending ask swallowed later unrelated answers, keeping
constant prices released the choice, two priced parts showed identical
card rows, and options were bound by label. Prices parts are matched on
the pins the planner wrote; the planner's prices ask carries fixed
current/constant options and only a current pick releases the choice;
any other proposal clears the pending refusal; each card row names its
part; and the orchestrator's ask_user takes prices_of, the part's
spec_id, and builds that question's options from the part's
open_prices, which the model reads unchanged.

* fix(harness): hand back for a constant search the planner cannot run

A found constant series could cross provenance, method or currency,
pinning it dropped the user's prices decision, and keeping constant
prices with none shown repeated the planner's ask until the round
budget ran out. constant_found matches only series that differ in
price basis; a requirement pinning one counts as the part; keeping
constant with none shown ends the entry with prices_unserved, telling
the orchestrator to search the part in constant prices once. Two
prices questions on one part are refused, a typed message clears a
pending prices refusal, and origin_of follows the whole alignment map.

* fix(harness): hold a prices choice over the count picked beside it

A user picking another count and constant prices in one question
(net beside a gross part) got a plan with no prices decision, or a
hand-back searching the wrong reading. A part's series are now every
count its block offers and what measures the same in its currency;
the constant series is resolved against the series pinned, a current
choice moves a constant pin to the count it measures, and
prices_unserved names the reading pinned. Its Studio text reads as a
search for the chosen prices.

* fix(harness): give each priced series one owning part

The widened part series claimed a sibling part of the same flow pair
that differed only in count, so a gross part and a net part priced
apart looped until the draft budget ran out, and an unpriced net part
took the gross choice. Parts.owner_of gives each shown series one
owner: the reading itself, else the reading measuring the same thing,
else the first reading offering it as another count.

* fix(harness): keep windows of one measure in one priced part

Owning each series by reading split two windows of one measure into
two parts, so search order decided which carried the user's prices,
the other window could drop the choice and the hand-back could loop.
Readings of one measure and currency are one part, owned by the priced
reading among them whatever the order; only another count or currency
makes a separate part.

* fix(harness): ask one prices question per measure

Two prices questions over windows of one measure, answered apart,
left the second window's choice lost. ask_user refuses an ask that
prices two parts measuring the same thing, as it refuses two on one
part (measure_key).

* fix(harness): let the card switch prices and words release them

The plan card offered the user's prices row as a switch, but admission
rebuilt the earlier choice, so no replan could land; and a free-text
answer to the planner's prices ask kept a superseded constant choice,
ending a valid current-prices plan on prices_unserved. A switch of the
prices row is now a new answer for that measure; the pick lock leaves
the picked slot out of the required citations when admission writes
it; other code-written rows build no pick; and a nonblank free-text
answer releases the choice while a skip keeps it.

* fix(harness): drop a prices switch that did not land

A refused or stopped prices switch stayed held for the rest of the
turn, though the card went back to the earlier prices, so every later
switch was refused. A switch counts only while its pick entry is live
for that slot or once the plan on record holds the switched label.
* fix(harness): word a decision's enum options for the reader

The plan card showed a rank decision's options as the raw RankAmong
values the planner copied, "population" and "named", and a user read
"population" as per person. card_choice_text now words an option that
names a value the decision fixes from a registry of every enum
DecisionFixes can hold (FIXED_VALUE_WORDS) and the plan's context:
"all 32 donors in DAC2a" and "only the donors named". Option ids and
picks still read the planner's text, so switching is unchanged.

* fix(harness): word options from every citing requirement, truthfully

A pooled share read "one total across all donors" whatever its
denominator, several ranks under one decision took the first rank's
source and count, a named rank with no names read "only the donors
named", and group_by entity read "by entity". Each option's words now
come from every requirement citing the decision and are used only where
they agree: a share's scope from denominator_scope_phrase, a named rank
from the EntityIn filter that keeps its list ("only donors picked by
name" with none), and group_by from the aggregates' one party. Where the
true words cannot be built the planner's text stays.

* fix(harness): give every fixed value words that hold in any plan

Where a plan gave no context, an option still showed the raw value
("pooled", "per axis", "entity"). Each value now carries a fallback
true in every plan beside its builder, a total grouped by entity names
every role it keeps, a share names its denominator's measure, and
direction reads "highest value first". The sweep test requires a
distinct fallback for every member that is not the raw value.

* fix(harness): name the breakdown a grouped total keeps

A total grouped by entity keeps the input's breakdown, but its option
read "by recipient" for ODA to Kenya by sector. _by_party now keeps every
role that is not open, so it reads "by recipient and sector", the
breakdown option reads "by sector", and the entity fallback reads "by
each party, place or category kept".
* fix(studio): name each approved choice by its choice_id

`plan.approved` listed each approved choice by its question and answers only, so a client could not tell a planner decision (`decision/<slot>`) from a scope choice and filed planner decisions under the plan's measure basis. `PlanApprovedChoice` now carries the `choice_id` the plan card's `PlanDecision` carries.

* docs(studio): version approved choice ids as their own delta and test a real planner decision

The contract states choice_id from v1.9 in its own delta and says a stored plan.approved lacks it. The translate and route tests build the planner decision as the planner does and approve it after switching.
…'s statement (#108)

* fix(planner): derive alignment per requirement from the planner's statement

Admission repinned an aligned input to its compile S' before deriving
any decision, wrote the compile into pin_sources[S'] and bridged S' back
to its origin by spec_id, so a direct pin of S' was read as the aligned
input's origin and a share could divide one series by itself.

Admission now reads the plan as the planner stated it (authored, then
unwindowed, then unaligned through the per-requirement alignments
table), derives measure, scope, whole and prices decisions on the stated
pins, gives the moved axes one decision per requirement
(scope_<axis>_of_<rid>), and repins aligned requirements last. Compiles
write only aligned_pins; series facts for an unshown compile come from
held_sources.

* fix(harness): switch and name card rows per requirement

The card switch found an aligned row's source through a stored reading
of its compile and moved every requirement on the compiled spec_id. It
now builds its patch from the statement view: a switch of an origin's
row moves every requirement stating it, and a moved-axis row resubmits
from that requirement's AlignedPin, so the user takes that input over.
Row ids come from the requirement citing the row. A compile is no
longer stored as a reading (store_searched_pin_readings keeps searched
pins only); a later turn reads its dataset as a translator source under
the requirement's own words. A direct pin of an unshown compile is a
series of its own for the prices hold.

* fix(harness): keep aligned inputs on their stated pins at every seam

An amendment that dropped a comparison left the average on its net pin
citing the gross moved-axis row; admit now re-derives the decisions of
every carried aligned requirement on its stated pin. _window_pins no
longer re-keys a moved-axis decision onto the compile, and the switch
takes an aligned row's stated pin from its AlignedInput. A switch of
one axis a compile moved leaves the other axis the planner's, a first
draft reads no earlier plan's alignments, and _choice_not_offered reads
the statement view.

A turn closed with an aligned input kept the compile's block but not
its stated pin's, so a later turn restating the comparison could not
realign it. PlanSource.aligned_from records the stated pins and
close_turn keeps their Search blocks, as it keeps a windowed copy's
origin.

* fix(harness): count an aligned input on its compile's basis in the answer

With compiles no longer stored as readings, an aligned input dropped
out of the answer's closing lines: basis_line described the other
inputs alone and lost how the compile was served. PlanSource now
records the compile's basis (AlignedBasis, with its served scope from
record_served_scopes), read only by basis_line, and loose_match_notes
counts each aligned input's stated pin, so the closest-match note for
the user's words survives. _spec_blocks prefers a shown block over a
compile of the same spec_id, so a user's row keeps its own names.

* docs(harness): state the alignment rules as they stand

Comment and docstring wording on the per-requirement alignment path.
…takes (#107)

* feat(harness): migrate stored bound refs to the list form the reader takes

harness.bound_refs_migration rewrites each stored spec block's _bound_refs mapping into BoundEntity rows, dry run by default, guarded on the row's update_time.

* fix(harness): run the bound refs migration after the reader-first release and report every block

The runbook deploys the main release that reads both forms first, then applies twice, fifteen minutes apart, on the dev image. Each block's error is recorded by type, the job exits 1 on any unconvertible block, and unchanged state is never copied.
…n the card (#109)

* fix(submission): keep a share's sides apart when alignment would make them one series

_sides_one_series called two leaves one series only when they shared a
base part (scope.base.spec_id), so a share whose numerator was Search's
own multilateral dataset and whose total alignment moved onto
provider:multilateral fetched one series twice and answered 100%. The
guard now compares the series each side reads once alignment has moved
it: the compiled identity plus the facet each read resolves, with a
moved side read from its held compile. Test fixtures now give France
its own DAC1 identity and an unnarrowed basis block its own base.

* fix(harness): word nested and literal fixed values, and name a filter that met a stated choice

A place_level filter's chosen option showed its raw PlaceType ("Country"),
a threshold option its ThresholdOp ("gte"), and a stat_vars option
written "sum" read "total" because FIXED_VALUE_WORDS keyed one table by
value across fields. FIXED_ENUMS now maps every enum or Literal field a
decision can fix, nested ones included, to its values, and
FIXED_VALUE_WORDS is keyed by field then value.

The basis line said "the option you asked for is not available here"
when Search dropped a stated provider:bilateral on DAC2a while the plan
met it by narrowing the donors. PlanSource.narrowed records the filters
on each source's roles, and the clause reads "bilateral: read as only
donor countries".

* fix(harness): read a share's series without its search window, and only a level filter as a class

A pin's identity carries the years of the search that found it, so
_Series kept two pins of one series apart when their searches spanned
different years and the total still moved onto the numerator; the core
now comes from dataset_of. A list of named donors does not apply a
stated provider class, so only a place_level filter stands in for it in
the basis line. narrowing_words had no caller left and is removed.
* fix(submission): refuse a share that divides one series by itself

A planner could write both sides of a share on one dataset, with a
decision naming the part that no input applied, and the plan answered
100%. _sides_one_series leaves same-series sides alone so alignment
never moves them, so nothing refused the plan. _share_of_itself now
refuses (share_of_itself) any share or quotient whose sides read one
series over the same reads once every pin is final, naming the
numerator's other provider scopes, unless a held card pick put that
input there. Pooled shares and a quotient by an axis total are skipped.
Fixtures that gave two inputs one series (_malaria_block,
_totals_paired) now give each its own.

* fix(submission): close the share-of-itself refusal's remaining routes

A pick both sides cite, such as a year decision, exempted a planner-made
share of one series over itself; the exemption now needs one side to be
the pick's input and the other the requirements it kept. Unpinned
identical inputs compare by requirement_revision and their reads, and a
side entity naming the only entity of a one-series dataset no longer
changes the side's shape. The refusal names the part in Search's words
for the planner to put in measure, which it can act on.

* fix(submission): admit a share of itself a pick keeps unchanged

After a user's own pick made a share of one series over itself, a second
pick on an unrelated decision refused that share, and the scope lock
kept the planner from changing it. A share whose step and every leaf are
kept by the pick is skipped: it was admitted before and stays as it was.

* fix(submission): keep a share whole under a pick only when all it reads is kept

The kept-share exemption checked only the share step and its leaves, so
a replan that rewrote an aggregate between them onto the denominator's
series was admitted. It now requires the pick to keep every requirement
the share step reaches.
migrate_block and the prod hotfix's writer share one mapping-to-rows function.
…nts with their plan (#122)

* fix(harness): switch a directly pinned compile's row and keep alignments with their plan

The switch resolves a compile-pinned row from the compile block the card reads, and admit replaces recorded alignments on every fresh plan, so an earlier plan's pins cannot be read as the new plan's.

* fix(harness): switch a compile row from the pinning requirement and clear alignments with their plan

The switch reads a stored reading first and falls back to the held compile only where the card does, words the switched reading from the requirement that pins the compile, and carries no origin provenance. clear_plan clears the plan and its alignments together, and every admission saves its alignments.

* fix(harness): switch a compile row from the card's block and refuse words the plan does not share

The switch reads the block held_sources holds when no stored reading has the scope, words the switched reading from every requirement that pins the source and refuses when they differ, and shares requirement_parties with a fresh search.
@jm-rivera jm-rivera changed the title release: ai b9ad157 to production release: ai 0f949ec to production Oct 29, 2026
main holds #111, which reads both forms of _bound_refs and the release's ledger records so production could take the bound refs migration. The release reads and writes those forms itself, so its tree stands unchanged and main is recorded as merged.
@jm-rivera
jm-rivera merged commit 775eeff into main Oct 30, 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