Skip to content

fix(plugin-detail): record:details stops reading requiredPermissions - #10279

Closed
objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/issue-10200-record-details-stop-reading-required-permissions
Closed

objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/issue-10200-record-details-stop-reading-required-permissions

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #10200
Clause-②: no

Ruling item 1 of objectui#10200 (maintainer ruling, comment 5815200174): record:details stops reading requiredPermissions. Ruling item 2 (the @objectstack/spec pin bump for enforceFieldSecurity / redactFields) is not in this PR and stays open on the card: no installable spec build declares those two keys yet. packages/types is untouched (acceptance item 3).

What changed

  • packages/plugin-detail/src/renderers/record-details.tsx: the requiredPermissions read and the block-level hasCapabilities gate it drove (the "Insufficient permissions to view details." notice) are removed. Nothing else the block does moves: the enforceFieldSecurity / redactFields fold, the !ctx placeholder and the hook sequence are unchanged (no hook added or removed; usePermissions() is still called and still read further down for getObjectApiOperations). A docblock at the old site says why the key is not read, and names the instrument that goes red the day the contract declares it on this block.
  • Tests, all under packages/plugin-detail/src/renderers/__tests__/:
    • record-blocks.requiredPermissions-gate.test.tsx: record:details leaves the gated-block table (record:highlights and record:related_list keep every existing pin). A new describe pins the ruling on a real MePermissionsProvider (details in the next section).
    • record-details.test.tsx: the rules-of-hooks permission-flip pin now asserts the body is still rendered after the flip, not hidden.
    • detailRendererUndeclaredKeys-8649.test.ts: record-details.tsx leaves the routed-key ledger for requiredPermissions and enters a new retired-read ledger that asserts the read is ABSENT from the file.
    • record-details.hideFieldsUncast-9965.test.ts: requiredPermissions leaves the honest-cast ledger, whose expiry leg asserts a cast read still exists; that is no longer true.
  • .changeset/10200-record-details-stop-reading-required-permissions.md, a patch bump.

safeParse table, re-run against the installed pin at the time of the work (acceptance item 2)

@objectstack/spec 17.4.0, resolved from this worktree's node_modules (from packages/plugin-detail), with RecordDetailsProps.safeParse executed:

input verdict
{ requiredPermissions: ['a'] } REFUSE, unrecognized_keys
{ enforceFieldSecurity: true } REFUSE, unrecognized_keys
{ redactFields: ['a'] } REFUSE, unrecognized_keys
CONTROL { hideFields: ['a'] } PASS
CONTROL { zzzNonsense: 1 } REFUSE, unrecognized_keys

Other readings from the same run:

  • ComponentPropsMap['record:details'] === RecordDetailsProps is true.
  • RecordDetailsProps.shape has no requiredPermissions.
  • Positive control on the spelling: RecordQuickActionsProps.safeParse({ requiredPermissions: ['a'] }) passes.

⇒ Item 1's premise still holds on the installed pin, and no row flipped. The two item-2 rows are still pin lag.

The pin, and proof that it can fail

New describe: "record:details — requiredPermissions no longer gates the block (objectui#10200)". It mounts the real stock MePermissionsProvider with a reported-empty capability set, so the provider genuinely does not hold crm.manage:

  • The first two pins render record:details and record:highlights with the same requiredPermissions: ['crm.manage'] under the same provider, in the same tree. The highlights block is the lit control: it is still refused, which proves the provider says no. The details body renders and shows no refusal notice. One pin uses a normal record context; the other uses an empty objectName, where the old gate also fired.
  • DETECTOR: a delegating hasCapabilities wrapper records that the details renderer never asks the capability path about the authored names, and never asks the object-action path either.
  • Wiring control: the gated sibling alone does fire that wrapper, so the negative above is a real reading.

