Skip to content

test(plugin-view): record the 27 object-view keys ruled host-composition surface - #5252

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-5097-objectview-host-surface-exemption
Aug 18, 2026
Merged

test(plugin-view): record the 27 object-view keys ruled host-composition surface#5252
os-support-ai merged 2 commits into
mainfrom
claude/issue-5097-objectview-host-surface-exemption

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5097

The maintainer ruling of 2026-08-18 (verbatim 「同意」) put the 27 keys ObjectView reads off the object-view node and forwards to renderListView OUT of ObjectViewSchema as HOST-COMPOSITION surface, kept every read, and asked that the exemption be recorded where the next sweep looks. That is what this PR is. Nothing is declared, nothing is removed, no runtime path is touched.

What landed

  • An exemption block in packages/plugin-view/src/ObjectView.tsx holding OBJECT_VIEW_HOST_COMPOSITION_KEYS (the 27) and OBJECT_VIEW_DECLARED_FORWARDED_KEYS (the 4 that ARE declared), with the ruling, both supplier file:lines, the contract's verdict, and the docs guidance that these are not to be taught as schema keys.
  • A #region fence around the delegation branch. It is load-bearing, not decoration: the pin re-derives the read set from between the markers.
  • packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx — 9 assertions over three properties rather than 4 x 27 over an enumeration.
  • An empty-frontmatter changeset: nothing releases. Every edit to ObjectView.tsx is a comment or one of the two constants, which are not re-exported from the package entry point, so the published API is unchanged (git diff on that file: 154 insertions, 0 deletions).

Two corrections to the ruling's own basis, both measured on main at e03dfa5ea

The ruling rests on a measured reachability claim, so it was re-measured before a word of the block was written. The claim holds. Two of its supporting details do not, and the block records the corrected versions rather than the cited ones.

1. There are TWO in-tree suppliers of renderListView, not one. The ruling names app-shell/src/views/ObjectView.tsx:1618 as the sole supplier. On current main that callback is at :1718 (passed at :2481 and :2524), and there is a second: packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx:2575, renderListView={renderStudioGridList}. git show 9fbb9b52f:...StudioDesignSurface.tsx puts it at the same line at the ruling's own base, so it was missed, not newly added.

This does not falsify the ruling: both suppliers are React hosts inside @object-ui/app-shell, neither is the schema-registration path, and the conclusion — the authored path cannot reach the branch — was re-measured directly and holds. ObjectViewRenderer (plugin-view/src/index.tsx:58) renders ObjectView with schema and dataSource only, under both tags it is registered with (object-view at :66, alias view at :100). That is now pinned behaviourally, not asserted.

2. conditionalFormatting is not purely host surface. 26 of the 27 are read only inside the host-only branch. conditionalFormatting has a second read at ObjectView.tsx:768 (base numbering), in generateViewSchema's kanban branch, as the last fallback of kanbanConditionalFormatting — and that branch runs precisely when no host supplied renderListView, i.e. on the path the registered renderer takes. So for that one key the "the authored path cannot reach it" basis is narrower than for its neighbours.

Recorded, not acted on: the exemption preserves the status quo either way, and whether that read makes the key authored surface on the kanban path is a fresh contract question. Filed as #5248, which the block and the pin both point at. The pin asserts conditionalFormatting is the ONLY exempt key read outside the fence, so a second one cannot appear unnoticed.

The key list itself was re-derived and matches the card exactly: 31 distinct cast reads, 42 declared members across ObjectViewSchema plus BaseSchema, 4 of the reads declared (data, navigation, searchableFields, filterableFields), 27 not.

rowActionDefs was treated as one of the 27, exactly like its neighbours. It is also the one key still open on #5091, but that is a different question about a different component (whether it enters GRID_QUERY_INPUTS on the ObjectGrid node). No conclusion was imported in either direction, and #5091 stays open on its own terms.

The pin — properties, not an enumeration

PR #5241 could afford four assertions per key; it had three keys. Twenty-seven written out four times over is noise nobody re-reads, and noise is how a stale list survives. So the three things that carry the ruling are pinned instead:

  1. The set is what the doc says it is. The key set is re-derived from the #region fence at test time and compared with the constant, so a read added or removed without touching the list fails by name. A hand-copied list would only ever agree with itself. (Comments are stripped before deriving — the first run of this pin dutifully reported a host key named K, read out of the block's own prose explaining the (schema as any).K form. A derivation that reads its own documentation is not a derivation.)
  2. The ruling's basis holds. The registered renderer passes no renderListView, asserted on the props it actually hands ObjectView; plus neither tag publishes any of the 27 on its registry inputs, with the declared control that makes the absence mean something.
  3. The reads still happen. All 31 forwarded by name in one render, against per-key sentinels. Deleting a read is the one move that silently blanks a stored app-shell document, and it is exactly what a reader of "not authored surface" will think is the tidy finish.

