Skip to content

fix(core,components): one titleFormat interpolator for the record title (#10447, #10446) - #10555

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10447-one-title-interpolator
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10447-one-title-interpolator

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10447
Fixes #10446
Clause-②: no — one renderer's private interpolation rebinds to the existing core authority, and core's empty test aligns with recordDisplayValueAt. No export, schema or accept set moves.

Folded pair, one branch, executed from the triage on each card (comments 5827587063 and 5827564760) and the PM claims 5828659772 / 5828664602. objectui#10446's core fix lands first, then the H1 rebinds, so no commit on this branch renders HT-2026-003 - in the H1.

Commits

  1. 4c140cb9c — objectui#10446, @object-ui/core. formatTitleTemplate judges each placeholder through the value half of recordDisplayValueAt, now the module-private displayValueOf: trim, then empty, with an expanded reference reduced to its display name. It used to test the raw value against null, undefined and the empty string on its own, so a whitespace-only value counted as resolved and the final whitespace collapse left its separator dangling. One implementation of the emptiness rule instead of two spellings of it. Exported signatures unchanged.
  2. ef988ac2f — objectui#10447, @object-ui/components. The record page H1's titleFormat rung hands the declared template (string or Expression envelope) to core's formatTitleTemplate and takes its answer, including core's empty "nothing resolved"; the rung's separate unresolved-token test is gone. cleanupTitleSeparators had no other caller and is deleted with its test file. The rungs' order (objectui#9436) does not move.
  3. 47536ebd8 — a typing fix inside the same helper: the declaration build refused a copy that re-widened to possibly-undefined (TS18048).
  4. b06b17020 — prose only: two pending changesets reconciled with the rung's one interpolator (see "Reconciled pending changesets" below), and this change's changeset names both.

Measured before the change: the two interpolators, token by token

Probe at base 6881e9e47: the real page:header H1 and the header's own rung (its interpolate plus cleanupTitleSeparators, plus its "contains a brace, so decline" test) against core's formatTitleTemplate, same record. "declines" means the rung handed over to the unified resolver, which renders core's way.

Token rule header rung core H1 before H1 after
{account} - {deal_no}, account expanded to Acme Q3-042 Acme - Q3-042 Q3-042 Acme - Q3-042
{a} - {b}, b whitespace-only A A - A A
{a}, {b}, b missing A, A A, A
Session — {a}, a missing Session empty Session next rung
Static title (no placeholder) Static title empty Static title next rung
{status}, select in_progress In Progress in_progress In Progress In Progress (kept)
{{a}} - {{b}} declines A - B A - B A - B
{ a } - { b } declines A - B A - B A - B
{first-name} {a} declines Ada L Ada L Ada L
value containing a brace declines the value the value the value
controls: dotted path, lookup with no display name, array, null, empty string, 0, Expression envelope, the separators · : –, a 3-part template with its middle empty equal equal equal equal

After the change every row where core answers non-empty has H1 equal to core (select rows: equal to core over the labelled record). Where core answers empty, the H1 walks on to the next rung, as getRecordDisplayName walks from its template rung.

Select option labels (H2): kept, without touching formatTitleTemplate's signature

  • The header routed a top-level select token through fieldOptionLabel; core has no such hook.
  • withOptionLabels builds a copy of the record whose select values read as their option labels, and the rung renders core over that copy. optionLabelFor is the one label lookup in the file, shared with interpolate, so the two readers cannot drift.
  • Like-for-like with the dedupe: only top-level scalars are mapped (null, undefined, expanded references and arrays stay as they are), and a mapped value is never empty because the label falls back to the raw value. So the same placeholders resolve on the copy as on the raw record, and record:details, which renders the template over the raw record, still decides about the field the H1 shows. Pinned in plugin-detail (the Status-row cases below).
  • In-repo census, with a lit control. Instrument: git grep -n -E "titleFormat['\"]?\s*[:=]" -- ':!*.test.*' ':!**/__tests__/**' ':!**/CHANGELOG.md'. The one object declaration it finds outside tests is content/docs/guide/building-crud-app.md's titleFormat: '{title}', a text field, so no in-repo titleFormat names a select field. Control: the same pattern over test files is non-empty and includes page-header-title.test.tsx's {status} select case. First-party producers outside this repo (objectstack platform objects, e.g. sys_audit_log with {action} · {object_name} and sys_member with {user_id} ({role})) do name select fields, and each of those also declares a nameField, so their template rung is reached only when that pointer is blank on a record. Re-derive with git grep -n titleFormat origin/main -- 'packages/**/src/**' in objectstack.

interpolate's callers (H3)

It keeps two callers, both page:header's own templates: title and subtitle. They are unchanged. The titleFormat rung was the third and no longer comes through it, so interpolate stays. Instrument: grep -n "interpolate(" packages/components/src/renderers/layout/containers.tsx.

Who the #10446 rule reaches (H4)

  • formatTitleTemplate is called by getRecordDisplayName's template rung (core), DetailView.resolveDisplayTitle and the record:details H1 dedupe (plugin-detail), and now page:header. @object-ui/app-shell re-exports it as formatRecordTitle.
  • getRecordDisplayName, so every resolver-backed surface: app-shell RecordDetailView; components page:header; core sort-values; fields index and LookupField; plugin-calendar ObjectCalendar; plugin-detail DetailView and RelatedList; plugin-gantt ObjectGantt; plugin-kanban ObjectKanban; plugin-list ObjectGallery; plugin-map ObjectMap; plugin-tree ObjectTree. Instrument: git grep -n "getRecordDisplayName(" -- 'packages/**/src/**' 'apps/**/src/**' ':!**/__tests__/**' ':!*.test.*'.
  • The existing record-title suites stay green, and every other test file that mentions titleFormat was run too (Gates below).

Pins, red on base then green on head

New files:

  • packages/core/src/utils/__tests__/record-title.whitespacePlaceholder-10446.test.ts
  • packages/components/src/__tests__/page-header-title.oneInterpolator-10447.test.tsx
  • packages/plugin-detail/src/renderers/__tests__/record-details.titleFormatOneInterpolator-10447.test.tsx

Red leg, from committed head ef988ac2f: both source files checked out at base 6881e9e47, new tests kept. On-disk proof before the run: displayValueOf 0 hits and the old empty test 1 hit in record-title.ts; withOptionLabels 0 hits and cleanupTitleSeparators 2 hits in containers.tsx. Result Tests 11 failed | 25 passed (36):

Restore leg: git checkout HEAD -- both paths inside an EXIT/INT/TERM trap, blob hashes equal to ef988ac2f's (d7e714385, 86a980376), git diff HEAD empty. 47536ebd8 differs from ef988ac2f only by the typing fix inside withOptionLabels, a file the red leg replaces with base, so the red and green numbers carry over. Green leg: the three files pass on head inside the package runs listed under Gates; the two render files alone read Tests 21 passed (21).

Gates

Code, type and test gates ran at 47536ebd8; the changeset gates were re-run on the prose commit b06b17020 (no code or test file moved between the two).

  • pnpm --workspace-concurrency=2 --filter "@object-ui/plugin-detail^..." build (the dependency closure, includes core and components): exit 0.
  • type-check of @object-ui/core, @object-ui/components, @object-ui/plugin-detail: Done ×3, exit 0. Each package's test program lists its new test file (tsc -p tsconfig.test.json --listFiles, 1 match per package).
  • pnpm exec vitest run packages/core/: Test Files 172 passed (172), Tests 3501 passed (3501).
  • pnpm exec vitest run packages/components/ packages/plugin-detail/: Test Files 507 passed | 2 skipped (509), Tests 4976 passed | 25 skipped (5001).
  • Every other test file that mentions titleFormat (app-shell, fields, plugin-dashboard, plugin-grid, plugin-map, react, types): Test Files 21 passed (21), Tests 218 passed (218).
  • pnpm --filter @object-ui/components run test:dist: Test Files 2 passed (2).
  • check-changeset-presence (at b06b17020): ✅ 7 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s): .changeset/10447-one-title-interpolator.md.
  • check:new-line-citations (at b06b17020): VERDICT new-cross-file-line-citations: 0 new citation(s).
  • check:control-bytes (at b06b17020): ✅ check-control-bytes: OK.
  • At b06b17020: check-changeset-no-major ✅ No changeset declares a major bump.; check-changeset-fixed ✅ All workspace packages are in the changeset fixed group.; check:pending-changeset-literals ✅ No test source names a pending changeset.; check-changeset-overwrite exit 0, report-only, listing exactly the two reconciled entries (2 modified, 0 deleted); check:changeset-claims exit 0, report-only, and the entries it still lists name the touched files for other aspects (reviewed; none describes the titleFormat rung's interpolation).
  • check:test-path-roots, check-vi-mock-override-shape: exit 0.
  • eslint on the six touched source and test files: 0 errors (warnings are the repo-wide no-explicit-any / only-export-components level). The package-wide pnpm lint is CI's run; not measured here.

Reconciled pending changesets

Two changesets still pending in the same release described the rung as rendered by the header's own interpolation. Each is reconciled in the form objectui#10533 / #10540 used, with its frontmatter byte-identical (md5 of the frontmatter block equal before and after: 3d9e2a1e59bc11924bb99ffd9eea81e3 and ce888ebde4729564f67bca69a7928743):

  • .changeset/9436-page-header-declared-pointer-order.md: item 3 now reads "rendered at this change with the header's own interpolation", and "At this change, objects that declare a pointer and no titleFormat, and objects that declare a titleFormat and no pointer, render exactly as before". A dated note follows: "Dated note, 2026-09-25 — the record title's titleFormat rung now renders through core's formatTitleTemplate — objectui#10447". It states that option labels are still translated, via withOptionLabels, and that the order is unchanged.
  • .changeset/9174-interpolate-fastpath-trim.md: "At this change page:header's title/subtitle (and the record-title titleFormat) all went through interpolate()". The same dated note follows, stating that title and subtitle still go through interpolate(), so that entry's fix holds for them unchanged.
  • .changeset/10447-one-title-interpolator.md names both entries.

Every other pending .changeset/*.md was re-grepped for present-tense claims about the header's own titleFormat interpolation or cleanupTitleSeparators, and none has one. Instrument: git grep -n -i -E "cleanupTitleSeparators|separator cleanup|interpolat|formatTitleTemplate|titleFormat" -- .changeset/. The nearest hits are about page:header's title (objectui#10117, #8923) or other readers, and still hold.

File surface

  • packages/core/src/utils/record-title.ts, and beside it the new packages/core/src/utils/__tests__/record-title.whitespacePlaceholder-10446.test.ts.
  • packages/components/src/renderers/layout/containers.tsx, and beside it the new page-header-title.oneInterpolator-10447.test.tsx, a docblock-only edit to page-header-title.test.tsx, and the deleted cleanup-title-separators.test.ts (its function had no other caller).
  • packages/plugin-detail/src/renderers/__tests__/record-details.titleFormatOneInterpolator-10447.test.tsx: new, test-only, outside the claims' listed surface ("tests beside each"). It is where page:header and record:details render together (the objectui#9436 and objectui#10360 agreement files live beside it), which the H5 pin needs. No plugin-detail source changes.
  • .changeset/10447-one-title-interpolator.md (patch on @object-ui/core and @object-ui/components), plus the two reconciled entries above.

Scope

  • objectui#10499 is not addressed here. The H1's row source is unchanged (ctx.data, the raw served row). This neither helps nor hinders that fix beyond one fact: the rung and record:details' dedupe now call the same function and differ only in the row they pass it.
  • objectui#10462 (action-entry params, same file) is untouched.

Acceptance notes

  • Behaviour change on the rung, by the one-authority rule: a template none of whose placeholders resolves no longer shows its literal text in the H1 (Session — {user_id} with no user used to read Session); the H1 walks on to the next rung.
  • A resolved placeholder now emits the trimmed string recordDisplayValueAt returns. After core's whitespace collapse this is visible only where a padded value sits against a non-space literal, for example ({code}).
  • DetailView.resolveDisplayTitle still renders the template over the raw record, so on the non-synthesized detail header a select token reads as its raw value where page:header shows the label. That predates this change and is read from source only, not measured through a running page.

Generated by Claude Code

…ordDisplayValueAt does (#10446)

A whitespace-only placeholder value counted as resolved, so
`{contract_no} - {name}` with a blank `name` rendered `HT-2026-003 -` in
`formatTitleTemplate` and in `getRecordDisplayName`'s titleFormat rung.

The value half of `recordDisplayValueAt` (trim, then empty; an expanded
reference reduced to its display name) is now a module-private
`displayValueOf`, and each placeholder goes through it. One implementation
of the emptiness rule instead of two spellings of it.

Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
Co-authored-by: Claude <noreply@anthropic.com>
…h core's formatTitleTemplate (#10447)

`page:header` rendered the legacy template with its own `interpolate` plus
`cleanupTitleSeparators`, while `getRecordDisplayName`, `DetailView` and
`record:details`' H1 dedupe call core's `formatTitleTemplate`. The two had
different token rules: an expanded lookup token rendered as nothing in the
H1 and as its display name in core, so `{account} - {deal_no}` read
`Q3-042` in the H1 while the dedupe compared `Acme - Q3-042` and left the
duplicate "Deal No" row printed under the heading.

The rung now hands the declared template to `formatTitleTemplate` and takes
its answer, including its empty "nothing resolved". The H1's translated
option label for a select token is kept by mapping select values to labels
on a copy of the record (`withOptionLabels`) before core renders it, so the
formatter's signature does not move; the label lookup is one helper
(`optionLabelFor`) shared with `interpolate`. `cleanupTitleSeparators` had
no other caller and goes with its test. `interpolate` keeps its two other
callers, the header's own `title` and `subtitle`. The rungs' order is
unchanged.

Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
Co-authored-by: Claude <noreply@anthropic.com>
…n build narrows it (#10447)

Spreading an `any` record into the `Record | undefined` copy re-widened it
to the declared type, and the declaration build refused
`copy[key] = label` with TS18048. The source record is now typed once.

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

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 8 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/6771-retire-body-child-list-dialect.md

  • names renderers/layout/containers.tsx → packages/components/src/renderers/layout/containers.tsx — edited by this change

    Non-rendering readers keep their arm on the same rule, and none of them renders anything: while a renderer still reaches stored body content, a reader that must see the SAME content keeps its arm, or the renderer draws what the reader cannot find. Those are the two tab-subtree walkers in renderers/layout/containers.tsx, app-shell's pageSchemaIntrospect (CONTAINER_KEYS) and PageBlockInspector (STRUCTURAL_PROP_KEYS, the inspector half of a stored properties.body), and the CLI's OBJECTUI_STRUCTURAL_KEYS — a file-IDENTIFICATION marker, where keeping body is what lets an old file still be recognised as an ObjectUI node and therefore refused, instead of silently not judged.

.changeset/7182-declared-action-ids-one-rule.md

  • names containers.tsx → packages/components/src/renderers/layout/containers.tsx — edited by this change

    New on @object-ui/types, beside actionRendersAt: the pure resolveDeclaredActionIds(elements, registeredActions), with the DeclaredActionsResolution / DeclaredActionsRefusal result types (the shape classifier stays module-internal: called with no registry, the function already returns the registry-independent verdict a renderer needs before its lookup). Both renderers call it; the whole-array switch in record-quick-actions.tsx and the per-element normalisation in containers.tsx are gone. The rule is closed: a string is an id, a non-null non-array object is an inline definition, and any other element (null, a number, a nested array) is refused at its index too. An all-id array resolves by name in authored order, first registration winning on a duplicate name; ids that name nothing are reported back with their index for the caller to warn about once its lookup has settled.

.changeset/7287-resolve-title-field-shared-ladder.md

  • names record-title.ts → packages/core/src/utils/record-title.ts — edited by this change

    ADR-0079 collapsed ~6 divergent record-title resolvers onto one — @object-ui/core's record-title.ts, whose header tells the "Untitled everywhere" story that produced it. plugin-detail grew one back. resolveTitleField now calls core's resolveNameField and nothing else, so the detail page, the list column and the lookup chip cannot disagree about which field titles an object.

.changeset/8155-app-root-residue-swept.md

  • names containers.tsx → packages/components/src/renderers/layout/containers.tsx — edited by this change

    Swept as a class, not as two coordinates. Every other place in this tree that stated app was a bound expression-scope root is corrected in the same change — the diagnostic copy and its byte-pin, the ambient-scope docblocks in @object-ui/react (SchemaRenderer, useExpression), @object-ui/core (ActionRunner.ParamDef.visible, RowPredicateOptions.scope), @object-ui/components (form.tsx, containers.tsx), @object-ui/plugin-detail, @object-ui/plugin-form (docblock and README), @object-ui/app-shell and the console app, plus fourteen test fixtures that transcribed the old bag with an app key. The fixtures in @object-ui/app-shell now call buildExpressionScope instead of transcribing it, so that pair cannot drift again.

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

  • names record-title.ts → packages/core/src/utils/record-title.ts — edited by this change

    A kanban card printed its record title twice — once as the card heading, once as the first row of the card body. ObjectKanban resolves each heading through ADR-0079's getRecordDisplayName, then builds a skip set so the title field's raw value is not rendered again as a card field. That skip set read objectDef.NAME_FIELD_KEY, a key nothing produces: @objectstack/spec@17's object schema declares nameField (canonical) and displayNameField (its deprecated alias), and NAME_FIELD_KEY occurs nowhere in the framework tree — this repo reads it only as the last rung of the compatibility ladder inside record-title.ts, and never emits it.

.changeset/8871-page-node-refuses-breadcrumbs.md

  • names core/src/utils/record-title.ts → packages/core/src/utils/record-title.ts — edited by this change

    What was measured, on this branch's base 93127bd6f. Zero readers, with a point-access probe rather than a bare word: on that base \.breadcrumbs scores 0 tree-wide (exit 1) against \.breadcrumb\b's 12 files tree-wide (10 under packages/) as the lit control. At head the same two probes read 16 and 13 and \.breadcrumbs is exit 0 over 4 files — every hit one of this branch's own four files (this changeset, the refusal pin, layout.ts, zod/layout.zod.ts) quoting the probe string, and the pin's own exclusions put head back at exit 1. The base reading is the measurement; the head reading is this branch's echo of it. The bare word would have lied — it also names Sentry's own unrelated concept (app-shell/src/observability/sentry.ts) and appears in two comments listing UI surfaces (core/src/utils/record-title.ts, layout/src/NavigationRenderer.tsx), so a bare probe reports five readers that do not exist.

.changeset/9436-core-declared-name-field-export.md

  • names record-title.ts → packages/core/src/utils/record-title.ts — edited by this change

    The function already existed privately in record-title.ts and its behaviour is unchanged. It is exported so that a caller which renders titleFormat itself can rank the declared pointer above the template and the derivation below it, without re-typing the ?? chain:

.changeset/record-picker-label-placeholder-i18n-5637.md

  • names renderers/layout/containers.tsx → packages/components/src/renderers/layout/containers.tsx — edited by this change

    KNOWN GAP, unchanged by this release: the sibling label read sites in renderers/layout/containers.tsx compose translateLabel(pickLocalized(…), language), and that second helper is not applied here — translateLabel and its KNOWN_LABEL_DICT are module-private to that file. Only the locale-map resolution lands in this change; a plain-English string label is still rendered verbatim in every language, exactly as before.

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 4a7ef0de5 (merge-base with origin/main): 7 file(s) changed outside .changeset/, read against 1440 pending declaration(s) that publish a body (2022 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3046.1 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-C4S3A0sW.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) 27.95KB 10.04KB
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.52KB 3.45KB
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.13KB 7.95KB
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) 545.27KB 130.41KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 257.41KB 65.16KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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.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.01KB 3.93KB
plugin-calendar (index.js) 51.38KB 14.60KB
plugin-charts (index.js) 74.95KB 20.88KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.25KB 61.40KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.50KB 37.51KB
plugin-gantt (index.js) 169.64KB 41.92KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.43KB 28.26KB
plugin-map (index.js) 23.82KB 7.42KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.31KB 21.78KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
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) 18.27KB 6.20KB
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

…ung's one interpolator (#10447)

`.changeset/9436-page-header-declared-pointer-order.md` (item 3) and
`.changeset/9174-interpolate-fastpath-trim.md` both publish in the same
release and described the record-title `titleFormat` rung as rendered by
`page:header`'s own `interpolate()`. Each sentence is scoped to "at this
change" and followed by a dated note: the rung now renders through core's
`formatTitleTemplate`, with select option labels still translated via
`withOptionLabels`. Frontmatter unchanged in both (md5 of the block equal
before and after). The objectui#10447 changeset names both entries.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3046.1 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-Cxo9wr3l.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) 27.95KB 10.04KB
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.52KB 3.45KB
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.13KB 7.95KB
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) 545.27KB 130.41KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 257.41KB 65.16KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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.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.01KB 3.93KB
plugin-calendar (index.js) 51.38KB 14.60KB
plugin-charts (index.js) 74.95KB 20.88KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.25KB 61.40KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.50KB 37.51KB
plugin-gantt (index.js) 169.64KB 41.92KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.43KB 28.26KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.31KB 21.78KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
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) 18.27KB 6.20KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b06b17020aba9d933e8e0f0eda7f1d437cda93f6

① Derived judgments

Inputs read. Cards #10447/#10446 bodies and all 4 comments (triage 5827587063 / 5827564760, claims 5828659772 / 5828664602). PR files (10), git rev-parse refs/review/pr-10555 = head. Merge-base 6881e9e47. Four commits in the order the claim requires: 4c140cb9c core (#10446) first, then ef988ac2f components, 47536ebd8 typing fix, b06b17020 changeset prose.

#10446 — exact rule, faithful extraction. Base recordDisplayValueAt body (5 lines after the guard) is byte-for-byte the new module-private displayValueOf; recordDisplayValueAt now returns displayValueOf(record[field]) after the same guard, so its other callers see no change. formatTitleTemplate replaces its own object → displayName + null/undefined/'' test with displayValueOf and emits the returned string. Exported signature unchanged (base and head both (titleFormat: string | { source?: string } | undefined | null, record: any): string; published 17.6.0 dist/utils/record-title.d.ts line 23 identical). Probe (node type-stripping over git archive of head and base core src, @object-ui/types stubbed) over 18 values — undefined, null, '', 3 spaces, 2×U+00A0 (bytes \302\240 confirmed in the test), padded, 0, false, expanded with name, expanded with blank name, bare {id}, [], ['a','b'], [{name}], Date, NaN, {name:0}, {name:false} — head: 18/18 agree with recordDisplayValueAt; base: 16/18, the two disagreements are spaces and NBSP (- K vs K), the card. Template probes: {contract_no} - {name} blank name → base HT-2026-003 -, head HT-2026-003; getRecordDisplayName same. ({code}) with ' X9 ' → base ( X9 ), head (X9) — the admitted trim.

#10447 — H1 rung. Head rung is formatTitleTemplate(objSchema?.titleFormat, withOptionLabels(data, objSchema, fieldOptionLabel, rawObjectName)); the envelope/string unwrap now happens in core (core reads both). titleCandidate order unchanged: explicitTitle || declaredTitle || interpolatedTitleFormat || unifiedTitle || recordKeyTitle (only the !includes('{') wrapper dropped). That wrapper's removal changes no H1: core emits a { only for a literal brace outside a placeholder ({a} { → A {, same on base and head), and on base that rung declined into unifiedTitle, whose step 3 is the same formatTitleTemplate call, so the final string was already core's. Row source const data: any = ctx!.data untouched; the 7 hunks of containers.tsx are import line, new helpers after translateLabel, interpolate docblock + body, cleanupTitleSeparators deletion, the rung, titleCandidate. No params code (#10462) and nothing of #10499 touched.

withOptionLabels like-for-like. Maps only top-level non-null non-object scalars whose field has an options array; null/undefined/objects/arrays/dotted paths pass through, so core judges them as on the raw record. Mapped value = fieldOptionLabel(...) → i18n resolve(), which returns a translation only when non-empty, else fallback = match?.label ? String(match.label) : raw; so a mapped value is non-empty whenever the raw value is, except the degenerate case of an option whose authored label is whitespace-only (then the copy's placeholder trims to empty while the raw resolves) — base interpolate produced the same drop via its whitespace collapse, so no regression. Dedupe side (record-details.tsx 597–617) renders formatTitleTemplate(titleFormat, data) over the raw record and hides the field whose recordDisplayValueAt equals the whole string; the head pins (T1: H1 In Progress, Status row hidden) show the same field is decided on. optionLabelFor is the one extraction and interpolate's refactor is equivalent (same three guards, same Array.isArray(options) gate, resolve() never returns undefined).

cleanupTitleSeparators deletion. git grep on base: 2 hits in containers.tsx (definition + the one rung call), the deleted test file, and two historical CHANGELOG lines; head: CHANGELOG only. Components entry chain is side-effect only (src/index.ts → import './renderers' → import './layout' → import './containers'); src/index.ts has no re-export of it. Published @object-ui/components@17.6.0: dist/index.js, dist/index.d.ts, dist/index.umd.cjs each 0 hits; the body is inlined into index.js (3 regex hits), and dist/renderers/layout/containers.d.ts:29 declares it, but package.json exports is "." and "./style.css" only (typesVersions null), so that path is unreachable. Not a published export.

interpolate callers. Head: exactly two, explicitTitle (line 1455) and subtitle (line 1462), both page:header's own templates; body unchanged except the extraction above.

Stop condition. formatTitleTemplate signature did not change (see above). Not tripped.

Admitted behaviour changes. (a) no-placeholder / nothing-resolved template no longer shows literal text, walks on; (b) resolved values trimmed. Both are exactly what core already did for getRecordDisplayName, DetailView.resolveDisplayTitle and the dedupe; triage 5827587063 ruled "the shared authority wins and the other rebinds and is deleted", so they follow the ruling and need none.

Pins. Read, not run. Core file: 4 + 11 cases; THE CARD at both doors, leading blank, spaces and NBSP rows are red on base for the card's reason (probe: - K); padded, 0, false, arrays, lookups green on both. page-header file: 1 + 10 + 1 + 4 cases; red on base: THE CARD (Q3-042 vs Acme - Q3-042), whitespace (H1 HT-2026-003 vs base core HT-2026-003 -), comma (base cleanup class lacks ,; core's SEPARATOR_CLASS has it), literal prefix; the double-brace / inner-whitespace / hyphenated-key / brace-in-value rows declined on base into core, so green as controls; all 4 select-label cases green on base. Plugin-detail file: 3 + 2, red on base for THE CARD and #10446; the 2 select cases green. 5 + 4 + 2 = 11 red, 25 green, matching the body's 11 failed | 25 passed (36).

Pending changesets. Frontmatter md5 base = head: 9436-… 3d9e2a1e59bc11924bb99ffd9eea81e3, 9174-… ce888ebde4729564f67bca69a7928743 (both as the body states). Edits are "at this change" scoping plus a ⚠️ **Dated note, 2026-09-25 — … — objectui#10447.** Later in this same release … block, the form main already carries (7 such notes, e.g. 6235-mergedsort-wrap.md, 8973-grid-orderby-normalize.md). Note contents true against the diff (rung through core, labels via withOptionLabels, order unchanged, title/subtitle still via interpolate). Re-grep of the other pending entries for cleanupTitleSeparators|separator cleanup|interpolat|formatTitleTemplate|titleFormat|whitespace-only|dangling: 10117:48 (author title through interpolate) still true; 8923:28 ('{name}' in title) still true; 8351, 10360, 8394 describe the dedupe / other bands and still hold. None falsified.

File surface. 10 files: record-title.ts + new core test; containers.tsx + new test + docblock-only edit to page-header-title.test.tsx + deleted cleanup-title-separators.test.ts; new plugin-detail test (test-only, outside the claims' "tests beside each" letter, needed for the triage pin "page:header beside record:details", mirrors sibling record-details.titleFormatPlaceholderDedupe-10360.test.tsx imports; acceptable); 3 changesets. No source file outside the claims.

② Semver level

patch on @object-ui/core and @object-ui/components, Clause-②: no. Core: one exported function's emptiness rule tightened to the documented authority, signatures unchanged — patch. Components: the removed function was a module-level export never reachable from the package entry (entry dist 0 hits; exports map blocks the deep path), so no published removal; AGENTS.md §9's "breaking ⇒ minor" does not engage. Changeset prose checked sentence by sentence against the probes and diff (Acme composite, comma stripped, Session — {user_id} walks on, select label kept, rung order unchanged, title/subtitle untouched, exported signatures unchanged): all true.

③ Boundary flags

  • CI on head: 42 check-runs; 32 completed (all success or skipped: e.g. Test (dist pins), Changeset Bump Policy, Changeset Declaration, Build & E2E, Lint-adjacent gates), 10 in_progress: Test shards 1–8, Type Check, Lint, Build Docs, Bundle Analysis, Spec Main Shape Gate, Doc Snippet Type Check, Skill Example Check. No failure recorded; not yet green.
  • Replacement body true against the diff: commit list and short SHAs match git log; restore-leg blob hashes d7e714385 / 86a980376 match git ls-tree ef988ac2f; 47536ebd8 diff is confined to withOptionLabels (5 lines); measurement table rows agree with the probes; caller census (getRecordDisplayName rung, DetailView, two record-details call sites, page:header, app-shell formatRecordTitle) matches git grep; reconciled-changeset md5s match. One imprecision: the census instrument as written hits 9 non-test lines (pass-throughs in resolveActionParams.ts, a JSX prop in LookupField.tsx, core's own signature), of which building-crud-app.md:128 is indeed the only literal template declaration — the conclusion holds, the "one hit" wording overstates the instrument.
  • PR assignee unset; dev report comment not posted (classifier refusals) — procedural, for the seat.
  • Residuals, not blockers: DetailView.resolveDisplayTitle still renders raw select values (pre-existing, stated in the body); an option whose authored label is whitespace-only would empty the copy's placeholder (pre-existing on base).
  • Nothing in the diff should stop landing; landing waits on the 10 in-progress runs.

Implemented-by: claude/issue-10447-one-title-interpolator
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 09:06
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 3261e64 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10447-one-title-interpolator branch September 25, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment