Skip to content

fix(react): memoise the scoped-style schema SchemaRenderer hands down - #6591

Draft
os-support-ai wants to merge 4 commits into
mainfrom
claude/issue-6270-schemarenderer-scopeclass-memo
Draft

fix(react): memoise the scoped-style schema SchemaRenderer hands down#6591
os-support-ai wants to merge 4 commits into
mainfrom
claude/issue-6270-schemarenderer-scopeclass-memo

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#6270

Verified at bc5773fbb.

The defect, re-measured

The card named SchemaRenderer.tsx:1076; the line is now :1252 (re-measured, not inherited):

constschemaForComponent=scopeClass
? { ...evaluatedSchema,className: mergedClassName}
: evaluatedSchema;

Confirmed: for a node taking the scopeClass branch this allocated a new object on every SchemaRenderer render, even when the evaluatedSchema memo directly above it held. Measured through the real SchemaRenderer path with a probe registered in the real ComponentRegistry:

nodedistinct schema identities across 1 parent re-renderafter
plain node (control)1 (stable)1
responsiveStyles: { base: … } (control)1 (stable)1
responsiveStyles: { large: … } (fix case)3 (fresh every render)1

The fix is a hoist, not a one-line useMemo

The card's suggested shape — "memoize schemaForComponent on [evaluatedSchema, mergedClassName]" — cannot be written at that line. The use site sits after if (!evaluatedSchema) return null, the _hidden return, and the unresolved-component returns. A useMemo there is a conditional hook: a node toggling hidden → visible would call one more hook than the previous render and React would throw "Rendered more hooks than during the previous render."

So the whole ADR-0065 scoped-styling computation moves into one useMemo keyed on [evaluatedSchema, autoStyleId], hoisted above the early returns, returning { scopeClass, scopedCss, mergedClassName, schemaForComponent }. That dep set is complete: every value is a pure function of the evaluated node (className, id, responsiveStyles) plus the useId fallback.

The non-scoped branch keeps handing down evaluatedSchemaitself, never a copy — a copy there would spread the same instability to every node in the tree.

Anti-staleness is pinned, not assumed

Memoising an object that legitimately changes is this fix's failure mode, so four tests pin the opposite direction — a changed className, a changed value, a changed breakpoint, and (the strongest) a live interpolated value with the schema prop identity held constant, driven through PageVariablesProvider. All four must deliver a new identity.

Reverse verification

Direction predicted before running, then measured. Pre-fix SchemaRenderer.tsx restored from the pinned merge-base 6a7893d57; mutation and restore both proven on disk (anchored grep counts in both directions + git hash-object against the HEAD blob), never by an exit code.

pre-mutation : fixed form 1, pre-fix form 0
post-mutation : fixed form 0, pre-fix form 1 (on-disk hash == BASE blob)
→ Tests 2 failed | 9 passed (11)
× node with a sized responsiveStyles breakpoint (the fix case)
× holds across several parent re-renders, not just one
post-restore : on-disk hash == HEAD blob, `git diff HEAD` empty

Exactly the two fix-direction assertions go red; every control stays green.

The first pass predicted 2 red and got 3 — the boundedness pin went red too, contradicting its own doc comment. Cause: it counted identities from mount, and SchemaRenderer force-updates itself once after mounting (re-checking ComponentRegistry for a lazily registered component), so mount alone yields two renders — which pre-fix already handed down two different objects. It was folding parent-render instability in and duplicating the fix-case test. Now measured from the last render before the first click, so it covers only re-renders the parent took no part in. Fixed in bc5773fbb; the run above is the corrected one.

The two inherited claims, re-measured

1. "Not new exposure from #6018" — CONFIRMED, with a date correction.

memokeyed [schema] bycommitdate
mapConfig#5976 → PR #6016538ed92462026-08-24
dataConfig#6018 → PR #62662aa2c226a2026-08-25

mapConfig's [schema] keying predates dataConfig's by one day, so #6018 genuinely only made a second memo share an existing exposure. ⚠️ One refinement: "pre-existing" is true, "long-standing" would not be — before #6016 (2026-08-24) mapConfig was not keyed on [schema] at all, it was unmemoised. The exposure was ~1 day old when the card was filed, not ancient.

Worth recording: ObjectMap's own memo doc comments assert that "the identity that reaches this component is ALREADY stable across the renders that matter" and name SchemaRenderer's evaluatedSchema as one of three callers handing over a memoised node. That sentence was false for scoped-style nodes until this PR. It is true now.

2. "Bounded — it is NOT an infinite loop" — CONFIRMED, and now pinned as a test rather than an argument.

React re-renders only the subtree below the component that set state, so a consumer's own setData never re-runs SchemaRenderer, and the consumer keeps the very object React last handed it. The fetch effect's other deps (schema.filter, schema.sort) are nested references a shallow spread preserves, so they were stable even pre-fix. Cost was one redundant refetch cycle per parent render, never runaway. a consumer re-rendering itself keeps the exact schema object it was handed fixes this as a property of the renderer; it is green on both sides of the fix and turns red only if something upstream starts re-rendering SchemaRenderer in response to a consumer's state.

⚠️ One correction to the card's framing: the instability does not need a parent re-render to fire at all. SchemaRenderer's mount effect calls forceUpdate() once to re-check the registry, so every mount of a scoped node already handed its child two different schema objects. Still bounded; just more frequent than "on parent render" reads.

The reproduction trap, pinned

hasResponsiveStyles requires large / medium / small / xsmall; { base: … } does not take the branch. The first test proves from the deliveredschema.className which fixture is on which side, so the trap cannot silently turn the suite into an assertion that cannot fail. Every zero reading in it has a positive control in the same query shape.

Gates — each one's own verdict line

gateverdict
vitest run packages/react/ packages/plugin-map/Test Files 77 passed (77) / Tests 999 passed (999)
vitest run --shard=1/4 (whole repo)Test Files 529 passed (529) / Tests 6562 passed | 1 skipped (6563)
vitest run packages/core/src/styling/ packages/plugin-detail/Test Files 111 passed (111) / Tests 1041 passed (1041)
pnpm --filter @object-ui/react type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.test.json)
eslint packages/react --format json128 files, 0 errors, 363 warnings
node scripts/check-changeset-presence.mjs✅ … declares 1 changeset(s)
node scripts/check-changeset-no-major.mjs✅ No changeset declares a major bump.
node scripts/check-control-bytes.mjs✅ OK (scanned 5444 tracked text file(s); skipped 85 binary)
node scripts/check-self-import.mjs✅ No package names itself inside its own src/.
check:vi-mock-specifiers / check:shell-escape-residue✅ OK

Exit codes captured before any pipe (cmd > file 2>&1; EXIT=$?), never read off a tail.

Type-check coverage was verified, not assumedtsc -p tsconfig.test.json --listFiles confirms both SchemaRenderer.tsx and the new test file are in the checked set (positive control: an existing sibling test is too). A type-check that excluded *.test.ts would have been a true statement about nothing.

Declared narrowing.pnpm lint (turbo run lint, whole repo) and shards 2–4 were not run: a single shard exceeded the 600s foreground cap. What replaces them, and why it excludes nothing this diff could move: the behaviour change reaches only nodes carrying responsiveStyles, and the complete set of files mentioning it is core/src/styling/scoped-styles.ts (+ its test), plugin-detail/src/synth/buildDefaultPageSchema.ts, react/src/SchemaRenderer.tsx, the new test, and two index.css files — every one of those packages is in a green run above. For lint: the root eslint.config.js declares noparserOptions.project / projectService, so type-aware linting is off and no untouched file's verdict can move under this diff; packages/react (the only package with changed source) is fully linted at 0 errors. CI still runs the full farm.

Scope note for the reviewer

plugin-map and every downstream memo key were left untouched, per the dispatch order. ⚠️ The triage comment on #6270 adopted a second item into scope — re-keying the load-bearing fetch effects onto the primitives they read (dataConfig.provider, dataConfig.object) so useMemo returns to being a pure optimisation — and the dispatch order I received explicitly forbids touching downstream renderers' memo keys. I followed the dispatch order and am flagging the conflict rather than picking a side. That work is unstarted and still worth doing: useMemo is not a semantic guarantee, and the fetch effect's correctness currently rests on a cache React is allowed to discard. This PR makes the identity stable; it does not make it guaranteed.


Generated by Claude Code

…yle node
Measures, through the real SchemaRenderer path, whether the `schema` object a
downstream component receives keeps its identity across a parent re-render.
Fails today for a node carrying a sized `responsiveStyles` breakpoint: the
scope-class merge allocates a fresh object every render, so every downstream
`[schema]` memo re-runs. Plain and `base`-only nodes stay stable (controls).
Refs objectui#6270
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
A node carrying `responsiveStyles` (ADR-0065) takes a branch that rebuilds the
schema object to merge the scope class into `className`. That spread was not
memoised, so it allocated a new object on every SchemaRenderer render even when
the `evaluatedSchema` memo above it held — and every downstream renderer keyed
on `[schema]` saw a fresh identity and re-ran.
Computed in a memo keyed on `[evaluatedSchema, autoStyleId]`, hoisted above the
early returns: the use site sits after `if (!evaluatedSchema) return null` and
the `_hidden` return, so a useMemo written there would be a conditional hook and
a node toggling hidden -> visible would crash on the hook count.
Fixes objectui#6270
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
Adds the pin for the card's second claim: a consuming renderer's OWN setState
re-renders only its subtree, so it keeps the exact schema object SchemaRenderer
handed it. That is what makes this a redundant-recompute cost and not a refetch
loop. Green on both sides of the fix by design.
Also replaces the module-level `let` the harness reassigned during render with
real click events — that reassignment is a render side effect
(`react-hooks/globals`, 3 errors), and in a file that measures render counts the
instrument must not break the rule under test.
Adds the changeset for the @object-ui/react behaviour change.
Refs objectui#6270
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
…force-update
Reverse-verification caught this test contradicting its own doc comment: it
claimed to be green on both sides of the fix, and went red on the pre-fix tree.
Cause: it counted identities from mount. SchemaRenderer force-updates itself
once after mounting (it re-checks ComponentRegistry for a lazily registered
component), so mount alone yields two SchemaRenderer renders — and pre-fix those
two already handed down two different schema objects. The test was folding that
parent-render instability in and duplicating the fix-case test instead of
pinning the independent property.
Now measured from the last render before the first click, so it covers only
re-renders the parent took no part in.
Refs objectui#6270
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPTED on substance. ⛔ NOT enqueued: this head has no verdict yet.

Substance

Accepted. The three things this seat asked for are all present and all measured rather than asserted:

  • The card's proposed shape was overturned with a reason — a useMemo at that use site sits below if (!evaluatedSchema) return null and the _hidden / unresolved-component returns, so it is a conditional hook and a node toggling hidden → visible crashes React on the hook count. The hoist is the correct shape, and the non-scoped branch still hands down evaluatedSchema itself rather than a copy — which is what keeps the fix from spreading the instability it removes.
  • The pin can go red. Reverse verification predicted the direction first, restored the pre-fix file from the pinned merge-base, proved mutation and restore on disk by git hash-object against both blobs, and landed exactly the two fix-direction assertions in the red with every control green.
  • The first reverse-verification pass predicted 2 red and got 3, and that was reported rather than smoothed over. The third red was the dev's own boundedness pin contradicting its doc comment because it counted identities from mount — which is also how the forceUpdate() correction below was found. Reporting the miscount is what makes the rest of the numbers usable.

Both inherited claims were re-measured and both came back with corrections, both accepted: the exposure was ~1 day old, not long-standing (mapConfig was unmemoised before #6016 on 2026-08-24); and the instability fires on every mount, not merely per parent render, because SchemaRenderer's mount effect calls forceUpdate() once to re-check the registry. The card's framing was wrong on the second point and this PR's is right.

⛔ Both dispatch constraints verified against the file list: three files, none under packages/types, and plugin-map and the downstream memo keys untouched.

The scope conflict — resolved, and not on this PR

Resolved on the card: this PR keeps Fixes #6270, and the half the dispatch order deferred is now objectui#6592, filed with the adopted text quoted verbatim and Blocked-by: #6270. Flagging it instead of silently picking a side was the right call — the dispatch order was the later and more specific instruction, and widening the diff would have made this PR's reverse verification unreadable.

⛔ Why this is not going into the merge queue

Read at head bc5773fbbe0f (committed 16:19:14Z):

readingvalue
check-runs on head0
check-suites on head4 — vercel, fly-io, claude, cloudflare-workers-and-pages, all queued, none of them GitHub Actions
combined commit statussuccess, total_count 1
control — same two queries on PR #6590's head29 check-runs, 23 check-suites

The GitHub Actions farm has not been scheduled for this head at all. The success aggregate is the known false comfort: it is a negative screen only, and here it is reporting on one context out of a farm that never started. Zero checks is NOT MEASURED, not green — the same rule that applies to a prerequisite-not-met gate.

Draft status is not the explanation: PRs #6587 and #6589 are both drafts and both have Actions runs on their heads right now. The two live candidates are a scheduling backlog and a push that landed without firing its trigger; this seat has established neither, so it is not acting on either.

First step for whoever picks this up: re-read the head's check-suites. If the Actions suite is still absent, update_pull_request_branch (this PR is also behind) both refreshes the base and produces a new head that the farm will pick up — at which point the reviewed head changes and the verdict must be re-read on the new one. ⛔ Do not enqueue on a head whose farm never ran.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.4 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-CLeU4UrE.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)11.71KB4.46KB
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)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
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.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.74KB27.50KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
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)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
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.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Correction to my review comment above — the farm was not missing, it was late.

