fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw - #7223

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome
Sep 1, 2026
Merged

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw#7223
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #7210 — half 1 only. Halves 2 and 3 stay open on that card, deliberately; see "Half 3" below.

What changed

ListView renders one record-count bar for every viewType: the row count, the "Showing first N records. More data may be available." warning tied to its own $top: pageSize query, and a rows-per-page selector that re-issues that query. It is now suppressed on gantt alone, behind a named constant (surfaceDrawsFetchedRows) whose doc comment carries the measurement and the reason.

One line of behaviour, one guard, two controls, plus a pin in plugin-gantt for the fact the guard rests on.

Why suppression rather than "make it describe the chart"

The ruling allowed either. ListView cannot report the chart's row count without a new upward channel from ObjectGantt — more published surface, and a channel whose meaning changes if a ceiling is ever added. Suppression is invariant to that decision, which is the property the split was made for. None of the bar's three parts describes the chart: not the count, not the warning, and not the page-size selector, which re-issues a query the chart ignores.

Measured, not inherited

Everything below was measured in this branch. Nothing was copied from the report.

Request count — the order said "count them yourself", and the answer is not two. Harness: one list-view of viewType: 'gantt', 18 rows in the store, pagination.pageSize: 6, instrumented adapter recording every find.

3 find calls
1. $top: 6, $select: [...], authored filter array -- ListView's own paged query
2. no $top, no $select, no $expand -- ObjectGantt, before its schema read settles
3. no $top, no $select, $expand: [owner] -- ObjectGantt, after it settles

Calls 2 and 3 are one defect the reporter's browser capture read as one request: the second/third split is the "two queries, the first unexpanded" regime already recorded for ObjectGantt on #6482, which is no longer open; re-confirmed here. Only call 1 has a $top at all.

The chart consumes the unbounded fetch. The DOM reading reproduced in the harness's own terms: the paged query answered 6 rows, and the chart rendered 18 gantt-task-row-* rows while the bar under it read 6 records - Showing first 6 records. More data may be available. That is the defect in miniature, one screen, both halves visible.

The chrome is shared, so the change is scoped. Same harness, same store, only viewType differing:

viewTypefind callsrecord-count barhonest?
gantt36 records - Showing first 6 records... while the chart drew 18no
grid118 records (server total, no warning)yes
kanban16 records - Showing first 6 records..., and the board drew 6yes

kanban is the load-bearing control: a non-grid, page-scoped surface whose warning is correct and had to survive.

The mechanism.plugin-gantt/src/index.tsx's ObjectGanttRenderer destructures { schema } and hands its child only schema and dataSource. Every sibling wrapper — grid, kanban, calendar, map, tree — spreads {...props}. So the dataListView passes never reaches the chart, ObjectGantt.reload's rest.data short-circuit is unreachable from the registry path, and the component queries for itself with no $top. Pinned in packages/plugin-gantt/src/ObjectGantt.hostDataProp-7210.test.tsx.

Half 3 — condition FAILED, so both requests stay

The order was explicit: measure whether the paged request has any consumer other than the footer; if it does, stop and report rather than pick a dialect. It does. Reads of ListView's data on the gantt surface:

lineconsumer
ListView.tsx:2801client-side CSV / JSON export slices data
ListView.tsx:2938UserFilters data={data} — option counts (showCount)
ListView.tsx:3573refresh indicator
ListView.tsx:3583load-error panel vs. rendering the view
ListView.tsx:3630loading skeleton vs. rendering the view
ListView.tsx:3841the record-count bar (this PR)

The last two are behavioural, not cosmetic, and one of them was measured directly: with the paged response delayed 600ms and the unbounded one immediate, at t=250ms the skeleton was up and the chart had 0 rows on screen although its own data had already arrived. The paged query decides whether the chart mounts at all. Removing it is not a no-op, so no dialect was picked and nothing was deduped.

Falsified ZONE 2 assumptions

  • "exactly two requests" — three (see above). One paged, two unbounded.
  • "the fetch path is ObjectView's non-grid list-view path" — the paged request in the report is ListView's (plugin-list), identified by its $select projection and $top, which ObjectView's non-grid fetch does not emit. ObjectView (plugin-view) delegates to ListView through renderListView and skips its own fetch when it does. The unbounded one is ObjectGantt.reload.
  • "the footer is rendered by shared paging chrome" — CONFIRMED, and it decided the width: one block in ListView, rendered for every view type, correct on all of them but gantt.
  • "@objectstack/rest applies no cap when limit is absent" — NOT MEASURED here. Out of reach from this repo; nothing in this change depends on it. What is measured is one layer up: the request carries no $top, so no page size authored on the view can bound it.
  • "the chart consumes the unbounded one" — CONFIRMED, reproduced as row counts in the harness rather than inferred.

Not touched, deliberately

Reverse verification

Direction and counts predicted before running: drop surfaceDrawsFetchedRows from the bar's render condition and exactly the two gantt cases go red while both controls stay green — 2 failed / 2 passed. Observed exactly that, then 4 passed after restore.

Mutation proved on disk: removed-token count 2 to 1, mutated-line count 0 to 1, blob hash 4a1ce3c1 to 7f55c73b. Restore proved by state: git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to 4a1ce3c1, the HEAD blob.

Verification (all at f3d7b3cd3)

  • pnpm exec vitest run packages/plugin-list/ packages/plugin-gantt/113 files passed, 1183 tests passed.
  • 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. Both new test files proved present in the tsconfig.test.json program via tsc --listFiles (1 hit each), so "typecheck is clean" actually covers them.
  • Gates: 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 (46/46 packages, 0 errors), and all four changeset guards — every one exit 0. check:sdui-registration-pins returned exit 2, PRECONDITION NOT MET (no apps/console build to weigh) — recorded as NOT MEASURED, not as a pass; this diff adds no registration and moves no entry array. Left to CI.
  • ESLint, narrowed and declared: 3 changed files, 0 errors (--format json, file count read from that output, not guessed). The narrowing is a measurement rather than a skip because eslint.config.js enables no type-aware linting (no project / projectService in its languageOptions), so no untouched file's verdict can move because of this diff, and the diff adds no config and no inline directive.

Generated by Claude Code


Generated by Claude Code

The bar reports ListView's own paged query — the row count, the "Showing
first N records" warning tied to `$top: pageSize`, and the rows-per-page
selector that re-issues it. Every other view type draws the `data` ListView
hands down, so there the bar is accurate. The registered `object-gantt`
renderer forwards no prop but `schema`, so the chart queries for itself with
no `$top`: measured at 18 rows / pageSize 6, the chart drew 18 while the bar
read "6 records - Showing first 6 records. More data may be available."
Scoped to gantt; grid and kanban controls pin that the shared chrome is
untouched elsewhere. The gantt's own query is left exactly as it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C-Qg7c4O.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.22KB56.04KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.24KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. Reviewer of record: domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM

Verified over git: clean against origin/main3561bd2ca, and clean against #7214's queue build (which main is about to become), and clean against #7220. Changeset present. The functional change is one named constant and one added conjunct in a render condition.

Half 3's condition failed, and stopping was the right answer

The dispatch made the dedupe conditional: measure whether the paged request has any consumer besides the footer, and if it has others, stop rather than pick a filter dialect. It has five, two of them behavioural — data gates the loading skeleton and the load-error panel (so it decides whether the chart mounts at all), feeds UserFilters' 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. That is the observation that makes suppression safe and removal wrong.

So nothing was deduped. That is a full success against the order as written, not a partial delivery.

My premise was wrong again: three requests, not two

The order said "That exactly two requests are issued. Count them yourself." The answer is three — one paged, and two unbounded, ObjectGantt.reload firing before and after its own object-schema read settles, the second adding $expand. Neither unbounded call carries $top.

Third PM premise falsified by a lane this shift, and again only because the order required checking my reading rather than the card's.

The root cause is the valuable find

ObjectGanttRenderer destructures { schema } and forwards schema and dataSource only — the only view wrapper that does not spread host props; grid, kanban, calendar, map and tree all do. So ListView's data never reaches the chart, ObjectGantt queries for itself, and the double fetch is invisible on a read-through of ListView because the live comment there claims the data prop "short-circuits the renderer's own fetch". That comment is false, and its falseness is exactly what hid this. Filed as #7222.

Suppression over re-description was the right of the two permitted shapes

The ruling allowed either. Suppression was chosen because re-description would need a new upward channel from ObjectGantt to ListView whose meaning would change if a ceiling is ever added — i.e. it would bet on half 2's outcome. Suppression is invariant to it. That is precisely the property the split was made for, and the docblock says so in place.

And it correctly refuses the tempting wrong fix: forwarding data to 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.

Scoping is pinned by controls, not by assertion

The chrome is shared — one block rendered for every viewType — and is correct everywhere but gantt, so a naive edit would have broken four honest footers to fix one lying one. The harness (18 rows, pageSize: 6) is run three ways: gantt (bar reads 6, chart draws 18 — the defect), grid (reads 18, the server total, no warning), and kanban (reads 6, and the board really is drawing those 6). The two controls are what demonstrate the scoping rather than claim it, and both are in the test file.

Reverse verification: predicted 2 failed / 2 passed, observed exactly that, restore proven by state plus blob equality with the mutation proven on disk before any run output was read.

check:sdui-registration-pins returned exit 2 with its own "This is exit 2, not a pass" banner and was recorded as NOT MEASURED rather than as a colour. That is the distinction this seat cares most about, and the gate itself was quoted rather than paraphrased.

One cosmetic nit, non-blocking

The docblock writes the renderer as < ObjectGantt schema={bound} dataSource={dataSource} / > with spaces inside the brackets. That defence is for GitHub issue and PR bodies, where an unclosed < eats the rest of the body — it is not needed in a source comment, where the spacing just costs a little readability. Harmless; fix on the next touch if convenient.

PM follow-ups accepted

Armed.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw - #7223

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome
Sep 1, 2026
Merged

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw#7223
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #7210 — half 1 only. Halves 2 and 3 stay open on that card, deliberately; see "Half 3" below.

What changed

ListView renders one record-count bar for every viewType: the row count, the "Showing first N records. More data may be available." warning tied to its own $top: pageSize query, and a rows-per-page selector that re-issues that query. It is now suppressed on gantt alone, behind a named constant (surfaceDrawsFetchedRows) whose doc comment carries the measurement and the reason.

One line of behaviour, one guard, two controls, plus a pin in plugin-gantt for the fact the guard rests on.

Why suppression rather than "make it describe the chart"

The ruling allowed either. ListView cannot report the chart's row count without a new upward channel from ObjectGantt — more published surface, and a channel whose meaning changes if a ceiling is ever added. Suppression is invariant to that decision, which is the property the split was made for. None of the bar's three parts describes the chart: not the count, not the warning, and not the page-size selector, which re-issues a query the chart ignores.

Measured, not inherited

Everything below was measured in this branch. Nothing was copied from the report.

Request count — the order said "count them yourself", and the answer is not two. Harness: one list-view of viewType: 'gantt', 18 rows in the store, pagination.pageSize: 6, instrumented adapter recording every find.

3 find calls
1. $top: 6, $select: [...], authored filter array -- ListView's own paged query
2. no $top, no $select, no $expand -- ObjectGantt, before its schema read settles
3. no $top, no $select, $expand: [owner] -- ObjectGantt, after it settles

Calls 2 and 3 are one defect the reporter's browser capture read as one request: the second/third split is the "two queries, the first unexpanded" regime already recorded for ObjectGantt on #6482, which is no longer open; re-confirmed here. Only call 1 has a $top at all.

The chart consumes the unbounded fetch. The DOM reading reproduced in the harness's own terms: the paged query answered 6 rows, and the chart rendered 18 gantt-task-row-* rows while the bar under it read 6 records - Showing first 6 records. More data may be available. That is the defect in miniature, one screen, both halves visible.

The chrome is shared, so the change is scoped. Same harness, same store, only viewType differing:

viewTypefind callsrecord-count barhonest?
gantt36 records - Showing first 6 records... while the chart drew 18no
grid118 records (server total, no warning)yes
kanban16 records - Showing first 6 records..., and the board drew 6yes

kanban is the load-bearing control: a non-grid, page-scoped surface whose warning is correct and had to survive.

The mechanism.plugin-gantt/src/index.tsx's ObjectGanttRenderer destructures { schema } and hands its child only schema and dataSource. Every sibling wrapper — grid, kanban, calendar, map, tree — spreads {...props}. So the dataListView passes never reaches the chart, ObjectGantt.reload's rest.data short-circuit is unreachable from the registry path, and the component queries for itself with no $top. Pinned in packages/plugin-gantt/src/ObjectGantt.hostDataProp-7210.test.tsx.

Half 3 — condition FAILED, so both requests stay

The order was explicit: measure whether the paged request has any consumer other than the footer; if it does, stop and report rather than pick a dialect. It does. Reads of ListView's data on the gantt surface:

lineconsumer
ListView.tsx:2801client-side CSV / JSON export slices data
ListView.tsx:2938UserFilters data={data} — option counts (showCount)
ListView.tsx:3573refresh indicator
ListView.tsx:3583load-error panel vs. rendering the view
ListView.tsx:3630loading skeleton vs. rendering the view
ListView.tsx:3841the record-count bar (this PR)

The last two are behavioural, not cosmetic, and one of them was measured directly: with the paged response delayed 600ms and the unbounded one immediate, at t=250ms the skeleton was up and the chart had 0 rows on screen although its own data had already arrived. The paged query decides whether the chart mounts at all. Removing it is not a no-op, so no dialect was picked and nothing was deduped.

Falsified ZONE 2 assumptions

  • "exactly two requests" — three (see above). One paged, two unbounded.
  • "the fetch path is ObjectView's non-grid list-view path" — the paged request in the report is ListView's (plugin-list), identified by its $select projection and $top, which ObjectView's non-grid fetch does not emit. ObjectView (plugin-view) delegates to ListView through renderListView and skips its own fetch when it does. The unbounded one is ObjectGantt.reload.
  • "the footer is rendered by shared paging chrome" — CONFIRMED, and it decided the width: one block in ListView, rendered for every view type, correct on all of them but gantt.
  • "@objectstack/rest applies no cap when limit is absent" — NOT MEASURED here. Out of reach from this repo; nothing in this change depends on it. What is measured is one layer up: the request carries no $top, so no page size authored on the view can bound it.
  • "the chart consumes the unbounded one" — CONFIRMED, reproduced as row counts in the harness rather than inferred.

Not touched, deliberately

Reverse verification

Direction and counts predicted before running: drop surfaceDrawsFetchedRows from the bar's render condition and exactly the two gantt cases go red while both controls stay green — 2 failed / 2 passed. Observed exactly that, then 4 passed after restore.

Mutation proved on disk: removed-token count 2 to 1, mutated-line count 0 to 1, blob hash 4a1ce3c1 to 7f55c73b. Restore proved by state: git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to 4a1ce3c1, the HEAD blob.

Verification (all at f3d7b3cd3)

  • pnpm exec vitest run packages/plugin-list/ packages/plugin-gantt/113 files passed, 1183 tests passed.
  • 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. Both new test files proved present in the tsconfig.test.json program via tsc --listFiles (1 hit each), so "typecheck is clean" actually covers them.
  • Gates: 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 (46/46 packages, 0 errors), and all four changeset guards — every one exit 0. check:sdui-registration-pins returned exit 2, PRECONDITION NOT MET (no apps/console build to weigh) — recorded as NOT MEASURED, not as a pass; this diff adds no registration and moves no entry array. Left to CI.
  • ESLint, narrowed and declared: 3 changed files, 0 errors (--format json, file count read from that output, not guessed). The narrowing is a measurement rather than a skip because eslint.config.js enables no type-aware linting (no project / projectService in its languageOptions), so no untouched file's verdict can move because of this diff, and the diff adds no config and no inline directive.

Generated by Claude Code


Generated by Claude Code

The bar reports ListView's own paged query — the row count, the "Showing
first N records" warning tied to `$top: pageSize`, and the rows-per-page
selector that re-issues it. Every other view type draws the `data` ListView
hands down, so there the bar is accurate. The registered `object-gantt`
renderer forwards no prop but `schema`, so the chart queries for itself with
no `$top`: measured at 18 rows / pageSize 6, the chart drew 18 while the bar
read "6 records - Showing first 6 records. More data may be available."
Scoped to gantt; grid and kanban controls pin that the shared chrome is
untouched elsewhere. The gantt's own query is left exactly as it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C-Qg7c4O.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.22KB56.04KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.24KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. Reviewer of record: domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM

Verified over git: clean against origin/main3561bd2ca, and clean against #7214's queue build (which main is about to become), and clean against #7220. Changeset present. The functional change is one named constant and one added conjunct in a render condition.

Half 3's condition failed, and stopping was the right answer

The dispatch made the dedupe conditional: measure whether the paged request has any consumer besides the footer, and if it has others, stop rather than pick a filter dialect. It has five, two of them behavioural — data gates the loading skeleton and the load-error panel (so it decides whether the chart mounts at all), feeds UserFilters' 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. That is the observation that makes suppression safe and removal wrong.

So nothing was deduped. That is a full success against the order as written, not a partial delivery.

My premise was wrong again: three requests, not two

The order said "That exactly two requests are issued. Count them yourself." The answer is three — one paged, and two unbounded, ObjectGantt.reload firing before and after its own object-schema read settles, the second adding $expand. Neither unbounded call carries $top.

Third PM premise falsified by a lane this shift, and again only because the order required checking my reading rather than the card's.

The root cause is the valuable find

ObjectGanttRenderer destructures { schema } and forwards schema and dataSource only — the only view wrapper that does not spread host props; grid, kanban, calendar, map and tree all do. So ListView's data never reaches the chart, ObjectGantt queries for itself, and the double fetch is invisible on a read-through of ListView because the live comment there claims the data prop "short-circuits the renderer's own fetch". That comment is false, and its falseness is exactly what hid this. Filed as #7222.

Suppression over re-description was the right of the two permitted shapes

The ruling allowed either. Suppression was chosen because re-description would need a new upward channel from ObjectGantt to ListView whose meaning would change if a ceiling is ever added — i.e. it would bet on half 2's outcome. Suppression is invariant to it. That is precisely the property the split was made for, and the docblock says so in place.

And it correctly refuses the tempting wrong fix: forwarding data to 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.

Scoping is pinned by controls, not by assertion

The chrome is shared — one block rendered for every viewType — and is correct everywhere but gantt, so a naive edit would have broken four honest footers to fix one lying one. The harness (18 rows, pageSize: 6) is run three ways: gantt (bar reads 6, chart draws 18 — the defect), grid (reads 18, the server total, no warning), and kanban (reads 6, and the board really is drawing those 6). The two controls are what demonstrate the scoping rather than claim it, and both are in the test file.

Reverse verification: predicted 2 failed / 2 passed, observed exactly that, restore proven by state plus blob equality with the mutation proven on disk before any run output was read.

check:sdui-registration-pins returned exit 2 with its own "This is exit 2, not a pass" banner and was recorded as NOT MEASURED rather than as a colour. That is the distinction this seat cares most about, and the gate itself was quoted rather than paraphrased.

One cosmetic nit, non-blocking

The docblock writes the renderer as < ObjectGantt schema={bound} dataSource={dataSource} / > with spaces inside the brackets. That defence is for GitHub issue and PR bodies, where an unclosed < eats the rest of the body — it is not needed in a source comment, where the spacing just costs a little readability. Harmless; fix on the next touch if convenient.

PM follow-ups accepted

Armed.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw - #7223

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome
Sep 1, 2026
Merged

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw#7223
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #7210 — half 1 only. Halves 2 and 3 stay open on that card, deliberately; see "Half 3" below.

What changed

ListView renders one record-count bar for every viewType: the row count, the "Showing first N records. More data may be available." warning tied to its own $top: pageSize query, and a rows-per-page selector that re-issues that query. It is now suppressed on gantt alone, behind a named constant (surfaceDrawsFetchedRows) whose doc comment carries the measurement and the reason.

One line of behaviour, one guard, two controls, plus a pin in plugin-gantt for the fact the guard rests on.

Why suppression rather than "make it describe the chart"

The ruling allowed either. ListView cannot report the chart's row count without a new upward channel from ObjectGantt — more published surface, and a channel whose meaning changes if a ceiling is ever added. Suppression is invariant to that decision, which is the property the split was made for. None of the bar's three parts describes the chart: not the count, not the warning, and not the page-size selector, which re-issues a query the chart ignores.

Measured, not inherited

Everything below was measured in this branch. Nothing was copied from the report.

Request count — the order said "count them yourself", and the answer is not two. Harness: one list-view of viewType: 'gantt', 18 rows in the store, pagination.pageSize: 6, instrumented adapter recording every find.

3 find calls
1. $top: 6, $select: [...], authored filter array -- ListView's own paged query
2. no $top, no $select, no $expand -- ObjectGantt, before its schema read settles
3. no $top, no $select, $expand: [owner] -- ObjectGantt, after it settles

Calls 2 and 3 are one defect the reporter's browser capture read as one request: the second/third split is the "two queries, the first unexpanded" regime already recorded for ObjectGantt on #6482, which is no longer open; re-confirmed here. Only call 1 has a $top at all.

The chart consumes the unbounded fetch. The DOM reading reproduced in the harness's own terms: the paged query answered 6 rows, and the chart rendered 18 gantt-task-row-* rows while the bar under it read 6 records - Showing first 6 records. More data may be available. That is the defect in miniature, one screen, both halves visible.

The chrome is shared, so the change is scoped. Same harness, same store, only viewType differing:

viewTypefind callsrecord-count barhonest?
gantt36 records - Showing first 6 records... while the chart drew 18no
grid118 records (server total, no warning)yes
kanban16 records - Showing first 6 records..., and the board drew 6yes

kanban is the load-bearing control: a non-grid, page-scoped surface whose warning is correct and had to survive.

The mechanism.plugin-gantt/src/index.tsx's ObjectGanttRenderer destructures { schema } and hands its child only schema and dataSource. Every sibling wrapper — grid, kanban, calendar, map, tree — spreads {...props}. So the dataListView passes never reaches the chart, ObjectGantt.reload's rest.data short-circuit is unreachable from the registry path, and the component queries for itself with no $top. Pinned in packages/plugin-gantt/src/ObjectGantt.hostDataProp-7210.test.tsx.

Half 3 — condition FAILED, so both requests stay

The order was explicit: measure whether the paged request has any consumer other than the footer; if it does, stop and report rather than pick a dialect. It does. Reads of ListView's data on the gantt surface:

lineconsumer
ListView.tsx:2801client-side CSV / JSON export slices data
ListView.tsx:2938UserFilters data={data} — option counts (showCount)
ListView.tsx:3573refresh indicator
ListView.tsx:3583load-error panel vs. rendering the view
ListView.tsx:3630loading skeleton vs. rendering the view
ListView.tsx:3841the record-count bar (this PR)

The last two are behavioural, not cosmetic, and one of them was measured directly: with the paged response delayed 600ms and the unbounded one immediate, at t=250ms the skeleton was up and the chart had 0 rows on screen although its own data had already arrived. The paged query decides whether the chart mounts at all. Removing it is not a no-op, so no dialect was picked and nothing was deduped.

Falsified ZONE 2 assumptions

  • "exactly two requests" — three (see above). One paged, two unbounded.
  • "the fetch path is ObjectView's non-grid list-view path" — the paged request in the report is ListView's (plugin-list), identified by its $select projection and $top, which ObjectView's non-grid fetch does not emit. ObjectView (plugin-view) delegates to ListView through renderListView and skips its own fetch when it does. The unbounded one is ObjectGantt.reload.
  • "the footer is rendered by shared paging chrome" — CONFIRMED, and it decided the width: one block in ListView, rendered for every view type, correct on all of them but gantt.
  • "@objectstack/rest applies no cap when limit is absent" — NOT MEASURED here. Out of reach from this repo; nothing in this change depends on it. What is measured is one layer up: the request carries no $top, so no page size authored on the view can bound it.
  • "the chart consumes the unbounded one" — CONFIRMED, reproduced as row counts in the harness rather than inferred.

Not touched, deliberately

Reverse verification

Direction and counts predicted before running: drop surfaceDrawsFetchedRows from the bar's render condition and exactly the two gantt cases go red while both controls stay green — 2 failed / 2 passed. Observed exactly that, then 4 passed after restore.

Mutation proved on disk: removed-token count 2 to 1, mutated-line count 0 to 1, blob hash 4a1ce3c1 to 7f55c73b. Restore proved by state: git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to 4a1ce3c1, the HEAD blob.

Verification (all at f3d7b3cd3)

  • pnpm exec vitest run packages/plugin-list/ packages/plugin-gantt/113 files passed, 1183 tests passed.
  • 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. Both new test files proved present in the tsconfig.test.json program via tsc --listFiles (1 hit each), so "typecheck is clean" actually covers them.
  • Gates: 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 (46/46 packages, 0 errors), and all four changeset guards — every one exit 0. check:sdui-registration-pins returned exit 2, PRECONDITION NOT MET (no apps/console build to weigh) — recorded as NOT MEASURED, not as a pass; this diff adds no registration and moves no entry array. Left to CI.
  • ESLint, narrowed and declared: 3 changed files, 0 errors (--format json, file count read from that output, not guessed). The narrowing is a measurement rather than a skip because eslint.config.js enables no type-aware linting (no project / projectService in its languageOptions), so no untouched file's verdict can move because of this diff, and the diff adds no config and no inline directive.

Generated by Claude Code


Generated by Claude Code

The bar reports ListView's own paged query — the row count, the "Showing
first N records" warning tied to `$top: pageSize`, and the rows-per-page
selector that re-issues it. Every other view type draws the `data` ListView
hands down, so there the bar is accurate. The registered `object-gantt`
renderer forwards no prop but `schema`, so the chart queries for itself with
no `$top`: measured at 18 rows / pageSize 6, the chart drew 18 while the bar
read "6 records - Showing first 6 records. More data may be available."
Scoped to gantt; grid and kanban controls pin that the shared chrome is
untouched elsewhere. The gantt's own query is left exactly as it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C-Qg7c4O.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.22KB56.04KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.24KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. Reviewer of record: domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM

Verified over git: clean against origin/main3561bd2ca, and clean against #7214's queue build (which main is about to become), and clean against #7220. Changeset present. The functional change is one named constant and one added conjunct in a render condition.

Half 3's condition failed, and stopping was the right answer

The dispatch made the dedupe conditional: measure whether the paged request has any consumer besides the footer, and if it has others, stop rather than pick a filter dialect. It has five, two of them behavioural — data gates the loading skeleton and the load-error panel (so it decides whether the chart mounts at all), feeds UserFilters' 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. That is the observation that makes suppression safe and removal wrong.

So nothing was deduped. That is a full success against the order as written, not a partial delivery.

My premise was wrong again: three requests, not two

The order said "That exactly two requests are issued. Count them yourself." The answer is three — one paged, and two unbounded, ObjectGantt.reload firing before and after its own object-schema read settles, the second adding $expand. Neither unbounded call carries $top.

Third PM premise falsified by a lane this shift, and again only because the order required checking my reading rather than the card's.

The root cause is the valuable find

ObjectGanttRenderer destructures { schema } and forwards schema and dataSource only — the only view wrapper that does not spread host props; grid, kanban, calendar, map and tree all do. So ListView's data never reaches the chart, ObjectGantt queries for itself, and the double fetch is invisible on a read-through of ListView because the live comment there claims the data prop "short-circuits the renderer's own fetch". That comment is false, and its falseness is exactly what hid this. Filed as #7222.

Suppression over re-description was the right of the two permitted shapes

The ruling allowed either. Suppression was chosen because re-description would need a new upward channel from ObjectGantt to ListView whose meaning would change if a ceiling is ever added — i.e. it would bet on half 2's outcome. Suppression is invariant to it. That is precisely the property the split was made for, and the docblock says so in place.

And it correctly refuses the tempting wrong fix: forwarding data to 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.

Scoping is pinned by controls, not by assertion

The chrome is shared — one block rendered for every viewType — and is correct everywhere but gantt, so a naive edit would have broken four honest footers to fix one lying one. The harness (18 rows, pageSize: 6) is run three ways: gantt (bar reads 6, chart draws 18 — the defect), grid (reads 18, the server total, no warning), and kanban (reads 6, and the board really is drawing those 6). The two controls are what demonstrate the scoping rather than claim it, and both are in the test file.

Reverse verification: predicted 2 failed / 2 passed, observed exactly that, restore proven by state plus blob equality with the mutation proven on disk before any run output was read.

check:sdui-registration-pins returned exit 2 with its own "This is exit 2, not a pass" banner and was recorded as NOT MEASURED rather than as a colour. That is the distinction this seat cares most about, and the gate itself was quoted rather than paraphrased.

One cosmetic nit, non-blocking

The docblock writes the renderer as < ObjectGantt schema={bound} dataSource={dataSource} / > with spaces inside the brackets. That defence is for GitHub issue and PR bodies, where an unclosed < eats the rest of the body — it is not needed in a source comment, where the spacing just costs a little readability. Harmless; fix on the next touch if convenient.

PM follow-ups accepted

Armed.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw - #7223

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome
Sep 1, 2026
Merged

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw#7223
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #7210 — half 1 only. Halves 2 and 3 stay open on that card, deliberately; see "Half 3" below.

What changed

ListView renders one record-count bar for every viewType: the row count, the "Showing first N records. More data may be available." warning tied to its own $top: pageSize query, and a rows-per-page selector that re-issues that query. It is now suppressed on gantt alone, behind a named constant (surfaceDrawsFetchedRows) whose doc comment carries the measurement and the reason.

One line of behaviour, one guard, two controls, plus a pin in plugin-gantt for the fact the guard rests on.

Why suppression rather than "make it describe the chart"

The ruling allowed either. ListView cannot report the chart's row count without a new upward channel from ObjectGantt — more published surface, and a channel whose meaning changes if a ceiling is ever added. Suppression is invariant to that decision, which is the property the split was made for. None of the bar's three parts describes the chart: not the count, not the warning, and not the page-size selector, which re-issues a query the chart ignores.

Measured, not inherited

Everything below was measured in this branch. Nothing was copied from the report.

Request count — the order said "count them yourself", and the answer is not two. Harness: one list-view of viewType: 'gantt', 18 rows in the store, pagination.pageSize: 6, instrumented adapter recording every find.

3 find calls
1. $top: 6, $select: [...], authored filter array -- ListView's own paged query
2. no $top, no $select, no $expand -- ObjectGantt, before its schema read settles
3. no $top, no $select, $expand: [owner] -- ObjectGantt, after it settles

Calls 2 and 3 are one defect the reporter's browser capture read as one request: the second/third split is the "two queries, the first unexpanded" regime already recorded for ObjectGantt on #6482, which is no longer open; re-confirmed here. Only call 1 has a $top at all.

The chart consumes the unbounded fetch. The DOM reading reproduced in the harness's own terms: the paged query answered 6 rows, and the chart rendered 18 gantt-task-row-* rows while the bar under it read 6 records - Showing first 6 records. More data may be available. That is the defect in miniature, one screen, both halves visible.

The chrome is shared, so the change is scoped. Same harness, same store, only viewType differing:

viewTypefind callsrecord-count barhonest?
gantt36 records - Showing first 6 records... while the chart drew 18no
grid118 records (server total, no warning)yes
kanban16 records - Showing first 6 records..., and the board drew 6yes

kanban is the load-bearing control: a non-grid, page-scoped surface whose warning is correct and had to survive.

The mechanism.plugin-gantt/src/index.tsx's ObjectGanttRenderer destructures { schema } and hands its child only schema and dataSource. Every sibling wrapper — grid, kanban, calendar, map, tree — spreads {...props}. So the dataListView passes never reaches the chart, ObjectGantt.reload's rest.data short-circuit is unreachable from the registry path, and the component queries for itself with no $top. Pinned in packages/plugin-gantt/src/ObjectGantt.hostDataProp-7210.test.tsx.

Half 3 — condition FAILED, so both requests stay

The order was explicit: measure whether the paged request has any consumer other than the footer; if it does, stop and report rather than pick a dialect. It does. Reads of ListView's data on the gantt surface:

lineconsumer
ListView.tsx:2801client-side CSV / JSON export slices data
ListView.tsx:2938UserFilters data={data} — option counts (showCount)
ListView.tsx:3573refresh indicator
ListView.tsx:3583load-error panel vs. rendering the view
ListView.tsx:3630loading skeleton vs. rendering the view
ListView.tsx:3841the record-count bar (this PR)

The last two are behavioural, not cosmetic, and one of them was measured directly: with the paged response delayed 600ms and the unbounded one immediate, at t=250ms the skeleton was up and the chart had 0 rows on screen although its own data had already arrived. The paged query decides whether the chart mounts at all. Removing it is not a no-op, so no dialect was picked and nothing was deduped.

Falsified ZONE 2 assumptions

  • "exactly two requests" — three (see above). One paged, two unbounded.
  • "the fetch path is ObjectView's non-grid list-view path" — the paged request in the report is ListView's (plugin-list), identified by its $select projection and $top, which ObjectView's non-grid fetch does not emit. ObjectView (plugin-view) delegates to ListView through renderListView and skips its own fetch when it does. The unbounded one is ObjectGantt.reload.
  • "the footer is rendered by shared paging chrome" — CONFIRMED, and it decided the width: one block in ListView, rendered for every view type, correct on all of them but gantt.
  • "@objectstack/rest applies no cap when limit is absent" — NOT MEASURED here. Out of reach from this repo; nothing in this change depends on it. What is measured is one layer up: the request carries no $top, so no page size authored on the view can bound it.
  • "the chart consumes the unbounded one" — CONFIRMED, reproduced as row counts in the harness rather than inferred.

Not touched, deliberately

Reverse verification

Direction and counts predicted before running: drop surfaceDrawsFetchedRows from the bar's render condition and exactly the two gantt cases go red while both controls stay green — 2 failed / 2 passed. Observed exactly that, then 4 passed after restore.

Mutation proved on disk: removed-token count 2 to 1, mutated-line count 0 to 1, blob hash 4a1ce3c1 to 7f55c73b. Restore proved by state: git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to 4a1ce3c1, the HEAD blob.

Verification (all at f3d7b3cd3)

  • pnpm exec vitest run packages/plugin-list/ packages/plugin-gantt/113 files passed, 1183 tests passed.
  • 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. Both new test files proved present in the tsconfig.test.json program via tsc --listFiles (1 hit each), so "typecheck is clean" actually covers them.
  • Gates: 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 (46/46 packages, 0 errors), and all four changeset guards — every one exit 0. check:sdui-registration-pins returned exit 2, PRECONDITION NOT MET (no apps/console build to weigh) — recorded as NOT MEASURED, not as a pass; this diff adds no registration and moves no entry array. Left to CI.
  • ESLint, narrowed and declared: 3 changed files, 0 errors (--format json, file count read from that output, not guessed). The narrowing is a measurement rather than a skip because eslint.config.js enables no type-aware linting (no project / projectService in its languageOptions), so no untouched file's verdict can move because of this diff, and the diff adds no config and no inline directive.

Generated by Claude Code


Generated by Claude Code

The bar reports ListView's own paged query — the row count, the "Showing
first N records" warning tied to `$top: pageSize`, and the rows-per-page
selector that re-issues it. Every other view type draws the `data` ListView
hands down, so there the bar is accurate. The registered `object-gantt`
renderer forwards no prop but `schema`, so the chart queries for itself with
no `$top`: measured at 18 rows / pageSize 6, the chart drew 18 while the bar
read "6 records - Showing first 6 records. More data may be available."
Scoped to gantt; grid and kanban controls pin that the shared chrome is
untouched elsewhere. The gantt's own query is left exactly as it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C-Qg7c4O.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.22KB56.04KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.24KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. Reviewer of record: domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM

Verified over git: clean against origin/main3561bd2ca, and clean against #7214's queue build (which main is about to become), and clean against #7220. Changeset present. The functional change is one named constant and one added conjunct in a render condition.

Half 3's condition failed, and stopping was the right answer

The dispatch made the dedupe conditional: measure whether the paged request has any consumer besides the footer, and if it has others, stop rather than pick a filter dialect. It has five, two of them behavioural — data gates the loading skeleton and the load-error panel (so it decides whether the chart mounts at all), feeds UserFilters' 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. That is the observation that makes suppression safe and removal wrong.

So nothing was deduped. That is a full success against the order as written, not a partial delivery.

My premise was wrong again: three requests, not two

The order said "That exactly two requests are issued. Count them yourself." The answer is three — one paged, and two unbounded, ObjectGantt.reload firing before and after its own object-schema read settles, the second adding $expand. Neither unbounded call carries $top.

Third PM premise falsified by a lane this shift, and again only because the order required checking my reading rather than the card's.

The root cause is the valuable find

ObjectGanttRenderer destructures { schema } and forwards schema and dataSource only — the only view wrapper that does not spread host props; grid, kanban, calendar, map and tree all do. So ListView's data never reaches the chart, ObjectGantt queries for itself, and the double fetch is invisible on a read-through of ListView because the live comment there claims the data prop "short-circuits the renderer's own fetch". That comment is false, and its falseness is exactly what hid this. Filed as #7222.

Suppression over re-description was the right of the two permitted shapes

The ruling allowed either. Suppression was chosen because re-description would need a new upward channel from ObjectGantt to ListView whose meaning would change if a ceiling is ever added — i.e. it would bet on half 2's outcome. Suppression is invariant to it. That is precisely the property the split was made for, and the docblock says so in place.

And it correctly refuses the tempting wrong fix: forwarding data to 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.

Scoping is pinned by controls, not by assertion

The chrome is shared — one block rendered for every viewType — and is correct everywhere but gantt, so a naive edit would have broken four honest footers to fix one lying one. The harness (18 rows, pageSize: 6) is run three ways: gantt (bar reads 6, chart draws 18 — the defect), grid (reads 18, the server total, no warning), and kanban (reads 6, and the board really is drawing those 6). The two controls are what demonstrate the scoping rather than claim it, and both are in the test file.

Reverse verification: predicted 2 failed / 2 passed, observed exactly that, restore proven by state plus blob equality with the mutation proven on disk before any run output was read.

check:sdui-registration-pins returned exit 2 with its own "This is exit 2, not a pass" banner and was recorded as NOT MEASURED rather than as a colour. That is the distinction this seat cares most about, and the gate itself was quoted rather than paraphrased.

One cosmetic nit, non-blocking

The docblock writes the renderer as < ObjectGantt schema={bound} dataSource={dataSource} / > with spaces inside the brackets. That defence is for GitHub issue and PR bodies, where an unclosed < eats the rest of the body — it is not needed in a source comment, where the spacing just costs a little readability. Harmless; fix on the next touch if convenient.

PM follow-ups accepted

Armed.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw - #7223

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome
Sep 1, 2026
Merged

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw#7223
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #7210 — half 1 only. Halves 2 and 3 stay open on that card, deliberately; see "Half 3" below.

What changed

ListView renders one record-count bar for every viewType: the row count, the "Showing first N records. More data may be available." warning tied to its own $top: pageSize query, and a rows-per-page selector that re-issues that query. It is now suppressed on gantt alone, behind a named constant (surfaceDrawsFetchedRows) whose doc comment carries the measurement and the reason.

One line of behaviour, one guard, two controls, plus a pin in plugin-gantt for the fact the guard rests on.

Why suppression rather than "make it describe the chart"

The ruling allowed either. ListView cannot report the chart's row count without a new upward channel from ObjectGantt — more published surface, and a channel whose meaning changes if a ceiling is ever added. Suppression is invariant to that decision, which is the property the split was made for. None of the bar's three parts describes the chart: not the count, not the warning, and not the page-size selector, which re-issues a query the chart ignores.

Measured, not inherited

Everything below was measured in this branch. Nothing was copied from the report.

Request count — the order said "count them yourself", and the answer is not two. Harness: one list-view of viewType: 'gantt', 18 rows in the store, pagination.pageSize: 6, instrumented adapter recording every find.

3 find calls
1. $top: 6, $select: [...], authored filter array -- ListView's own paged query
2. no $top, no $select, no $expand -- ObjectGantt, before its schema read settles
3. no $top, no $select, $expand: [owner] -- ObjectGantt, after it settles

Calls 2 and 3 are one defect the reporter's browser capture read as one request: the second/third split is the "two queries, the first unexpanded" regime already recorded for ObjectGantt on #6482, which is no longer open; re-confirmed here. Only call 1 has a $top at all.

The chart consumes the unbounded fetch. The DOM reading reproduced in the harness's own terms: the paged query answered 6 rows, and the chart rendered 18 gantt-task-row-* rows while the bar under it read 6 records - Showing first 6 records. More data may be available. That is the defect in miniature, one screen, both halves visible.

The chrome is shared, so the change is scoped. Same harness, same store, only viewType differing:

viewTypefind callsrecord-count barhonest?
gantt36 records - Showing first 6 records... while the chart drew 18no
grid118 records (server total, no warning)yes
kanban16 records - Showing first 6 records..., and the board drew 6yes

kanban is the load-bearing control: a non-grid, page-scoped surface whose warning is correct and had to survive.

The mechanism.plugin-gantt/src/index.tsx's ObjectGanttRenderer destructures { schema } and hands its child only schema and dataSource. Every sibling wrapper — grid, kanban, calendar, map, tree — spreads {...props}. So the dataListView passes never reaches the chart, ObjectGantt.reload's rest.data short-circuit is unreachable from the registry path, and the component queries for itself with no $top. Pinned in packages/plugin-gantt/src/ObjectGantt.hostDataProp-7210.test.tsx.

Half 3 — condition FAILED, so both requests stay

The order was explicit: measure whether the paged request has any consumer other than the footer; if it does, stop and report rather than pick a dialect. It does. Reads of ListView's data on the gantt surface:

lineconsumer
ListView.tsx:2801client-side CSV / JSON export slices data
ListView.tsx:2938UserFilters data={data} — option counts (showCount)
ListView.tsx:3573refresh indicator
ListView.tsx:3583load-error panel vs. rendering the view
ListView.tsx:3630loading skeleton vs. rendering the view
ListView.tsx:3841the record-count bar (this PR)

The last two are behavioural, not cosmetic, and one of them was measured directly: with the paged response delayed 600ms and the unbounded one immediate, at t=250ms the skeleton was up and the chart had 0 rows on screen although its own data had already arrived. The paged query decides whether the chart mounts at all. Removing it is not a no-op, so no dialect was picked and nothing was deduped.

Falsified ZONE 2 assumptions

  • "exactly two requests" — three (see above). One paged, two unbounded.
  • "the fetch path is ObjectView's non-grid list-view path" — the paged request in the report is ListView's (plugin-list), identified by its $select projection and $top, which ObjectView's non-grid fetch does not emit. ObjectView (plugin-view) delegates to ListView through renderListView and skips its own fetch when it does. The unbounded one is ObjectGantt.reload.
  • "the footer is rendered by shared paging chrome" — CONFIRMED, and it decided the width: one block in ListView, rendered for every view type, correct on all of them but gantt.
  • "@objectstack/rest applies no cap when limit is absent" — NOT MEASURED here. Out of reach from this repo; nothing in this change depends on it. What is measured is one layer up: the request carries no $top, so no page size authored on the view can bound it.
  • "the chart consumes the unbounded one" — CONFIRMED, reproduced as row counts in the harness rather than inferred.

Not touched, deliberately

Reverse verification

Direction and counts predicted before running: drop surfaceDrawsFetchedRows from the bar's render condition and exactly the two gantt cases go red while both controls stay green — 2 failed / 2 passed. Observed exactly that, then 4 passed after restore.

Mutation proved on disk: removed-token count 2 to 1, mutated-line count 0 to 1, blob hash 4a1ce3c1 to 7f55c73b. Restore proved by state: git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to 4a1ce3c1, the HEAD blob.

Verification (all at f3d7b3cd3)

  • pnpm exec vitest run packages/plugin-list/ packages/plugin-gantt/113 files passed, 1183 tests passed.
  • 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. Both new test files proved present in the tsconfig.test.json program via tsc --listFiles (1 hit each), so "typecheck is clean" actually covers them.
  • Gates: 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 (46/46 packages, 0 errors), and all four changeset guards — every one exit 0. check:sdui-registration-pins returned exit 2, PRECONDITION NOT MET (no apps/console build to weigh) — recorded as NOT MEASURED, not as a pass; this diff adds no registration and moves no entry array. Left to CI.
  • ESLint, narrowed and declared: 3 changed files, 0 errors (--format json, file count read from that output, not guessed). The narrowing is a measurement rather than a skip because eslint.config.js enables no type-aware linting (no project / projectService in its languageOptions), so no untouched file's verdict can move because of this diff, and the diff adds no config and no inline directive.

Generated by Claude Code


Generated by Claude Code

The bar reports ListView's own paged query — the row count, the "Showing
first N records" warning tied to `$top: pageSize`, and the rows-per-page
selector that re-issues it. Every other view type draws the `data` ListView
hands down, so there the bar is accurate. The registered `object-gantt`
renderer forwards no prop but `schema`, so the chart queries for itself with
no `$top`: measured at 18 rows / pageSize 6, the chart drew 18 while the bar
read "6 records - Showing first 6 records. More data may be available."
Scoped to gantt; grid and kanban controls pin that the shared chrome is
untouched elsewhere. The gantt's own query is left exactly as it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C-Qg7c4O.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.22KB56.04KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.24KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. Reviewer of record: domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM

Verified over git: clean against origin/main3561bd2ca, and clean against #7214's queue build (which main is about to become), and clean against #7220. Changeset present. The functional change is one named constant and one added conjunct in a render condition.

Half 3's condition failed, and stopping was the right answer

The dispatch made the dedupe conditional: measure whether the paged request has any consumer besides the footer, and if it has others, stop rather than pick a filter dialect. It has five, two of them behavioural — data gates the loading skeleton and the load-error panel (so it decides whether the chart mounts at all), feeds UserFilters' 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. That is the observation that makes suppression safe and removal wrong.

So nothing was deduped. That is a full success against the order as written, not a partial delivery.

My premise was wrong again: three requests, not two

The order said "That exactly two requests are issued. Count them yourself." The answer is three — one paged, and two unbounded, ObjectGantt.reload firing before and after its own object-schema read settles, the second adding $expand. Neither unbounded call carries $top.

Third PM premise falsified by a lane this shift, and again only because the order required checking my reading rather than the card's.

The root cause is the valuable find

ObjectGanttRenderer destructures { schema } and forwards schema and dataSource only — the only view wrapper that does not spread host props; grid, kanban, calendar, map and tree all do. So ListView's data never reaches the chart, ObjectGantt queries for itself, and the double fetch is invisible on a read-through of ListView because the live comment there claims the data prop "short-circuits the renderer's own fetch". That comment is false, and its falseness is exactly what hid this. Filed as #7222.

Suppression over re-description was the right of the two permitted shapes

The ruling allowed either. Suppression was chosen because re-description would need a new upward channel from ObjectGantt to ListView whose meaning would change if a ceiling is ever added — i.e. it would bet on half 2's outcome. Suppression is invariant to it. That is precisely the property the split was made for, and the docblock says so in place.

And it correctly refuses the tempting wrong fix: forwarding data to 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.

Scoping is pinned by controls, not by assertion

The chrome is shared — one block rendered for every viewType — and is correct everywhere but gantt, so a naive edit would have broken four honest footers to fix one lying one. The harness (18 rows, pageSize: 6) is run three ways: gantt (bar reads 6, chart draws 18 — the defect), grid (reads 18, the server total, no warning), and kanban (reads 6, and the board really is drawing those 6). The two controls are what demonstrate the scoping rather than claim it, and both are in the test file.

Reverse verification: predicted 2 failed / 2 passed, observed exactly that, restore proven by state plus blob equality with the mutation proven on disk before any run output was read.

check:sdui-registration-pins returned exit 2 with its own "This is exit 2, not a pass" banner and was recorded as NOT MEASURED rather than as a colour. That is the distinction this seat cares most about, and the gate itself was quoted rather than paraphrased.

One cosmetic nit, non-blocking

The docblock writes the renderer as < ObjectGantt schema={bound} dataSource={dataSource} / > with spaces inside the brackets. That defence is for GitHub issue and PR bodies, where an unclosed < eats the rest of the body — it is not needed in a source comment, where the spacing just costs a little readability. Harmless; fix on the next touch if convenient.

PM follow-ups accepted

Armed.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw - #7223

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome
Sep 1, 2026
Merged

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw#7223
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #7210 — half 1 only. Halves 2 and 3 stay open on that card, deliberately; see "Half 3" below.

