') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(plugin-grid): retire the FIELD-meta-dead `titleFormat` relational key by os-sam · Pull Request #7020 · objectstack-ai/objectui · GitHub
Skip to content

fix(plugin-grid): retire the FIELD-meta-dead titleFormat relational key - #7020

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6874-titleformat-relational-meta
Aug 31, 2026
Merged

fix(plugin-grid): retire the FIELD-meta-dead titleFormat relational key#7020
os-sam merged 1 commit into
mainfrom
claude/issue-6874-titleformat-relational-meta

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6874

Retires titleFormat from ObjectGrid's RELATIONAL_META_KEYS, in the same shape objectui#6711 used for reference_to_field on this same list: remove the key, record the measurement in the docblock beside the list, pin the absence at all three call sites, add a changeset.

What was measured

applyRelationalMeta copied eight keys off the object-schema field def onto every relational column's fieldMeta, at all three of generateColumns's column-building call sites. titleFormat was one of them and has zero readers on a FIELD meta.

This is a different kind of zero from objectui#6711's, and a stronger one. titleFormat is a real, live key with many readers — it simply has none on a field meta. The sweep did not fail to find readers; it found every member read of the identifier across packages/ and apps/ (tests included) and classified each one by its receiver:

ReceiverSitesSurface
objectDef / objectSchema / objSchemacore/utils/record-title.ts, components/renderers/layout/containers.tsx, plugin-detail/DetailView.tsx, plugin-kanban/ObjectKanban.tsx, plugin-calendar/ObjectCalendar.tsx, react/hooks/useRecordSearch.tsOBJECT schema
refObjectSchema?.titleFormatfields/widgets/LookupField.tsx:377OBJECT schema (the referenced object's, via getSchema(referenceTo))
param.titleFormatapp-shell/utils/paramToField.ts:181resolved ActionParamDef

Zero field metas. RecordPickerDialog and lookupColumnDisplay take it as a prop, and the repo's single titleFormat= pass is titleFormat={refTitleFormat} in LookupField — object-schema sourced. So copying reference_to is what makes titleFormat work on this path, and copying titleFormat onto the meta reached nothing.

Note app-shell/utils/resolveActionParams.ts:536 reads field.title_format — snake_case, a different spelling on a different surface — and its line 249 titleFormat is a key in the RESOLVED_ONLY_PARAM_KEYS diagnostic-message table, not a read.

plugin-dashboard/src/recordFields.tsx had already recorded this exact measurement as its reason for not copying the key into that seam, so the key was a measured no-op in two seams and had been retired from only one.

Must-answer: is there a THIRD reader-less key? Enumeration, not examples

No. All seven surviving keys have a measured FIELD-meta reader. For each key, the reader and the surface it reads from:

KeyFIELD-meta readersReads fromReader-less?
reference_tofields/src/index.tsx:1941 (LookupCellRenderer, off the cell's field prop); LookupField.tsx:256 (fieldMeta?.reference_to); UserField.tsx:48 (meta?.reference_to)FIELD metaNo
referencefields/src/index.tsx:1942; LookupField.tsx:256; UserField.tsx:48FIELD metaNo
display_fieldfields/src/index.tsx:1948; LookupField.tsx:252; UserField.tsx:49FIELD metaNo
id_fieldLookupField.tsx:254 (fieldMeta?.id_field || 'id')FIELD metaNo
description_fieldLookupField.tsx:253 (fieldMeta?.description_field ?? fieldMeta?.descriptionField)FIELD metaNo
lookup_filtersLookupField.tsx:270; UserField.tsx:53FIELD metaNo
lookupFiltersLookupField.tsx:270; UserField.tsx:53 (second arm of the same ??)FIELD metaNo

The first three are read on the cell path (getCellRenderer to LookupCellRenderer); the last four on the inline editor path, which is what makes them earn their place on this grid but not on the read-only dashboard seam.

The same sweep is the control against a blind instrument: it found a real reader for every surviving key, so its zero for titleFormat is a reading, not an artefact.

Reported, not acted on

  • lookup_filtersandlookupFilters are both on the list — one concept, two spellings, read at the same two sites as the two arms of a single ??. That is this repo's field || schema shape (objectui#3233). Left alone here per dispatch; flagging for triage.
  • The converse of this finding — keys the grid's own cell and picker read but never receive (displayField, reference_field, descriptionField, lookup_columns) — is already tracked as objectui#6875. Not duplicated.

Behaviour is provably unchanged

Not merely believed — three independent legs:

  1. Static: every member read of the identifier is enumerated above; none is on a field meta.
  2. Dynamic: the only computed access to the meta bag anywhere in @object-ui/fields or plugin-grid is applyRelationalMeta's own write (fieldMeta[key] = fieldDef[key]). No consumer bracket-reads or enumerates the bag (Object.keys/entries/values over it: zero hits), so there is no dynamic path by which the key's presence could reach a renderer.
  3. Falsifiability: because a reader-less key changes no rendering, a green suite proves nothing — so the pin was reverse-verified. Re-adding titleFormat to the array turned the new pin red in exactly the predicted way: 3 failed / 3 passed, one failure per call site, with the survivor-presence controls staying green. Mutation confirmed on disk before the run (injected-text count 1, original 0, blob hash moved off the HEAD blob); restore confirmed after (git diff HEAD empty and hash back to the HEAD blob).

Second item: two stale descriptions re-synced

objectui#6711 falsified these when it landed; both are comments only.

  • plugin-dashboard/src/recordFields.tsx — the docblock enumerating RELATIONAL_META_KEYS as nine keys including reference_to_field, and "the grid's nine".
  • plugin-dashboard/src/__tests__/lookupRelationalMeta-6694.test.tsx — "copies NINE keys" and the fixture comment "The six the grid also copies".

⛔ The assertions in that test are untouched and still pin the dashboard's own three-key boundary — the diff on that file is comment lines only. All six keys stay on its fixture on purpose: that seam's boundary does not move when the grid's list does.

plugin-grid's own relationalMetaCopySet-6711.test.tsx needed its survivor control narrowed from eight keys to seven — that is the control set legitimately shrinking with the list it controls, the same edit objectui#6711 would have made to a predecessor.

Verification

Run from the repo root (canonical invocation; no -- before paths), heavy runs serialized through the shared verify lock.

CheckCommandResult
New pin + updated pinpnpm exec vitest run packages/plugin-grid/src/__tests__/relationalMetaCopySet-6874.test.tsx packages/plugin-grid/src/__tests__/relationalMetaCopySet-6711.test.tsx2 files, 12 passed
Reverse verificationkey re-added, same pin re-run3 failed / 3 passed — one failure per call site, as predicted; restored and confirmed
plugin-grid (full)pnpm exec vitest run packages/plugin-grid/100 files, 920 passed
plugin-dashboard (full) + every titleFormat reader testpnpm exec vitest run packages/plugin-dashboard/ core/record-title{,.stepNumbering} fields/lookupCellDisplay{Name,Field} components/page-header-title react/useRecordSearch plugin-map/ObjectMap.markerTitle app-shell/paramToField types/object-schema-metadata-spec-derivation93 files, 935 passed
type-checkpnpm --filter @object-ui/plugin-grid --filter @object-ui/plugin-dashboard type-checkboth Done, exit 0
lintpnpm --filter @object-ui/plugin-grid --filter @object-ui/plugin-dashboard lintexit 0 — 0 errors (419 + 706 pre-existing warnings)
changeset guardpnpm changeset:checkNo changeset declares a major bump, exit 0
control bytespnpm check:control-bytesOK (scanned 5822 tracked text files), exit 0

All results above are from the final commit aa20ab010.

The type-check genuinely covers the new test file.packages/plugin-grid/tsconfig.json excludes **/__tests__/**, so a green tsc --noEmit alone would say nothing about it; the type-check script chains tsc -p tsconfig.test.json, and --listFiles on that project reports the new file as a program input (1 hit, out of 1757 program files). Confirmed rather than assumed.

Lint narrowing, declared. The repo-wide pnpm lint was not run; the two packages holding every changed file were linted in full instead. The narrowing is measured, not assumed: (1) the population comes from the repo's own wiring — git diff --name-only puts all changed files in exactly these two packages, and each package's lint script is eslint ., the same command CI's turbo run lint runs for them; (2) the counts are from full-package runs (419 and 706 problems), not a file subset; (3) eslint.config.js configures no type-aware linting (no project / projectService / parserOptions.project), so no rule reads cross-file type information and this diff cannot move the verdict on any file outside these two packages. CI runs the full farm regardless.


Generated by Claude Code

… key
`ObjectGrid`'s `RELATIONAL_META_KEYS` copied eight keys off the object-schema
field def onto every relational column's `fieldMeta`, at all three of
`generateColumns`'s column-building call sites. `titleFormat` was one of them
and has ZERO readers on a field meta.
This is a different zero from the one objectui#6711 acted on, and a stronger
one: `titleFormat` is a real, live key with many readers — it just has none on
a field meta. The sweep did not fail to find readers; it found every member
read of the identifier across `packages/` and `apps/` and classified each by
receiver. All of them are OBJECT schemas (`objectDef` / `objectSchema` /
`objSchema`, and `refObjectSchema?.titleFormat` in `LookupField` — the
referenced object's schema, which is what this grid's own inline picker reads).
The one remaining receiver is `param.titleFormat` on a resolved
`ActionParamDef`. So copying `reference_to` is what makes `titleFormat` work on
this path; copying `titleFormat` reached nothing.
Nothing renders differently, and that does not rest on the member sweep alone:
the only computed access to the meta bag in `@object-ui/fields` or
`plugin-grid` is `applyRelationalMeta`'s own write, so no consumer can pick the
key up dynamically.
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 seven survivors as the control against vacuity.
Also re-syncs the two `plugin-dashboard` descriptions of the grid's copy set
that objectui#6711 had already falsified — comments only; that seam's
assertions still pin its own three-key boundary and are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-CDO7jhxr.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.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.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)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
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)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 13:10
@os-sam
os-sam enabled auto-merge August 31, 2026 13:10
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit f9984c0Aug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6874-titleformat-relational-meta branch August 31, 2026 13:46
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): titleFormat in ObjectGrid's RELATIONAL_META_KEYS has zero FIELD-meta readers — every reader takes it off the OBJECT schema

2 participants

@os-sam@claude