Re-read at 16:56Z on the same headbc5773fbbe0f:

16:45Z16:56Z
check-runs029 — 20 success, 3 skipped, 6 in progress
check-suites4, none GitHub Actions23, including 19 github-actions

Still in progress: Lint, Type Check, Test (shard 1/4 … 4/4). Zero failures so far.

So the reading was accurate when taken and the conclusion drawn from it was not: I offered two candidates, a scheduling backlog and a push that never fired its trigger, and the answer was neither in the shape I meant — it was ordinary scheduling latency of roughly ten minutes on an unchanged head.

Ignore the "first step" in my comment above. Do not run update_pull_request_branch to shake the farm loose — it never needed shaking, and a branch update would have produced a new head, thrown away the 20 checks already green, and restarted the wait. behind is still not a call to action.

The actual next step is the ordinary one: wait for the six running checks, then judge on every check green rather than the required subset, on this head. This PR remains ACCEPTED on substance and ⛔ not enqueued.

⭐ Worth carrying into the lane's readings: zero scheduled checks is NOT MEASURED, and "not measured" is a statement about the clock as much as about the trigger. Re-read before diagnosing; an absent farm at T+26min was a full farm at T+37min.


Generated by Claude Code

@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Cleared to land — review complete against the code tree. Blocked only on API quota.

Supersedes the "not enqueued" hold in my two comments above. Nothing about the change is outstanding; the only thing missing is the mechanical flip-and-enqueue, which I could not perform.

Verdict on the reviewed head

bc5773fbbe0f — the same head I reviewed, so this verdict is not stale:

check-runs29 — 26 success, 3 skipped, 0 failures, 0 pending
the 3 skipsdependabot, Test (coverage), Test (coverage shard ${{ matrix.shard }}/4) — the known benign trio; the last is an unexpanded matrix template in the check name, not a shard that failed to run
mergeable_stateclean (it was behind earlier; it is not any more)

⛔ Entry qualification was judged on every check green, not the required subset.

The code tree was read, not the report

Reviewed the diff at 6a7893d57804..bc5773fbbe0f directly rather than accepting the description:

  • The hoisted useMemo is keyed [evaluatedSchema, autoStyleId] and sits above if (!evaluatedSchema) return null, so it is unconditional. The old inline computation is replaced by a destructure of its result; both renderer-channel comments survive verbatim.
  • ⛔ The non-scoped branch returns schemaForComponent: nodeevaluatedSchemaitself, never a copy. This was the detail worth checking by hand: a copy there would have spread the very instability the PR removes to every node in the tree.
  • Non-object / primitive nodes fall through untouched.
  • 3 files. packages/types untouched; plugin-map and every downstream memo key untouched, per the dispatch order.

One thing the refactor could have silently changed, checked rather than assumed. The old branch tested hasResponsiveStyles(_responsiveStyles); the new memo tests node.responsiveStyles. Those are the same value only if _responsiveStyles comes from the evaluated node — and it does: it is a destructuring rename in the DOM-prop strip at :1275, whose source object is } = evaluatedSchema. Same value, so the branch condition is unchanged. Had that source been anything else, the {large} fixture would have stopped taking the branch and the whole identity table would have been measuring nothing.

Why it is not in the queue

update_pull_request (draft → ready) failed twice with API rate limit already exceeded for user ID 318092878. The draft toggle is a GraphQL mutation and that quota is independent of REST's — which is why reads on this PR still answer normally. ⛔ Not retried further and not blind-waited on; this environment cannot query the GraphQL reset.

Remaining steps for whoever picks this up — no further review needed:

  1. Flip draft → ready.
  2. Enqueue, then confirm from the timeline (added_to_merge_queue), not from auto_merge, which is a false negative here.
  3. On merge, Fixes #6270 closes that card — that is correct and intended. Its deferred half is already carried by objectui#6592, filed before this was cleared precisely so the close does not take it along.

⛔ If the head moves for any reason, this verdict expires with it and the checks must be re-read on the new head.


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): SchemaRenderer hands a render-fresh schema object to any node with responsiveStyles, defeating every downstream [schema] memo

2 participants

@os-support-ai@claude