fix(types): make the 8-literal union the one canonical TableColumn.type - #6370

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union
Aug 25, 2026
Merged

fix(types): make the 8-literal union the one canonical TableColumn.type#6370
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#5853

TableColumn.type had three disagreeing authorities. The interface declared 8 literals, the
zod mirror declared z.string() and accepted anything, and the renderer branched on a third
set it could only reach through an as any cast.

Implements the operative maintainer ruling — 2026-08-25, os-steve, verbatim 「其他同意」,
Option B: the 8-literal interface union is the canonical value set. The earlier
2026-08-24 ruling (canonical = the renderer's live read set, 12 values) is superseded and is
not what this PR builds.


1. Corpus measurement — ran FIRST, before any edit (the fork clause)

The ruling's step 1: measure authored out-of-union spellings before tightening, and stop
if authored usage is material
. It is not material. It is zero.

Method, with a control probe so a zero-hit reads as real. A structured walk of every
JSON file under examples/, content/, apps/, e2e/, docs/ and packages/
(591 files parsed), collecting every object carrying accessorKey (the TableColumn
discriminator) and every object inside any columns array, plus a regex sweep of the
non-JSON corpus. Control: type: 'currency' → 22 hits; type: 'banana' → 0 hits.

measurementresult
authored type: 'int' in a column position — repo-wide0
authored type: 'integer' in a column position — repo-wide0
authored type: 'float' in a column position — repo-wide0
authored type: 'double' in a column position — repo-wide0
objects with accessorKey carrying a type3 (number, text, currency — all declared)
objects with accessorKey carrying an OUT-OF-UNION type0

The only out-of-union hits in the whole corpus, with the file list the ruling asked for:

  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json$.columns[2], $.columns[4]type: "select"
  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json$.columns[3], $.columns[4]type: "select"
  • content/docs/plugins/plugin-grid.mdxtype: "select" / type: "badge" in object-grid examples

None of these is a TableColumn. Their keys are field + type, which is
ListColumn (@objectstack/spec/ui) — ObjectGrid's column INPUT vocabulary, a different
declared type that this PR does not touch. They are authored against the spec's field-type
vocabulary and keep validating exactly as before. They matter here only as evidence for §4:
they are what flows INTO the inference whose output §3 folds.

The three repo-wide type: 'integer' hits are JSON-Schema primitives in
packages/app-shell/.../json-schema-to-fields, unrelated to this key.

Verdict: fork clause not triggered — no authored document breaks.

2. Coordinates re-derived on this branch's base (e4559d1e0)

Both rulings' line numbers were stale, and so were the PM's.

itemruling / card / dispatch saidmeasured on e4559d1e0
the as any cast:2151 (ruling) / :2152 (card) / :2192 (dispatch)packages/components/src/renderers/complex/data-table.tsx:2192 ✅ dispatch correct
ObjectGrid emit seam:2206 (ruling) / :2236 (dispatch)packages/plugin-grid/src/ObjectGrid.tsx:2236 ✅ dispatch correct
zod mirrordata-display.zod.ts:109:109type: z.string().optional().describe('Column type')
interface uniondata-display.ts:255:255 ✅ unchanged from the card

One correction to the card's own model of the renderer. The card and both rulings treat
NUMERIC_EDIT_TYPES plus a datetime-local branch as the renderer's read set. Measured, the
renderer reads TableColumn.type at exactly one site (:2192editType), feeding
three branches. formatCellValue is type-agnostic (regex on the value). So the live read set
is {date, datetime, datetime-local, number, currency, percent, int, integer, float, double}
— everything else already fell through to the text input before this PR.

3. What happens to an out-of-union field type at the seam (T1)

The seam forwards an object schema's field type, whose vocabulary is @objectstack/spec's
FieldType. Measured against the installed dist: 49 options, of which 42 are outside the
union
; 7 of the 8 declared literals are spec field types and only action is UI-only.
int / integer / float / double / datetime-local are not in the spec enum at all
— the renderer's extra dialect was never a spec vocabulary.

Ruled disposition, and it is total — three outcomes, no fourth:

  1. a canonical spelling passes through unchanged;
  2. a known alias folds onto its canonical spelling (int/integer/float/doublenumber);
  3. anything else yields undefined — the type ANNOTATION is dropped, and the COLUMN IS NEVER DROPPED.

This is where the 42 spec types (select, lookup, user, file, formula, …) land, and
it is behaviour-preserving at the only consumer that reads the key: data-table's editor
branches on date / datetime / the numeric set and otherwise falls through to a text input
— which is exactly the undefined path. The dedicated widget those fields DO get comes from
the host's renderCellEditor, which resolves the field through column.accessorKey and
never reads type (ObjectGrid.tsx:3036). Pinned behaviourally, both halves: the column
still renders its header and cells, and an undeclared type opens the same editor as no type
at all.

⛔ Not folded onto 'text': that would assert something false about a lookup column and
leak the lie into any future reader. Absence says only what is true.

One addition beyond the four aliases the ruling names:datetime-localdatetime.
It is part of the same undeclared renderer dialect (it had its own editor branch at :2273),
so deleting that branch without the fold would silently drop behaviour. Repo precedent is
verbatim — packages/types/src/ui-action.ts:199 already documents datetime-local
datetime as legacy-dialect resolution. Flagged here rather than done quietly.

4. Second-producer census (step 4) — the card named one; there are two

producerwrites TableColumn.type?source of the value
plugin-grid/src/ObjectGrid.tsxyes — 5 paths4 column literals in generateColumns() (:1826, :1915, :1964, :2037) + the fieldDef.type enrichment (:2236), all verbatim
plugin-dashboard/src/ObjectDataTable.tsxyes — the SECOND producerenrich() spreads ...fieldMeta, and buildFieldMeta returns type: overrides.type ?? meta?.type (recordFields.tsx:151) — the raw field type, same defect
plugin-detail/src/RelatedList.tsx❌ nowrites fieldType (a different key) + cell; its type: def.type is inside a FieldMetadata, not a column
plugin-dashboard/DashboardRenderer.tsx:758, DashboardGridLayout.tsx:328❌ nopass author options.columns through, else deriveStaticTableColumns which writes no type

Per the ruling ("a second producer found = the same normalization at its seam"),
ObjectDataTable gets the same fold.

Why the ObjectGrid fold is a separate .map() pass rather than folded into the
enrichment map: that map early-returns for _actions and for any column whose accessorKey
has no fieldDef — and a heuristic inferColumnType() value (select, user) rides out on
exactly those columns. Folding inside it would miss them. The seam test deliberately uses an
inline-data grid with no object schema, which is that path.

5. Ablations — direction and count predicted BEFORE running

Both run under the shared verify lock. Both mutations were proved on disk (grep counts of
the injected and removed text in both directions, plus a git hash-object before/after
change) and both restores were proved byte-identical to the HEAD blob, not merely
trusted to the EXIT trap.

Neither needed a rebuild, and that is a measured claim, not an assumption:
vitest.config.mts:261 aliases @object-ui/typespackages/types/src, so these suites
resolve the subject through source, not dist — the stale-dist false-green that makes
an ablation vacuous cannot arise here.

ablationpredictedactualmatch
A — revert the zod tightening to z.string()RED, 9 failures (1 mirror-parity + 1 money + 7 out-of-union refusals)RED, 9 failed / 20 passed✅ exact
B — delete the ObjectGrid emit-seam foldRED, 4 failures (int, integer, float, double); control number and the select column-survives case still passRED, 4 failed / 2 passed — exactly those four✅ exact

Where my prediction was wrong. For ablation B I predicted the unfolded int column would
open a text input; the observed reading was null. Cause: the built-in fallback <Input>
renders with no type attribute rather than type="text", so the probe reads null.
Direction and count were right; the predicted observable was not. The assertion still
separates folded from unfolded, which is what the ablation is for.

6. Gates — each with its own verdict line and exit code

Derived from the CI job step lists under .github/workflows/. All run after the final
commit, against a2bae3d7f, on a clean tree.

gateexitits own verdict line
type-check (types, components, plugin-grid, plugin-dashboard)0all four echo type-check: Done; each runs tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are type-checked too
vitest types + plugin-grid + plugin-dashboard0Test Files 230 passed (230) · Tests 2277 passed (2277)
vitest components (batch A: renderers/ui/notifications)0Test Files 88 passed (88) · Tests 770 passed (770)
vitest components (batch B: src/__tests__)0Test Files 103 passed (103) · Tests 968 passed (968)
turbo run lint (4 packages + root)0Tasks: 5 successful, 5 total0 errors (685+912+386+244+28 pre-existing warnings, none new: my 9 changed files report errors: 0)
check-changeset-presence.mjs0✅ 9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs0✅ No changeset declares a 'major' bump.
check-changeset-fixed.mjs0✅ All workspace packages are in the changeset fixed group.
check-control-bytes.mjs0✅ check-control-bytes: OK (scanned 5241 tracked text file(s))
check:readme-exports0✅ check-readme-exports: OK (… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated); 0 unbuilt)
check:published-dist0✅ No published package's build output carries tooling material.
check:spec-symbols0✅ spec symbol derivation: 1306 files scanned against 4959 spec export names
check:self-import0✅ No package names itself inside its own src/.
check:esm-specifiers0Specifier leg: no un-ledgered package emits an extensionless relative specifier.
check:doc-types0✅ Every documented component type is registered.
check:doc-snippets0Semantic phase: 267 of 267 block(s) judged, 0 failed.the narrowing did not break a documented example
check:doc-fences0✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced …
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it.
lint:coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors
type-check:coverage0✅ test type-check coverage: 41/41 packages compile their tests
check:vi-mock-specifiers0✅ check-vi-mock-specifiers: OK (3757 tracked source file(s))
check:shell-escape-residue0✅ check-shell-escape-residue: OK (4/4 root(s) resolved)
turbo run build --filter='./packages/*'0Tasks: 39 successful, 39 total

On the components suite — it ran COMPLETE; the split is batching, not narrowing. A single
run hit the container's ~10-minute foreground cap (exit 143). Rather than narrow coverage, I
split it by directory. Population read from vitest's own config, not from my guess:
vitest list --filesOnly packages/components reports 191 files; batches A + B ran
88 + 103 = 191. (A find glob counted 193 — the two extras are .snap snapshot files,
not tests.)

check:readme-exports was red on first run and that reading was NOT MEASURED, not a
failure
: its own message said type entry ./dist/index.d.ts is not on disk -- run pnpm build first for all 236 items, none of them in a package I touched. After building all 39 packages
it is a real green with 0 unbuilt.

Gates deliberately not run locally, as CI owns them: test:e2e, test:e2e:live:ci,
check:node-esm-load (full build leg; the --specifiers-only leg is green above),
check:i18n-*, check:action-forward-parity, check:designer-field-key-parity,
check:icon-record-names, check:skills-paths, type-check:scripts,
type-check:vitest-setup — no file in this diff is an input to any of them.

7. The pin that stops this regressing (T2)

#5684's guard is key-set only — it checks key presence, and type was present on both
sides the whole time, which is exactly how this instance survived while its siblings were
caught. The value-level pin has two halves:

  • packages/types/src/__tests__/table-column-type-canonical.test.ts — interface tuple ↔ zod
    enum parity, loud refusal (money fails with type in the error path), and
    normalizeTableColumnTypetotality over all 49 spec field types.
  • packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx
    the third end neither of those can see: every literal the renderer branches on, derived
    from its source, must be a declared member. Carries blind-spot guards (no switch, no
    includes(editType), exactly one .has(editType)) so a hidden branch fails the instrument
    instead of silently under-counting it.
  • packages/plugin-grid/src/__tests__/columnTypeEmitSeam.test.tsx — the seam, pinned
    behaviourally through the editor a column actually opens, with a control probe.

A future new inference value turning these red is BY DESIGN, and the note at each pin
says so and names the two correct repairs (publish the value across all three ends, or fold it
at the producer) so the next reader does not "fix" it by loosening the mirror.

The zod mirror now builds its z.enumfromTABLE_COLUMN_TYPES rather than restating the
members, so the types↔zod leg cannot drift structurally; the pins guard the legs that cannot
be derived.

Changeset

minor for @object-ui/types (published-validator accept-set narrowing + new public API),
patch for the three others. Never majorcheck-changeset-no-major.mjs green above.
The text names the newly-refused spellings in two groups (typos/invented names; object-schema
field types written into a column slot) and states that in-repo authored metadata needs no
migration, with the §1 measurement behind it.


Generated by Claude Code

`TableColumn.type` disagreed three ways: the interface declared 8 literals,
the zod mirror declared `z.string()` and accepted anything, and the renderer
branched on a third set it could only read through an `as any` cast.
Per the maintainer ruling of 2026-08-25 (Option B), the 8-literal interface
union is canonical:
- `TABLE_COLUMN_TYPES` is now the single declaration of the vocabulary, and
the zod mirror builds its `z.enum` from it, so the two cannot drift.
- Column-inference producers fold their inferred value onto that vocabulary at
their emit seam (`normalizeTableColumnType`) instead of forwarding an object
schema's field type verbatim. Two producers do this: `ObjectGrid` and
`ObjectDataTable`. An out-of-union type drops the `type` annotation — never
the column.
- The renderer's undeclared dialect (`int`/`integer`/`float`/`double` in
`NUMERIC_EDIT_TYPES`, the `datetime-local` branch) is gone and the `as any`
cast with it, so the read is typed against what the interface publishes.
A value-level parity pin covers all three ends; objectui#5684's guard is
key-set only and cannot see value drift.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.8 KB3266.6 KB
Main entry chunk (gzip)154.1 KB350 KB
Entry fileindex-Dk1KuX-U.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)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.84KB114.57KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.18KB47.97KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.46KB34.48KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)127.52KB31.01KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.46KB54.47KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.70KB7.69KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Projects

None yet

1 participant