Ablation. The fix was committed first, at b0451c5. The mutation went through ablation-replace.mjs, which proves on disk both the plant and the restore:

  • The removed read and gate were re-planted in place of the new docblock: anchor count 1 → 0, replacement count 0 → 1, blob 9c4f2d35ad06 → 410176e69d4a.
  • The same four test files were then run:
 Test Files  3 failed | 1 passed (4)
      Tests  5 failed | 67 passed (72)
  • Red, as intended: the three record:details inverse pins, the rules-of-hooks flip pin, and the retired-read ledger leg.
  • Green, as intended: the capability-spy wiring control and every sibling-block pin.
  • Restore proven: the blob after restore equals the HEAD blob (9c4f2d35ad06) and git diff HEAD is empty.
  • No dist/ sits in the resolution path: the tests import the renderer by relative path from src.

Verification (final head ed52015)

check exit reading
pnpm exec vitest run over 28 test files 0 28 files, 352 tests passed
pnpm --filter @object-ui/plugin-detail run type-check 0 tsc --noEmit && tsc -p tsconfig.test.json
eslint --format json on the 5 touched TS files, with the package config 0 5 files linted, 0 errors, 51 pre-existing warnings (no-explicit-any / react-refresh)
node scripts/check-changeset-presence.mjs 0 1 released package changed, 1 changeset declared
node scripts/check-changeset-no-major.mjs 0 no major
pnpm check:vi-mock-override-shape / check:vi-mock-inherit / check:vi-mock-specifiers 0 / 0 / 0 a vi.mock factory gained a hasCapabilities override
pnpm check:test-path-roots / check:control-bytes / check:new-line-citations / check:pending-changeset-literals / check:changeset-claims 0 each 0 new line citations; changeset-claims is report-only, see note 2
node scripts/check-governed-queue-guard.mjs --test over the 6 paths 0 NOT GOVERNED

Notes on the table:

  • The 28 test files are every test file that imports or reads the touched renderer or the touched tests, derived with git grep. They include readers in app-shell, core, types, plugin-form and plugin-kanban.
  • Type-check ran after turbo run build --filter='@object-ui/plugin-detail^...' (11/11 tasks, cache-restored). --listFilesOnly on tsconfig.test.json shows all 5 touched TS files are in the program.

Declared narrowings (the full farm is CI's):

  • Lint. Only the touched files were linted. Population, read from eslint's own API over the package: 256 of 262 tracked files (6 ignored), and all 5 touched files are in it. The file count comes from the --format json output. Invariance: eslint.config.js configures no type-aware linting (no parserOptions.project / projectService), and no eslint-rules/* rule reads the filesystem, so this diff cannot move a verdict on an untouched file.
  • Tests. The full plugin-detail suite was not run locally. Behaviour changes only when a record:details node authors a non-empty requiredPermissions, and the hook sequence is unchanged. The only tests in the tree that author that key on record:details are the ones edited here (git grep).

Acceptance notes

  1. Sibling blocks, same class, NOT changed here. The ruling scopes item 1 to record:details. record:highlights and record:related_list read requiredPermissions exactly as record:details did, and the installed pin refuses the key on both:

    • RecordHighlightsProps.safeParse({ fields: ['a'], requiredPermissions: ['a'] }) is refused with unrecognized_keys. The control without the key passes.
    • RecordRelatedListProps behaves the same way, again with a passing control.

    The spec's own docblock on RecordDetailsProps names the key as deliberately undeclared on all three blocks. Removing a security gate is the maintainer floor, so this PR leaves both untouched and reports them to the seat.

  2. Pending changesets whose prose this change narrows. Two unreleased changesets describe the old record:details behaviour. Neither is yet in packages/plugin-detail/CHANGELOG.md:

    • .changeset/10155-record-blocks-capability-gate.md: its headline names record:details.
    • .changeset/8649-detail-renderer-undeclared-keys.md: it says the key is "read by all three renderers".

    Both files are outside this claim's file surface, so this PR's own changeset states the supersession instead. Amending either body is the seat's call.

  3. Behaviour change for reviewers. A record:details node carrying requiredPermissions used to be hidden from a viewer without the capability. It now renders. Such a document is refused at publish by the pinned spec. No in-tree producer (examples, apps, app-shell synth) authors the key on this block (git grep), so the only reachable population is raw nodes that bypass the contract. Server-side record and field access is unaffected: this gate was a browser-side hide.


Generated by Claude Code

The contract's strict RecordDetailsProps deliberately does not declare
requiredPermissions, so a record:details document carrying it is refused
at publish, yet the renderer read it and hid the whole block behind it.
Per the maintainer ruling on the card, the read and the block-level gate
it drove are removed from record-details.tsx; everything else the block
does is unchanged.

Pins: a record:details node carrying the key renders its body under a
real MePermissionsProvider that reports the capability unheld, with
record:highlights in the same tree still refused as the lit control, and
a spy proving the capability path is never asked. The 8649 routed-key
ledger and the 9965 honest-cast ledger drop the retired record-details
entry; the 8649 ledger pins its absence instead.

record:highlights and record:related_list are untouched.

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

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 2 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/7997-detail-view-related-retired.md

  • names renderers/record-details.tsx → packages/plugin-detail/src/renderers/record-details.tsx — edited by this change

    Why it retired. @objectstack/spec declares no DetailView schema at all — every DetailView occurrence in packages/spec/src is prose about this repo's own RecordDetailView.tsx — so this array mirrored no protocol schema and drifted freely: it declared columns as TableColumn[] while the renderer it fed also accepted bare field names, { field, label } and legacy { name, label } spellings. The axis that carried the ruling was measured zero pull: no application code authored the member, both internal producers of a detail-view node (RecordDetailDrawer, renderers/record-details.tsx) synthesize it without related, and the only in-tree authorings carrying real columns were two documents — both rewritten here.

.changeset/8400-kanban-name-field-skip-set.md

  • names record-details.tsx → packages/plugin-detail/src/renderers/record-details.tsx — edited by this change

    Deliberately one rung, unlike the same dedupe in record-details.tsx, which also carries deriveTitleField: that ladder filters a synthesized field list, whereas this one filters an author-declared cardFields, where dropping a field the author asked for is a worse failure than a repeated title. A regression test pins both directions, including an object whose declared and derived pointers disagree.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 4ab4f1ba2 (merge-base with origin/main): 5 file(s) changed outside .changeset/, read against 1286 pending declaration(s) that publish a body (1855 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3037.3 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-DeSFix1b.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) 25.05KB 9.16KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 546.88KB 130.96KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 253.40KB 63.99KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 33.36KB 10.88KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.02KB 11.00KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.82KB 20.13KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.00KB 35.19KB
plugin-designer (index.js) 215.98KB 44.34KB
plugin-detail (index.js) 257.32KB 67.09KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 144.65KB 37.10KB
plugin-gantt (index.js) 167.99KB 41.37KB
plugin-grid (index.js) 215.46KB 58.88KB
plugin-kanban (index.js) 48.83KB 15.21KB
plugin-list (index.js) 113.90KB 28.11KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.18KB 21.05KB
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) 106.60KB 35.16KB
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.38KB 1.98KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.74KB 2.54KB
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) 15.71KB 5.30KB
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

…cord:details stops reading requiredPermissions

The objectui#10155 and objectui#8649 changesets are unreleased and publish
verbatim into the same release as this change. Narrow their
requiredPermissions claims to record:highlights and record:related_list,
with one clause each that record:details no longer reads the key. Their
frontmatter is untouched. This change's own changeset drops the
supersession sentence, which pointed at the text now corrected.

Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
Co-authored-by: Claude <noreply@anthropic.com>
…cord-details-stop-reading-required-permissions
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3037.2 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-DvuTtIEn.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) 25.05KB 9.16KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 546.87KB 130.97KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 253.41KB 64.01KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 33.36KB 10.88KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.02KB 11.00KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.82KB 20.13KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.00KB 35.19KB
plugin-designer (index.js) 215.98KB 44.34KB
plugin-detail (index.js) 257.34KB 67.12KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 144.65KB 37.10KB
plugin-gantt (index.js) 167.99KB 41.37KB
plugin-grid (index.js) 215.46KB 58.88KB
plugin-kanban (index.js) 48.83KB 15.21KB
plugin-list (index.js) 113.90KB 28.11KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.56KB 3.71KB
plugin-view (index.js) 85.18KB 21.05KB
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) 106.60KB 35.16KB
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.38KB 1.98KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.74KB 2.54KB
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) 15.71KB 5.30KB
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

This was referenced Sep 24, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Closed unmerged, per ruling A on objectui#10281 (comment 5823922380, maintainer 「19913 同意A」, recorded by the domain:spec seat 4).

The ruling holds that the protocol declaration wins: objectstack-ai/objectstack#18159 declares requiredPermissions on record:details as the ADR-0066 capability gate, fail-closed, and objectstack-ai/objectstack PR #19913 lands it. This PR implemented objectui#10200's item 1 (the renderer stops reading requiredPermissions on record:details). That item's premise was a spec docblock that predates the #18159 ruling and that #19913 removes, so item 1 is withdrawn and the renderer keeps reading the gate.

⛔ Not merged; the branch is left as is. No follow-up is owed from this PR.

domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C.


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…ormat in every record-title reader (ADR-0079 order) (objectstack-ai#10358)

Fixes objectstack-ai#9436
Clause-②: yes

This PR executes ruling C1 on this card: the ADR-0079 declared name
pointer outranks `titleFormat` in every record-title reader. The
authority is class-1 ruling comment 5657441402, ratified in comment
5814246926. The scope is the re-scoped claim, comment 5820165439: one
ruled order, three readers, one PR. LookupField is left out (see
Acceptance notes).

## The new public export (why `Clause-②: yes`)

`@object-ui/core` now exports **`declaredNameField(objectDef)`**. The
function already existed privately in `record-title.ts`, and its
behaviour is unchanged. It returns the object's declared record-title
pointer exactly as `getRecordDisplayName` reads it at steps 1+2:
`nameField`, then the deprecated `displayNameField` and `NAME_FIELD_KEY`
aliases. It returns `undefined` when none is declared, and it **never
derives**. Never deriving is the difference from `resolveNameField`, and
it is what lets a caller put the type-aware derivation on a lower rung
than its own `titleFormat` rendering. All three readers value it the
same way, `recordDisplayValueAt(record, declaredNameField(objectDef))`.
None of them re-types the `??` chain. The claim marks this export
`Clause-②: yes`, so a review-tier contract review is owed before
enqueue.

## Authority, as read

- `@objectstack/spec` 17.4.0 (installed), `titleFormat` describe:
"[DEPRECATED → nameField (ADR-0079)] Render-only title template; the
server cannot return or query it, and an explicit nameField now takes
precedence."
- `@object-ui/core` `getRecordDisplayName` docblock, step 3:
"`objectDef.titleFormat` (rendered template) — LEGACY, render-only, kept
for back-compat only; an explicitly-declared field (1/2) now wins over
it."

## `PageHeaderRenderer` title ladder, before and after

The new rung is inserted directly above the template. Nothing else
moves, and the template keeps the header's own interpolation (i18n
option labels, separator cleanup).

| # | Before (`main`) | After (this PR) |
|---|---|---|
| 1 | explicit `schema.title` | explicit `schema.title` |
| 2 | `titleFormat`, via the header's interpolation | **declared
pointer**: `nameField`, then `displayNameField`, when it holds a value
on the record. This is a new rung, filtered by the same floor test as
rung 4. |
| 3 | unified resolver: `nameField`, `displayNameField`, core
`formatTitleTemplate`, type-aware derivation | `titleFormat`, via the
header's interpolation (unchanged) |
| 4 | record-key rung, only for an object naming no title field |
unified resolver (unchanged) |
| 5 | `${objectLabel} ${id}` placeholder | record-key rung (unchanged) |
| 6 | | `${objectLabel} ${id}` placeholder (unchanged) |

On an object without a `titleFormat`, rung 2 answers exactly what rung 3
answered before. A pointer that is blank on the record falls through to
the template, just as `getRecordDisplayName` walks from a blank steps
1+2 to step 3.

## The other two readers of the same order

- **`DetailView.resolveDisplayTitle`**: `primaryField`, then **declared
pointer (new step 1b)**, then `titleFormat` (still step 2), then the
unified resolver, then `schema.title`, then the record-key probe, then
the floor. The rung is numbered 1b so that existing citations of "step
2" as the template step stay true.
- **`record:details` H1 dedupe**: when the declared pointer holds a
value, that field IS the H1. Its row is hidden and the template is not
consulted. Otherwise the objectstack-ai#8351 template scan and the value walk run
unchanged.

