Skip to content

feat(plugin-grid): type the data-table schema slot ObjectGrid fills, shown able to fail (#6459) - #6637

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-6459-datatable-schema-slot-typing
Aug 28, 2026
Merged

feat(plugin-grid): type the data-table schema slot ObjectGrid fills, shown able to fail (#6459)#6637
os-sales merged 2 commits into
mainfrom
claude/issue-6459-datatable-schema-slot-typing

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Part of #6459

What this does

packages/plugin-grid/src/ObjectGrid.tsx built its data-table schema as const dataTableSchema: any, so the ~46 keys assembled there — and the columns slot itself — were checked against nothing, while the imported DataTableSchema went unused as that object's annotation. This PR:

  • annotates the flat literal with a new seam type ObjectGridDataTableSchema;
  • annotates buildGroupTableSchema's return with the same type;
  • drops the (dataTableSchema.columns as any[]).map((c: any) => …) cast, which existed only because the surrounding value was any;
  • pins the instrument, its boundary, and the census holds in dataTableSchemaSlot-6459.test.ts (7 compile-time pins);
  • adds a minor changeset (matching PR precedent for the producer half of this seam).

Part of, not Fixes: the annotation landed and is shown able to fail (below), but the census leaves two keys that need a maintainer ruling on DataTableSchema itself, which is out of this card's fence.

The measured mechanism — why the annotation is NOT a bare DataTableSchema

The card's sizing note predicted a bare annotation would be inert through the spread in buildGroupTableSchema (excess-property checking is a freshness check). Measured on 38a123cac before shaping the fix, the truth is stronger — the spread is not even needed:

  • Probe M1 (bare annotation + bogus key, predicted inert, observed inert):const dataTableSchema: DataTableSchema with bogusKeyForProbe6459: true written longhand in the fresh literaltsc --noEmit exit 0, zero diagnostics.

The root cause is BaseSchema's [key: string]: any index signature, which DataTableSchema inherits: under an index signature every key is a member, so excess-property checking never has a non-member to refuse — at a fresh literal, through a spread, anywhere. This is the terminal case of the #6626 rule ("a pin enforced by a key's non-membership silently stops enforcing the moment the key becomes a member"): with an index signature there is no non-membership, ever.

The seam type strips the signature (RemoveIndexSignature applied to DataTableSchema — derived, never a hand-copied member list) and re-adds the two measured holds. That is what turns the annotation from inert to able-to-fail.

Shown able to fail (reverse verification, commit-first)

Both probes on the committed implementation; mutation proven on disk by anchored grep counts plus a blob-hash difference vs HEAD; restore proven by observation (git diff HEAD empty AND git hash-object = HEAD blob), never by exit code; both probe scripts carry trap restore EXIT INT TERM with absolute paths.

  • Probe R1 — bogus key longhand in the flat literal. Predicted RED; observed:
    src/ObjectGrid.tsx(3393,5): error TS2353: Object literal may only specify known properties, and 'bogusKeyForProbe6459' does not exist in type 'ObjectGridDataTableSchema'. (tsc exit 2)
  • Probe R2 — bogus key longhand in buildGroupTableSchema's annotated return literal. Predicted RED; observed:
    src/ObjectGrid.tsx(3592,5): error TS2353: … 'bogusGroupKeyProbe6459' does not exist in type 'ObjectGridDataTableSchema'. (tsc exit 2)

The instrument's boundary, stated and pinned rather than papered over: a bogus key riding a non-fresh value assigned into the slot is admitted — assignability admits extra members; that is structural typing. Both real writers are literals sitting directly in annotated positions, so the live mechanism at this seam is freshness — unlike the producer seam, where .map() laundered freshness away and only ?: never tombstones could bite. A key RETIRED by ruling still needs a per-key tombstone; an open census cannot be tombstoned, because a tombstone needs the key's name. Pinned as a MUST-COMPILE test so nobody mistakes this for tombstone-grade coverage.

The census — a REPORT for a ruling, not a declaration

Diffing every key the two literals write (46 flat + 8 group) against DataTableSchema + BaseSchema declared members, on 38a123cac. The card's speculative list (pagination, manualPagination, rowCount, frozenColumns, singleClickEdit, selectionResetKey, disableInnerScroll, borderless) has since been declared on DataTableSchema — the interface grew after the card was filed. Exactly two undeclared keys survive:

keyread by data-table?documented?if absent
renderCellEditorYesdata-table.tsx reads it via its own (schema as any).renderCellEditor cast and hands cell editing to the returned widget (the renderer itself acknowledges the key is undeclared)No schema-level docs; mentioned only in passing in a packages/types docblock as "the host's renderCellEditor"Inline editing falls back to the built-in text/number/date inputs — degraded but functional
cellClassName (schema-level)Yes — destructured off the schema and folded into every body cell's className (three call sites; the column-level twin IS declared, on TableColumn)No schema-level docs (docs cover only the column-level key)The grid's row-height density styling stops reaching cells — silent visual regression, no error

⛔ Deliberately NOT done here, per the dispatch fence: declaring either key on DataTableSchema (that is packages/types — published, human floor, and currently held by PR #6627), or retiring either (both have live readers; removal is a behaviour change). Both are HELD at the seam (ObjectGridDataTableSchemaHolds), visible instead of anonymous, until the ruling. The open question for the ruling is whether DataTableSchema should declare them (and document them), or whether they should remain seam-level holds.

Census side-observation, recorded for completeness: caption: schema.label || schema.title reads title off ObjectGridSchema through the same BaseSchema index signature (ObjectGridSchema declares no title), so that read is equally invisible to any key check on the grid's own authoring surface. Out of this card's fence.

Verification

  • pnpm --filter @object-ui/plugin-grid type-check — both programs (tsc --noEmit and tsc -p tsconfig.test.json) green; the test program's --listFiles names dataTableSchemaSlot-6459.test.ts (1 hit — verified, not assumed).
  • New pin suite at runtime, repo-root invocation: pnpm exec vitest run packages/plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts → 1 file, 7/7 passed.
  • Targeted eslint on both changed files: exit 0 (0 errors; 217 pre-existing warnings in ObjectGrid.tsx, none on the added lines).
  • node scripts/check-changeset-presence.mjs → 1 released-package source file changed, 1 changeset declared. check-changeset-no-major green (minor). check:control-bytes green.
  • Repo-root vitest union at 2df78a8b (the final commit), run as 16 sequential --shard=N/16 invocations of the root pnpm test (CI runs the same whole-repo suite in 4 shards):
    • 16/16 shards command-exit 0 (per the shared verify-lock wrapper's own verdict lines, never a piped $?); 2132 test files passed, 26859 tests passed, 1 skipped (the skip pre-exists on main, shard 3).
    • Shard tallies (files / tests): 134/1797 · 134/1494 · 134/1391+1s · 134/2535 · 133/1877 · 133/1396 · 133/1451 · 133/1824 · 133/1775 · 133/1600 · 133/1547 · 133/2067 · 133/1819 · 133/1253 · 133/1551 · 133/1482.
    • Observation, not a failure: 12 of the 16 green shard logs carry stray ECONNREFUSED 127.0.0.1:3000 stack traces on stderr while every file in them passes — connection-refusal paths under test printing unhandled noise. Recorded because the noise is easy to misread as a red gate when a run is killed early.

Worktree cleaned after PR creation. No service was started; nothing listens on any port from this task.


Generated by Claude Code

…6459)
`const dataTableSchema: any` becomes `ObjectGridDataTableSchema` =
RemoveIndexSignature<DataTableSchema> & the two measured schema-level holds
(renderCellEditor, cellClassName); buildGroupTableSchema's return is annotated
with the same type and the `(dataTableSchema.columns as any[])` cast drops.
Measured on 38a123c before choosing the shape: a bare `DataTableSchema`
annotation with an undeclared bogus key written longhand in the fresh literal
compiles with ZERO diagnostics — BaseSchema's `[key: string]: any` makes every
key a member, so excess-property checking never has a non-member to refuse.
Stripping the index signature is what makes the annotation able to fail.
Part of #6459
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
…, and the census holds
Seven compile-time pins in dataTableSchemaSlot-6459.test.ts: the bare
`DataTableSchema` annotation is blind (index signature admits a bogus key at a
fresh literal — MUST COMPILE), the strip is real (`string` no longer indexes
the seam type), the seam type refuses longhand bogus keys at both writer
shapes (fresh literal; spread + longhand in an annotated return), a non-fresh
value still smuggles (assignability — the pinned boundary), the two HELD keys
are accepted, and declared members survive the strip with modifiers intact.
Plus the changeset (minor, matching #6004's precedent).
Part of #6459
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

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.3 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-DZpGdrcH.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)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
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)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
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.95KB10.97KB
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)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
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)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.46KB34.48KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.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)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
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)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
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-sales
os-sales marked this pull request as ready for review August 28, 2026 07:57
@os-sales
os-sales added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit f241a4dAug 28, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6459-datatable-schema-slot-typing branch August 28, 2026 08:11
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.

2 participants

@os-sales@claude