Skip to content

fix(app-shell): the default-inspector family and its panel hosts gate Save on CEL errors (#4527) - #4558

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4527-phase2-default-family
Aug 13, 2026
Merged

fix(app-shell): the default-inspector family and its panel hosts gate Save on CEL errors (#4527)#4558
yinlianghui merged 1 commit into
mainfrom
claude/issue-4527-phase2-default-family

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4527.

Phase 2, following PR #4547 and the phase-2 ruling. Fixes this time: every site the ruling names is wired. widgets.tsx's ConditionWidget remains explicitly deferred by that ruling and does not block closure.

The structural cause

There are two inspector registries and #4306 extended only one. MetadataInspectorProps carried onBlockingIssuesChange; MetadataDefaultInspectorProps — the contract every "no selection" inspector renders through — had no such member. So the whole default family showed its CEL parse errors and saved anyway, and no host could pass a callback that did not exist. That contract now carries the same optional member.

site aggregation reaches Save via
HookDefaultInspector single count, stamped by hook ObjectHooksPanel's own Save
ActionDefaultInspector per-site map over visible / disabled Data pillar (via ObjectActionsPanel)
ViewVariantInspector home path already aggregated in #4547 metadata editor's default branch
ObjectValidationsPanel per-rule map, keyed by rule name Data pillar

Hosts wired: metadata editor's no-selection branch, Studio design pillar (both its scoped and default rail renders), Data pillar's panel family, and the hooks panel's own button.

Two measured corrections to the ruling's model

Both changed what the work actually is, so they are stated rather than smoothed over.

1. Three of the four panel hosts do not own a Save. Only ObjectHooksPanel writes on its own (client.save('hook', …)). ObjectActionsPanel, ObjectValidationsPanel and ObjectSettingsPanel write through the object draft — their own headers say so — and all three are rendered inside DataPillar. So they report upward and the pillar holds a second count beside the field-inspector count #4306 gave it, stamped with the panel tab: exactly one panel is mounted at a time, so a tab the author has left can never retract its verdict.

2. ObjectSettingsPanel is inert today, and is wired anyway — declared, not hidden. It renders ObjectDefaultInspector, which mounts no CEL editor at all, so nothing downstream can ever call the callback. It is wired to keep the host family uniform (the next CEL editor added there is gated by construction), and only its forwarding is covered — there is no verdict to produce. Flagged so nobody reads it as tested behaviour.

A third, smaller one: the metadata editor reaches a default inspector only for types that also have a canvas preview, because that panel lives inside the PreviewComponent branch; a type without one falls through to a plain SchemaForm. view is the type where that branch actually hosts CEL, which is why the host test uses it — and why hook / action default inspectors are not reachable from that host at all, but from the Studio panels, where they are gated in their own suites.

Red-first

Predicted the split in writing before running. Predicted signature, inherited from #4306/#4547: the reporter is never called, so the count a host would hold is undefined and every asserted count fails, the 0 cases included. Measured against unfixed code, verbatim:

AssertionError: expected undefined to be 1 // Object.is equality
AssertionError: expected undefined to be +0 // Object.is equality

One prediction was wrong, and informatively so. I predicted the view home path would go red at runtime. It did not: ViewDefaultInspector spreads {...props} into ViewVariantInspector, so once #4547 taught the variant inspector to aggregate, the callback already reached it. The home path's defect was purely the missing type — no host could pass the prop without a compile error, and none did. Those two render cases are therefore reported as pins, not red signals, and the actually-red assertion for that path is a type-level one that fails to compile until the contract carries the channel.

Reverse verification removed the fix with git diff + git checkout -- (never git stash — objectui#3430) and re-ran all seven suites: 6 files failed, 20 failed / 2 passed — the 2 passes being exactly the two spread-carried home-path cases above. Restored with git apply and sha256 verified on all 9 files.

Verification

Changeset

@object-ui/app-shell: patch, by .d.ts measurement both ways with dist/ and tsconfig.tsbuildinfo cleared between builds. dist/index.d.ts is byte-identical before and after. The ruling forecast minor on the assumption that MetadataDefaultInspectorProps is entry-reachable like its sibling; measurement says it is not — the package entry re-exports MetadataInspectorProps from inspector-registry and nothing at all from default-inspector-registry, and the package declares no subpath exports. That asymmetry is also why #4536 graded minor and this does not.

Surface

inspector-registry.ts is not touched: the type extended here lives in the sibling default-inspector-registry.ts, so the conditional authorization for that file was not needed or used. ObjectFieldInspector.tsx, the permission editors, plugin packages and content/docs/releases/ all untouched. ResourceEditPage.tsx and StudioDesignSurface.tsx were edited under the lifted phase-1 restriction; no client.get( call site was modified.


Generated by Claude Code

… Save on CEL errors (#4527)

There are TWO inspector registries and #4306 extended only one.
MetadataInspectorProps carried onBlockingIssuesChange; MetadataDefaultInspectorProps
did not, so every "no selection" inspector rendered its CEL parse error while Save
stayed writable -- and no host could pass a callback that did not exist.

MetadataDefaultInspectorProps now carries the same optional member.
HookDefaultInspector reports its guard; ActionDefaultInspector aggregates its two
predicate editors through a per-site map (two editors lint independently, so a
shared counter would hand back a writable Save the moment one of two broken
predicates was fixed); the view home panel already aggregated and now has a
contract to report through.

Hosts hold and expire the counts. The metadata editor gates its no-selection
branch as well as its scoped one, each stamped so neither reads the other's
verdict. Studio's design pillar gates its rail -- an unfinished edge of #4306,
since the same malformed-CEL publish was reachable there with the gate inert. The
Data pillar gains a second count for its panel family (validations / actions /
settings write through the object draft and own no Save), stamped with the panel
tab because only one panel is mounted at a time. The hooks panel writes on its own
and gates its own per-hook Save.

Every count is DERIVED from what it describes and pruned by what still exists: a
deleted rule or action drops out immediately, so a fault cannot wedge Save shut
with no editor left to fix it in. A faulty rule merely navigated away from stays
counted -- it is still in the document and saving would still publish it.

Also wired ObjectValidationsPanel, a sixth ConditionBuilder consumer the original
report did not list. widgets.tsx's ConditionWidget stays deferred by ruling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 13, 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 13, 2026 8:52am

Request Review

@github-actions github-actions Bot added the tests label Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-CneN-1Np.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) 25.13KB 5.40KB
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) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
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) 5.02KB 0.88KB
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) 3.37KB 1.34KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 158.47KB 43.13KB
fields (index.js) 230.18KB 57.13KB
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.84KB 1.45KB
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) 46.86KB 12.91KB
plugin-charts (index.js) 62.10KB 17.67KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 120.95KB 31.53KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.88KB 59.99KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.27KB 40.01KB
plugin-grid (index.js) 189.36KB 50.33KB
plugin-kanban (index.js) 52.74KB 14.53KB
plugin-list (index.js) 111.13KB 27.12KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.16KB 10.96KB
plugin-timeline (index.js) 26.68KB 7.66KB
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

