Skip to content

fix(fields): FLS-filter the lookup dropdown's and record picker's drawn columns; gate PeoplePicker's $expand (#10373) - #10411

Merged
os-litant merged 6 commits into
mainfrom
claude/issue-10373-fields-display-fls
Sep 25, 2026
Merged

os-litant merged 6 commits into
mainfrom
claude/issue-10373-fields-display-fls

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10373
Clause-②: no

What changes

Head: 2322673f3. The renderer-side FLS rulings objectui#7215 / objectui#7230 say "FLS gates the OUTPUT", and the objectui#7429 sweep applied them. RelatedList gates its $expand and every column it draws (keepReadableColumns), with no exception for the title. In @object-ui/fields only the $expand lists were gated. This PR applies the family's one shape, !perms.isLoaded || perms.checkField(object, f, 'read') with perms in the memo deps, to everything these three widgets draw or request:

  • LookupField dropdown columns. A new readablePreviewColumns memo, which previewOf renders, filters on referenceTo. That is the object candidateExpand already judges. The id column is kept.
  • LookupField option label. recordToOption builds the label from the row with the policy-denied fields removed (fieldReadGate / withoutDeniedFields). That is the row ObjectStack's FieldMasker already serves, so on ObjectStack no label changes.
    • A denied display field falls through the existing chain: a titleFormat template, then getRecordDisplayName, then the other name-like keys, then the id. A titleFormat token naming a denied field renders as an empty slot.
    • Every recordToOption call applies the gate, so the chip and the read-only rendering show the same label.
    • The committed value is unchanged. The option onSelectRecord receives keeps the served row's other fields; only its label changes.
  • RecordPickerDialog. A new readableColumns memo, filtered on objectName, feeds the header, skeleton, cells and renderGrid slot.
    • The display column is gated like any other column. The id column never is.
    • When the policy leaves no column to draw, the picker draws the id column instead, so every row stays selectable and identifiable.
    • The display column's titleFormat reads the row with the denied fields removed.
    • Selection reads the id from the row (getRecordId), and onSelectRecords is unchanged.
  • PeoplePicker. effectiveExpand drops relations denied on objectName, the object the picker queries. This applies both when the list is derived from subtitle paths and when the caller passes expand.

The $expand memos of the dropdown and the picker are unchanged from main. They read the unfiltered column list and gate their own output, in the objectui#7429 shape. Both lists ask checkField about the same names on the same object.

Why the display matters beyond $expand. A denied relation column was left out of $expand and arrived as a bare key. The lookup cell renderer then resolved that key with its own findOne, one per row. The pins assert zero such reads, and that the column is not drawn.

Surface additions, declared and approved by the seat on the card, claim amended:

  • .changeset/10223-lookup-candidates-expand.md: body corrected, frontmatter byte-identical. Its last FLS sentence said a column left out of $expand "is resolved one by one as before". It now says a column the policy denies is not drawn, and a field it denies is not shown in an option's label or the picker's title column.
  • .changeset/lookup-dropdown-cell-renderer-5492.md: body corrected, frontmatter byte-identical. Its last paragraph said a slot is dropped "only when the record holds no value". That now holds among the columns the policy lets the user read.
  • packages/types/src/__tests__/field-metadata-rows-option-description-6140.test.ts: one anchor string moved, forced by this diff; the pin's meaning is unchanged. The read site still exists: recordToOption still emits description, now in const option = { value: val, label: String(label), description, ...record };. A repo-wide fixed-string grep of every removed line of the three sources found no other source-text anchor.

Two CI fixes on this PR.

  1. TS18047 at 346275b07. The package tsc reported "'shown' is possibly 'null'" at both copies of withoutDeniedFields. The helper now builds the shown row from Object.entries and returns it only when a field was withheld. There is no nullable accumulator and no non-null assertion. Evidence:
    • cd packages/fields && pnpm exec tsc -p tsconfig.json --noEmit exits 0 at dc2488fb3.
    • The same command on the 346275b07 sources reproduces both CI errors, exit 2.
    • tsc -p tsconfig.test.json on those sources also reports both errors, so the test program would have caught this too. It was not run at 346275b07 before that push.
  2. The 6140 anchor at 0e3b8bb13. Fixed as described above.