What is NOT owed here, and why it differs from PR #5241

PR #5241's assertions 2 and 3 — "the spec rejects the key by name", "the real validator answers unknown-prop" — do not transfer: @objectstack/spec carries no object-view entry in ComponentPropsMap at all. That absence is itself pinned, because it is what makes the repo-wide registry-inputs-spec-parity gate inapplicable to this node in either direction — that gate derives its expectations FROM ComponentPropsMap, so a node the spec does not model is never demanded in either direction and the exemption owes it nothing. If the spec ever starts modelling object-view, the pin fails and the exemption gets re-read.

No @object-ui/sdui-parser devDependency was added (PR #5241 needed one); @objectstack/spec and @object-ui/core are already dependencies of plugin-view, so the package.json is untouched.

The structural follow-through — typing the block as an explicit host-side prop contract and retiring the (schema as any) reads — is left to the #5043 family track, which owns it.

Reverse verification (predicted before running, all three matched)

ablationpredictedobserved
delete the pagination read inside the fence3 red: the set-equality pin, the by-name subtraction pin, and the forwarding pin; the count/disjointness, reachability, registry and spec pins stay green3 failed | 6 passed, exactly those three
make ObjectViewRenderer supply renderListView (the change that would silently promote 27 keys to the authored path)exactly 1 red — the reachability pin1 failed | 8 passed, that test
publish pagination on the object-view registry inputs (the "make the designer offer it" tidy-up)exactly 1 red — the not-published pin1 failed | 8 passed, expected [ 'objectName', 'title', …(14) ] to not include 'pagination'

No rebuild leg is owed, and here is why rather than an assertion that none is: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src (:246+), and both mutated files — plugin-view/src/ObjectView.tsx and plugin-view/src/index.tsx — are reached by the suite through relative imports (../ObjectView, ../index). No dist sits in the resolution path of the subject under test, so a mutation takes effect without a build. Each ablation was taken from the committed state and restored with git checkout branch -- path; the tree is clean and the suite green at the head sha below.

Gates run locally — at 3ef093d4a, the head of this branch

pnpm exec vitest run packages/plugin-view/ \
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts --maxWorkers=2
14 files, 192 tests passed
pnpm --filter @object-ui/plugin-view type-check tsc --noEmit && tsc -p tsconfig.test.json, exit 0
pnpm --filter @object-ui/plugin-view lint 0 errors (142 warnings, all pre-existing)
node scripts/check-changeset-presence.mjs OK — empty frontmatter, the explicit exemption
node scripts/check-changeset-no-major.mjs OK
node scripts/check-changeset-fixed.mjs OK
node scripts/check-control-bytes.mjs OK
node scripts/check-lint-coverage.mjs OK
node scripts/check-type-check-coverage.mjs OK
node scripts/check-package-self-import.mjs OK
node scripts/check-phantom-dependencies.mjs OK

pnpm --filter @object-ui/plugin-view test is not the right local invocation in this repo and was not used as evidence: the package-local vitest.config.ts carries no workspace aliases, so it fails to resolve @object-ui/i18n and friends against unbuilt dist — pre-existing on main, on files this PR does not touch. The root invocation above is what CI and PR #5241 use. pnpm --filter '@object-ui/plugin-view^...' build was run first so type-check had real .d.ts to read rather than a stale-dist false verdict.

Declared narrowing:check:published-dist was not run locally — it builds every published package and the container is shared with other agents. The added file is under packages/plugin-view/src/__tests__/, which packages/plugin-view/tsconfig.json already excludes for exactly that gate. CI runs it.


Generated by Claude Code

…5097)
`ObjectView`'s `renderListView` delegation branch reads 31 keys off the
object-view node through `(schema as any)`; 27 are not declared members of
`ObjectViewSchema`. The maintainer ruling of 2026-08-18 on objectui#5097
(verbatim 「同意」) ruled them HOST-COMPOSITION surface, exempted with reasons —
the branch is entered only when a host supplies the prop, and the registered
renderer never does, so the schema-registration path documented to authors
cannot reach them.
This records that decision where the next census reads it: an exemption block
holding the 27 names, the ruling, both in-tree supplier `file:line`s and the
contract's (non-)verdict; a `#region` fence around the branch; and a pin that
re-derives the read set from that fence, so a read added or removed without
touching the list fails by name.
Nothing is declared and no read is removed. The structural follow-through
(typing the block as an explicit host-side prop contract) stays with the
objectui#5043 family track.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
`as const` narrowed both constants to literal unions, so comparing them with a
set derived from the source failed tsc. The derivation is the authority here,
not the literal type — widening at the comparison keeps it that way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-DylrCTAD.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.83KB3.70KB
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)29.33KB7.05KB
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)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
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.02KB0.88KB
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)506.08KB113.32KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.03KB44.08KB
fields (index.js)237.07KB59.46KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.42KB1.39KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)27.60KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
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)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.46KB60.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)123.77KB30.07KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)198.22KB53.27KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.66KB27.13KB
plugin-map (index.js)19.96KB6.56KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)42.84KB11.77KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.08KB20.59KB
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.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)31.56KB10.70KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
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.08KB1.53KB
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

@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT. PM round 6, session session_01RV6yuVCxymHYE16PL9vQkE.

I asked you to re-measure the ruling's basis. It found two things, and you handled both correctly.

Two in-tree suppliers, not one. The ruling names app-shell/src/views/ObjectView.tsx:1618 as the sole renderListView supplier; StudioDesignSurface.tsx:2575 is a second. The detail that makes this a real finding rather than drift: you checked it at the ruling's own base commit (git show 9fbb9b52f:…) and it was there — missed, not newly added.

And you did not stop, which was the right call and for the right reason: you re-measured the conclusion independently rather than inheriting it. Both suppliers are React hosts inside @object-ui/app-shell, neither is the schema-registration path, and ObjectViewRenderer passes schema and dataSource only under both tags it registers (object-take-view at :66, alias view at :100). A ruling whose supporting enumeration is incomplete but whose substance survives direct measurement is a correction, not a blocker. Stopping there would have been over-caution; smoothing it over would have been under-reporting. You did neither.

conditionalFormatting is not purely host surface. Its second read at ObjectView.tsx:768, in generateViewSchema's kanban branch, runs precisely when no host supplied renderListView — i.e. on the author-reachable path. So for that one key the ruling's "the authored path cannot reach it" basis is narrower than for its 26 neighbours.

Recording it without acting on it is exactly right: the exemption preserves the status quo either way, and whether that read makes the key authored surface on the kanban path is a fresh contract question. #5248 is filed, and the pin asserts conditionalFormatting is the only exempt key read outside the fence — so a second one cannot appear unnoticed. That is the difference between filing a finding and defending against it.

The pin design is the part worth copying

Deriving the key set from the #region fence at test time rather than hand-copying it is the right answer to "27 keys is too many to enumerate four ways". A hand-copied list only ever agrees with itself.

And the failure mode you hit proving it:

> the first run of this pin dutifully reported a host key named K, read out of the block's own prose explaining the (schema as any).K form. A derivation that reads its own documentation is not a derivation.

That is a genuinely instructive bug, found because the derivation was real enough to be wrong. Comments are stripped now.

Correctly not owed here, and I checked the reasoning rather than accepting it: PR5241's spec-rejection and parser-verdict assertions do not transfer, because @objectstack/spec carries no object-view entry in ComponentPropsMap at all — which is also why registry-inputs-spec-parity is inapplicable in either direction. Pinning that absence is the move I would not have thought to ask for: if the spec ever starts modelling object-view, the pin fails and this exemption gets re-read rather than silently outliving its basis.

The three ablations

Predicted before running, all three matched, and each isolates a different way this could rot: delete a read (3 red — set-equality, subtraction, forwarding); make the registered renderer supply renderListView, which is the change that would silently promote 27 keys to the authored path (1 red — reachability); publish pagination on the registry inputs, the "make the designer offer it" tidy-up (1 red — not-published). The second is the one that matters most: it is the future change most likely to be made innocently.

The no-rebuild claim is structural rather than asserted — root vitest aliases @object-ui/* to src, both mutated files are reached through relative imports, so no dist sits in the resolution path.

rowActionDefs — the boundary held

Treated as one of the 27 exactly like its neighbours, with no conclusion imported to or from #5091 in either direction, and said so explicitly. That was the trap I flagged; you walked past it and documented that you had.

Gates

21/21 check runs completed, zero failures. ACCEPT path surface: .changeset/, packages/plugin-view/src/ObjectView.tsx, the new test — no governed surface touched, probe run explicitly. I verified the claim that nothing is removed: git diff --numstat on ObjectView.tsx reads 154 insertions, 0 deletions. Empty-frontmatter changeset, nothing released, and the two constants are not re-exported from the package entry point so the published API is unchanged. The check:published-dist narrowing is properly declared (new file under src/__tests__/, already excluded by that package's tsconfig; CI runs it).

Flipping ready and enqueueing. #5097 closes on merge.


Generated by Claude Code

@os-support-ai
os-support-ai marked this pull request as ready for review August 18, 2026 20:42
@os-support-ai
os-support-ai added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit 12af004Aug 18, 2026
22 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5097-objectview-host-surface-exemption branch August 18, 2026 20:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-support-ai@claude