fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority - #7109

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict
Sep 1, 2026
Merged

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority#7109
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6868

Both deliverables of the maintainer's ruling (维护者, 2026-08-31, 决裁批 #13) now land, and both are presentation-layer. Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM

Round 2 note. This PR first landed (b) as per-field attribution rendered in the record-level save bar, because the transport needed one file outside the dispatched fence. The PM measured the sizing, extended the fence by exactly that one file, and chose option A over closing on the partial — on the grounds that 就地 (in place) is in the ruling's own text, and narrowing a ruling to fit a fence is the one thing a seat executing it must not do. The hint now renders beside the input.

(a) The decision is written down

The ruling is recorded in the headers of both modules where the question gets asked: InlineEditSaveBar.tsx (which handles the refusal) and InlineFieldInput.tsx (which runs no rules and, until this PR, took no error prop — for two years that read as an oversight).

The ruling's own framing is why it is written twice: 此前这是一个「缺席」,现在是一个「决定」. An absence and a decision are indistinguishable in code until someone writes the second one down. Each header states that the server is the validation authority here, quotes the ruling, points at #6868, and says plainly what a future editor must not do: no shared evaluator, no second validation implementation, no buildValidationRules call from this surface.

(b) The server's reason now renders beside the input it refused

Before: cleanError handed the user the backend's own string, e.g. VALIDATION_FAILED: Validation failed for crm_opportunity, after they had edited three fields.

After: the reason appears under the field the server named, with role="alert", and that input is marked aria-invalid. The save bar keeps a labelled per-field summary as well — a rejected field can be collapsed or scrolled out of view, which is the same reason form.tsx keeps its banner on the form surface.

The transport.InlineEditContextValue gains fieldErrors (field machine name to the server's reason, nullable) and setFieldErrors, the exact companions of the error / setError pair it already carried and already documented as "driven by the save bar". This was the missing piece and nothing else was: the save bar and the field rows are siblings under InlineEditProvider in both persistence modes (app-shell/RecordDetailView.tsx:2340/:2509; RecordDetailDrawer.tsx:346/:398), so there was no channel between the component that receives a refusal and the components that render the fields it is about. Cleared by enter() and teardown exactly like error, so an attribution cannot outlive its session. No host wiring was neededHeaderHighlight already reads the session and DetailSection now does too — so app-shell and RecordDetailDrawer are untouched, as the sizing predicted.

Attribution never guesses, two sources in strict order:

  1. The envelope — via @object-ui/react's extractFieldErrors, the single in-repo normaliser, the same one form.tsx uses. It reads all three shapes a VALIDATION_FAILED arrives in (validationErrors from @object-ui/data-objectstack's re-wrap, details.fields from the raw @objectstack/client error, or a bare fields) and drops any entry with no usable field. That drop is load-bearing: a wrong mark on an innocent input is worse than the undirected string it replaces. It is the only source on the DataSource path, whose write is one atomic multi-key update.
  2. The call shape, callback mode only — the drawer loops onFieldSave(field, value) one key per call, so a rejection belongs to that key by construction. The atomic path passes undefined, so a multi-key write can never be attributed this way. The envelope still wins when both are available (pinned).

Anything not field-scoped — a network failure, a permission denial, a bare Error — keeps the cleaned single-line message. Nothing here evaluates a rule.

The error prop on InlineFieldInput is no longer dead

It was deliberately not shipped in round 1: with no value to put in it, it would have been a declared-but-dead prop. Option A creates the value, so it is threaded now and forwarded to the widgets' published #3222 slot.

Measured, and it changes what that slot is for: the #3222error slot marks aria-invalid — it does not render text, on this surface or on the form one, where form.tsx (not the widget) draws the visible message. So threading it buys the a11y marking, and the visible hint is this package's markup either way. That is why the delegated tail's drop of error (the subject of #7008, which reproduces here independently) was never a blocker for this work, and no packages/fields edit was made.

Clause-② check, run before the interface was widened

The PM authorised the addition on the reading that InlineEditContextValue is a runtime React context shape, not an authoring contract, and asked me to stop and report if measurement said otherwise. It does not:

  • Zero references to InlineEditContextValue anywhere outside its own module and one prose comment — control FieldSchema hits 4 files under scripts/ in the same sweep, so the zero is a reading and not a broken grep.
  • Zero mentions in any authoring-surface gate (check-spec-symbols, check-element-data-source-declaration, check-designer-field-key-parity).
  • packages/react/README.md mentions InlineEditzero times (control: SchemaRenderer, 13 hits), so no documented contract enumerates it.
  • The slot is populated from React state by the save bar and read by two renderers. No document is parsed into it, and nothing about what metadata the platform accepts or refuses changes.

⇒ Not clause ②. It is an additive public API member, which is why @object-ui/react is graded minor.

Evidence

  • PinInlineEditSaveBar.serverVerdictFieldHint-6868.test.tsx, 11 tests, all named under --reporter=verbose. Four are new this round and drive the real tree (DetailSection / HeaderHighlight and the save bar as siblings under one provider, as both hosts mount them): the reason is drawn under the refused field and only that field; the refused input is aria-invalid while the accepted one is not (exactly 1 marked element); the hint clears on cancel + re-enter; and it reaches the highlights strip too, proving one session serves both surfaces. The first asserts its own control — both editors really rendered — so the zero on the unrefused field is a real absence, not an empty harness.
  • Counter-probe (ablation) — mapping removed (attributeInlineRefusal forced to null). Mutation proven on disk first (marker occurrences 1/1, blob 364735c27180c2fb), restore proven after (git diff HEAD empty, blob back to 364735c2). Result 9 of 11 red. The 2 that stay green are, by name, DROPS an entry with no usable field and leaves a NON field-scoped failure on the cleaned string — the two negative cases, which assert the fallback the ablation forces everywhere. Green is the correct direction for those, and it is the same pair as round 1.
  • Cross-package reverse verification — a key the new type must reject was pasted into the consumer and the build went red with Type 'Record<string, string> | null' is not assignable to type 'number'. The message names the new member's declared type rather than "property does not exist", which is what makes it discriminating: plugin-detail is compiling against the rebuilt@object-ui/react declarations, not a cache. Restored and re-proven byte-identical.
  • Suites at 2012c0a87: pnpm exec vitest run packages/plugin-detail/src packages/react/srcTest Files 187 passed (187) / Tests 1938 passed (1938). Typecheck: both packages, each echoing tsc --noEmit && tsc -p tsconfig.test.json, exit 0. Root-relative vitest throughout, so the pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378 silent re-root could not occur.
  • Gates at 2012c0a87, all exit 0: check:control-bytes, check:phantom-deps, check:self-import, check:esm-specifiers, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys, check:i18n-drift, check:element-data-source-declaration, and all four changeset gates — check-changeset-presence reporting "6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)".
  • NOT MEASURED, reported as such and not as green: check:readme-exports (exit 1) and check:sdui-registration-pins (exit 2) both refuse to render a verdict without a full build — "the population COLLAPSED — this run proves nothing … packagesRead: found 12, floor is 25" and "No console build to weigh … a run with nothing to read has measured nothing". Both are repo-wide scans needing all 40 packages plus apps/console built; CI owns them. Neither can be affected by this diff — nothing is removed from any barrel and no registration array is touched.
  • eslint (plain form, never --no-inline-config): exit 0, 0 errors. Every changed source file's warning profile is identical to its origin/main baseline, measured file by file — InlineEditContext 4+1, InlineEditSaveBar 7, InlineFieldInput 34+5, DetailSection 10+1+1, HeaderHighlight 8. Zero new.

Changeset: @object-ui/plugin-detail: patch (nothing enters or leaves its callable surface; an existing surface renders an existing refusal better) and @object-ui/react: minor (one additive public API member, spelled out in the changeset body). No major — refused by the gate, and objectui's major is a cross-repo pin, not a severity dial.

⛔ Draft. Not ready, not enqueued, no auto-merge — the PM arms landing.

… inline-edit hints
The inline-edit save bar surfaced the backend's raw string when a write was
refused, leaving the user to guess which edited field was wrong. The refusal
has always been field-scoped; this reads it through @object-ui/react's
extractFieldErrors -- the same single normaliser the form surface uses -- and
renders one reason per rejected field, named by that field's label.
Attribution never guesses: an envelope entry with no usable `field` is dropped,
and callback (drawer) mode attributes from the call shape, where onFieldSave
carries exactly one key per call. Non field-scoped failures keep the cleaned
single-line message.
Also records the maintainer's ruling on objectui#6868 in both modules' headers:
the server is the validation authority on this surface. That was an absence and
is now a decision. No client-side evaluator was added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Its pin drives it through the rendered save bar, which is the path that
actually reaches the mapping, so the export bought nothing and cost a
react-refresh/only-export-components warning this file did not have before
(measured: baseline 7 no-explicit-any + 0 react-refresh; now identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.3 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DnRUybMw.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.09KB63.41KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

…ut it refused
Option A, authorised by the PM after the measurement showed the honest
completion of the objectui#6868 ruling needs one adjacent file: the save bar
and the field rows are siblings under InlineEditProvider in both persistence
modes, so there was no channel between the component that receives a refusal
and the components that render the fields it is about.
- @object-ui/react: InlineEditContextValue gains `fieldErrors` (field machine
name to the server's reason, nullable) and `setFieldErrors`, the exact
companions of the `error`/`setError` pair it already carried. Cleared by
enter() and teardown like `error`, so an attribution cannot outlive its
session. Additive; nothing removed, nothing reshaped.
- InlineEditSaveBar publishes the attributed refusal onto the session instead
of holding it locally, and keeps the record-level summary for a field that is
collapsed or scrolled out of view.
- DetailSection and HeaderHighlight read it and draw the reason under the
input, with role="alert".
- InlineFieldInput takes `error` and forwards it to the widgets' published
#3222 slot (aria-invalid), and marks the terminal raw input directly. The
prop is no longer dead now that a value exists to put in it.
Presentation only. Nothing here evaluates a rule: the server remains the sole
validation authority on this surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.5 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-Caj1su0T.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warren
os-warren marked this pull request as ready for review September 1, 2026 01:42
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 03380aaSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-6868-inline-edit-server-verdict branch September 1, 2026 01:55
os-warren pushed a commit that referenced this pull request Sep 1, 2026
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR #7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ectstack-ai#7128)
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR objectstack-ai#7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority - #7109

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict
Sep 1, 2026
Merged

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority#7109
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6868

Both deliverables of the maintainer's ruling (维护者, 2026-08-31, 决裁批 #13) now land, and both are presentation-layer. Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM

Round 2 note. This PR first landed (b) as per-field attribution rendered in the record-level save bar, because the transport needed one file outside the dispatched fence. The PM measured the sizing, extended the fence by exactly that one file, and chose option A over closing on the partial — on the grounds that 就地 (in place) is in the ruling's own text, and narrowing a ruling to fit a fence is the one thing a seat executing it must not do. The hint now renders beside the input.

(a) The decision is written down

The ruling is recorded in the headers of both modules where the question gets asked: InlineEditSaveBar.tsx (which handles the refusal) and InlineFieldInput.tsx (which runs no rules and, until this PR, took no error prop — for two years that read as an oversight).

The ruling's own framing is why it is written twice: 此前这是一个「缺席」,现在是一个「决定」. An absence and a decision are indistinguishable in code until someone writes the second one down. Each header states that the server is the validation authority here, quotes the ruling, points at #6868, and says plainly what a future editor must not do: no shared evaluator, no second validation implementation, no buildValidationRules call from this surface.

(b) The server's reason now renders beside the input it refused

Before: cleanError handed the user the backend's own string, e.g. VALIDATION_FAILED: Validation failed for crm_opportunity, after they had edited three fields.

After: the reason appears under the field the server named, with role="alert", and that input is marked aria-invalid. The save bar keeps a labelled per-field summary as well — a rejected field can be collapsed or scrolled out of view, which is the same reason form.tsx keeps its banner on the form surface.

The transport.InlineEditContextValue gains fieldErrors (field machine name to the server's reason, nullable) and setFieldErrors, the exact companions of the error / setError pair it already carried and already documented as "driven by the save bar". This was the missing piece and nothing else was: the save bar and the field rows are siblings under InlineEditProvider in both persistence modes (app-shell/RecordDetailView.tsx:2340/:2509; RecordDetailDrawer.tsx:346/:398), so there was no channel between the component that receives a refusal and the components that render the fields it is about. Cleared by enter() and teardown exactly like error, so an attribution cannot outlive its session. No host wiring was neededHeaderHighlight already reads the session and DetailSection now does too — so app-shell and RecordDetailDrawer are untouched, as the sizing predicted.

Attribution never guesses, two sources in strict order:

  1. The envelope — via @object-ui/react's extractFieldErrors, the single in-repo normaliser, the same one form.tsx uses. It reads all three shapes a VALIDATION_FAILED arrives in (validationErrors from @object-ui/data-objectstack's re-wrap, details.fields from the raw @objectstack/client error, or a bare fields) and drops any entry with no usable field. That drop is load-bearing: a wrong mark on an innocent input is worse than the undirected string it replaces. It is the only source on the DataSource path, whose write is one atomic multi-key update.
  2. The call shape, callback mode only — the drawer loops onFieldSave(field, value) one key per call, so a rejection belongs to that key by construction. The atomic path passes undefined, so a multi-key write can never be attributed this way. The envelope still wins when both are available (pinned).

Anything not field-scoped — a network failure, a permission denial, a bare Error — keeps the cleaned single-line message. Nothing here evaluates a rule.

The error prop on InlineFieldInput is no longer dead

It was deliberately not shipped in round 1: with no value to put in it, it would have been a declared-but-dead prop. Option A creates the value, so it is threaded now and forwarded to the widgets' published #3222 slot.

Measured, and it changes what that slot is for: the #3222error slot marks aria-invalid — it does not render text, on this surface or on the form one, where form.tsx (not the widget) draws the visible message. So threading it buys the a11y marking, and the visible hint is this package's markup either way. That is why the delegated tail's drop of error (the subject of #7008, which reproduces here independently) was never a blocker for this work, and no packages/fields edit was made.

Clause-② check, run before the interface was widened

The PM authorised the addition on the reading that InlineEditContextValue is a runtime React context shape, not an authoring contract, and asked me to stop and report if measurement said otherwise. It does not:

  • Zero references to InlineEditContextValue anywhere outside its own module and one prose comment — control FieldSchema hits 4 files under scripts/ in the same sweep, so the zero is a reading and not a broken grep.
  • Zero mentions in any authoring-surface gate (check-spec-symbols, check-element-data-source-declaration, check-designer-field-key-parity).
  • packages/react/README.md mentions InlineEditzero times (control: SchemaRenderer, 13 hits), so no documented contract enumerates it.
  • The slot is populated from React state by the save bar and read by two renderers. No document is parsed into it, and nothing about what metadata the platform accepts or refuses changes.

⇒ Not clause ②. It is an additive public API member, which is why @object-ui/react is graded minor.

Evidence

  • PinInlineEditSaveBar.serverVerdictFieldHint-6868.test.tsx, 11 tests, all named under --reporter=verbose. Four are new this round and drive the real tree (DetailSection / HeaderHighlight and the save bar as siblings under one provider, as both hosts mount them): the reason is drawn under the refused field and only that field; the refused input is aria-invalid while the accepted one is not (exactly 1 marked element); the hint clears on cancel + re-enter; and it reaches the highlights strip too, proving one session serves both surfaces. The first asserts its own control — both editors really rendered — so the zero on the unrefused field is a real absence, not an empty harness.
  • Counter-probe (ablation) — mapping removed (attributeInlineRefusal forced to null). Mutation proven on disk first (marker occurrences 1/1, blob 364735c27180c2fb), restore proven after (git diff HEAD empty, blob back to 364735c2). Result 9 of 11 red. The 2 that stay green are, by name, DROPS an entry with no usable field and leaves a NON field-scoped failure on the cleaned string — the two negative cases, which assert the fallback the ablation forces everywhere. Green is the correct direction for those, and it is the same pair as round 1.
  • Cross-package reverse verification — a key the new type must reject was pasted into the consumer and the build went red with Type 'Record<string, string> | null' is not assignable to type 'number'. The message names the new member's declared type rather than "property does not exist", which is what makes it discriminating: plugin-detail is compiling against the rebuilt@object-ui/react declarations, not a cache. Restored and re-proven byte-identical.
  • Suites at 2012c0a87: pnpm exec vitest run packages/plugin-detail/src packages/react/srcTest Files 187 passed (187) / Tests 1938 passed (1938). Typecheck: both packages, each echoing tsc --noEmit && tsc -p tsconfig.test.json, exit 0. Root-relative vitest throughout, so the pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378 silent re-root could not occur.
  • Gates at 2012c0a87, all exit 0: check:control-bytes, check:phantom-deps, check:self-import, check:esm-specifiers, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys, check:i18n-drift, check:element-data-source-declaration, and all four changeset gates — check-changeset-presence reporting "6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)".
  • NOT MEASURED, reported as such and not as green: check:readme-exports (exit 1) and check:sdui-registration-pins (exit 2) both refuse to render a verdict without a full build — "the population COLLAPSED — this run proves nothing … packagesRead: found 12, floor is 25" and "No console build to weigh … a run with nothing to read has measured nothing". Both are repo-wide scans needing all 40 packages plus apps/console built; CI owns them. Neither can be affected by this diff — nothing is removed from any barrel and no registration array is touched.
  • eslint (plain form, never --no-inline-config): exit 0, 0 errors. Every changed source file's warning profile is identical to its origin/main baseline, measured file by file — InlineEditContext 4+1, InlineEditSaveBar 7, InlineFieldInput 34+5, DetailSection 10+1+1, HeaderHighlight 8. Zero new.

Changeset: @object-ui/plugin-detail: patch (nothing enters or leaves its callable surface; an existing surface renders an existing refusal better) and @object-ui/react: minor (one additive public API member, spelled out in the changeset body). No major — refused by the gate, and objectui's major is a cross-repo pin, not a severity dial.

⛔ Draft. Not ready, not enqueued, no auto-merge — the PM arms landing.

… inline-edit hints
The inline-edit save bar surfaced the backend's raw string when a write was
refused, leaving the user to guess which edited field was wrong. The refusal
has always been field-scoped; this reads it through @object-ui/react's
extractFieldErrors -- the same single normaliser the form surface uses -- and
renders one reason per rejected field, named by that field's label.
Attribution never guesses: an envelope entry with no usable `field` is dropped,
and callback (drawer) mode attributes from the call shape, where onFieldSave
carries exactly one key per call. Non field-scoped failures keep the cleaned
single-line message.
Also records the maintainer's ruling on objectui#6868 in both modules' headers:
the server is the validation authority on this surface. That was an absence and
is now a decision. No client-side evaluator was added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Its pin drives it through the rendered save bar, which is the path that
actually reaches the mapping, so the export bought nothing and cost a
react-refresh/only-export-components warning this file did not have before
(measured: baseline 7 no-explicit-any + 0 react-refresh; now identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.3 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DnRUybMw.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.09KB63.41KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

…ut it refused
Option A, authorised by the PM after the measurement showed the honest
completion of the objectui#6868 ruling needs one adjacent file: the save bar
and the field rows are siblings under InlineEditProvider in both persistence
modes, so there was no channel between the component that receives a refusal
and the components that render the fields it is about.
- @object-ui/react: InlineEditContextValue gains `fieldErrors` (field machine
name to the server's reason, nullable) and `setFieldErrors`, the exact
companions of the `error`/`setError` pair it already carried. Cleared by
enter() and teardown like `error`, so an attribution cannot outlive its
session. Additive; nothing removed, nothing reshaped.
- InlineEditSaveBar publishes the attributed refusal onto the session instead
of holding it locally, and keeps the record-level summary for a field that is
collapsed or scrolled out of view.
- DetailSection and HeaderHighlight read it and draw the reason under the
input, with role="alert".
- InlineFieldInput takes `error` and forwards it to the widgets' published
#3222 slot (aria-invalid), and marks the terminal raw input directly. The
prop is no longer dead now that a value exists to put in it.
Presentation only. Nothing here evaluates a rule: the server remains the sole
validation authority on this surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.5 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-Caj1su0T.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warren
os-warren marked this pull request as ready for review September 1, 2026 01:42
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 03380aaSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-6868-inline-edit-server-verdict branch September 1, 2026 01:55
os-warren pushed a commit that referenced this pull request Sep 1, 2026
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR #7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ectstack-ai#7128)
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR objectstack-ai#7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority - #7109

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict
Sep 1, 2026
Merged

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority#7109
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6868

Both deliverables of the maintainer's ruling (维护者, 2026-08-31, 决裁批 #13) now land, and both are presentation-layer. Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM

Round 2 note. This PR first landed (b) as per-field attribution rendered in the record-level save bar, because the transport needed one file outside the dispatched fence. The PM measured the sizing, extended the fence by exactly that one file, and chose option A over closing on the partial — on the grounds that 就地 (in place) is in the ruling's own text, and narrowing a ruling to fit a fence is the one thing a seat executing it must not do. The hint now renders beside the input.

(a) The decision is written down

The ruling is recorded in the headers of both modules where the question gets asked: InlineEditSaveBar.tsx (which handles the refusal) and InlineFieldInput.tsx (which runs no rules and, until this PR, took no error prop — for two years that read as an oversight).

The ruling's own framing is why it is written twice: 此前这是一个「缺席」,现在是一个「决定」. An absence and a decision are indistinguishable in code until someone writes the second one down. Each header states that the server is the validation authority here, quotes the ruling, points at #6868, and says plainly what a future editor must not do: no shared evaluator, no second validation implementation, no buildValidationRules call from this surface.

(b) The server's reason now renders beside the input it refused

Before: cleanError handed the user the backend's own string, e.g. VALIDATION_FAILED: Validation failed for crm_opportunity, after they had edited three fields.

After: the reason appears under the field the server named, with role="alert", and that input is marked aria-invalid. The save bar keeps a labelled per-field summary as well — a rejected field can be collapsed or scrolled out of view, which is the same reason form.tsx keeps its banner on the form surface.

The transport.InlineEditContextValue gains fieldErrors (field machine name to the server's reason, nullable) and setFieldErrors, the exact companions of the error / setError pair it already carried and already documented as "driven by the save bar". This was the missing piece and nothing else was: the save bar and the field rows are siblings under InlineEditProvider in both persistence modes (app-shell/RecordDetailView.tsx:2340/:2509; RecordDetailDrawer.tsx:346/:398), so there was no channel between the component that receives a refusal and the components that render the fields it is about. Cleared by enter() and teardown exactly like error, so an attribution cannot outlive its session. No host wiring was neededHeaderHighlight already reads the session and DetailSection now does too — so app-shell and RecordDetailDrawer are untouched, as the sizing predicted.

Attribution never guesses, two sources in strict order:

  1. The envelope — via @object-ui/react's extractFieldErrors, the single in-repo normaliser, the same one form.tsx uses. It reads all three shapes a VALIDATION_FAILED arrives in (validationErrors from @object-ui/data-objectstack's re-wrap, details.fields from the raw @objectstack/client error, or a bare fields) and drops any entry with no usable field. That drop is load-bearing: a wrong mark on an innocent input is worse than the undirected string it replaces. It is the only source on the DataSource path, whose write is one atomic multi-key update.
  2. The call shape, callback mode only — the drawer loops onFieldSave(field, value) one key per call, so a rejection belongs to that key by construction. The atomic path passes undefined, so a multi-key write can never be attributed this way. The envelope still wins when both are available (pinned).

Anything not field-scoped — a network failure, a permission denial, a bare Error — keeps the cleaned single-line message. Nothing here evaluates a rule.

The error prop on InlineFieldInput is no longer dead

It was deliberately not shipped in round 1: with no value to put in it, it would have been a declared-but-dead prop. Option A creates the value, so it is threaded now and forwarded to the widgets' published #3222 slot.

Measured, and it changes what that slot is for: the #3222error slot marks aria-invalid — it does not render text, on this surface or on the form one, where form.tsx (not the widget) draws the visible message. So threading it buys the a11y marking, and the visible hint is this package's markup either way. That is why the delegated tail's drop of error (the subject of #7008, which reproduces here independently) was never a blocker for this work, and no packages/fields edit was made.

Clause-② check, run before the interface was widened

The PM authorised the addition on the reading that InlineEditContextValue is a runtime React context shape, not an authoring contract, and asked me to stop and report if measurement said otherwise. It does not:

  • Zero references to InlineEditContextValue anywhere outside its own module and one prose comment — control FieldSchema hits 4 files under scripts/ in the same sweep, so the zero is a reading and not a broken grep.
  • Zero mentions in any authoring-surface gate (check-spec-symbols, check-element-data-source-declaration, check-designer-field-key-parity).
  • packages/react/README.md mentions InlineEditzero times (control: SchemaRenderer, 13 hits), so no documented contract enumerates it.
  • The slot is populated from React state by the save bar and read by two renderers. No document is parsed into it, and nothing about what metadata the platform accepts or refuses changes.

⇒ Not clause ②. It is an additive public API member, which is why @object-ui/react is graded minor.

Evidence

  • PinInlineEditSaveBar.serverVerdictFieldHint-6868.test.tsx, 11 tests, all named under --reporter=verbose. Four are new this round and drive the real tree (DetailSection / HeaderHighlight and the save bar as siblings under one provider, as both hosts mount them): the reason is drawn under the refused field and only that field; the refused input is aria-invalid while the accepted one is not (exactly 1 marked element); the hint clears on cancel + re-enter; and it reaches the highlights strip too, proving one session serves both surfaces. The first asserts its own control — both editors really rendered — so the zero on the unrefused field is a real absence, not an empty harness.
  • Counter-probe (ablation) — mapping removed (attributeInlineRefusal forced to null). Mutation proven on disk first (marker occurrences 1/1, blob 364735c27180c2fb), restore proven after (git diff HEAD empty, blob back to 364735c2). Result 9 of 11 red. The 2 that stay green are, by name, DROPS an entry with no usable field and leaves a NON field-scoped failure on the cleaned string — the two negative cases, which assert the fallback the ablation forces everywhere. Green is the correct direction for those, and it is the same pair as round 1.
  • Cross-package reverse verification — a key the new type must reject was pasted into the consumer and the build went red with Type 'Record<string, string> | null' is not assignable to type 'number'. The message names the new member's declared type rather than "property does not exist", which is what makes it discriminating: plugin-detail is compiling against the rebuilt@object-ui/react declarations, not a cache. Restored and re-proven byte-identical.
  • Suites at 2012c0a87: pnpm exec vitest run packages/plugin-detail/src packages/react/srcTest Files 187 passed (187) / Tests 1938 passed (1938). Typecheck: both packages, each echoing tsc --noEmit && tsc -p tsconfig.test.json, exit 0. Root-relative vitest throughout, so the pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378 silent re-root could not occur.
  • Gates at 2012c0a87, all exit 0: check:control-bytes, check:phantom-deps, check:self-import, check:esm-specifiers, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys, check:i18n-drift, check:element-data-source-declaration, and all four changeset gates — check-changeset-presence reporting "6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)".
  • NOT MEASURED, reported as such and not as green: check:readme-exports (exit 1) and check:sdui-registration-pins (exit 2) both refuse to render a verdict without a full build — "the population COLLAPSED — this run proves nothing … packagesRead: found 12, floor is 25" and "No console build to weigh … a run with nothing to read has measured nothing". Both are repo-wide scans needing all 40 packages plus apps/console built; CI owns them. Neither can be affected by this diff — nothing is removed from any barrel and no registration array is touched.
  • eslint (plain form, never --no-inline-config): exit 0, 0 errors. Every changed source file's warning profile is identical to its origin/main baseline, measured file by file — InlineEditContext 4+1, InlineEditSaveBar 7, InlineFieldInput 34+5, DetailSection 10+1+1, HeaderHighlight 8. Zero new.

Changeset: @object-ui/plugin-detail: patch (nothing enters or leaves its callable surface; an existing surface renders an existing refusal better) and @object-ui/react: minor (one additive public API member, spelled out in the changeset body). No major — refused by the gate, and objectui's major is a cross-repo pin, not a severity dial.

⛔ Draft. Not ready, not enqueued, no auto-merge — the PM arms landing.

… inline-edit hints
The inline-edit save bar surfaced the backend's raw string when a write was
refused, leaving the user to guess which edited field was wrong. The refusal
has always been field-scoped; this reads it through @object-ui/react's
extractFieldErrors -- the same single normaliser the form surface uses -- and
renders one reason per rejected field, named by that field's label.
Attribution never guesses: an envelope entry with no usable `field` is dropped,
and callback (drawer) mode attributes from the call shape, where onFieldSave
carries exactly one key per call. Non field-scoped failures keep the cleaned
single-line message.
Also records the maintainer's ruling on objectui#6868 in both modules' headers:
the server is the validation authority on this surface. That was an absence and
is now a decision. No client-side evaluator was added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Its pin drives it through the rendered save bar, which is the path that
actually reaches the mapping, so the export bought nothing and cost a
react-refresh/only-export-components warning this file did not have before
(measured: baseline 7 no-explicit-any + 0 react-refresh; now identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.3 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DnRUybMw.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.09KB63.41KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

…ut it refused
Option A, authorised by the PM after the measurement showed the honest
completion of the objectui#6868 ruling needs one adjacent file: the save bar
and the field rows are siblings under InlineEditProvider in both persistence
modes, so there was no channel between the component that receives a refusal
and the components that render the fields it is about.
- @object-ui/react: InlineEditContextValue gains `fieldErrors` (field machine
name to the server's reason, nullable) and `setFieldErrors`, the exact
companions of the `error`/`setError` pair it already carried. Cleared by
enter() and teardown like `error`, so an attribution cannot outlive its
session. Additive; nothing removed, nothing reshaped.
- InlineEditSaveBar publishes the attributed refusal onto the session instead
of holding it locally, and keeps the record-level summary for a field that is
collapsed or scrolled out of view.
- DetailSection and HeaderHighlight read it and draw the reason under the
input, with role="alert".
- InlineFieldInput takes `error` and forwards it to the widgets' published
#3222 slot (aria-invalid), and marks the terminal raw input directly. The
prop is no longer dead now that a value exists to put in it.
Presentation only. Nothing here evaluates a rule: the server remains the sole
validation authority on this surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.5 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-Caj1su0T.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warren
os-warren marked this pull request as ready for review September 1, 2026 01:42
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 03380aaSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-6868-inline-edit-server-verdict branch September 1, 2026 01:55
os-warren pushed a commit that referenced this pull request Sep 1, 2026
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR #7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ectstack-ai#7128)
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR objectstack-ai#7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority - #7109

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict
Sep 1, 2026
Merged

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority#7109
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6868

Both deliverables of the maintainer's ruling (维护者, 2026-08-31, 决裁批 #13) now land, and both are presentation-layer. Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM

Round 2 note. This PR first landed (b) as per-field attribution rendered in the record-level save bar, because the transport needed one file outside the dispatched fence. The PM measured the sizing, extended the fence by exactly that one file, and chose option A over closing on the partial — on the grounds that 就地 (in place) is in the ruling's own text, and narrowing a ruling to fit a fence is the one thing a seat executing it must not do. The hint now renders beside the input.

(a) The decision is written down

The ruling is recorded in the headers of both modules where the question gets asked: InlineEditSaveBar.tsx (which handles the refusal) and InlineFieldInput.tsx (which runs no rules and, until this PR, took no error prop — for two years that read as an oversight).

The ruling's own framing is why it is written twice: 此前这是一个「缺席」,现在是一个「决定」. An absence and a decision are indistinguishable in code until someone writes the second one down. Each header states that the server is the validation authority here, quotes the ruling, points at #6868, and says plainly what a future editor must not do: no shared evaluator, no second validation implementation, no buildValidationRules call from this surface.

(b) The server's reason now renders beside the input it refused

Before: cleanError handed the user the backend's own string, e.g. VALIDATION_FAILED: Validation failed for crm_opportunity, after they had edited three fields.

After: the reason appears under the field the server named, with role="alert", and that input is marked aria-invalid. The save bar keeps a labelled per-field summary as well — a rejected field can be collapsed or scrolled out of view, which is the same reason form.tsx keeps its banner on the form surface.

The transport.InlineEditContextValue gains fieldErrors (field machine name to the server's reason, nullable) and setFieldErrors, the exact companions of the error / setError pair it already carried and already documented as "driven by the save bar". This was the missing piece and nothing else was: the save bar and the field rows are siblings under InlineEditProvider in both persistence modes (app-shell/RecordDetailView.tsx:2340/:2509; RecordDetailDrawer.tsx:346/:398), so there was no channel between the component that receives a refusal and the components that render the fields it is about. Cleared by enter() and teardown exactly like error, so an attribution cannot outlive its session. No host wiring was neededHeaderHighlight already reads the session and DetailSection now does too — so app-shell and RecordDetailDrawer are untouched, as the sizing predicted.

Attribution never guesses, two sources in strict order:

  1. The envelope — via @object-ui/react's extractFieldErrors, the single in-repo normaliser, the same one form.tsx uses. It reads all three shapes a VALIDATION_FAILED arrives in (validationErrors from @object-ui/data-objectstack's re-wrap, details.fields from the raw @objectstack/client error, or a bare fields) and drops any entry with no usable field. That drop is load-bearing: a wrong mark on an innocent input is worse than the undirected string it replaces. It is the only source on the DataSource path, whose write is one atomic multi-key update.
  2. The call shape, callback mode only — the drawer loops onFieldSave(field, value) one key per call, so a rejection belongs to that key by construction. The atomic path passes undefined, so a multi-key write can never be attributed this way. The envelope still wins when both are available (pinned).

Anything not field-scoped — a network failure, a permission denial, a bare Error — keeps the cleaned single-line message. Nothing here evaluates a rule.

The error prop on InlineFieldInput is no longer dead

It was deliberately not shipped in round 1: with no value to put in it, it would have been a declared-but-dead prop. Option A creates the value, so it is threaded now and forwarded to the widgets' published #3222 slot.

Measured, and it changes what that slot is for: the #3222error slot marks aria-invalid — it does not render text, on this surface or on the form one, where form.tsx (not the widget) draws the visible message. So threading it buys the a11y marking, and the visible hint is this package's markup either way. That is why the delegated tail's drop of error (the subject of #7008, which reproduces here independently) was never a blocker for this work, and no packages/fields edit was made.

Clause-② check, run before the interface was widened

The PM authorised the addition on the reading that InlineEditContextValue is a runtime React context shape, not an authoring contract, and asked me to stop and report if measurement said otherwise. It does not:

  • Zero references to InlineEditContextValue anywhere outside its own module and one prose comment — control FieldSchema hits 4 files under scripts/ in the same sweep, so the zero is a reading and not a broken grep.
  • Zero mentions in any authoring-surface gate (check-spec-symbols, check-element-data-source-declaration, check-designer-field-key-parity).
  • packages/react/README.md mentions InlineEditzero times (control: SchemaRenderer, 13 hits), so no documented contract enumerates it.
  • The slot is populated from React state by the save bar and read by two renderers. No document is parsed into it, and nothing about what metadata the platform accepts or refuses changes.

⇒ Not clause ②. It is an additive public API member, which is why @object-ui/react is graded minor.

Evidence

  • PinInlineEditSaveBar.serverVerdictFieldHint-6868.test.tsx, 11 tests, all named under --reporter=verbose. Four are new this round and drive the real tree (DetailSection / HeaderHighlight and the save bar as siblings under one provider, as both hosts mount them): the reason is drawn under the refused field and only that field; the refused input is aria-invalid while the accepted one is not (exactly 1 marked element); the hint clears on cancel + re-enter; and it reaches the highlights strip too, proving one session serves both surfaces. The first asserts its own control — both editors really rendered — so the zero on the unrefused field is a real absence, not an empty harness.
  • Counter-probe (ablation) — mapping removed (attributeInlineRefusal forced to null). Mutation proven on disk first (marker occurrences 1/1, blob 364735c27180c2fb), restore proven after (git diff HEAD empty, blob back to 364735c2). Result 9 of 11 red. The 2 that stay green are, by name, DROPS an entry with no usable field and leaves a NON field-scoped failure on the cleaned string — the two negative cases, which assert the fallback the ablation forces everywhere. Green is the correct direction for those, and it is the same pair as round 1.
  • Cross-package reverse verification — a key the new type must reject was pasted into the consumer and the build went red with Type 'Record<string, string> | null' is not assignable to type 'number'. The message names the new member's declared type rather than "property does not exist", which is what makes it discriminating: plugin-detail is compiling against the rebuilt@object-ui/react declarations, not a cache. Restored and re-proven byte-identical.
  • Suites at 2012c0a87: pnpm exec vitest run packages/plugin-detail/src packages/react/srcTest Files 187 passed (187) / Tests 1938 passed (1938). Typecheck: both packages, each echoing tsc --noEmit && tsc -p tsconfig.test.json, exit 0. Root-relative vitest throughout, so the pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378 silent re-root could not occur.
  • Gates at 2012c0a87, all exit 0: check:control-bytes, check:phantom-deps, check:self-import, check:esm-specifiers, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys, check:i18n-drift, check:element-data-source-declaration, and all four changeset gates — check-changeset-presence reporting "6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)".
  • NOT MEASURED, reported as such and not as green: check:readme-exports (exit 1) and check:sdui-registration-pins (exit 2) both refuse to render a verdict without a full build — "the population COLLAPSED — this run proves nothing … packagesRead: found 12, floor is 25" and "No console build to weigh … a run with nothing to read has measured nothing". Both are repo-wide scans needing all 40 packages plus apps/console built; CI owns them. Neither can be affected by this diff — nothing is removed from any barrel and no registration array is touched.
  • eslint (plain form, never --no-inline-config): exit 0, 0 errors. Every changed source file's warning profile is identical to its origin/main baseline, measured file by file — InlineEditContext 4+1, InlineEditSaveBar 7, InlineFieldInput 34+5, DetailSection 10+1+1, HeaderHighlight 8. Zero new.

Changeset: @object-ui/plugin-detail: patch (nothing enters or leaves its callable surface; an existing surface renders an existing refusal better) and @object-ui/react: minor (one additive public API member, spelled out in the changeset body). No major — refused by the gate, and objectui's major is a cross-repo pin, not a severity dial.

⛔ Draft. Not ready, not enqueued, no auto-merge — the PM arms landing.

… inline-edit hints
The inline-edit save bar surfaced the backend's raw string when a write was
refused, leaving the user to guess which edited field was wrong. The refusal
has always been field-scoped; this reads it through @object-ui/react's
extractFieldErrors -- the same single normaliser the form surface uses -- and
renders one reason per rejected field, named by that field's label.
Attribution never guesses: an envelope entry with no usable `field` is dropped,
and callback (drawer) mode attributes from the call shape, where onFieldSave
carries exactly one key per call. Non field-scoped failures keep the cleaned
single-line message.
Also records the maintainer's ruling on objectui#6868 in both modules' headers:
the server is the validation authority on this surface. That was an absence and
is now a decision. No client-side evaluator was added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Its pin drives it through the rendered save bar, which is the path that
actually reaches the mapping, so the export bought nothing and cost a
react-refresh/only-export-components warning this file did not have before
(measured: baseline 7 no-explicit-any + 0 react-refresh; now identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.3 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DnRUybMw.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.09KB63.41KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

…ut it refused
Option A, authorised by the PM after the measurement showed the honest
completion of the objectui#6868 ruling needs one adjacent file: the save bar
and the field rows are siblings under InlineEditProvider in both persistence
modes, so there was no channel between the component that receives a refusal
and the components that render the fields it is about.
- @object-ui/react: InlineEditContextValue gains `fieldErrors` (field machine
name to the server's reason, nullable) and `setFieldErrors`, the exact
companions of the `error`/`setError` pair it already carried. Cleared by
enter() and teardown like `error`, so an attribution cannot outlive its
session. Additive; nothing removed, nothing reshaped.
- InlineEditSaveBar publishes the attributed refusal onto the session instead
of holding it locally, and keeps the record-level summary for a field that is
collapsed or scrolled out of view.
- DetailSection and HeaderHighlight read it and draw the reason under the
input, with role="alert".
- InlineFieldInput takes `error` and forwards it to the widgets' published
#3222 slot (aria-invalid), and marks the terminal raw input directly. The
prop is no longer dead now that a value exists to put in it.
Presentation only. Nothing here evaluates a rule: the server remains the sole
validation authority on this surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.5 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-Caj1su0T.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warren
os-warren marked this pull request as ready for review September 1, 2026 01:42
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 03380aaSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-6868-inline-edit-server-verdict branch September 1, 2026 01:55
os-warren pushed a commit that referenced this pull request Sep 1, 2026
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR #7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ectstack-ai#7128)
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR objectstack-ai#7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority - #7109

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict
Sep 1, 2026
Merged

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority#7109
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6868

Both deliverables of the maintainer's ruling (维护者, 2026-08-31, 决裁批 #13) now land, and both are presentation-layer. Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM

Round 2 note. This PR first landed (b) as per-field attribution rendered in the record-level save bar, because the transport needed one file outside the dispatched fence. The PM measured the sizing, extended the fence by exactly that one file, and chose option A over closing on the partial — on the grounds that 就地 (in place) is in the ruling's own text, and narrowing a ruling to fit a fence is the one thing a seat executing it must not do. The hint now renders beside the input.

(a) The decision is written down

The ruling is recorded in the headers of both modules where the question gets asked: InlineEditSaveBar.tsx (which handles the refusal) and InlineFieldInput.tsx (which runs no rules and, until this PR, took no error prop — for two years that read as an oversight).

The ruling's own framing is why it is written twice: 此前这是一个「缺席」,现在是一个「决定」. An absence and a decision are indistinguishable in code until someone writes the second one down. Each header states that the server is the validation authority here, quotes the ruling, points at #6868, and says plainly what a future editor must not do: no shared evaluator, no second validation implementation, no buildValidationRules call from this surface.

(b) The server's reason now renders beside the input it refused

Before: cleanError handed the user the backend's own string, e.g. VALIDATION_FAILED: Validation failed for crm_opportunity, after they had edited three fields.

After: the reason appears under the field the server named, with role="alert", and that input is marked aria-invalid. The save bar keeps a labelled per-field summary as well — a rejected field can be collapsed or scrolled out of view, which is the same reason form.tsx keeps its banner on the form surface.

The transport.InlineEditContextValue gains fieldErrors (field machine name to the server's reason, nullable) and setFieldErrors, the exact companions of the error / setError pair it already carried and already documented as "driven by the save bar". This was the missing piece and nothing else was: the save bar and the field rows are siblings under InlineEditProvider in both persistence modes (app-shell/RecordDetailView.tsx:2340/:2509; RecordDetailDrawer.tsx:346/:398), so there was no channel between the component that receives a refusal and the components that render the fields it is about. Cleared by enter() and teardown exactly like error, so an attribution cannot outlive its session. No host wiring was neededHeaderHighlight already reads the session and DetailSection now does too — so app-shell and RecordDetailDrawer are untouched, as the sizing predicted.

Attribution never guesses, two sources in strict order:

  1. The envelope — via @object-ui/react's extractFieldErrors, the single in-repo normaliser, the same one form.tsx uses. It reads all three shapes a VALIDATION_FAILED arrives in (validationErrors from @object-ui/data-objectstack's re-wrap, details.fields from the raw @objectstack/client error, or a bare fields) and drops any entry with no usable field. That drop is load-bearing: a wrong mark on an innocent input is worse than the undirected string it replaces. It is the only source on the DataSource path, whose write is one atomic multi-key update.
  2. The call shape, callback mode only — the drawer loops onFieldSave(field, value) one key per call, so a rejection belongs to that key by construction. The atomic path passes undefined, so a multi-key write can never be attributed this way. The envelope still wins when both are available (pinned).

Anything not field-scoped — a network failure, a permission denial, a bare Error — keeps the cleaned single-line message. Nothing here evaluates a rule.

The error prop on InlineFieldInput is no longer dead

It was deliberately not shipped in round 1: with no value to put in it, it would have been a declared-but-dead prop. Option A creates the value, so it is threaded now and forwarded to the widgets' published #3222 slot.

Measured, and it changes what that slot is for: the #3222error slot marks aria-invalid — it does not render text, on this surface or on the form one, where form.tsx (not the widget) draws the visible message. So threading it buys the a11y marking, and the visible hint is this package's markup either way. That is why the delegated tail's drop of error (the subject of #7008, which reproduces here independently) was never a blocker for this work, and no packages/fields edit was made.

Clause-② check, run before the interface was widened

The PM authorised the addition on the reading that InlineEditContextValue is a runtime React context shape, not an authoring contract, and asked me to stop and report if measurement said otherwise. It does not:

  • Zero references to InlineEditContextValue anywhere outside its own module and one prose comment — control FieldSchema hits 4 files under scripts/ in the same sweep, so the zero is a reading and not a broken grep.
  • Zero mentions in any authoring-surface gate (check-spec-symbols, check-element-data-source-declaration, check-designer-field-key-parity).
  • packages/react/README.md mentions InlineEditzero times (control: SchemaRenderer, 13 hits), so no documented contract enumerates it.
  • The slot is populated from React state by the save bar and read by two renderers. No document is parsed into it, and nothing about what metadata the platform accepts or refuses changes.

⇒ Not clause ②. It is an additive public API member, which is why @object-ui/react is graded minor.

Evidence

  • PinInlineEditSaveBar.serverVerdictFieldHint-6868.test.tsx, 11 tests, all named under --reporter=verbose. Four are new this round and drive the real tree (DetailSection / HeaderHighlight and the save bar as siblings under one provider, as both hosts mount them): the reason is drawn under the refused field and only that field; the refused input is aria-invalid while the accepted one is not (exactly 1 marked element); the hint clears on cancel + re-enter; and it reaches the highlights strip too, proving one session serves both surfaces. The first asserts its own control — both editors really rendered — so the zero on the unrefused field is a real absence, not an empty harness.
  • Counter-probe (ablation) — mapping removed (attributeInlineRefusal forced to null). Mutation proven on disk first (marker occurrences 1/1, blob 364735c27180c2fb), restore proven after (git diff HEAD empty, blob back to 364735c2). Result 9 of 11 red. The 2 that stay green are, by name, DROPS an entry with no usable field and leaves a NON field-scoped failure on the cleaned string — the two negative cases, which assert the fallback the ablation forces everywhere. Green is the correct direction for those, and it is the same pair as round 1.
  • Cross-package reverse verification — a key the new type must reject was pasted into the consumer and the build went red with Type 'Record<string, string> | null' is not assignable to type 'number'. The message names the new member's declared type rather than "property does not exist", which is what makes it discriminating: plugin-detail is compiling against the rebuilt@object-ui/react declarations, not a cache. Restored and re-proven byte-identical.
  • Suites at 2012c0a87: pnpm exec vitest run packages/plugin-detail/src packages/react/srcTest Files 187 passed (187) / Tests 1938 passed (1938). Typecheck: both packages, each echoing tsc --noEmit && tsc -p tsconfig.test.json, exit 0. Root-relative vitest throughout, so the pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378 silent re-root could not occur.
  • Gates at 2012c0a87, all exit 0: check:control-bytes, check:phantom-deps, check:self-import, check:esm-specifiers, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys, check:i18n-drift, check:element-data-source-declaration, and all four changeset gates — check-changeset-presence reporting "6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)".
  • NOT MEASURED, reported as such and not as green: check:readme-exports (exit 1) and check:sdui-registration-pins (exit 2) both refuse to render a verdict without a full build — "the population COLLAPSED — this run proves nothing … packagesRead: found 12, floor is 25" and "No console build to weigh … a run with nothing to read has measured nothing". Both are repo-wide scans needing all 40 packages plus apps/console built; CI owns them. Neither can be affected by this diff — nothing is removed from any barrel and no registration array is touched.
  • eslint (plain form, never --no-inline-config): exit 0, 0 errors. Every changed source file's warning profile is identical to its origin/main baseline, measured file by file — InlineEditContext 4+1, InlineEditSaveBar 7, InlineFieldInput 34+5, DetailSection 10+1+1, HeaderHighlight 8. Zero new.

Changeset: @object-ui/plugin-detail: patch (nothing enters or leaves its callable surface; an existing surface renders an existing refusal better) and @object-ui/react: minor (one additive public API member, spelled out in the changeset body). No major — refused by the gate, and objectui's major is a cross-repo pin, not a severity dial.

⛔ Draft. Not ready, not enqueued, no auto-merge — the PM arms landing.

… inline-edit hints
The inline-edit save bar surfaced the backend's raw string when a write was
refused, leaving the user to guess which edited field was wrong. The refusal
has always been field-scoped; this reads it through @object-ui/react's
extractFieldErrors -- the same single normaliser the form surface uses -- and
renders one reason per rejected field, named by that field's label.
Attribution never guesses: an envelope entry with no usable `field` is dropped,
and callback (drawer) mode attributes from the call shape, where onFieldSave
carries exactly one key per call. Non field-scoped failures keep the cleaned
single-line message.
Also records the maintainer's ruling on objectui#6868 in both modules' headers:
the server is the validation authority on this surface. That was an absence and
is now a decision. No client-side evaluator was added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Its pin drives it through the rendered save bar, which is the path that
actually reaches the mapping, so the export bought nothing and cost a
react-refresh/only-export-components warning this file did not have before
(measured: baseline 7 no-explicit-any + 0 react-refresh; now identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.3 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DnRUybMw.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.09KB63.41KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

…ut it refused
Option A, authorised by the PM after the measurement showed the honest
completion of the objectui#6868 ruling needs one adjacent file: the save bar
and the field rows are siblings under InlineEditProvider in both persistence
modes, so there was no channel between the component that receives a refusal
and the components that render the fields it is about.
- @object-ui/react: InlineEditContextValue gains `fieldErrors` (field machine
name to the server's reason, nullable) and `setFieldErrors`, the exact
companions of the `error`/`setError` pair it already carried. Cleared by
enter() and teardown like `error`, so an attribution cannot outlive its
session. Additive; nothing removed, nothing reshaped.
- InlineEditSaveBar publishes the attributed refusal onto the session instead
of holding it locally, and keeps the record-level summary for a field that is
collapsed or scrolled out of view.
- DetailSection and HeaderHighlight read it and draw the reason under the
input, with role="alert".
- InlineFieldInput takes `error` and forwards it to the widgets' published
#3222 slot (aria-invalid), and marks the terminal raw input directly. The
prop is no longer dead now that a value exists to put in it.
Presentation only. Nothing here evaluates a rule: the server remains the sole
validation authority on this surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.5 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-Caj1su0T.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warren
os-warren marked this pull request as ready for review September 1, 2026 01:42
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 03380aaSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-6868-inline-edit-server-verdict branch September 1, 2026 01:55
os-warren pushed a commit that referenced this pull request Sep 1, 2026
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR #7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ectstack-ai#7128)
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR objectstack-ai#7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority - #7109

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict
Sep 1, 2026
Merged

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority#7109
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6868

Both deliverables of the maintainer's ruling (维护者, 2026-08-31, 决裁批 #13) now land, and both are presentation-layer. Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM

Round 2 note. This PR first landed (b) as per-field attribution rendered in the record-level save bar, because the transport needed one file outside the dispatched fence. The PM measured the sizing, extended the fence by exactly that one file, and chose option A over closing on the partial — on the grounds that 就地 (in place) is in the ruling's own text, and narrowing a ruling to fit a fence is the one thing a seat executing it must not do. The hint now renders beside the input.

(a) The decision is written down

The ruling is recorded in the headers of both modules where the question gets asked: InlineEditSaveBar.tsx (which handles the refusal) and InlineFieldInput.tsx (which runs no rules and, until this PR, took no error prop — for two years that read as an oversight).

The ruling's own framing is why it is written twice: 此前这是一个「缺席」,现在是一个「决定」. An absence and a decision are indistinguishable in code until someone writes the second one down. Each header states that the server is the validation authority here, quotes the ruling, points at #6868, and says plainly what a future editor must not do: no shared evaluator, no second validation implementation, no buildValidationRules call from this surface.

(b) The server's reason now renders beside the input it refused

Before: cleanError handed the user the backend's own string, e.g. VALIDATION_FAILED: Validation failed for crm_opportunity, after they had edited three fields.

After: the reason appears under the field the server named, with role="alert", and that input is marked aria-invalid. The save bar keeps a labelled per-field summary as well — a rejected field can be collapsed or scrolled out of view, which is the same reason form.tsx keeps its banner on the form surface.

The transport.InlineEditContextValue gains fieldErrors (field machine name to the server's reason, nullable) and setFieldErrors, the exact companions of the error / setError pair it already carried and already documented as "driven by the save bar". This was the missing piece and nothing else was: the save bar and the field rows are siblings under InlineEditProvider in both persistence modes (app-shell/RecordDetailView.tsx:2340/:2509; RecordDetailDrawer.tsx:346/:398), so there was no channel between the component that receives a refusal and the components that render the fields it is about. Cleared by enter() and teardown exactly like error, so an attribution cannot outlive its session. No host wiring was neededHeaderHighlight already reads the session and DetailSection now does too — so app-shell and RecordDetailDrawer are untouched, as the sizing predicted.

Attribution never guesses, two sources in strict order:

  1. The envelope — via @object-ui/react's extractFieldErrors, the single in-repo normaliser, the same one form.tsx uses. It reads all three shapes a VALIDATION_FAILED arrives in (validationErrors from @object-ui/data-objectstack's re-wrap, details.fields from the raw @objectstack/client error, or a bare fields) and drops any entry with no usable field. That drop is load-bearing: a wrong mark on an innocent input is worse than the undirected string it replaces. It is the only source on the DataSource path, whose write is one atomic multi-key update.
  2. The call shape, callback mode only — the drawer loops onFieldSave(field, value) one key per call, so a rejection belongs to that key by construction. The atomic path passes undefined, so a multi-key write can never be attributed this way. The envelope still wins when both are available (pinned).

Anything not field-scoped — a network failure, a permission denial, a bare Error — keeps the cleaned single-line message. Nothing here evaluates a rule.

The error prop on InlineFieldInput is no longer dead

It was deliberately not shipped in round 1: with no value to put in it, it would have been a declared-but-dead prop. Option A creates the value, so it is threaded now and forwarded to the widgets' published #3222 slot.

Measured, and it changes what that slot is for: the #3222error slot marks aria-invalid — it does not render text, on this surface or on the form one, where form.tsx (not the widget) draws the visible message. So threading it buys the a11y marking, and the visible hint is this package's markup either way. That is why the delegated tail's drop of error (the subject of #7008, which reproduces here independently) was never a blocker for this work, and no packages/fields edit was made.

Clause-② check, run before the interface was widened

The PM authorised the addition on the reading that InlineEditContextValue is a runtime React context shape, not an authoring contract, and asked me to stop and report if measurement said otherwise. It does not:

  • Zero references to InlineEditContextValue anywhere outside its own module and one prose comment — control FieldSchema hits 4 files under scripts/ in the same sweep, so the zero is a reading and not a broken grep.
  • Zero mentions in any authoring-surface gate (check-spec-symbols, check-element-data-source-declaration, check-designer-field-key-parity).
  • packages/react/README.md mentions InlineEditzero times (control: SchemaRenderer, 13 hits), so no documented contract enumerates it.
  • The slot is populated from React state by the save bar and read by two renderers. No document is parsed into it, and nothing about what metadata the platform accepts or refuses changes.

⇒ Not clause ②. It is an additive public API member, which is why @object-ui/react is graded minor.

Evidence

  • PinInlineEditSaveBar.serverVerdictFieldHint-6868.test.tsx, 11 tests, all named under --reporter=verbose. Four are new this round and drive the real tree (DetailSection / HeaderHighlight and the save bar as siblings under one provider, as both hosts mount them): the reason is drawn under the refused field and only that field; the refused input is aria-invalid while the accepted one is not (exactly 1 marked element); the hint clears on cancel + re-enter; and it reaches the highlights strip too, proving one session serves both surfaces. The first asserts its own control — both editors really rendered — so the zero on the unrefused field is a real absence, not an empty harness.
  • Counter-probe (ablation) — mapping removed (attributeInlineRefusal forced to null). Mutation proven on disk first (marker occurrences 1/1, blob 364735c27180c2fb), restore proven after (git diff HEAD empty, blob back to 364735c2). Result 9 of 11 red. The 2 that stay green are, by name, DROPS an entry with no usable field and leaves a NON field-scoped failure on the cleaned string — the two negative cases, which assert the fallback the ablation forces everywhere. Green is the correct direction for those, and it is the same pair as round 1.
  • Cross-package reverse verification — a key the new type must reject was pasted into the consumer and the build went red with Type 'Record<string, string> | null' is not assignable to type 'number'. The message names the new member's declared type rather than "property does not exist", which is what makes it discriminating: plugin-detail is compiling against the rebuilt@object-ui/react declarations, not a cache. Restored and re-proven byte-identical.
  • Suites at 2012c0a87: pnpm exec vitest run packages/plugin-detail/src packages/react/srcTest Files 187 passed (187) / Tests 1938 passed (1938). Typecheck: both packages, each echoing tsc --noEmit && tsc -p tsconfig.test.json, exit 0. Root-relative vitest throughout, so the pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378 silent re-root could not occur.
  • Gates at 2012c0a87, all exit 0: check:control-bytes, check:phantom-deps, check:self-import, check:esm-specifiers, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys, check:i18n-drift, check:element-data-source-declaration, and all four changeset gates — check-changeset-presence reporting "6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)".
  • NOT MEASURED, reported as such and not as green: check:readme-exports (exit 1) and check:sdui-registration-pins (exit 2) both refuse to render a verdict without a full build — "the population COLLAPSED — this run proves nothing … packagesRead: found 12, floor is 25" and "No console build to weigh … a run with nothing to read has measured nothing". Both are repo-wide scans needing all 40 packages plus apps/console built; CI owns them. Neither can be affected by this diff — nothing is removed from any barrel and no registration array is touched.
  • eslint (plain form, never --no-inline-config): exit 0, 0 errors. Every changed source file's warning profile is identical to its origin/main baseline, measured file by file — InlineEditContext 4+1, InlineEditSaveBar 7, InlineFieldInput 34+5, DetailSection 10+1+1, HeaderHighlight 8. Zero new.

Changeset: @object-ui/plugin-detail: patch (nothing enters or leaves its callable surface; an existing surface renders an existing refusal better) and @object-ui/react: minor (one additive public API member, spelled out in the changeset body). No major — refused by the gate, and objectui's major is a cross-repo pin, not a severity dial.

⛔ Draft. Not ready, not enqueued, no auto-merge — the PM arms landing.

… inline-edit hints
The inline-edit save bar surfaced the backend's raw string when a write was
refused, leaving the user to guess which edited field was wrong. The refusal
has always been field-scoped; this reads it through @object-ui/react's
extractFieldErrors -- the same single normaliser the form surface uses -- and
renders one reason per rejected field, named by that field's label.
Attribution never guesses: an envelope entry with no usable `field` is dropped,
and callback (drawer) mode attributes from the call shape, where onFieldSave
carries exactly one key per call. Non field-scoped failures keep the cleaned
single-line message.
Also records the maintainer's ruling on objectui#6868 in both modules' headers:
the server is the validation authority on this surface. That was an absence and
is now a decision. No client-side evaluator was added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Its pin drives it through the rendered save bar, which is the path that
actually reaches the mapping, so the export bought nothing and cost a
react-refresh/only-export-components warning this file did not have before
(measured: baseline 7 no-explicit-any + 0 react-refresh; now identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.3 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DnRUybMw.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.09KB63.41KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

…ut it refused
Option A, authorised by the PM after the measurement showed the honest
completion of the objectui#6868 ruling needs one adjacent file: the save bar
and the field rows are siblings under InlineEditProvider in both persistence
modes, so there was no channel between the component that receives a refusal
and the components that render the fields it is about.
- @object-ui/react: InlineEditContextValue gains `fieldErrors` (field machine
name to the server's reason, nullable) and `setFieldErrors`, the exact
companions of the `error`/`setError` pair it already carried. Cleared by
enter() and teardown like `error`, so an attribution cannot outlive its
session. Additive; nothing removed, nothing reshaped.
- InlineEditSaveBar publishes the attributed refusal onto the session instead
of holding it locally, and keeps the record-level summary for a field that is
collapsed or scrolled out of view.
- DetailSection and HeaderHighlight read it and draw the reason under the
input, with role="alert".
- InlineFieldInput takes `error` and forwards it to the widgets' published
#3222 slot (aria-invalid), and marks the terminal raw input directly. The
prop is no longer dead now that a value exists to put in it.
Presentation only. Nothing here evaluates a rule: the server remains the sole
validation authority on this surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.5 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-Caj1su0T.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warren
os-warren marked this pull request as ready for review September 1, 2026 01:42
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 03380aaSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-6868-inline-edit-server-verdict branch September 1, 2026 01:55
os-warren pushed a commit that referenced this pull request Sep 1, 2026
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR #7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ectstack-ai#7128)
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR objectstack-ai#7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority - #7109

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict
Sep 1, 2026
Merged

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority#7109
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6868

Both deliverables of the maintainer's ruling (维护者, 2026-08-31, 决裁批 #13) now land, and both are presentation-layer. Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM

Round 2 note. This PR first landed (b) as per-field attribution rendered in the record-level save bar, because the transport needed one file outside the dispatched fence. The PM measured the sizing, extended the fence by exactly that one file, and chose option A over closing on the partial — on the grounds that 就地 (in place) is in the ruling's own text, and narrowing a ruling to fit a fence is the one thing a seat executing it must not do. The hint now renders beside the input.

(a) The decision is written down

The ruling is recorded in the headers of both modules where the question gets asked: InlineEditSaveBar.tsx (which handles the refusal) and InlineFieldInput.tsx (which runs no rules and, until this PR, took no error prop — for two years that read as an oversight).

The ruling's own framing is why it is written twice: 此前这是一个「缺席」,现在是一个「决定」. An absence and a decision are indistinguishable in code until someone writes the second one down. Each header states that the server is the validation authority here, quotes the ruling, points at #6868, and says plainly what a future editor must not do: no shared evaluator, no second validation implementation, no buildValidationRules call from this surface.

(b) The server's reason now renders beside the input it refused

Before: cleanError handed the user the backend's own string, e.g. VALIDATION_FAILED: Validation failed for crm_opportunity, after they had edited three fields.

After: the reason appears under the field the server named, with role="alert", and that input is marked aria-invalid. The save bar keeps a labelled per-field summary as well — a rejected field can be collapsed or scrolled out of view, which is the same reason form.tsx keeps its banner on the form surface.

The transport.InlineEditContextValue gains fieldErrors (field machine name to the server's reason, nullable) and setFieldErrors, the exact companions of the error / setError pair it already carried and already documented as "driven by the save bar". This was the missing piece and nothing else was: the save bar and the field rows are siblings under InlineEditProvider in both persistence modes (app-shell/RecordDetailView.tsx:2340/:2509; RecordDetailDrawer.tsx:346/:398), so there was no channel between the component that receives a refusal and the components that render the fields it is about. Cleared by enter() and teardown exactly like error, so an attribution cannot outlive its session. No host wiring was neededHeaderHighlight already reads the session and DetailSection now does too — so app-shell and RecordDetailDrawer are untouched, as the sizing predicted.

Attribution never guesses, two sources in strict order:

  1. The envelope — via @object-ui/react's extractFieldErrors, the single in-repo normaliser, the same one form.tsx uses. It reads all three shapes a VALIDATION_FAILED arrives in (validationErrors from @object-ui/data-objectstack's re-wrap, details.fields from the raw @objectstack/client error, or a bare fields) and drops any entry with no usable field. That drop is load-bearing: a wrong mark on an innocent input is worse than the undirected string it replaces. It is the only source on the DataSource path, whose write is one atomic multi-key update.
  2. The call shape, callback mode only — the drawer loops onFieldSave(field, value) one key per call, so a rejection belongs to that key by construction. The atomic path passes undefined, so a multi-key write can never be attributed this way. The envelope still wins when both are available (pinned).

Anything not field-scoped — a network failure, a permission denial, a bare Error — keeps the cleaned single-line message. Nothing here evaluates a rule.

The error prop on InlineFieldInput is no longer dead

It was deliberately not shipped in round 1: with no value to put in it, it would have been a declared-but-dead prop. Option A creates the value, so it is threaded now and forwarded to the widgets' published #3222 slot.

Measured, and it changes what that slot is for: the #3222error slot marks aria-invalid — it does not render text, on this surface or on the form one, where form.tsx (not the widget) draws the visible message. So threading it buys the a11y marking, and the visible hint is this package's markup either way. That is why the delegated tail's drop of error (the subject of #7008, which reproduces here independently) was never a blocker for this work, and no packages/fields edit was made.

Clause-② check, run before the interface was widened

The PM authorised the addition on the reading that InlineEditContextValue is a runtime React context shape, not an authoring contract, and asked me to stop and report if measurement said otherwise. It does not:

  • Zero references to InlineEditContextValue anywhere outside its own module and one prose comment — control FieldSchema hits 4 files under scripts/ in the same sweep, so the zero is a reading and not a broken grep.
  • Zero mentions in any authoring-surface gate (check-spec-symbols, check-element-data-source-declaration, check-designer-field-key-parity).
  • packages/react/README.md mentions InlineEditzero times (control: SchemaRenderer, 13 hits), so no documented contract enumerates it.
  • The slot is populated from React state by the save bar and read by two renderers. No document is parsed into it, and nothing about what metadata the platform accepts or refuses changes.

⇒ Not clause ②. It is an additive public API member, which is why @object-ui/react is graded minor.

Evidence

  • PinInlineEditSaveBar.serverVerdictFieldHint-6868.test.tsx, 11 tests, all named under --reporter=verbose. Four are new this round and drive the real tree (DetailSection / HeaderHighlight and the save bar as siblings under one provider, as both hosts mount them): the reason is drawn under the refused field and only that field; the refused input is aria-invalid while the accepted one is not (exactly 1 marked element); the hint clears on cancel + re-enter; and it reaches the highlights strip too, proving one session serves both surfaces. The first asserts its own control — both editors really rendered — so the zero on the unrefused field is a real absence, not an empty harness.
  • Counter-probe (ablation) — mapping removed (attributeInlineRefusal forced to null). Mutation proven on disk first (marker occurrences 1/1, blob 364735c27180c2fb), restore proven after (git diff HEAD empty, blob back to 364735c2). Result 9 of 11 red. The 2 that stay green are, by name, DROPS an entry with no usable field and leaves a NON field-scoped failure on the cleaned string — the two negative cases, which assert the fallback the ablation forces everywhere. Green is the correct direction for those, and it is the same pair as round 1.
  • Cross-package reverse verification — a key the new type must reject was pasted into the consumer and the build went red with Type 'Record<string, string> | null' is not assignable to type 'number'. The message names the new member's declared type rather than "property does not exist", which is what makes it discriminating: plugin-detail is compiling against the rebuilt@object-ui/react declarations, not a cache. Restored and re-proven byte-identical.
  • Suites at 2012c0a87: pnpm exec vitest run packages/plugin-detail/src packages/react/srcTest Files 187 passed (187) / Tests 1938 passed (1938). Typecheck: both packages, each echoing tsc --noEmit && tsc -p tsconfig.test.json, exit 0. Root-relative vitest throughout, so the pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378 silent re-root could not occur.
  • Gates at 2012c0a87, all exit 0: check:control-bytes, check:phantom-deps, check:self-import, check:esm-specifiers, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys, check:i18n-drift, check:element-data-source-declaration, and all four changeset gates — check-changeset-presence reporting "6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)".
  • NOT MEASURED, reported as such and not as green: check:readme-exports (exit 1) and check:sdui-registration-pins (exit 2) both refuse to render a verdict without a full build — "the population COLLAPSED — this run proves nothing … packagesRead: found 12, floor is 25" and "No console build to weigh … a run with nothing to read has measured nothing". Both are repo-wide scans needing all 40 packages plus apps/console built; CI owns them. Neither can be affected by this diff — nothing is removed from any barrel and no registration array is touched.
  • eslint (plain form, never --no-inline-config): exit 0, 0 errors. Every changed source file's warning profile is identical to its origin/main baseline, measured file by file — InlineEditContext 4+1, InlineEditSaveBar 7, InlineFieldInput 34+5, DetailSection 10+1+1, HeaderHighlight 8. Zero new.

Changeset: @object-ui/plugin-detail: patch (nothing enters or leaves its callable surface; an existing surface renders an existing refusal better) and @object-ui/react: minor (one additive public API member, spelled out in the changeset body). No major — refused by the gate, and objectui's major is a cross-repo pin, not a severity dial.

⛔ Draft. Not ready, not enqueued, no auto-merge — the PM arms landing.

… inline-edit hints
The inline-edit save bar surfaced the backend's raw string when a write was
refused, leaving the user to guess which edited field was wrong. The refusal
has always been field-scoped; this reads it through @object-ui/react's
extractFieldErrors -- the same single normaliser the form surface uses -- and
renders one reason per rejected field, named by that field's label.
Attribution never guesses: an envelope entry with no usable `field` is dropped,
and callback (drawer) mode attributes from the call shape, where onFieldSave
carries exactly one key per call. Non field-scoped failures keep the cleaned
single-line message.
Also records the maintainer's ruling on objectui#6868 in both modules' headers:
the server is the validation authority on this surface. That was an absence and
is now a decision. No client-side evaluator was added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Its pin drives it through the rendered save bar, which is the path that
actually reaches the mapping, so the export bought nothing and cost a
react-refresh/only-export-components warning this file did not have before
(measured: baseline 7 no-explicit-any + 0 react-refresh; now identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.3 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DnRUybMw.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.09KB63.41KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

…ut it refused
Option A, authorised by the PM after the measurement showed the honest
completion of the objectui#6868 ruling needs one adjacent file: the save bar
and the field rows are siblings under InlineEditProvider in both persistence
modes, so there was no channel between the component that receives a refusal
and the components that render the fields it is about.
- @object-ui/react: InlineEditContextValue gains `fieldErrors` (field machine
name to the server's reason, nullable) and `setFieldErrors`, the exact
companions of the `error`/`setError` pair it already carried. Cleared by
enter() and teardown like `error`, so an attribution cannot outlive its
session. Additive; nothing removed, nothing reshaped.
- InlineEditSaveBar publishes the attributed refusal onto the session instead
of holding it locally, and keeps the record-level summary for a field that is
collapsed or scrolled out of view.
- DetailSection and HeaderHighlight read it and draw the reason under the
input, with role="alert".
- InlineFieldInput takes `error` and forwards it to the widgets' published
#3222 slot (aria-invalid), and marks the terminal raw input directly. The
prop is no longer dead now that a value exists to put in it.
Presentation only. Nothing here evaluates a rule: the server remains the sole
validation authority on this surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.5 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-Caj1su0T.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warren
os-warren marked this pull request as ready for review September 1, 2026 01:42
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 03380aaSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-6868-inline-edit-server-verdict branch September 1, 2026 01:55
os-warren pushed a commit that referenced this pull request Sep 1, 2026
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR #7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ectstack-ai#7128)
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR objectstack-ai#7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority - #7109

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict
Sep 1, 2026
Merged

fix(plugin-detail): present the server's VALIDATION_FAILED as per-field inline-edit hints, and record the ruling that the server is the authority#7109
os-warren merged 3 commits into
mainfrom
claude/issue-6868-inline-edit-server-verdict

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6868

Both deliverables of the maintainer's ruling (维护者, 2026-08-31, 决裁批 #13) now land, and both are presentation-layer. Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM

Round 2 note. This PR first landed (b) as per-field attribution rendered in the record-level save bar, because the transport needed one file outside the dispatched fence. The PM measured the sizing, extended the fence by exactly that one file, and chose option A over closing on the partial — on the grounds that 就地 (in place) is in the ruling's own text, and narrowing a ruling to fit a fence is the one thing a seat executing it must not do. The hint now renders beside the input.

(a) The decision is written down

The ruling is recorded in the headers of both modules where the question gets asked: InlineEditSaveBar.tsx (which handles the refusal) and InlineFieldInput.tsx (which runs no rules and, until this PR, took no error prop — for two years that read as an oversight).

The ruling's own framing is why it is written twice: 此前这是一个「缺席」,现在是一个「决定」. An absence and a decision are indistinguishable in code until someone writes the second one down. Each header states that the server is the validation authority here, quotes the ruling, points at #6868, and says plainly what a future editor must not do: no shared evaluator, no second validation implementation, no buildValidationRules call from this surface.

(b) The server's reason now renders beside the input it refused

Before: cleanError handed the user the backend's own string, e.g. VALIDATION_FAILED: Validation failed for crm_opportunity, after they had edited three fields.

After: the reason appears under the field the server named, with role="alert", and that input is marked aria-invalid. The save bar keeps a labelled per-field summary as well — a rejected field can be collapsed or scrolled out of view, which is the same reason form.tsx keeps its banner on the form surface.

The transport.InlineEditContextValue gains fieldErrors (field machine name to the server's reason, nullable) and setFieldErrors, the exact companions of the error / setError pair it already carried and already documented as "driven by the save bar". This was the missing piece and nothing else was: the save bar and the field rows are siblings under InlineEditProvider in both persistence modes (app-shell/RecordDetailView.tsx:2340/:2509; RecordDetailDrawer.tsx:346/:398), so there was no channel between the component that receives a refusal and the components that render the fields it is about. Cleared by enter() and teardown exactly like error, so an attribution cannot outlive its session. No host wiring was neededHeaderHighlight already reads the session and DetailSection now does too — so app-shell and RecordDetailDrawer are untouched, as the sizing predicted.

Attribution never guesses, two sources in strict order:

  1. The envelope — via @object-ui/react's extractFieldErrors, the single in-repo normaliser, the same one form.tsx uses. It reads all three shapes a VALIDATION_FAILED arrives in (validationErrors from @object-ui/data-objectstack's re-wrap, details.fields from the raw @objectstack/client error, or a bare fields) and drops any entry with no usable field. That drop is load-bearing: a wrong mark on an innocent input is worse than the undirected string it replaces. It is the only source on the DataSource path, whose write is one atomic multi-key update.
  2. The call shape, callback mode only — the drawer loops onFieldSave(field, value) one key per call, so a rejection belongs to that key by construction. The atomic path passes undefined, so a multi-key write can never be attributed this way. The envelope still wins when both are available (pinned).

Anything not field-scoped — a network failure, a permission denial, a bare Error — keeps the cleaned single-line message. Nothing here evaluates a rule.

The error prop on InlineFieldInput is no longer dead

It was deliberately not shipped in round 1: with no value to put in it, it would have been a declared-but-dead prop. Option A creates the value, so it is threaded now and forwarded to the widgets' published #3222 slot.

Measured, and it changes what that slot is for: the #3222error slot marks aria-invalid — it does not render text, on this surface or on the form one, where form.tsx (not the widget) draws the visible message. So threading it buys the a11y marking, and the visible hint is this package's markup either way. That is why the delegated tail's drop of error (the subject of #7008, which reproduces here independently) was never a blocker for this work, and no packages/fields edit was made.

Clause-② check, run before the interface was widened

The PM authorised the addition on the reading that InlineEditContextValue is a runtime React context shape, not an authoring contract, and asked me to stop and report if measurement said otherwise. It does not:

  • Zero references to InlineEditContextValue anywhere outside its own module and one prose comment — control FieldSchema hits 4 files under scripts/ in the same sweep, so the zero is a reading and not a broken grep.
  • Zero mentions in any authoring-surface gate (check-spec-symbols, check-element-data-source-declaration, check-designer-field-key-parity).
  • packages/react/README.md mentions InlineEditzero times (control: SchemaRenderer, 13 hits), so no documented contract enumerates it.
  • The slot is populated from React state by the save bar and read by two renderers. No document is parsed into it, and nothing about what metadata the platform accepts or refuses changes.

⇒ Not clause ②. It is an additive public API member, which is why @object-ui/react is graded minor.

Evidence

  • PinInlineEditSaveBar.serverVerdictFieldHint-6868.test.tsx, 11 tests, all named under --reporter=verbose. Four are new this round and drive the real tree (DetailSection / HeaderHighlight and the save bar as siblings under one provider, as both hosts mount them): the reason is drawn under the refused field and only that field; the refused input is aria-invalid while the accepted one is not (exactly 1 marked element); the hint clears on cancel + re-enter; and it reaches the highlights strip too, proving one session serves both surfaces. The first asserts its own control — both editors really rendered — so the zero on the unrefused field is a real absence, not an empty harness.
  • Counter-probe (ablation) — mapping removed (attributeInlineRefusal forced to null). Mutation proven on disk first (marker occurrences 1/1, blob 364735c27180c2fb), restore proven after (git diff HEAD empty, blob back to 364735c2). Result 9 of 11 red. The 2 that stay green are, by name, DROPS an entry with no usable field and leaves a NON field-scoped failure on the cleaned string — the two negative cases, which assert the fallback the ablation forces everywhere. Green is the correct direction for those, and it is the same pair as round 1.
  • Cross-package reverse verification — a key the new type must reject was pasted into the consumer and the build went red with Type 'Record<string, string> | null' is not assignable to type 'number'. The message names the new member's declared type rather than "property does not exist", which is what makes it discriminating: plugin-detail is compiling against the rebuilt@object-ui/react declarations, not a cache. Restored and re-proven byte-identical.
  • Suites at 2012c0a87: pnpm exec vitest run packages/plugin-detail/src packages/react/srcTest Files 187 passed (187) / Tests 1938 passed (1938). Typecheck: both packages, each echoing tsc --noEmit && tsc -p tsconfig.test.json, exit 0. Root-relative vitest throughout, so the pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378 silent re-root could not occur.
  • Gates at 2012c0a87, all exit 0: check:control-bytes, check:phantom-deps, check:self-import, check:esm-specifiers, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys, check:i18n-drift, check:element-data-source-declaration, and all four changeset gates — check-changeset-presence reporting "6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)".
  • NOT MEASURED, reported as such and not as green: check:readme-exports (exit 1) and check:sdui-registration-pins (exit 2) both refuse to render a verdict without a full build — "the population COLLAPSED — this run proves nothing … packagesRead: found 12, floor is 25" and "No console build to weigh … a run with nothing to read has measured nothing". Both are repo-wide scans needing all 40 packages plus apps/console built; CI owns them. Neither can be affected by this diff — nothing is removed from any barrel and no registration array is touched.
  • eslint (plain form, never --no-inline-config): exit 0, 0 errors. Every changed source file's warning profile is identical to its origin/main baseline, measured file by file — InlineEditContext 4+1, InlineEditSaveBar 7, InlineFieldInput 34+5, DetailSection 10+1+1, HeaderHighlight 8. Zero new.

Changeset: @object-ui/plugin-detail: patch (nothing enters or leaves its callable surface; an existing surface renders an existing refusal better) and @object-ui/react: minor (one additive public API member, spelled out in the changeset body). No major — refused by the gate, and objectui's major is a cross-repo pin, not a severity dial.

⛔ Draft. Not ready, not enqueued, no auto-merge — the PM arms landing.

… inline-edit hints
The inline-edit save bar surfaced the backend's raw string when a write was
refused, leaving the user to guess which edited field was wrong. The refusal
has always been field-scoped; this reads it through @object-ui/react's
extractFieldErrors -- the same single normaliser the form surface uses -- and
renders one reason per rejected field, named by that field's label.
Attribution never guesses: an envelope entry with no usable `field` is dropped,
and callback (drawer) mode attributes from the call shape, where onFieldSave
carries exactly one key per call. Non field-scoped failures keep the cleaned
single-line message.
Also records the maintainer's ruling on objectui#6868 in both modules' headers:
the server is the validation authority on this surface. That was an absence and
is now a decision. No client-side evaluator was added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Its pin drives it through the rendered save bar, which is the path that
actually reaches the mapping, so the export bought nothing and cost a
react-refresh/only-export-components warning this file did not have before
(measured: baseline 7 no-explicit-any + 0 react-refresh; now identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.3 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DnRUybMw.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.09KB63.41KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

…ut it refused
Option A, authorised by the PM after the measurement showed the honest
completion of the objectui#6868 ruling needs one adjacent file: the save bar
and the field rows are siblings under InlineEditProvider in both persistence
modes, so there was no channel between the component that receives a refusal
and the components that render the fields it is about.
- @object-ui/react: InlineEditContextValue gains `fieldErrors` (field machine
name to the server's reason, nullable) and `setFieldErrors`, the exact
companions of the `error`/`setError` pair it already carried. Cleared by
enter() and teardown like `error`, so an attribution cannot outlive its
session. Additive; nothing removed, nothing reshaped.
- InlineEditSaveBar publishes the attributed refusal onto the session instead
of holding it locally, and keeps the record-level summary for a field that is
collapsed or scrolled out of view.
- DetailSection and HeaderHighlight read it and draw the reason under the
input, with role="alert".
- InlineFieldInput takes `error` and forwards it to the widgets' published
#3222 slot (aria-invalid), and marks the terminal raw input directly. The
prop is no longer dead now that a value exists to put in it.
Presentation only. Nothing here evaluates a rule: the server remains the sole
validation authority on this surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.5 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-Caj1su0T.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.86KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warren
os-warren marked this pull request as ready for review September 1, 2026 01:42
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 03380aaSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-6868-inline-edit-server-verdict branch September 1, 2026 01:55
os-warren pushed a commit that referenced this pull request Sep 1, 2026
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR #7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…ectstack-ai#7128)
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this
factory. The rest of the contract — `error`, `onUploadingChange`, and the
"Host plumbing" block — still type-checked, read as supported, and never
reached the widget.
`error` was the live one: `InlineFieldInput` has passed it into this factory
since PR objectstack-ai#7109 and the factory dropped it, so an inline-edit control that had
failed validation never reported `aria-invalid`.
The keys travel through a new sibling executor, `toHostProps`, never through
`DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time
assertions make the two executors partition the contract.
`dataSource` precedence is stated: the explicit prop wins over
`SchemaRendererContext`, the order `LookupField` already implements. The
factory is a conduit and resolves nothing.
`RequiredFieldsDialog` now hands its computed required-validation state to the
control, and its stale `takes no id to associate with` justification — false
since objectui#7009 — is corrected.
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-warren@claude