Skip to content

feat(fields,app-shell,plugin-detail): a pointer whose target the platform deleted draws a tombstone in the declared option's label (objectui#12489) - #12665

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-12489-reference-states
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-12489-reference-states

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12489
Clause-②: yes

Step 4 of objectui#12489, the build ruling A item 3 calls for (ruling 6203737086): the record pointer renderer draws the tombstone from the option's own label when the platform asserts the target was deleted. It reads nothing and links nothing. RecordPreviewCard's "the caller renders it" rule is unchanged, and the pointer renderer is that caller.

What changed

  • @object-ui/fields: resolveRecordPointer(field, row, optionsOf?) reads @objectstack/spec's FieldSchema.referenceDeletedWhen: { field, value } beside referenceVia. When the row's sibling select holds the declared option, the returned RecordPointer carries deletedLabel: that option's label, looked up in the options the call site supplies. The predicate is evaluated here and nowhere else. With no optionsOf, or no option with that value, the pair comes back as before; the raw option value is never used as the text. The default face under RECORD_POINTER_CARD_TYPE draws the label in place of the record id.
  • @object-ui/plugin-detail: DetailSection passes the object field's referenceDeletedWhen and the sibling's options, translated with the same translateOptions call the grid already uses for select options. So the tombstone reads the way the option reads in its own cell, localized labels included. It adds no i18n key.
  • @object-ui/app-shell: the pointer face draws a pair carrying deletedLabel as a tombstone (data-record-pointer="deleted", a wastebasket icon, the label, in the card's frame) and does not mount RecordPreviewCard. It makes no record read and no definition read, draws no link and shows no id.
  • Docs: content/docs/guide/fields.md and packages/fields/README.md.

Clause-②: yes (widening). RecordPointer gains the optional deletedLabel. resolveRecordPointer's field parameter gains the optional referenceDeletedWhen, and the function gains an optional third parameter, optionsOf. Nothing narrows: every existing call and return shape type-checks and resolves as before. There is no new export.

The four states on the served page, against the sheet's pins

approvalRequestsDataSource.servedPageReferenceStates-12489.test.tsx uses the real RecordDetailView on the app-scoped record route, through ApprovalRecordReads, under the root SchemaRendererProvider as ConsoleShell mounts it, with objectstack's sys_approval_request_detail page copied whole. The request definition carries the services declaration (record_id declares { field: 'cancel_reason', value: 'record_deleted' }) and the contract's cancel_reason labels.

State Sheet pin Served page (measured)
readable recordLink: link one link to the record path, inside the card, named by the title
refused (403) recordLinkRefused-11878: no link, keeps the snapshot title no link; the card's cause-free "This record cannot be opened"; snapshot title not drawn (see notes)
deleted (platform-asserted) tombstone: "Related record deleted", no link, no bare id the same label, 0 target reads, no link, id absent from the tombstone and the Details tab
unresolvable unresolvableReference: "This record cannot be opened" the same text, no link, no id in the card

Controls: a cancelled row with no cancel_reason reads the target and draws the card; a 404 on a row the platform did not flag draws no tombstone. A zh-CN bundle draws the tombstone as the localized option label. The sheet (ApprovalsInboxPage.tsx) and its four pins are untouched.

Evidence

  • Test union at head b78ab082 (packages/fields/, packages/plugin-detail/, packages/app-shell/src/views/record-preview/, every approvalRequestsDataSource.* test, and the four sheet pins): Test Files 552 passed, 2 skipped (554); Tests 6937 passed, 21 skipped (6958).
  • Ablations, each confirmed on disk and restored to the HEAD blob with an empty git diff HEAD:
    • predicate forced false: 9 failed;
    • a target read made anyway in the tombstone branch: 3 failed (the zero-read pins);
    • label taken from the raw option value: 8 failed;
    • the call site skips translateOptions: 2 failed (the localized pins).
  • Closure build --filter '@object-ui/app-shell...': exit 0.
  • type-check: exit 0 for fields, plugin-detail, app-shell and console. Console needed five plugin dists built first; they are outside the closure.
  • Exit 0 for check:changeset-claims, changeset:check, check:test-path-roots, check:spec-symbols, changeset presence and new line citations.
  • eslint on the changed files: 0 errors.
  • NOT MEASURED: a live reading. Published @objectstack/spec@18.0.0-next.2 lacks the services declaration (objectstack PR #23798), so a live request page cannot show the predicate yet; the served-page harness is the evidence.

Acceptance notes

  • The refused state on the served page shows the card's cause-free text and no snapshot title, while the sheet keeps the title as plain text. A refused read still never yields a link. Left to objectui#12484.
  • The sheet's tombstone also shows record_title on its meta line; the served tombstone is the option label alone, per the ruling. Left to objectui#12484.
  • The served predicate is the single declared condition (cancel_reason = record_deleted). The sheet also checks status = cancelled; the contract declares cancel_reason present only on cancelled rows.
  • The page H1 (display_title) shows the raw record id in every state. objectui#12484 carries it.

Provenance: built by the cloud session session_01RXFSkyCWZHTfnnf5iKrQFN (claim 6214527805) and opened by the domain:ui seat 1 PM (session_011K9g23W67dy2zwnyzaoB6o) from that session's final message, because a cloud session cannot open a PR from its container. The seat changed nothing in the text except this provenance line, which replaces the dev's own one-line attribution.


Generated by Claude Code

…form deleted draws a tombstone in the declared option's label

resolveRecordPointer reads @objectstack/spec's referenceDeletedWhen beside
referenceVia: on a row whose sibling select holds the declared option, the
pair carries deletedLabel, that option's label from the caller's options.
DetailSection passes the key and the sibling's options translated as the grid
translates select options. The app-shell pointer face draws the tombstone in
that label without mounting RecordPreviewCard: no read, no link, no id. The
default text face draws the label in place of the id.

Pins: the rule and default face (fields), the face under the real details
grid (app-shell), and the four reference states on the served approval
request page against the inbox sheet's pins (console).

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 70 chunks) 3118.3 KB 3119.4 KB
Main entry chunk (gzip) 65.9 KB 350 KB
Entry file index-TlNiaeqI.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) 21.19KB 7.76KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.39KB 6.02KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
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) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 597.83KB 144.29KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.20KB 69.22KB
fields (index.js) 290.52KB 74.25KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.67KB 11.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.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) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
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.04KB 3.92KB
plugin-calendar (index.js) 54.94KB 16.05KB
plugin-charts (index.js) 84.97KB 23.36KB
plugin-chatbot (index.js) 201.89KB 48.13KB
plugin-dashboard (index.js) 146.15KB 39.55KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 263.07KB 69.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 180.92KB 46.73KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 270.08KB 74.80KB
plugin-kanban (index.js) 59.42KB 18.48KB
plugin-list (index.js) 131.40KB 33.50KB
plugin-map (index.js) 27.07KB 8.96KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 93.43KB 23.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

Live E2E (informational) red on b78ab082: not this PR's · domain:ui seat 1 · session_011K9g23W67dy2zwnyzaoB6o · 2026-10-16T12:40Z

Job 116974377685 dies in e2e/live/ci/start-backend.sh before any spec runs, on the same cold-cache npm crash (Cannot read properties of null (reading 'edgesOut')) that main's own bump PR #12648 hit. objectui#12649 tracks it; no fix exists yet, and the crash reproduces on a cold cache, so the seat takes no re-run. The full reasoning is on PR #12656 (6214488000). The lane is informational and blocks no merge; every other check is judged on this head as usual.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: b78ab082fae914af5f0c59338876ee9a5097e8bf
Local-runs: none

Face: the Clause-②: yes declaration on claim 6214527805 (objectui#12489 step 4; ruling A item 3, 6203737086, names the widening). The committed tree has no published schema and no governed path: check-governed-merges --pr reports NOT governed, +854/−16 over 11 files. Inputs: the diff, the card with its rulings, the cloud dev's report (on the card), and this head's check-runs (32 success, 3 skipped by design, Live E2E (informational) base-red with a matching signature, 6215702442).

① Derived judgments

  • @object-ui/fields, RecordPointer (exported, index.tsx:3198): gains the optional deletedLabel. Every existing producer and consumer still type-checks. A widening. Correct.
  • @object-ui/fields, resolveRecordPointer (exported, index.tsx:3217):
    • Its field parameter gains the optional referenceDeletedWhen: { field, value }, which matches the spec key's shape.
    • It gains an optional third parameter, optionsOf. The parameter types (ReferenceDeletedWhen, OptionsOf) are module-local aliases. They reach the published surface only structurally, through the exported signature, and are not exported by name.
    • Every existing call (resolveRecordPointer(field, row)) resolves exactly as before. With no optionsOf, no matching option, or a blank label, no deletedLabel is produced, so the raw option value is never shown.
    • A widening. Correct.
  • The predicate has one home. It is evaluated only in resolveDeletedLabel in @object-ui/fields. DetailSection passes the declaration and the sibling's translated options, and the app-shell face reads only deletedLabel. Correct, as ruling A requires.
  • @object-ui/app-shell (the pointer face, side-effect registered, not exported): on deletedLabel it draws DeletedRecordPointer and does not mount RecordPreviewCard. That means no target read, no definition read, no link and no id, and the zero-read pins hold this. Without deletedLabel, the card path is unchanged. Correct.
  • @object-ui/plugin-detail (DetailSection, the call site): it adds two arguments to the internal call, and no prop or export changes. Correct.
  • No i18n key is added: the label is the option's own label, translated through the grid's translateOptions.

② Semver level

@object-ui/fields minor, @object-ui/app-shell minor and @object-ui/plugin-detail minor. Each is additive and declares Clause-②: yes (widening) in words, and each changeset's prose matches its diff. Changeset Bump Policy and Changeset Fixed Group Check pass on this head. Consistent.

③ Boundary flags

The dev's four open questions:

  1. A. data-record-pointer="deleted" stays its own attribute. RecordPreviewCard's header defines its four states and leaves deletion to the caller.
  2. A. The refused state's missing snapshot title on the served page is left to objectui#12484. A refused read still never yields a link.
  3. A. The single declared predicate is accepted. Ruling A and the spec key are single-condition, and the contract puts cancel_reason on cancelled rows only.
  4. A. The tombstone is the option label alone, per ruling A. The sheet's record_title meta line goes with objectui#12484's page migration.

Out of scope, carried to objectui#12484: the page H1 display_title shows the raw record id in every state. Live reading: NOT MEASURED. Published next.2 lacks the services declaration (objectstack PR #23798); the served-page harness is the evidence.

Implemented-by: claude/issue-12489-reference-states
Reviewed-by: session_011K9g23W67dy2zwnyzaoB6o

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:ui seat 1 · session_011K9g23W67dy2zwnyzaoB6o · 2026-10-16T13:39Z

What was reviewed:

  • Head b78ab082fa, against objectui#12489 step 4 (ruling A item 3, 6203737086).
  • The claim 6214527805 (Clause-②: yes, mode:cloud, cloud session session_01RXFSkyCWZHTfnnf5iKrQFN) and the dev's report on the card.
  • The contract review of record, 6216747309 (TIER_DEFAULT, PASS, same head).
  1. Shape.
    • Fixes #12489 is the first line and the only closing keyword. Clause-②: yes starts a line, and the assignee is os-tesla.
    • 11 files. Inside the claim: RecordPointer / resolveRecordPointer / the default face in packages/fields/src/index.tsx, the DetailSection call site, the app-shell pointer face, the served-page pin, the tests beside, and three changesets.
    • Two docs files, content/docs/guide/fields.md and packages/fields/README.md, follow the implementation, and the seat accepts them. Their sentences were checked against the diff.
    • RecordPreviewCard.tsx and the side sheet with its four pins are untouched.
    • The surface is NOT governed, at 870 changed lines.
  2. Premises measured.
    • The predicate lives only in @object-ui/fields.
    • The tombstone face mounts no card, so there is no read, no link and no id.
    • The label is the option's own label, translated as the grid translates select options. No new i18n key is added.
  3. Gates (check-runs pinned to this head).
    • 32 success and 3 skipped by design, including Lint, Repo Gates, Type Check, Test (shard 1–16/16), Test (dist pins), Build & E2E and Bundle Analysis (eager closure 3118.3 KB of 3119.4 KB).
    • Live E2E (informational) is the base-red npm crash, objectui#12649 (6215702442).
    • The dev's four ablations each go red.
  4. The four states on the served page, measured on the served-page harness under the root SchemaRendererProvider:
    • readable, deleted and unresolvable match the sheet's pins;
    • refused keeps no snapshot title (the sheet keeps it as plain text). That is reported and carried to objectui#12484; a refused read never yields a link.
  5. Accepted deviations.
    • Live reading: NOT MEASURED. The services declaration (objectstack PR #23798) is not in published next.2.
    • The dev's open questions are all answered A in the contract review.
  6. Landing.
    • Ready, then auto-merge into the merge queue.
    • After the merge: verify by content, close the card through Fixes, strip pm:dispatched, and archive the cloud session.
    • Face 4, objectui#12490, stays pm:on-hold for the record:json release.

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