@os-support-ai
, '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): make the 8-literal union the one canonical TableColumn.type - #6370

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union
Aug 25, 2026
Merged

fix(types): make the 8-literal union the one canonical TableColumn.type#6370
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#5853

TableColumn.type had three disagreeing authorities. The interface declared 8 literals, the
zod mirror declared z.string() and accepted anything, and the renderer branched on a third
set it could only reach through an as any cast.

Implements the operative maintainer ruling — 2026-08-25, os-steve, verbatim 「其他同意」,
Option B: the 8-literal interface union is the canonical value set. The earlier
2026-08-24 ruling (canonical = the renderer's live read set, 12 values) is superseded and is
not what this PR builds.


1. Corpus measurement — ran FIRST, before any edit (the fork clause)

The ruling's step 1: measure authored out-of-union spellings before tightening, and stop
if authored usage is material
. It is not material. It is zero.

Method, with a control probe so a zero-hit reads as real. A structured walk of every
JSON file under examples/, content/, apps/, e2e/, docs/ and packages/
(591 files parsed), collecting every object carrying accessorKey (the TableColumn
discriminator) and every object inside any columns array, plus a regex sweep of the
non-JSON corpus. Control: type: 'currency' → 22 hits; type: 'banana' → 0 hits.

measurementresult
authored type: 'int' in a column position — repo-wide0
authored type: 'integer' in a column position — repo-wide0
authored type: 'float' in a column position — repo-wide0
authored type: 'double' in a column position — repo-wide0
objects with accessorKey carrying a type3 (number, text, currency — all declared)
objects with accessorKey carrying an OUT-OF-UNION type0

The only out-of-union hits in the whole corpus, with the file list the ruling asked for:

  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json$.columns[2], $.columns[4]type: "select"
  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json$.columns[3], $.columns[4]type: "select"
  • content/docs/plugins/plugin-grid.mdxtype: "select" / type: "badge" in object-grid examples

None of these is a TableColumn. Their keys are field + type, which is
ListColumn (@objectstack/spec/ui) — ObjectGrid's column INPUT vocabulary, a different
declared type that this PR does not touch. They are authored against the spec's field-type
vocabulary and keep validating exactly as before. They matter here only as evidence for §4:
they are what flows INTO the inference whose output §3 folds.

The three repo-wide type: 'integer' hits are JSON-Schema primitives in
packages/app-shell/.../json-schema-to-fields, unrelated to this key.

Verdict: fork clause not triggered — no authored document breaks.

2. Coordinates re-derived on this branch's base (e4559d1e0)

Both rulings' line numbers were stale, and so were the PM's.

itemruling / card / dispatch saidmeasured on e4559d1e0
the as any cast:2151 (ruling) / :2152 (card) / :2192 (dispatch)packages/components/src/renderers/complex/data-table.tsx:2192 ✅ dispatch correct
ObjectGrid emit seam:2206 (ruling) / :2236 (dispatch)packages/plugin-grid/src/ObjectGrid.tsx:2236 ✅ dispatch correct
zod mirrordata-display.zod.ts:109:109type: z.string().optional().describe('Column type')
interface uniondata-display.ts:255:255 ✅ unchanged from the card

One correction to the card's own model of the renderer. The card and both rulings treat
NUMERIC_EDIT_TYPES plus a datetime-local branch as the renderer's read set. Measured, the
renderer reads TableColumn.type at exactly one site (:2192editType), feeding
three branches. formatCellValue is type-agnostic (regex on the value). So the live read set
is {date, datetime, datetime-local, number, currency, percent, int, integer, float, double}
— everything else already fell through to the text input before this PR.

3. What happens to an out-of-union field type at the seam (T1)

The seam forwards an object schema's field type, whose vocabulary is @objectstack/spec's
FieldType. Measured against the installed dist: 49 options, of which 42 are outside the
union
; 7 of the 8 declared literals are spec field types and only action is UI-only.
int / integer / float / double / datetime-local are not in the spec enum at all
— the renderer's extra dialect was never a spec vocabulary.

Ruled disposition, and it is total — three outcomes, no fourth:

  1. a canonical spelling passes through unchanged;
  2. a known alias folds onto its canonical spelling (int/integer/float/doublenumber);
  3. anything else yields undefined — the type ANNOTATION is dropped, and the COLUMN IS NEVER DROPPED.

This is where the 42 spec types (select, lookup, user, file, formula, …) land, and
it is behaviour-preserving at the only consumer that reads the key: data-table's editor
branches on date / datetime / the numeric set and otherwise falls through to a text input
— which is exactly the undefined path. The dedicated widget those fields DO get comes from
the host's renderCellEditor, which resolves the field through column.accessorKey and
never reads type (ObjectGrid.tsx:3036). Pinned behaviourally, both halves: the column
still renders its header and cells, and an undeclared type opens the same editor as no type
at all.

⛔ Not folded onto 'text': that would assert something false about a lookup column and
leak the lie into any future reader. Absence says only what is true.

One addition beyond the four aliases the ruling names:datetime-localdatetime.
It is part of the same undeclared renderer dialect (it had its own editor branch at :2273),
so deleting that branch without the fold would silently drop behaviour. Repo precedent is
verbatim — packages/types/src/ui-action.ts:199 already documents datetime-local
datetime as legacy-dialect resolution. Flagged here rather than done quietly.

4. Second-producer census (step 4) — the card named one; there are two

producerwrites TableColumn.type?source of the value
plugin-grid/src/ObjectGrid.tsxyes — 5 paths4 column literals in generateColumns() (:1826, :1915, :1964, :2037) + the fieldDef.type enrichment (:2236), all verbatim
plugin-dashboard/src/ObjectDataTable.tsxyes — the SECOND producerenrich() spreads ...fieldMeta, and buildFieldMeta returns type: overrides.type ?? meta?.type (recordFields.tsx:151) — the raw field type, same defect
plugin-detail/src/RelatedList.tsx❌ nowrites fieldType (a different key) + cell; its type: def.type is inside a FieldMetadata, not a column
plugin-dashboard/DashboardRenderer.tsx:758, DashboardGridLayout.tsx:328❌ nopass author options.columns through, else deriveStaticTableColumns which writes no type

Per the ruling ("a second producer found = the same normalization at its seam"),
ObjectDataTable gets the same fold.

Why the ObjectGrid fold is a separate .map() pass rather than folded into the
enrichment map: that map early-returns for _actions and for any column whose accessorKey
has no fieldDef — and a heuristic inferColumnType() value (select, user) rides out on
exactly those columns. Folding inside it would miss them. The seam test deliberately uses an
inline-data grid with no object schema, which is that path.

5. Ablations — direction and count predicted BEFORE running

Both run under the shared verify lock. Both mutations were proved on disk (grep counts of
the injected and removed text in both directions, plus a git hash-object before/after
change) and both restores were proved byte-identical to the HEAD blob, not merely
trusted to the EXIT trap.

Neither needed a rebuild, and that is a measured claim, not an assumption:
vitest.config.mts:261 aliases @object-ui/typespackages/types/src, so these suites
resolve the subject through source, not dist — the stale-dist false-green that makes
an ablation vacuous cannot arise here.

ablationpredictedactualmatch
A — revert the zod tightening to z.string()RED, 9 failures (1 mirror-parity + 1 money + 7 out-of-union refusals)RED, 9 failed / 20 passed✅ exact
B — delete the ObjectGrid emit-seam foldRED, 4 failures (int, integer, float, double); control number and the select column-survives case still passRED, 4 failed / 2 passed — exactly those four✅ exact

Where my prediction was wrong. For ablation B I predicted the unfolded int column would
open a text input; the observed reading was null. Cause: the built-in fallback <Input>
renders with no type attribute rather than type="text", so the probe reads null.
Direction and count were right; the predicted observable was not. The assertion still
separates folded from unfolded, which is what the ablation is for.

6. Gates — each with its own verdict line and exit code

Derived from the CI job step lists under .github/workflows/. All run after the final
commit, against a2bae3d7f, on a clean tree.

gateexitits own verdict line
type-check (types, components, plugin-grid, plugin-dashboard)0all four echo type-check: Done; each runs tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are type-checked too
vitest types + plugin-grid + plugin-dashboard0Test Files 230 passed (230) · Tests 2277 passed (2277)
vitest components (batch A: renderers/ui/notifications)0Test Files 88 passed (88) · Tests 770 passed (770)
vitest components (batch B: src/__tests__)0Test Files 103 passed (103) · Tests 968 passed (968)
turbo run lint (4 packages + root)0Tasks: 5 successful, 5 total0 errors (685+912+386+244+28 pre-existing warnings, none new: my 9 changed files report errors: 0)
check-changeset-presence.mjs0✅ 9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs0✅ No changeset declares a 'major' bump.
check-changeset-fixed.mjs0✅ All workspace packages are in the changeset fixed group.
check-control-bytes.mjs0✅ check-control-bytes: OK (scanned 5241 tracked text file(s))
check:readme-exports0✅ check-readme-exports: OK (… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated); 0 unbuilt)
check:published-dist0✅ No published package's build output carries tooling material.
check:spec-symbols0✅ spec symbol derivation: 1306 files scanned against 4959 spec export names
check:self-import0✅ No package names itself inside its own src/.
check:esm-specifiers0Specifier leg: no un-ledgered package emits an extensionless relative specifier.
check:doc-types0✅ Every documented component type is registered.
check:doc-snippets0Semantic phase: 267 of 267 block(s) judged, 0 failed.the narrowing did not break a documented example
check:doc-fences0✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced …
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it.
lint:coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors
type-check:coverage0✅ test type-check coverage: 41/41 packages compile their tests
check:vi-mock-specifiers0✅ check-vi-mock-specifiers: OK (3757 tracked source file(s))
check:shell-escape-residue0✅ check-shell-escape-residue: OK (4/4 root(s) resolved)
turbo run build --filter='./packages/*'0Tasks: 39 successful, 39 total

On the components suite — it ran COMPLETE; the split is batching, not narrowing. A single
run hit the container's ~10-minute foreground cap (exit 143). Rather than narrow coverage, I
split it by directory. Population read from vitest's own config, not from my guess:
vitest list --filesOnly packages/components reports 191 files; batches A + B ran
88 + 103 = 191. (A find glob counted 193 — the two extras are .snap snapshot files,
not tests.)

check:readme-exports was red on first run and that reading was NOT MEASURED, not a
failure
: its own message said type entry ./dist/index.d.ts is not on disk -- run pnpm build first for all 236 items, none of them in a package I touched. After building all 39 packages
it is a real green with 0 unbuilt.

Gates deliberately not run locally, as CI owns them: test:e2e, test:e2e:live:ci,
check:node-esm-load (full build leg; the --specifiers-only leg is green above),
check:i18n-*, check:action-forward-parity, check:designer-field-key-parity,
check:icon-record-names, check:skills-paths, type-check:scripts,
type-check:vitest-setup — no file in this diff is an input to any of them.

7. The pin that stops this regressing (T2)

#5684's guard is key-set only — it checks key presence, and type was present on both
sides the whole time, which is exactly how this instance survived while its siblings were
caught. The value-level pin has two halves:

  • packages/types/src/__tests__/table-column-type-canonical.test.ts — interface tuple ↔ zod
    enum parity, loud refusal (money fails with type in the error path), and
    normalizeTableColumnTypetotality over all 49 spec field types.
  • packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx
    the third end neither of those can see: every literal the renderer branches on, derived
    from its source, must be a declared member. Carries blind-spot guards (no switch, no
    includes(editType), exactly one .has(editType)) so a hidden branch fails the instrument
    instead of silently under-counting it.
  • packages/plugin-grid/src/__tests__/columnTypeEmitSeam.test.tsx — the seam, pinned
    behaviourally through the editor a column actually opens, with a control probe.

A future new inference value turning these red is BY DESIGN, and the note at each pin
says so and names the two correct repairs (publish the value across all three ends, or fold it
at the producer) so the next reader does not "fix" it by loosening the mirror.

The zod mirror now builds its z.enumfromTABLE_COLUMN_TYPES rather than restating the
members, so the types↔zod leg cannot drift structurally; the pins guard the legs that cannot
be derived.

Changeset

minor for @object-ui/types (published-validator accept-set narrowing + new public API),
patch for the three others. Never majorcheck-changeset-no-major.mjs green above.
The text names the newly-refused spellings in two groups (typos/invented names; object-schema
field types written into a column slot) and states that in-repo authored metadata needs no
migration, with the §1 measurement behind it.


Generated by Claude Code

`TableColumn.type` disagreed three ways: the interface declared 8 literals,
the zod mirror declared `z.string()` and accepted anything, and the renderer
branched on a third set it could only read through an `as any` cast.
Per the maintainer ruling of 2026-08-25 (Option B), the 8-literal interface
union is canonical:
- `TABLE_COLUMN_TYPES` is now the single declaration of the vocabulary, and
the zod mirror builds its `z.enum` from it, so the two cannot drift.
- Column-inference producers fold their inferred value onto that vocabulary at
their emit seam (`normalizeTableColumnType`) instead of forwarding an object
schema's field type verbatim. Two producers do this: `ObjectGrid` and
`ObjectDataTable`. An out-of-union type drops the `type` annotation — never
the column.
- The renderer's undeclared dialect (`int`/`integer`/`float`/`double` in
`NUMERIC_EDIT_TYPES`, the `datetime-local` branch) is gone and the `as any`
cast with it, so the read is typed against what the interface publishes.
A value-level parity pin covers all three ends; objectui#5684's guard is
key-set only and cannot see value drift.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.8 KB3266.6 KB
Main entry chunk (gzip)154.1 KB350 KB
Entry fileindex-Dk1KuX-U.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)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.84KB114.57KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.18KB47.97KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.46KB34.48KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)127.52KB31.01KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.46KB54.47KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.70KB7.69KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Projects

None yet

1 participant

@os-support-ai
, '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): make the 8-literal union the one canonical TableColumn.type - #6370

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union
Aug 25, 2026
Merged

fix(types): make the 8-literal union the one canonical TableColumn.type#6370
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#5853

TableColumn.type had three disagreeing authorities. The interface declared 8 literals, the
zod mirror declared z.string() and accepted anything, and the renderer branched on a third
set it could only reach through an as any cast.

Implements the operative maintainer ruling — 2026-08-25, os-steve, verbatim 「其他同意」,
Option B: the 8-literal interface union is the canonical value set. The earlier
2026-08-24 ruling (canonical = the renderer's live read set, 12 values) is superseded and is
not what this PR builds.


1. Corpus measurement — ran FIRST, before any edit (the fork clause)

The ruling's step 1: measure authored out-of-union spellings before tightening, and stop
if authored usage is material
. It is not material. It is zero.

Method, with a control probe so a zero-hit reads as real. A structured walk of every
JSON file under examples/, content/, apps/, e2e/, docs/ and packages/
(591 files parsed), collecting every object carrying accessorKey (the TableColumn
discriminator) and every object inside any columns array, plus a regex sweep of the
non-JSON corpus. Control: type: 'currency' → 22 hits; type: 'banana' → 0 hits.

measurementresult
authored type: 'int' in a column position — repo-wide0
authored type: 'integer' in a column position — repo-wide0
authored type: 'float' in a column position — repo-wide0
authored type: 'double' in a column position — repo-wide0
objects with accessorKey carrying a type3 (number, text, currency — all declared)
objects with accessorKey carrying an OUT-OF-UNION type0

The only out-of-union hits in the whole corpus, with the file list the ruling asked for:

  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json$.columns[2], $.columns[4]type: "select"
  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json$.columns[3], $.columns[4]type: "select"
  • content/docs/plugins/plugin-grid.mdxtype: "select" / type: "badge" in object-grid examples

None of these is a TableColumn. Their keys are field + type, which is
ListColumn (@objectstack/spec/ui) — ObjectGrid's column INPUT vocabulary, a different
declared type that this PR does not touch. They are authored against the spec's field-type
vocabulary and keep validating exactly as before. They matter here only as evidence for §4:
they are what flows INTO the inference whose output §3 folds.

The three repo-wide type: 'integer' hits are JSON-Schema primitives in
packages/app-shell/.../json-schema-to-fields, unrelated to this key.

Verdict: fork clause not triggered — no authored document breaks.

2. Coordinates re-derived on this branch's base (e4559d1e0)

Both rulings' line numbers were stale, and so were the PM's.

itemruling / card / dispatch saidmeasured on e4559d1e0
the as any cast:2151 (ruling) / :2152 (card) / :2192 (dispatch)packages/components/src/renderers/complex/data-table.tsx:2192 ✅ dispatch correct
ObjectGrid emit seam:2206 (ruling) / :2236 (dispatch)packages/plugin-grid/src/ObjectGrid.tsx:2236 ✅ dispatch correct
zod mirrordata-display.zod.ts:109:109type: z.string().optional().describe('Column type')
interface uniondata-display.ts:255:255 ✅ unchanged from the card

One correction to the card's own model of the renderer. The card and both rulings treat
NUMERIC_EDIT_TYPES plus a datetime-local branch as the renderer's read set. Measured, the
renderer reads TableColumn.type at exactly one site (:2192editType), feeding
three branches. formatCellValue is type-agnostic (regex on the value). So the live read set
is {date, datetime, datetime-local, number, currency, percent, int, integer, float, double}
— everything else already fell through to the text input before this PR.

3. What happens to an out-of-union field type at the seam (T1)

The seam forwards an object schema's field type, whose vocabulary is @objectstack/spec's
FieldType. Measured against the installed dist: 49 options, of which 42 are outside the
union
; 7 of the 8 declared literals are spec field types and only action is UI-only.
int / integer / float / double / datetime-local are not in the spec enum at all
— the renderer's extra dialect was never a spec vocabulary.

Ruled disposition, and it is total — three outcomes, no fourth:

  1. a canonical spelling passes through unchanged;
  2. a known alias folds onto its canonical spelling (int/integer/float/doublenumber);
  3. anything else yields undefined — the type ANNOTATION is dropped, and the COLUMN IS NEVER DROPPED.

This is where the 42 spec types (select, lookup, user, file, formula, …) land, and
it is behaviour-preserving at the only consumer that reads the key: data-table's editor
branches on date / datetime / the numeric set and otherwise falls through to a text input
— which is exactly the undefined path. The dedicated widget those fields DO get comes from
the host's renderCellEditor, which resolves the field through column.accessorKey and
never reads type (ObjectGrid.tsx:3036). Pinned behaviourally, both halves: the column
still renders its header and cells, and an undeclared type opens the same editor as no type
at all.

⛔ Not folded onto 'text': that would assert something false about a lookup column and
leak the lie into any future reader. Absence says only what is true.

One addition beyond the four aliases the ruling names:datetime-localdatetime.
It is part of the same undeclared renderer dialect (it had its own editor branch at :2273),
so deleting that branch without the fold would silently drop behaviour. Repo precedent is
verbatim — packages/types/src/ui-action.ts:199 already documents datetime-local
datetime as legacy-dialect resolution. Flagged here rather than done quietly.

4. Second-producer census (step 4) — the card named one; there are two

producerwrites TableColumn.type?source of the value
plugin-grid/src/ObjectGrid.tsxyes — 5 paths4 column literals in generateColumns() (:1826, :1915, :1964, :2037) + the fieldDef.type enrichment (:2236), all verbatim
plugin-dashboard/src/ObjectDataTable.tsxyes — the SECOND producerenrich() spreads ...fieldMeta, and buildFieldMeta returns type: overrides.type ?? meta?.type (recordFields.tsx:151) — the raw field type, same defect
plugin-detail/src/RelatedList.tsx❌ nowrites fieldType (a different key) + cell; its type: def.type is inside a FieldMetadata, not a column
plugin-dashboard/DashboardRenderer.tsx:758, DashboardGridLayout.tsx:328❌ nopass author options.columns through, else deriveStaticTableColumns which writes no type

Per the ruling ("a second producer found = the same normalization at its seam"),
ObjectDataTable gets the same fold.

Why the ObjectGrid fold is a separate .map() pass rather than folded into the
enrichment map: that map early-returns for _actions and for any column whose accessorKey
has no fieldDef — and a heuristic inferColumnType() value (select, user) rides out on
exactly those columns. Folding inside it would miss them. The seam test deliberately uses an
inline-data grid with no object schema, which is that path.

5. Ablations — direction and count predicted BEFORE running

Both run under the shared verify lock. Both mutations were proved on disk (grep counts of
the injected and removed text in both directions, plus a git hash-object before/after
change) and both restores were proved byte-identical to the HEAD blob, not merely
trusted to the EXIT trap.

Neither needed a rebuild, and that is a measured claim, not an assumption:
vitest.config.mts:261 aliases @object-ui/typespackages/types/src, so these suites
resolve the subject through source, not dist — the stale-dist false-green that makes
an ablation vacuous cannot arise here.

ablationpredictedactualmatch
A — revert the zod tightening to z.string()RED, 9 failures (1 mirror-parity + 1 money + 7 out-of-union refusals)RED, 9 failed / 20 passed✅ exact
B — delete the ObjectGrid emit-seam foldRED, 4 failures (int, integer, float, double); control number and the select column-survives case still passRED, 4 failed / 2 passed — exactly those four✅ exact

Where my prediction was wrong. For ablation B I predicted the unfolded int column would
open a text input; the observed reading was null. Cause: the built-in fallback <Input>
renders with no type attribute rather than type="text", so the probe reads null.
Direction and count were right; the predicted observable was not. The assertion still
separates folded from unfolded, which is what the ablation is for.

6. Gates — each with its own verdict line and exit code

Derived from the CI job step lists under .github/workflows/. All run after the final
commit, against a2bae3d7f, on a clean tree.

gateexitits own verdict line
type-check (types, components, plugin-grid, plugin-dashboard)0all four echo type-check: Done; each runs tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are type-checked too
vitest types + plugin-grid + plugin-dashboard0Test Files 230 passed (230) · Tests 2277 passed (2277)
vitest components (batch A: renderers/ui/notifications)0Test Files 88 passed (88) · Tests 770 passed (770)
vitest components (batch B: src/__tests__)0Test Files 103 passed (103) · Tests 968 passed (968)
turbo run lint (4 packages + root)0Tasks: 5 successful, 5 total0 errors (685+912+386+244+28 pre-existing warnings, none new: my 9 changed files report errors: 0)
check-changeset-presence.mjs0✅ 9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs0✅ No changeset declares a 'major' bump.
check-changeset-fixed.mjs0✅ All workspace packages are in the changeset fixed group.
check-control-bytes.mjs0✅ check-control-bytes: OK (scanned 5241 tracked text file(s))
check:readme-exports0✅ check-readme-exports: OK (… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated); 0 unbuilt)
check:published-dist0✅ No published package's build output carries tooling material.
check:spec-symbols0✅ spec symbol derivation: 1306 files scanned against 4959 spec export names
check:self-import0✅ No package names itself inside its own src/.
check:esm-specifiers0Specifier leg: no un-ledgered package emits an extensionless relative specifier.
check:doc-types0✅ Every documented component type is registered.
check:doc-snippets0Semantic phase: 267 of 267 block(s) judged, 0 failed.the narrowing did not break a documented example
check:doc-fences0✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced …
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it.
lint:coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors
type-check:coverage0✅ test type-check coverage: 41/41 packages compile their tests
check:vi-mock-specifiers0✅ check-vi-mock-specifiers: OK (3757 tracked source file(s))
check:shell-escape-residue0✅ check-shell-escape-residue: OK (4/4 root(s) resolved)
turbo run build --filter='./packages/*'0Tasks: 39 successful, 39 total

On the components suite — it ran COMPLETE; the split is batching, not narrowing. A single
run hit the container's ~10-minute foreground cap (exit 143). Rather than narrow coverage, I
split it by directory. Population read from vitest's own config, not from my guess:
vitest list --filesOnly packages/components reports 191 files; batches A + B ran
88 + 103 = 191. (A find glob counted 193 — the two extras are .snap snapshot files,
not tests.)

check:readme-exports was red on first run and that reading was NOT MEASURED, not a
failure
: its own message said type entry ./dist/index.d.ts is not on disk -- run pnpm build first for all 236 items, none of them in a package I touched. After building all 39 packages
it is a real green with 0 unbuilt.

Gates deliberately not run locally, as CI owns them: test:e2e, test:e2e:live:ci,
check:node-esm-load (full build leg; the --specifiers-only leg is green above),
check:i18n-*, check:action-forward-parity, check:designer-field-key-parity,
check:icon-record-names, check:skills-paths, type-check:scripts,
type-check:vitest-setup — no file in this diff is an input to any of them.

7. The pin that stops this regressing (T2)

#5684's guard is key-set only — it checks key presence, and type was present on both
sides the whole time, which is exactly how this instance survived while its siblings were
caught. The value-level pin has two halves:

  • packages/types/src/__tests__/table-column-type-canonical.test.ts — interface tuple ↔ zod
    enum parity, loud refusal (money fails with type in the error path), and
    normalizeTableColumnTypetotality over all 49 spec field types.
  • packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx
    the third end neither of those can see: every literal the renderer branches on, derived
    from its source, must be a declared member. Carries blind-spot guards (no switch, no
    includes(editType), exactly one .has(editType)) so a hidden branch fails the instrument
    instead of silently under-counting it.
  • packages/plugin-grid/src/__tests__/columnTypeEmitSeam.test.tsx — the seam, pinned
    behaviourally through the editor a column actually opens, with a control probe.

A future new inference value turning these red is BY DESIGN, and the note at each pin
says so and names the two correct repairs (publish the value across all three ends, or fold it
at the producer) so the next reader does not "fix" it by loosening the mirror.

The zod mirror now builds its z.enumfromTABLE_COLUMN_TYPES rather than restating the
members, so the types↔zod leg cannot drift structurally; the pins guard the legs that cannot
be derived.

Changeset

minor for @object-ui/types (published-validator accept-set narrowing + new public API),
patch for the three others. Never majorcheck-changeset-no-major.mjs green above.
The text names the newly-refused spellings in two groups (typos/invented names; object-schema
field types written into a column slot) and states that in-repo authored metadata needs no
migration, with the §1 measurement behind it.


Generated by Claude Code

`TableColumn.type` disagreed three ways: the interface declared 8 literals,
the zod mirror declared `z.string()` and accepted anything, and the renderer
branched on a third set it could only read through an `as any` cast.
Per the maintainer ruling of 2026-08-25 (Option B), the 8-literal interface
union is canonical:
- `TABLE_COLUMN_TYPES` is now the single declaration of the vocabulary, and
the zod mirror builds its `z.enum` from it, so the two cannot drift.
- Column-inference producers fold their inferred value onto that vocabulary at
their emit seam (`normalizeTableColumnType`) instead of forwarding an object
schema's field type verbatim. Two producers do this: `ObjectGrid` and
`ObjectDataTable`. An out-of-union type drops the `type` annotation — never
the column.
- The renderer's undeclared dialect (`int`/`integer`/`float`/`double` in
`NUMERIC_EDIT_TYPES`, the `datetime-local` branch) is gone and the `as any`
cast with it, so the read is typed against what the interface publishes.
A value-level parity pin covers all three ends; objectui#5684's guard is
key-set only and cannot see value drift.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.8 KB3266.6 KB
Main entry chunk (gzip)154.1 KB350 KB
Entry fileindex-Dk1KuX-U.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)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.84KB114.57KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.18KB47.97KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.46KB34.48KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)127.52KB31.01KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.46KB54.47KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.70KB7.69KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Projects

None yet

1 participant

@os-support-ai
, '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): make the 8-literal union the one canonical TableColumn.type - #6370

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union
Aug 25, 2026
Merged

fix(types): make the 8-literal union the one canonical TableColumn.type#6370
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#5853

TableColumn.type had three disagreeing authorities. The interface declared 8 literals, the
zod mirror declared z.string() and accepted anything, and the renderer branched on a third
set it could only reach through an as any cast.

Implements the operative maintainer ruling — 2026-08-25, os-steve, verbatim 「其他同意」,
Option B: the 8-literal interface union is the canonical value set. The earlier
2026-08-24 ruling (canonical = the renderer's live read set, 12 values) is superseded and is
not what this PR builds.


1. Corpus measurement — ran FIRST, before any edit (the fork clause)

The ruling's step 1: measure authored out-of-union spellings before tightening, and stop
if authored usage is material
. It is not material. It is zero.

Method, with a control probe so a zero-hit reads as real. A structured walk of every
JSON file under examples/, content/, apps/, e2e/, docs/ and packages/
(591 files parsed), collecting every object carrying accessorKey (the TableColumn
discriminator) and every object inside any columns array, plus a regex sweep of the
non-JSON corpus. Control: type: 'currency' → 22 hits; type: 'banana' → 0 hits.

measurementresult
authored type: 'int' in a column position — repo-wide0
authored type: 'integer' in a column position — repo-wide0
authored type: 'float' in a column position — repo-wide0
authored type: 'double' in a column position — repo-wide0
objects with accessorKey carrying a type3 (number, text, currency — all declared)
objects with accessorKey carrying an OUT-OF-UNION type0

The only out-of-union hits in the whole corpus, with the file list the ruling asked for:

  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json$.columns[2], $.columns[4]type: "select"
  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json$.columns[3], $.columns[4]type: "select"
  • content/docs/plugins/plugin-grid.mdxtype: "select" / type: "badge" in object-grid examples

None of these is a TableColumn. Their keys are field + type, which is
ListColumn (@objectstack/spec/ui) — ObjectGrid's column INPUT vocabulary, a different
declared type that this PR does not touch. They are authored against the spec's field-type
vocabulary and keep validating exactly as before. They matter here only as evidence for §4:
they are what flows INTO the inference whose output §3 folds.

The three repo-wide type: 'integer' hits are JSON-Schema primitives in
packages/app-shell/.../json-schema-to-fields, unrelated to this key.

Verdict: fork clause not triggered — no authored document breaks.

2. Coordinates re-derived on this branch's base (e4559d1e0)

Both rulings' line numbers were stale, and so were the PM's.

itemruling / card / dispatch saidmeasured on e4559d1e0
the as any cast:2151 (ruling) / :2152 (card) / :2192 (dispatch)packages/components/src/renderers/complex/data-table.tsx:2192 ✅ dispatch correct
ObjectGrid emit seam:2206 (ruling) / :2236 (dispatch)packages/plugin-grid/src/ObjectGrid.tsx:2236 ✅ dispatch correct
zod mirrordata-display.zod.ts:109:109type: z.string().optional().describe('Column type')
interface uniondata-display.ts:255:255 ✅ unchanged from the card

One correction to the card's own model of the renderer. The card and both rulings treat
NUMERIC_EDIT_TYPES plus a datetime-local branch as the renderer's read set. Measured, the
renderer reads TableColumn.type at exactly one site (:2192editType), feeding
three branches. formatCellValue is type-agnostic (regex on the value). So the live read set
is {date, datetime, datetime-local, number, currency, percent, int, integer, float, double}
— everything else already fell through to the text input before this PR.

3. What happens to an out-of-union field type at the seam (T1)

The seam forwards an object schema's field type, whose vocabulary is @objectstack/spec's
FieldType. Measured against the installed dist: 49 options, of which 42 are outside the
union
; 7 of the 8 declared literals are spec field types and only action is UI-only.
int / integer / float / double / datetime-local are not in the spec enum at all
— the renderer's extra dialect was never a spec vocabulary.

Ruled disposition, and it is total — three outcomes, no fourth:

  1. a canonical spelling passes through unchanged;
  2. a known alias folds onto its canonical spelling (int/integer/float/doublenumber);
  3. anything else yields undefined — the type ANNOTATION is dropped, and the COLUMN IS NEVER DROPPED.

This is where the 42 spec types (select, lookup, user, file, formula, …) land, and
it is behaviour-preserving at the only consumer that reads the key: data-table's editor
branches on date / datetime / the numeric set and otherwise falls through to a text input
— which is exactly the undefined path. The dedicated widget those fields DO get comes from
the host's renderCellEditor, which resolves the field through column.accessorKey and
never reads type (ObjectGrid.tsx:3036). Pinned behaviourally, both halves: the column
still renders its header and cells, and an undeclared type opens the same editor as no type
at all.

⛔ Not folded onto 'text': that would assert something false about a lookup column and
leak the lie into any future reader. Absence says only what is true.

One addition beyond the four aliases the ruling names:datetime-localdatetime.
It is part of the same undeclared renderer dialect (it had its own editor branch at :2273),
so deleting that branch without the fold would silently drop behaviour. Repo precedent is
verbatim — packages/types/src/ui-action.ts:199 already documents datetime-local
datetime as legacy-dialect resolution. Flagged here rather than done quietly.

4. Second-producer census (step 4) — the card named one; there are two

producerwrites TableColumn.type?source of the value
plugin-grid/src/ObjectGrid.tsxyes — 5 paths4 column literals in generateColumns() (:1826, :1915, :1964, :2037) + the fieldDef.type enrichment (:2236), all verbatim
plugin-dashboard/src/ObjectDataTable.tsxyes — the SECOND producerenrich() spreads ...fieldMeta, and buildFieldMeta returns type: overrides.type ?? meta?.type (recordFields.tsx:151) — the raw field type, same defect
plugin-detail/src/RelatedList.tsx❌ nowrites fieldType (a different key) + cell; its type: def.type is inside a FieldMetadata, not a column
plugin-dashboard/DashboardRenderer.tsx:758, DashboardGridLayout.tsx:328❌ nopass author options.columns through, else deriveStaticTableColumns which writes no type

Per the ruling ("a second producer found = the same normalization at its seam"),
ObjectDataTable gets the same fold.

Why the ObjectGrid fold is a separate .map() pass rather than folded into the
enrichment map: that map early-returns for _actions and for any column whose accessorKey
has no fieldDef — and a heuristic inferColumnType() value (select, user) rides out on
exactly those columns. Folding inside it would miss them. The seam test deliberately uses an
inline-data grid with no object schema, which is that path.

5. Ablations — direction and count predicted BEFORE running

Both run under the shared verify lock. Both mutations were proved on disk (grep counts of
the injected and removed text in both directions, plus a git hash-object before/after
change) and both restores were proved byte-identical to the HEAD blob, not merely
trusted to the EXIT trap.

Neither needed a rebuild, and that is a measured claim, not an assumption:
vitest.config.mts:261 aliases @object-ui/typespackages/types/src, so these suites
resolve the subject through source, not dist — the stale-dist false-green that makes
an ablation vacuous cannot arise here.

ablationpredictedactualmatch
A — revert the zod tightening to z.string()RED, 9 failures (1 mirror-parity + 1 money + 7 out-of-union refusals)RED, 9 failed / 20 passed✅ exact
B — delete the ObjectGrid emit-seam foldRED, 4 failures (int, integer, float, double); control number and the select column-survives case still passRED, 4 failed / 2 passed — exactly those four✅ exact

Where my prediction was wrong. For ablation B I predicted the unfolded int column would
open a text input; the observed reading was null. Cause: the built-in fallback <Input>
renders with no type attribute rather than type="text", so the probe reads null.
Direction and count were right; the predicted observable was not. The assertion still
separates folded from unfolded, which is what the ablation is for.

6. Gates — each with its own verdict line and exit code

Derived from the CI job step lists under .github/workflows/. All run after the final
commit, against a2bae3d7f, on a clean tree.

gateexitits own verdict line
type-check (types, components, plugin-grid, plugin-dashboard)0all four echo type-check: Done; each runs tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are type-checked too
vitest types + plugin-grid + plugin-dashboard0Test Files 230 passed (230) · Tests 2277 passed (2277)
vitest components (batch A: renderers/ui/notifications)0Test Files 88 passed (88) · Tests 770 passed (770)
vitest components (batch B: src/__tests__)0Test Files 103 passed (103) · Tests 968 passed (968)
turbo run lint (4 packages + root)0Tasks: 5 successful, 5 total0 errors (685+912+386+244+28 pre-existing warnings, none new: my 9 changed files report errors: 0)
check-changeset-presence.mjs0✅ 9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs0✅ No changeset declares a 'major' bump.
check-changeset-fixed.mjs0✅ All workspace packages are in the changeset fixed group.
check-control-bytes.mjs0✅ check-control-bytes: OK (scanned 5241 tracked text file(s))
check:readme-exports0✅ check-readme-exports: OK (… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated); 0 unbuilt)
check:published-dist0✅ No published package's build output carries tooling material.
check:spec-symbols0✅ spec symbol derivation: 1306 files scanned against 4959 spec export names
check:self-import0✅ No package names itself inside its own src/.
check:esm-specifiers0Specifier leg: no un-ledgered package emits an extensionless relative specifier.
check:doc-types0✅ Every documented component type is registered.
check:doc-snippets0Semantic phase: 267 of 267 block(s) judged, 0 failed.the narrowing did not break a documented example
check:doc-fences0✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced …
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it.
lint:coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors
type-check:coverage0✅ test type-check coverage: 41/41 packages compile their tests
check:vi-mock-specifiers0✅ check-vi-mock-specifiers: OK (3757 tracked source file(s))
check:shell-escape-residue0✅ check-shell-escape-residue: OK (4/4 root(s) resolved)
turbo run build --filter='./packages/*'0Tasks: 39 successful, 39 total

On the components suite — it ran COMPLETE; the split is batching, not narrowing. A single
run hit the container's ~10-minute foreground cap (exit 143). Rather than narrow coverage, I
split it by directory. Population read from vitest's own config, not from my guess:
vitest list --filesOnly packages/components reports 191 files; batches A + B ran
88 + 103 = 191. (A find glob counted 193 — the two extras are .snap snapshot files,
not tests.)

check:readme-exports was red on first run and that reading was NOT MEASURED, not a
failure
: its own message said type entry ./dist/index.d.ts is not on disk -- run pnpm build first for all 236 items, none of them in a package I touched. After building all 39 packages
it is a real green with 0 unbuilt.

Gates deliberately not run locally, as CI owns them: test:e2e, test:e2e:live:ci,
check:node-esm-load (full build leg; the --specifiers-only leg is green above),
check:i18n-*, check:action-forward-parity, check:designer-field-key-parity,
check:icon-record-names, check:skills-paths, type-check:scripts,
type-check:vitest-setup — no file in this diff is an input to any of them.

7. The pin that stops this regressing (T2)

#5684's guard is key-set only — it checks key presence, and type was present on both
sides the whole time, which is exactly how this instance survived while its siblings were
caught. The value-level pin has two halves:

  • packages/types/src/__tests__/table-column-type-canonical.test.ts — interface tuple ↔ zod
    enum parity, loud refusal (money fails with type in the error path), and
    normalizeTableColumnTypetotality over all 49 spec field types.
  • packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx
    the third end neither of those can see: every literal the renderer branches on, derived
    from its source, must be a declared member. Carries blind-spot guards (no switch, no
    includes(editType), exactly one .has(editType)) so a hidden branch fails the instrument
    instead of silently under-counting it.
  • packages/plugin-grid/src/__tests__/columnTypeEmitSeam.test.tsx — the seam, pinned
    behaviourally through the editor a column actually opens, with a control probe.

A future new inference value turning these red is BY DESIGN, and the note at each pin
says so and names the two correct repairs (publish the value across all three ends, or fold it
at the producer) so the next reader does not "fix" it by loosening the mirror.

The zod mirror now builds its z.enumfromTABLE_COLUMN_TYPES rather than restating the
members, so the types↔zod leg cannot drift structurally; the pins guard the legs that cannot
be derived.

Changeset

minor for @object-ui/types (published-validator accept-set narrowing + new public API),
patch for the three others. Never majorcheck-changeset-no-major.mjs green above.
The text names the newly-refused spellings in two groups (typos/invented names; object-schema
field types written into a column slot) and states that in-repo authored metadata needs no
migration, with the §1 measurement behind it.


Generated by Claude Code

`TableColumn.type` disagreed three ways: the interface declared 8 literals,
the zod mirror declared `z.string()` and accepted anything, and the renderer
branched on a third set it could only read through an `as any` cast.
Per the maintainer ruling of 2026-08-25 (Option B), the 8-literal interface
union is canonical:
- `TABLE_COLUMN_TYPES` is now the single declaration of the vocabulary, and
the zod mirror builds its `z.enum` from it, so the two cannot drift.
- Column-inference producers fold their inferred value onto that vocabulary at
their emit seam (`normalizeTableColumnType`) instead of forwarding an object
schema's field type verbatim. Two producers do this: `ObjectGrid` and
`ObjectDataTable`. An out-of-union type drops the `type` annotation — never
the column.
- The renderer's undeclared dialect (`int`/`integer`/`float`/`double` in
`NUMERIC_EDIT_TYPES`, the `datetime-local` branch) is gone and the `as any`
cast with it, so the read is typed against what the interface publishes.
A value-level parity pin covers all three ends; objectui#5684's guard is
key-set only and cannot see value drift.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.8 KB3266.6 KB
Main entry chunk (gzip)154.1 KB350 KB
Entry fileindex-Dk1KuX-U.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)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.84KB114.57KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.18KB47.97KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.46KB34.48KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)127.52KB31.01KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.46KB54.47KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.70KB7.69KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Projects

None yet

1 participant

@os-support-ai
, '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): make the 8-literal union the one canonical TableColumn.type - #6370

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union
Aug 25, 2026
Merged

fix(types): make the 8-literal union the one canonical TableColumn.type#6370
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#5853

TableColumn.type had three disagreeing authorities. The interface declared 8 literals, the
zod mirror declared z.string() and accepted anything, and the renderer branched on a third
set it could only reach through an as any cast.

Implements the operative maintainer ruling — 2026-08-25, os-steve, verbatim 「其他同意」,
Option B: the 8-literal interface union is the canonical value set. The earlier
2026-08-24 ruling (canonical = the renderer's live read set, 12 values) is superseded and is
not what this PR builds.


1. Corpus measurement — ran FIRST, before any edit (the fork clause)

The ruling's step 1: measure authored out-of-union spellings before tightening, and stop
if authored usage is material
. It is not material. It is zero.

Method, with a control probe so a zero-hit reads as real. A structured walk of every
JSON file under examples/, content/, apps/, e2e/, docs/ and packages/
(591 files parsed), collecting every object carrying accessorKey (the TableColumn
discriminator) and every object inside any columns array, plus a regex sweep of the
non-JSON corpus. Control: type: 'currency' → 22 hits; type: 'banana' → 0 hits.

measurementresult
authored type: 'int' in a column position — repo-wide0
authored type: 'integer' in a column position — repo-wide0
authored type: 'float' in a column position — repo-wide0
authored type: 'double' in a column position — repo-wide0
objects with accessorKey carrying a type3 (number, text, currency — all declared)
objects with accessorKey carrying an OUT-OF-UNION type0

The only out-of-union hits in the whole corpus, with the file list the ruling asked for:

  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json$.columns[2], $.columns[4]type: "select"
  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json$.columns[3], $.columns[4]type: "select"
  • content/docs/plugins/plugin-grid.mdxtype: "select" / type: "badge" in object-grid examples

None of these is a TableColumn. Their keys are field + type, which is
ListColumn (@objectstack/spec/ui) — ObjectGrid's column INPUT vocabulary, a different
declared type that this PR does not touch. They are authored against the spec's field-type
vocabulary and keep validating exactly as before. They matter here only as evidence for §4:
they are what flows INTO the inference whose output §3 folds.

The three repo-wide type: 'integer' hits are JSON-Schema primitives in
packages/app-shell/.../json-schema-to-fields, unrelated to this key.

Verdict: fork clause not triggered — no authored document breaks.

2. Coordinates re-derived on this branch's base (e4559d1e0)

Both rulings' line numbers were stale, and so were the PM's.

itemruling / card / dispatch saidmeasured on e4559d1e0
the as any cast:2151 (ruling) / :2152 (card) / :2192 (dispatch)packages/components/src/renderers/complex/data-table.tsx:2192 ✅ dispatch correct
ObjectGrid emit seam:2206 (ruling) / :2236 (dispatch)packages/plugin-grid/src/ObjectGrid.tsx:2236 ✅ dispatch correct
zod mirrordata-display.zod.ts:109:109type: z.string().optional().describe('Column type')
interface uniondata-display.ts:255:255 ✅ unchanged from the card

One correction to the card's own model of the renderer. The card and both rulings treat
NUMERIC_EDIT_TYPES plus a datetime-local branch as the renderer's read set. Measured, the
renderer reads TableColumn.type at exactly one site (:2192editType), feeding
three branches. formatCellValue is type-agnostic (regex on the value). So the live read set
is {date, datetime, datetime-local, number, currency, percent, int, integer, float, double}
— everything else already fell through to the text input before this PR.

3. What happens to an out-of-union field type at the seam (T1)

The seam forwards an object schema's field type, whose vocabulary is @objectstack/spec's
FieldType. Measured against the installed dist: 49 options, of which 42 are outside the
union
; 7 of the 8 declared literals are spec field types and only action is UI-only.
int / integer / float / double / datetime-local are not in the spec enum at all
— the renderer's extra dialect was never a spec vocabulary.

Ruled disposition, and it is total — three outcomes, no fourth:

  1. a canonical spelling passes through unchanged;
  2. a known alias folds onto its canonical spelling (int/integer/float/doublenumber);
  3. anything else yields undefined — the type ANNOTATION is dropped, and the COLUMN IS NEVER DROPPED.

This is where the 42 spec types (select, lookup, user, file, formula, …) land, and
it is behaviour-preserving at the only consumer that reads the key: data-table's editor
branches on date / datetime / the numeric set and otherwise falls through to a text input
— which is exactly the undefined path. The dedicated widget those fields DO get comes from
the host's renderCellEditor, which resolves the field through column.accessorKey and
never reads type (ObjectGrid.tsx:3036). Pinned behaviourally, both halves: the column
still renders its header and cells, and an undeclared type opens the same editor as no type
at all.

⛔ Not folded onto 'text': that would assert something false about a lookup column and
leak the lie into any future reader. Absence says only what is true.

One addition beyond the four aliases the ruling names:datetime-localdatetime.
It is part of the same undeclared renderer dialect (it had its own editor branch at :2273),
so deleting that branch without the fold would silently drop behaviour. Repo precedent is
verbatim — packages/types/src/ui-action.ts:199 already documents datetime-local
datetime as legacy-dialect resolution. Flagged here rather than done quietly.

4. Second-producer census (step 4) — the card named one; there are two

producerwrites TableColumn.type?source of the value
plugin-grid/src/ObjectGrid.tsxyes — 5 paths4 column literals in generateColumns() (:1826, :1915, :1964, :2037) + the fieldDef.type enrichment (:2236), all verbatim
plugin-dashboard/src/ObjectDataTable.tsxyes — the SECOND producerenrich() spreads ...fieldMeta, and buildFieldMeta returns type: overrides.type ?? meta?.type (recordFields.tsx:151) — the raw field type, same defect
plugin-detail/src/RelatedList.tsx❌ nowrites fieldType (a different key) + cell; its type: def.type is inside a FieldMetadata, not a column
plugin-dashboard/DashboardRenderer.tsx:758, DashboardGridLayout.tsx:328❌ nopass author options.columns through, else deriveStaticTableColumns which writes no type

Per the ruling ("a second producer found = the same normalization at its seam"),
ObjectDataTable gets the same fold.

Why the ObjectGrid fold is a separate .map() pass rather than folded into the
enrichment map: that map early-returns for _actions and for any column whose accessorKey
has no fieldDef — and a heuristic inferColumnType() value (select, user) rides out on
exactly those columns. Folding inside it would miss them. The seam test deliberately uses an
inline-data grid with no object schema, which is that path.

5. Ablations — direction and count predicted BEFORE running

Both run under the shared verify lock. Both mutations were proved on disk (grep counts of
the injected and removed text in both directions, plus a git hash-object before/after
change) and both restores were proved byte-identical to the HEAD blob, not merely
trusted to the EXIT trap.

Neither needed a rebuild, and that is a measured claim, not an assumption:
vitest.config.mts:261 aliases @object-ui/typespackages/types/src, so these suites
resolve the subject through source, not dist — the stale-dist false-green that makes
an ablation vacuous cannot arise here.

ablationpredictedactualmatch
A — revert the zod tightening to z.string()RED, 9 failures (1 mirror-parity + 1 money + 7 out-of-union refusals)RED, 9 failed / 20 passed✅ exact
B — delete the ObjectGrid emit-seam foldRED, 4 failures (int, integer, float, double); control number and the select column-survives case still passRED, 4 failed / 2 passed — exactly those four✅ exact

Where my prediction was wrong. For ablation B I predicted the unfolded int column would
open a text input; the observed reading was null. Cause: the built-in fallback <Input>
renders with no type attribute rather than type="text", so the probe reads null.
Direction and count were right; the predicted observable was not. The assertion still
separates folded from unfolded, which is what the ablation is for.

6. Gates — each with its own verdict line and exit code

Derived from the CI job step lists under .github/workflows/. All run after the final
commit, against a2bae3d7f, on a clean tree.

gateexitits own verdict line
type-check (types, components, plugin-grid, plugin-dashboard)0all four echo type-check: Done; each runs tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are type-checked too
vitest types + plugin-grid + plugin-dashboard0Test Files 230 passed (230) · Tests 2277 passed (2277)
vitest components (batch A: renderers/ui/notifications)0Test Files 88 passed (88) · Tests 770 passed (770)
vitest components (batch B: src/__tests__)0Test Files 103 passed (103) · Tests 968 passed (968)
turbo run lint (4 packages + root)0Tasks: 5 successful, 5 total0 errors (685+912+386+244+28 pre-existing warnings, none new: my 9 changed files report errors: 0)
check-changeset-presence.mjs0✅ 9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs0✅ No changeset declares a 'major' bump.
check-changeset-fixed.mjs0✅ All workspace packages are in the changeset fixed group.
check-control-bytes.mjs0✅ check-control-bytes: OK (scanned 5241 tracked text file(s))
check:readme-exports0✅ check-readme-exports: OK (… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated); 0 unbuilt)
check:published-dist0✅ No published package's build output carries tooling material.
check:spec-symbols0✅ spec symbol derivation: 1306 files scanned against 4959 spec export names
check:self-import0✅ No package names itself inside its own src/.
check:esm-specifiers0Specifier leg: no un-ledgered package emits an extensionless relative specifier.
check:doc-types0✅ Every documented component type is registered.
check:doc-snippets0Semantic phase: 267 of 267 block(s) judged, 0 failed.the narrowing did not break a documented example
check:doc-fences0✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced …
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it.
lint:coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors
type-check:coverage0✅ test type-check coverage: 41/41 packages compile their tests
check:vi-mock-specifiers0✅ check-vi-mock-specifiers: OK (3757 tracked source file(s))
check:shell-escape-residue0✅ check-shell-escape-residue: OK (4/4 root(s) resolved)
turbo run build --filter='./packages/*'0Tasks: 39 successful, 39 total

On the components suite — it ran COMPLETE; the split is batching, not narrowing. A single
run hit the container's ~10-minute foreground cap (exit 143). Rather than narrow coverage, I
split it by directory. Population read from vitest's own config, not from my guess:
vitest list --filesOnly packages/components reports 191 files; batches A + B ran
88 + 103 = 191. (A find glob counted 193 — the two extras are .snap snapshot files,
not tests.)

check:readme-exports was red on first run and that reading was NOT MEASURED, not a
failure
: its own message said type entry ./dist/index.d.ts is not on disk -- run pnpm build first for all 236 items, none of them in a package I touched. After building all 39 packages
it is a real green with 0 unbuilt.

Gates deliberately not run locally, as CI owns them: test:e2e, test:e2e:live:ci,
check:node-esm-load (full build leg; the --specifiers-only leg is green above),
check:i18n-*, check:action-forward-parity, check:designer-field-key-parity,
check:icon-record-names, check:skills-paths, type-check:scripts,
type-check:vitest-setup — no file in this diff is an input to any of them.

7. The pin that stops this regressing (T2)

#5684's guard is key-set only — it checks key presence, and type was present on both
sides the whole time, which is exactly how this instance survived while its siblings were
caught. The value-level pin has two halves:

  • packages/types/src/__tests__/table-column-type-canonical.test.ts — interface tuple ↔ zod
    enum parity, loud refusal (money fails with type in the error path), and
    normalizeTableColumnTypetotality over all 49 spec field types.
  • packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx
    the third end neither of those can see: every literal the renderer branches on, derived
    from its source, must be a declared member. Carries blind-spot guards (no switch, no
    includes(editType), exactly one .has(editType)) so a hidden branch fails the instrument
    instead of silently under-counting it.
  • packages/plugin-grid/src/__tests__/columnTypeEmitSeam.test.tsx — the seam, pinned
    behaviourally through the editor a column actually opens, with a control probe.

A future new inference value turning these red is BY DESIGN, and the note at each pin
says so and names the two correct repairs (publish the value across all three ends, or fold it
at the producer) so the next reader does not "fix" it by loosening the mirror.

The zod mirror now builds its z.enumfromTABLE_COLUMN_TYPES rather than restating the
members, so the types↔zod leg cannot drift structurally; the pins guard the legs that cannot
be derived.

Changeset

minor for @object-ui/types (published-validator accept-set narrowing + new public API),
patch for the three others. Never majorcheck-changeset-no-major.mjs green above.
The text names the newly-refused spellings in two groups (typos/invented names; object-schema
field types written into a column slot) and states that in-repo authored metadata needs no
migration, with the §1 measurement behind it.


Generated by Claude Code

`TableColumn.type` disagreed three ways: the interface declared 8 literals,
the zod mirror declared `z.string()` and accepted anything, and the renderer
branched on a third set it could only read through an `as any` cast.
Per the maintainer ruling of 2026-08-25 (Option B), the 8-literal interface
union is canonical:
- `TABLE_COLUMN_TYPES` is now the single declaration of the vocabulary, and
the zod mirror builds its `z.enum` from it, so the two cannot drift.
- Column-inference producers fold their inferred value onto that vocabulary at
their emit seam (`normalizeTableColumnType`) instead of forwarding an object
schema's field type verbatim. Two producers do this: `ObjectGrid` and
`ObjectDataTable`. An out-of-union type drops the `type` annotation — never
the column.
- The renderer's undeclared dialect (`int`/`integer`/`float`/`double` in
`NUMERIC_EDIT_TYPES`, the `datetime-local` branch) is gone and the `as any`
cast with it, so the read is typed against what the interface publishes.
A value-level parity pin covers all three ends; objectui#5684's guard is
key-set only and cannot see value drift.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.8 KB3266.6 KB
Main entry chunk (gzip)154.1 KB350 KB
Entry fileindex-Dk1KuX-U.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)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.84KB114.57KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.18KB47.97KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.46KB34.48KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)127.52KB31.01KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.46KB54.47KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.70KB7.69KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Projects

None yet

1 participant

@os-support-ai
, '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): make the 8-literal union the one canonical TableColumn.type - #6370

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union
Aug 25, 2026
Merged

fix(types): make the 8-literal union the one canonical TableColumn.type#6370
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#5853

TableColumn.type had three disagreeing authorities. The interface declared 8 literals, the
zod mirror declared z.string() and accepted anything, and the renderer branched on a third
set it could only reach through an as any cast.

Implements the operative maintainer ruling — 2026-08-25, os-steve, verbatim 「其他同意」,
Option B: the 8-literal interface union is the canonical value set. The earlier
2026-08-24 ruling (canonical = the renderer's live read set, 12 values) is superseded and is
not what this PR builds.


1. Corpus measurement — ran FIRST, before any edit (the fork clause)

The ruling's step 1: measure authored out-of-union spellings before tightening, and stop
if authored usage is material
. It is not material. It is zero.

Method, with a control probe so a zero-hit reads as real. A structured walk of every
JSON file under examples/, content/, apps/, e2e/, docs/ and packages/
(591 files parsed), collecting every object carrying accessorKey (the TableColumn
discriminator) and every object inside any columns array, plus a regex sweep of the
non-JSON corpus. Control: type: 'currency' → 22 hits; type: 'banana' → 0 hits.

measurementresult
authored type: 'int' in a column position — repo-wide0
authored type: 'integer' in a column position — repo-wide0
authored type: 'float' in a column position — repo-wide0
authored type: 'double' in a column position — repo-wide0
objects with accessorKey carrying a type3 (number, text, currency — all declared)
objects with accessorKey carrying an OUT-OF-UNION type0

The only out-of-union hits in the whole corpus, with the file list the ruling asked for:

  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json$.columns[2], $.columns[4]type: "select"
  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json$.columns[3], $.columns[4]type: "select"
  • content/docs/plugins/plugin-grid.mdxtype: "select" / type: "badge" in object-grid examples

None of these is a TableColumn. Their keys are field + type, which is
ListColumn (@objectstack/spec/ui) — ObjectGrid's column INPUT vocabulary, a different
declared type that this PR does not touch. They are authored against the spec's field-type
vocabulary and keep validating exactly as before. They matter here only as evidence for §4:
they are what flows INTO the inference whose output §3 folds.

The three repo-wide type: 'integer' hits are JSON-Schema primitives in
packages/app-shell/.../json-schema-to-fields, unrelated to this key.

Verdict: fork clause not triggered — no authored document breaks.

2. Coordinates re-derived on this branch's base (e4559d1e0)

Both rulings' line numbers were stale, and so were the PM's.

itemruling / card / dispatch saidmeasured on e4559d1e0
the as any cast:2151 (ruling) / :2152 (card) / :2192 (dispatch)packages/components/src/renderers/complex/data-table.tsx:2192 ✅ dispatch correct
ObjectGrid emit seam:2206 (ruling) / :2236 (dispatch)packages/plugin-grid/src/ObjectGrid.tsx:2236 ✅ dispatch correct
zod mirrordata-display.zod.ts:109:109type: z.string().optional().describe('Column type')
interface uniondata-display.ts:255:255 ✅ unchanged from the card

One correction to the card's own model of the renderer. The card and both rulings treat
NUMERIC_EDIT_TYPES plus a datetime-local branch as the renderer's read set. Measured, the
renderer reads TableColumn.type at exactly one site (:2192editType), feeding
three branches. formatCellValue is type-agnostic (regex on the value). So the live read set
is {date, datetime, datetime-local, number, currency, percent, int, integer, float, double}
— everything else already fell through to the text input before this PR.

3. What happens to an out-of-union field type at the seam (T1)

The seam forwards an object schema's field type, whose vocabulary is @objectstack/spec's
FieldType. Measured against the installed dist: 49 options, of which 42 are outside the
union
; 7 of the 8 declared literals are spec field types and only action is UI-only.
int / integer / float / double / datetime-local are not in the spec enum at all
— the renderer's extra dialect was never a spec vocabulary.

Ruled disposition, and it is total — three outcomes, no fourth:

  1. a canonical spelling passes through unchanged;
  2. a known alias folds onto its canonical spelling (int/integer/float/doublenumber);
  3. anything else yields undefined — the type ANNOTATION is dropped, and the COLUMN IS NEVER DROPPED.

This is where the 42 spec types (select, lookup, user, file, formula, …) land, and
it is behaviour-preserving at the only consumer that reads the key: data-table's editor
branches on date / datetime / the numeric set and otherwise falls through to a text input
— which is exactly the undefined path. The dedicated widget those fields DO get comes from
the host's renderCellEditor, which resolves the field through column.accessorKey and
never reads type (ObjectGrid.tsx:3036). Pinned behaviourally, both halves: the column
still renders its header and cells, and an undeclared type opens the same editor as no type
at all.

⛔ Not folded onto 'text': that would assert something false about a lookup column and
leak the lie into any future reader. Absence says only what is true.

One addition beyond the four aliases the ruling names:datetime-localdatetime.
It is part of the same undeclared renderer dialect (it had its own editor branch at :2273),
so deleting that branch without the fold would silently drop behaviour. Repo precedent is
verbatim — packages/types/src/ui-action.ts:199 already documents datetime-local
datetime as legacy-dialect resolution. Flagged here rather than done quietly.

4. Second-producer census (step 4) — the card named one; there are two

producerwrites TableColumn.type?source of the value
plugin-grid/src/ObjectGrid.tsxyes — 5 paths4 column literals in generateColumns() (:1826, :1915, :1964, :2037) + the fieldDef.type enrichment (:2236), all verbatim
plugin-dashboard/src/ObjectDataTable.tsxyes — the SECOND producerenrich() spreads ...fieldMeta, and buildFieldMeta returns type: overrides.type ?? meta?.type (recordFields.tsx:151) — the raw field type, same defect
plugin-detail/src/RelatedList.tsx❌ nowrites fieldType (a different key) + cell; its type: def.type is inside a FieldMetadata, not a column
plugin-dashboard/DashboardRenderer.tsx:758, DashboardGridLayout.tsx:328❌ nopass author options.columns through, else deriveStaticTableColumns which writes no type

Per the ruling ("a second producer found = the same normalization at its seam"),
ObjectDataTable gets the same fold.

Why the ObjectGrid fold is a separate .map() pass rather than folded into the
enrichment map: that map early-returns for _actions and for any column whose accessorKey
has no fieldDef — and a heuristic inferColumnType() value (select, user) rides out on
exactly those columns. Folding inside it would miss them. The seam test deliberately uses an
inline-data grid with no object schema, which is that path.

5. Ablations — direction and count predicted BEFORE running

Both run under the shared verify lock. Both mutations were proved on disk (grep counts of
the injected and removed text in both directions, plus a git hash-object before/after
change) and both restores were proved byte-identical to the HEAD blob, not merely
trusted to the EXIT trap.

Neither needed a rebuild, and that is a measured claim, not an assumption:
vitest.config.mts:261 aliases @object-ui/typespackages/types/src, so these suites
resolve the subject through source, not dist — the stale-dist false-green that makes
an ablation vacuous cannot arise here.

ablationpredictedactualmatch
A — revert the zod tightening to z.string()RED, 9 failures (1 mirror-parity + 1 money + 7 out-of-union refusals)RED, 9 failed / 20 passed✅ exact
B — delete the ObjectGrid emit-seam foldRED, 4 failures (int, integer, float, double); control number and the select column-survives case still passRED, 4 failed / 2 passed — exactly those four✅ exact

Where my prediction was wrong. For ablation B I predicted the unfolded int column would
open a text input; the observed reading was null. Cause: the built-in fallback <Input>
renders with no type attribute rather than type="text", so the probe reads null.
Direction and count were right; the predicted observable was not. The assertion still
separates folded from unfolded, which is what the ablation is for.

6. Gates — each with its own verdict line and exit code

Derived from the CI job step lists under .github/workflows/. All run after the final
commit, against a2bae3d7f, on a clean tree.

gateexitits own verdict line
type-check (types, components, plugin-grid, plugin-dashboard)0all four echo type-check: Done; each runs tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are type-checked too
vitest types + plugin-grid + plugin-dashboard0Test Files 230 passed (230) · Tests 2277 passed (2277)
vitest components (batch A: renderers/ui/notifications)0Test Files 88 passed (88) · Tests 770 passed (770)
vitest components (batch B: src/__tests__)0Test Files 103 passed (103) · Tests 968 passed (968)
turbo run lint (4 packages + root)0Tasks: 5 successful, 5 total0 errors (685+912+386+244+28 pre-existing warnings, none new: my 9 changed files report errors: 0)
check-changeset-presence.mjs0✅ 9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs0✅ No changeset declares a 'major' bump.
check-changeset-fixed.mjs0✅ All workspace packages are in the changeset fixed group.
check-control-bytes.mjs0✅ check-control-bytes: OK (scanned 5241 tracked text file(s))
check:readme-exports0✅ check-readme-exports: OK (… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated); 0 unbuilt)
check:published-dist0✅ No published package's build output carries tooling material.
check:spec-symbols0✅ spec symbol derivation: 1306 files scanned against 4959 spec export names
check:self-import0✅ No package names itself inside its own src/.
check:esm-specifiers0Specifier leg: no un-ledgered package emits an extensionless relative specifier.
check:doc-types0✅ Every documented component type is registered.
check:doc-snippets0Semantic phase: 267 of 267 block(s) judged, 0 failed.the narrowing did not break a documented example
check:doc-fences0✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced …
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it.
lint:coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors
type-check:coverage0✅ test type-check coverage: 41/41 packages compile their tests
check:vi-mock-specifiers0✅ check-vi-mock-specifiers: OK (3757 tracked source file(s))
check:shell-escape-residue0✅ check-shell-escape-residue: OK (4/4 root(s) resolved)
turbo run build --filter='./packages/*'0Tasks: 39 successful, 39 total

On the components suite — it ran COMPLETE; the split is batching, not narrowing. A single
run hit the container's ~10-minute foreground cap (exit 143). Rather than narrow coverage, I
split it by directory. Population read from vitest's own config, not from my guess:
vitest list --filesOnly packages/components reports 191 files; batches A + B ran
88 + 103 = 191. (A find glob counted 193 — the two extras are .snap snapshot files,
not tests.)

check:readme-exports was red on first run and that reading was NOT MEASURED, not a
failure
: its own message said type entry ./dist/index.d.ts is not on disk -- run pnpm build first for all 236 items, none of them in a package I touched. After building all 39 packages
it is a real green with 0 unbuilt.

Gates deliberately not run locally, as CI owns them: test:e2e, test:e2e:live:ci,
check:node-esm-load (full build leg; the --specifiers-only leg is green above),
check:i18n-*, check:action-forward-parity, check:designer-field-key-parity,
check:icon-record-names, check:skills-paths, type-check:scripts,
type-check:vitest-setup — no file in this diff is an input to any of them.

7. The pin that stops this regressing (T2)

#5684's guard is key-set only — it checks key presence, and type was present on both
sides the whole time, which is exactly how this instance survived while its siblings were
caught. The value-level pin has two halves:

  • packages/types/src/__tests__/table-column-type-canonical.test.ts — interface tuple ↔ zod
    enum parity, loud refusal (money fails with type in the error path), and
    normalizeTableColumnTypetotality over all 49 spec field types.
  • packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx
    the third end neither of those can see: every literal the renderer branches on, derived
    from its source, must be a declared member. Carries blind-spot guards (no switch, no
    includes(editType), exactly one .has(editType)) so a hidden branch fails the instrument
    instead of silently under-counting it.
  • packages/plugin-grid/src/__tests__/columnTypeEmitSeam.test.tsx — the seam, pinned
    behaviourally through the editor a column actually opens, with a control probe.

A future new inference value turning these red is BY DESIGN, and the note at each pin
says so and names the two correct repairs (publish the value across all three ends, or fold it
at the producer) so the next reader does not "fix" it by loosening the mirror.

The zod mirror now builds its z.enumfromTABLE_COLUMN_TYPES rather than restating the
members, so the types↔zod leg cannot drift structurally; the pins guard the legs that cannot
be derived.

Changeset

minor for @object-ui/types (published-validator accept-set narrowing + new public API),
patch for the three others. Never majorcheck-changeset-no-major.mjs green above.
The text names the newly-refused spellings in two groups (typos/invented names; object-schema
field types written into a column slot) and states that in-repo authored metadata needs no
migration, with the §1 measurement behind it.


Generated by Claude Code

`TableColumn.type` disagreed three ways: the interface declared 8 literals,
the zod mirror declared `z.string()` and accepted anything, and the renderer
branched on a third set it could only read through an `as any` cast.
Per the maintainer ruling of 2026-08-25 (Option B), the 8-literal interface
union is canonical:
- `TABLE_COLUMN_TYPES` is now the single declaration of the vocabulary, and
the zod mirror builds its `z.enum` from it, so the two cannot drift.
- Column-inference producers fold their inferred value onto that vocabulary at
their emit seam (`normalizeTableColumnType`) instead of forwarding an object
schema's field type verbatim. Two producers do this: `ObjectGrid` and
`ObjectDataTable`. An out-of-union type drops the `type` annotation — never
the column.
- The renderer's undeclared dialect (`int`/`integer`/`float`/`double` in
`NUMERIC_EDIT_TYPES`, the `datetime-local` branch) is gone and the `as any`
cast with it, so the read is typed against what the interface publishes.
A value-level parity pin covers all three ends; objectui#5684's guard is
key-set only and cannot see value drift.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.8 KB3266.6 KB
Main entry chunk (gzip)154.1 KB350 KB
Entry fileindex-Dk1KuX-U.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)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.84KB114.57KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.18KB47.97KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.46KB34.48KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)127.52KB31.01KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.46KB54.47KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.70KB7.69KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Projects

None yet

1 participant

@os-support-ai
, '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): make the 8-literal union the one canonical TableColumn.type - #6370

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union
Aug 25, 2026
Merged

