Skip to content

refactor(plugin-grid): dismantle the emit-side fitContent cast, re-point the 6458 anti-vacuity control - #6734

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6424-dismantle-fitcontent-cast
Aug 29, 2026
Merged

refactor(plugin-grid): dismantle the emit-side fitContent cast, re-point the 6458 anti-vacuity control#6734
os-sales merged 1 commit into
mainfrom
claude/issue-6424-dismantle-fitcontent-cast

Conversation

@claude

@claude claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Part of #6424

Maintainer ruling 2026-08-29 (live director session batch 6, verbatim 「其他同意」) adopted option 甲 (declare) and ordered the (col as any).fitContent sites dismantled in the same PR. The declare half had already landed — headerIcon by PR #6615, fitContent by PR #6673 — and this branch verified that on origin/main rather than taking it on report:

packages/types/src/data-display.ts:369   headerIcon?: React.ReactNode;
packages/types/src/data-display.ts:386   fitContent?: boolean;
packages/types/src/zod/data-display.zod.ts:138   fitContent: z.boolean().optional()

The two data-table.tsx cast sites went with PR #6673 (they read col.fitContent directly now). This lands the remaining emit-side one.

What this buys, measured rather than asserted: nothing at the type level

The cast bought zero type safety, and that is a measurement from the TypeScript checker, not a reading of the source. applyColumnChrome is (col: any), so orderedColumns is any[], and the grouped-width loop widens it a second time with as any[]:

FOR-OF line 3666: binding 'col' : any  isAny=true
   iterated expression: orderedColumns as any[]
CAST-READ line 3675: (col as any).fitContent
   receiver 'col' BEFORE cast : any  isAny=true
   expression AFTER cast      : any  isAny=true
   => cast changes the type?  : false
CONTROL groupedColumnWidths line 3665: Record NAME-TO-VALUE  isAny=false  (expect isAny=false)

The control in the same query shape reports a non-any type, so isAny=true is a reading and not a stuck instrument. The only observable effect of the removal is at the lint layer: ObjectGrid.tsx per-file eslint warnings go 212 to 211, with the test file at 0/0 on both refs.

Line-number note for anyone tracking this on the card: the site is at :3675 on this ref. All three circulating numbers — :3418, :3587, :3606 — are stale.

The load-bearing half is the guard, and one inherited finding needed correcting

columnReadBoundary-6458.test.ts used this exact cast as anti-vacuity control 3 — its only proof that the scanner can find a real cast in the real file at real scale. Removing the cast alone turns it red, re-measured here before any edit:

AssertionError: expected [] to include 'fitContent'
Test Files  1 failed (1) | Tests  1 failed | 7 passed (8)

That control's own comment requires it to be re-pointed at another real cast and never deleted. The prior round recorded that there was no other candidate in the file and that executing the ruling literally would therefore require weakening a gate.

That finding is narrower than it reads, and re-measuring changed the answer. It is true only for the receiver col. Scanning the whole file with the guard's own regex shape generalised over the receiver finds 35 cast reads, 14 of them outside the guarded region. After the dismantling, 13 remain outside it, across six receivers:

by receiver: {"cols":1,"fieldDef":6,"schema":2,"base":2,"translated":1,"opt":1}
col-receiver casts outside region: 0

So a re-point is possible; only the scanner's hard-coded col stood in the way. That is this card family's own lesson landing on itself a third time — cast count is not the instrument, and neither is a cast census taken through a receiver-specific pattern.

The re-point, and why this anchor rather than one of the twelve others

The receiver becomes a parameter defaulting to col, so every bound assertion is unchanged: same regex construction, same region, same meaning. Control 3's anchor moves to (schema as any).hideRowHeightToggle.

Chosen on a principle, not merely moved: it is the only kind here that cannot expire silently. hideRowHeightToggle is a deliberately held non-authoring key (#5091) — a host-written switch that ComponentPropsMap['object-grid']'s strictObject refuses by name, so the cast is load-bearing rather than incidental — and it is independently pinned by gridNonAuthorKeys.test.tsx, in that file's key table and in a behavioural test. If it is ever retired, that guard goes red in the same run and names it. That is exactly the failure the old anchor had: fitContent was nothing else's premise, so dismantling it broke this control with no other signal — the silent-expiry shape this family keeps recording.

Control 2 gains a pair of assertions pinning that the receiver parameter is honoured rather than ignored, in both directions. Without them a receiver-blind pattern would leave control 3 passing while the bound assertions' scoping to col quietly became a fiction.

The re-pointed control proved able to fail

Both legs proved the mutation on disk by anchored text counts plus a moved blob hash, ran under trap ... EXIT INT TERM with absolute paths, restored via explicit git checkout HEAD -- ABSOLUTE_PATH, and proved the restore by observation (blob hash back to the HEAD blob and git diff HEAD empty) rather than by an editor's exit code.

D1 — retire the new anchor the way fitContent's was retired. Control 3 goes red:

expected [ 'maxInlineRowActions' ] to include 'hideRowHeightToggle'
Test Files  1 failed (1) | Tests  1 failed | 7 passed (8)

The returned array is non-empty, which is the part worth reading: the control is pinned to that specific key, not merely to "some cast on schema" — it would still fail if the anchor alone went.

D2 — make the scanner ignore its receiver parameter. Control 2 goes red by name, and so does a bound assertion, showing the exact failure mode the new pins exist to catch:

expected [ 'someKey' ] to deeply equal []
expected [ 'filter', 'scale' ] to deeply equal []
Test Files  1 failed (1) | Tests  2 failed | 6 passed (8)

A receiver-blind pattern drags unrelated casts into the guarded region's bound. Control 2 catches it first.

What was run

Everything heavy went through the shared verify lock; verdicts are quoted from its own VERDICT line, and gate exit codes were captured before any pipe. Union at final commit 93173fa7b:

  • pnpm --filter @object-ui/plugin-grid type-check (tsc --noEmit && tsc -p tsconfig.test.json, script name echoed so it is not a zero-match no-op) — no error TS lines, VERDICT command-exit 0
  • pnpm exec vitest run --maxWorkers=2 packages/plugin-grid/ from the repo root — Test Files 98 passed (98), Tests 897 passed (897)
  • pnpm --filter @object-ui/plugin-grid lint681 problems (0 errors, 681 warnings); per-file on the diff, ObjectGrid.tsx 212 to 211 warnings and the test file 0/0 on both refs
  • Closure built first (pnpm --filter '@object-ui/plugin-grid^...' build) because packages/types/dist did not exist in a fresh worktree — without it every type reading would have been NOT MEASURED
  • Gates, each quoting its own verdict: check:control-bytes (5584 tracked text files, so it saw the new changeset), check:vi-mock-specifiers, check:shell-escape-residue, check:phantom-deps, check:self-import, check:spec-symbols, check:esm-specifiers, check-changeset-presence (ruled one owed at EXIT=1 first, green after), check-changeset-no-major

Declared narrowing: the repo-wide lint and vitest farms were not run locally. The population is read from eslint's own config — turbo run lint over per-package eslint . units, so only plugin-grid's unit can move, and it ran in full. Type-aware linting is not enabled, so this diff cannot move any untouched file's verdict. CI runs the farm exactly once regardless.

Changeset: empty frontmatter, the gate's own explicit exemption, since nothing published changes behaviour.

Deliberately not in this PR

Generated by Claude Code


Generated by Claude Code

…int the 6458 control

Maintainer ruling 2026-08-29 (option 甲) completes its declare branch: with
`TableColumn` declaring `fitContent` since PR #6673, the `(col as any).fitContent`
read in `ObjectGrid`'s grouped-width pass loses its cast.

What that buys is measured rather than assumed: nothing at the type level. The
receiver is `any` either way — `applyColumnChrome` is `(col: any)`, so
`orderedColumns` is `any[]` and the loop widens it a second time. The TypeScript
checker reports `isAny=true` for the receiver both before and after the cast,
against a control in the same query shape that reports a non-`any` type.

The load-bearing half is the guard. `columnReadBoundary-6458.test.ts` used that
cast as anti-vacuity control 3 — its only proof that the scanner can find a real
cast in the real file at real scale — and that control's own comment requires it
to be re-pointed at another real cast and never deleted. Its scanner hard-coded
the receiver to `col`, and the dismantling left zero `col` casts outside the
guarded region, so the receiver becomes a parameter (defaulting to `col`, leaving
every bound assertion unchanged) and the anchor moves to
`(schema as any).hideRowHeightToggle`.

That anchor is picked on a principle, not merely moved: a deliberately held
non-authoring key (objectui#5091) that `gridNonAuthorKeys.test.tsx` independently
pins, so it cannot expire silently the way `fitContent` just did. Control 2 gains
assertions pinning that the receiver parameter is honoured, so the bound
assertions' `col` scoping cannot quietly become a fiction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 49 chunks) 3233.1 KB 3266.6 KB
Main entry chunk (gzip) 157.3 KB 350 KB
Entry file index-Bq7D31z7.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 511.66KB 116.30KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 239.31KB 60.18KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.89KB 12.91KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.44KB 34.48KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.29KB 62.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.01KB 32.23KB
plugin-gantt (index.js) 165.20KB 40.37KB
plugin-grid (index.js) 201.53KB 54.54KB
plugin-kanban (index.js) 53.11KB 14.62KB
plugin-list (index.js) 113.11KB 27.58KB
plugin-map (index.js) 20.17KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 26.44KB 7.59KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.87KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 67.73KB 22.54KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants