Skip to content

fix(react): guard mapDensity against prototype-member rowHeight spellings (#4442) - #4457

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4442-bridge-prototype-guard
Aug 12, 2026
Merged

fix(react): guard mapDensity against prototype-member rowHeight spellings (#4442)#4457
yinlianghui merged 1 commit into
mainfrom
claude/issue-4442-bridge-prototype-guard

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Closes #4442

Defect

bridgeListView's mapDensity indexed a plain object literal with an unchecked key, so the lookup reached Object.prototype. The parameter is typed RowHeight, but the boundary a host's stored view definition actually crosses is SpecBridge.transformListView, whose parameter is any:

new SpecBridge().transformListView({ name: 'x', rowHeight: 'toString' }).density
// => [Function toString]

That is a function returned from a read whose return type is three strings or nothing. bridgeListView writes the key under if (density), and a function is truthy — so the bad value was not merely returned, it was stored on a SchemaNode whose renderer expects 'compact' | 'comfortable' | 'spacious'. Same for constructor, valueOf, hasOwnProperty, isPrototypeOf, propertyIsEnumerable and toLocaleString.

Fix

One guard, the exact convention PR #4447 applied to the core twin (rowHeightToDensityMode) and the repo's existing shape at eight other sites (freeze-schema.ts:135, metadata-admin/predicate.ts:305, …):

if (!rowHeight) return undefined;
if (!Object.prototype.hasOwnProperty.call(ROW_HEIGHT_TO_DENSITY, rowHeight)) {
  return undefined;
}
return ROW_HEIGHT_TO_DENSITY[rowHeight];

Test half: #4440's agreement pin (RowHeightDensityAgreement.test.ts) carried a comment saying prototype-member keys were deliberately excluded because the bridge still leaked a function, pointing here. That exclusion comment is gone and both of its off-spec lists now carry the seven prototype-member spellings, plus one separate case pinning the exact expression the issue measured.

Red-first evidence

Method: commit the work, then restore only the pre-fix source from HEAD~ (git checkout HEAD~ -- packages/react/src/spec-bridge/bridges/list-view.ts) so the extended pin runs unchanged against the unfixed bridge. No git stash — the stash stack is shared across worktrees (AGENTS.md §9).

Pre-fix, 9 of 19 fail — the 7 new it.each rows, the boundary case, and the agreement invariant:

AssertionError: expected [Function toString] to be undefined
- Expected: undefined
+ Received: [Function toString]
 ❯ RowHeightDensityAgreement.test.ts:89:43

AssertionError: expected [Function Object] to be undefined      // 'constructor'
AssertionError: expected [Function valueOf] to be undefined     // 'valueOf'

FAIL … > never writes a function into the SchemaNode density (#4442)
AssertionError: expected [Function toString] to be undefined
 ❯ RowHeightDensityAgreement.test.ts:100:28

FAIL … > agrees for every off-spec input without either side being read first
AssertionError: expected undefined to be [Function toString] // Object.is equality
 ❯ RowHeightDensityAgreement.test.ts:123:51

 Test Files  1 failed (1)
      Tests  9 failed | 10 passed (19)

Restoring the fix (git checkout HEAD -- …) turns the same run green with a clean git status --porcelain:

 Test Files  1 passed (1)
      Tests  19 passed (19)

Verification

Gate Result
pnpm --filter '@object-ui/react^...' build (dependency closure, first) green
pnpm exec vitest run packages/react/ 39 files, 536 passed
pnpm --filter '@object-ui/react' type-check (tsc --noEmit and tsc -p tsconfig.test.json) both green
pnpm exec eslint on the two changed files 0 errors (2 pre-existing no-explicit-any warnings on untouched mapColumn lines)
Emitted .d.ts diff, pre-fix vs post-fix build byte-identical across all 62 declaration files

Grading

Patch for @object-ui/react. The ruling made this conditional on the emitted .d.ts not moving; it does not — the guard is runtime-only inside a non-exported function whose signature is unchanged, verified by rebuilding the package on both sides and diffing all 62 declaration files (identical). So no escalation to minor under the #4403/#4177 precedent. No spec-valid rowHeight changes its answer; only off-spec prototype-member spellings move, from a leaked function to abstention.

Merged origin/main (d0c3b26#4448/#4449/#4450) before verifying: git diff confirms those three touch no file under packages/react/src/spec-bridge/, so #4450's repo-wide type="button" sweep is disjoint from this surface. Fast-forward, no conflicts.


Generated by Claude Code

…ings (#4442)

bridgeListView's mapDensity indexed a plain object literal with an unchecked
key, so the lookup reached Object.prototype. The parameter is typed RowHeight,
but the boundary a host's stored view definition actually crosses is
SpecBridge.transformListView, whose parameter is any -- so rowHeight: 'toString'
came back as Object.prototype.toString, a function, out of a read whose return
type is three strings or nothing. bridgeListView writes the key under
`if (density)` and a function is truthy, so the value was stored on a SchemaNode
whose renderer expects 'compact' | 'comfortable' | 'spacious'.

Guarded with Object.prototype.hasOwnProperty.call(...), the same guard
@object-ui/core's rowHeightToDensityMode grew in #4440 (PR #4447) and the repo's
convention at eight other sites. The #4440 agreement pin now covers the
prototype-member family in both of its off-spec lists instead of excluding it
with a comment pointing here.

Closes #4442
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 12, 2026 9:34am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.32KB 108.45KB
core (index.js) 2.99KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 153.42KB 41.19KB
fields (index.js) 228.99KB 56.82KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.33KB 1.20KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 62.01KB 17.63KB
plugin-chatbot (index.js) 181.17KB 43.03KB
plugin-dashboard (index.js) 120.75KB 31.38KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.03KB 59.77KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 187.99KB 49.92KB
plugin-kanban (index.js) 48.62KB 13.42KB
plugin-list (index.js) 110.20KB 26.79KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.99KB 10.74KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.08KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.73KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Collaborator Author

ACCEPT — step-7 复核 by PM session session_017Qqyix2QcnpUC9XeYVDzx3 (wind-down mode, focused review).

Flipping ready + arming auto-merge. Slot NOT refilled per maintainer's wind-down instruction.


Generated by Claude Code


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 12, 2026 09:44
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 8f85f8b Aug 12, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4442-bridge-prototype-guard branch August 12, 2026 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bridgeListView's mapDensity indexes its table with an unguarded key, so rowHeight: 'toString' returns a function as the density

2 participants