Skip to content

fix(client): request a scalar target on its own and track the tier a session has reached (rows-first c5b) - #1036

Closed
paddymul wants to merge 20 commits into
adr-003-stats-tiers-and-size-policyfrom
feat/rowsfirst-c5b-client-tier-bookkeeping
Closed

paddymul wants to merge 20 commits into
adr-003-stats-tiers-and-size-policyfrom
feat/rowsfirst-c5b-client-tier-bookkeeping

Conversation

@paddymul

@paddymul paddymul commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

An end-to-end browser run on the merged stack (the server phases and the client of c5, PR #1032) found two defects in the client.

A scalar target is never requested. With BUCKAROO_STATS_FULL_AUTO_ROWS=5000000 and stats_tier auto on a 10.8M-row parquet, and with a host-named stats_tier scalar, the first frame is {status: "not_computed", reason: "size"} ("host" for the host's choice) with tier_target: "scalar" and no auto_request (absent means true). The client sent 0 stats_request messages. desiredRun in StateOrchestrator.ts returns nothing for not_computed whenever auto_request is true, because only pending is run automatically and demand runs need auto_request false. The server contract (the module docs of stats_policy.py and stats_wire.py) says auto_request means the client should request up to tier_target on its own.

The compute control is stuck at scalar. With stats_tier schema (first frame auto_request: false, requestable: ["scalar", "full"]), three consecutive clicks each sent {tier: "scalar", force: true, incremental: true}. Each reply was a final stats_update {tier: "scalar", status: "not_computed"}, and df_meta.stats.tier stayed "schema", so the next click chose scalar again and the status bar kept the same button. The client had no record of the tier a run reached.

Those are the numbers from the integration run. I reproduced both against a real server before and after the change, at a smaller scale (see Tests, "End to end").

Phase and plan references

Client phase c5b, a follow-up to c5 (plans/03-no-summary-stats-for-large-files.md, section 6 "Phase 5", with sections 3.2 and 3.4). Also plan 1 sections 3 and 4.0 and plan 2 sections 3 and 4.1. The server contract is in the interface notes of p33 (#1029: auto_request, tier_target, requestable) and p34 (#1031: a scalar run changes nothing on the session, and its replies carry tier: "scalar").

Approach

Scalar target. autoRequestTier(stats) (WidgetTypes.tsx) is the tier the client should request on its own: tier_target when the status is not_computed, auto_request is not false, the reason is not ceiling or cost, the target is a tier above schema, and the tier reached is below it. It ignores requestable, which lists the tiers above the target. The scheduler gains a run kind, the target run. After the first rows (or the first-paint timeout) it sends one stats_request {stats_gen, scope: "raw", incremental: true, tier}, not forced, and marks the stats pending in a new df_meta as forceStats does, since the server sends no frame to a capable client while it runs. It then answers each reply that is not final (a new df_data_dict under the same df_meta) with the same request. A final reply, a refusal or a new gen ends it, and it is not started again for the state it ended in. With auto_request false the scheduler still idles except for demand_columns runs, and reason ceiling requests nothing.

Tier reached. The server leaves df_meta.stats.tier at schema after a scalar run, and a final reply names the tier that was asked for, not whether it ran: a refusal (ceiling, cost) carries the asked tier too, and nothing in the reply says whether the run covered the whole table or named columns. So the client keeps its own record.

  • requestStats records the request it sends on the model under stats_requested ({gen, tier?, columns?}), before sending it.
  • StatsChannel takes a final stats_update as the run that reached its tier for the whole table when it left the session not_computed, carries a payload (a refusal has none), and its tier and gen are those of the last request, which named a tier and no columns. It then sets df_meta.stats.reached_tier to the higher of that tier and the one already there. A run for some columns, a refusal, stats_aborted, and a reply to no request record nothing.
  • reached_tier lives in df_meta.stats, which every frame from the server replaces together with all_stats. A new gen therefore resets it, and the tier never outlives the stats it describes. StatsChannel is the only writer.
  • tierReached(stats) is the higher of the server's tier and reached_tier. nextRequestTier, and so canRequestStats and forceStats, read it: the first click asks for scalar, the second for full, and then nothing is left. A demand run is skipped once the tier reached covers the tier it would ask for.

Request columns. A request that names a tier no longer carries the grid's visible-columns hint, since the server reads columns with a tier as the scope of the run. The control and demand requests already carried none or their own columns, so only the new target request is affected.

Status bar. For a not_computed session that has reached a tier above schema, the cell names the tier on screen ("Basic stats" after scalar) beside the control for the next tier ("Compute full stats"). When nothing is left to request, or there is no handler, the control is replaced by a label: "Basic stats computed" (the tooltip says what scalar covers), or "Summary stats computed" for full. This holds under the ceiling too, since the stats on screen are not unavailable. A session that has reached nothing reads as before ("Compute summary stats", "Continue computing stats", "Summary stats unavailable: size limit").

What changes

  • src/components/WidgetTypes.tsx: reached_tier, higherTier, tierReached, autoRequestTier; nextRequestTier and demandTier read the tier reached.
  • src/server/StatsChannel.ts: STATS_REQUESTED_KEY and StatsRequested; reached_tier on a final reply.
  • src/server/StateOrchestrator.ts: requestStats records the request and drops the hint when a tier is named; the target run in the scheduler; StatsModel now includes set.
  • src/components/StatusBar.tsx, src/components/StatsEmptyState.tsx (exports TIER_DETAILS): the tier label.

Tests

Tests-only commit fea8ba9a, pushed before the fix. Locally, 37 jest tests and the 4 new Storybook Playwright tests failed on it, all on assertions. The Playwright tests run a new TierRuns story: the real model, channel, scheduler and status bar against a scripted server. On CI exactly JS / Build + Test and Storybook Playwright Tests failed; the other 26 entries completed without failure (25 SUCCESS, including the Read the Docs status, and deploy SKIPPED). The tests that already pass on the code before the fix are not in that commit: 29 test cases, namely the negative cases of autoRequestTier, the channel's "records nothing" cases, the scheduler's "sends nothing" cases and the status bar's unchanged labels. They are added with the fix, as in the earlier phases.

Fix commit 488a6a5a. On CI all 28 entries completed, 27 SUCCESS (26 check runs and the Read the Docs status) and deploy SKIPPED, including JS / Build + Test, Storybook Playwright Tests, Server Playwright Tests and all nine Python / Test jobs.

  • Jest: WidgetTypes.test.ts (tierReached, nextRequestTier, autoRequestTier), StatsChannel.test.ts (the tier a final reply reached, through a real WebSocketModel), StateOrchestrator.test.ts (the target run, the control's tier, and a "tier bookkeeping" block end to end through WebSocketModel with a fake socket: target to final reply, gen change, refusal, ceiling reply, scalar then full, a server that allows scalar only, a run for one column), StatusBar.stats.test.tsx (the labels).
  • Storybook Playwright, stats-scheduler-states.spec.ts: a scalar target is requested without a click and the status bar names the tier; the control asks for scalar, then full, and is gone after the full run; a server that allows scalar only leaves a "Basic stats computed" label; a new gen starts over.

Local runs before the fix push: jest 636 passed (35 suites), tsc -b clean, pnpm run build. Storybook Playwright with one worker and no retries: stats-scheduler-states 13 of 13, and the other four files CI runs 12 of 12. The Python unit suite (no Python file changed) gave 1182 passed and 5 skipped, with 3 lazy-widget tests failing in the full run (test_stats_clear_status_on_success, test_execution_update_messages, test_lazy_widget_init_should_not_block_but_does_with_mp_and_slow_exec) that pass alone (14 passed), the shared ~/.buckaroo sqlite case.

Server Playwright, locally: 53 of 54 on the first full run and 54 of 54 on the second. The failing test is server-buckaroo-search.spec.ts "searching filters the table data, not just the status bar count", which the c5 PR also recorded. It is not caused by this PR. On the base bundle (this PR's changes stashed, bundles rebuilt) four runs of that spec against a freshly started server gave a failure on the first run (0.9 s) and passes on the next three (3.7 s each). That session has no stats request. The test reads the text of .df-viewer as soon as the first .ag-cell is visible, and the first window of rows can come back after the pinned-row cells render. I did not pin why it is only the first run on a fresh server process. I suspect the first buckaroo-mode load in a process is slower (imports); warming the process with a throwaway session, or waiting for a row cell in the test, would decide it. The test is not changed here.

End to end. A scratch checkout of the integration branch (server phases p33, p34, p36a, p37 and the c5 client) with this PR's two commits cherry-picked, a real python -m buckaroo.server on a free port, a 50,000-row by 5-column xorq memtable build, and the standalone page driven by Playwright Chromium with the WebSocket frames read. The scale is 50,000 rows with BUCKAROO_STATS_FULL_AUTO_ROWS=10000, in place of 10.8M rows with 5,000,000, so the policy path is the same but the timings are not those of the large file. Nothing from that checkout is committed.

Session (first frame) Client before Client after
auto, size: not_computed, reason size, tier_target scalar 0 requests; the button asks for full 1 request {tier: "scalar", incremental: true} with no click; reply final, status not_computed, reason size, payload; the cell reads "Basic stats" with "Compute full stats"; one click runs full (2 requests), then "Summary stats ready" and no control
scalar, host: reason host, tier_target scalar 0 requests; the button asks for full the same, with reason host (1 request, then 1 forced full request after the click)
schema: auto_request false, requestable scalar and full 3 clicks sent 3 times {tier: "scalar", force: true, incremental: true}; the button never changed click 1 sends scalar (final reply, not computed) and the cell reads "Basic stats" with "Compute full stats"; click 2 sends full; "Summary stats ready", and the third click has no control
ceiling (BUCKAROO_STATS_CEILING_SCALAR_CELLS=1000), auto and scalar: reason ceiling, auto_request false, requestable empty 0 requests; "Summary stats unavailable: size limit"; no control

No test line of an earlier phase was edited (the test diffs add lines only). One test of this phase's own tests commit was corrected in the fix commit: it advanced the first-paint timeout in a single step, which leaves the zero-delay request timer queued, so it now steps as the existing first-paint test does.

Why default behaviour is unchanged

A session with no df_meta.stats, a pending session, and a not_computed session whose auto_request is false or whose reason is ceiling or cost send what they sent before. The target run starts only for a session that a server with the policy reports as not_computed with a target above schema and auto_request not false, and only to a client that advertises stats_ondemand. That needs the host to opt in with stats_tier or a server that resolves auto; the server default is unchanged. requestStats now also writes one model key on every request, which only StatsChannel reads. The hint is dropped only from requests that name a tier, and those were the control's (no hint already), the demand run's (its own columns) and the new target request.

Deviations from the plan

  • The tier reached is a field the client sets in df_meta.stats (reached_tier), derived from the client's own last request and the tier of the final reply. The task text asks for it to be tracked from the tier of final replies. The reply alone cannot say whether it covered the whole table, so the request is part of the rule.
  • A run for some columns never counts as reaching a tier. A scalar demand run therefore leaves the whole-table control at scalar.
  • The target run marks the stats pending on the model itself, which the c4 scheduler did not do for any run. The server sends no frame while a capable client's run is in progress, so without it the loading text and the placeholder rows wait for the first reply and the control stays on screen.
  • The target request names no columns, so a scalar target covers the whole table. The visible-columns ordering hint of c4b is not sent with a tier.
  • StatsModel (the scheduler's view of a model) and requestStats now need set, to record the request. Every IModel has it.
  • tier_target full with not_computed and auto_request true is requested the same way (the tier is the target); no server sends that today.

Not in this PR

  • Any server change. A server change would make the client rule simpler: a final reply that carried the columns of its run (or a scope), or a df_meta.stats.tier that moved to the highest tier a run reached, would replace the request record. The client is correct without either.
  • A paused run on a session with a scalar target: the control's tier for Continue is still the smallest requestable tier above the one reached, which for a scalar target with nothing reached is full. Continuing the target tier belongs with the cost guard (phase 6a).
  • Filtered-scope behaviour under search. A frame from the server replaces all_stats and reached_tier together, and the scalar target is not requested again for the same gen after its run ended.
  • The optional config upgrade (plan 3 phase 6b), the tallyman repo and the buckaroo-js-core release.

Stack

Built on feat/rowsfirst-c5-client-not-computed-control (#1032), which contains c4b (#1030), c4 (#1027), c2 (#1025) and c0a (#1020). The PR targets main, so the diff includes their commits until they merge: c0a 29e1e42c and 405f4ded; c2 fe046acc, 3118435a, 1fd9cff8 and 5935432d; c4 dd6ebe6a, d6afbf98 and 95dcceec; c4b d7eda002, 5d1e0467 and e5649605; c5 01609fa3, f2c4c0a4, 17376859, 7b30aad1, 30bf7c02 and 9992b2e2. This phase's commits are fea8ba9a (tests) and 488a6a5a (fix). Read only those.

🤖 Generated with Claude Code

paddymul and others added 19 commits October 3, 2026 22:58
…protocol (rows-first c0a)

Tests for the client half of plan 1 phase 0a. A server that sends the
summary stats separately, or not at all, leaves the first message
without values the client has been assuming.

BuckarooView: a change that reached the model before the effect
subscribed, one initial_state with metadata decoding once, and
out-of-order decodes applying the newer.

Pinned rows: a valueless key shows a placeholder with its own row id
while df_meta.stats.status is pending, and is omitted when not_computed.
The simple tooltip returns nothing for a valueless cell. color_map is
silent without bins and restyles when they arrive. An unrelated
df_data_dict update keeps the in-flight indicator when the server
reports df_meta.stats.

One Storybook Playwright test covers pending, not_computed and complete
in a real browser, and is added to the Storybook list in
scripts/test_playwright_storybook.sh.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… c0a)

BuckarooView reads model.get() for every key once its listeners are in
place, so a change:* emitted before the effect ran reaches React. All
df_data_dict values go through one decoder (makeLatestDictDecoder) that
skips a dict it has already seen and applies only the newest decode, so
an initial_state with metadata decodes once and out-of-order decodes
cannot overwrite a newer one. standalone.tsx gets the same decoder and
catch-up.

df_meta.stats.status reaches the grid as stats_status. While pending, a
required pinned key with no value becomes a placeholder row that carries
its key as index (unique row id, empty value cells). When not_computed
it is omitted. A missing df_meta.stats, or any other status, keeps the
old behaviour.

The simple tooltip returns nothing for a cell with no value or no row
data. color_map no longer logs when bins are missing, and the grid
refreshes the color-mapped columns when their bins change after the first
render. That needs RenderApiModule, which was not registered, so
api.refreshCells logged AG Grid error 200 and did nothing.

BuckarooInfiniteWidget keeps inFlight when only df_data_dict changed and
the server reports df_meta.stats; the answer to the dispatched change is
a frame with a new df_meta. Without df_meta.stats the rule is unchanged.

The Playwright story test no longer hovers a valueless pinned cell: with
the old tooltip it still passed, since AG Grid does not call the tooltip
for an empty value.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Jest tests, driven through a WebSocketModel with a fake socket, for the
client half of the stats wire: a stats_update is key-merged into all_stats
and dropped when its stats_gen is not the expected one, the expected gen
follows df_meta.stats.gen on every applied initial_state, a merge in
flight is discarded or redone when a frame replaces the dict, and
stats_aborted moves the status. Plus withStatsCapability and
BuckarooServerView putting ?caps=stats_update on the WebSocket URL, and a
server Playwright test that the standalone page does the same.

StatsChannel.ts is a stub (identity withStatsCapability) so the tests fail
on assertions rather than on a missing module.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ct (rows-first c2)

Three more cases for the stats_update merge, found untested after the
first test commit: a wide parquet_b64 payload as the server sends it (the
shared summary_stats fixture, not a json envelope), a model that holds no
df_data_dict yet, and a dict with no all_stats key.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two stats_update messages for one gen are applied in arrival order even
when the first payload decodes more slowly: the final update merges last
and the status completes only after both merges.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ability (rows-first c2)

StatsChannel is the client half of the stats wire. WebSocketModel hands it
every text frame first. A stats_update for the gen the model's df_meta
reports is decoded and key-merged into df_data_dict.all_stats (new row
objects, a new dict), updates apply one at a time in arrival order, and a
final update sets df_meta.stats to complete at the update's tier. A merge
is dropped when the gen moves on while it decodes and redone when a frame
replaces the dict. stats_aborted for the expected gen sets error or
not_computed. Other message types are ignored as before.

withStatsCapability adds caps=stats_update to a WebSocket URL; both wiring
copies (BuckarooServerView and the standalone page) use it so the server
treats them as capable clients.

The guard tests that already pass before the fix (caps already present, no
df_meta.stats, ignored aborts, unknown types, infinite_resp pairing) are
added here, with a makeModel(null) helper so a model without stats can be
built.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ows-first c4)

Jest tests for the scheduler (StateOrchestrator rewritten around an IModel:
a stats_request after the first infinite_resp, one request per reply until
final, nothing after a state change, nothing for a search_string-only change),
for the status bar's stats column and Compute summary stats control, for the
widget and BuckarooView wiring of that control, and for pinned rows in the
error state. Playwright tests on Storybook cover the four states without
layout shift and the control's request, and the server spec runs the real
standalone page against a session whose first frame says the stats are pending.

StateOrchestrator.ts, StatusBar.tsx and BuckarooWidgetInfinite.tsx carry only
the signatures the tests use, so the tests fail on assertions.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tus layout and the control wiring (rows-first c4)

Cases found untested or mis-specified after the first push, written against
the same stubs: a model that starts complete waits out the delay for its first
state change, as does a change made after the earlier stats completed; a
request's time is measured once; the Compute summary stats button calls its
handler with no arguments; the standalone page offers that button for a session
whose stats are not computed and asks for nothing until it is clicked. The
layout spec now says what holds across statuses (nothing above or beside the
grid moves, and the grid's height follows the pinned area), and the story loads
the widget's stylesheet as the real page does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…g, not computed and error states (rows-first c4)

StateOrchestrator becomes the client scheduler for the stats wire. It takes an
IModel, so it sends stats_request {stats_gen, scope: "raw"} through model.send
and watches change:df_meta, change:df_data_dict, change:buckaroo_state and
msg:custom. It asks once the first infinite_resp has arrived (or after 1.5 s
without one), asks again for each reply that leaves df_meta.stats pending, and
stops when the status changes. A change to post_processing, cleaning_method or
quick_command_args waits out a delay of twice the last request's time (200 to
3000 ms) before asking for the next gen; a search_string-only change, or any
other field, leaves the schedule alone. Nothing is requested unless
df_meta.stats says pending.

WebSocketModel starts the scheduler, so BuckarooServerView and the standalone
page get it with no wiring of their own. The status bar gets a fixed-width stats
column, for sessions that report df_meta.stats, showing loading, a Compute
summary stats button that sends stats_request {force: true}, the error reason, or
ready. Valueless pinned rows are omitted in the error state, as in not_computed.
requestStats and StateOrchestrator are exported for hosts with their own IModel.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…umns hint (rows-first c4b)

The scheduler and the Compute summary stats control should send
stats_request with incremental: true and, when the grid has reported them,
the columns it shows as the columns hint. A run is one request per partial
reply, with the stats pending until the final reply, and a gen change
mid-run stops it and starts one for the new gen.

Jest: the scheduler's request shape, hint and chain against a fake model and
against a real WebSocketModel (partial replies, a final that carries the
complete all_stats, a whole-run final from a server that ignores the field,
a gen change mid-run), requestStats, BuckarooView's callbacks, and the grid
reporting its viewport columns. Playwright: the server spec (a harness
answers stats_request over a real session, including a wide table whose hint
follows a horizontal scroll) and the Storybook control. The existing request
assertions gain incremental: true.

The two unused on_visible_columns props are stubs so the tests compile.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… test releases a reply (rows-first c4b)

The grid reports the columns it shows a frame after it renders them, and
the wide-table test released the reply as soon as the header cells had
changed, so on a loaded machine the next request could go out with the
earlier hint. Wait two animation frames first. No assertion changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ith a columns hint (rows-first c4b)

requestStats, which the scheduler and the Compute summary stats control both
use, now sends stats_request with incremental: true and, when the grid has
reported them, the columns it shows as the columns hint, read from the
model's visible_columns key when the request goes out. A server on the unit
path answers with a partial stats_update per request, the scheduler asks
again for each, StatsChannel merges them without leaving "pending", and the
final reply with the complete all_stats ends the run. A server that ignores
the fields answers with a whole-run final, which ends it the same way.

DFViewerInfinite takes on_visible_columns and reports the data columns that
have a header cell in the grid's viewport on grid ready and on
onVirtualColumnsChanged. BuckarooInfiniteWidget forwards it, and BuckarooView
and the standalone page store the list on the model with setVisibleColumns.
The columns are read from the rendered header cells because the grid's column
API is not registered, and registering it would switch on the column-state
calls that are inert today.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rol and demand units (rows-first c5)

Tests for plan 3 phase 5, with the stubs they compile against (empty
StatsEmptyState, forceStats and the df_meta.stats helpers return nothing):

- a not_computed session renders typed columns with no blank pinned rows, the
  summary view shows an empty state with the compute control, and for reason
  "ceiling" a message and no control
- auto_request false leaves the scheduler idle except for demand_columns, which
  it asks for as one scoped request
- the control sends stats_request {force, tier} with the tier taken from
  requestable (scalar before full), and a per-column form names the columns;
  the scheduler continues a forced run
- a final stats_update with status not_computed and reason ceiling ends in the
  ceiling message
- the client advertises ?caps=stats_update,stats_ondemand from both wiring
  copies

Existing assertions of the capability URL and of the control's request change
because the request shape changes by design.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ing and finished demand runs (rows-first c5)

Found while running the first test commit's behaviour end to end against the
p33 server:

- the control marks the stats pending on the client, since the server sends no
  frame to a capable client, so the loading text shows at once and a second
  click cannot start a second chain of requests; a refusal or a run for some
  columns returns the session to not_computed, and a full frame for the same
  gen ends the run
- a session the server sized at the ceiling itself (reason "size", requestable
  empty) reads as over the limit: the message and no control
- a demand run that has ended is not asked for again when a later frame
  arrives, and a full frame for the same state does not end one in flight

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…or the stats policy (rows-first c5)

The client side of plan 3 phase 5, against the df_meta.stats fields of the
policy wire (p33): tier_target, reason, auto_request, requestable, estimate,
demand_columns, omitted_keys and approx_keys.

- ?caps= carries stats_update and stats_ondemand, from both wiring copies, so a
  server applies its policy to this client.
- A final stats_update may carry status and reason. {final: true, status:
  "not_computed", reason: "ceiling"} with no payload ends in the ceiling
  message; a reply with a payload and a status merges it and leaves that status.
- The control is forceStats: stats_request {force, tier}, the tier being the
  smallest requestable above the one reached (scalar before full), with a
  per-column form that names columns. It marks the stats pending, and the
  scheduler continues the run, one request per reply that is not final.
- With auto_request false the scheduler asks only for demand_columns, as one
  scoped request per gen and list of columns.
- The status bar says why the stats are missing and offers the control unless
  the ceiling refused them. The summary view shows an empty state with the
  reason, the size and the control, and a column picker, in place of its grid.
  Nothing changes for a session whose df_meta.stats is absent.

Needs a buckaroo-js-core release and a tallyman bump.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…control's clicks (rows-first c5)

The Storybook Playwright job failed on the c5 fix commit: clicking "Compute
summary stats" in the status bar timed out with ".ag-body-horizontal-scroll-container
from .ag-body-horizontal-scroll.ag-scrollbar-scrolling.ag-scrollbar-invisible
subtree intercepts pointer events". The control's button was 20px high in a
20px row, so the browser scrolled the grid to reveal it, AG Grid flagged the
horizontal scrollbar as scrolling, and its overlay (positioned over the last
16px of the one-row grid, not drawn) sat above the button.

- The button is 16px high and its container fills the row, so it fits inside it.
- The status bar's invisible scrollbar overlay takes no pointer events. A
  scrollbar the browser draws in its own space (no ag-scrollbar-invisible) is
  left alone, and the strip still scrolls with the wheel.

Checked with the overlay held in its scrolling state: a click on the control
fails with the CI message on the previous CSS and lands with this one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…mns computed (rows-first c5)

A scoped run (the per-column form, or the demand columns) ends with status
not_computed and a payload that StatsChannel merges into all_stats. The
summary view shows the empty state for as long as the status is not_computed,
so those stats are not shown anywhere.

The tests pin a df_meta.stats.computed_columns list that StatsChannel sets
when a run ends not computed, and the summary view showing its grid once the
list is not empty. DFMetaStats carries the field as a type only, so the
failures are assertions. Two assertions of this branch's own tests that read
the end state of a scoped run now expect the list.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…mmary view (rows-first c5)

A run for some columns ends with status not_computed and a payload, and
StatsChannel merges the payload into all_stats. The summary view showed the
empty state for as long as the status was not_computed, so the stats of the
column the user picked, or of the demand columns, were not shown.

StatsChannel now keeps the columns the replies of a run filled (a column
counts where some stat row has a value for it) and a final reply or a
not_requestable refusal that leaves the session not computed puts them in
df_meta.stats.computed_columns. A new gen, or a frame that replaced the
stats, starts the list over. The summary view shows its grid, not the
empty state, when the list is not empty.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… and for the tier a session has reached (rows-first c5b)

A session the server sized to scalar is never requested (desiredRun returns nothing for not_computed while auto_request is true), and the compute control asks for scalar on every click because df_meta.stats.tier stays schema after a scalar run. The tests cover the scheduler (the target tier is requested once, continued to its final reply, not requested for auto_request false, ceiling, cost or a reached target), the channel (a final reply to a whole-table request records the tier reached, reset by a new gen), the control (scalar, then full, then nothing), the status bar label and the Storybook flow. tierReached and autoRequestTier are stubs here.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

📦 TestPyPI package published

pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37355456647

or with uv:

uv pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37355456647

MCP server for Claude Code

claude mcp add buckaroo-table -- uvx --from "buckaroo[mcp]==0.15.9.dev37355456647" --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo-table

📖 Docs preview

🎨 Storybook preview

…session has reached (rows-first c5b)

The scheduler gains a target run: a not_computed session whose auto_request is not false and whose tier_target is above the tier reached (autoRequestTier) gets one incremental stats_request for that tier, not forced and with no columns, after the first rows. The run marks the stats pending as forceStats does, is continued for each reply that is not final, ends on a final reply, a refusal or a new gen, and is not started again for the state it ended in. A reason of ceiling or cost, auto_request false and a schema target request nothing.

requestStats records the request it sends under stats_requested, and StatsChannel reads it to tell a final reply to a run for the whole table (a tier and no columns, with a payload, leaving the session not computed) from a refusal or a run for some columns. Only the first records df_meta.stats.reached_tier, the higher of what was there and the reply's tier. A frame from the server replaces df_meta, so a new gen resets it. tierReached reads the server's tier and reached_tier, nextRequestTier and demandTier read tierReached, so the control asks for scalar, then full, then nothing. A request that names a tier no longer carries the grid's columns hint. The status bar names the tier on screen and replaces the control with a computed label when no tier is left.

One of the phase's own tests had a timer step that never reached its request (it advanced the first-paint timeout in one step, which leaves the zero-delay request timer queued); it now steps as the existing first-paint test does. The guard tests that pass on the code before the fix are added here.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@paddymul
paddymul changed the base branch from main to adr-003-stats-tiers-and-size-policy October 6, 2026 14:40
@paddymul

paddymul commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Closing. Under the revised D2 of ADR-002 (#1043, 7943412) the server pushes the policy's target tier itself, and it should report the tier reached in df_meta.stats, which makes the client-side reached_tier record unnecessary (as this PR's description notes).

The status-bar tier labels still apply and are the part to carry into a client UI PR on top of #1025. The branch is kept for them.

@paddymul paddymul closed this Oct 6, 2026

This branch was successfully deployed

1 active deployment
testpypi — 488a6a5a Deployed Oct 5, 2026 by paddymul via Publish to TestPyPI #1719
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