fix(types): carry the tombstone remediation text into the refusal message - #6930

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message
Aug 31, 2026
Merged

fix(types): carry the tombstone remediation text into the refusal message#6930
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6105

The nine ADR-0049 retirement tombstones on StaticTableColumnSchema refused the
right keys at the right paths, but the .describe() remediation string never
reached the author: .describe() is schema METADATA. What an author who wrote
align: 'right' actually read was zod's generic Invalid input: expected never, received string — which key is wrong, nothing about why it was retired or what
to write instead. Loud refusal is the ruled outcome; half its payload was being
dropped.

Routing — settled by measurement before any code was written

The first triage touch left this open: the fix might belong in the spec's
error-guidance face (objectstack packages/spec) rather than here, to be settled
by reading which package owns the parse call. Measured, three ways, all
pointing here:

  1. StaticTableColumnSchema is declared only in this repo
    (packages/types/src/zod/data-display.zod.ts). @objectstack/spec declares
    no schema carrying these members — grep minWidth packages/spec/src/ui/*.zod.ts
    is empty, and accessorKey appears there only inside a prose comment in
    conversions/registry.ts.
  2. The spec's UI component-type vocabulary
    (packages/spec/src/ui/component-type-vocabulary.ts) has no table entry at
    all. The static table is an ObjectUI-native component; spec's nearby
    retiredKey tombstones cover object-grid, a different component.
  3. The author-visible message is printed by this repo's CLI:
    packages/cli/src/commands/validate.ts and check.ts call
    safeValidateSchema from @object-ui/types/zod (→ AnyComponentSchema
    TableSchemaStaticTableColumnSchema) and print issue.message,
    issue.path and issue.code verbatim.

So the parse producing the author-visible message for these keys is owned by
@object-ui/types. The card stays in this repo.

The change — one shared mechanism, nine members

retirementTombstone() (new, packages/types/src/zod/tombstone.zod.ts) takes the
guidance string once and writes it into both author-facing channels:

exportfunctionretirementTombstone(guidance: string){returnz.never({error: guidance}).optional().describe(guidance);}

z.never({ error }) for the parse-time issue message, .describe() for generated
JSON-Schema and docs. One argument feeding two channels is the point — the message
an author reads and the text the docs publish cannot drift apart, because there is
only one string. DashboardConfigSchema.aria (objectui#5852) landed this spelling
by hand; this is that spelling as a shared mechanism, not nine hand-written
strings.

Applied to exactly the nine keys triage named: minWidth, align, fixed,
type, sortable, filterable, resizable, editable, cell. Every describe
string is carried through byte-identical — the diff changes only the call wrapper.

Authoring align: 'right' on a static table column now reports:

RETIRED (objectui#5474) — never read by the static table; use data-table, or a cellClassName like text-right

Clause ② — the accept set is unchanged, proved not assumed

Same probe run before and after, importing the source module
(packages/types/src/zod/data-display.zod.ts) via tsx, never dist
packages/types/dist does not exist in a fresh worktree, so no stale artefact
can be read.

membersuccess b/aissue path b/aissue code b/amessage
minWidthfalse / false["minWidth"] = sameinvalid_type = samechanged
alignfalse / false["align"] = sameinvalid_type = samechanged
fixedfalse / false["fixed"] = sameinvalid_type = samechanged
typefalse / false["type"] = sameinvalid_type = samechanged
sortablefalse / false["sortable"] = sameinvalid_type = samechanged
filterablefalse / false["filterable"] = sameinvalid_type = samechanged
resizablefalse / false["resizable"] = sameinvalid_type = samechanged
editablefalse / false["editable"] = sameinvalid_type = samechanged
cellfalse / false["cell"] = sameinvalid_type = samechanged
declared-key controltrue / trueparsed output identical

Only message moved. The refine-based route triage listed as the other
candidate was not taken precisely because it reports custom here — a contract
change wearing a message change's clothes. z.never({ error }) keeps
invalid_type, and z.input still types the key never, so tsc still refuses
it at the authoring site first.

.describe()survives: all 19 members' .description values compared before
and after are byte-identical, the nine included.

Scope — deliberately nine, not the whole population

Mechanical z.never( walk over packages/types/src/zod/**: 21 tombstone
declarations
(24 grep hits, 3 of them prose in doc comments):

filetombstonesnote
data-display.zod.ts1714 on StaticTableColumnSchema (the nine converted here + 5 later arrivals), hoverable + striped on TableSchema, timeScale on the gantt shape
overlay.zod.ts2command-item type pair (objectui#6523)
complex.zod.ts1DashboardConfigSchema.ariaalready carries { error } (objectui#5852)
crud.zod.ts1confirm (objectui#4314) — the tombstone convention's origin

Converted: the nine only. The remaining 11 still-generic tombstones are
reported, not touched — widening past what triage scoped would change the stated
scope after the fact. They are cheap follow-ups now that the mechanism exists.
The five later arrivals on the same shape (headerIcon / fitContent,
objectui#6424; format / options / currency, objectui#6425) plus
hoverable / striped are pinned as an explicit scope boundary test, so the
unconverted half is a recorded decision with a test behind it rather than an
oversight.

Verification

Pinned in packages/types/src/__tests__/static-table-narrow-surface.test.ts
(3 new tests), on the message content, with a declared-key control parsing
green in the same test so no green can come from a vacuous parse. The
both-channels invariant is asserted derived (message === describe) with two
literal anchors beside it, since two empty strings are also equal, plus one full
literal pin on align.

Ablation, direction predicted in writing before the run. Predicted: RED in
exactly 2 of the 3 new tests (the nine-member loop on align, and the align
literal pin); the scope-boundary test and the pre-existing "REFUSES a column
authoring align" test stay GREEN, since the accept set is untouched by the
mutation. Mutated the factalign back to the bare
z.never().optional().describe(...) with the same text — under trap ... EXIT INT TERM
with absolute paths from git rev-parse --show-toplevel.

Mutation proved on disk: anchored counts both directions (injected 1, removed 0)
and git hash-object moved 74c7bfa6…4140e557…. No rebuild leg applies and
that is proved, not assumed: the suite imports ../zod/data-display.zod by
relative source path and packages/types/dist does not exist.

Observed: Tests 2 failed | 32 passed (34) — the two predicted tests, by name.
Restore proved both ways: git diff HEAD empty for the file and
hash-object back to the HEAD blob 74c7bfa6…, restored with
git checkout HEAD -- with an absolute path.

Gates — all measured on 494a66e0, exit codes captured before any pipe

gateresult
pnpm --filter @object-ui/types type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)
root-form pnpm exec vitest run packages/types/exit 0 — 75 files / 861 tests, from 75 / 858 before (+3, no file lost)
pnpm exec eslint . in packages/types (plain form)exit 0 — 139 files linted, 0 errors, 259 pre-existing warnings, my 3 files 0 errors
node scripts/check-changeset-presence.mjsexit 0 — 1 changeset declared
check:control-bytescheck:self-importcheck:esm-specifierscheck:node-esm-loadcheck:phantom-depscheck:pre-install-import-graphcheck:readme-exportscheck:spec-symbolscheck:vi-mock-specifierscheck:vi-mock-inheritcheck:side-effects-arrayall exit 0

The type-check genuinely covers the edited files rather than excluding them —
tsc --listFiles shows tombstone.zod.ts and data-display.zod.ts in the main
program and static-table-narrow-surface.test.ts + tombstone.zod.ts in the
tsconfig.test.json program.

check:published-dist is red on main, not from this branch: the finding is
@object-ui/fields → dist/__tests__/numberInputBrowserReadings.d.ts, a package
this diff never touches (4 files changed, all under packages/types and
.changeset). Already tracked as objectui#6861.

The helper is deliberately not re-exported from index.zod.ts: nothing
outside @object-ui/types declares these schemas, so this adds no public API.

Generated by Claude Code


Generated by Claude Code

…sage
The nine ADR-0049 tombstones on `StaticTableColumnSchema` refused an authored
value at the right path, but the `.describe()` remediation string never reached
the author: `.describe()` is schema metadata, so what an author read was zod's
generic `Invalid input: expected never, received string`. Loud refusal is the
ruled outcome and half its payload was being dropped.
`retirementTombstone()` (`zod/tombstone.zod.ts`) is the one shared mechanism:
the guidance string is written ONCE and carried into both `z.never({ error })`
(the parse-time issue message) and `.describe()` (generated JSON-Schema/docs,
byte-identical). One string, so the two channels cannot drift apart.
Accept set untouched — same `success`, same issue `path`, same issue `code`
(`invalid_type`) for all nine, measured member-by-member before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BPHEWE5c.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
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)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)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.40KB62.44KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
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.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
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-sam
os-sam marked this pull request as ready for review August 31, 2026 01:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 40c479aAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6105-tombstone-refusal-message branch August 31, 2026 01:42
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…ion text
`retirementTombstone()` (objectui#6105 / PR #6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ion text (objectstack-ai#7005)
`retirementTombstone()` (objectui#6105 / PR objectstack-ai#6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
objectstack-ai#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, '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(types): carry the tombstone remediation text into the refusal message - #6930

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message
Aug 31, 2026
Merged

fix(types): carry the tombstone remediation text into the refusal message#6930
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6105

The nine ADR-0049 retirement tombstones on StaticTableColumnSchema refused the
right keys at the right paths, but the .describe() remediation string never
reached the author: .describe() is schema METADATA. What an author who wrote
align: 'right' actually read was zod's generic Invalid input: expected never, received string — which key is wrong, nothing about why it was retired or what
to write instead. Loud refusal is the ruled outcome; half its payload was being
dropped.

Routing — settled by measurement before any code was written

The first triage touch left this open: the fix might belong in the spec's
error-guidance face (objectstack packages/spec) rather than here, to be settled
by reading which package owns the parse call. Measured, three ways, all
pointing here:

  1. StaticTableColumnSchema is declared only in this repo
    (packages/types/src/zod/data-display.zod.ts). @objectstack/spec declares
    no schema carrying these members — grep minWidth packages/spec/src/ui/*.zod.ts
    is empty, and accessorKey appears there only inside a prose comment in
    conversions/registry.ts.
  2. The spec's UI component-type vocabulary
    (packages/spec/src/ui/component-type-vocabulary.ts) has no table entry at
    all. The static table is an ObjectUI-native component; spec's nearby
    retiredKey tombstones cover object-grid, a different component.
  3. The author-visible message is printed by this repo's CLI:
    packages/cli/src/commands/validate.ts and check.ts call
    safeValidateSchema from @object-ui/types/zod (→ AnyComponentSchema
    TableSchemaStaticTableColumnSchema) and print issue.message,
    issue.path and issue.code verbatim.

So the parse producing the author-visible message for these keys is owned by
@object-ui/types. The card stays in this repo.

The change — one shared mechanism, nine members

retirementTombstone() (new, packages/types/src/zod/tombstone.zod.ts) takes the
guidance string once and writes it into both author-facing channels:

exportfunctionretirementTombstone(guidance: string){returnz.never({error: guidance}).optional().describe(guidance);}

z.never({ error }) for the parse-time issue message, .describe() for generated
JSON-Schema and docs. One argument feeding two channels is the point — the message
an author reads and the text the docs publish cannot drift apart, because there is
only one string. DashboardConfigSchema.aria (objectui#5852) landed this spelling
by hand; this is that spelling as a shared mechanism, not nine hand-written
strings.

Applied to exactly the nine keys triage named: minWidth, align, fixed,
type, sortable, filterable, resizable, editable, cell. Every describe
string is carried through byte-identical — the diff changes only the call wrapper.

Authoring align: 'right' on a static table column now reports:

RETIRED (objectui#5474) — never read by the static table; use data-table, or a cellClassName like text-right

Clause ② — the accept set is unchanged, proved not assumed

Same probe run before and after, importing the source module
(packages/types/src/zod/data-display.zod.ts) via tsx, never dist
packages/types/dist does not exist in a fresh worktree, so no stale artefact
can be read.

membersuccess b/aissue path b/aissue code b/amessage
minWidthfalse / false["minWidth"] = sameinvalid_type = samechanged
alignfalse / false["align"] = sameinvalid_type = samechanged
fixedfalse / false["fixed"] = sameinvalid_type = samechanged
typefalse / false["type"] = sameinvalid_type = samechanged
sortablefalse / false["sortable"] = sameinvalid_type = samechanged
filterablefalse / false["filterable"] = sameinvalid_type = samechanged
resizablefalse / false["resizable"] = sameinvalid_type = samechanged
editablefalse / false["editable"] = sameinvalid_type = samechanged
cellfalse / false["cell"] = sameinvalid_type = samechanged
declared-key controltrue / trueparsed output identical

Only message moved. The refine-based route triage listed as the other
candidate was not taken precisely because it reports custom here — a contract
change wearing a message change's clothes. z.never({ error }) keeps
invalid_type, and z.input still types the key never, so tsc still refuses
it at the authoring site first.

.describe()survives: all 19 members' .description values compared before
and after are byte-identical, the nine included.

Scope — deliberately nine, not the whole population

Mechanical z.never( walk over packages/types/src/zod/**: 21 tombstone
declarations
(24 grep hits, 3 of them prose in doc comments):

filetombstonesnote
data-display.zod.ts1714 on StaticTableColumnSchema (the nine converted here + 5 later arrivals), hoverable + striped on TableSchema, timeScale on the gantt shape
overlay.zod.ts2command-item type pair (objectui#6523)
complex.zod.ts1DashboardConfigSchema.ariaalready carries { error } (objectui#5852)
crud.zod.ts1confirm (objectui#4314) — the tombstone convention's origin

Converted: the nine only. The remaining 11 still-generic tombstones are
reported, not touched — widening past what triage scoped would change the stated
scope after the fact. They are cheap follow-ups now that the mechanism exists.
The five later arrivals on the same shape (headerIcon / fitContent,
objectui#6424; format / options / currency, objectui#6425) plus
hoverable / striped are pinned as an explicit scope boundary test, so the
unconverted half is a recorded decision with a test behind it rather than an
oversight.

Verification

Pinned in packages/types/src/__tests__/static-table-narrow-surface.test.ts
(3 new tests), on the message content, with a declared-key control parsing
green in the same test so no green can come from a vacuous parse. The
both-channels invariant is asserted derived (message === describe) with two
literal anchors beside it, since two empty strings are also equal, plus one full
literal pin on align.

Ablation, direction predicted in writing before the run. Predicted: RED in
exactly 2 of the 3 new tests (the nine-member loop on align, and the align
literal pin); the scope-boundary test and the pre-existing "REFUSES a column
authoring align" test stay GREEN, since the accept set is untouched by the
mutation. Mutated the factalign back to the bare
z.never().optional().describe(...) with the same text — under trap ... EXIT INT TERM
with absolute paths from git rev-parse --show-toplevel.

Mutation proved on disk: anchored counts both directions (injected 1, removed 0)
and git hash-object moved 74c7bfa6…4140e557…. No rebuild leg applies and
that is proved, not assumed: the suite imports ../zod/data-display.zod by
relative source path and packages/types/dist does not exist.

Observed: Tests 2 failed | 32 passed (34) — the two predicted tests, by name.
Restore proved both ways: git diff HEAD empty for the file and
hash-object back to the HEAD blob 74c7bfa6…, restored with
git checkout HEAD -- with an absolute path.

Gates — all measured on 494a66e0, exit codes captured before any pipe

gateresult
pnpm --filter @object-ui/types type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)
root-form pnpm exec vitest run packages/types/exit 0 — 75 files / 861 tests, from 75 / 858 before (+3, no file lost)
pnpm exec eslint . in packages/types (plain form)exit 0 — 139 files linted, 0 errors, 259 pre-existing warnings, my 3 files 0 errors
node scripts/check-changeset-presence.mjsexit 0 — 1 changeset declared
check:control-bytescheck:self-importcheck:esm-specifierscheck:node-esm-loadcheck:phantom-depscheck:pre-install-import-graphcheck:readme-exportscheck:spec-symbolscheck:vi-mock-specifierscheck:vi-mock-inheritcheck:side-effects-arrayall exit 0

The type-check genuinely covers the edited files rather than excluding them —
tsc --listFiles shows tombstone.zod.ts and data-display.zod.ts in the main
program and static-table-narrow-surface.test.ts + tombstone.zod.ts in the
tsconfig.test.json program.

check:published-dist is red on main, not from this branch: the finding is
@object-ui/fields → dist/__tests__/numberInputBrowserReadings.d.ts, a package
this diff never touches (4 files changed, all under packages/types and
.changeset). Already tracked as objectui#6861.

The helper is deliberately not re-exported from index.zod.ts: nothing
outside @object-ui/types declares these schemas, so this adds no public API.

Generated by Claude Code


Generated by Claude Code

…sage
The nine ADR-0049 tombstones on `StaticTableColumnSchema` refused an authored
value at the right path, but the `.describe()` remediation string never reached
the author: `.describe()` is schema metadata, so what an author read was zod's
generic `Invalid input: expected never, received string`. Loud refusal is the
ruled outcome and half its payload was being dropped.
`retirementTombstone()` (`zod/tombstone.zod.ts`) is the one shared mechanism:
the guidance string is written ONCE and carried into both `z.never({ error })`
(the parse-time issue message) and `.describe()` (generated JSON-Schema/docs,
byte-identical). One string, so the two channels cannot drift apart.
Accept set untouched — same `success`, same issue `path`, same issue `code`
(`invalid_type`) for all nine, measured member-by-member before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BPHEWE5c.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
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)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)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.40KB62.44KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
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.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
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-sam
os-sam marked this pull request as ready for review August 31, 2026 01:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 40c479aAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6105-tombstone-refusal-message branch August 31, 2026 01:42
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…ion text
`retirementTombstone()` (objectui#6105 / PR #6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ion text (objectstack-ai#7005)
`retirementTombstone()` (objectui#6105 / PR objectstack-ai#6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
objectstack-ai#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, '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(types): carry the tombstone remediation text into the refusal message - #6930

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message
Aug 31, 2026
Merged

fix(types): carry the tombstone remediation text into the refusal message#6930
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6105

The nine ADR-0049 retirement tombstones on StaticTableColumnSchema refused the
right keys at the right paths, but the .describe() remediation string never
reached the author: .describe() is schema METADATA. What an author who wrote
align: 'right' actually read was zod's generic Invalid input: expected never, received string — which key is wrong, nothing about why it was retired or what
to write instead. Loud refusal is the ruled outcome; half its payload was being
dropped.

Routing — settled by measurement before any code was written

The first triage touch left this open: the fix might belong in the spec's
error-guidance face (objectstack packages/spec) rather than here, to be settled
by reading which package owns the parse call. Measured, three ways, all
pointing here:

  1. StaticTableColumnSchema is declared only in this repo
    (packages/types/src/zod/data-display.zod.ts). @objectstack/spec declares
    no schema carrying these members — grep minWidth packages/spec/src/ui/*.zod.ts
    is empty, and accessorKey appears there only inside a prose comment in
    conversions/registry.ts.
  2. The spec's UI component-type vocabulary
    (packages/spec/src/ui/component-type-vocabulary.ts) has no table entry at
    all. The static table is an ObjectUI-native component; spec's nearby
    retiredKey tombstones cover object-grid, a different component.
  3. The author-visible message is printed by this repo's CLI:
    packages/cli/src/commands/validate.ts and check.ts call
    safeValidateSchema from @object-ui/types/zod (→ AnyComponentSchema
    TableSchemaStaticTableColumnSchema) and print issue.message,
    issue.path and issue.code verbatim.

So the parse producing the author-visible message for these keys is owned by
@object-ui/types. The card stays in this repo.

The change — one shared mechanism, nine members

retirementTombstone() (new, packages/types/src/zod/tombstone.zod.ts) takes the
guidance string once and writes it into both author-facing channels:

exportfunctionretirementTombstone(guidance: string){returnz.never({error: guidance}).optional().describe(guidance);}

z.never({ error }) for the parse-time issue message, .describe() for generated
JSON-Schema and docs. One argument feeding two channels is the point — the message
an author reads and the text the docs publish cannot drift apart, because there is
only one string. DashboardConfigSchema.aria (objectui#5852) landed this spelling
by hand; this is that spelling as a shared mechanism, not nine hand-written
strings.

Applied to exactly the nine keys triage named: minWidth, align, fixed,
type, sortable, filterable, resizable, editable, cell. Every describe
string is carried through byte-identical — the diff changes only the call wrapper.

Authoring align: 'right' on a static table column now reports:

RETIRED (objectui#5474) — never read by the static table; use data-table, or a cellClassName like text-right

Clause ② — the accept set is unchanged, proved not assumed

Same probe run before and after, importing the source module
(packages/types/src/zod/data-display.zod.ts) via tsx, never dist
packages/types/dist does not exist in a fresh worktree, so no stale artefact
can be read.

membersuccess b/aissue path b/aissue code b/amessage
minWidthfalse / false["minWidth"] = sameinvalid_type = samechanged
alignfalse / false["align"] = sameinvalid_type = samechanged
fixedfalse / false["fixed"] = sameinvalid_type = samechanged
typefalse / false["type"] = sameinvalid_type = samechanged
sortablefalse / false["sortable"] = sameinvalid_type = samechanged
filterablefalse / false["filterable"] = sameinvalid_type = samechanged
resizablefalse / false["resizable"] = sameinvalid_type = samechanged
editablefalse / false["editable"] = sameinvalid_type = samechanged
cellfalse / false["cell"] = sameinvalid_type = samechanged
declared-key controltrue / trueparsed output identical

Only message moved. The refine-based route triage listed as the other
candidate was not taken precisely because it reports custom here — a contract
change wearing a message change's clothes. z.never({ error }) keeps
invalid_type, and z.input still types the key never, so tsc still refuses
it at the authoring site first.

.describe()survives: all 19 members' .description values compared before
and after are byte-identical, the nine included.

Scope — deliberately nine, not the whole population

Mechanical z.never( walk over packages/types/src/zod/**: 21 tombstone
declarations
(24 grep hits, 3 of them prose in doc comments):

filetombstonesnote
data-display.zod.ts1714 on StaticTableColumnSchema (the nine converted here + 5 later arrivals), hoverable + striped on TableSchema, timeScale on the gantt shape
overlay.zod.ts2command-item type pair (objectui#6523)
complex.zod.ts1DashboardConfigSchema.ariaalready carries { error } (objectui#5852)
crud.zod.ts1confirm (objectui#4314) — the tombstone convention's origin

Converted: the nine only. The remaining 11 still-generic tombstones are
reported, not touched — widening past what triage scoped would change the stated
scope after the fact. They are cheap follow-ups now that the mechanism exists.
The five later arrivals on the same shape (headerIcon / fitContent,
objectui#6424; format / options / currency, objectui#6425) plus
hoverable / striped are pinned as an explicit scope boundary test, so the
unconverted half is a recorded decision with a test behind it rather than an
oversight.

Verification

Pinned in packages/types/src/__tests__/static-table-narrow-surface.test.ts
(3 new tests), on the message content, with a declared-key control parsing
green in the same test so no green can come from a vacuous parse. The
both-channels invariant is asserted derived (message === describe) with two
literal anchors beside it, since two empty strings are also equal, plus one full
literal pin on align.

Ablation, direction predicted in writing before the run. Predicted: RED in
exactly 2 of the 3 new tests (the nine-member loop on align, and the align
literal pin); the scope-boundary test and the pre-existing "REFUSES a column
authoring align" test stay GREEN, since the accept set is untouched by the
mutation. Mutated the factalign back to the bare
z.never().optional().describe(...) with the same text — under trap ... EXIT INT TERM
with absolute paths from git rev-parse --show-toplevel.

Mutation proved on disk: anchored counts both directions (injected 1, removed 0)
and git hash-object moved 74c7bfa6…4140e557…. No rebuild leg applies and
that is proved, not assumed: the suite imports ../zod/data-display.zod by
relative source path and packages/types/dist does not exist.

Observed: Tests 2 failed | 32 passed (34) — the two predicted tests, by name.
Restore proved both ways: git diff HEAD empty for the file and
hash-object back to the HEAD blob 74c7bfa6…, restored with
git checkout HEAD -- with an absolute path.

Gates — all measured on 494a66e0, exit codes captured before any pipe

gateresult
pnpm --filter @object-ui/types type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)
root-form pnpm exec vitest run packages/types/exit 0 — 75 files / 861 tests, from 75 / 858 before (+3, no file lost)
pnpm exec eslint . in packages/types (plain form)exit 0 — 139 files linted, 0 errors, 259 pre-existing warnings, my 3 files 0 errors
node scripts/check-changeset-presence.mjsexit 0 — 1 changeset declared
check:control-bytescheck:self-importcheck:esm-specifierscheck:node-esm-loadcheck:phantom-depscheck:pre-install-import-graphcheck:readme-exportscheck:spec-symbolscheck:vi-mock-specifierscheck:vi-mock-inheritcheck:side-effects-arrayall exit 0

The type-check genuinely covers the edited files rather than excluding them —
tsc --listFiles shows tombstone.zod.ts and data-display.zod.ts in the main
program and static-table-narrow-surface.test.ts + tombstone.zod.ts in the
tsconfig.test.json program.

check:published-dist is red on main, not from this branch: the finding is
@object-ui/fields → dist/__tests__/numberInputBrowserReadings.d.ts, a package
this diff never touches (4 files changed, all under packages/types and
.changeset). Already tracked as objectui#6861.

The helper is deliberately not re-exported from index.zod.ts: nothing
outside @object-ui/types declares these schemas, so this adds no public API.

Generated by Claude Code


Generated by Claude Code

…sage
The nine ADR-0049 tombstones on `StaticTableColumnSchema` refused an authored
value at the right path, but the `.describe()` remediation string never reached
the author: `.describe()` is schema metadata, so what an author read was zod's
generic `Invalid input: expected never, received string`. Loud refusal is the
ruled outcome and half its payload was being dropped.
`retirementTombstone()` (`zod/tombstone.zod.ts`) is the one shared mechanism:
the guidance string is written ONCE and carried into both `z.never({ error })`
(the parse-time issue message) and `.describe()` (generated JSON-Schema/docs,
byte-identical). One string, so the two channels cannot drift apart.
Accept set untouched — same `success`, same issue `path`, same issue `code`
(`invalid_type`) for all nine, measured member-by-member before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BPHEWE5c.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
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)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)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.40KB62.44KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
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.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
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-sam
os-sam marked this pull request as ready for review August 31, 2026 01:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 40c479aAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6105-tombstone-refusal-message branch August 31, 2026 01:42
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…ion text
`retirementTombstone()` (objectui#6105 / PR #6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ion text (objectstack-ai#7005)
`retirementTombstone()` (objectui#6105 / PR objectstack-ai#6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
objectstack-ai#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, '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(types): carry the tombstone remediation text into the refusal message - #6930

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message
Aug 31, 2026
Merged

fix(types): carry the tombstone remediation text into the refusal message#6930
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6105

The nine ADR-0049 retirement tombstones on StaticTableColumnSchema refused the
right keys at the right paths, but the .describe() remediation string never
reached the author: .describe() is schema METADATA. What an author who wrote
align: 'right' actually read was zod's generic Invalid input: expected never, received string — which key is wrong, nothing about why it was retired or what
to write instead. Loud refusal is the ruled outcome; half its payload was being
dropped.

Routing — settled by measurement before any code was written

The first triage touch left this open: the fix might belong in the spec's
error-guidance face (objectstack packages/spec) rather than here, to be settled
by reading which package owns the parse call. Measured, three ways, all
pointing here:

  1. StaticTableColumnSchema is declared only in this repo
    (packages/types/src/zod/data-display.zod.ts). @objectstack/spec declares
    no schema carrying these members — grep minWidth packages/spec/src/ui/*.zod.ts
    is empty, and accessorKey appears there only inside a prose comment in
    conversions/registry.ts.
  2. The spec's UI component-type vocabulary
    (packages/spec/src/ui/component-type-vocabulary.ts) has no table entry at
    all. The static table is an ObjectUI-native component; spec's nearby
    retiredKey tombstones cover object-grid, a different component.
  3. The author-visible message is printed by this repo's CLI:
    packages/cli/src/commands/validate.ts and check.ts call
    safeValidateSchema from @object-ui/types/zod (→ AnyComponentSchema
    TableSchemaStaticTableColumnSchema) and print issue.message,
    issue.path and issue.code verbatim.

So the parse producing the author-visible message for these keys is owned by
@object-ui/types. The card stays in this repo.

The change — one shared mechanism, nine members

retirementTombstone() (new, packages/types/src/zod/tombstone.zod.ts) takes the
guidance string once and writes it into both author-facing channels:

exportfunctionretirementTombstone(guidance: string){returnz.never({error: guidance}).optional().describe(guidance);}

z.never({ error }) for the parse-time issue message, .describe() for generated
JSON-Schema and docs. One argument feeding two channels is the point — the message
an author reads and the text the docs publish cannot drift apart, because there is
only one string. DashboardConfigSchema.aria (objectui#5852) landed this spelling
by hand; this is that spelling as a shared mechanism, not nine hand-written
strings.

Applied to exactly the nine keys triage named: minWidth, align, fixed,
type, sortable, filterable, resizable, editable, cell. Every describe
string is carried through byte-identical — the diff changes only the call wrapper.

Authoring align: 'right' on a static table column now reports:

RETIRED (objectui#5474) — never read by the static table; use data-table, or a cellClassName like text-right

Clause ② — the accept set is unchanged, proved not assumed

Same probe run before and after, importing the source module
(packages/types/src/zod/data-display.zod.ts) via tsx, never dist
packages/types/dist does not exist in a fresh worktree, so no stale artefact
can be read.

membersuccess b/aissue path b/aissue code b/amessage
minWidthfalse / false["minWidth"] = sameinvalid_type = samechanged
alignfalse / false["align"] = sameinvalid_type = samechanged
fixedfalse / false["fixed"] = sameinvalid_type = samechanged
typefalse / false["type"] = sameinvalid_type = samechanged
sortablefalse / false["sortable"] = sameinvalid_type = samechanged
filterablefalse / false["filterable"] = sameinvalid_type = samechanged
resizablefalse / false["resizable"] = sameinvalid_type = samechanged
editablefalse / false["editable"] = sameinvalid_type = samechanged
cellfalse / false["cell"] = sameinvalid_type = samechanged
declared-key controltrue / trueparsed output identical

Only message moved. The refine-based route triage listed as the other
candidate was not taken precisely because it reports custom here — a contract
change wearing a message change's clothes. z.never({ error }) keeps
invalid_type, and z.input still types the key never, so tsc still refuses
it at the authoring site first.

.describe()survives: all 19 members' .description values compared before
and after are byte-identical, the nine included.

Scope — deliberately nine, not the whole population

Mechanical z.never( walk over packages/types/src/zod/**: 21 tombstone
declarations
(24 grep hits, 3 of them prose in doc comments):

filetombstonesnote
data-display.zod.ts1714 on StaticTableColumnSchema (the nine converted here + 5 later arrivals), hoverable + striped on TableSchema, timeScale on the gantt shape
overlay.zod.ts2command-item type pair (objectui#6523)
complex.zod.ts1DashboardConfigSchema.ariaalready carries { error } (objectui#5852)
crud.zod.ts1confirm (objectui#4314) — the tombstone convention's origin

Converted: the nine only. The remaining 11 still-generic tombstones are
reported, not touched — widening past what triage scoped would change the stated
scope after the fact. They are cheap follow-ups now that the mechanism exists.
The five later arrivals on the same shape (headerIcon / fitContent,
objectui#6424; format / options / currency, objectui#6425) plus
hoverable / striped are pinned as an explicit scope boundary test, so the
unconverted half is a recorded decision with a test behind it rather than an
oversight.

Verification

Pinned in packages/types/src/__tests__/static-table-narrow-surface.test.ts
(3 new tests), on the message content, with a declared-key control parsing
green in the same test so no green can come from a vacuous parse. The
both-channels invariant is asserted derived (message === describe) with two
literal anchors beside it, since two empty strings are also equal, plus one full
literal pin on align.

Ablation, direction predicted in writing before the run. Predicted: RED in
exactly 2 of the 3 new tests (the nine-member loop on align, and the align
literal pin); the scope-boundary test and the pre-existing "REFUSES a column
authoring align" test stay GREEN, since the accept set is untouched by the
mutation. Mutated the factalign back to the bare
z.never().optional().describe(...) with the same text — under trap ... EXIT INT TERM
with absolute paths from git rev-parse --show-toplevel.

Mutation proved on disk: anchored counts both directions (injected 1, removed 0)
and git hash-object moved 74c7bfa6…4140e557…. No rebuild leg applies and
that is proved, not assumed: the suite imports ../zod/data-display.zod by
relative source path and packages/types/dist does not exist.

Observed: Tests 2 failed | 32 passed (34) — the two predicted tests, by name.
Restore proved both ways: git diff HEAD empty for the file and
hash-object back to the HEAD blob 74c7bfa6…, restored with
git checkout HEAD -- with an absolute path.

Gates — all measured on 494a66e0, exit codes captured before any pipe

gateresult
pnpm --filter @object-ui/types type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)
root-form pnpm exec vitest run packages/types/exit 0 — 75 files / 861 tests, from 75 / 858 before (+3, no file lost)
pnpm exec eslint . in packages/types (plain form)exit 0 — 139 files linted, 0 errors, 259 pre-existing warnings, my 3 files 0 errors
node scripts/check-changeset-presence.mjsexit 0 — 1 changeset declared
check:control-bytescheck:self-importcheck:esm-specifierscheck:node-esm-loadcheck:phantom-depscheck:pre-install-import-graphcheck:readme-exportscheck:spec-symbolscheck:vi-mock-specifierscheck:vi-mock-inheritcheck:side-effects-arrayall exit 0

The type-check genuinely covers the edited files rather than excluding them —
tsc --listFiles shows tombstone.zod.ts and data-display.zod.ts in the main
program and static-table-narrow-surface.test.ts + tombstone.zod.ts in the
tsconfig.test.json program.

check:published-dist is red on main, not from this branch: the finding is
@object-ui/fields → dist/__tests__/numberInputBrowserReadings.d.ts, a package
this diff never touches (4 files changed, all under packages/types and
.changeset). Already tracked as objectui#6861.

The helper is deliberately not re-exported from index.zod.ts: nothing
outside @object-ui/types declares these schemas, so this adds no public API.

Generated by Claude Code


Generated by Claude Code

…sage
The nine ADR-0049 tombstones on `StaticTableColumnSchema` refused an authored
value at the right path, but the `.describe()` remediation string never reached
the author: `.describe()` is schema metadata, so what an author read was zod's
generic `Invalid input: expected never, received string`. Loud refusal is the
ruled outcome and half its payload was being dropped.
`retirementTombstone()` (`zod/tombstone.zod.ts`) is the one shared mechanism:
the guidance string is written ONCE and carried into both `z.never({ error })`
(the parse-time issue message) and `.describe()` (generated JSON-Schema/docs,
byte-identical). One string, so the two channels cannot drift apart.
Accept set untouched — same `success`, same issue `path`, same issue `code`
(`invalid_type`) for all nine, measured member-by-member before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BPHEWE5c.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
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)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)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.40KB62.44KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
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.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
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-sam
os-sam marked this pull request as ready for review August 31, 2026 01:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 40c479aAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6105-tombstone-refusal-message branch August 31, 2026 01:42
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…ion text
`retirementTombstone()` (objectui#6105 / PR #6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ion text (objectstack-ai#7005)
`retirementTombstone()` (objectui#6105 / PR objectstack-ai#6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
objectstack-ai#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, '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(types): carry the tombstone remediation text into the refusal message - #6930

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message
Aug 31, 2026
Merged

fix(types): carry the tombstone remediation text into the refusal message#6930
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6105

The nine ADR-0049 retirement tombstones on StaticTableColumnSchema refused the
right keys at the right paths, but the .describe() remediation string never
reached the author: .describe() is schema METADATA. What an author who wrote
align: 'right' actually read was zod's generic Invalid input: expected never, received string — which key is wrong, nothing about why it was retired or what
to write instead. Loud refusal is the ruled outcome; half its payload was being
dropped.

Routing — settled by measurement before any code was written

The first triage touch left this open: the fix might belong in the spec's
error-guidance face (objectstack packages/spec) rather than here, to be settled
by reading which package owns the parse call. Measured, three ways, all
pointing here:

  1. StaticTableColumnSchema is declared only in this repo
    (packages/types/src/zod/data-display.zod.ts). @objectstack/spec declares
    no schema carrying these members — grep minWidth packages/spec/src/ui/*.zod.ts
    is empty, and accessorKey appears there only inside a prose comment in
    conversions/registry.ts.
  2. The spec's UI component-type vocabulary
    (packages/spec/src/ui/component-type-vocabulary.ts) has no table entry at
    all. The static table is an ObjectUI-native component; spec's nearby
    retiredKey tombstones cover object-grid, a different component.
  3. The author-visible message is printed by this repo's CLI:
    packages/cli/src/commands/validate.ts and check.ts call
    safeValidateSchema from @object-ui/types/zod (→ AnyComponentSchema
    TableSchemaStaticTableColumnSchema) and print issue.message,
    issue.path and issue.code verbatim.

So the parse producing the author-visible message for these keys is owned by
@object-ui/types. The card stays in this repo.

The change — one shared mechanism, nine members

retirementTombstone() (new, packages/types/src/zod/tombstone.zod.ts) takes the
guidance string once and writes it into both author-facing channels:

exportfunctionretirementTombstone(guidance: string){returnz.never({error: guidance}).optional().describe(guidance);}

z.never({ error }) for the parse-time issue message, .describe() for generated
JSON-Schema and docs. One argument feeding two channels is the point — the message
an author reads and the text the docs publish cannot drift apart, because there is
only one string. DashboardConfigSchema.aria (objectui#5852) landed this spelling
by hand; this is that spelling as a shared mechanism, not nine hand-written
strings.

Applied to exactly the nine keys triage named: minWidth, align, fixed,
type, sortable, filterable, resizable, editable, cell. Every describe
string is carried through byte-identical — the diff changes only the call wrapper.

Authoring align: 'right' on a static table column now reports:

RETIRED (objectui#5474) — never read by the static table; use data-table, or a cellClassName like text-right

Clause ② — the accept set is unchanged, proved not assumed

Same probe run before and after, importing the source module
(packages/types/src/zod/data-display.zod.ts) via tsx, never dist
packages/types/dist does not exist in a fresh worktree, so no stale artefact
can be read.

membersuccess b/aissue path b/aissue code b/amessage
minWidthfalse / false["minWidth"] = sameinvalid_type = samechanged
alignfalse / false["align"] = sameinvalid_type = samechanged
fixedfalse / false["fixed"] = sameinvalid_type = samechanged
typefalse / false["type"] = sameinvalid_type = samechanged
sortablefalse / false["sortable"] = sameinvalid_type = samechanged
filterablefalse / false["filterable"] = sameinvalid_type = samechanged
resizablefalse / false["resizable"] = sameinvalid_type = samechanged
editablefalse / false["editable"] = sameinvalid_type = samechanged
cellfalse / false["cell"] = sameinvalid_type = samechanged
declared-key controltrue / trueparsed output identical

Only message moved. The refine-based route triage listed as the other
candidate was not taken precisely because it reports custom here — a contract
change wearing a message change's clothes. z.never({ error }) keeps
invalid_type, and z.input still types the key never, so tsc still refuses
it at the authoring site first.

.describe()survives: all 19 members' .description values compared before
and after are byte-identical, the nine included.

Scope — deliberately nine, not the whole population

Mechanical z.never( walk over packages/types/src/zod/**: 21 tombstone
declarations
(24 grep hits, 3 of them prose in doc comments):

filetombstonesnote
data-display.zod.ts1714 on StaticTableColumnSchema (the nine converted here + 5 later arrivals), hoverable + striped on TableSchema, timeScale on the gantt shape
overlay.zod.ts2command-item type pair (objectui#6523)
complex.zod.ts1DashboardConfigSchema.ariaalready carries { error } (objectui#5852)
crud.zod.ts1confirm (objectui#4314) — the tombstone convention's origin

Converted: the nine only. The remaining 11 still-generic tombstones are
reported, not touched — widening past what triage scoped would change the stated
scope after the fact. They are cheap follow-ups now that the mechanism exists.
The five later arrivals on the same shape (headerIcon / fitContent,
objectui#6424; format / options / currency, objectui#6425) plus
hoverable / striped are pinned as an explicit scope boundary test, so the
unconverted half is a recorded decision with a test behind it rather than an
oversight.

Verification

Pinned in packages/types/src/__tests__/static-table-narrow-surface.test.ts
(3 new tests), on the message content, with a declared-key control parsing
green in the same test so no green can come from a vacuous parse. The
both-channels invariant is asserted derived (message === describe) with two
literal anchors beside it, since two empty strings are also equal, plus one full
literal pin on align.

Ablation, direction predicted in writing before the run. Predicted: RED in
exactly 2 of the 3 new tests (the nine-member loop on align, and the align
literal pin); the scope-boundary test and the pre-existing "REFUSES a column
authoring align" test stay GREEN, since the accept set is untouched by the
mutation. Mutated the factalign back to the bare
z.never().optional().describe(...) with the same text — under trap ... EXIT INT TERM
with absolute paths from git rev-parse --show-toplevel.

Mutation proved on disk: anchored counts both directions (injected 1, removed 0)
and git hash-object moved 74c7bfa6…4140e557…. No rebuild leg applies and
that is proved, not assumed: the suite imports ../zod/data-display.zod by
relative source path and packages/types/dist does not exist.

Observed: Tests 2 failed | 32 passed (34) — the two predicted tests, by name.
Restore proved both ways: git diff HEAD empty for the file and
hash-object back to the HEAD blob 74c7bfa6…, restored with
git checkout HEAD -- with an absolute path.

Gates — all measured on 494a66e0, exit codes captured before any pipe

gateresult
pnpm --filter @object-ui/types type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)
root-form pnpm exec vitest run packages/types/exit 0 — 75 files / 861 tests, from 75 / 858 before (+3, no file lost)
pnpm exec eslint . in packages/types (plain form)exit 0 — 139 files linted, 0 errors, 259 pre-existing warnings, my 3 files 0 errors
node scripts/check-changeset-presence.mjsexit 0 — 1 changeset declared
check:control-bytescheck:self-importcheck:esm-specifierscheck:node-esm-loadcheck:phantom-depscheck:pre-install-import-graphcheck:readme-exportscheck:spec-symbolscheck:vi-mock-specifierscheck:vi-mock-inheritcheck:side-effects-arrayall exit 0

The type-check genuinely covers the edited files rather than excluding them —
tsc --listFiles shows tombstone.zod.ts and data-display.zod.ts in the main
program and static-table-narrow-surface.test.ts + tombstone.zod.ts in the
tsconfig.test.json program.

check:published-dist is red on main, not from this branch: the finding is
@object-ui/fields → dist/__tests__/numberInputBrowserReadings.d.ts, a package
this diff never touches (4 files changed, all under packages/types and
.changeset). Already tracked as objectui#6861.

The helper is deliberately not re-exported from index.zod.ts: nothing
outside @object-ui/types declares these schemas, so this adds no public API.

Generated by Claude Code


Generated by Claude Code

…sage
The nine ADR-0049 tombstones on `StaticTableColumnSchema` refused an authored
value at the right path, but the `.describe()` remediation string never reached
the author: `.describe()` is schema metadata, so what an author read was zod's
generic `Invalid input: expected never, received string`. Loud refusal is the
ruled outcome and half its payload was being dropped.
`retirementTombstone()` (`zod/tombstone.zod.ts`) is the one shared mechanism:
the guidance string is written ONCE and carried into both `z.never({ error })`
(the parse-time issue message) and `.describe()` (generated JSON-Schema/docs,
byte-identical). One string, so the two channels cannot drift apart.
Accept set untouched — same `success`, same issue `path`, same issue `code`
(`invalid_type`) for all nine, measured member-by-member before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BPHEWE5c.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
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)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)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.40KB62.44KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
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.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
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-sam
os-sam marked this pull request as ready for review August 31, 2026 01:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 40c479aAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6105-tombstone-refusal-message branch August 31, 2026 01:42
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…ion text
`retirementTombstone()` (objectui#6105 / PR #6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ion text (objectstack-ai#7005)
`retirementTombstone()` (objectui#6105 / PR objectstack-ai#6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
objectstack-ai#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, '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(types): carry the tombstone remediation text into the refusal message - #6930

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message
Aug 31, 2026
Merged

fix(types): carry the tombstone remediation text into the refusal message#6930
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6105

The nine ADR-0049 retirement tombstones on StaticTableColumnSchema refused the
right keys at the right paths, but the .describe() remediation string never
reached the author: .describe() is schema METADATA. What an author who wrote
align: 'right' actually read was zod's generic Invalid input: expected never, received string — which key is wrong, nothing about why it was retired or what
to write instead. Loud refusal is the ruled outcome; half its payload was being
dropped.

Routing — settled by measurement before any code was written

The first triage touch left this open: the fix might belong in the spec's
error-guidance face (objectstack packages/spec) rather than here, to be settled
by reading which package owns the parse call. Measured, three ways, all
pointing here:

  1. StaticTableColumnSchema is declared only in this repo
    (packages/types/src/zod/data-display.zod.ts). @objectstack/spec declares
    no schema carrying these members — grep minWidth packages/spec/src/ui/*.zod.ts
    is empty, and accessorKey appears there only inside a prose comment in
    conversions/registry.ts.
  2. The spec's UI component-type vocabulary
    (packages/spec/src/ui/component-type-vocabulary.ts) has no table entry at
    all. The static table is an ObjectUI-native component; spec's nearby
    retiredKey tombstones cover object-grid, a different component.
  3. The author-visible message is printed by this repo's CLI:
    packages/cli/src/commands/validate.ts and check.ts call
    safeValidateSchema from @object-ui/types/zod (→ AnyComponentSchema
    TableSchemaStaticTableColumnSchema) and print issue.message,
    issue.path and issue.code verbatim.

So the parse producing the author-visible message for these keys is owned by
@object-ui/types. The card stays in this repo.

The change — one shared mechanism, nine members

retirementTombstone() (new, packages/types/src/zod/tombstone.zod.ts) takes the
guidance string once and writes it into both author-facing channels:

exportfunctionretirementTombstone(guidance: string){returnz.never({error: guidance}).optional().describe(guidance);}

z.never({ error }) for the parse-time issue message, .describe() for generated
JSON-Schema and docs. One argument feeding two channels is the point — the message
an author reads and the text the docs publish cannot drift apart, because there is
only one string. DashboardConfigSchema.aria (objectui#5852) landed this spelling
by hand; this is that spelling as a shared mechanism, not nine hand-written
strings.

Applied to exactly the nine keys triage named: minWidth, align, fixed,
type, sortable, filterable, resizable, editable, cell. Every describe
string is carried through byte-identical — the diff changes only the call wrapper.

Authoring align: 'right' on a static table column now reports:

RETIRED (objectui#5474) — never read by the static table; use data-table, or a cellClassName like text-right

Clause ② — the accept set is unchanged, proved not assumed

Same probe run before and after, importing the source module
(packages/types/src/zod/data-display.zod.ts) via tsx, never dist
packages/types/dist does not exist in a fresh worktree, so no stale artefact
can be read.

membersuccess b/aissue path b/aissue code b/amessage
minWidthfalse / false["minWidth"] = sameinvalid_type = samechanged
alignfalse / false["align"] = sameinvalid_type = samechanged
fixedfalse / false["fixed"] = sameinvalid_type = samechanged
typefalse / false["type"] = sameinvalid_type = samechanged
sortablefalse / false["sortable"] = sameinvalid_type = samechanged
filterablefalse / false["filterable"] = sameinvalid_type = samechanged
resizablefalse / false["resizable"] = sameinvalid_type = samechanged
editablefalse / false["editable"] = sameinvalid_type = samechanged
cellfalse / false["cell"] = sameinvalid_type = samechanged
declared-key controltrue / trueparsed output identical

Only message moved. The refine-based route triage listed as the other
candidate was not taken precisely because it reports custom here — a contract
change wearing a message change's clothes. z.never({ error }) keeps
invalid_type, and z.input still types the key never, so tsc still refuses
it at the authoring site first.

.describe()survives: all 19 members' .description values compared before
and after are byte-identical, the nine included.

Scope — deliberately nine, not the whole population

Mechanical z.never( walk over packages/types/src/zod/**: 21 tombstone
declarations
(24 grep hits, 3 of them prose in doc comments):

filetombstonesnote
data-display.zod.ts1714 on StaticTableColumnSchema (the nine converted here + 5 later arrivals), hoverable + striped on TableSchema, timeScale on the gantt shape
overlay.zod.ts2command-item type pair (objectui#6523)
complex.zod.ts1DashboardConfigSchema.ariaalready carries { error } (objectui#5852)
crud.zod.ts1confirm (objectui#4314) — the tombstone convention's origin

Converted: the nine only. The remaining 11 still-generic tombstones are
reported, not touched — widening past what triage scoped would change the stated
scope after the fact. They are cheap follow-ups now that the mechanism exists.
The five later arrivals on the same shape (headerIcon / fitContent,
objectui#6424; format / options / currency, objectui#6425) plus
hoverable / striped are pinned as an explicit scope boundary test, so the
unconverted half is a recorded decision with a test behind it rather than an
oversight.

Verification

Pinned in packages/types/src/__tests__/static-table-narrow-surface.test.ts
(3 new tests), on the message content, with a declared-key control parsing
green in the same test so no green can come from a vacuous parse. The
both-channels invariant is asserted derived (message === describe) with two
literal anchors beside it, since two empty strings are also equal, plus one full
literal pin on align.

Ablation, direction predicted in writing before the run. Predicted: RED in
exactly 2 of the 3 new tests (the nine-member loop on align, and the align
literal pin); the scope-boundary test and the pre-existing "REFUSES a column
authoring align" test stay GREEN, since the accept set is untouched by the
mutation. Mutated the factalign back to the bare
z.never().optional().describe(...) with the same text — under trap ... EXIT INT TERM
with absolute paths from git rev-parse --show-toplevel.

Mutation proved on disk: anchored counts both directions (injected 1, removed 0)
and git hash-object moved 74c7bfa6…4140e557…. No rebuild leg applies and
that is proved, not assumed: the suite imports ../zod/data-display.zod by
relative source path and packages/types/dist does not exist.

Observed: Tests 2 failed | 32 passed (34) — the two predicted tests, by name.
Restore proved both ways: git diff HEAD empty for the file and
hash-object back to the HEAD blob 74c7bfa6…, restored with
git checkout HEAD -- with an absolute path.

Gates — all measured on 494a66e0, exit codes captured before any pipe

gateresult
pnpm --filter @object-ui/types type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)
root-form pnpm exec vitest run packages/types/exit 0 — 75 files / 861 tests, from 75 / 858 before (+3, no file lost)
pnpm exec eslint . in packages/types (plain form)exit 0 — 139 files linted, 0 errors, 259 pre-existing warnings, my 3 files 0 errors
node scripts/check-changeset-presence.mjsexit 0 — 1 changeset declared
check:control-bytescheck:self-importcheck:esm-specifierscheck:node-esm-loadcheck:phantom-depscheck:pre-install-import-graphcheck:readme-exportscheck:spec-symbolscheck:vi-mock-specifierscheck:vi-mock-inheritcheck:side-effects-arrayall exit 0

The type-check genuinely covers the edited files rather than excluding them —
tsc --listFiles shows tombstone.zod.ts and data-display.zod.ts in the main
program and static-table-narrow-surface.test.ts + tombstone.zod.ts in the
tsconfig.test.json program.

check:published-dist is red on main, not from this branch: the finding is
@object-ui/fields → dist/__tests__/numberInputBrowserReadings.d.ts, a package
this diff never touches (4 files changed, all under packages/types and
.changeset). Already tracked as objectui#6861.

The helper is deliberately not re-exported from index.zod.ts: nothing
outside @object-ui/types declares these schemas, so this adds no public API.

Generated by Claude Code


Generated by Claude Code

…sage
The nine ADR-0049 tombstones on `StaticTableColumnSchema` refused an authored
value at the right path, but the `.describe()` remediation string never reached
the author: `.describe()` is schema metadata, so what an author read was zod's
generic `Invalid input: expected never, received string`. Loud refusal is the
ruled outcome and half its payload was being dropped.
`retirementTombstone()` (`zod/tombstone.zod.ts`) is the one shared mechanism:
the guidance string is written ONCE and carried into both `z.never({ error })`
(the parse-time issue message) and `.describe()` (generated JSON-Schema/docs,
byte-identical). One string, so the two channels cannot drift apart.
Accept set untouched — same `success`, same issue `path`, same issue `code`
(`invalid_type`) for all nine, measured member-by-member before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BPHEWE5c.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
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)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)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.40KB62.44KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
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.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
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-sam
os-sam marked this pull request as ready for review August 31, 2026 01:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 40c479aAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6105-tombstone-refusal-message branch August 31, 2026 01:42
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…ion text
`retirementTombstone()` (objectui#6105 / PR #6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ion text (objectstack-ai#7005)
`retirementTombstone()` (objectui#6105 / PR objectstack-ai#6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
objectstack-ai#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, '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(types): carry the tombstone remediation text into the refusal message - #6930

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message
Aug 31, 2026
Merged

fix(types): carry the tombstone remediation text into the refusal message#6930
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6105

The nine ADR-0049 retirement tombstones on StaticTableColumnSchema refused the
right keys at the right paths, but the .describe() remediation string never
reached the author: .describe() is schema METADATA. What an author who wrote
align: 'right' actually read was zod's generic Invalid input: expected never, received string — which key is wrong, nothing about why it was retired or what
to write instead. Loud refusal is the ruled outcome; half its payload was being
dropped.

Routing — settled by measurement before any code was written

The first triage touch left this open: the fix might belong in the spec's
error-guidance face (objectstack packages/spec) rather than here, to be settled
by reading which package owns the parse call. Measured, three ways, all
pointing here:

  1. StaticTableColumnSchema is declared only in this repo
    (packages/types/src/zod/data-display.zod.ts). @objectstack/spec declares
    no schema carrying these members — grep minWidth packages/spec/src/ui/*.zod.ts
    is empty, and accessorKey appears there only inside a prose comment in
    conversions/registry.ts.
  2. The spec's UI component-type vocabulary
    (packages/spec/src/ui/component-type-vocabulary.ts) has no table entry at
    all. The static table is an ObjectUI-native component; spec's nearby
    retiredKey tombstones cover object-grid, a different component.
  3. The author-visible message is printed by this repo's CLI:
    packages/cli/src/commands/validate.ts and check.ts call
    safeValidateSchema from @object-ui/types/zod (→ AnyComponentSchema
    TableSchemaStaticTableColumnSchema) and print issue.message,
    issue.path and issue.code verbatim.

So the parse producing the author-visible message for these keys is owned by
@object-ui/types. The card stays in this repo.

The change — one shared mechanism, nine members

retirementTombstone() (new, packages/types/src/zod/tombstone.zod.ts) takes the
guidance string once and writes it into both author-facing channels:

exportfunctionretirementTombstone(guidance: string){returnz.never({error: guidance}).optional().describe(guidance);}

z.never({ error }) for the parse-time issue message, .describe() for generated
JSON-Schema and docs. One argument feeding two channels is the point — the message
an author reads and the text the docs publish cannot drift apart, because there is
only one string. DashboardConfigSchema.aria (objectui#5852) landed this spelling
by hand; this is that spelling as a shared mechanism, not nine hand-written
strings.

Applied to exactly the nine keys triage named: minWidth, align, fixed,
type, sortable, filterable, resizable, editable, cell. Every describe
string is carried through byte-identical — the diff changes only the call wrapper.

Authoring align: 'right' on a static table column now reports:

RETIRED (objectui#5474) — never read by the static table; use data-table, or a cellClassName like text-right

Clause ② — the accept set is unchanged, proved not assumed

Same probe run before and after, importing the source module
(packages/types/src/zod/data-display.zod.ts) via tsx, never dist
packages/types/dist does not exist in a fresh worktree, so no stale artefact
can be read.

membersuccess b/aissue path b/aissue code b/amessage
minWidthfalse / false["minWidth"] = sameinvalid_type = samechanged
alignfalse / false["align"] = sameinvalid_type = samechanged
fixedfalse / false["fixed"] = sameinvalid_type = samechanged
typefalse / false["type"] = sameinvalid_type = samechanged
sortablefalse / false["sortable"] = sameinvalid_type = samechanged
filterablefalse / false["filterable"] = sameinvalid_type = samechanged
resizablefalse / false["resizable"] = sameinvalid_type = samechanged
editablefalse / false["editable"] = sameinvalid_type = samechanged
cellfalse / false["cell"] = sameinvalid_type = samechanged
declared-key controltrue / trueparsed output identical

Only message moved. The refine-based route triage listed as the other
candidate was not taken precisely because it reports custom here — a contract
change wearing a message change's clothes. z.never({ error }) keeps
invalid_type, and z.input still types the key never, so tsc still refuses
it at the authoring site first.

.describe()survives: all 19 members' .description values compared before
and after are byte-identical, the nine included.

Scope — deliberately nine, not the whole population

Mechanical z.never( walk over packages/types/src/zod/**: 21 tombstone
declarations
(24 grep hits, 3 of them prose in doc comments):

filetombstonesnote
data-display.zod.ts1714 on StaticTableColumnSchema (the nine converted here + 5 later arrivals), hoverable + striped on TableSchema, timeScale on the gantt shape
overlay.zod.ts2command-item type pair (objectui#6523)
complex.zod.ts1DashboardConfigSchema.ariaalready carries { error } (objectui#5852)
crud.zod.ts1confirm (objectui#4314) — the tombstone convention's origin

Converted: the nine only. The remaining 11 still-generic tombstones are
reported, not touched — widening past what triage scoped would change the stated
scope after the fact. They are cheap follow-ups now that the mechanism exists.
The five later arrivals on the same shape (headerIcon / fitContent,
objectui#6424; format / options / currency, objectui#6425) plus
hoverable / striped are pinned as an explicit scope boundary test, so the
unconverted half is a recorded decision with a test behind it rather than an
oversight.

Verification

Pinned in packages/types/src/__tests__/static-table-narrow-surface.test.ts
(3 new tests), on the message content, with a declared-key control parsing
green in the same test so no green can come from a vacuous parse. The
both-channels invariant is asserted derived (message === describe) with two
literal anchors beside it, since two empty strings are also equal, plus one full
literal pin on align.

Ablation, direction predicted in writing before the run. Predicted: RED in
exactly 2 of the 3 new tests (the nine-member loop on align, and the align
literal pin); the scope-boundary test and the pre-existing "REFUSES a column
authoring align" test stay GREEN, since the accept set is untouched by the
mutation. Mutated the factalign back to the bare
z.never().optional().describe(...) with the same text — under trap ... EXIT INT TERM
with absolute paths from git rev-parse --show-toplevel.

Mutation proved on disk: anchored counts both directions (injected 1, removed 0)
and git hash-object moved 74c7bfa6…4140e557…. No rebuild leg applies and
that is proved, not assumed: the suite imports ../zod/data-display.zod by
relative source path and packages/types/dist does not exist.

Observed: Tests 2 failed | 32 passed (34) — the two predicted tests, by name.
Restore proved both ways: git diff HEAD empty for the file and
hash-object back to the HEAD blob 74c7bfa6…, restored with
git checkout HEAD -- with an absolute path.

Gates — all measured on 494a66e0, exit codes captured before any pipe

gateresult
pnpm --filter @object-ui/types type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)
root-form pnpm exec vitest run packages/types/exit 0 — 75 files / 861 tests, from 75 / 858 before (+3, no file lost)
pnpm exec eslint . in packages/types (plain form)exit 0 — 139 files linted, 0 errors, 259 pre-existing warnings, my 3 files 0 errors
node scripts/check-changeset-presence.mjsexit 0 — 1 changeset declared
check:control-bytescheck:self-importcheck:esm-specifierscheck:node-esm-loadcheck:phantom-depscheck:pre-install-import-graphcheck:readme-exportscheck:spec-symbolscheck:vi-mock-specifierscheck:vi-mock-inheritcheck:side-effects-arrayall exit 0

The type-check genuinely covers the edited files rather than excluding them —
tsc --listFiles shows tombstone.zod.ts and data-display.zod.ts in the main
program and static-table-narrow-surface.test.ts + tombstone.zod.ts in the
tsconfig.test.json program.

check:published-dist is red on main, not from this branch: the finding is
@object-ui/fields → dist/__tests__/numberInputBrowserReadings.d.ts, a package
this diff never touches (4 files changed, all under packages/types and
.changeset). Already tracked as objectui#6861.

The helper is deliberately not re-exported from index.zod.ts: nothing
outside @object-ui/types declares these schemas, so this adds no public API.

Generated by Claude Code


Generated by Claude Code

…sage
The nine ADR-0049 tombstones on `StaticTableColumnSchema` refused an authored
value at the right path, but the `.describe()` remediation string never reached
the author: `.describe()` is schema metadata, so what an author read was zod's
generic `Invalid input: expected never, received string`. Loud refusal is the
ruled outcome and half its payload was being dropped.
`retirementTombstone()` (`zod/tombstone.zod.ts`) is the one shared mechanism:
the guidance string is written ONCE and carried into both `z.never({ error })`
(the parse-time issue message) and `.describe()` (generated JSON-Schema/docs,
byte-identical). One string, so the two channels cannot drift apart.
Accept set untouched — same `success`, same issue `path`, same issue `code`
(`invalid_type`) for all nine, measured member-by-member before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BPHEWE5c.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
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)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)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.40KB62.44KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
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.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
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-sam
os-sam marked this pull request as ready for review August 31, 2026 01:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 40c479aAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6105-tombstone-refusal-message branch August 31, 2026 01:42
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…ion text
`retirementTombstone()` (objectui#6105 / PR #6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ion text (objectstack-ai#7005)
`retirementTombstone()` (objectui#6105 / PR objectstack-ai#6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
objectstack-ai#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, '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(types): carry the tombstone remediation text into the refusal message - #6930

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message
Aug 31, 2026
Merged

fix(types): carry the tombstone remediation text into the refusal message#6930
os-sam merged 1 commit into
mainfrom
claude/issue-6105-tombstone-refusal-message

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6105

The nine ADR-0049 retirement tombstones on StaticTableColumnSchema refused the
right keys at the right paths, but the .describe() remediation string never
reached the author: .describe() is schema METADATA. What an author who wrote
align: 'right' actually read was zod's generic Invalid input: expected never, received string — which key is wrong, nothing about why it was retired or what
to write instead. Loud refusal is the ruled outcome; half its payload was being
dropped.

Routing — settled by measurement before any code was written

The first triage touch left this open: the fix might belong in the spec's
error-guidance face (objectstack packages/spec) rather than here, to be settled
by reading which package owns the parse call. Measured, three ways, all
pointing here:

  1. StaticTableColumnSchema is declared only in this repo
    (packages/types/src/zod/data-display.zod.ts). @objectstack/spec declares
    no schema carrying these members — grep minWidth packages/spec/src/ui/*.zod.ts
    is empty, and accessorKey appears there only inside a prose comment in
    conversions/registry.ts.
  2. The spec's UI component-type vocabulary
    (packages/spec/src/ui/component-type-vocabulary.ts) has no table entry at
    all. The static table is an ObjectUI-native component; spec's nearby
    retiredKey tombstones cover object-grid, a different component.
  3. The author-visible message is printed by this repo's CLI:
    packages/cli/src/commands/validate.ts and check.ts call
    safeValidateSchema from @object-ui/types/zod (→ AnyComponentSchema
    TableSchemaStaticTableColumnSchema) and print issue.message,
    issue.path and issue.code verbatim.

So the parse producing the author-visible message for these keys is owned by
@object-ui/types. The card stays in this repo.

The change — one shared mechanism, nine members

retirementTombstone() (new, packages/types/src/zod/tombstone.zod.ts) takes the
guidance string once and writes it into both author-facing channels:

exportfunctionretirementTombstone(guidance: string){returnz.never({error: guidance}).optional().describe(guidance);}

z.never({ error }) for the parse-time issue message, .describe() for generated
JSON-Schema and docs. One argument feeding two channels is the point — the message
an author reads and the text the docs publish cannot drift apart, because there is
only one string. DashboardConfigSchema.aria (objectui#5852) landed this spelling
by hand; this is that spelling as a shared mechanism, not nine hand-written
strings.

Applied to exactly the nine keys triage named: minWidth, align, fixed,
type, sortable, filterable, resizable, editable, cell. Every describe
string is carried through byte-identical — the diff changes only the call wrapper.

Authoring align: 'right' on a static table column now reports:

RETIRED (objectui#5474) — never read by the static table; use data-table, or a cellClassName like text-right

Clause ② — the accept set is unchanged, proved not assumed

Same probe run before and after, importing the source module
(packages/types/src/zod/data-display.zod.ts) via tsx, never dist
packages/types/dist does not exist in a fresh worktree, so no stale artefact
can be read.

membersuccess b/aissue path b/aissue code b/amessage
minWidthfalse / false["minWidth"] = sameinvalid_type = samechanged
alignfalse / false["align"] = sameinvalid_type = samechanged
fixedfalse / false["fixed"] = sameinvalid_type = samechanged
typefalse / false["type"] = sameinvalid_type = samechanged
sortablefalse / false["sortable"] = sameinvalid_type = samechanged
filterablefalse / false["filterable"] = sameinvalid_type = samechanged
resizablefalse / false["resizable"] = sameinvalid_type = samechanged
editablefalse / false["editable"] = sameinvalid_type = samechanged
cellfalse / false["cell"] = sameinvalid_type = samechanged
declared-key controltrue / trueparsed output identical

Only message moved. The refine-based route triage listed as the other
candidate was not taken precisely because it reports custom here — a contract
change wearing a message change's clothes. z.never({ error }) keeps
invalid_type, and z.input still types the key never, so tsc still refuses
it at the authoring site first.

.describe()survives: all 19 members' .description values compared before
and after are byte-identical, the nine included.

Scope — deliberately nine, not the whole population

Mechanical z.never( walk over packages/types/src/zod/**: 21 tombstone
declarations
(24 grep hits, 3 of them prose in doc comments):

filetombstonesnote
data-display.zod.ts1714 on StaticTableColumnSchema (the nine converted here + 5 later arrivals), hoverable + striped on TableSchema, timeScale on the gantt shape
overlay.zod.ts2command-item type pair (objectui#6523)
complex.zod.ts1DashboardConfigSchema.ariaalready carries { error } (objectui#5852)
crud.zod.ts1confirm (objectui#4314) — the tombstone convention's origin

Converted: the nine only. The remaining 11 still-generic tombstones are
reported, not touched — widening past what triage scoped would change the stated
scope after the fact. They are cheap follow-ups now that the mechanism exists.
The five later arrivals on the same shape (headerIcon / fitContent,
objectui#6424; format / options / currency, objectui#6425) plus
hoverable / striped are pinned as an explicit scope boundary test, so the
unconverted half is a recorded decision with a test behind it rather than an
oversight.

Verification

Pinned in packages/types/src/__tests__/static-table-narrow-surface.test.ts
(3 new tests), on the message content, with a declared-key control parsing
green in the same test so no green can come from a vacuous parse. The
both-channels invariant is asserted derived (message === describe) with two
literal anchors beside it, since two empty strings are also equal, plus one full
literal pin on align.

Ablation, direction predicted in writing before the run. Predicted: RED in
exactly 2 of the 3 new tests (the nine-member loop on align, and the align
literal pin); the scope-boundary test and the pre-existing "REFUSES a column
authoring align" test stay GREEN, since the accept set is untouched by the
mutation. Mutated the factalign back to the bare
z.never().optional().describe(...) with the same text — under trap ... EXIT INT TERM
with absolute paths from git rev-parse --show-toplevel.

Mutation proved on disk: anchored counts both directions (injected 1, removed 0)
and git hash-object moved 74c7bfa6…4140e557…. No rebuild leg applies and
that is proved, not assumed: the suite imports ../zod/data-display.zod by
relative source path and packages/types/dist does not exist.

Observed: Tests 2 failed | 32 passed (34) — the two predicted tests, by name.
Restore proved both ways: git diff HEAD empty for the file and
hash-object back to the HEAD blob 74c7bfa6…, restored with
git checkout HEAD -- with an absolute path.

Gates — all measured on 494a66e0, exit codes captured before any pipe

gateresult
pnpm --filter @object-ui/types type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)
root-form pnpm exec vitest run packages/types/exit 0 — 75 files / 861 tests, from 75 / 858 before (+3, no file lost)
pnpm exec eslint . in packages/types (plain form)exit 0 — 139 files linted, 0 errors, 259 pre-existing warnings, my 3 files 0 errors
node scripts/check-changeset-presence.mjsexit 0 — 1 changeset declared
check:control-bytescheck:self-importcheck:esm-specifierscheck:node-esm-loadcheck:phantom-depscheck:pre-install-import-graphcheck:readme-exportscheck:spec-symbolscheck:vi-mock-specifierscheck:vi-mock-inheritcheck:side-effects-arrayall exit 0

The type-check genuinely covers the edited files rather than excluding them —
tsc --listFiles shows tombstone.zod.ts and data-display.zod.ts in the main
program and static-table-narrow-surface.test.ts + tombstone.zod.ts in the
tsconfig.test.json program.

check:published-dist is red on main, not from this branch: the finding is
@object-ui/fields → dist/__tests__/numberInputBrowserReadings.d.ts, a package
this diff never touches (4 files changed, all under packages/types and
.changeset). Already tracked as objectui#6861.

The helper is deliberately not re-exported from index.zod.ts: nothing
outside @object-ui/types declares these schemas, so this adds no public API.

Generated by Claude Code


Generated by Claude Code

…sage
The nine ADR-0049 tombstones on `StaticTableColumnSchema` refused an authored
value at the right path, but the `.describe()` remediation string never reached
the author: `.describe()` is schema metadata, so what an author read was zod's
generic `Invalid input: expected never, received string`. Loud refusal is the
ruled outcome and half its payload was being dropped.
`retirementTombstone()` (`zod/tombstone.zod.ts`) is the one shared mechanism:
the guidance string is written ONCE and carried into both `z.never({ error })`
(the parse-time issue message) and `.describe()` (generated JSON-Schema/docs,
byte-identical). One string, so the two channels cannot drift apart.
Accept set untouched — same `success`, same issue `path`, same issue `code`
(`invalid_type`) for all nine, measured member-by-member before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BPHEWE5c.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
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)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)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.40KB62.44KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
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.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
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-sam
os-sam marked this pull request as ready for review August 31, 2026 01:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 40c479aAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6105-tombstone-refusal-message branch August 31, 2026 01:42
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…ion text
`retirementTombstone()` (objectui#6105 / PR #6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ion text (objectstack-ai#7005)
`retirementTombstone()` (objectui#6105 / PR objectstack-ai#6930) writes a guidance string
ONCE into both author-facing channels — `z.never({ error })`, the parse-time
issue message, and `.describe()`, the generated JSON-Schema and docs surface.
Nine tombstones on `StaticTableColumnSchema` were converted there; eleven
declarations were left on the bare `z.never().optional().describe(...)`
spelling and kept answering with zod's generic `Invalid input: expected never,
received string` — which names the key and says nothing about why it was
retired or what to write instead.
Five of the eleven sat on `StaticTableColumnSchema` itself, so its author read
guidance on nine keys and zod's generic on five: a shape that teaches the
message means something, then withholds it. Converted here:
`headerIcon` / `fitContent` / `format` / `options` / `currency`
(StaticTableColumnSchema), `hoverable` / `striped` (TableSchema), `timeScale`
(TimelineSchema), `type` on both `MenuItemSchema` union arms, and `confirm`
(ActionSchema) — the key that established the convention and was the last one
still emitting the generic message.
The accept set does not move. Measured member-by-member on the SOURCE modules
(tsx; `packages/types/dist` does not exist in a fresh worktree): for all 11
conversions, the 9 already-converted controls and 6 live-value controls,
`safeParse` reports the same `success`, `path`, `code` (`invalid_type`) and
`expected` (`never`) before and after; only the message differs. All 132
`.description` values on the five affected schemas are byte-identical.
`MenuItemSchema` is a union, so its top-level issue stays zod's own
`invalid_union` / `Invalid input` at path `[]` and the guidance rides the
per-arm issues; that is pinned in the tests rather than glossed.
objectstack-ai#6930's scope-boundary pin — which asserted the remaining seven still emitted
the generic message — is flipped rather than deleted: it now holds them to the
same standard as the nine, with the live column and table as in-test
non-vacuity controls.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude