Repository navigation
A gantt lens fetches its rows twice — once paged (which feeds the footer) and once unbounded (which feeds the chart), so the footer misdescribes the chart and the real fetch has no ceiling #7210
Description
Activity
- added a commit that references this issue
on Sep 1, 2026 - addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 1, 2026 Claimed and SPLIT —
domain:uiexecution seat- session
session_012wwHa4aaFybxXrfmfHioDM - branch
claude/issue-7210-gantt-paging-chrome
The card asks that the two observations be judged together, and they have been. Judged together, they do not ship together — one of the three is a decision this seat cannot make, and the other two do not depend on how it is made. That is the split, and it is the reason for it.
Dispatched now — halves 1 and 3
Half 1 (the footer misdescribes the chart) is dispatched because it is invariant to half 2. This is the card's own observation doing the work: "whichever way it is settled, the footer and the chart should be describing the same fetch." Correcting the paging chrome to describe the fetch the visualisation actually consumed is right if a ceiling is later added, right if one never is, and right today. It is not a bet on the outcome — so it does not have to wait for it.
Half 3 (the same rows fetched twice, under two different filter dialects) is dispatched conditionally, and deliberately not as "dedupe the requests." Deduping means choosing between the authored rule array and the compiled
["and", …]tree, and choosing wrong changes which rows come back — a silent wrong answer in a card that is about silent wrong answers. So the lane is instructed to measure first: does the paged request have any consumer other than the footer? If the footer is its only consumer, then correcting the footer to describe the chart's fetch leaves that request dead, and it can be removed without anyone picking a dialect. If it has other consumers, the lane reports and stops rather than guessing.The dialect disagreement is recorded either way. Two code paths compiling the same authored filter differently is worth its own card if half 3 does not dispose of it.
⚠️ Escalated, NOT dispatched — half 2Whether a non-grid view is allowed an unbounded fetch is a maintainer decision, and the card is right that the current state is the absence of a
toprather than a decision. It is genuinely two-sided: a gantt does need enough rows to compute a truthful range, which is a real argument against naive paging — and at 100k scheduled rows it is the whole table into the browser with no knob reachable from view metadata, sincepagination.pageSizecannot bound a request that never carried atop.I am not ruling that, and the lane is instructed not to touch it. Adding a cap changes what a gantt draws for every existing deployment whose result set exceeds it — from a complete chart to a quietly truncated one. That is the more dangerous direction than the status quo, because a truncated schedule still looks like a schedule.
Carried to the escalation list on #5560.
One correction to the record
This card's own history is the strongest argument in it.
objectstack-ai/duly#96was filed concluding from this footer that the gantt's range and grouping were computed over a 100-row slice — sound reasoning, wrong answer, and it cost a browser session to disprove. That is half 1's severity measured rather than asserted: the footer did not merely fail to inform, it actively produced a false finding in a downstream repo.
Generated by Claude Code
- session
os-dev-report
{ "issue": 7210, "status": "done", "branch": "claude/issue-7210-gantt-paging-chrome", "pr": "https://github.com/objectstack-ai/objectui/pull/7223", "premise_still_valid": true, "summary": "Half 1 shipped: ListView's record-count bar (row count + 'Showing first N records. More data may be available.' + rows-per-page selector) is now suppressed on the gantt surface alone, behind a named constant `surfaceDrawsFetchedRows` in packages/plugin-list/src/ListView.tsx. Suppression rather than re-description, because ListView cannot report the chart's count without a new upward channel from ObjectGantt whose meaning would change if a ceiling is ever added; suppression is invariant to half 2, which is the property the split was made for. HALF 3'S CONDITION FAILED and nothing was deduped: the paged request has five consumers besides the footer, two of them behavioural. HOW MANY REQUESTS: three, not two - one paged ($top: pageSize, $select, authored filter array, issued by ListView) and TWO unbounded (ObjectGantt.reload, before and after its own object-schema read settles; the second adds $expand). Neither unbounded call carries $top. IS THE CHROME SHARED: yes - one block in ListView rendered for every viewType, and it is correct everywhere but gantt, so the change is scoped to gantt and both a grid and a kanban control pin that. WHY THE GANTT IS DIFFERENT: plugin-gantt's ObjectGanttRenderer destructures { schema } and hands its child only schema and dataSource, forwarding no host props - the only view wrapper that does not (grid, kanban, calendar, map and tree all spread props). So ListView's `data` never reaches the chart and ObjectGantt queries for itself. Forwarding it would cap the chart at one page, which is exactly half 2's forbidden direction, so it was not done.", "tests": "All at f3d7b3cd3 (git rev-parse --short HEAD of the final commit). (1) `pnpm exec vitest run packages/plugin-list/ packages/plugin-gantt/` from the repo root: 'Test Files 113 passed (113) / Tests 1183 passed (1183)'. (2) New files: packages/plugin-list/src/__tests__/ListView.ganttPagingChrome-7210.test.tsx (4 cases: 2 gantt, 1 grid control, 1 kanban control) and packages/plugin-gantt/src/ObjectGantt.hostDataProp-7210.test.tsx (2 cases: adapter rows drawn instead of the host `data` page; no $top or $skip in any issued query). (3) REVERSE VERIFICATION, direction and counts predicted BEFORE running: removing `surfaceDrawsFetchedRows &&` from the bar's render condition turns exactly the two gantt cases red and leaves both controls green, i.e. 2 failed / 2 passed. Observed exactly that ('Tests 2 failed | 2 passed (4)'), then 'Tests 4 passed (4)' after restore. Mutation proved on disk BEFORE reading any run: removed-token count 2 to 1 and mutated-line count 0 to 1 (grep -o | wc -l, entries not lines), plus blob hash 4a1ce3c11b87efae... to 7f55c73b8e98b7b9...; the script carried a `trap restore EXIT INT TERM` with an absolute REPO_ROOT path. Restore proved by STATE, not by exit code: `git checkout HEAD -- ABSOLUTE_PATH` (never the bare form), then `git diff HEAD`, `git diff --cached` and `git status --short` all empty and the blob hash back to the HEAD blob 4a1ce3c1. (4) Typecheck: `pnpm --filter @object-ui/plugin-list run type-check` and the same for @object-ui/plugin-gantt, both exit 0 over a freshly built dependency closure (`pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' --filter '@object-ui/plugin-gantt^...' build`, exit 0). Because each package's type-check is `tsc --noEmit && tsc -p tsconfig.test.json`, I proved the two NEW test files are actually inside that program with `tsc -p tsconfig.test.json --listFiles`: 1 hit each. (5) Gates, each run with output redirected before the exit code was read: check:control-bytes, check:phantom-deps, check:self-import, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys, check:i18n-dead-keys, check:element-data-source-declaration, check:side-effects-array, lint:coverage ('lint coverage: 46/46 packages linted, 0 with outstanding errors'), and all four changeset guards (fixed / no-major / overwrite / presence, the last printing 'this change declares 1 changeset(s)') - every one exit 0. check:sdui-registration-pins returned exit 2 with its own 'This is exit 2, not a pass' banner (no apps/console build to weigh): recorded as NOT MEASURED, not as a colour; the diff adds no registration and moves no entry array, so it is left to CI. (6) ESLint, NARROWED and declared: 3 changed files, 0 errors, exit 0. The three pieces of evidence the narrowing needs: the population came from eslint's own flat config resolving the paths (not my guess at what counts), the file count 3 was read from `--format json` output, and eslint.config.js enables NO type-aware linting (no `project` / `projectService` under its `languageOptions`), so no untouched file's verdict can move because of this diff - which the diff also cannot do by other means, adding no config and no inline directive. (7) Control-byte self-scan over all four changed files with grep -naP: no hits. Every heavy run went through /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=objectui-7210.", "mcp_calls": "6 - one search_issues (dedup), two issue_write creates, one create_pull_request, one pull_request_read (body read-back), one add_issue_comment (this report). Every card and comment READ went through the zero-quota embedded-JSON web channel; repo-scoped REST is 403 for this session (probed first: GET /repos/objectstack-ai/objectui returned 403 'GitHub access is not enabled for this session'), and `gh` is absent.", "open_questions": [], "out_of_scope_findings": [ "filed as #7221: one authored view filter reaches the wire in two dialects - ListView forwards the authored value while the gantt path recompiles it through ElementDataSourceGate's mergeFilterNodes (plugin-gantt declares `filter: true` in its mapping), which wraps two or more surviving sources as ['and', ...nodes]. Required by the ruling's item 4 (report, do not repair). Nobody has established the two shapes select the same rows; that measurement is the card's first step.", "filed as #7222: ObjectGanttRenderer forwards no host prop but `schema` - the only view wrapper that does not. Two certain consequences (ListView's `{...(ganttOwnsData ? {} : { data })}` clause is inert for a gantt, and the live comment above it stating that the prop 'short-circuits the renderer's own fetch' is false, which is precisely what makes #7210's double fetch invisible on a read-through) and one SUSPECTED, explicitly marked unmeasured (ListView's onRowClick / rowHeight / onRowSelect cannot arrive either, so an authored gantt `navigation` config may not be honoured - not verified in a browser). The card explicitly does NOT propose spreading props, because forwarding `data` would cap the chart at one page.", "NOT filed, deliberately, to avoid duplicating a card that already owns it: ObjectGantt issues TWO unbounded queries per load (the pre-schema one carries no $expand and is a wasted round trip). That is the exact 'two queries, the first unexpanded' regime already tabulated against plugin-gantt/src/ObjectGantt.tsx on #6482, which is no longer open. Re-confirmed here by measurement; recommend REOPENING #6482 rather than filing a fourth card.", "NOT filed, belongs to #7210's own still-open half 3: plugin-view's ObjectView non-grid path double-fetches for a gantt in the same way (its own $top: 100 query plus the chart's unbounded one). No paging chrome renders on that path, so half 1 had nothing to correct there." ] }Generated by Claude Code
Generated by Claude Code
Halves 1 and 3 resolved — PR #7223 landed
5015fcf52. ⛔ This card STAYS OPEN for half 2.Landing note from the
domain:uiseat (sessionsession_012wwHa4aaFybxXrfmfHioDM). Clearingpm:dispatched; keepingneeds-user-decision.Verified by content on
origin/main, with a live control:probe on main surfaceDrawsFetchedRows(the fix)2 record-count-bartestid — live control1 The control is the one that matters: the bar was conditioned, not deleted. A probe that only checked the fix landed would pass equally if the whole record-count bar had been ripped out, which is the failure mode worth guarding against on a change whose mechanism is "stop rendering something".
Half 1 — shipped
The record-count bar is suppressed on the gantt surface alone. Everywhere else it keeps reporting, because everywhere else it is telling the truth — pinned by a grid control and a kanban control in the test file, not by assertion. On the same harness (18 rows,
pageSize: 6) the grid reads 18 and the kanban reads 6 while genuinely drawing 6; only the gantt read 6 while drawing 18.Suppression rather than re-description, deliberately: re-describing would need a new upward channel from
ObjectGanttwhose meaning would change if a ceiling is ever added — i.e. it would bet on half 2's outcome. Suppression is invariant to it, which is the property this card was split on.Half 3 — condition FAILED, nothing deduped, and that is the correct outcome
The dispatch made the dedupe conditional on the paged request having no consumer besides the footer. It has five, two of them behavioural:
datagates the loading skeleton and the load-error panel (so it decides whether the chart mounts at all), feedsUserFilters' option counts, and backs the client-side export.Measured, not reasoned — with the paged response delayed, the skeleton was up and the chart had 0 rows though its own unbounded response had already arrived.
So the request could not be removed, and the lane stopped rather than picking between the two filter dialects. Good: choosing wrong there would have silently changed which rows come back, on a card about silent wrong answers.
Corrections to this card's own measurements
- Three requests, not two. One paged, and two unbounded —
ObjectGantt.reloadfires before and after its own object-schema read settles, the second adding$expand. Neither unbounded call carries$top. - The root cause is a prop-forwarding asymmetry.
ObjectGanttRendererforwards onlyschemaanddataSource— the only view wrapper that does not spread host props; grid, kanban, calendar, map and tree all do. SoListView'sdatanever reaches the chart. Worse, the live comment inListViewclaims thedataprop "short-circuits the renderer's own fetch" — that comment is false, and its falseness is exactly what made this double fetch invisible to anyone reading the code. Filed as finding(plugin-gantt):ObjectGanttRendererforwards no host prop butschema— the one view wrapper that does not, soListView'sdata/onRowClicknever arrive and a live comment says the opposite #7222.
⚠️ Still open on this card: half 2Whether a non-grid view may fetch unbounded remains a maintainer decision, escalated on #5560 and unanswered. Nothing in #7223 pre-empts it.
⛔ Note the tempting wrong fix is now explicitly fenced off in the code: forwarding
datato the chart would cap a gantt at one page — "turning a complete schedule into a quietly truncated one that still looks like a schedule." That is half 2's forbidden direction reached by the back door, and the landed docblock says so.Filed alongside
- finding(plugin-gantt/plugin-list): one authored view filter reaches the wire in two dialects — ListView forwards it, the gantt path recompiles it through
mergeFilterNodes#7221 — one authored filter reaches the wire in two dialects (report-don't-repair, per the ruling). Nobody has established the two shapes select the same rows; that measurement is the card's first step. - finding(plugin-gantt):
ObjectGanttRendererforwards no host prop butschema— the one view wrapper that does not, soListView'sdata/onRowClicknever arrive and a live comment says the opposite #7222 — the prop-forwarding asymmetry above. useSettledSchemawas extracted and published with ONE adopter — the convergence #6482 asked for is 1 of 4, and the gantt's ungated double fetch is still live #7225 — while checking the lane's recommendation to reopen finding(ui): the schema-settled$expandgate is now four hand copies, and four more views resolve the schema without gating on it #6482, I measured thatuseSettledSchemawas extracted and published with exactly one non-test adopter, so that convergence is 1 of 4 and the gantt's ungated double fetch is still live. Filed as a new card rather than reopening finding(ui): the schema-settled$expandgate is now four hand copies, and four more views resolve the schema without gating on it #6482, whose body is now a five-week-old snapshot of a tree that has moved.
Generated by Claude Code
- Three requests, not two. One paged, and two unbounded —
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actionsMaintainer ruling recorded — a′: a non-grid view may fetch unbounded up to a platform ceiling constant; past it, a loud footnote, never a silent truncation
Director seat (objectstack #12708), summon #8, session
session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #6 in which this card was item 4 with the recommendation a′ (fallback a; b and c not recommended). Verbatim reply: 「7205 B,其他同意」 — "其他同意" covers this card, so a′ is adopted as recommended.
Scope of this ruling. Halves 1 and 3 of the card (the footer describing the wrong request; two requests per load) have landed via PR #7223. What remained open was half 2, "is a non-grid view allowed an unbounded fetch": that is what is ruled here.
Ruled: a′. A non-grid visualisation (gantt, and by the same rule calendar, map, tree) is allowed to fetch the whole filtered result set, because a truthful range or layout needs all of it, but the fetch carries a platform-level hard ceiling expressed as a named constant in the renderer, not as an authorable view key. When the result set exceeds the ceiling the visualisation draws the first N rows and shows a loud footnote of the form "showing first N of M records; narrow the filter" (objectui#7148's chart footnote is the precedent for placement and tone). The dangerous direction is silent truncation: a cut-off schedule still looks like a schedule, so the ceiling must never be crossed quietly.
Not taken: a (unbounded with a documentation note only, still the whole table into the browser at 100k rows); b (truncate at
pageSize, silent truncation); c (an authorablemaxRowskey, a new permanent key that every author would have to set).Execution:
domain:uilane; dispatched together with the gantt gating from objectui#7225 (same component, one PR). The ceiling value is set by the implementer after measuring it across the four non-grid views so it is one constant, not four. Clause-② no (no contract change; the constant is internal and the footnote is renderer copy). Pin: with a seeded set above the ceiling, the DOM row count equals the ceiling and the footnote names both N and M; below it, no footnote and the full set draws.State transition, same stroke:
needs-user-decision→pm:queue;priority:p2retained. Ledger: objectstack director seat post #12708, summon #8.
Generated by Claude Code
5 remaining items
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actionsClaim —
domain:uiexecution seat, chain head of the two-card dispatch (#7210 + #7225), one branch, one PR.- session
session_01EMrWaQw3XS5DxTHxp4yRyC - branch
claude/issue-7210-nongrid-ceiling-settled-schema - worktree
objectui-issue-7210, based onorigin/mainatd53e472
⛔ Assignees untouched — the PM claim (comment 5511301369) owns that field; this comment is the session-and-branch half of the claim, per the repo's claim-first rule.
This card's half: ruling a′ (comment 5508048888) — a platform row ceiling on the four non-grid views' fetches (gantt, calendar, map, tree), expressed as a named constant in the renderer, plus the loud footnote naming both N and M. Not re-litigated; not folded into #7225's gating half, which is a separate commit on the same branch.
Fence cleared, reported early: the ceiling constant lands in
packages/react(the PM's preferred home, besideuseSettledSchema— all four plugins already depend on it).packages/coreandpackages/typesare NOT touched, so there is no collision with the sibling cards holding those barrels this round.Structured report follows on this card when the draft PR is open.
Generated by Claude Code
- session
os-dev-report
{ "issue": 7210, "also_closes": 7225, "status": "done", "branch": "claude/issue-7210-nongrid-ceiling-settled-schema", "pr": "https://github.com/objectstack-ai/objectui/pull/7391", "premise_still_valid": true, "summary": "Both rulings implemented on one branch, one draft PR, one commit per card so each is independently checkable. #7210 ruling a-prime: ObjectGantt / ObjectCalendar / ObjectMap / ObjectTree each issued a `find` with NO `$top` at all; all four now send `NON_GRID_ROW_CEILING_TOP`, draw at most `NON_GRID_ROW_CEILING`, and render a `role=note` footnote naming both N and M when the cut bites. The constant lives in `packages/react` (the PM's preferred home) — `packages/core` and `packages/types` are UNTOUCHED, so the barrel fence held and there is nothing to report there. CEILING = 2000, set after measuring all four (see `ceiling_measurement`). #7225 ruling B: ObjectKanban, plugin-view/ObjectView and ObjectCalendar migrated onto `useSettledSchema` (each a one-line call — the hook was extracted FROM these shapes); the kanban's rejected read moves console.warn -> console.error and its test spy moved with it, now ASSERTING on the channel rather than silencing it. Ask 2: the gantt's duplicate ungated query is gated — one expanded query per load instead of two, the first previously unexpanded. Gating is not capping: the two halves touch the same lines of `ObjectGantt.reload` and are deliberately separate commits with separate pins.", "ceiling_measurement": "2000, measured not asserted. jsdom lane, @testing-library/react, REAL child views (not stubs), inline `value` provider, mount to settled paint; DOM elements materialised and mount ms per view: n=250 gantt 726/314ms, calendar 415/231ms, map 478/152ms, tree 1306/326ms · n=1000 gantt 726/226, calendar 415/235, map 1512/299, tree 5206/1025 · n=2000 gantt 726/484, calendar 415/237, map 2770/1250, TREE 10406/2720 · n=4000 gantt 726/420, calendar 415/211, map 3020/648, tree 20806/3105 · n=8000 tree 41606/7597. THREE OF FOUR HOLD DOM FLAT as rows grow and for structural reasons that will not change (gantt virtualises its task list and timeline window; the calendar month grid draws at most four events per day cell; the map auto-clusters above 100 markers — `shouldCluster = enableClustering ?? (schema.enableClustering || filteredMarkers.length > 100)`). ObjectTree is the outlier and therefore the BINDING view: it flattens every expanded node into the document at a strictly linear 5.2 DOM elements per record with no virtualisation. Budget applied: keep the worst of the four inside ~10,000 DOM elements (an order of magnitude above Lighthouse's ~1,400-element excessive-DOM warning, and the last point at which the tree's mount stays under ~3s in an environment that does no layout and no paint). 2,000 is where that lands, measured directly rather than interpolated: 10,406 elements. It is also ~10x the real application result set the card was filed from (186 rows), which is what keeps the footnote meaningful when it appears. SCOPE DECLARED: these are SHARED-BOX jsdom seconds, not browser wall clock, and jsdom does no layout/paint — the DOM-element counts are the environment-independent half and the RATIO between the four views is the load-bearing reading, not the milliseconds. The harness was temporary and deleted; `git status --short` proved empty before the first commit.", "tests": "All at `c5a46298c` (git rev-parse --short HEAD of the final commit); every heavy run through /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=objectui-7210; vitest invoked canonically from the repo root. (1) SUITE, at that sha: `pnpm exec vitest run` over the eight touched packages -> 'Test Files 284 passed (284) / Tests 2865 passed (2865)'. (2) TYPE-CHECK, at that sha: the same eight packages, `tsc --noEmit && tsc -p tsconfig.test.json`, exit 0, 8 'type-check: Done' lines echoed and 0 'error TS' lines, over a freshly built dependency closure (`--workspace-concurrency=2` BEFORE the filters, `'PKGNAME^...'` upstream form, exit 0). NOT-MEASURED trap closed: the new test files are provably inside those programs — `tsc -p tsconfig.test.json --listFiles` gives 2 hits in plugin-gantt (rowCeiling + fetchGate), 1 in react, 1 in plugin-tree. (3) PIN FOR #7210's RULING, red-then-green: packages/plugin-tree/src/ObjectTree.rowCeiling-7210.test.tsx checks the ruling literally against REAL rendered `tbody tr` rows (the tree is the only one of the four whose DOM tracks the result set) — above the ceiling the rendered row count EQUALS the ceiling and the footnote names both numbers; below it the full set draws and there is no footnote. Siblings: ObjectGantt.rowCeiling-7210 (3 cases incl. an inline `value` set that is never capped by us), ObjectCalendar.rowCeiling-7210, ObjectMap.rowCeiling-7210, and packages/react/src/utils/nonGridRowCeiling.test.tsx (7 cases on the mechanism: ceiling+1 is the probe row; exactly-at-ceiling is NOT truncated; one past IS; a BARE ARRAY response with no `total` is still detected). (4) PIN FOR #7225's RULING: packages/plugin-gantt/src/ObjectGantt.fetchGate-7225.test.tsx — one query per load carrying its expansion, never an unexpanded one, plus BOTH settle-with-nothing paths (no `getObjectSchema`; a read that rejects) so #7232's trap cannot come back as a chart that never loads. (5) ABLATIONS — four legs, each proving the mutation ON DISK before any run (anchored token counts AND `git hash-object` against the HEAD blob, empty hash treated as FAILURE), each restored with `git checkout HEAD -- ABSOLUTE_PATH` under a `trap restore EXIT INT TERM` whose paths are absolute (REPO_ROOT from `git rev-parse --show-toplevel`), and each restore proved by STATE (`git diff HEAD` and `git status --short` empty, blob hash back to the HEAD blob) rather than by an exit code. Predictions recorded BEFORE running. (a) drop the gantt's `$top` ceiling: predicted 1 failed / 2 passed, OBSERVED 1 failed / 2 passed. (b) drop the tree's footnote render: predicted 1 failed / 1 passed, OBSERVED 1 failed / 1 passed. (c) THE DISCRIMINATION CONTROL — delete the three migrated components' gate lines and run their three gate suites: predicted red on the order of the previous seat's 14/22, OBSERVED 14 failed / 8 passed of 22, reproducing that seat's number EXACTLY, now on the MIGRATED tree — which is what makes 'the suites are unchanged and green' non-vacuous. (d) drop the gantt's gate line: predicted 2 failed / 3 passed, OBSERVED 4 FAILED / 1 PASSED — a PREDICTION MISS, reported not adjusted, and written into the pin's own docblock. Direction as predicted, magnitude higher: I assumed the second query came from `objectSchema`'s identity changing, so a definition settling as `null` would still produce one query. It does not — the second query comes from `objectSchemaReady` sitting in the effect's DEPENDENCY LIST, which flips false->true on every path including both settle-with-nothing paths; the gate line and the dependency are two halves of one mechanism and the ablation removed only one. (6) GATES, each with output redirected BEFORE the exit code was read (never `cmd | tail`), all exit 0: check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:control-bytes, check:phantom-deps, check:self-import, check:vi-mock-specifiers, check:vi-mock-inherit, check:side-effects-array, check:element-data-source-declaration, check:readme-exports, changeset:check, type-check:coverage, lint:coverage, check:sdui-registration-pins, check:dist-completeness, check:esm-specifiers, check:doc-types, check:doc-snippets, check:doc-fences, check:eager-closure. The i18n parity suite (packages/i18n) ran green inside the 284/2865 above. (7) LINT IS THE FULL FARM, NOT A NARROWING: `pnpm exec turbo run lint --concurrency=2` -> 'Tasks: 47 successful, 47 total', exit 0; plus `lint:root` exit 0. Both warnings-only and all pre-existing (780 and 33 warnings, 0 errors). (8) check:readme-exports and check:sdui-registration-pins were run against a FULL `turbo run build` (43/43 tasks) — on the first attempt readme-exports refused with 'the population COLLAPSED -- this run proves nothing' (17 packages read against a floor of 25), which I recorded as a PREREQUISITE FAILURE and not a colour, then satisfied. With everything built it reads 37 of 40 packages and 410 self-imports judged, 0 fabricated. (9) Control-byte self-scan with `grep -naP` over all 33 changed files: 0 hits.", "eager_closure_budget": "⚠️ THE ONE THING WORTH WATCHING ON MERGE. The first build of this branch put the `framework` chunk 0.2 KB OVER its 511.7 KB per-chunk gzip ceiling (`check:eager-closure` exit 1). I did NOT raise the ceiling. A CONTROL settled whose bytes those were: a full console build of plain `origin/main` (1688986a3) in a second worktree measures framework at 510.8 KB with 0.9 KB headroom, against 511.9 KB on this branch — so the overrun is MINE, not main's. A per-chunk diff of the two eager-closure reports attributes exactly 1,075 gzipped bytes to this branch (~227 in @object-ui/react, the rest in eagerly-loaded locale packs). Paid for rather than budgeted around, in commit c5a46298c: both footnote sentences tighten to the form the ruling itself uses ('showing first N of M records; narrow the filter') in all ten packs — shorter AND closer to the ruled wording; the same two sentences ship TWICE in framework (the `en` pack and the provider-less fallback map), so each edit counts double and the fallback map now says so with the measured headroom beside it; and `data-ceiling-drawn` / `data-ceiling-total` were test-only attributes on a note that already renders both numbers as text, so they are gone and the three pins that read them assert the text instead. Framework is now 511.6 KB against the 511.7 KB ceiling — gate exit 0, ceiling untouched. ⚠️ 0.2 KB is not comfort: main moved this chunk in the hour before the measurement and the gate weighs the MERGE ref, so another lane landing framework bytes before this merges can turn Bundle Analysis red with no further change here. If that happens the fix is a re-measure, not a widened ceiling — and I would rather the PM hear that now than read it as a surprise CI failure.", "deviations": "Six, all deliberate and all in the PR body. (1) THREE EXISTING PINS MOVED, keeping their point: ObjectGantt.hostDataProp-7210, ObjectGantt.elementDataSource and ObjectMap.elementDataSource each asserted `$top` was ABSENT, and each was written while half 2 was an open decision (the first says so in as many words). They now assert the cap is the PLATFORM's and, crucially, that an authored `limit: 3` / `pagination.pageSize: 2` still cannot reach the query — which is what they always existed to pin. (2) THE #7231 STALE-RELOAD PIN KEEPS EVERY ASSERTION AND CHANGES ONLY ITS OVERLAP GENERATOR: it produced two in-flight reloads out of the gantt's DUPLICATE mount query (`expect(find.mock.calls.length).toBe(2)`), which is exactly what #7225 ask 2 removes, so it would have waited forever. The overlap now comes from a silent toolbar refresh superseded by a filter-change reload — real, the reason the guard exists, untouched by gating, and what case 3 always used. The `finally` guard itself is not modified, and the helper's `toBe(1)` on mount is now that file's live control on the gate. (3) #7232 REPORTED, NOT ABSORBED — see `open_questions`-adjacent note below; no closing keyword for it on the PR. (4) A THIRD COMMIT for docs (AGENTS.md #2), kept separate so the two card halves stay independently checkable as code: packages/react/README.md gains a NON_GRID_ROW_CEILING section and stops calling the three components hand copies, and content/docs/guide/data-source.md's per-block table said `— no row cap` for object-calendar / object-gantt / object-map, which is now false in the dangerous direction; the cells read `— platform ceiling` with a paragraph keeping what the cell still means (an authored limit STILL cannot reach these queries). (5) A FOURTH COMMIT for the eager-closure trim, above. (6) The ablation prediction miss in leg (d), above. ⛔ NOT DONE, deliberately: #7222 (forwarding `data` remains the fenced-off wrong fix — it caps the chart at one page, this ruling's forbidden direction by the back door), #7230, #7204/#7224/#7228, and GanttView.tsx is untouched.", "issue_7232": "REPORTED, per the dispatch's explicit instruction, and NOT absorbed. Gating waits on readiness, so the gantt's three non-settling exits (`if (!effectiveDataSource) return;`, `if (!resource) return;`, and its `catch`) had to settle or the gated query would be held open forever — a chart that never loads on a code path that reads as correct. I did not write a bespoke settle and #7232 is not repaired as a card: the gantt now uses the ALREADY-PUBLISHED `useSettledSchema`, which settles on every exit by construction. That is what #7232's own body directs — 'whoever picks up the gantt's gating ... should read this first and add the settle-on-every-exit as part of that change'. No `Fixes`/`fix` keyword for #7232 appears anywhere in the PR body or in any commit message (audited mechanically, because GitHub's parser ignores negation: an earlier draft of the PR body put a closing keyword directly before that card number inside a NEGATED sentence, which would have silently CLOSED the card on merge; caught by a mechanical scan of the body and rewritten so no keyword sits next to it). Both settle-with-nothing paths are pinned behaviourally in ObjectGantt.fetchGate-7225.test.tsx.", "zone2_assumptions": "(1) CONSTANT'S HOME — CONFIRMED, fence not hit. `packages/react/src/utils/nonGridRowCeiling.tsx`, exported from the package entry; all four plugins already depend on @object-ui/react and it already hosts useSettledSchema. packages/core and packages/types are not touched (the only `core`-facing change is REMOVING three now-unused `extractRecords` imports from plugin files), so there is no barrel collision with #7349 or #7182. (2) THE GANTT'S 2 find + 1 getObjectSchema PROFILE — RE-DERIVED, not inherited: the pre-gate expand sets are `[null, ['owner']]`, pinned in the new fetchGate file (the old regime's signature is quoted there); the fix takes it to one expanded call. (3) THE 14/22 DISCRIMINATION — REPRODUCED EXACTLY (14 failed / 8 passed of 22) on the migrated tree, which is the control that makes the green non-vacuous. The seat's other figures (3 files, +17/-111, 497/497) describe its scratch migration, not this PR's diff, so they are not restated as if measured here. (4) THE FOUR NON-GRID VIEWS — CONFIRMED as the actual set, by reading every `find` call site in packages/plugin-*/src: gantt, calendar, map and tree are the only ones issuing an uncapped object query. object-kanban (`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`) and object-timeline (`$top: schema.limit ?? DEFAULT_TIMELINE_LIMIT`) already cap; plugin-view/ObjectView's own non-grid query carries `$top: 100`. ⚠️ ONE MORE surfaced that the ruling does not name — ObjectGallery, also uncapped — filed rather than folded in; see out_of_scope_findings.", "environment_note": "Per the card-specific constraint: every ceiling and footnote assertion was pinned in JSDOM (this repo's `dom` vitest project) and NONE of them depends on container size or on a media query. The note is a sibling element in each component's own tree, so it is present or absent regardless of layout — deliberately, because happy-dom never fires container-size effects and jsdom applies media-query rules irrespective of innerWidth, which would make a size-dependent pin unmeasurable rather than merely flaky. The map pins additionally stub `react-map-gl/maplibre` the way the sibling ObjectMap tests do (no WebGL in this lane). No layout-dependent fact is claimed anywhere.", "files_changed": "33 files, +1628 / -212. Source: packages/react/src/utils/nonGridRowCeiling.tsx (new), packages/react/src/index.ts, packages/plugin-gantt/src/ObjectGantt.tsx, packages/plugin-calendar/src/ObjectCalendar.tsx, packages/plugin-map/src/ObjectMap.tsx, packages/plugin-tree/src/ObjectTree.tsx, packages/plugin-kanban/src/ObjectKanban.tsx, packages/plugin-view/src/ObjectView.tsx. i18n: all ten packs in packages/i18n/src/locales (two new `common.*` keys). Tests: 5 new (react/nonGridRowCeiling.test.tsx, ObjectGantt.rowCeiling-7210, ObjectGantt.fetchGate-7225, ObjectCalendar.rowCeiling-7210, ObjectMap.rowCeiling-7210, ObjectTree.rowCeiling-7210) and 4 updated (the three moved `$top` pins, the #7231 pin, the kanban console spy). Docs: packages/react/README.md, content/docs/guide/data-source.md. Changesets: .changeset/7210-non-grid-row-ceiling.md (@object-ui/react minor for the new exports, i18n + the four plugins patch) and .changeset/7225-settled-schema-convergence.md (plugin-kanban / plugin-view / plugin-calendar / plugin-gantt patch, as the ruling specifies).", "mcp_calls": "3 — two add_issue_comment (the claim on each card) and one issue_write (filing #7390). Everything else went through the zero-quota channels: repo-scoped REST reads for both cards' bodies and all comments, REST for the duplicate scan and for creating the draft PR (HTTP 201), and git for the branch. NOTE FOR THE CHANNEL LEDGER: `gh` is absent in this container; repo-scoped REST reads AND writes work (GET repos/... 200, POST pulls 201, POST issues/N/comments 201), but the SEARCH endpoint is refused — `GET /search/issues` returns 403 'sessions are bound to their configured repositories. Use repository-scoped endpoints'. So the mandatory duplicate check for #7390 ran as REST list pages + local grep over 338 open issues (control: the same scan fires 14 'gallery' hits and 10 'unbounded/row cap' hits, so the zero for 'gallery + unbounded fetch' is a reading, not a broken query).", "open_questions": [], "out_of_scope_findings": [ "filed as #7390: plugin-list/ObjectGallery issues the same unbounded `find` this PR just capped on the four ruled views — no `$top`, no ceiling, and no footnote either. Deliberately NOT capped here: the ruling's scope is the non-grid family, whose argument is that those four cannot be paged without lying (a gantt's range, a map's camera fit, a tree's parent pointers). A gallery is page-shaped and renders under ListView's paging chrome, so the honest fix is plausibly PAGING rather than this ceiling — and extending a ruled scope by inference is not the lane's call. The card puts both options with their costs.", "NOT filed, recorded here instead because it is a fact about the shared budget rather than a defect in any one file: the `framework` per-chunk eager-closure ceiling has ~0.2 KB of headroom after this branch and had 0.9 KB before it, while main is actively consuming it. Several lanes shipping user-facing copy will keep colliding with it. That is a governance question (re-baseline vs. shrink the payload — objectui#5324 already names the shrink candidates), not something a single lane should resolve by widening a number." ] }
Generated by Claude Code
⛔ Fork for the maintainer — ruling a′'s footnote does not fit under the
frameworkper-chunk gzip ceiling, and no version of it doesdomain:uiexecution seat, sessionsession_01EMrWaQw3XS5DxTHxp4yRyC, on PR #7391 (head91876eb55).Everything else on this card is delivered and green — both halves implemented, all pins red-then-green, the 14/22 discrimination control reproduced, 284 files / 2,865 tests passing, the full lint farm at 47/47, and 20 of 21 gates at exit 0. The structured report is the comment above. This is the one thing I cannot resolve from inside the lane, and it is a decision rather than a task.
The measurement
One base (
a6e5f7050), control built by detaching this worktree toorigin/mainand rebuilding the console:tree frameworkgzagainst the 524,000-byte ceiling origin/mainalone523,823 177 bytes of headroom — for the entire repo PR #7391 524,476 over by 476 PR #7391 with the ten-pack footnote copy deleted 524,053 still over by 53 ⭐ The third row is the one that settles it. The two i18n keys across ten locale packs cost 423 bytes. The ceiling mechanism's code — the constant, the helper, the note component, zero strings — costs 230. So even a completely untranslated footnote does not fit in 177 bytes. Shaving is not a route; there is no implementation of this ruling that lands under the current ceiling.
That third row is a real ablation, not arithmetic: the keys were stripped from all ten packs, the console rebuilt, the report re-read; mutation proved on disk (20 key lines to 0), restored under a trap with absolute paths, restore proved by state.
⚠️ Reported because it nearly fooled me: the first attempt at that ablation was void. Stripping the packs broke the build — this branch reads its fallback default from theenpack — so the build exited 2, the script read the stale report from the previous build, and it reported the two keys as costing 0 bytes. A stale artifact beside a failed build reads exactly like a clean measurement. The script now refuses to read the report unless the build exited 0.What was already paid, and kept
Before concluding this, ~600 bytes were recovered and those changes stay because they are right on their own merits:
- the copy tightened to the ruling's own form — "showing first N of M records; narrow the filter" — in all ten packs, so it is shorter and closer to the ruled wording;
- the provider-less fallback default is read from the
enpack instead of retyped, so identical English no longer ships twice in the same eagerly-loaded chunk; - two test-only
data-attributes dropped from a note that renders both numbers as text anyway.
⛔ The ceiling was not raised. The gate offers that route for intended growth, but raising a gate threshold is the maintainer's floor, not a dev's.
The options — all yours, none of them available to this lane
- Re-baseline
framework, withPER_CHUNK_BASELINEmoved alongside it and the bytes justified in the PR. 653 bytes buys a ruled safety footnote, in ten languages, across four views, closing a silent-truncation class. - Shrink the eager payload first, then land this. The locale packs are eagerly reachable because
@object-ui/i18n's entry statically re-exports all ten (export { default as zh } from './locales/zh.js', ×10). Making that lazy would free far more than any footnote costs. It is an architectural change to a published entry — a separate ruled card (objectui#5324 and objectui#6795 already name payload-shrink candidates), and out of scope here under this dispatch's Clause-② "no". - Take objectui#7148's precedent literally.
⚠️ Worth your attention on the merits, not just the bytes: ruling a′ names that chart footnote as the precedent "for placement and tone" — and that footnote is plain untranslated English in JSX, with no i18n keys at all. Matching it exactly removes the 423 bytes of pack copy. It still would not fit today (the 230-byte code row), but "follow finding(plugin-charts): a sankey with mixed positive and negative rows silently DROPS the negative ones and draws a partial flow as if it were complete #7148" and "translate the copy into ten packs" are in genuine tension, and which one ruling a′ meant is not mine to decide.
Why this will not wait quietly
frameworkmoved 510.8 KB → 511.5 KB onmainduring the ~90 minutes these measurements took — the headroom is being consumed by other lanes in real time, not by this branch. And objectui#7194's ruled refusal copy lands in these same ten packs, so it meets the same 177 bytes. Whatever is decided here decides that card too, and the landing order between them is the PM's.⛔ I have stopped shaving, as instructed, rather than buying bytes by weakening the footnote below what ruling a′ specifies — it must name both N and M, and the unknown-total variant exists so that an adapter reporting no
totalstill gets a definite sentence instead of "of undefined".
Generated by Claude Code
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actionspm:dispatched→pm:blocked— ruling a′ is IMPLEMENTED; the card is parked on a governance decision, not on outstanding workdomain:uiexecution seat, PM sessionsession_01EMrWaQw3XS5DxTHxp4yRyC. Paired with the label change and theBlocked-by:/Unlock-action:lines added to the body in the same round.The delivery is complete
PR #7391 implements ruling a′ in full:
ObjectGantt/ObjectCalendar/ObjectMap/ObjectTreeeach previously issued afindwith no$topat all; all four now sendNON_GRID_ROW_CEILING_TOP, draw at mostNON_GRID_ROW_CEILING, and render arole=notefootnote naming both N and M. The constant lives inpackages/react;packages/coreandpackages/typesare untouched, so the barrel fence held.The ceiling is 2,000, measured rather than interpolated.
ObjectTreeis the binding view — it flattens every expanded node at a linear ~5.2 DOM elements per record with no virtualisation, while the other three hold DOM flat for structural reasons (the gantt virtualises, the calendar month grid caps at four events per day cell, the map auto-clusters above 100 markers). At n=2000 the tree renders 10,406 elements; at n=4000, 20,806. The budget was "keep the worst of the four inside ~10,000 elements", an order of magnitude above Lighthouse's ~1,400 excessive-DOM warning.⚠️ Scope: shared-box jsdom, so the DOM counts are the environment-independent half and the ratio between views is the load-bearing reading, not the millisecond figures.Why it is blocked
Blocked-by: #7399
The
frameworkper-chunk eager-closure ceiling leaves 177 bytes for the whole repository (mainalone: 523,823 B against 524,000 B). This card's mechanism code alone — zero strings — costs 230 B, so it does not fit even with the footnote deleted entirely; with the ten-pack copy removed the PR still measures 53 B over. ⇒ There is no version of ruling a′ that lands under today's ceiling, which is why this is one decision rather than a shaving exercise.⛔ The lane did not raise the ceiling and did not weaken the footnote. Both were available; both were refused. #7399 puts the choice to the maintainer with three options and their costs, and covers #7194 in the same decision because its ruled refusal copy lands in the same packs.
Accepted as a delivery
A reported fork is a legitimate outcome, not a failed round, and I am recording it as one. The dev measured the two halves separately — mechanism bytes vs. copy bytes, each against an
origin/maincontrol — and that split is what made the fork decidable: without it, "the footnote is too big" would have looked like a wording problem for someone to keep shaving at.Two measurement failures it caught and reported rather than quietly re-running are worth keeping:
- The attribution ablation's first attempt was void — stripping the i18n keys broke the build, and the script read a stale
eager-closure.jsonfrom the previous build, reporting the two keys as costing 0 bytes. A stale artifact beside a failed build reads exactly like a clean measurement. Hardened to refuse the report unless the build exits 0. - That same ablation restored the source but left a mutated
packages/i18n/diston disk, and the next type-check failed with twoTS2339s. Diagnosed as a stale-dist artifact and proved so by rebuilding from the restored source. ⇒ The restore leg of an ablation is only as good as what it restores —distwas outside it.
Related
#7399 (the decision) · #7225 (the other half of PR #7391) · #7194 (same ceiling, same decision) · #7390 (filed out-of-scope:
plugin-list/ObjectGalleryissues the same unboundedfind, deliberately not capped here because a gallery is page-shaped and paging may be the honest fix) · #6631 (where this ceiling question was deferred, with the trigger that has now fired)
Generated by Claude Code
- The attribution ablation's first attempt was void — stripping the i18n keys broke the build, and the script read a stale
Contract review PASS on PR #7391 at head
9a556bdf7— director seat (objectstack #12708), summon #12, sessionsession_01WXyGTWPbbreqXow7Z2pZCk, taking over the commissioned review that could not start (3× 529). Judgment and checklist: #7391 (comment).needs:contract-reviewcleared on all three carriers (this card, #7225, the PR); PR marked ready and auto-merge armed. This card closes viaFixeswhen the queue lands it.
Generated by Claude Code
- added a commit that references this issue
on Sep 3, 2026 - added a commit that references this issue
on Sep 9, 2026
Found in a real application (
objectstack-ai/duly) against published@objectstack/*17.2.0, measured in a browser on a seeded store of 186 rows. Filed unassigned. Two related observations from one page load; they share a cause and I would rather they be judged together than split.Distinct from objectui#7189, which is
plugin-grid'suseGroupedDatagrouping over a fetched page. This is the opposite problem on a different surface: the gantt is not page-scoped, and the paging chrome above it says otherwise.Measured
One load of a
type: 'gantt'list view (groupByField: 'owner', a two-ruleis_not_nullfilter,sort: visible_from asc, nopaginationblock authored) issues both of these:Note they do not even agree on a filter dialect — the paged one sends the authored rule array, the unbounded one sends a compiled
["and", …]tree.The chart draws the second one. Counted off the DOM rather than inferred: 186
gantt-task-row-rows plus 12gantt-task-row-__group__rows, and the virtualised task list'sscrollHeightis 7920px = 198 rows × 40px. The bars span the full2026-01-31 → 2026-12-31range of the whole result set, six months past the 100-row page's ownmax(due_date)of2026-06-30.The footer reports the first one, in the paging chrome under the chart:
Why each half is worth something
1. The footer is wrong about the thing it sits under. It is the only paging disclosure on the screen, and a reader takes it as describing the chart. Here it warned that a complete chart might be partial. The same mechanism inverts the moment anything caps the second request: the footer would then say
100 recordswhile the chart drew some other number, and nothing on screen would reconcile them. Either way it is not a statement about the chart.That reading is not hypothetical — it is what happened. A card was filed in the application repo (
objectstack-ai/duly#96) concluding from this footer that the gantt's timeline range and owner grouping were computed over a 100-row slice. The reasoning was sound given what the screen said; it just was not what the code does. It cost a browser session to disprove.2. The fetch that matters has no ceiling.
@objectstack/restapplies no cap whenlimitis absent, so the unbounded request returns the entire filtered result set. At 186 rows that is invisible. On a task object with 100k scheduled rows it is the whole table into the browser, with no page-size knob reachable from the view metadata — authoringpagination.pageSizecannot bound it, because this request never carried atopto begin with.The two halves pull in opposite directions, which is the actual design question: whichever way it is settled, the footer and the chart should be describing the same fetch.
Suggested direction
top.Related, same component, both closed: objectui#6419 and objectui#6460 are previous findings on this
ObjectViewnon-grid fetch path.Application impact
None today, and no workaround is in place for it —
dulyis scoping that lens to open work for a product reason (a schedule of finished work is not what the screen is for), which incidentally puts the result set at 33 rows and makes both requests agree. That is our data size, not a fix; nothing in an application can bound the unbounded request or correct the footer.PM state (added by the
domain:uiexecution seat, 2026-09-02)Implementation is complete and delivered on PR #7391 — ruling a′ is implemented across all four non-grid views. The card is parked on a governance decision, not on outstanding work.
Blocked-by: #7399
Unlock-action: re-check PR #7391
#7399 asks the maintainer to choose how ruled user-facing copy lands against the
frameworkper-chunk eager-closure ceiling, which currently leaves 177 bytes for the whole repository — less than this card's mechanism costs with every string deleted. ⛔ The lane did not raise the ceiling and did not weaken the ruled footnote; both were refused deliberately.