Found while browser-testing a real board in a consuming app (objectstack-ai/ats), where a pipeline kanban over 200 records displayed lane counts of 77 · 19 · 2 against a true distribution of 88 · 46 · 28 · 14 · 9 · 15.
Mechanism
Two lines, both in this package:
src/ObjectKanban.tsx:264 — the fetch is windowed: $top: schema.limit ?? DEFAULT_KANBAN_LIMIT, with DEFAULT_KANBAN_LIMIT = 100.
src/KanbanImpl.tsx:663 — the header renders ({col.cards.length}).
col.cards is the client-side array produced by grouping what came back. So the header is a count of fetched rows that fell into this lane, not a count of the group. Over any object with more rows than the window, every lane number is wrong and they sum to limit.
Why this is worse than an ordinary off-by-window bug
The numbers look right. 77 · 19 · 2 is a plausible recruiting funnel; nothing about it says "truncated". A reader has no signal that the board is showing a sample — no ellipsis, no "100 of 200", no styling difference. The failure is silent and the artefact is credible, which is the combination that gets a wrong number into a status report.
This is a consequence of #4025's fix, not of the original design
#4025 established that the cap was written as options: { $top: 100 }, a key no adapter reads, so it never reached the wire — the board "fetched whatever the server chose to return, then grouped all of it into lanes client-side". DEFAULT_KANBAN_LIMIT's own doc comment records this and notes "the window is now real".
That fix was correct. But making the window real is exactly what turned a count that had been accidentally right (whole result set grouped) into one that is systematically wrong. The header label was never revisited, because before #4025 there was effectively nothing to revisit.
Related, not duplicate
Shape of a fix
Not prescribing one, but the options seem to be:
- Ask the server for group counts — an aggregate over the whole filtered set keyed by
groupByField, the way a grid's server-side grouping already gets its header numbers. Correct at any size; costs one extra query.
- Say what the number means — render
77 of 88, or mark the lane as windowed when rows.length === limit. Cheap, honest, and does not pretend to a total the board never fetched.
⛔ What should not happen is raising DEFAULT_KANBAN_LIMIT: it moves the threshold without changing the property, and the failure stays silent for boards above the new number.
Reproduction
Any object-kanban with groupByField over an object holding more than 100 rows and no authored limit. In the case measured: ats_application, 200 rows, grouped by stage, lanes summed to the window rather than to 200.
Found while browser-testing a real board in a consuming app (
objectstack-ai/ats), where a pipeline kanban over 200 records displayed lane counts of 77 · 19 · 2 against a true distribution of 88 · 46 · 28 · 14 · 9 · 15.Mechanism
Two lines, both in this package:
src/ObjectKanban.tsx:264— the fetch is windowed:$top: schema.limit ?? DEFAULT_KANBAN_LIMIT, withDEFAULT_KANBAN_LIMIT = 100.src/KanbanImpl.tsx:663— the header renders({col.cards.length}).col.cardsis the client-side array produced by grouping what came back. So the header is a count of fetched rows that fell into this lane, not a count of the group. Over any object with more rows than the window, every lane number is wrong and they sum tolimit.Why this is worse than an ordinary off-by-window bug
The numbers look right. 77 · 19 · 2 is a plausible recruiting funnel; nothing about it says "truncated". A reader has no signal that the board is showing a sample — no ellipsis, no "100 of 200", no styling difference. The failure is silent and the artefact is credible, which is the combination that gets a wrong number into a status report.
This is a consequence of #4025's fix, not of the original design
#4025 established that the cap was written as
options: { $top: 100 }, a key no adapter reads, so it never reached the wire — the board "fetched whatever the server chose to return, then grouped all of it into lanes client-side".DEFAULT_KANBAN_LIMIT's own doc comment records this and notes "the window is now real".That fix was correct. But making the window real is exactly what turned a count that had been accidentally right (whole result set grouped) into one that is systematically wrong. The header label was never revisited, because before #4025 there was effectively nothing to revisit.
Related, not duplicate
ObjectKanbanSchemateachlimiton anobject-kanban, the renderer reads it as$top— and the spec's strictComponentPropsMaprefuses it by name #8172 (open) —limitauthoring onobject-kanban. Touches the same knob; raisinglimitmitigates this issue without fixing it, since any board can exceed whatever the author picks.Shape of a fix
Not prescribing one, but the options seem to be:
groupByField, the way a grid's server-side grouping already gets its header numbers. Correct at any size; costs one extra query.77 of 88, or mark the lane as windowed whenrows.length === limit. Cheap, honest, and does not pretend to a total the board never fetched.⛔ What should not happen is raising
DEFAULT_KANBAN_LIMIT: it moves the threshold without changing the property, and the failure stays silent for boards above the new number.Reproduction
Any
object-kanbanwithgroupByFieldover an object holding more than 100 rows and no authoredlimit. In the case measured:ats_application, 200 rows, grouped bystage, lanes summed to the window rather than to 200.