PM step-7 复核 — ACCEPT (session_017Qqyix2QcnpUC9XeYVDzx3) — and with this, the CEL-gate family (#4306#4547 → here) is closed except the explicitly-deferred ConditionWidget.

  • The contract landed in the RIGHT file: default-inspector-registry.ts, not the off-limits sibling — the conditional authorization existed and was correctly left unused. All three sub-premise corrections are measured and declared rather than smoothed: the panel family reports into DataPillar's second stamped hold because only ObjectHooksPanel owns a Save; ObjectSettingsPanel's channel is inert (no CEL editor to report) and is wired-and-declared per the ruling; and the metadata editor reaches default inspectors only inside the PreviewComponent branch.
  • The aggregation semantics show real judgment: ActionDefaultInspector's per-SITE map (a shared counter hands back Save when one of two broken predicates is fixed), ObjectValidationsPanel's per-rule map where a DESELECTED rule's verdict deliberately SURVIVES (it is still in the document; saving would still publish it) while a DELETED rule drops, DataPillar's viewMode stamping so a tab the author left can never retract, and ResourceEditPage's ':default' sentinel keeping scoped and default verdicts from reading each other.
  • The honest prediction miss is the report's centerpiece: the view home path was already healed at runtime by phase 1's {...props} spread — the defect was purely the missing TYPE, so the two render cases are labelled PINS and the genuine red is a type-level assertion that fails to compile until the contract carries the member. Fabricating the predicted runtime red would have been template-shaped fiction; this is the correct refusal.
  • Grading beat my own forecast by measurement: patch, because default-inspector-registry is NOT entry-reachable — the exact asymmetry that made fix(app-shell): inspectors can block Save — the field inspector gates on CEL errors (#4306) #4536 minor. dts byte-identical both ways confirms it.
  • The self-inflicted NUL (perl -0pi with $/ in the replacement) was caught by the mandated self-scan, repaired by byte surgery, and verified — recorded here so the pattern joins the banned list: perl -i with $/ in a replacement is a NUL generator.
  • 184-file regression green, NET ZERO warnings, all phase-1/Field inspector: Save is not gated on CEL errors — a parse-fault formula saves and publishes as the live field definition #4306 pins untouched, CI 20/20 on per-job conclusions. Merging this closes CEL blocking errors are dropped on the floor in ConditionBuilder and ConditionalFormattingEditor too — same ungated-Save family as #4306 #4527; the deferred ConditionWidget question is recorded on the card with its measurement precondition.

Auto-merge armed (squash) — landing verified per the merge-queue discipline.


Generated by Claude Code


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CEL blocking errors are dropped on the floor in ConditionBuilder and ConditionalFormattingEditor too — same ungated-Save family as #4306

2 participants