Skip to content

fix(plugin-grid): retire the dead reference_to_field relational meta key - #6876

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6711-drop-dead-relational-meta-key
Aug 30, 2026
Merged

fix(plugin-grid): retire the dead reference_to_field relational meta key#6876
os-sam merged 2 commits into
mainfrom
claude/issue-6711-drop-dead-relational-meta-key

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#6711

ObjectGrid's RELATIONAL_META_KEYS listed nine keys that applyRelationalMeta copies off
the object-schema field def onto every relational column's fieldMeta, at all three of
generateColumns's column-building call sites. reference_to_field is removed from that
list. Nothing renders differently — the key had no readers — so the value of this PR is in
the measurement, and the pin that keeps the absence from being re-added silently.

The sweep, re-run on this branch's base (2c1c967)

Whole-repo, excluding node_modules / dist / .git, the identifier reference_to_field
occurred in exactly five places, none of them a read:

packages/plugin-grid/src/ObjectGrid.tsx:434 the array literal (the write)
packages/plugin-dashboard/src/recordFields.tsx:97,116 prose recording that nothing reads it
packages/plugin-dashboard/src/__tests__/lookupRelationalMeta-6694.test.tsx:261,279 fixture
.changeset/6694-dashboard-lookup-reference-meta.md:44 prose

No member access, no destructuring, no bracket read. The same sweep across the producer repo
(objectstack, packages/ + apps/) finds zero occurrences of the identifier at all, so
nothing on either side of the seam writes or reads it.

The eight-mate control, re-measured, is what makes that zero a reading. Member-read
counts for the other eight entries, same instrument, same base:

key.key member readsa real reader
reference_to57LookupCellRenderer, FilterConditionField, UserField
reference81LookupCellRenderer, UserField, GridField
display_field14LookupCellRenderer, LookupField:252, UserField:49
id_field7LookupField:254
description_field2LookupField:253
lookup_filters6LookupField:270, UserField:53
lookupFilters5LookupField:270, UserField:53
titleFormat15see below — none of them off a FIELD meta
reference_to_field0none

⚠️One control cell does not read as the card assumed, and it is worth saying.
titleFormat has 15 member reads but not one of them is off a field meta — every reader
takes it off an OBJECT schema (refObjectSchema?.titleFormat in LookupField:377,
objectDef?.titleFormat in core/utils/record-title.ts, objSchema?.titleFormat in
containers.tsx, the ADR-0079 chain in kanban / calendar / gantt / map / detail).
plugin-dashboard/src/recordFields.tsx already records that same measurement as its reason
for excluding the key from its own copy set. So titleFormat is a second dead entry in this
list of a slightly different kind. It is deliberately NOT removed here — that is a second
adjudication, filed as #6874 — but the measurement is written into the docblock beside the
list so the next reader is not misled by this PR's own control table.

The stop condition was checked before deleting

reference_to_field is an ad-hoc key on an untyped bag, not a member of a declared,
published type:

  • applyRelationalMeta(fieldMeta: Record, fieldDef: Record) writes into Record<string, any>
    (the three call sites each build const fieldMeta: Record<string, any> = ...; none of them
    spreads the field def wholesale, so this list is the only road onto the bag).
  • The bag reaches renderers as field={fieldMeta as any}, and the declared prop type is
    CellRendererProps.field: FieldMetadata (packages/fields/src/index.tsx:315).
    FieldMetadata does not declare reference_to_fieldLookupFieldMetadata declares
    reference_field, a different key — and BaseFieldMetadata carries no index signature.
  • @objectstack/spec@17.2.0's FieldSchema does not declare it either: it is in none of that
    schema's 64 props, and the string does not occur anywhere in the installed package.

⚠️ The cheap check the card proposed — "if the spec does not declare it, no field def can
carry it, so the copy is unreachable" — does NOT hold as stated, and this PR does not rely on
it. Six of the eight surviving mates (reference_to, display_field, id_field,
description_field, lookup_filters, titleFormat) are equally absent from the spec's 64
props and are demonstrably live at runtime, because the object schema the grid reads is the
objectql runtime shape rather than the spec's authorable one. Spec absence is corroboration
here, not the discriminator. The discriminator is the zero-reader sweep.

