Skip to content

refactor(react): bridgeListView maps only the five rowHeight spellings the spec admits (#4352) - #4439

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4352-mapdensity-dead-spellings
Aug 12, 2026
Merged

refactor(react): bridgeListView maps only the five rowHeight spellings the spec admits (#4352)#4439
yinlianghui merged 1 commit into
mainfrom
claude/issue-4352-mapdensity-dead-spellings

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4352

Implements the delegated ruling on #4352: delete the four rowHeight spellings mapDensity carried that RowHeightSchema does not admit, conditioned on the card's own empirical pre-check. AGENTS.md #0.1 — one strict contract beats N dialects.

mapDensity had a nine-key table (compact, short, comfortable, spacious, small, medium, large, tall, extra_tall) against a five-value spec enum (short | compact | medium | tall | extra_tall), so comfortable / spacious / small / large were unreachable from any spec-valid list view. They survived because the parameter was widened back to rowHeight?: string to admit them, and the fixture asserting three of them compiled against nothing (the package's build tsconfig excludes tests). PR #4351 re-spelled the fixture; this PR takes the source half.

Empirical pre-check — the ruling's stop condition, run FIRST

The ruling was conditioned on this: a LIVE authored legacy spelling means STOP (the #0.1 fix would then be metadata conversion, a different card). Result: clean — zero authored uses of any of the four.

Swept word-boundary-aware, every hit read and classified, across three repos:

ScopeWhat was searchedLegacy spellings authored as a rowHeight value
objectui @ 24bb2de4fexamples/, apps/ (incl. console view metadata), all packages/, every *.json / *.yml / *.yamlrowHeight / densityMode / row_height assigned one of the four; the four words on any line mentioning rowHeight; every comfortable / spacious occurrence repo-wide (75 lines, all read)none
objectstack @ 5d24f4b94packages/spec, examples/app-showcase, examples/app-crm, packages/platform-objectssame patterns; plus every authored rowHeight value in the reponone — every authored value is either the boolean userActions.rowHeight flag or one of compact / short / medium / tall
hotcrm @ 333259a (a real downstream app on @objectstack 17.0.0-rc.5)same patternsnonesrc/views/lead.view.ts:182 authors rowHeight: 'medium'

Classification of what the sweep did turn up, since three of the four words are common English and two of them are also the output vocabulary:

  • Every comfortable / spacious in objectui is density-sideDensityMode (compact | comfortable | spacious), the three-step vocabulary mapDensity maps onto — or English prose. The only rowHeight-side occurrences in the entire repo were the four dead keys in list-view.ts itself.
  • small / large appear only as unrelated component variants (examples/schema-catalog/.../components-basic-text/small.json authors variant: "small") and prose.
  • Two hits author densityMode: 'spacious' — the legacy density alias, not a rowHeight. It cannot deliver a legacy spelling into this bridge: normalizeListViewSchema folds it through DENSITY_MODE_TO_ROW_HEIGHT, typed Record< DensityMode, RowHeight >, whose codomain is compact / medium / tall. That was the one producer path worth ruling out, and it is ruled out by its type.

The change

packages/react/src/spec-bridge/bridges/list-view.ts only.

The four keys are gone; the parameter takes the spec's RowHeight instead of string, so the widening dies with the branch; and the table is lifted to a Record< RowHeight, … > const, which buys the property @object-ui/core's equivalent maps already have — a row height added upstream fails the build here instead of silently arriving with no density. A non-spec rowHeight falls through to undefined, and since the bridge writes if (density) node.density = density, the key is simply absent.

Red-first (pre-fix, captured against the still-lenient branch)

The new pin was written and run before the source was touched. Exactly four red, one per spelling, each naming the density the legacy value was being rehabilitated into:

 × leaves density unset for the off-spec rowHeight comfortable
× leaves density unset for the off-spec rowHeight spacious
× leaves density unset for the off-spec rowHeight small
× leaves density unset for the off-spec rowHeight large
AssertionError: expected 'comfortable' to be undefined
AssertionError: expected 'spacious' to be undefined (spacious, large)
AssertionError: expected 'compact' to be undefined (small)
Test Files 1 failed | 2 passed (3)
Tests 4 failed | 58 passed (62)

The pin routes through SpecBridge.transformListView, whose parameter is any. That is deliberate and is the honest framing: the untyped boundary is the one a host's stored JSON actually crosses, and after this change it is the only way to get such a value in — writing it on bridgeListView directly no longer type-checks, which is the static half of the same fix. A control case (rowHeight: 'gargantuan', a string in neither vocabulary) pins the fall-through itself and is green on both sides.

The five spec values keep PR #4351's coverage, unchanged — no gap to extend.

Changeset grading — measured, not assumed

.changeset/mapdensity-dead-rowheight-spellings-4352.md: '@object-ui/react': minor. Never major, per the version-alignment rule.

The measurement that decides it, run as part of reverse verification below: the emitted packages/react/dist/spec-bridge/bridges/list-view.d.ts is byte-identical before and after. mapDensity is module-local and never reached the published surface, and bridgeListView: BridgeFn< ListViewSpec > with ListViewSpec = Partial< ListView > is untouched. So nothing published narrows in type — which is what would have made this #4403's minor, and it does not apply.

What does change is runtime output of an exported function, for a class of input: a host passing rowHeight: 'comfortable' used to get density: 'comfortable' back and now gets no density key. The sweep found zero such hosts in the three repos visible to me, but @object-ui/react is published, so an out-of-tree host is not excludable — this is exactly the "behaviour change for any host still passing a legacy spelling" #4352 flagged as the separate decision. AGENTS.md's version-alignment rule is decisive for that case: objectui's own breaking changes are graded minor with the breaking semantics spelled out in the body, which the changeset does. patch (#4403's grading for a module-local, non-exported table) would have been defensible only if nothing observable changed, and something does.

Verification (local, all green)

  • Repo-root vitest packages/react/38 files, 517 tests passed.
  • packages/reactboth tsc commands: tsc --noEmit (exit 0) and tsc -p tsconfig.test.json (exit 0).
  • Build closure first, per the fresh-worktree rule: pnpm --filter '@object-ui/react^...' build — exit 0, before any judging.
  • eslint on both touched files — 0 errors (2 warnings, both pre-existing @typescript-eslint/no-explicit-any on mapColumn's untouched signature at lines 22 / 28).
  • check:control-bytes — OK, 4140 tracked text files; plus a self-scan of the touched files for the wider control-byte range: no hits.
  • check:phantom-deps — OK, every in-scope import declared by the package that publishes it.
  • check-changeset-presence2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s).
  • check-changeset-no-major — OK.

Reverse verification (three directions predicted before running)

Taken out with git checkout origin/main -- FILE and put back with git checkout HEAD -- FILE — never git stash.

A. Revert the deletion → the new pins red. Predicted normal direction (red, not the inverted or more-diagnostics case: the assertion reads a value the deleted limb produces, not a count a gate reports). Measured, exactly four:

 ❯ SpecBridge.test.ts (23 tests | 4 failed)
× leaves density unset for the off-spec rowHeight comfortable
× leaves density unset for the off-spec rowHeight spacious
× leaves density unset for the off-spec rowHeight small
× leaves density unset for the off-spec rowHeight large
AssertionError: expected 'comfortable' to be undefined

B. Controls green on both sides. Predicted and measured: maps every spec rowHeight to a density (the five spec values) and leaves density unset for a rowHeight in no vocabulary at all both pass in the reverted state — 19 of the file's 23 tests passed there, all four failures being the pins from A. A green cannot be bought by breaking the surviving mappings.

C. Emitted .d.ts identical. Predicted identical, measured identical (diff empty) — the changeset-grading measurement above.

Restored afterwards: git status clean, git diff HEAD empty, rebuild green, packages/react/ back to 38 files / 517 tests passed.

Surface discipline

Touched packages/react/src/spec-bridge/bridges/list-view.ts, its __tests__/SpecBridge.test.ts, and the changeset. Nothing in packages/components/**, packages/plugin-chatbot/**, packages/plugin-dashboard/**, or any new sweep-gate suite. P1SpecBridge.test.ts was read and left alone: its three density cases already use spec values (short / tall / extra_tall) and stay green. No out-of-scope findings.


Generated by Claude Code

…s the spec admits (#4352)
`mapDensity` carried a nine-key table while `RowHeightSchema` admits five, so
`comfortable`, `spacious`, `small` and `large` were unreachable from any
spec-valid list view. The parameter was widened back to `rowHeight?: string` to
let them in, and the fixture asserting three of them compiled against nothing
(the build tsconfig excludes tests), so four branches of renderer-side dialect
read as live capability.
Delete them. The parameter takes the spec's `RowHeight` honestly and the table
is `Record<RowHeight, ...>`, so a row height added upstream fails the build here
instead of arriving with no density. AGENTS.md #0.1 — one strict contract beats
N dialects; an off-spec `rowHeight` falls through to no density and gets fixed
at the producer.
Empirical pre-check required by the ruling: zero authored uses of the four
spellings across this repo, the objectstack example apps and the console's view
metadata.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 12, 2026 7:15am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-B7R9OEpn.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.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)489.20KB108.43KB
core (index.js)2.99KB1.14KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)153.42KB41.19KB
fields (index.js)228.99KB56.82KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.33KB1.20KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.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.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)62.01KB17.63KB
plugin-chatbot (index.js)181.17KB43.03KB
plugin-dashboard (index.js)120.75KB31.38KB
plugin-designer (index.js)211.16KB42.76KB
plugin-detail (index.js)239.03KB59.77KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)187.99KB49.92KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.21KB26.79KB
plugin-map (index.js)18.05KB5.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.99KB10.74KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.96KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
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)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
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-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — PM 复核 (session session_017Qqyix2QcnpUC9XeYVDzx3), closes #4352.

  • The pre-check is the family's most thorough: three repos including a real downstream app, every hit read and classified, the one producer path ruled out BY TYPE (the densityMode fold's codomain cannot emit a legacy spelling), and the stale-shared-checkout catch — discarding a sweep pass that predated test(react): type-check the whole test tree, and retire the narrow typetests project (#4040) #4351 rather than trusting it — is exactly the premise discipline the loop runs on.
  • The changeset judgment is affirmed at minor: the flagged non-clean conditional (type surface byte-identical per RV-C, behavior surface changed for off-spec input) resolves by the version-alignment rule precisely as graded — an out-of-tree host passing a legacy spelling observes the change, and that is what minor-with-semantics exists for. No downgrade.
  • The scope+ (module-level Record<RowHeight,…>) is the ruling's own kill-the-widening requirement executed, with the upstream-enum-growth build-failure property as a bonus; the any-boundary test routing is correctly reasoned (the compile error IS half the fix, and that boundary is where stored JSON actually crosses).
  • RV-C (the .d.ts byte-identity as the measurement BEHIND the grade, not decoration) and the B controls (a green cannot be bought by breaking survivors) are to standard. finding(core): rowHeightToDensityMode coerces an off-spec rowHeight to comfortable, the opposite of what the spec bridge now does with the same input #4440 (core's opposite answer for the same off-spec input) is triaged next. CI converged.

Flipping ready + arming auto-merge.


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(react): bridgeListView's mapDensity carries four rowHeight spellings no spec-valid list view can hold

2 participants

@yinlianghui@claude