What changed

ListView renders one record-count bar for every viewType: the row count, the "Showing first N records. More data may be available." warning tied to its own $top: pageSize query, and a rows-per-page selector that re-issues that query. It is now suppressed on gantt alone, behind a named constant (surfaceDrawsFetchedRows) whose doc comment carries the measurement and the reason.

One line of behaviour, one guard, two controls, plus a pin in plugin-gantt for the fact the guard rests on.

Why suppression rather than "make it describe the chart"

The ruling allowed either. ListView cannot report the chart's row count without a new upward channel from ObjectGantt — more published surface, and a channel whose meaning changes if a ceiling is ever added. Suppression is invariant to that decision, which is the property the split was made for. None of the bar's three parts describes the chart: not the count, not the warning, and not the page-size selector, which re-issues a query the chart ignores.

Measured, not inherited

Everything below was measured in this branch. Nothing was copied from the report.

Request count — the order said "count them yourself", and the answer is not two. Harness: one list-view of viewType: 'gantt', 18 rows in the store, pagination.pageSize: 6, instrumented adapter recording every find.

3 find calls
1. $top: 6, $select: [...], authored filter array -- ListView's own paged query
2. no $top, no $select, no $expand -- ObjectGantt, before its schema read settles
3. no $top, no $select, $expand: [owner] -- ObjectGantt, after it settles

Calls 2 and 3 are one defect the reporter's browser capture read as one request: the second/third split is the "two queries, the first unexpanded" regime already recorded for ObjectGantt on #6482, which is no longer open; re-confirmed here. Only call 1 has a $top at all.

The chart consumes the unbounded fetch. The DOM reading reproduced in the harness's own terms: the paged query answered 6 rows, and the chart rendered 18 gantt-task-row-* rows while the bar under it read 6 records - Showing first 6 records. More data may be available. That is the defect in miniature, one screen, both halves visible.

The chrome is shared, so the change is scoped. Same harness, same store, only viewType differing:

viewTypefind callsrecord-count barhonest?
gantt36 records - Showing first 6 records... while the chart drew 18no
grid118 records (server total, no warning)yes
kanban16 records - Showing first 6 records..., and the board drew 6yes

kanban is the load-bearing control: a non-grid, page-scoped surface whose warning is correct and had to survive.

The mechanism.plugin-gantt/src/index.tsx's ObjectGanttRenderer destructures { schema } and hands its child only schema and dataSource. Every sibling wrapper — grid, kanban, calendar, map, tree — spreads {...props}. So the dataListView passes never reaches the chart, ObjectGantt.reload's rest.data short-circuit is unreachable from the registry path, and the component queries for itself with no $top. Pinned in packages/plugin-gantt/src/ObjectGantt.hostDataProp-7210.test.tsx.

Half 3 — condition FAILED, so both requests stay

The order was explicit: measure whether the paged request has any consumer other than the footer; if it does, stop and report rather than pick a dialect. It does. Reads of ListView's data on the gantt surface:

lineconsumer
ListView.tsx:2801client-side CSV / JSON export slices data
ListView.tsx:2938UserFilters data={data} — option counts (showCount)
ListView.tsx:3573refresh indicator
ListView.tsx:3583load-error panel vs. rendering the view
ListView.tsx:3630loading skeleton vs. rendering the view
ListView.tsx:3841the record-count bar (this PR)

The last two are behavioural, not cosmetic, and one of them was measured directly: with the paged response delayed 600ms and the unbounded one immediate, at t=250ms the skeleton was up and the chart had 0 rows on screen although its own data had already arrived. The paged query decides whether the chart mounts at all. Removing it is not a no-op, so no dialect was picked and nothing was deduped.

Falsified ZONE 2 assumptions

  • "exactly two requests" — three (see above). One paged, two unbounded.
  • "the fetch path is ObjectView's non-grid list-view path" — the paged request in the report is ListView's (plugin-list), identified by its $select projection and $top, which ObjectView's non-grid fetch does not emit. ObjectView (plugin-view) delegates to ListView through renderListView and skips its own fetch when it does. The unbounded one is ObjectGantt.reload.
  • "the footer is rendered by shared paging chrome" — CONFIRMED, and it decided the width: one block in ListView, rendered for every view type, correct on all of them but gantt.
  • "@objectstack/rest applies no cap when limit is absent" — NOT MEASURED here. Out of reach from this repo; nothing in this change depends on it. What is measured is one layer up: the request carries no $top, so no page size authored on the view can bound it.
  • "the chart consumes the unbounded one" — CONFIRMED, reproduced as row counts in the harness rather than inferred.

Not touched, deliberately

Reverse verification

Direction and counts predicted before running: drop surfaceDrawsFetchedRows from the bar's render condition and exactly the two gantt cases go red while both controls stay green — 2 failed / 2 passed. Observed exactly that, then 4 passed after restore.

Mutation proved on disk: removed-token count 2 to 1, mutated-line count 0 to 1, blob hash 4a1ce3c1 to 7f55c73b. Restore proved by state: git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to 4a1ce3c1, the HEAD blob.

Verification (all at f3d7b3cd3)

  • pnpm exec vitest run packages/plugin-list/ packages/plugin-gantt/113 files passed, 1183 tests passed.
  • 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. Both new test files proved present in the tsconfig.test.json program via tsc --listFiles (1 hit each), so "typecheck is clean" actually covers them.
  • Gates: 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 (46/46 packages, 0 errors), and all four changeset guards — every one exit 0. check:sdui-registration-pins returned exit 2, PRECONDITION NOT MET (no apps/console build to weigh) — recorded as NOT MEASURED, not as a pass; this diff adds no registration and moves no entry array. Left to CI.
  • ESLint, narrowed and declared: 3 changed files, 0 errors (--format json, file count read from that output, not guessed). The narrowing is a measurement rather than a skip because eslint.config.js enables no type-aware linting (no project / projectService in its languageOptions), so no untouched file's verdict can move because of this diff, and the diff adds no config and no inline directive.

Generated by Claude Code


Generated by Claude Code

The bar reports ListView's own paged query — the row count, the "Showing
first N records" warning tied to `$top: pageSize`, and the rows-per-page
selector that re-issues it. Every other view type draws the `data` ListView
hands down, so there the bar is accurate. The registered `object-gantt`
renderer forwards no prop but `schema`, so the chart queries for itself with
no `$top`: measured at 18 rows / pageSize 6, the chart drew 18 while the bar
read "6 records - Showing first 6 records. More data may be available."
Scoped to gantt; grid and kanban controls pin that the shared chrome is
untouched elsewhere. The gantt's own query is left exactly as it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C-Qg7c4O.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.22KB56.04KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.24KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. Reviewer of record: domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM

Verified over git: clean against origin/main3561bd2ca, and clean against #7214's queue build (which main is about to become), and clean against #7220. Changeset present. The functional change is one named constant and one added conjunct in a render condition.

Half 3's condition failed, and stopping was the right answer

The dispatch made the dedupe conditional: measure whether the paged request has any consumer besides the footer, and if it has others, stop rather than pick a filter dialect. It has five, two of them behavioural — data gates the loading skeleton and the load-error panel (so it decides whether the chart mounts at all), feeds UserFilters' 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. That is the observation that makes suppression safe and removal wrong.

So nothing was deduped. That is a full success against the order as written, not a partial delivery.

My premise was wrong again: three requests, not two

The order said "That exactly two requests are issued. Count them yourself." The answer is three — one paged, and two unbounded, ObjectGantt.reload firing before and after its own object-schema read settles, the second adding $expand. Neither unbounded call carries $top.

Third PM premise falsified by a lane this shift, and again only because the order required checking my reading rather than the card's.

The root cause is the valuable find

ObjectGanttRenderer destructures { schema } and forwards schema and dataSource only — the only view wrapper that does not spread host props; grid, kanban, calendar, map and tree all do. So ListView's data never reaches the chart, ObjectGantt queries for itself, and the double fetch is invisible on a read-through of ListView because the live comment there claims the data prop "short-circuits the renderer's own fetch". That comment is false, and its falseness is exactly what hid this. Filed as #7222.

Suppression over re-description was the right of the two permitted shapes

The ruling allowed either. Suppression was chosen because re-description would need a new upward channel from ObjectGantt to ListView whose meaning would change if a ceiling is ever added — i.e. it would bet on half 2's outcome. Suppression is invariant to it. That is precisely the property the split was made for, and the docblock says so in place.

And it correctly refuses the tempting wrong fix: forwarding data to 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.

Scoping is pinned by controls, not by assertion

The chrome is shared — one block rendered for every viewType — and is correct everywhere but gantt, so a naive edit would have broken four honest footers to fix one lying one. The harness (18 rows, pageSize: 6) is run three ways: gantt (bar reads 6, chart draws 18 — the defect), grid (reads 18, the server total, no warning), and kanban (reads 6, and the board really is drawing those 6). The two controls are what demonstrate the scoping rather than claim it, and both are in the test file.

Reverse verification: predicted 2 failed / 2 passed, observed exactly that, restore proven by state plus blob equality with the mutation proven on disk before any run output was read.

check:sdui-registration-pins returned exit 2 with its own "This is exit 2, not a pass" banner and was recorded as NOT MEASURED rather than as a colour. That is the distinction this seat cares most about, and the gate itself was quoted rather than paraphrased.

One cosmetic nit, non-blocking

The docblock writes the renderer as < ObjectGantt schema={bound} dataSource={dataSource} / > with spaces inside the brackets. That defence is for GitHub issue and PR bodies, where an unclosed < eats the rest of the body — it is not needed in a source comment, where the spacing just costs a little readability. Harmless; fix on the next touch if convenient.

PM follow-ups accepted

Armed.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw - #7223

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome
Sep 1, 2026
Merged

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw#7223
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #7210 — half 1 only. Halves 2 and 3 stay open on that card, deliberately; see "Half 3" below.

What changed

ListView renders one record-count bar for every viewType: the row count, the "Showing first N records. More data may be available." warning tied to its own $top: pageSize query, and a rows-per-page selector that re-issues that query. It is now suppressed on gantt alone, behind a named constant (surfaceDrawsFetchedRows) whose doc comment carries the measurement and the reason.

One line of behaviour, one guard, two controls, plus a pin in plugin-gantt for the fact the guard rests on.

Why suppression rather than "make it describe the chart"

The ruling allowed either. ListView cannot report the chart's row count without a new upward channel from ObjectGantt — more published surface, and a channel whose meaning changes if a ceiling is ever added. Suppression is invariant to that decision, which is the property the split was made for. None of the bar's three parts describes the chart: not the count, not the warning, and not the page-size selector, which re-issues a query the chart ignores.

Measured, not inherited

Everything below was measured in this branch. Nothing was copied from the report.

Request count — the order said "count them yourself", and the answer is not two. Harness: one list-view of viewType: 'gantt', 18 rows in the store, pagination.pageSize: 6, instrumented adapter recording every find.

3 find calls
1. $top: 6, $select: [...], authored filter array -- ListView's own paged query
2. no $top, no $select, no $expand -- ObjectGantt, before its schema read settles
3. no $top, no $select, $expand: [owner] -- ObjectGantt, after it settles

Calls 2 and 3 are one defect the reporter's browser capture read as one request: the second/third split is the "two queries, the first unexpanded" regime already recorded for ObjectGantt on #6482, which is no longer open; re-confirmed here. Only call 1 has a $top at all.

The chart consumes the unbounded fetch. The DOM reading reproduced in the harness's own terms: the paged query answered 6 rows, and the chart rendered 18 gantt-task-row-* rows while the bar under it read 6 records - Showing first 6 records. More data may be available. That is the defect in miniature, one screen, both halves visible.

The chrome is shared, so the change is scoped. Same harness, same store, only viewType differing:

viewTypefind callsrecord-count barhonest?
gantt36 records - Showing first 6 records... while the chart drew 18no
grid118 records (server total, no warning)yes
kanban16 records - Showing first 6 records..., and the board drew 6yes

kanban is the load-bearing control: a non-grid, page-scoped surface whose warning is correct and had to survive.

The mechanism.plugin-gantt/src/index.tsx's ObjectGanttRenderer destructures { schema } and hands its child only schema and dataSource. Every sibling wrapper — grid, kanban, calendar, map, tree — spreads {...props}. So the dataListView passes never reaches the chart, ObjectGantt.reload's rest.data short-circuit is unreachable from the registry path, and the component queries for itself with no $top. Pinned in packages/plugin-gantt/src/ObjectGantt.hostDataProp-7210.test.tsx.

Half 3 — condition FAILED, so both requests stay

The order was explicit: measure whether the paged request has any consumer other than the footer; if it does, stop and report rather than pick a dialect. It does. Reads of ListView's data on the gantt surface:

lineconsumer
ListView.tsx:2801client-side CSV / JSON export slices data
ListView.tsx:2938UserFilters data={data} — option counts (showCount)
ListView.tsx:3573refresh indicator
ListView.tsx:3583load-error panel vs. rendering the view
ListView.tsx:3630loading skeleton vs. rendering the view
ListView.tsx:3841the record-count bar (this PR)

The last two are behavioural, not cosmetic, and one of them was measured directly: with the paged response delayed 600ms and the unbounded one immediate, at t=250ms the skeleton was up and the chart had 0 rows on screen although its own data had already arrived. The paged query decides whether the chart mounts at all. Removing it is not a no-op, so no dialect was picked and nothing was deduped.

Falsified ZONE 2 assumptions

  • "exactly two requests" — three (see above). One paged, two unbounded.
  • "the fetch path is ObjectView's non-grid list-view path" — the paged request in the report is ListView's (plugin-list), identified by its $select projection and $top, which ObjectView's non-grid fetch does not emit. ObjectView (plugin-view) delegates to ListView through renderListView and skips its own fetch when it does. The unbounded one is ObjectGantt.reload.
  • "the footer is rendered by shared paging chrome" — CONFIRMED, and it decided the width: one block in ListView, rendered for every view type, correct on all of them but gantt.
  • "@objectstack/rest applies no cap when limit is absent" — NOT MEASURED here. Out of reach from this repo; nothing in this change depends on it. What is measured is one layer up: the request carries no $top, so no page size authored on the view can bound it.
  • "the chart consumes the unbounded one" — CONFIRMED, reproduced as row counts in the harness rather than inferred.

Not touched, deliberately

Reverse verification

Direction and counts predicted before running: drop surfaceDrawsFetchedRows from the bar's render condition and exactly the two gantt cases go red while both controls stay green — 2 failed / 2 passed. Observed exactly that, then 4 passed after restore.

Mutation proved on disk: removed-token count 2 to 1, mutated-line count 0 to 1, blob hash 4a1ce3c1 to 7f55c73b. Restore proved by state: git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to 4a1ce3c1, the HEAD blob.

Verification (all at f3d7b3cd3)

  • pnpm exec vitest run packages/plugin-list/ packages/plugin-gantt/113 files passed, 1183 tests passed.
  • 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. Both new test files proved present in the tsconfig.test.json program via tsc --listFiles (1 hit each), so "typecheck is clean" actually covers them.
  • Gates: 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 (46/46 packages, 0 errors), and all four changeset guards — every one exit 0. check:sdui-registration-pins returned exit 2, PRECONDITION NOT MET (no apps/console build to weigh) — recorded as NOT MEASURED, not as a pass; this diff adds no registration and moves no entry array. Left to CI.
  • ESLint, narrowed and declared: 3 changed files, 0 errors (--format json, file count read from that output, not guessed). The narrowing is a measurement rather than a skip because eslint.config.js enables no type-aware linting (no project / projectService in its languageOptions), so no untouched file's verdict can move because of this diff, and the diff adds no config and no inline directive.

Generated by Claude Code


Generated by Claude Code

The bar reports ListView's own paged query — the row count, the "Showing
first N records" warning tied to `$top: pageSize`, and the rows-per-page
selector that re-issues it. Every other view type draws the `data` ListView
hands down, so there the bar is accurate. The registered `object-gantt`
renderer forwards no prop but `schema`, so the chart queries for itself with
no `$top`: measured at 18 rows / pageSize 6, the chart drew 18 while the bar
read "6 records - Showing first 6 records. More data may be available."
Scoped to gantt; grid and kanban controls pin that the shared chrome is
untouched elsewhere. The gantt's own query is left exactly as it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C-Qg7c4O.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.22KB56.04KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.24KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. Reviewer of record: domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM

Verified over git: clean against origin/main3561bd2ca, and clean against #7214's queue build (which main is about to become), and clean against #7220. Changeset present. The functional change is one named constant and one added conjunct in a render condition.

Half 3's condition failed, and stopping was the right answer

The dispatch made the dedupe conditional: measure whether the paged request has any consumer besides the footer, and if it has others, stop rather than pick a filter dialect. It has five, two of them behavioural — data gates the loading skeleton and the load-error panel (so it decides whether the chart mounts at all), feeds UserFilters' 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. That is the observation that makes suppression safe and removal wrong.

So nothing was deduped. That is a full success against the order as written, not a partial delivery.

My premise was wrong again: three requests, not two

The order said "That exactly two requests are issued. Count them yourself." The answer is three — one paged, and two unbounded, ObjectGantt.reload firing before and after its own object-schema read settles, the second adding $expand. Neither unbounded call carries $top.

Third PM premise falsified by a lane this shift, and again only because the order required checking my reading rather than the card's.

The root cause is the valuable find

ObjectGanttRenderer destructures { schema } and forwards schema and dataSource only — the only view wrapper that does not spread host props; grid, kanban, calendar, map and tree all do. So ListView's data never reaches the chart, ObjectGantt queries for itself, and the double fetch is invisible on a read-through of ListView because the live comment there claims the data prop "short-circuits the renderer's own fetch". That comment is false, and its falseness is exactly what hid this. Filed as #7222.

Suppression over re-description was the right of the two permitted shapes

The ruling allowed either. Suppression was chosen because re-description would need a new upward channel from ObjectGantt to ListView whose meaning would change if a ceiling is ever added — i.e. it would bet on half 2's outcome. Suppression is invariant to it. That is precisely the property the split was made for, and the docblock says so in place.

And it correctly refuses the tempting wrong fix: forwarding data to 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.

Scoping is pinned by controls, not by assertion

The chrome is shared — one block rendered for every viewType — and is correct everywhere but gantt, so a naive edit would have broken four honest footers to fix one lying one. The harness (18 rows, pageSize: 6) is run three ways: gantt (bar reads 6, chart draws 18 — the defect), grid (reads 18, the server total, no warning), and kanban (reads 6, and the board really is drawing those 6). The two controls are what demonstrate the scoping rather than claim it, and both are in the test file.

Reverse verification: predicted 2 failed / 2 passed, observed exactly that, restore proven by state plus blob equality with the mutation proven on disk before any run output was read.

check:sdui-registration-pins returned exit 2 with its own "This is exit 2, not a pass" banner and was recorded as NOT MEASURED rather than as a colour. That is the distinction this seat cares most about, and the gate itself was quoted rather than paraphrased.

One cosmetic nit, non-blocking

The docblock writes the renderer as < ObjectGantt schema={bound} dataSource={dataSource} / > with spaces inside the brackets. That defence is for GitHub issue and PR bodies, where an unclosed < eats the rest of the body — it is not needed in a source comment, where the spacing just costs a little readability. Harmless; fix on the next touch if convenient.

PM follow-ups accepted

Armed.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw - #7223

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome
Sep 1, 2026
Merged

