Skip to content

feat: run analysis on DFO's engine and serve cards and shares from dfo-apis - #173

Merged
jm-rivera merged 48 commits into
feat/dashboard-enginefrom
feat/dashboard-build
Oct 29, 2026
Merged

jm-rivera merged 48 commits into
feat/dashboard-enginefrom
feat/dashboard-build

Conversation

@jm-rivera

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

Copy link
Copy Markdown
Contributor

Closes #175

Datasets runs analysis on DFO's own engine, and Presentation serves cards and shares that follow new data. POST /api/datasets/analysis takes a recipe tree whose roots are Specs, exact versions, derived frames or refs, with arguments in role words. It evaluates the tree with dfo_analysis_engine, stores the derived frame and its recipe, and answers a format 2 ref. Requests are keyed before any read and their answers stored (datasets.analysis_answer), so a retry joins the running request. Runs are bounded by nodes as the tree is walked and by root rows before any read. GET /api/datasets/derived/{ops}/{ocv} answers a derived frame's manifest (retention, citation, recipe, periods, and left_out, every series dropped for missing data), and .../file serves its rows as the stored Parquet file or as csv, xlsx or a bundle.

The output digest (out2) names each row's stat_var. A variable that compare, sum_of, share_of or quotient_of writes is named mv_ plus a digest of its verb and its inputs' names, fixed size at any depth (output_digest.minted_variable_names), so equal computations over equal variables share a digest across recipes, engine releases and rebuilds. A frame pair's cv_ preimage includes the party-totals cut and sorts groupby.by, as dfo-ai's data-mcp computes it, so a request with the cut and one without name two pairs. A run whose pair holds no frame answers the pair its replay key names, and supersedes that recipe when its file has aged out (frame_recipe.superseded_at). A cached answer or reused pair whose output is revoked refuses 410 snapshot_revoked.

dfo_platform.caller.resolve_caller names each caller without an API key, by the website's vouched x-dfo-caller digest or the trusted X-Forwarded-For entry. datasets.caller_allowance counts each caller's pins, shares and cards per UTC day. A spent allowance answers 429 allowance_exhausted, not retryable, with Retry-After naming the reset, and a refund locks the counter row first. Every new copy into pinned storage (pin, share create, replace and snapshot conversion) goes through retention_admission.copy_to_pinned. It spends the caller's pin allowance (default 100 a day), the open callers' budget (2,000) and the global budget (2,500), skips copies that exist, and refunds only a deduplicated copy or a missing source.

Card revisions record the rendering of the frame they draw, and creating or rendering a card pins nothing. A share (/api/presentation/shares) publishes cards with a shw_ write token that replaces, snapshots or takes it down, and pins each frame on first inclusion. A derived card pins its frame and each root still held. Public slot routes serve each slot's card and rows with freshness, ETags and CORS, and a live slot checks for new data at most every 15 minutes through the analysis route's stored-answer path. History records what each slot serves, written in the transaction that writes each revision and dated in commit order, and pages newest first (limit, before, next). datasets-maintenance take-down-share <id> takes down, without its token, the shares the website records in share_cleanup.

just local-stack up|seed|smoke|down runs both surfaces on Postgres 18 and fake-gcs-server with fixture upstreams, for the SDK and website journeys.

Deploying: apply Datasets migrations 0039 to 0043 and presentation migrations 0011 to 0018 before traffic; set DFO_CALLER_HMAC_KEY and DFO_CALLER_PRINCIPAL to the website's CALLER_HMAC_KEY and DFO_CALLER_PRINCIPAL; run datasets-maintenance import-shares with the website's export, then datasets-maintenance normalise-recipes; restart the presentation instances. The website (DevelopmentFinanceObservatory/website#77), the SDK and dfo-ai's runner go out after this deploys. Second of two stacked PRs, on #170.

Left for follow-ups: a row count in the derived manifest (#176), several source refs per card series (#177), and reclaiming pinned copies nothing references (#183). Nothing deletes pinned storage today, so the daily budgets bound how fast it grows.

Testing: the CI jobs, with pytest -n 8 in five shards with DFO_SPEC_KIT_REQUIRED=1 over tests and the engine package after merging dev (42,201 and 1,625 passed), including every output-digest and operation-outputs vector in the client contract. 42 Datasets and 18 presentation migrations applied on Postgres 18, a re-run was a no-op, and the live store tests passed (91 and 20), including the refund race and history ordering under row locks. On the local stack the smoke passed 22 checks, the SDK journeys passed (Python 13, TypeScript 7, a grouped-root share in both, and the crossing test), the share journey through the website proxy passed 3 of 3, and the website's share migration round trip passed. /code-review xhigh ran over every commit, and an outside review's findings are fixed here or listed above.

dfo_platform.caller.resolve_caller names each request's caller without
an API key. A request vouched for by the website proxy
(x-dfo-caller-principal) is named by its x-dfo-caller digest; any
other is named by the X-Forwarded-For entry DFO_TRUSTED_PROXY_HOPS from
the right, hashed with the website's preimage (IPv6 folded to its /64)
so both routes share one namespace and no raw address is stored.

datasets.caller_allowance (migration 0039) counts each caller's pins
and shares per UTC day; a spent allowance answers 429
allowance_exhausted with Retry-After at the next midnight. The pin door
now admits any caller: grant holders keep the global budget alone, and
other callers spend their allowance inside an open-caller ceiling of
200 copies per 24 hours.

spec-kit moves to 2765bc3, which registers the new endpoints and codes.
POST /api/datasets/analysis takes a recipe tree whose roots are Specs,
exact versions, derived frames or refs, with verb arguments in the
caller's words (roles such as origin, destination and place; `on`
pairing data with other). The runner reads the roots with their
renderings, settles the rolling period once, evaluates the tree with
dfo_analysis_engine, digests the output and stores the derived frame
and its recipe, answering a format 2 ref. A rerun stores nothing new,
and `latest` and exact runs share one path.

A root still building answers 202 with Retry-After. Runs have their own
pool of 2, hold a materialise permit, and are bounded by nodes, root
rows and join rows. POST /api/datasets/analysis/anchor returns the
latest period for given roots and stores nothing.
GET /api/datasets/derived/{ops}/{ocv} answers a derived frame's
manifest: retention (pinned or held_until), the roots' merged citation,
the recipe, resolved periods, and left_out, every series dropped for
missing data at any step of the tree, naming the step, input side,
reason and rows. .../file serves its rows as csv, xlsx or parquet,
alone or bundled. The engine now reports each step's drops, and the
runner stores them in the recipe.

POST /api/datasets/analysis also takes a bare derived root, re-running
its stored recipe, so fetching a derived ref with latest=True keeps the
same ops_ and ocv_ when no root moved.

datasets-maintenance diff-recipes re-runs every stored recipe on its
own root versions without writing anything and compares each output
frame's cv_ and output digest with the stored frame's, reporting equal,
differs, roots unreadable and frame unreadable. record-recipe-golden
records recipes and their frames for a test that the runner reproduces
each one; three synthetic cases stand in until dev recipes are
recorded.
`just local-stack up|seed|smoke|down|status` starts Postgres 18 and
fake-gcs-server in docker, applies both migrators, and serves the
Datasets and Presentation surfaces on 127.0.0.1:58083 with fixed local
principals and caller settings. Upstream sources are fixture doubles,
so nothing reaches GCP, Gemini or Data Commons.

The seed mints four readings through the real mint door (German ODA to
Kenya, Ghana and Nigeria; ODA to Ethiopia by donor; Kenya population;
Kenya GNI), idempotently, and the smoke script checks each one's
versions, manifest, observations and file bytes over HTTP.
artifact_store.signing.sign_get_url returns the storage emulator's
media link when STORAGE_EMULATOR_HOST is set, with no credential work,
and signs exactly as before otherwise. dfo_apis.app exposes
build_presentation_surface, which create_app and the local stack both
use, so the local app patches nothing and imports no private name.
…endering

Card revisions store the rendering of the frame they draw
(component_revision.frame_rendering, migration 0011), read from the
version's records on every load, and a render judges its target
against that recorded rendering. Card reads and renders carry the
frame's retention: pinned, or held_until counted from when DFO stored
the frame. An expired frame's refusal names dfo.fetch(..., latest=True).

Creating, revising and live-rendering a card pin nothing.
presentation_store.pin_card_frames pins a revision's frame when a share
first includes it, and pinning a derived frame also pins the root
versions its recipe names.
POST /api/presentation/shares publishes cards as a share with an
8-character id and answers its url, embed_url, one data URL per slot
and a shw_ write token. latest is required, the caller's daily share
allowance is spent, and the cards' frames are pinned when a share
first includes them.

PUT replaces a live share's cards with the token, keeping each slot's
URL by position; a snapshot cannot be replaced (share_not_replaceable).
DELETE takes the share down with the token. GET reads a share publicly
and never shows the token. The token is checked before the mode, and a
share with no token refuses every token.

Presentation migration 0012 adds share, share_revision, share_slot and
served_frame.
The analysis runner named flow datasets' enrichment columns by role
word (origin, destination) while stored identities key them by column
(financingProvider), so every analysis over a flow dataset refused
column_unknown. It now matches both.

The analysis route answers every Datasets code at its datasets.analysis
status, with no fallback, now that spec-kit 4bcb6dc maps them. The
definition and refresh helpers the runner reads roots through are
public (served_spec, serve_spec, answer_frame_refresh).

The local stack's smoke now runs a quotient_of over seeded readings,
reads the derived manifest and Parquet rows, re-runs the derived root
with latest, draws a card on the result, and creates, replaces and
deletes a live share with its write token.
GET /api/presentation/shares/{id}/slots/{n}/data serves a slot's rows
as JSON, csv or parquet, publicly and across origins. A snapshot slot
serves its card's stored frame; a live slot draws the card on the
newest data, through the refresh path for datasets and the runner for
derived frames, checking at most once every 15 minutes per slot and
stating current, stale or pending in the body and x-dfo-freshness. A
strong ETag answers If-None-Match with 304.

Each frame a slot serves is recorded, and GET .../history lists them
newest first with their retention and whether each is still fetchable.
POST .../snapshot with the write token turns a live share into a
snapshot in place, pinning what each slot serves. Create takes a kind
(card, canvas, search, conversation) and a website-owned payload.

datasets-maintenance import-shares reads the website's share export
idempotently, keeps each id, and writes a fresh write token for every
signed-in owner's share to a 0600 file the website reads.
A card on an ops_ frame cited "Unattributed source": the frame store
cited a derived frame from its rows' provenance refs, which a quotient
of two sources leaves null, and a null ref mapped straight to the
unattributed source. The frame store now cites a derived frame from
its recipe's roots through datasets_api.derived.recipe_citation, the
function the derived manifest uses, and a row with no ref stands for
every source the manifest cites. A frame cached before its recipe was
written is cited again once the recipe exists.
… rules on replace

A live slot whose newest frame is still building is checked again after
the retry time the refresh path or runner gave, clamped to 1 to 900
seconds, so a finished rebuild shows within that time; current and
stale slots keep the 15-minute window. The slot check stores its next
check time (migration 0014).

Replacing a share applies the per-kind card rule create uses, so a live
search share takes no cards. A snapshot share, conversations included,
refuses share_not_replaceable before its cards are read.
The derived file route serves a bare Parquet request as the stored
canonical frame's bytes, so the body hashes to the ref's artifact
digest and clients can check and cache it; csv, xlsx and bundles still
render.

Runner roots take the groupby a fetched frame was minted with (a
StatVar sum or a breakdown) beside without_party_totals and columns.
The groupby is part of the root's rendering, so it enters the recipe's
normal form, the output digest and the manifest's roots, and the root
reads through the same head rendering a definition read uses.
recorded_dataset_root is the one reader from a mint record to a root,
shared by the runner, recipe replay and the golden.
…nown

A card on a derived frame published without steps was refused
schema_invalid: a row with no provenance ref took the id of the first
cited source, while the card description listed the computed version's
inputs, whose ids are null for such a publish. with_card_description
now gives a null row a description only when the record knows an input
id, and then the row and the description share it; otherwise the card
carries no description and its provenance lists the roots' sources.

The analysis door builds every refusal through
classify_analysis_exception, and the code sweep lists its two engine
code passthroughs. The local-stack smoke checks that a card on a
derived frame cites the derived manifest's sources in order.
…tryable

GET /api/presentation/shares/{id}/slots/{n} answers the slot's card in
the components read shape, drawn on the frame the slot serves, with the
same freshness, ETag and CORS as the slot data route; both routes take
their frame from serve_on_slot_frame, so they never disagree. A site
can draw a live share without the render principal.

Share create takes an optional client-minted shw_ write token: a retry
with the same token and body answers the first share, and the same
token with another body is refused (migration 0015). Card create
replies carry retention as reads do, and a frame with no version record
states null retention. binding_invalid and constraint_violation answer
retryable: false, since their fix is a corrected request. A share
payload may be up to 10 MiB, on create and on import.
…es its own

A derived frame reached clients as the canonical frame, whose unit is a
tagged struct, while a dataset version's file carries unit as text. A
published derived frame now also stores its bare frame-layout Parquet
file through render_and_store, the pipeline dataset files use, and the
ref's artifact digest and the publish response name that file. The
derived file route serves it, rendering it again from the canonical
frame if its bytes are gone, so both kinds of frame read the same way.
The canonical frame stays stored for cards, replay and pins.
…hment and site

The derived frame file rendered with an optional enrichment and a fixed
placeholder site and time. The analysis stores now carry the request's
enrichment and site, the publish path passes them and a real prepared
time to render_and_store, and render_and_store requires its enrichment
again. A bare frame file reads none of them, so its digest is the same
whenever the frame is published.

A missing-field schema_invalid takes its retryability from
retryable_for, like every other refusal, and answers false.
tests/e2e/test_dashboard_journey.py runs one client's path through both
surfaces sharing one Datasets cell: a compare over a summed root and a
second reading answers a format 2 ref whose file hashes to its digest;
a card cites the roots' sources; a live share serves the derived frame
with ETags, follows new data after the freshness window, and lists both
frames in its history; replace, a wrong token, snapshot conversion and
its pins, a retried create, unshare, and an open caller's pins up to
the allowance each answer as a client expects. A strict xfail
reproduces a derived frame over a summed root that stops following new
data. tests/live_refresh/composed_app.py mounts the share routes and
the shared cell as the app does, and conformance clause C12 states the
pin rule and names the test.
A derived frame over a root read with a groupby sum stopped following
new data when, at analysis time, the sum equalled the plain frame: the
latest re-run refreshed the root by version alone, and a version that
links both renderings is ambiguous once new data separates them, so it
refused rendering_not_recorded and a live share served its old frame
as stale. LiveRootSource now serves a root under latest through the
same path as a fresh Spec root, with the rendering the recipe recorded,
so a root is always read by version and rendering.
…wnloads

spec-kit 5a1959de maps the share routes' refusals: an oversize share
payload answers 413 payload_too_large before any allowance is spent, an
I/O failure on a share route answers retryable 503
service_unavailable, and pin_not_permitted is retired with the
PinNotPermitted refusal. The presentation surface classifies each
route by its own door.

A card's select lists are a set: canonical_select sorts them and drops
repeats before an intent is stored, so the same intent draws the same
table whatever order a client sends. A derived frame's card takes its
default title from the frame's recipe, naming each composed variable
from its roots' labels.

The local stack serves signed download links on the API origin and
answers the graph node reads card citations make, so clients need no
link rewriting and card creates no longer wait on a refused read.
… over a population

The analysis route published derived frames without steps, so their
records named no inputs and their cards were titled by ops_ id. Each
verb call now builds its step with steps.build_step (call_steps.py),
and the runner publishes the steps with a retrieve per root, so a
derived card reads like one the MCP published. A derived frame's party
and type columns stay out of card series labels when their role column
is present.

A request may carry anchor_roots, the roots /analysis/anchor was asked
about, so a rolling call over one side of a paired population resolves
its latest period over the whole population. They enter the recipe's
normal form and replay key. A token holder's repeat DELETE of a share
answers 204.
GET /api/presentation/shares/{id}/slots/{n}/data takes the formats and
bundle option the components data route takes, through the same
export_component_data call on the frame the slot serves, so a live
card's bundle and spreadsheet downloads follow new data. A bundle of
json is refused.

An analysis step list over the byte bound sheds its coverage members,
largest first, until it fits (call_steps.shed_coverage), and the frame
is published with those steps.
Analysis sums the roots' recorded rows and refuses past the root-row
ceiling before reading a frame, and refuses an output past the publish
ceiling before storing it. Columns are put in one order in
mint_options, so equal requests give one normal form; a stored pair
whose recipe or bytes differ answers 500 internal_error and logs both
identities. The latest period comes from the engine's
latest_period_of.

A request whose caller-principal header does not match
DFO_CALLER_PRINCIPAL is refused 503, where it fell back to the address
and pooled every website user under one allowance.

The pin budgets and the caller's allowance are spent together in one
transaction per UTC day (migration 0040), so concurrent pins cannot
pass a ceiling, and Retry-After counts to the next UTC midnight.
A replace refuses under the row lock when the share became a snapshot
or its revision moved. A slot check takes one claim, the first check
included, and the slot keeps its last frame, freshness and check time
while the claim runs; a dead claimer's claim lapses after twice the
components timeout (migration 0016). Create, replace, snapshot and a
replayed create record each slot's frame, so history lists a new
share at once.

import-shares pins every imported card's and slot's frame as create
and snapshot do, and writes and fsyncs each token line before the
share commits. A row with no provenance takes a derived frame's cited
source on ops_ frames only; a dataset version keeps "Unattributed
source". The shares door's public GET routes answer a cross-origin
preflight with If-None-Match and keep their ETag and freshness
headers through the platform CORS layer. Unsigned emulator links need
DFO_LOCAL_STACK=1, which the local stack sets.
POST /api/datasets/analysis keys each request before reading a frame
(the working tree, the resolved root records, latest and the
execution identity) and stores its 200 body in datasets.analysis_answer
(migration 0041). A request whose answer is stored, with its frame file
still served, answers at once; one whose key is running answers 202
with Retry-After 5, so a client retrying after a 504 joins the one run
and then reads the stored answer.

The shares door waits up to 5 s for a free thread before refusing 503,
so a share page with more slots than threads loads.
A share's history records an entry whenever what a slot shows changes,
including a return to a frame it showed before, and an empty entry
(no card, revision or frame, never fetchable) when a write leaves the
slot without a card. A client asking what a slot showed after a
replace emptied it reads that it showed nothing. Migration 0017 keys
served_frame by an identity column and lets the card and frame
columns be null together.
Creating or revising a card spends one unit of the caller's daily card
allowance (DATASETS_CARD_DAILY_ALLOWANCE, default 500; migration 0042);
a principal holding the publish grant spends none. A share create that
fails after spending, at the pin or the store, gives its unit back. A
live share holds at most 20 cards that draw a frame, checked on create
and replace. A slot read takes the history lock only when what the
slot shows changed, and a slot that becomes a note records that it
shows no frame.
…30968

datasets-maintenance normalise-recipes sorts each stored recipe root's
columns, as the analysis route now writes them, recomputes its
normal-form hash and replay key, and moves the presentation revisions
that carry the old hash, then the recipe row; --dry-run counts, a
second run changes nothing, and a recipe it cannot reproduce or whose
new key another row holds is reported and left. Run it after
import-shares. A card create replayed with its write token spends no
card unit. spec-kit is pinned to 3e30968, which maps
allowance_exhausted on presentation writes.
Comments, docstrings, route descriptions and the READMEs for the
analysis, pin, card and share routes and the local stack read as
plain statements.
The output digest is out2: each row leads with its stat_var, so two
frames pinning the same numbers to different variables differ, and the
synthetic recipe goldens are recorded again. allowance_exhausted is
served not retryable, with Retry-After naming the reset, on pins,
shares and cards. spec-kit is pinned to 03d8f5a.
…ion vectors

The analysis test doubles hand their fixed clock to the artifact store,
inventory and recipe store, so retention tests hold on any date. A new
test runs every operation-outputs vector in the client contract through
the analysis runner and checks its rows or refusal code.
…igest

The output digest names a variable that compare, sum_of, share_of or
quotient_of writes by its verb and its inputs' variables, walked from
the recipe tree, so a one-step and a filter-then-quotient_of with the
same rows share a digest, an engine release or rebuild that moves no
number keeps it, and swapped sides differ. A new analysis golden pins
the filter-then-quotient case, and record_case keys root variables by
root record. The composed app describes its cards as production does,
and the dashboard journey asserts sources under description.sources.
spec-kit is pinned to 17f0469.
@jm-rivera
jm-rivera added this pull request to stack #174 October 29, 2026 01:12
@jm-rivera jm-rivera changed the title feat/dashboard build feat(presentation): serve shares from dfo-apis and name computed variables in the output digest Oct 29, 2026
… engine

Analysis counts nodes as it walks, bounds anchor reads by root rows,
refuses malformed ref members and rolling shapes, cuts the left-out
list to the recipe's byte bound, and refuses a failed rebuild in place
of waiting on it; GET on the analysis paths answers 405, and
diff-recipes keys stored roots by record. Live share checks run
analyses through the stored-answer path and share one running set with
the analysis route, and refresh under the card's recorded rendering.

Sharing a derived card checks its roots first, pins its frame, and
pins each root still held; a frame with no recipe yet refuses
retryable recipe_pending. Stated retention follows the frame's own
version and revocation, and card reads and renders answer null when it
cannot be read. A card records its rendering at create.

A share create answers once its write commits, history records what
each slot serves and pages by keyset (migration 0018), every GET on a
public path carries Access-Control-Allow-Origin, a NaN payload is
refused before any spend, and a conditional slot read answers 304 from
its inputs. settle leaves caller keys alone and locks out concurrent
pins, a pin racing to an existing copy refunds its units, pin budgets
name their reset, and a derived frame's rendered file is linked to its
version.

call_verb refuses op_spec_invalid unless its inputs fill exactly the
slots the op spec names, an `on` list holding anything but column
names refuses op_spec_invalid, and a non-object rolling predicate
refuses op_spec_invalid. A paired side with several rows per member
and period refuses role_ambiguous naming that side and its entity
binding. Any `on` makes rolling pairing explicit, so an anchor never
reads flat populations the call does not use. A paired summary names
the fields the single-series summary does, and a step that cannot be
built is logged and left out of a computed result. Compare's data side
lives in one table, and the engine tests run in every shard of the
suite.
The local app builds one Datasets link and passes its cell and running analyses to both surfaces, as dfo_apis.app does, so the stack starts again and live share checks join analysis runs.
@jm-rivera
jm-rivera removed this pull request from stack #174 October 29, 2026 02:29
@jm-rivera
jm-rivera changed the base branch from feat/dashboard-card-store to feat/dashboard-engine October 29, 2026 02:29
@jm-rivera
jm-rivera added this pull request to stack #178 October 29, 2026 02:29
@jm-rivera jm-rivera changed the title feat(presentation): serve shares from dfo-apis and name computed variables in the output digest feat: run analysis on DFO's engine and serve cards and shares from dfo-apis Oct 29, 2026
minted_variable_names names a minted variable mv_ plus the first 32 hex
of SHA-256 over the RFC 8785 JSON of {verb, slot: sorted input names}.
A name document holds its inputs' names, so a name is 35 characters at
any depth; the JSON-text form doubled with each minting step.

_stored_file_digest reads through read_servable_version, so a cached
answer or a run that reuses a stored pair refuses snapshot_revoked (410)
when that output is revoked.

refund_together locks the allowance row before choosing between delete
and decrement, so a spend that commits between the two no longer leaves
the failed operation charged.
create_share, replace_share and snapshot_share take each card's
rendering and write every slot's served_frame entry after the revision
insert, under the share row lock. The revision's created_at is read from
clock_timestamp() while that lock is held, and each entry takes it as
first_served_at, so history lists writes in commit order and as_of
between two replaces selects the first. A write whose history insert
fails commits nothing and answers 503.

The door's post-commit _record_shown and _record_slots, and the store's
record_emptied, are removed. record_served takes the same lock and
records only while the latest revision still shows that card in that
slot, so a read that loaded an older revision adds nothing after a
replace.
frame_ref_for puts "without_party_totals": true in the cv_ preimage when
the cut is set and sorts groupby.by, and mint_options sorts by, so a
request with the cut and one without, over a dataset the cut leaves
unchanged, name two pairs and both answer 200; a replay of the cut
recipe keeps the cut. data-mcp in dfo-ai computes the same pairs, and
both repos pin cv_bb722d06259f357e6961a081f1c1f141 for one input.

When a run's own pair holds no frame, _store answers the pair
read_replay names for its recipe, so a frame stored under the earlier
naming is served instead of refusing ReplayKeyHeld. The ref, the top
tree node and any divergence report name the pair the frame is stored
under.
copy_to_pinned (datasets_api.retention_admission) is the one path into
pinned storage: the pin door's artifact, and each frame and derived root
a share create, replace or snapshot conversion pins. It spends the
global pin budget, the open-caller budget and the caller's pin
allowance for each new physical copy, skips copies that already exist,
and refunds only a copy that turned out deduplicated or whose source is
gone. A copy that stands keeps its units when the share write fails.
Share routes answer PinBudgetExhausted and OpenPinBudgetExhausted as
503 with Retry-After, and allowance_exhausted as 429. LiveFrameStore
takes the ledger as a required argument; share_import passes None.
DATASETS_PIN_DAILY_ALLOWANCE defaults to 200.

datasets-maintenance take-down-share <share_id> takes a share down as
the owner's delete does, for the shares the website records in its
share_cleanup table; an id with no share up exits 0. normalise-recipes
also sorts groupby.by. An imported share records each slot's history
entry, with its card revision's rendering, in the import transaction.
b7a40ad names minted variables mv_ plus a fixed-size digest and admits
a derived ref root that carries the groupby DFO read it with.
When an analysis run answers the pair its replay key names, every field
of the reply (ref, tree, summary and both frame_content_version values)
names that pair and describes the stored frame. If that pair's file is
no longer servable, supersede_replay marks the old recipe superseded and
the run publishes under its own pair; migration 0043 adds
frame_recipe.superseded_at and keeps replay keys unique among recipes
not superseded.

record_served records only while the revision the read served is still
the latest, so a read in flight during a replace or snapshot conversion
adds nothing. The shares door passes its allowance ledger to the frame
store it builds, and live.py builds none of its own.

Pin defaults: 100 new copies per caller a day, 2,000 for open callers
together and 2,500 in all, so one caller can spend at most 5% of the
open budget. Docs and refusal text say share routes spend them too.
0cb55e3 adds a nested minted-name vector with name documents and records
the grouped-root ref vectors from a dfo-apis run, with by sorted.
A share replace pins the frames it adds against its caller's pin
allowance, so the smoke PUTs send X-Forwarded-For as the create does.
# Conflicts:
#	src/datasets_api/README.md
#	src/datasets_api/config.py
)

* perf(datasets): send no request for members with no dispatch facet

_plan_collection_chunks packed eight consecutive members and skipped a
chunk only when none had a dispatch facet, so 753 of the 1,974 members
the total-ODA mint fetched held no facet and rode for nothing. Idle
members now plan into chunks of their own that take the skip path and
send no request, sync_estimate counts only members with coverage rows
under the bound roles with lanes per component, and the batch target
rises from 8 to 16. Total ODA sends 77 requests where it sent 247, with
the same content version, rows and artefacts.

* fix(datasets): never plan fewer requests for more members

stat_var_chunk_positions planned 3 requests for 5 members and 4 for 4,
so when the election idled a covered member the estimate could fall a
request short of dispatch. Each group now splits into min(lanes, n)
consecutive runs whose sizes differ by at most one, so the count never
falls as members are added and the estimate can only overshoot.
…rices (#191)

* fix(search): let named values a family group lacks drop out of its collapse

A named value no partition of a group carries no longer blocks the others, so commitments steers the collapse when the unit text also names RealValue.

* fix(search): elect the partition carrying the most named values, unconvertible ones first

A price basis can still be served by conversion after election and any other named value cannot, so the collapse ranks partitions by the values they carry that unit conversion cannot reach, then by those it can.
* fix(search): merge parts that bind the differing slot to a set

_build_merged_slots reads a value as a set of one and merges the union, refusing code sets that share a code and sets on the where axis.

* fix(search): state one provenance for a merged slot and keep a whole member set's ref

A merged slot filter reads text_recognized when any member named the property in text, a value without a ref refuses the merge, and a merged set equal to one member's set keeps that member's set_ref.
# Conflicts:
#	packages/dfo-data-tools/pyproject.toml
#	pyproject.toml
#	uv.lock
@jm-rivera
jm-rivera merged commit 3605201 into dev Oct 29, 2026
An error occurred while trying to automatically change base from feat/dashboard-engine to dev October 29, 2026 22:10
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.

Make a frame pair's identity agree with the recipe normal form on the party-totals cut and groupby.by order

1 participant