fix(types): make the 8-literal union the one canonical TableColumn.type#6370
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#5853

TableColumn.type had three disagreeing authorities. The interface declared 8 literals, the
zod mirror declared z.string() and accepted anything, and the renderer branched on a third
set it could only reach through an as any cast.

Implements the operative maintainer ruling — 2026-08-25, os-steve, verbatim 「其他同意」,
Option B: the 8-literal interface union is the canonical value set. The earlier
2026-08-24 ruling (canonical = the renderer's live read set, 12 values) is superseded and is
not what this PR builds.


1. Corpus measurement — ran FIRST, before any edit (the fork clause)

The ruling's step 1: measure authored out-of-union spellings before tightening, and stop
if authored usage is material
. It is not material. It is zero.

Method, with a control probe so a zero-hit reads as real. A structured walk of every
JSON file under examples/, content/, apps/, e2e/, docs/ and packages/
(591 files parsed), collecting every object carrying accessorKey (the TableColumn
discriminator) and every object inside any columns array, plus a regex sweep of the
non-JSON corpus. Control: type: 'currency' → 22 hits; type: 'banana' → 0 hits.

measurementresult
authored type: 'int' in a column position — repo-wide0
authored type: 'integer' in a column position — repo-wide0
authored type: 'float' in a column position — repo-wide0
authored type: 'double' in a column position — repo-wide0
objects with accessorKey carrying a type3 (number, text, currency — all declared)
objects with accessorKey carrying an OUT-OF-UNION type0

The only out-of-union hits in the whole corpus, with the file list the ruling asked for:

  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json$.columns[2], $.columns[4]type: "select"
  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json$.columns[3], $.columns[4]type: "select"
  • content/docs/plugins/plugin-grid.mdxtype: "select" / type: "badge" in object-grid examples

None of these is a TableColumn. Their keys are field + type, which is
ListColumn (@objectstack/spec/ui) — ObjectGrid's column INPUT vocabulary, a different
declared type that this PR does not touch. They are authored against the spec's field-type
vocabulary and keep validating exactly as before. They matter here only as evidence for §4:
they are what flows INTO the inference whose output §3 folds.

The three repo-wide type: 'integer' hits are JSON-Schema primitives in
packages/app-shell/.../json-schema-to-fields, unrelated to this key.

Verdict: fork clause not triggered — no authored document breaks.

2. Coordinates re-derived on this branch's base (e4559d1e0)

Both rulings' line numbers were stale, and so were the PM's.

itemruling / card / dispatch saidmeasured on e4559d1e0
the as any cast:2151 (ruling) / :2152 (card) / :2192 (dispatch)packages/components/src/renderers/complex/data-table.tsx:2192 ✅ dispatch correct
ObjectGrid emit seam:2206 (ruling) / :2236 (dispatch)packages/plugin-grid/src/ObjectGrid.tsx:2236 ✅ dispatch correct
zod mirrordata-display.zod.ts:109:109type: z.string().optional().describe('Column type')
interface uniondata-display.ts:255:255 ✅ unchanged from the card

One correction to the card's own model of the renderer. The card and both rulings treat
NUMERIC_EDIT_TYPES plus a datetime-local branch as the renderer's read set. Measured, the
renderer reads TableColumn.type at exactly one site (:2192editType), feeding
three branches. formatCellValue is type-agnostic (regex on the value). So the live read set
is {date, datetime, datetime-local, number, currency, percent, int, integer, float, double}
— everything else already fell through to the text input before this PR.

3. What happens to an out-of-union field type at the seam (T1)

The seam forwards an object schema's field type, whose vocabulary is @objectstack/spec's
FieldType. Measured against the installed dist: 49 options, of which 42 are outside the
union
; 7 of the 8 declared literals are spec field types and only action is UI-only.
int / integer / float / double / datetime-local are not in the spec enum at all
— the renderer's extra dialect was never a spec vocabulary.

Ruled disposition, and it is total — three outcomes, no fourth:

  1. a canonical spelling passes through unchanged;
  2. a known alias folds onto its canonical spelling (int/integer/float/doublenumber);
  3. anything else yields undefined — the type ANNOTATION is dropped, and the COLUMN IS NEVER DROPPED.

This is where the 42 spec types (select, lookup, user, file, formula, …) land, and
it is behaviour-preserving at the only consumer that reads the key: data-table's editor
branches on date / datetime / the numeric set and otherwise falls through to a text input
— which is exactly the undefined path. The dedicated widget those fields DO get comes from
the host's renderCellEditor, which resolves the field through column.accessorKey and
never reads type (ObjectGrid.tsx:3036). Pinned behaviourally, both halves: the column
still renders its header and cells, and an undeclared type opens the same editor as no type
at all.

⛔ Not folded onto 'text': that would assert something false about a lookup column and
leak the lie into any future reader. Absence says only what is true.

One addition beyond the four aliases the ruling names:datetime-localdatetime.
It is part of the same undeclared renderer dialect (it had its own editor branch at :2273),
so deleting that branch without the fold would silently drop behaviour. Repo precedent is
verbatim — packages/types/src/ui-action.ts:199 already documents datetime-local
datetime as legacy-dialect resolution. Flagged here rather than done quietly.

4. Second-producer census (step 4) — the card named one; there are two

producerwrites TableColumn.type?source of the value
plugin-grid/src/ObjectGrid.tsxyes — 5 paths4 column literals in generateColumns() (:1826, :1915, :1964, :2037) + the fieldDef.type enrichment (:2236), all verbatim
plugin-dashboard/src/ObjectDataTable.tsxyes — the SECOND producerenrich() spreads ...fieldMeta, and buildFieldMeta returns type: overrides.type ?? meta?.type (recordFields.tsx:151) — the raw field type, same defect
plugin-detail/src/RelatedList.tsx❌ nowrites fieldType (a different key) + cell; its type: def.type is inside a FieldMetadata, not a column
plugin-dashboard/DashboardRenderer.tsx:758, DashboardGridLayout.tsx:328❌ nopass author options.columns through, else deriveStaticTableColumns which writes no type

Per the ruling ("a second producer found = the same normalization at its seam"),
ObjectDataTable gets the same fold.

Why the ObjectGrid fold is a separate .map() pass rather than folded into the
enrichment map: that map early-returns for _actions and for any column whose accessorKey
has no fieldDef — and a heuristic inferColumnType() value (select, user) rides out on
exactly those columns. Folding inside it would miss them. The seam test deliberately uses an
inline-data grid with no object schema, which is that path.

5. Ablations — direction and count predicted BEFORE running

Both run under the shared verify lock. Both mutations were proved on disk (grep counts of
the injected and removed text in both directions, plus a git hash-object before/after
change) and both restores were proved byte-identical to the HEAD blob, not merely
trusted to the EXIT trap.

Neither needed a rebuild, and that is a measured claim, not an assumption:
vitest.config.mts:261 aliases @object-ui/typespackages/types/src, so these suites
resolve the subject through source, not dist — the stale-dist false-green that makes
an ablation vacuous cannot arise here.

ablationpredictedactualmatch
A — revert the zod tightening to z.string()RED, 9 failures (1 mirror-parity + 1 money + 7 out-of-union refusals)RED, 9 failed / 20 passed✅ exact
B — delete the ObjectGrid emit-seam foldRED, 4 failures (int, integer, float, double); control number and the select column-survives case still passRED, 4 failed / 2 passed — exactly those four✅ exact

Where my prediction was wrong. For ablation B I predicted the unfolded int column would
open a text input; the observed reading was null. Cause: the built-in fallback <Input>
renders with no type attribute rather than type="text", so the probe reads null.
Direction and count were right; the predicted observable was not. The assertion still
separates folded from unfolded, which is what the ablation is for.

6. Gates — each with its own verdict line and exit code

Derived from the CI job step lists under .github/workflows/. All run after the final
commit, against a2bae3d7f, on a clean tree.

gateexitits own verdict line
type-check (types, components, plugin-grid, plugin-dashboard)0all four echo type-check: Done; each runs tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are type-checked too
vitest types + plugin-grid + plugin-dashboard0Test Files 230 passed (230) · Tests 2277 passed (2277)
vitest components (batch A: renderers/ui/notifications)0Test Files 88 passed (88) · Tests 770 passed (770)
vitest components (batch B: src/__tests__)0Test Files 103 passed (103) · Tests 968 passed (968)
turbo run lint (4 packages + root)0Tasks: 5 successful, 5 total0 errors (685+912+386+244+28 pre-existing warnings, none new: my 9 changed files report errors: 0)
check-changeset-presence.mjs0✅ 9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs0✅ No changeset declares a 'major' bump.
check-changeset-fixed.mjs0✅ All workspace packages are in the changeset fixed group.
check-control-bytes.mjs0✅ check-control-bytes: OK (scanned 5241 tracked text file(s))
check:readme-exports0✅ check-readme-exports: OK (… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated); 0 unbuilt)
check:published-dist0✅ No published package's build output carries tooling material.
check:spec-symbols0✅ spec symbol derivation: 1306 files scanned against 4959 spec export names
check:self-import0✅ No package names itself inside its own src/.
check:esm-specifiers0Specifier leg: no un-ledgered package emits an extensionless relative specifier.
check:doc-types0✅ Every documented component type is registered.
check:doc-snippets0Semantic phase: 267 of 267 block(s) judged, 0 failed.the narrowing did not break a documented example
check:doc-fences0✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced …
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it.
lint:coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors
type-check:coverage0✅ test type-check coverage: 41/41 packages compile their tests
check:vi-mock-specifiers0✅ check-vi-mock-specifiers: OK (3757 tracked source file(s))
check:shell-escape-residue0✅ check-shell-escape-residue: OK (4/4 root(s) resolved)
turbo run build --filter='./packages/*'0Tasks: 39 successful, 39 total

On the components suite — it ran COMPLETE; the split is batching, not narrowing. A single
run hit the container's ~10-minute foreground cap (exit 143). Rather than narrow coverage, I
split it by directory. Population read from vitest's own config, not from my guess:
vitest list --filesOnly packages/components reports 191 files; batches A + B ran
88 + 103 = 191. (A find glob counted 193 — the two extras are .snap snapshot files,
not tests.)

check:readme-exports was red on first run and that reading was NOT MEASURED, not a
failure
: its own message said type entry ./dist/index.d.ts is not on disk -- run pnpm build first for all 236 items, none of them in a package I touched. After building all 39 packages
it is a real green with 0 unbuilt.

Gates deliberately not run locally, as CI owns them: test:e2e, test:e2e:live:ci,
check:node-esm-load (full build leg; the --specifiers-only leg is green above),
check:i18n-*, check:action-forward-parity, check:designer-field-key-parity,
check:icon-record-names, check:skills-paths, type-check:scripts,
type-check:vitest-setup — no file in this diff is an input to any of them.

7. The pin that stops this regressing (T2)

#5684's guard is key-set only — it checks key presence, and type was present on both
sides the whole time, which is exactly how this instance survived while its siblings were
caught. The value-level pin has two halves:

  • packages/types/src/__tests__/table-column-type-canonical.test.ts — interface tuple ↔ zod
    enum parity, loud refusal (money fails with type in the error path), and
    normalizeTableColumnTypetotality over all 49 spec field types.
  • packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx
    the third end neither of those can see: every literal the renderer branches on, derived
    from its source, must be a declared member. Carries blind-spot guards (no switch, no
    includes(editType), exactly one .has(editType)) so a hidden branch fails the instrument
    instead of silently under-counting it.
  • packages/plugin-grid/src/__tests__/columnTypeEmitSeam.test.tsx — the seam, pinned
    behaviourally through the editor a column actually opens, with a control probe.

A future new inference value turning these red is BY DESIGN, and the note at each pin
says so and names the two correct repairs (publish the value across all three ends, or fold it
at the producer) so the next reader does not "fix" it by loosening the mirror.

The zod mirror now builds its z.enumfromTABLE_COLUMN_TYPES rather than restating the
members, so the types↔zod leg cannot drift structurally; the pins guard the legs that cannot
be derived.

Changeset

minor for @object-ui/types (published-validator accept-set narrowing + new public API),
patch for the three others. Never majorcheck-changeset-no-major.mjs green above.
The text names the newly-refused spellings in two groups (typos/invented names; object-schema
field types written into a column slot) and states that in-repo authored metadata needs no
migration, with the §1 measurement behind it.


Generated by Claude Code

`TableColumn.type` disagreed three ways: the interface declared 8 literals,
the zod mirror declared `z.string()` and accepted anything, and the renderer
branched on a third set it could only read through an `as any` cast.
Per the maintainer ruling of 2026-08-25 (Option B), the 8-literal interface
union is canonical:
- `TABLE_COLUMN_TYPES` is now the single declaration of the vocabulary, and
the zod mirror builds its `z.enum` from it, so the two cannot drift.
- Column-inference producers fold their inferred value onto that vocabulary at
their emit seam (`normalizeTableColumnType`) instead of forwarding an object
schema's field type verbatim. Two producers do this: `ObjectGrid` and
`ObjectDataTable`. An out-of-union type drops the `type` annotation — never
the column.
- The renderer's undeclared dialect (`int`/`integer`/`float`/`double` in
`NUMERIC_EDIT_TYPES`, the `datetime-local` branch) is gone and the `as any`
cast with it, so the read is typed against what the interface publishes.
A value-level parity pin covers all three ends; objectui#5684's guard is
key-set only and cannot see value drift.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.8 KB3266.6 KB
Main entry chunk (gzip)154.1 KB350 KB
Entry fileindex-Dk1KuX-U.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)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.84KB114.57KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.18KB47.97KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.46KB34.48KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)127.52KB31.01KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.46KB54.47KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.70KB7.69KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Projects

None yet

1 participant

@os-support-ai
, '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): make the 8-literal union the one canonical TableColumn.type - #6370

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union
Aug 25, 2026
Merged

fix(types): make the 8-literal union the one canonical TableColumn.type#6370
os-support-ai merged 1 commit into
mainfrom
claude/issue-5853-tablecolumn-type-canonical-union

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#5853

TableColumn.type had three disagreeing authorities. The interface declared 8 literals, the
zod mirror declared z.string() and accepted anything, and the renderer branched on a third
set it could only reach through an as any cast.

Implements the operative maintainer ruling — 2026-08-25, os-steve, verbatim 「其他同意」,
Option B: the 8-literal interface union is the canonical value set. The earlier
2026-08-24 ruling (canonical = the renderer's live read set, 12 values) is superseded and is
not what this PR builds.


1. Corpus measurement — ran FIRST, before any edit (the fork clause)

The ruling's step 1: measure authored out-of-union spellings before tightening, and stop
if authored usage is material
. It is not material. It is zero.

Method, with a control probe so a zero-hit reads as real. A structured walk of every
JSON file under examples/, content/, apps/, e2e/, docs/ and packages/
(591 files parsed), collecting every object carrying accessorKey (the TableColumn
discriminator) and every object inside any columns array, plus a regex sweep of the
non-JSON corpus. Control: type: 'currency' → 22 hits; type: 'banana' → 0 hits.

measurementresult
authored type: 'int' in a column position — repo-wide0
authored type: 'integer' in a column position — repo-wide0
authored type: 'float' in a column position — repo-wide0
authored type: 'double' in a column position — repo-wide0
objects with accessorKey carrying a type3 (number, text, currency — all declared)
objects with accessorKey carrying an OUT-OF-UNION type0

The only out-of-union hits in the whole corpus, with the file list the ruling asked for:

  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json$.columns[2], $.columns[4]type: "select"
  • examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json$.columns[3], $.columns[4]type: "select"
  • content/docs/plugins/plugin-grid.mdxtype: "select" / type: "badge" in object-grid examples

None of these is a TableColumn. Their keys are field + type, which is
ListColumn (@objectstack/spec/ui) — ObjectGrid's column INPUT vocabulary, a different
declared type that this PR does not touch. They are authored against the spec's field-type
vocabulary and keep validating exactly as before. They matter here only as evidence for §4:
they are what flows INTO the inference whose output §3 folds.

The three repo-wide type: 'integer' hits are JSON-Schema primitives in
packages/app-shell/.../json-schema-to-fields, unrelated to this key.

Verdict: fork clause not triggered — no authored document breaks.

2. Coordinates re-derived on this branch's base (e4559d1e0)

Both rulings' line numbers were stale, and so were the PM's.

itemruling / card / dispatch saidmeasured on e4559d1e0
the as any cast:2151 (ruling) / :2152 (card) / :2192 (dispatch)packages/components/src/renderers/complex/data-table.tsx:2192 ✅ dispatch correct
ObjectGrid emit seam:2206 (ruling) / :2236 (dispatch)packages/plugin-grid/src/ObjectGrid.tsx:2236 ✅ dispatch correct
zod mirrordata-display.zod.ts:109:109type: z.string().optional().describe('Column type')
interface uniondata-display.ts:255:255 ✅ unchanged from the card

One correction to the card's own model of the renderer. The card and both rulings treat
NUMERIC_EDIT_TYPES plus a datetime-local branch as the renderer's read set. Measured, the
renderer reads TableColumn.type at exactly one site (:2192editType), feeding
three branches. formatCellValue is type-agnostic (regex on the value). So the live read set
is {date, datetime, datetime-local, number, currency, percent, int, integer, float, double}
— everything else already fell through to the text input before this PR.

3. What happens to an out-of-union field type at the seam (T1)

The seam forwards an object schema's field type, whose vocabulary is @objectstack/spec's
FieldType. Measured against the installed dist: 49 options, of which 42 are outside the
union
; 7 of the 8 declared literals are spec field types and only action is UI-only.
int / integer / float / double / datetime-local are not in the spec enum at all
— the renderer's extra dialect was never a spec vocabulary.

Ruled disposition, and it is total — three outcomes, no fourth:

  1. a canonical spelling passes through unchanged;
  2. a known alias folds onto its canonical spelling (int/integer/float/doublenumber);
  3. anything else yields undefined — the type ANNOTATION is dropped, and the COLUMN IS NEVER DROPPED.

This is where the 42 spec types (select, lookup, user, file, formula, …) land, and
it is behaviour-preserving at the only consumer that reads the key: data-table's editor
branches on date / datetime / the numeric set and otherwise falls through to a text input
— which is exactly the undefined path. The dedicated widget those fields DO get comes from
the host's renderCellEditor, which resolves the field through column.accessorKey and
never reads type (ObjectGrid.tsx:3036). Pinned behaviourally, both halves: the column
still renders its header and cells, and an undeclared type opens the same editor as no type
at all.

⛔ Not folded onto 'text': that would assert something false about a lookup column and
leak the lie into any future reader. Absence says only what is true.

One addition beyond the four aliases the ruling names:datetime-localdatetime.
It is part of the same undeclared renderer dialect (it had its own editor branch at :2273),
so deleting that branch without the fold would silently drop behaviour. Repo precedent is
verbatim — packages/types/src/ui-action.ts:199 already documents datetime-local
datetime as legacy-dialect resolution. Flagged here rather than done quietly.

4. Second-producer census (step 4) — the card named one; there are two

producerwrites TableColumn.type?source of the value
plugin-grid/src/ObjectGrid.tsxyes — 5 paths4 column literals in generateColumns() (:1826, :1915, :1964, :2037) + the fieldDef.type enrichment (:2236), all verbatim
plugin-dashboard/src/ObjectDataTable.tsxyes — the SECOND producerenrich() spreads ...fieldMeta, and buildFieldMeta returns type: overrides.type ?? meta?.type (recordFields.tsx:151) — the raw field type, same defect
plugin-detail/src/RelatedList.tsx❌ nowrites fieldType (a different key) + cell; its type: def.type is inside a FieldMetadata, not a column
plugin-dashboard/DashboardRenderer.tsx:758, DashboardGridLayout.tsx:328❌ nopass author options.columns through, else deriveStaticTableColumns which writes no type

Per the ruling ("a second producer found = the same normalization at its seam"),
ObjectDataTable gets the same fold.

Why the ObjectGrid fold is a separate .map() pass rather than folded into the
enrichment map: that map early-returns for _actions and for any column whose accessorKey
has no fieldDef — and a heuristic inferColumnType() value (select, user) rides out on
exactly those columns. Folding inside it would miss them. The seam test deliberately uses an
inline-data grid with no object schema, which is that path.

5. Ablations — direction and count predicted BEFORE running

Both run under the shared verify lock. Both mutations were proved on disk (grep counts of
the injected and removed text in both directions, plus a git hash-object before/after
change) and both restores were proved byte-identical to the HEAD blob, not merely
trusted to the EXIT trap.

Neither needed a rebuild, and that is a measured claim, not an assumption:
vitest.config.mts:261 aliases @object-ui/typespackages/types/src, so these suites
resolve the subject through source, not dist — the stale-dist false-green that makes
an ablation vacuous cannot arise here.

ablationpredictedactualmatch
A — revert the zod tightening to z.string()RED, 9 failures (1 mirror-parity + 1 money + 7 out-of-union refusals)RED, 9 failed / 20 passed✅ exact
B — delete the ObjectGrid emit-seam foldRED, 4 failures (int, integer, float, double); control number and the select column-survives case still passRED, 4 failed / 2 passed — exactly those four✅ exact

Where my prediction was wrong. For ablation B I predicted the unfolded int column would
open a text input; the observed reading was null. Cause: the built-in fallback <Input>
renders with no type attribute rather than type="text", so the probe reads null.
Direction and count were right; the predicted observable was not. The assertion still
separates folded from unfolded, which is what the ablation is for.

6. Gates — each with its own verdict line and exit code

Derived from the CI job step lists under .github/workflows/. All run after the final
commit, against a2bae3d7f, on a clean tree.

gateexitits own verdict line
type-check (types, components, plugin-grid, plugin-dashboard)0all four echo type-check: Done; each runs tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are type-checked too
vitest types + plugin-grid + plugin-dashboard0Test Files 230 passed (230) · Tests 2277 passed (2277)
vitest components (batch A: renderers/ui/notifications)0Test Files 88 passed (88) · Tests 770 passed (770)
vitest components (batch B: src/__tests__)0Test Files 103 passed (103) · Tests 968 passed (968)
turbo run lint (4 packages + root)0Tasks: 5 successful, 5 total0 errors (685+912+386+244+28 pre-existing warnings, none new: my 9 changed files report errors: 0)
check-changeset-presence.mjs0✅ 9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs0✅ No changeset declares a 'major' bump.
check-changeset-fixed.mjs0✅ All workspace packages are in the changeset fixed group.
check-control-bytes.mjs0✅ check-control-bytes: OK (scanned 5241 tracked text file(s))
check:readme-exports0✅ check-readme-exports: OK (… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated); 0 unbuilt)
check:published-dist0✅ No published package's build output carries tooling material.
check:spec-symbols0✅ spec symbol derivation: 1306 files scanned against 4959 spec export names
check:self-import0✅ No package names itself inside its own src/.
check:esm-specifiers0Specifier leg: no un-ledgered package emits an extensionless relative specifier.
check:doc-types0✅ Every documented component type is registered.
check:doc-snippets0Semantic phase: 267 of 267 block(s) judged, 0 failed.the narrowing did not break a documented example
check:doc-fences0✅ check:doc-fences — every TypeScript block in 223 document(s) is fenced …
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it.
lint:coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors
type-check:coverage0✅ test type-check coverage: 41/41 packages compile their tests
check:vi-mock-specifiers0✅ check-vi-mock-specifiers: OK (3757 tracked source file(s))
check:shell-escape-residue0✅ check-shell-escape-residue: OK (4/4 root(s) resolved)
turbo run build --filter='./packages/*'0Tasks: 39 successful, 39 total

On the components suite — it ran COMPLETE; the split is batching, not narrowing. A single
run hit the container's ~10-minute foreground cap (exit 143). Rather than narrow coverage, I
split it by directory. Population read from vitest's own config, not from my guess:
vitest list --filesOnly packages/components reports 191 files; batches A + B ran
88 + 103 = 191. (A find glob counted 193 — the two extras are .snap snapshot files,
not tests.)

check:readme-exports was red on first run and that reading was NOT MEASURED, not a
failure
: its own message said type entry ./dist/index.d.ts is not on disk -- run pnpm build first for all 236 items, none of them in a package I touched. After building all 39 packages
it is a real green with 0 unbuilt.

Gates deliberately not run locally, as CI owns them: test:e2e, test:e2e:live:ci,
check:node-esm-load (full build leg; the --specifiers-only leg is green above),
check:i18n-*, check:action-forward-parity, check:designer-field-key-parity,
check:icon-record-names, check:skills-paths, type-check:scripts,
type-check:vitest-setup — no file in this diff is an input to any of them.

7. The pin that stops this regressing (T2)

#5684's guard is key-set only — it checks key presence, and type was present on both
sides the whole time, which is exactly how this instance survived while its siblings were
caught. The value-level pin has two halves:

  • packages/types/src/__tests__/table-column-type-canonical.test.ts — interface tuple ↔ zod
    enum parity, loud refusal (money fails with type in the error path), and
    normalizeTableColumnTypetotality over all 49 spec field types.
  • packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx
    the third end neither of those can see: every literal the renderer branches on, derived
    from its source, must be a declared member. Carries blind-spot guards (no switch, no
    includes(editType), exactly one .has(editType)) so a hidden branch fails the instrument
    instead of silently under-counting it.
  • packages/plugin-grid/src/__tests__/columnTypeEmitSeam.test.tsx — the seam, pinned
    behaviourally through the editor a column actually opens, with a control probe.

A future new inference value turning these red is BY DESIGN, and the note at each pin
says so and names the two correct repairs (publish the value across all three ends, or fold it
at the producer) so the next reader does not "fix" it by loosening the mirror.

The zod mirror now builds its z.enumfromTABLE_COLUMN_TYPES rather than restating the
members, so the types↔zod leg cannot drift structurally; the pins guard the legs that cannot
be derived.

Changeset

minor for @object-ui/types (published-validator accept-set narrowing + new public API),
patch for the three others. Never majorcheck-changeset-no-major.mjs green above.
The text names the newly-refused spellings in two groups (typos/invented names; object-schema
field types written into a column slot) and states that in-repo authored metadata needs no
migration, with the §1 measurement behind it.


Generated by Claude Code

`TableColumn.type` disagreed three ways: the interface declared 8 literals,
the zod mirror declared `z.string()` and accepted anything, and the renderer
branched on a third set it could only read through an `as any` cast.
Per the maintainer ruling of 2026-08-25 (Option B), the 8-literal interface
union is canonical:
- `TABLE_COLUMN_TYPES` is now the single declaration of the vocabulary, and
the zod mirror builds its `z.enum` from it, so the two cannot drift.
- Column-inference producers fold their inferred value onto that vocabulary at
their emit seam (`normalizeTableColumnType`) instead of forwarding an object
schema's field type verbatim. Two producers do this: `ObjectGrid` and
`ObjectDataTable`. An out-of-union type drops the `type` annotation — never
the column.
- The renderer's undeclared dialect (`int`/`integer`/`float`/`double` in
`NUMERIC_EDIT_TYPES`, the `datetime-local` branch) is gone and the `as any`
cast with it, so the read is typed against what the interface publishes.
A value-level parity pin covers all three ends; objectui#5684's guard is
key-set only and cannot see value drift.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.8 KB3266.6 KB
Main entry chunk (gzip)154.1 KB350 KB
Entry fileindex-Dk1KuX-U.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)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.84KB114.57KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.18KB47.97KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.46KB34.48KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)127.52KB31.01KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.46KB54.47KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.70KB7.69KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Projects

None yet

1 participant

@os-support-ai