fix(plugin-list): a gantt view no longer shows a record-count bar that describes a request it does not draw#7223
os-warren merged 1 commit into
mainfrom
claude/issue-7210-gantt-paging-chrome

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #7210 — half 1 only. Halves 2 and 3 stay open on that card, deliberately; see "Half 3" below.

What changed

ListView renders one record-count bar for every viewType: the row count, the "Showing first N records. More data may be available." warning tied to its own $top: pageSize query, and a rows-per-page selector that re-issues that query. It is now suppressed on gantt alone, behind a named constant (surfaceDrawsFetchedRows) whose doc comment carries the measurement and the reason.

One line of behaviour, one guard, two controls, plus a pin in plugin-gantt for the fact the guard rests on.

Why suppression rather than "make it describe the chart"

The ruling allowed either. ListView cannot report the chart's row count without a new upward channel from ObjectGantt — more published surface, and a channel whose meaning changes if a ceiling is ever added. Suppression is invariant to that decision, which is the property the split was made for. None of the bar's three parts describes the chart: not the count, not the warning, and not the page-size selector, which re-issues a query the chart ignores.

Measured, not inherited

Everything below was measured in this branch. Nothing was copied from the report.

Request count — the order said "count them yourself", and the answer is not two. Harness: one list-view of viewType: 'gantt', 18 rows in the store, pagination.pageSize: 6, instrumented adapter recording every find.

3 find calls
1. $top: 6, $select: [...], authored filter array -- ListView's own paged query
2. no $top, no $select, no $expand -- ObjectGantt, before its schema read settles
3. no $top, no $select, $expand: [owner] -- ObjectGantt, after it settles

Calls 2 and 3 are one defect the reporter's browser capture read as one request: the second/third split is the "two queries, the first unexpanded" regime already recorded for ObjectGantt on #6482, which is no longer open; re-confirmed here. Only call 1 has a $top at all.

The chart consumes the unbounded fetch. The DOM reading reproduced in the harness's own terms: the paged query answered 6 rows, and the chart rendered 18 gantt-task-row-* rows while the bar under it read 6 records - Showing first 6 records. More data may be available. That is the defect in miniature, one screen, both halves visible.

The chrome is shared, so the change is scoped. Same harness, same store, only viewType differing:

viewTypefind callsrecord-count barhonest?
gantt36 records - Showing first 6 records... while the chart drew 18no
grid118 records (server total, no warning)yes
kanban16 records - Showing first 6 records..., and the board drew 6yes

kanban is the load-bearing control: a non-grid, page-scoped surface whose warning is correct and had to survive.

The mechanism.plugin-gantt/src/index.tsx's ObjectGanttRenderer destructures { schema } and hands its child only schema and dataSource. Every sibling wrapper — grid, kanban, calendar, map, tree — spreads {...props}. So the dataListView passes never reaches the chart, ObjectGantt.reload's rest.data short-circuit is unreachable from the registry path, and the component queries for itself with no $top. Pinned in packages/plugin-gantt/src/ObjectGantt.hostDataProp-7210.test.tsx.

Half 3 — condition FAILED, so both requests stay

The order was explicit: measure whether the paged request has any consumer other than the footer; if it does, stop and report rather than pick a dialect. It does. Reads of ListView's data on the gantt surface:

lineconsumer
ListView.tsx:2801client-side CSV / JSON export slices data
ListView.tsx:2938UserFilters data={data} — option counts (showCount)
ListView.tsx:3573refresh indicator
ListView.tsx:3583load-error panel vs. rendering the view
ListView.tsx:3630loading skeleton vs. rendering the view
ListView.tsx:3841the record-count bar (this PR)

The last two are behavioural, not cosmetic, and one of them was measured directly: with the paged response delayed 600ms and the unbounded one immediate, at t=250ms the skeleton was up and the chart had 0 rows on screen although its own data had already arrived. The paged query decides whether the chart mounts at all. Removing it is not a no-op, so no dialect was picked and nothing was deduped.

Falsified ZONE 2 assumptions

  • "exactly two requests" — three (see above). One paged, two unbounded.
  • "the fetch path is ObjectView's non-grid list-view path" — the paged request in the report is ListView's (plugin-list), identified by its $select projection and $top, which ObjectView's non-grid fetch does not emit. ObjectView (plugin-view) delegates to ListView through renderListView and skips its own fetch when it does. The unbounded one is ObjectGantt.reload.
  • "the footer is rendered by shared paging chrome" — CONFIRMED, and it decided the width: one block in ListView, rendered for every view type, correct on all of them but gantt.
  • "@objectstack/rest applies no cap when limit is absent" — NOT MEASURED here. Out of reach from this repo; nothing in this change depends on it. What is measured is one layer up: the request carries no $top, so no page size authored on the view can bound it.
  • "the chart consumes the unbounded one" — CONFIRMED, reproduced as row counts in the harness rather than inferred.

Not touched, deliberately

Reverse verification

Direction and counts predicted before running: drop surfaceDrawsFetchedRows from the bar's render condition and exactly the two gantt cases go red while both controls stay green — 2 failed / 2 passed. Observed exactly that, then 4 passed after restore.

Mutation proved on disk: removed-token count 2 to 1, mutated-line count 0 to 1, blob hash 4a1ce3c1 to 7f55c73b. Restore proved by state: git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to 4a1ce3c1, the HEAD blob.

Verification (all at f3d7b3cd3)

  • pnpm exec vitest run packages/plugin-list/ packages/plugin-gantt/113 files passed, 1183 tests passed.
  • 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. Both new test files proved present in the tsconfig.test.json program via tsc --listFiles (1 hit each), so "typecheck is clean" actually covers them.
  • Gates: 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 (46/46 packages, 0 errors), and all four changeset guards — every one exit 0. check:sdui-registration-pins returned exit 2, PRECONDITION NOT MET (no apps/console build to weigh) — recorded as NOT MEASURED, not as a pass; this diff adds no registration and moves no entry array. Left to CI.
  • ESLint, narrowed and declared: 3 changed files, 0 errors (--format json, file count read from that output, not guessed). The narrowing is a measurement rather than a skip because eslint.config.js enables no type-aware linting (no project / projectService in its languageOptions), so no untouched file's verdict can move because of this diff, and the diff adds no config and no inline directive.

Generated by Claude Code


Generated by Claude Code

The bar reports ListView's own paged query — the row count, the "Showing
first N records" warning tied to `$top: pageSize`, and the rows-per-page
selector that re-issues it. Every other view type draws the `data` ListView
hands down, so there the bar is accurate. The registered `object-gantt`
renderer forwards no prop but `schema`, so the chart queries for itself with
no `$top`: measured at 18 rows / pageSize 6, the chart drew 18 while the bar
read "6 records - Showing first 6 records. More data may be available."
Scoped to gantt; grid and kanban controls pin that the shared chrome is
untouched elsewhere. The gantt's own query is left exactly as it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C-Qg7c4O.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.22KB56.04KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.24KB27.61KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. Reviewer of record: domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM

Verified over git: clean against origin/main3561bd2ca, and clean against #7214's queue build (which main is about to become), and clean against #7220. Changeset present. The functional change is one named constant and one added conjunct in a render condition.

Half 3's condition failed, and stopping was the right answer

The dispatch made the dedupe conditional: measure whether the paged request has any consumer besides the footer, and if it has others, stop rather than pick a filter dialect. It has five, two of them behavioural — data gates the loading skeleton and the load-error panel (so it decides whether the chart mounts at all), feeds UserFilters' 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. That is the observation that makes suppression safe and removal wrong.

So nothing was deduped. That is a full success against the order as written, not a partial delivery.

My premise was wrong again: three requests, not two

The order said "That exactly two requests are issued. Count them yourself." The answer is three — one paged, and two unbounded, ObjectGantt.reload firing before and after its own object-schema read settles, the second adding $expand. Neither unbounded call carries $top.

Third PM premise falsified by a lane this shift, and again only because the order required checking my reading rather than the card's.

The root cause is the valuable find

ObjectGanttRenderer destructures { schema } and forwards schema and dataSource only — the only view wrapper that does not spread host props; grid, kanban, calendar, map and tree all do. So ListView's data never reaches the chart, ObjectGantt queries for itself, and the double fetch is invisible on a read-through of ListView because the live comment there claims the data prop "short-circuits the renderer's own fetch". That comment is false, and its falseness is exactly what hid this. Filed as #7222.

Suppression over re-description was the right of the two permitted shapes

The ruling allowed either. Suppression was chosen because re-description would need a new upward channel from ObjectGantt to ListView whose meaning would change if a ceiling is ever added — i.e. it would bet on half 2's outcome. Suppression is invariant to it. That is precisely the property the split was made for, and the docblock says so in place.

And it correctly refuses the tempting wrong fix: forwarding data to 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.

Scoping is pinned by controls, not by assertion

The chrome is shared — one block rendered for every viewType — and is correct everywhere but gantt, so a naive edit would have broken four honest footers to fix one lying one. The harness (18 rows, pageSize: 6) is run three ways: gantt (bar reads 6, chart draws 18 — the defect), grid (reads 18, the server total, no warning), and kanban (reads 6, and the board really is drawing those 6). The two controls are what demonstrate the scoping rather than claim it, and both are in the test file.

Reverse verification: predicted 2 failed / 2 passed, observed exactly that, restore proven by state plus blob equality with the mutation proven on disk before any run output was read.

check:sdui-registration-pins returned exit 2 with its own "This is exit 2, not a pass" banner and was recorded as NOT MEASURED rather than as a colour. That is the distinction this seat cares most about, and the gate itself was quoted rather than paraphrased.

One cosmetic nit, non-blocking

The docblock writes the renderer as < ObjectGantt schema={bound} dataSource={dataSource} / > with spaces inside the brackets. That defence is for GitHub issue and PR bodies, where an unclosed < eats the rest of the body — it is not needed in a source comment, where the spacing just costs a little readability. Harmless; fix on the next touch if convenient.

PM follow-ups accepted

Armed.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-warren