Verification (all at 099ff6a unless noted)

  • pnpm exec vitest run packages/plugin-grid/98 files, 901 tests, all passed
    (VERDICT command-exit 0). Run at 9be75d3, whose packages/** tree is byte-identical to
    099ff6a; the only later commit adds .changeset/6711-retire-reference-to-field.md.
  • pnpm exec vitest run packages/plugin-grid/src/__tests__/relationalMetaCopySet-6711.test.tsx
    — 6 passed, re-run at 099ff6a (VERDICT command-exit 0).
  • pnpm --filter @object-ui/plugin-grid run type-check — green (VERDICT command-exit 0).
    Both projects run: tsc --noEmit for src and tsc -p tsconfig.test.json, which is the one
    that compiles the new test file (include names src/**/*.test.tsx).
  • pnpm check:control-bytes — OK, 5728 tracked text files.
  • pnpm changeset:check plus node scripts/check-changeset-presence.mjs — all green;
    2 source files of 1 released package changed, 1 changeset declared.
  • pnpm check:phantom-deps, check:vi-mock-specifiers, check:self-import — all green.
  • eslint, narrowed to the two changed files with --no-inline-config --format json:
    2 files linted, 0 errors (213 + 7 pre-existing-style no-explicit-any warnings; the
    lint workflow deliberately sets no --max-warnings). The narrowing is sound because
    eslint.config.js enables no type-aware program (no project / projectService), so a
    diff confined to two files cannot move any untouched file's verdict.
  • Dependency closure built before type-check (turbo run build --concurrency=2 --filter=@object-ui/plugin-grid^..., 12 tasks), because tsconfig.test.json sets
    paths: {} and resolves @object-ui/* through built .d.ts.

The ablation, in the useful direction

A green suite proves nothing here: with no readers, removing the key changes no behaviour, so
every test stays green whether or not the removal is right. What is load-bearing is that the
new pin FAILS when the key comes back.

Measured: the key was re-inserted into RELATIONAL_META_KEYS on a committed tree, and the
mutation was confirmed on disk before the run — array-entry count 0 to 1, blob hash
012b1ac to b12b7f2 — not by the editor's exit code. Result:

× does not copy it (ListColumn objects)
× does not copy it (string columns)
× does not copy it (inline data + fields projection)
Tests 3 failed | 3 passed (6)

All three absence assertions red, one per call site, while the three control assertions
stayed green — so the red is specific to the re-added key, not a broken fixture. Restore leg
proven the same way: git checkout HEAD -- ... with an absolute path under a trap, then
git diff HEAD empty and the restored blob hash equal to the HEAD blob (012b1ac).

No rebuild step is involved on either leg, and that is a property of this repo rather than an
omission: vitest.config.mts aliases every @object-ui/* specifier to packages/*/src, and
the test imports ../ObjectGrid relatively, so both legs read source and never dist.

The pin, and why it is shaped this way

packages/plugin-grid/src/__tests__/relationalMetaCopySet-6711.test.tsx replaces the
registered lookup cell renderer with a probe (getCellRenderer checks the registry first,
so the probe receives exactly the bag the shipped renderer would get) and drives all three
column-building shapes: ListColumn objects, a string array, and the inline-data path with an
authored fields projection.

Each case asserts the absence AND that the eight surviving keys are present. The presence
half is not decoration — the first draft of this test passed vacuously, because the object
schema arrives from an async fetch and the first paint hands the renderer a bare
{ name, type } on which the absence assertion is trivially true. The fixture now waits for
label (written from the same objectDefField block immediately before
applyRelationalMeta, and not itself under test) before measuring.

What this does not establish

The sweep bounds this repo and the producer repo. A host application outside them could be
reading reference_to_field off the fieldMeta its cells receive; that was never a promise
this renderer declared, but the world was not measured, and such a host gets undefined
after this change. The changeset says so in the same words.

Card relationships

Generated by Claude Code


Generated by Claude Code

…a key
`ObjectGrid`'s `RELATIONAL_META_KEYS` copied nine keys off the object-schema
field def onto every relational column's `fieldMeta`, at all three of
`generateColumns`'s column-building call sites. `reference_to_field` had ZERO
member reads anywhere in this repo or in the producer repo — no member access,
no destructuring, no bracket read — and `@objectstack/spec`'s FieldSchema does
not declare it, so nothing authorable emits it either.
The control that makes that zero a reading: the same sweep over its list-mates
finds real readers for each of them in `@object-ui/fields`.
Removing a key nothing reads changes no rendering, so the suite proves nothing
by staying green; the absence is pinned instead, at all three call sites, with
a presence assertion on the eight survivors as the control against vacuity.
Same defect class the sibling producer retired twice (objectui#6625
`decimals`, objectui#6597 `referenceTo`). Measured for objectui#6711.
Patch bump for `@object-ui/plugin-grid` — the copy set no longer writes
`reference_to_field` onto a relational column's fieldMeta. Records what the
sweep bounds: this repo and the producer repo, not host applications.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.7 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CFkK9q8O.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)511.68KB116.36KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 09:03
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 19f3637Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6711-drop-dead-relational-meta-key branch August 30, 2026 09:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(plugin-grid): ObjectGrid copies reference_to_field onto every column's fieldMeta, and nothing in the repo reads it

2 participants

@os-sam@claude