Skip to content

fix(types): ObjectViewSchema table/form slots ship the members they promise - #6399

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-6269-objectview-slot-key-lists
Aug 25, 2026
Merged

fix(types): ObjectViewSchema table/form slots ship the members they promise#6399
os-support-ai merged 2 commits into
mainfrom
claude/issue-6269-objectview-slot-key-lists

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#6269

⚠️ Generic parameters are written spaced (Omit< T, K >) throughout — GitHub's body
sanitizer eats a fragment that opens with an identifier in angle brackets, which is what
mangled #6269's first revision. Source code is unaffected; this is about the prose here.

What was wrong

ObjectViewSchema's two slots were declared by deriving from the schema they document:

packages/types/src/objectql.ts:1490 table?: Partial< Omit< ObjectGridSchema, 'type' | 'objectName' > >;
packages/types/src/objectql.ts:1496 form?: Partial< Omit< ObjectFormSchema, 'type' | 'objectName' | 'mode' > >;

Both derived types declared zero properties. Omit< T, K > is
Pick< T, Exclude< keyof T, K > >, and keyof T on a type carrying a string index signature
is string | number — the literal member names are absorbed. Both source schemas inherit
BaseSchema's [key: string]: any (#5155), so each Pick rebuilt a type holding the index
signature and none of the named members.

Measured through the TypeScript checker against the emitted declaration, on unmodified
origin/main:

ObjectGridSchema -> 61 declared properties
Omit< ObjectGridSchema, 'type' | 'objectName' > -> 0 declared properties
ObjectFormSchema -> 67 declared properties
Omit< ObjectFormSchema, 'type' | 'objectName' | 'mode' > -> 0 declared properties

This is #6151's collapse in property position. Confirmed against the tree that #6151's
guard does not reach it: packages/types/src/__tests__/stack-schema-emitted-members.test.ts
walks the LayoutSchemaunion, and these two are properties on ObjectViewSchema, not
union members.

The repair — triage's option 2

Each Omit became a PartialPick over an explicit key list. Pick with literal keys
never computes keyof T, so it cannot collapse the same way.

  • ObjectGridSlotKey — 59 keys: every ObjectGridSchema member minus type, objectName.
  • ObjectFormSlotKey — 64 keys: every ObjectFormSchema member minus type, objectName,
    mode.

After the change, the same checker reading gives table59 and form64 declared
properties, every member optional (the Partial survived), and no string index signature
on either slot — which is what re-enables excess-property checks.

The duplicate-list hazard, neutralised

A hand-maintained key list drifts silently the moment someone adds a member to the source
schema. packages/types/src/__tests__/object-view-slot-key-lists.test.ts is the
checker-driven pin triage ruled: it emits the package's own declarations through the compiler
API and asserts each slot's member set equals the source schema's declared members minus
the identity keys. Same instrument that produced the 61 → 0 reading; set equality, not a spot
check.

The pin's comment records the removal condition triage asked for: these Pick lists exist
only because BaseSchema carries a root string index signature, so when a #5155 phase removes
it, Omit stops collapsing and the lists — plus this whole pin — become removable. One of the
pin's assertions (declaresStringIndex on the source schemas) is the tripwire that will
notice.

Ghost-assertion guard — both readings

The pin was run against unmodified origin/main for
packages/types/src/objectql.ts (the fix reverted from a committed state,
git checkout origin/main -- <path>, restored with git checkout HEAD -- <path>; restore
proven by an empty git diff HEAD and a matching git hash-object / git rev-parse HEAD:<path> pair):

treeresult
unmodified origin/mainTest Files 1 failed (1) · Tests 16 failed | 6 passed (22)
this branchTest Files 1 passed (1) · Tests 22 passed (22)

The failure on main is the measurement itself, not a proxy:

FAIL … > ObjectViewSchema.'table' ships 'ObjectGridSchema'’s configuration (objectui#6269)
> declares EXACTLY the source members minus the identity keys the view fixes
AssertionError: expected [] to deeply equal [ 'aggregations', 'ariaLabel', …(57) ]
FAIL … > declares NO string index signature — that is what re-opens the defect
AssertionError: expected true to be false

The 6 that still pass on main are the degenerate control — the source schemas still
report 61 / 67 members and still carry the #5155 index signature. That is exactly their job: a
pin reading only the derived type would pass if both sides collapsed to zero.

Degenerate control — re-derived, not inherited

ObjectGridSchema 61 and ObjectFormSchema 67 were measured in this pass through the
checker, not carried over from the card. Both are asserted explicitly, alongside a
declaresStringIndex assertion on each source schema.

Forward-drift case — proved

A member (driftProbe6269?: string) was temporarily added to ObjectGridSchema and left out
of the key list. Mutation confirmed on disk before the run (grep for the injected identifier
= 1 occurrence, 0 occurrences inside the key list; worktree blob hash b47b9da9… against the
HEAD blob 3ebeefac…), the run captured, the probe removed under an EXIT/INT/TERM
trap and the restore proven by a matching blob hash and zero residue:

Tests 2 failed | 20 passed (22)
- "disabledOn",
- "driftProbe6269", ← expected (recomputed from the source schema)
+ ← received (absent from the slot)

Both the set-equality assertion and the degenerate control red, which is the right
shape: a deliberate member addition has to be acknowledged in two places.

Blast radius — measured, zero breakages

Restoring named members re-enables excess-property checks on object literals assigned into
these slots, so table: { colunms: 3 } is now an error. That is the intent. What it actually
costs in this tree:

  • Repo-wide turbo run type-check: 81 successful, 81 total, 0 error TS — after a full
    turbo run build (43 successful, 43 total) so every consumer resolves the new.d.ts
    rather than a stale one.
  • check:doc-snippets: 267 of 267 covered blocks judged, 0 failed, compiled against the
    built types — that is the gate covering the 13 slot literals in
    content/docs/plugins/plugin-view.mdx.
  • Literal sweep: 42 slot literals inside object-view schema literals, across 71
    candidate files — 23 in .tsx, 6 in .json schema-catalog fixtures, 13 in .mdx docs.
    The 16 distinct keys they use between them (columns, defaultFilters, defaultSort,
    fields, filter, pageSize, pagination, selection, sort; sections, showCancel,
    showSubmit, subforms, submitText, plus columns/fields on the form side) are all
    in the new key lists. Zero legitimate usages break, so there is no list for the PM to
    adjudicate here.
  • No index signature was added back anywhere; the pin asserts its absence on both slots.

Also measured in this pass, as the card asked: PartialSchema< T >

packages/types/src/index.ts:905PartialSchema< T extends BaseSchema >, the generic shape
the card flagged as "not measured, and worth checking". It collapses too, for every
instantiation tried:

PartialSchema< ObjectGridSchema > -> 1 declared property: type (source: 61)
PartialSchema< ObjectFormSchema > -> 1 declared property: type (source: 67)
PartialSchema< ObjectViewSchema > -> 1 declared property: type
PartialSchema< ButtonSchema > -> 1 declared property: type (source: 27)

All four keep a live [key: string]: any. The { type: T['type'] } half of the intersection
is the only reason the count is 1 rather than 0; the & Partial< Omit< T, 'type' > > half
contributes nothing.

Not fixed here, per the dispatch order. Reported and proposed instead: filed as #6397
with the measurements, and it also carries the fact that decides it — PartialSchema has
zero consumers, in or out of packages/types. Grepped across packages/, apps/,
examples/, content/, docs/: two occurrences of the identifier, both the declaration and
its own emitted copy. My recommendation on that card is retirement (option 1) or sequencing
behind #5155 (option 3) — a repair in the #6269 style is not portable, because T is generic
and there is no literal key list to write.

Verification

All commands run at repo root. Green union measured on 642b91f87, which is this branch's
final commit (the merge of origin/main taken after the fix commit).

commandresult
pnpm exec turbo run build --filter="!@object-ui/site" --concurrency=2Tasks: 43 successful, 43 total
pnpm exec turbo run type-check --concurrency=2Tasks: 81 successful, 81 total, 0 error TS
pnpm exec vitest run packages/types/ packages/plugin-view/Test Files 85 passed (85) · Tests 935 passed (935)
pnpm exec vitest run packages/types/src/__tests__/object-view-slot-key-lists.test.tsTest Files 1 passed (1) · Tests 22 passed (22)
pnpm --filter @object-ui/types lint✖ 244 problems (0 errors, 244 warnings) — all pre-existing no-explicit-any; none in the added line range
pnpm run check:doc-types✅ Every documented component type is registered.
pnpm run check:doc-snippetsEvery covered documentation snippet compiles against the built types.
pnpm run check:control-bytes✅ check-control-bytes: OK (scanned 5282 tracked text file(s))
pnpm run changeset:check✅ No changeset declares a major bump.
node scripts/check-changeset-presence.mjs✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)

Declared narrowing.pnpm lint was run per-package (--filter @object-ui/types) rather
than repo-wide, and the repo-wide type-check / build / affected-package tests above were
measured on the fix commit; only the tests and the changeset/control-byte gates were re-run on
the origin/main merge head 642b91f87. The merge touched 45 files and none under
packages/types/, so the pin's inputs are byte-identical between the two commits. The lint
narrowing is a measurement rather than a gap on three counts: (1) the eslint population comes
from eslint's own flat config, and this diff touches exactly one package; (2)
--format json over the two changed files reports 2 files linted, 0 errors, 17 warnings,
all 17 at lines outside the added range (@@ -1430,0 +1431,165 @@ and the four slot-decl
hunks); (3) eslint.config.js declares no projectService, no parserOptions and no
project:, so linting is not type-aware and this type change cannot move the verdict on
any file the diff does not touch. CI runs the full farm regardless.

Changeset

.changeset/6269-objectview-slot-key-lists.mdpatch, @object-ui/types.


Draft, and staying draft — the PM lands it.

Generated by Claude Code


Generated by Claude Code

…romise
Both slots were declared by deriving from the schema they document:
table?: Partial<Omit<ObjectGridSchema, 'type' | 'objectName'>>;
form?: Partial<Omit<ObjectFormSchema, 'type' | 'objectName' | 'mode'>>;
and both derived types declared ZERO properties. `Omit<T, K>` is
`Pick<T, Exclude<keyof T, K>>`, and `keyof T` on a type carrying a string index
signature is `string | number` — the literal member names are absorbed. Both
source schemas inherit `BaseSchema`'s `[key: string]: any` (objectui#5155), so
each `Pick` rebuilt a type holding the index signature and none of the named
members. Measured through the TypeScript checker: ObjectGridSchema 61 members,
the Omit of it 0; ObjectFormSchema 67, the Omit of it 0.
Each `Omit` is now a `Partial<Pick<…>>` over an explicit key list — 59 keys for
table, 64 for form, every declared member minus the identity keys the view
fixes. `Pick` with literal keys never computes `keyof T`, so it cannot collapse.
The duplicate-list hazard is neutralised by a checker-driven pin that recomputes
each source schema's declared members and requires set equality with the slot,
so a member added to ObjectGridSchema and not to the list turns it red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3230.7 KB3266.6 KB
Main entry chunk (gzip)156.1 KB350 KB
Entry fileindex-9yiiG-P-.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.30KB4.28KB
app-shell (runtime-config.js)18.10KB6.51KB
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)505.84KB114.57KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.18KB47.97KB
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.66KB12.84KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.46KB34.48KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.05KB31.92KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.79KB54.60KB
plugin-kanban (index.js)52.87KB14.57KB
plugin-list (index.js)112.63KB27.45KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.70KB7.69KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
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)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
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)7.54KB2.63KB
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.87KB0.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-support-ai
os-support-ai marked this pull request as ready for review August 25, 2026 20:11
@os-support-ai
os-support-ai added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 6f81384Aug 25, 2026
28 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-6269-objectview-slot-key-lists branch August 25, 2026 20:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-support-ai@claude