Verification (head 2322673f3)

  • Pins: LookupField.displayFls-10373, RecordPickerDialog.displayFls-10373 and PeoplePicker.expandFls-10373 are part of the targeted run below. Every LookupField* / RecordPickerDialog* / PeoplePicker* suite, plus the four suites that read these sources as text (6140, 6153, relationalMetaCopySet.derivation, check-control-bytes), gave Test Files 25 passed (25) and Tests 200 passed (200).
  • Whole @object-ui/fields suite (pnpm exec vitest run packages/fields/ from the repo root): Test Files 184 passed | 1 skipped (185), Tests 3082 passed | 7 skipped (3089).
  • Red on 2b1f7fd51, rerun at 2322673f3. The two sources were checked out from 2b1f7fd51, each blob hash was verified on disk, and a trap restored them from HEAD. Result: Tests 6 failed | 16 passed (22). The 6 are the new display-field and titleFormat pins. The dropdown label differs from the stripping backend's; the picker draws Name; each titleFormat shows S-0. After the run, git diff HEAD was empty.
  • Red on base 721d1e008, with all three sources checked out: Tests 15 failed | 7 passed (22). The 7 greens are the controls whose names start with "control:".
  • Build and type-check:
    • pnpm --filter @object-ui/fields build (tsc && vite build && node scripts/build-css.mjs): exit 0.
    • pnpm --filter @object-ui/fields type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0, run after the closure build.
  • Lint: eslint --no-inline-config on the 7 touched .ts/.tsx files gives 0 errors. For each source, the per-rule warning counts equal the base content's (61 / 36 / 17; the 6140 test 0 / 0). eslint.config.js has no type-aware parser options, so this diff cannot move a verdict on an untouched file. A repo-wide pnpm lint is left to CI.
  • Gates that exit 0: check-changeset-presence, check-changeset-no-major, check-changeset-overwrite, check:new-line-citations (0 new), check:changeset-claims, check:control-bytes, check:test-path-roots, check:phantom-deps, check:unused-deps, check:pending-changeset-literals.
    • check-changeset-overwrite reports the two corrected bodies as its case 2 (correcting on purpose). It is report-only.
    • check-governed-queue-guard --test reports NOT GOVERNED.

Acceptance notes

  • Late policy. The columns and every memo-built label re-derive when the policy answer changes; a pin covers this in one mounted picker. A chip label hydrated by the fetch effect keeps the policy that effect ran under. MePermissionsProvider mounts children only after its first answer, so that path matters only on a later change of policy.
  • Out of scope, reported to the seat:
    • PeoplePicker rows draw plain subtitle fields such as email, and the avatar, without an FLS read.
    • The picker's filter bar can offer a denied column as a filter input.
    • Title masking on other surfaces, such as detail headers and other formatRecordTitle consumers, is the seat's own card.
  • Changeset: .changeset/10373-fields-display-fls.md, a patch for @object-ui/fields.

Generated by Claude Code

…wn columns; gate PeoplePicker's $expand

Under the renderer-side FLS rulings ("FLS gates the OUTPUT"), RelatedList
filters both its $expand and the columns it draws. In @object-ui/fields only
the $expand lists were gated:

- LookupField: the dropdown's previewed columns are now filtered by
  checkField(referenceTo, field, 'read') once the policy has loaded; the id
  column is kept, the option label is not a preview column.
- RecordPickerDialog: the drawn columns (header, cells, skeleton, renderGrid
  slot) are filtered on objectName; the display and id columns are kept.
- PeoplePicker: the $expand list (derived from dotted subtitle paths, or the
  caller's expand) drops relations denied on objectName.

Before the policy loads nothing is filtered; perms is in each memo's deps.
The $expand memos of the dropdown and picker are unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…tches through a click

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3043.4 KB 3104.5 KB
Main entry chunk (gzip) 148.4 KB 350 KB
Entry file index-0IzcfnAG.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 542.47KB 129.64KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 224.12KB 62.26KB
fields (index.js) 255.27KB 64.59KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 72.54KB 20.26KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.45KB 35.33KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.94KB 67.93KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.47KB 41.53KB
plugin-grid (index.js) 215.22KB 58.85KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.36KB 28.24KB
plugin-map (index.js) 22.05KB 7.14KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.72KB 8.96KB
plugin-tree (index.js) 10.74KB 3.76KB
plugin-view (index.js) 85.86KB 21.38KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

…o pending changesets

Patch round 1 of objectui#10373 (seat answers on the card):

- RecordPickerDialog: the display column is filtered like any other column
  (keepReadableColumns has no title exemption). The id column never is; when
  the policy leaves no column to draw, the id column is drawn so every row
  stays selectable. The display column's titleFormat reads the row with the
  denied fields removed.
- LookupField: recordToOption builds the option label from the row with the
  denied fields removed (the row FieldMasker serves), so a denied display
  field or titleFormat token falls through the existing chain. The value,
  description and carried record are the row as served.
- .changeset/10223-lookup-candidates-expand.md and
  .changeset/lookup-dropdown-cell-renderer-5492.md: one sentence each made
  false by this change is corrected; frontmatter untouched.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Console Performance Budget — not measured

This run did not produce a console bundle to measure, so there is no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing was measured — the numbers a real violation would carry are simply absent.

Step Outcome
Build packages failure
Check console performance budget skipped

See the workflow run for details.

No package size report: it is only generated from a complete package build, so a partial one is never shown.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3044.0 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-ItVtqWBM.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 542.47KB 129.64KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 224.12KB 62.26KB
fields (index.js) 256.84KB 65.05KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 72.54KB 20.26KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.45KB 35.33KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.94KB 67.93KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.47KB 41.53KB
plugin-grid (index.js) 215.22KB 58.85KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.36KB 28.24KB
plugin-map (index.js) 22.05KB 7.14KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.72KB 8.96KB
plugin-tree (index.js) 10.74KB 3.76KB
plugin-view (index.js) 85.86KB 21.38KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

… the package tsc)

The fields build's `tsc` (tsconfig.json) reported TS18047 "'shown' is possibly
'null'" at both copies of the helper: the lazily created copy was narrowed by
an `if` inside a loop, which the compiler does not carry to the `delete`. The
helper now builds the shown row unconditionally from Object.entries and
returns it only when a field was withheld, so the input comes back untouched
otherwise — the identity the label override relies on.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
… literal

`recordToOption` still emits `description` for fetched records; the option
literal now binds to `const option` so the label can be rebuilt from the
shown row (objectui#10373). The pin's meaning is unchanged: the widget still
reads and emits the declared `description` key.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3044.0 KB 3104.5 KB
Main entry chunk (gzip) 148.5 KB 350 KB
Entry file index-LpiFhOm3.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 542.47KB 129.64KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 224.12KB 62.26KB
fields (index.js) 256.84KB 65.07KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 72.54KB 20.26KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.45KB 35.33KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.94KB 67.93KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.47KB 41.53KB
plugin-grid (index.js) 215.22KB 58.85KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.36KB 28.24KB
plugin-map (index.js) 22.05KB 7.14KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.72KB 8.96KB
plugin-tree (index.js) 10.74KB 3.76KB
plugin-view (index.js) 85.86KB 21.38KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2322673f3de40ef3b0d1a2a2db49ef230cbb8b1a

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (126 of 126 model stamps at the review tier). Adopted by this seat.

① Derived judgments

  • Inputs. Head resolved from the PR resource is 2322673f3de40ef3b0d1a2a2db49ef230cbb8b1a, as expected; branch claude/issue-10373-fields-display-fls; merge-base with origin/main is 721d1e0085b9027e2439d1a2beaea81a7f723836 (the base the dev's ablation names). Read: triage 5822470924, claim 5822552559, report 5824136106, seat answers and amendment 1 5824165727, patch-round report 5824644418, amendment 2 5824666446, proposed-body.md. Every judgment below is from git show / git diff by sha and REST GETs; no gate or suite was run.

  • (a) Every output is gated. Enumerated at head:

    • LookupField dropdown preview columns: readablePreviewColumns filters previewColumns by !perms.isLoaded || !referenceTo || c.field === idField || perms.checkField(referenceTo, c.field, 'read'); deps [previewColumns, perms, referenceTo, idField]; previewOf renders from it and lists it in its own deps. Object judged: referenceTo, the same object candidateExpand judges. Right object.
    • Option label: every one of the nine recordToOption( call sites at head passes a gate (fieldReadGate(perms, referenceTo, idField) or a local readable built from it): fetchedOptions, the three fetch-effect branches, resolveSelectedOption, handlePickerSelectRecords, recentOptions, both handleCreate paths. fieldReadGate returns undefined while !perms.isLoaded || !objectName, so nothing is withheld before the answer. The label chain reads shown (the row minus denied fields) for titleFormat, getRecordDisplayName, displayField and the name-like keys; value stays record[idField] ?? record.id ?? .... perms and referenceTo are in the deps of fetchedOptions, resolveSelectedOption, recentOptions, handlePickerSelectRecords and handleCreate.
    • Chip and read-only label: both render opt.label, which is the gated label above, so they follow the same policy. See the residual below on their || opt[displayField] fallback.
    • RecordPickerDialog header, skeleton, cells, renderGrid slot: all four sites read readableColumns (if (!perms.isLoaded) return resolvedColumns; then c.field === idField || perms.checkField(objectName, c.field, 'read'), falling back to [{ field: idField, label: fieldToLabel(idField) }] when nothing survives); deps [resolvedColumns, perms, objectName, idField]. Object judged: objectName, the object expand judges. Right object. No resolvedColumns draw site remains.
    • Picker titleFormat column: renderCellContent passes withoutDeniedFields(toPredicateRecord(record, fieldsMeta), perms, objectName, idField) for the display column when titleFormat is set; deps include perms, objectName, idField. Gated on objectName.
    • PeoplePicker.effectiveExpand: both branches fill requested (caller expand, else the relations split off dotted subtitleFields), then one filter !perms.isLoaded || perms.checkField(objectName, f, 'read'); empty becomes undefined (no $expand key); deps [expand, subtitleFields, perms, objectName]. objectName is the object the picker queries; LookupField passes objectName={referenceTo} and no expand. Right object.
    • !perms.isLoaded passes everything through at every site, and usePermissions without a provider yields isLoaded: false, which is what the seven "control:" cases rely on. The shape matches RelatedList.keepReadableColumns (if (!perms?.isLoaded || !objectName) return cols; then checkField(objectName, key, 'read'), no title exemption).
    • The $expand memos candidateExpand and expand are untouched by the diff (context lines only).
    • Ungated drawn outputs found besides the dev's listed out-of-scope items: (1) the search-variant chip's avatar reads opt[avatarField] || opt.image off the served row with no FLS read (an image URL; the dev's out-of-scope note names only PeoplePicker's avatar); (2) the chip, read-only and compact-trigger labels fall back to opt[displayField], the raw served value through the option's record spread, when the built label is the empty string. That fallback is reachable only if the display field is denied AND a readable name-like key (label, name, full_name, title, subject, externalId) or the id is the empty string; with a denied display field the chain otherwise ends at the non-empty id. Both are pre-existing reads, narrow, and outside the card's enumerated surfaces (columns, labels, $expand); reported as residuals for the seat, not as breaches. (3) option.description is record[descriptionField] ungated, but it is not drawn at head: the description column is drawn through previewOf, which is gated; it rides on the option object like the rest of the served row.
    • The chip-hydration fetch effect computes readable when it runs and lists neither perms nor referenceTo-independent policy in its deps (an exhaustive-deps disable is pre-existing). The dev discloses this; MePermissionsProvider returns loadingFallback while loading && !data, so it matters only on a later policy change. Memo-built labels and every column list do re-derive.
  • (b) Selectability and value. The id column is exempt in both surfaces (c.field === idField in the picker, plus idField/id/_id in both withoutDeniedFields gates). When every column is denied the picker draws the id column (pinned: headers ['Id'], row click commits the id). handleRowClick, getRecordId, handleConfirm and onSelectRecords are unchanged from base. The onSelectRecord option is { value, label, description, ...record } with label re-applied when a field was withheld; its other keys are the served row (pinned: key set and non-label values equal between the open and gated runs). So yes, denied values the display now hides are still present on that option object and on pickerResolvedRecords / previewRows. Judgment: this is data the backend already sent to the client; the display gate is defence in depth for what is drawn, and the seat ruled the committed value and the handed-on records stay unfiltered. A host that renders those fields itself owns its own FLS. Acceptable and consistent with the contract.

  • (c) The two withoutDeniedFields copies. LookupField's takes (record, readable) where readable is the FieldReadGate from fieldReadGate; RecordPickerDialog's takes (record, perms, objectName, idField) and inlines the same gate (key === idField || key === 'id' || key === '_id' || perms.checkField(objectName, key, 'read')), returning record when !perms.isLoaded. Both: early-return on a non-object, iterate Object.entries, copy readable keys into a fresh object, set withheld on the first denied key, and return withheld ? shown : record. Behaviour is identical. The accumulator is a plain non-nullable object with a boolean flag; there is no non-null assertion and no nullable variable, so the diagnostic reported at 346275b07 cannot recur. Same-reference return when nothing is withheld: yes, and recordToOption relies on it (shown === record ? option : { ...option, label: String(label) }) so a denied field literally named label cannot return through the spread. No memo depends on that identity.

  • (d) Pins are real and can fail. All three import PermissionProvider from @object-ui/permissions and mount it with roles, userRoles={['viewer']} and a field-level permissions config (fieldPermissions: [{ field, read: false }]); no vi.mock, no stub of checkField, no double inside the widget. The only vi.fn doubles are the data sources (find / findOne / getObjectSchema), and perRowReads counts non-candidate find calls plus findOne calls to assert zero per-row resolution. Arithmetic: it( counts are 8 + 9 + 5 = 22; names starting control: are 3 + 2 + 2 = 7, so base red 15 failed / 7 passed is exactly "every non-control fails". Since 2b1f7fd51 the pin files changed by: LookupField 4 to 8 (one case rewritten, four added), RecordPickerDialog 6 to 9 (one rewritten, three added), PeoplePicker unchanged (5); of those nine, three are control: and six are the display-field / titleFormat / payload-shape cases, matching the reported 6 failed / 16 passed on the 2b1f7fd51 sources. PeoplePicker.tsx has no diff between 2b1f7fd51 and head, so checking out only the two sources for that ablation is correct. The late-policy pin asserts the same mounted grid by identity before and after the policy flip.

  • (e) The 6140 anchor pin. The diff to packages/types/src/__tests__/field-metadata-rows-option-description-6140.test.ts is one line: the text anchor moves from return { value: val, label: String(label), description, ...record }; to const option = { value: val, label: String(label), description, ...record };; the why string and the search-site anchor are untouched. The new literal exists once at head in LookupField.tsx (line of recordToOption), and the old one exists at base. The pin still means "the widget reads and emits description": recordToOption still emits the key, and both return branches carry it.

  • (f) Changeset truth. .changeset/10373-fields-display-fls.md (frontmatter '@object-ui/fields': patch): every sentence checked against head is true: the base state (only $expand gated; denied columns previewed and headed; denied relation resolved by the cell renderer; denied display field labelled options; titleFormat printed every token), the new behaviour (drawn columns gated on the referenced object, display column included, id column never, id fallback, renderGrid slot, pass-through before load and re-derive after), the label rule (row minus denied fields; chain ends at the id; a denied titleFormat token leaves an empty slot; committed value, onSelectRecords records and the onSelectRecord option's other fields unchanged), the PeoplePicker paragraph (picker: 'search' opens it at pickerVariant === 'search'; both expand sources gated; a subtitle segment through an unexpanded relation resolves to undefined in resolvePath and is dropped by getPersonSubtitle), and the defence-in-depth sentence: ObjectStack's FieldMasker.maskRecord does delete result[field] for every hidden field (the separate partial-mask rules only replace values of fields the caller may read). The two corrected files: frontmatter sha256 identical to base for both (5d63c14d…, the three-line '@object-ui/fields': patch block); the corrected sentences are true at head (a readable bare id is still resolved by the cell renderer; a denied column is not drawn; a denied field is absent from the option label and the picker's title column; the empty-slot rule now holds among readable columns); the untouched sentences of both files (collapse through toPredicateRecord, title attribute keeps the option label, recently-used rail asks the same expansion) remain true. No line-address citation in any of the three (the only regex hit is an ISO date inside the 5492 table). Hand re-read of every other pending changeset naming LookupField, RecordPickerDialog, PeoplePicker, recordToOption, previewColumns or the display column: 10120 ("LookupField's props are unchanged") still true, no prop was added; 6140 ("emits from recordToOption") still true; 6874 (refObjectSchema?.titleFormat read; one non-test titleFormat={refTitleFormat} pass) still true, the only added titleFormat= passes are in the new pins; 7245, 8672, 8755, 9964, lucky-donkeys-shave, 6875, 6711, 7166, 7435 speak to derivations and key spellings this diff does not move. No further sentence is made false.

  • (g) The proposed PR body. Every factual sentence checked holds at head: the two withoutDeniedFields and fieldReadGate descriptions; the id exemption; the id fallback; the titleFormat reads; getRecordId and unchanged onSelectRecords; both PeoplePicker branches; unchanged $expand memos; the three surface additions as described (the 10223 and 5492 sentences quoted are the ones replaced; the 6140 anchor literal is the one at head; my own repo-wide fixed-string grep of the 37 removed lines of at least 12 characters finds exactly the two coincidental hits the dev names, personDisplay.ts and check-readme-exports.mjs, neither a pin); eslint.config.js has no parserOptions / projectService / project / tsconfigRootDir; the build and type-check scripts of packages/fields/package.json are the quoted commands; the 7 touched .ts/.tsx files are 3 sources + 3 pins + the 6140 test; the test-file population of packages/fields is 185; 21 LookupField*/RecordPickerDialog*/PeoplePicker* suites plus the four named text-reading suites make 25; check-changeset-overwrite is report-only unless OS_CHANGESET_OVERWRITE_ENFORCE=1; MePermissionsProvider renders loadingFallback while loading && !data. The CI narrative matches the check-runs: 346275b07 has Test and Bundle Analysis failed with the rest cancelled; 0e3b8bb13 has Test and Test (shard 6/8) failed; head is all green. Counts I cannot re-measure without running (suite totals, per-rule lint counts, gate exit codes) are consistent with the green head and are not load-bearing for the contract. One loosely worded sentence, not false: "Both lists ask checkField about the same names on the same object" holds for the relation columns the $expand memo judges, a subset of what the display gate judges. Closing keyword: Fixes #10373 only. Family or version identifiers: none; the footer is the product-name attribution with the session URL, the same footer the current body carries.

② Semver level

patch on @object-ui/fields is right. objectui AGENTS.md, "版本号策略 (version alignment)": "minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进" and "推论:changeset 里不要声明 major —— … objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)", enforced by scripts/check-changeset-no-major.mjs. This diff is a bug fix under Clause-②: no: no export, prop, key or accept set moves; only what is drawn under a loaded policy narrows. Nothing breaking, so minor is not owed and major is banned; the head's Changeset Bump Policy check is green. @object-ui/types: scripts/check-changeset-presence.mjs counts a changed file when, clause (a), it is under the package's own src/ directory, with "No carve-out for test files under src/. A change confined to src/__tests__/ is answered by the empty-frontmatter exemption, in one line", and its verdict() passes when usableDeclarations(analysis).length is at least one for the change as a whole, not per package. The 6140 test is therefore a guarded file of a fixed-group package, and the gate asks that the change declare at least one changeset; .changeset/10373-fields-display-fls.md satisfies it (the head's Changeset Declaration check is green). A separate @object-ui/types entry, empty or otherwise, is not owed: a test-only change ships no behaviour, and the empty-frontmatter path is the answer only when a change has nothing else to declare.

③ Boundary flags

  • Closing keywords: Fixes #10373 is the only one in the current body and in the proposed body; none in the six commit messages or the title.
  • Commit messages, trailers and PR title: a regex scan for family and version tokens gives 0 hits in the title, the current body and the proposed body; in the commit messages the only hits are the standard co-author line's noreply address and the session-URL trailer, which carry no family or version token.
  • File surface, 10 files, exactly the claim plus both amendments: packages/fields/src/widgets/LookupField.tsx, RecordPickerDialog.tsx, PeoplePicker.tsx; the three pins beside them LookupField.displayFls-10373.test.tsx, RecordPickerDialog.displayFls-10373.test.tsx, PeoplePicker.expandFls-10373.test.tsx; .changeset/10373-fields-display-fls.md; amendment 1: .changeset/10223-lookup-candidates-expand.md, .changeset/lookup-dropdown-cell-renderer-5492.md; amendment 2: packages/types/src/__tests__/field-metadata-rows-option-description-6140.test.ts. Nothing outside it.
  • mergeable_state: clean (mergeable: true); the PR is still draft: true, state: open; base ref main.
  • Check-runs on 2322673f3de40ef3b0d1a2a2db49ef230cbb8b1a: 43 total, 40 success, 3 skipped, 0 failure, 0 in_progress. The non-successes are dependabot (skipped), Test (coverage) (skipped) and Test (coverage shard ${{ matrix.shard }}/4) (skipped). Polled in the foreground; the first pass found nothing pending, so no wait was needed. Combined commit status: success (one context). Five app-owned check-suites (hosting, deploy and fleet apps) carry no check-runs and sit queued; no GitHub Actions suite is pending.
  • Residuals for the seat, not breaches: the search-variant chip avatar read and the empty-label opt[displayField] fallback named in ①(a); the chip-hydration effect keeping the policy it ran under (disclosed).

Implemented-by: claude/issue-10373-fields-display-fls
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

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

Projects

None yet

2 participants