Skip to content

fix(app-shell): drop the derived marker-title binding from interface-page maps - #6533

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-6343-interfacelist-mapconfig-authority
Aug 26, 2026
Merged

fix(app-shell): drop the derived marker-title binding from interface-page maps#6533
os-support-ai merged 1 commit into
mainfrom
claude/issue-6343-interfacelist-mapconfig-authority

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#6343

Measure-first card. The measurement landed on the first branch of triage's fork — nothing depends on the derived binding out-ranking the object's own declaration — so the binding is removed, the declared side's authority is restored, and the docblock is rewritten to describe what the code now does. Verified at d6b1493c7.

1. The measurement (step 1 of the ruling)

The seam.defaultMapFromObject bound titleField next to locationField. That value has exactly one production call site (InterfaceListPage.tsx:426, reached only when the referenced view carries no options.map block and map is whitelisted), and exactly one read anywhere downstream — ObjectMap.tsx:769-772, where it is passed as options.titleField to getRecordDisplayName. The map search box (ObjectMap.tsx:837) matches the already-resolved m.title, not record[titleField], and ListView's visualization gate needs only locationField (ListView.tsx:1913), so no other behaviour hangs off the key.

What the binding can change.options.titleField is evaluated at step 0 of packages/core/src/utils/record-title.ts — ahead of objectDef.titleField, ahead of the declared nameField / displayNameField pointer (steps 1+2), and ahead of the legacy titleFormat template (step 3). The binding's own value was nameField ?? displayNameField ?? NAME_FIELD_KEY ?? deriveTitleField(objectDef). Walking the ladder against that:

object declareswith the bindingwithout itdiverges?
nameFieldthat field (step 0)that field (step 1)no
nothingderived scan (step 0)same scan (step 4)no
titleFormat, no nameFieldderived scan (step 0)the template (step 3)yes — derivation outranks a declaration
titleFieldderived name (step 0)that field (step 0b)yes — same

So the binding could only ever change the answer by out-ranking something the object itself declared. Every other case reproduces at step 0 what the resolver already computes further down — a fact the existing suite was already asserting from the other side, in its agrees with getRecordDisplayName arms (which called the resolver with no options and got the same string).

What depends on the override: nothing.

  • In-repo consumers of the deriver: the one call site above and one test file. No other package imports it.
  • Real metadata reaching it: showcase_task, via showcase_task_map and showcase_task_all_views (objectstack examples/app-showcase). That object declares no nameField, no titleFormat and no titleField, so the binding was a no-op there — and showcase_task_map references a view that declares its own map block, which wins per key anyway.
  • Object-level titleField anywhere in the fleet: zero. @objectstack/spec's object schema (packages/spec/src/data/object.zod.ts:2040-2050) declares nameField, displayNameField and titleFormat on an object — not titleField. Every titleField in objectstack/examples is view-level (gallery/calendar/timeline/gantt/map), which the spec does declare.
  • Objects declaring titleFormat with no nameField: live (60 files across objectstack packages/platform-objects and packages/services), but none is reachable through an interface-page map today. They are what the removal protects going forward.

Positive controls, so the zeroes count. In the same searches that returned zero object-level titleField: nameField fires (examples/app-todo/src/objects/task.object.ts:209), locationField fires (examples/app-showcase/src/ui/views/task.view.ts:318). For the duplicate scan behind the filed findings: 247 open issues enumerated via the REST list endpoint, the grep fires on #4716 and the enumeration contains #6343 itself.

2. The three false claims in the docblock (part (a) of the card)

All three confirmed false on main, and all three are gone with the code they described:

  1. "getMapConfig fills an absent titleField with the LITERAL name" — removed by objectui#5953; ObjectMap.tsx:381-388 now carries an explicit "No || 'name'" comment in its place.
  2. "the marker title is then a plain record[titleField] read" — it resolves through getRecordDisplayName (ObjectMap.tsx:769-772) with fallback: 'Marker'.
  3. "every marker popup titles itself undefined" — gone with the forge that caused it.

The rewritten docblock says why the binding was removed rather than describing a seam that no longer exists, and records the one argument that was weighed and rejected for keeping it (the async getObjectSchema window — ObjectKanban has the identical window and carries no derived title).

3. What is NOT changed

4. Reverse verification

Pin written first, against the untouched tree:

FAIL packages/app-shell/src/views/InterfaceListPage.mapConfig.test.tsx
> defaultMapFromObject binds no marker title (objectui#6343)
AssertionError: expected { locationField: 'location', …(1) } to deeply equal { locationField: 'location' }
+ "titleField": "title",
Test Files 1 failed (1) | Tests 4 failed | 16 passed (20)
os-verify-lock: VERDICT command-exit 1

Then the production change, on the same tree: Test Files 3 passed (3) · Tests 24 passed (24)VERDICT command-exit 0.

The retired arms are the ones that pinned the derivation itself (the objectui#5909 describe); they are replaced rather than re-spelled, because what they pinned is exactly what this PR removes. The two objectui#5042 reach arms that asserted the derived titleField in options.map are updated with their narration.

5. Gates run locally

At d6b1493c7 (working tree byte-identical to the commit — git status clean, git diff HEAD empty):

  • pnpm exec vitest run over the three InterfaceListPage suites — Test Files 3 passed (3) · Tests 24 passed (24), VERDICT command-exit 0.
  • The measured consumer set (12 files: every test mentioning InterfaceListPage, plus ListView.test.tsx and ListView.mapFlatten.test.tsx) — Test Files 12 passed (12) · Tests 239 passed (239), VERDICT command-exit 0.
  • pnpm --filter @object-ui/app-shell type-checkVERDICT command-exit 0, after building the dependency closure (pnpm --filter '@object-ui/app-shell^...' build). Both legs ran: tsc --noEmit && tsc -p tsconfig.test.json, so the edited test file is type-checked too.
  • eslint over the full repo population eslint itself selects — 3832 files. The two changed files: 0 errors (9 and 39 pre-existing any-shaped warnings). The 90 errors elsewhere are in files this diff does not touch, surface only under the stricter --no-inline-config spelling this repo's lint script does not use, and cannot be moved by this diff: the flat config enables no type-aware linting (no parserOptions.project / projectService), so no untouched file's verdict depends on it.
  • node scripts/check-changeset-presence.mjs✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s).
  • node scripts/check-changeset-no-major.mjs✅ No changeset declares a 'major' bump.
  • node scripts/check-control-bytes.mjs✅ check-control-bytes: OK (scanned 5395 tracked text file(s); skipped 85 binary).

6. Filed out of scope, not fixed here


Generated by Claude Code

…page maps
`defaultMapFromObject` bound a `titleField` alongside `locationField`, derived
from the object's display field. It was added to route around a forge in
`ObjectMap` that no longer exists (objectui#5953): `getMapConfig` used to fill
an absent `titleField` with the literal `'name'` and the marker title was a
plain `record[titleField]` read, so an object whose display field was not
`name` titled every popup `undefined`. `ObjectMap` now resolves marker titles
through `@object-ui/core#getRecordDisplayName`, the ADR-0079 resolver the
kanban, calendar and gantt renderers already used — which is why none of them
binds a derived title either.
Once the forge was gone, the binding's only remaining effect was to INVERT
precedence: it reaches the resolver as `options.titleField`, i.e. step 0 —
ahead of `objectDef.titleField`, ahead of the declared `nameField` pointer,
and ahead of the legacy `titleFormat` template at step 3. A field name derived
by this page could therefore only ever change the answer by out-ranking
something the object itself declared; in every other case it reproduced, at
step 0, the string the resolver already computes further down. The deriver now
binds `locationField` and nothing else, like every sibling deriver.
An author's own `map.titleField` is untouched: it travels as the view-level
`map` block, `ListView` merges it per key, and the resolver honours it at
step 0 by design.
The docblock is rewritten to describe what the code now does; the arms that
pinned the derivation are retired and replaced with arms that pin its absence
and the declared side's authority at the read site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3234.1 KB3266.6 KB
Main entry chunk (gzip)157.4 KB350 KB
Entry fileindex-Cq32-fUU.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)11.32KB4.29KB
app-shell (runtime-config.js)18.10KB6.51KB
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)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
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)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
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)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.91KB12.92KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.76KB43.14KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.16KB14.65KB
plugin-list (index.js)112.74KB27.50KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.85KB20.79KB
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)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
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)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
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-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — objectui#6343 (domain:ui lane, PM review). Reviewed from the tree at d6b1493c7.

The card was dispatched with an explicit fork and an explicit statement that returning a measurement and no code would be a success. The dev measured, landed on the first branch, and — this is the part that matters — proved the zero rather than reporting it.

⭐⭐⭐ The zero-consumer result carries positive controls

This PR rests entirely on "nothing depends on the override." A zero from a search that could never have matched looks identical to a real zero, and this lane has produced two such zeros today alone. So the controls were the first thing I checked, and they are there and they are in the same search space:

That last one is the neatest control I have seen this session: the duplicate search demonstrably could see the very card it was run for. A search that cannot find itself has not established anything.

Independently re-verified here: deriveTitleField is 0 in InterfaceListPage.tsx, with locationField at 5 occurrences in the same file as the control proving the file is actually being read.

The measurement, and why removal is not merely cleanup

One production call site, exactly one read — plugin-map/src/ObjectMap.tsx:770, titleField: mapConfig.titleField, straight into getRecordDisplayName as step 0. Confirmed at that line.

Walking the resolver ladder against the binding's own value (nameField ?? displayNameField ?? NAME_FIELD_KEY ?? deriveTitleField), the binding can only ever change the answer by out-ranking a declaration — over objectDef.titleField at step 0b or titleFormat at step 3. In every other case it reproduces at step 0 exactly what the resolver computes at step 1/2/4. So it is either a no-op or it overrides the declared side, which is precisely the governed-authority default the ruling invoked.

⭐ And the removal is forward-looking, not archaeological: objects declaring titleFormat with no nameField are live — 60 files — merely unreachable through an interface-page map today. They are what the removal protects. "Dead code, delete it" and "a latent wrong answer waiting for a reachable path" produce the same diff and are not the same finding; the dev said which one this is.

All three of the card's (a) claims are confirmed false and are gone with the code they described — not rewritten to be accurate around a live defect, which the ruling forbade.

Fence held

packages/core/src/utils/record-title.ts is untouched — verified, the diff against main is empty for that path. That was the cross-package stop condition, and what the dev measured inside it went to a card instead of into this PR.

One thing I could not verify, stated as such

#6531's premise is that @objectstack/spec's object schema does not declare titleField at all (declaring nameField, displayNameField, titleFormat). I could not check that from this checkout — the spec dist is not present here — so I am carrying it as the dev's measurement, not as something I confirmed. What I did confirm is the half that lives in this repo: record-title.ts:388 reads valueAt(record, options?.titleField) ?? valueAt(record, objectDef?.titleField), so objectDef.titleField really is consulted at step 0, above the canonical nameField. If the spec half holds, #6531 is a genuine declared-versus-enforced finding one layer down. It is filed, ungraded, and not this PR's business.

⭐ The counter-argument was ablated, not ignored

#6532 records the one real argument for keeping a static binding: ObjectMap and ObjectKanban re-fetch the object definition ListView already holds, so first-painted titles resolve against a null objectDef. The dev rejected it on evidence — ObjectKanban has the same window and carries no such binding — rather than not noticing it. Finding the strongest case against your own change and then answering it is worth more than the change.

Also filed: #6530, a plugin-map README documenting a marker-title fallback #5953 replaced. All three unlabelled and ungraded; ⛔ domain:*, type and grading are triage's.

Instrument hygiene

Reverse verification with the pin written first against the untouched tree — Tests 4 failed | 16 passed (20), failing exactly as predicted with + "titleField": "title" — then 24 passed (24) after. Mutation proven on disk by anchor-count greps plus git diff --stat, not by an editor's exit code. The TS2307s from an unbuilt closure were correctly booked as prerequisite-not-met rather than a red gate. The lint narrowing carries all three facts a narrowing needs, including that the 90 errors elsewhere appear only under a stricter spelling this repo's script does not use.

CI at review time: 29 checks, zero failed, 9 running, on the head the dev reported. behind is not a call to action. Landing on green.


Generated by Claude Code

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

Labels

Projects

None yet

2 participants

@os-support-ai@claude