Why all three move together was measured on the stop report (comment
5820114435). With only the header moved, the synthesized page
(`page:header` + `record:details`) showed a duplicate row under an
identical H1 for a composite template. For a single-field template it
also hid a row the H1 no longer showed. `DetailView` rendered
`HT-2026-001 - Acme Corporation` over `nameField: 'contract_no'`.

## Upgrade effect: whose H1 moves

The H1 moves on any object that declares BOTH a `nameField` (or
`displayNameField`) and a `titleFormat` whose rendering differs from
that field's value.

- **objectui `examples/**` and `apps/**`**: zero objects declare
`titleFormat` at all. In-repo declarations are test fixtures only.
- **ObjectStack platform objects**, measured once at objectstack
`61609edf` and not re-derived by any gate here. The instrument was `git
grep -l` for a `titleFormat` key over non-test, non-md sources, then a
per-file co-occurrence scan for `nameField` / `displayNameField`.
  - 42 platform objects declare both.
- 26 have a single-field template equal to the pointer, so nothing
visible changes.
- **16 have an H1 that moves.** 11 of them move to another field's
value: `sys_import_job`, `sys_job_queue`, `sys_job_run`, `sys_session`,
`sys_setting`, `sys_migration_journal`, `sys_activity`, `sys_audit_log`,
`sys_comment`, `sys_sharing_rule` and `sys_webhook`.
- **The other 5 declare `nameField: 'id'`, so their H1 becomes the raw
record id**: `sys_approval_action`, `sys_approval_approver`,
`sys_approval_request`, `sys_automation_run` and `sys_http_delivery`.

This is a known consequence of the ruled order, and nothing in the
renderer works around it. The fix belongs to those objects' metadata in
ObjectStack: designate a formula field as `nameField`, as the
`titleFormat` describe itself advises for a composite title. The seat is
carding that in objectstack. The changesets for `@object-ui/components`
and `@object-ui/plugin-detail` state the same effect. The
`@object-ui/core` changeset names the export.

## Pins

- `packages/components/src/__tests__/page-header-title.test.tsx`: the
pin "titleFormat still outranks nameField (legacy header behaviour)" is
**rewritten, not deleted**, as "the declared nameField outranks
titleFormat (ADR-0079 protocol order, objectstack-ai#9436)". The H1 must equal the
pointer's value, and the template must render nowhere.
  - A second pin covers the `displayNameField` alias.
- Four controls: a blank pointer falls through to the template; the
template still outranks the type-aware derivation; the template keeps
the header's option-label interpolation (a select value renders as `In
Progress`, not `in_progress`, which also guards against consulting the
whole resolver above the template); an explicit `schema.title` still
wins.
- `record-details.titleFormatNoDedupe-8351.test.tsx`: the two old-order
cases are **rewritten, not deleted**. "HALF 1" now asserts the pointer's
row DROPS. "hides the row the DECLARED POINTER names, not the one the
template names" is the other.
- The objectstack-ai#8351 template outcomes (composite hides nothing, empty walks on,
collapse hides that row) now run on an object with no declared pointer,
where the template really is the H1.
  - A scan-not-peek case is added.
- `record-details.headerDedupeAgreement-9436.test.tsx`: the stop
report's probe, committed. It renders the real `page:header` H1 beside
the real `record:details` body, for a composite and a single-field
template, plus a no-pointer control.
- `DetailView.declaredPointerOutranksTitleFormat-9436.test.tsx`: two
pins (`nameField`, `displayNameField`) and three controls: no pointer, a
blank pointer, and the view-level `primaryField` still winning.
- `record-title.declaredNameField-9436.test.ts`: the export's contract.
It covers the alias order, never deriving (with a control showing that
`resolveNameField` would derive), and agreement with
`getRecordDisplayName` at steps 1+2.

## Ablations (one-shot, run at `96e1b800`, the commit holding
implementation and pins)

Each ablation used `node ../objectstack/scripts/ablation-replace.mjs`.
That tool requires the anchor to hit exactly once, verifies the mutation
on disk (anchor count 1 to 0, blob hash changed), then restores and
proves the restore (blob equals HEAD, `git diff HEAD` empty). Each run
covered the same five files, 38 tests. Vitest aliases these packages to
`src`, so no `dist` was involved.

| Ablation | Mutation | Red (predicted = observed) | Green |
|---|---|---|---|
| A1: header back to template-first | `declaredTitle \|\|` becomes `''
\|\|` | both header order pins, and both agreement cases (the H1
assertion) | 34, including every control |
| A2: dedupe back to template-first | the declared-pointer branch
becomes `if (false)` | both flipped objectstack-ai#8351 cases, and both agreement
cases (the body assertion) | 34 |
| A3: `DetailView` back to template-first | its declared rung becomes
`if (false)` | both `DetailView` pins | 36 |

Later commits added the changesets, removed one unused eslint directive
from the agreement test (a comment line), and merged `main`. None of
them touches the mutated source lines.

## Verification at `d02362e7` (final head)

- `turbo run build --filter='@object-ui/plugin-detail^...'`: 11/11
successful. Then `type-check` of `@object-ui/core`,
`@object-ui/components` and `@object-ui/plugin-detail`: exit 0. All five
test files were confirmed inside the `tsconfig.test.json` programs by
`--listFiles`.
- From the repo root: `pnpm exec vitest run packages/core/
packages/plugin-detail/` gave 367 files passed and 1 skipped (a
pre-existing timezone `skipIf`), 5460 tests passed. `pnpm exec vitest
run packages/components/` gave 289 passed and 1 skipped, 2809 tests
passed.
- `pnpm check:control-bytes` OK. `pnpm check:new-line-citations`: 0 new
citations. `node scripts/check-changeset-presence.mjs`: 9 source files
of 3 released packages, 3 changesets. `check-changeset-no-major`,
`-fixed`, `-overwrite`: OK. `pnpm check:pending-changeset-literals` OK.
`pnpm check:test-path-roots` OK.
- `pnpm check:changeset-claims`: report-only. It flagged 10 pending
changesets naming a touched file. Each paragraph was read, and none is
falsified: they concern tab walkers, action ids, name-space
`resolveNameField`, `NAME_FIELD_KEY` being read only inside
`record-title.ts` (still true: the export lives there), and
`interpolate()` rendering `titleFormat` (still true).
- ESLint, narrowed to the 9 changed `.ts`/`.tsx` files: 0 errors
(warnings only, from rule classes already present in these files).
- The population is eslint's own config: `ESLint.isPathIgnored` is false
for 9 of 9.
  - The file count is read from `--format json`: 9.
- Invariance: `eslint.config.js` sets no `parserOptions` /
`projectService` (no type-aware linting), and no rule under
`eslint-rules/` reads other files. So this diff cannot move a verdict on
an untouched file.
- NOT MEASURED: `pnpm check:readme-exports`. Its prerequisite is every
package's `dist`: its only findings were missing `dist` entries and the
vacuity floors those trip. This diff edits no README, and the `core` /
`components` READMEs had zero findings. CI runs it on a full build.

## Hunk fences

- `record-details.tsx`: this PR's hunks are the import block and the
dedupe region, far from the `@@ -139` / `@@ -197` hunks of objectstack-ai#10279.
- `DetailView.tsx`: this PR's hunks are the import line and
`resolveDisplayTitle`, far from the `@@ -934` hunk of objectstack-ai#8941.
- `packages/fields/` is untouched.

## Acceptance notes

- **LookupField `recordToOption`** has the same defect class: the
referenced object's `titleFormat` outranks its declared pointer in
lookup option labels. Per the re-scope it is ⛔ not folded here.
`packages/fields/` is held by objectstack-ai#10223, and the seat files it as its own
card.
- **A pre-existing gap in the objectstack-ai#8351 dedupe scan**, not moved by this PR.
The scan only matches values against its candidate list: the name
pointer, the derived field and six literal names. Take an object with NO
declared pointer whose template collapses onto a field outside that
list, for example `{contract_no} - {name}` with `name` blank. Its H1
reads the `contract_no` value while the `contract_no` row still prints
under it. This PR reproduced it while retargeting "LIT CONTROL B"; the
template-scan code that decides it is unchanged from `main`. That
control now collapses onto `name`, a candidate. The gap is reported for
the seat to card.

The session for this change is
`https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant