Skip to content

fix(types,plugin-detail): stop publishing the retired owner field type (#4914) - #4932

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-4914-owner-contract-twins
Aug 17, 2026
Merged

fix(types,plugin-detail): stop publishing the retired owner field type (#4914)#4932
os-steve merged 1 commit into
mainfrom
claude/issue-4914-owner-contract-twins

Conversation

@os-steve

@os-steve os-steve commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Part of #4914 — the dispatched fast-follow scope only: items 1-3 (the published contract twins) + item 5 (InlineFieldInput). Items 4 and 6-15 stay recorded on that card, so this must not close it.

Follows #4814 / PR #4915 (merged 05:38Z), which retired the owner field-type spelling under ruling A′ and gave it a loud tombstone. That PR shrank the three public doc unions; this one shrinks their code twins, so the types package and the renderer finally agree.

Premise check (all four faces re-verified on this branch, post-#4915)

All four measured faces were present exactly as reported. Nothing was already gone, nothing had moved.

face measured
packages/types/src/zod/reports.zod.ts:49 runtime Zod enum still accepted type: 'owner'
packages/types/src/reports.ts:105 ReportField['type'] still offered it
packages/types/src/field-types.ts:584 UserFieldMetadata.type was 'user' | 'owner'
packages/plugin-detail/src/InlineFieldInput.tsx:107,290 routing set + isUserRef branch both live

#2231 / #2890 overlap check (owed by the claim): no collision. Both cards scope packages/types/src/zod/objectql.zod.ts (the ListViewSchema legacy-vocabulary phases) and zod/views.zod.ts (DetailViewSchema). Neither names reports.zod.ts, reports.ts or field-types.ts. The #2890 phase list (fieldscolumns, densityModerowHeight, the show* family, filtersfilter, objectNamedata.provider) touches no line this PR edits.

What changed

@object-ui/types — the three twins shrink. The Zod enum is the half that mattered most: it is a published runtime validator, so the contradiction was executable, not merely advisory — a report document authored with type: 'owner' validated green and then rendered a refusal. The two TS unions were the .d.ts autocomplete half, i.e. the surface an AI author copies from. Each carries a retirement comment naming the card and its twin, in the style PR #4915 used on bulkParamToField / paramToField.

@object-ui/plugin-detail — inline edit joins the tombstone. A retired spelling now renders the same RetiredFieldTombstone the record form does, read live from @object-ui/fields' RETIRED_FIELD_TYPES (never copied), reported once per spelling rather than once per row. Placed after the widget-hint branch, mirroring the form's own precedence.

The finding that changed the shape of this fix

The dispatch framed item 5 as "remove the two members, then make sure the removal is not silent". Measured, removing the two members alone changes nothing an author could see:

hasFieldEditWidget('owner') === true

packages/fields/src/FieldEditWidget.tsx:85 still maps owner: UserField in EDIT_WIDGETS, and resolveInlineEditType returns a type unchanged when it is already a key — so the alias table is never consulted and the retirement never applies. With the routing member deleted, the type simply reaches the same person picker down the delegation road instead of the routing road.

Verified directly during reverse verification (refusal branch ablated, both deletions in place): tombstone: false, plainTextInput: false, one element with role=button reading Select…. So the refusal branch is the only behavior-changing half; the two deletions are hygiene that moves in lockstep with it. Both source comments say this, because the naive reading is wrong in a way that would mislead the next editor.

EDIT_WIDGETS:85 is not one of the faces #4914 enumerates (that card lists FieldEditWidget.tsx:170, the cosmetic COMPACT_EDIT_TYPES set). Filed as #4931, a sub-issue of #4914 — out of this dispatch's file surface, untouched here. InlineFieldInput itself is fully protected regardless, since the refusal runs before both roads; the exposed consumer is the grid's inline cell editor.

Tests

  • packages/types/src/__tests__/owner-retired-contract-twins.test.ts — the Zod half pinned by refusal envelope (exactly one issue, type path, invalid_value, offered vocabulary lacks owner and still has user), not a bare success === false; the survivor idiom { type: 'user', name: 'owner' } pinned by a full green parse; a sibling-vocabulary check so the shrink cannot over-reach. The two TS halves pinned with @ts-expect-error, real enforcement here because packages/types/tsconfig.test.json is chained from type-check.
  • packages/plugin-detail/src/__tests__/InlineFieldInput.retiredFieldType.test.tsx — the refusal envelope (testid, role=alert, data-retired-field-type, the prescription text), both wrong outcomes pinned (a working picker and a quiet text box), once-per-spelling console dedupe, an expanded-value case, user unaffected, an unknown type left on its pre-existing quiet fallback, and a structural check quantified over the whole RETIRED_FIELD_TYPES table so the next retirement is covered the day it lands.
  • inlineEditTypeCoverage.test.tsx — comment refreshed; it explicitly said the routing set "still lists owner … not forced away here", which this PR makes false.

One deliberate non-assertion, stated because it looks like an omission: the test does not pin hasFieldEditWidget('owner') === true in either direction. That asymmetry belongs to #4931, and a pin on today's value would go red on the very fix that card proposes.

Verification, at final commit ddbf4d92e

Repo-root path-filter vitest; heavy runs under flock -E 99 -w 540 /tmp/os-heavy-verify.lock; dependency closures built before any type-check verdict.

packages/types/ + packages/plugin-detail/   Test Files 119 passed (119)   Tests 1230 passed (1230)
@object-ui/types type-check                 tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json -> Done
@object-ui/plugin-detail type-check         tsc --noEmit && tsc -p tsconfig.test.json -> Done
check:control-bytes    OK (4413 tracked text files)
check:doc-types        OK (143 mdx, 632 code blocks, 558 type literals)
check:self-import      OK      check:phantom-deps  OK
check:published-dist   OK (39 published packages)
changeset:check        OK

Downstream consumer sweep — this is a contract narrowing, so the dependents direction is the one that matters: pnpm --filter '...@object-ui/types' build then type-checkall green, covering plugin-report (the real ReportField consumer), plugin-view, plugin-list, plugin-designer, app-shell, apps/console, apps/site, examples/schema-catalog, examples/console-starter and the rest. No consumer was writing type: 'owner'; a repo-wide sweep for report fixtures spelling it found zero outside the fields package's own tombstone tests.

Reverse verification — direction predicted first, both legs from the committed state

Leg A (restore 'owner' to all three type faces). Predicted: Zod pin red on the refusal test; the two TS pins red at type-check, not vitest, and in the inverted shape — the expected error disappears rather than an illegal value being accepted. Observed exactly that:

Tests  1 failed | 4 passed (5)        # expect(result.success).toBe(false) -> received true
owner-retired-contract-twins.test.ts(104,7): error TS2578: Unused '@ts-expect-error' directive.
owner-retired-contract-twins.test.ts(119,7): error TS2578: Unused '@ts-expect-error' directive.

Leg B (drop the refusal branch, keep both deletions). Predicted: the retired field reaches a working person picker via delegation, so the tombstone assertions redden while the plain-text-input assertion stays green — the delegation road, not a text box. Observed:

Tests  4 failed | 6 passed (10)
AssertionError: expected [ one button element ] to have a length of +0 but got 1
probe: { tombstone: false, plainTextInput: false, buttons: 1, firstButtonText: "Select…", anyInput: false }

Both legs restored byte-identically via git checkout ddbf4d92e -- followed by the edited paths (git diff HEAD empty afterwards). No dist-ablation concern: these suites import from source, not dist/.

Changeset

Real patch changeset on @object-ui/types (published accept-set + type-surface shrink) and @object-ui/plugin-detail (behavior change). No skip-changeset label — that label does not exist in this repo (#4912), and this PR publishes real changes anyway.


Generated by Claude Code

…ype (#4914)

The three published contract twins in `@object-ui/types` drop `'owner'` — the
runtime `ReportFieldSchema.type` enum plus its TS twins `ReportField['type']`
and `UserFieldMetadata['type']` — so the package stops answering "legal" for a
spelling `@object-ui/fields` refuses with a tombstone (#4814, ruling A').

`InlineFieldInput` disposes consistently with that tombstone: a retired
spelling renders the same visible refusal the record form gives, instead of a
working person picker the form contradicts.

Part of #4914 (items 1-3 + 5).

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-CTYUWeyJ.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
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.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 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.65KB 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) 498.61KB 111.16KB
core (index.js) 4.06KB 1.61KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 157.05KB 43.28KB
fields (index.js) 232.86KB 58.11KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 39.16KB 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.74KB
mobile (index.js) 1.50KB 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.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.75KB 18.37KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 127.85KB 32.73KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 239.90KB 60.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 120.42KB 29.03KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 197.58KB 53.00KB
plugin-kanban (index.js) 52.72KB 14.54KB
plugin-list (index.js) 111.23KB 26.97KB
plugin-map (index.js) 17.91KB 5.72KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.97KB 11.33KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 83.81KB 20.49KB
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.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.53KB 9.41KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.28KB 0.68KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.55KB 2.07KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
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) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 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-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
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 (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
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

@os-steve
os-steve marked this pull request as ready for review August 17, 2026 06:33
@os-steve
os-steve added this pull request to the merge queue Aug 17, 2026
Merged via the queue into main with commit 598c89a Aug 17, 2026
21 checks passed
@os-steve
os-steve deleted the claude/issue-4914-owner-contract-twins branch August 17, 2026 06:34
os-steve pushed a commit that referenced this pull request Aug 17, 2026
…n refuses it

PR #4932 merged after this branch's base and shrank `owner` out of the
published field-type unions in `@object-ui/types`, so the four `type: 'owner'`
field literals in the new pin no longer compile (TS2322) — the contract twin
working exactly as designed.

The suite still has to author the refused spelling, because what it pins is the
RUNTIME disposition for a field that is ALREADY STORED that way, which no
compile-time union can reach. Confine the escape to one helper
(`retiredFieldLiteral`) so strictness holds everywhere else in the file, and
state there that the refusal is the point rather than an obstacle.

Verified the escape is load-bearing, not decorative: removing `as never`
reproduces TS2322 at all four call sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbRmJD3iPhXjKr6vhd4Qkv
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 17, 2026
…eld types (objectstack-ai#4942)

* fix(fields): close the FieldEditWidget delegation road for retired field types (objectstack-ai#4931)

`hasFieldEditWidget` answered `resolveInlineEditType(type) in EDIT_WIDGETS`, and
`resolveInlineEditType` returns a type unchanged when it is already a key of
`EDIT_WIDGETS`. While `owner: UserField` sat in that map the alias table — where
objectui#4814's retirement lives — was never consulted, so the retirement never
applied: `hasFieldEditWidget('owner')` was `true` and the grid's inline cell
editor still rendered a working person picker for a field the record form
refuses with a tombstone.

Gate on the live `RETIRED_FIELD_TYPES` table so the class closes, not the
spelling: `hasFieldEditWidget` -> false, `isInlineExcludedFieldType` -> true,
and `FieldEditWidget` renders no control while writing the once-per-spelling
prescription. Drop `owner` from `EDIT_WIDGETS` and from the cosmetic
`COMPACT_EDIT_TYPES` set (objectstack-ai#4914 item 11).

The exclusion half is load-bearing: `ObjectGrid` forces `editable: false` on a
column exactly when `isFieldInlineEditable` is false, so without it the column
stays editable and DataTable falls back to a plain text input that saves a bare
string over a stored person reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbRmJD3iPhXjKr6vhd4Qkv

* test(fields): escape the retired spelling now that the published union refuses it

PR objectstack-ai#4932 merged after this branch's base and shrank `owner` out of the
published field-type unions in `@object-ui/types`, so the four `type: 'owner'`
field literals in the new pin no longer compile (TS2322) — the contract twin
working exactly as designed.

The suite still has to author the refused spelling, because what it pins is the
RUNTIME disposition for a field that is ALREADY STORED that way, which no
compile-time union can reach. Confine the escape to one helper
(`retiredFieldLiteral`) so strictness holds everywhere else in the file, and
state there that the refusal is the point rather than an obstacle.

Verified the escape is load-bearing, not decorative: removing `as never`
reproduces TS2322 at all four call sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbRmJD3iPhXjKr6vhd4Qkv

---------

Co-authored-by: Claude <noreply@anthropic.com>
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