Repository navigation
fix(components): page:header resolves a lookup title candidate instead of rendering its expanded object - #10157
Conversation
…d of rendering its expanded object The record-chrome branch asked the unified ADR-0079 resolver with `deriveFromRecordKeys: false`, then re-spelled the rung it had just switched off as a raw `data?.name || … || data?.subject || …` chain. Raw `||` reads the STORED value, so for a lookup the truthy candidate is the expanded reference object — which went into JSX and took the whole header down with React #31. The breadcrumb resolves the same record through the same resolver WITHOUT that option, so its walk runs through `recordDisplayValueAt` and the embedded-object display chain. The copy is deleted and that rung is asked for instead of imitated. Also closed, both legs of the same divergence: - an object that NAMES its title field no longer borrows another field's value when that field is empty; it degrades to the existing `${objectLabel} ${id}` placeholder; - a whitespace-only value is empty here, as it is to the resolver. A non-string candidate is reduced through the same authority before rendering — a backstop, deliberately last, because alone it would only trade the crash for a header showing the wrong field. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
The first pin set did not separate the clauses. Restoring the raw walk
while KEEPING the defensive stringify left "did not crash" and "the
display name is on screen" both GREEN: the backstop resolves an
expanded reference through the same `displayNameOfEmbeddedObject`, so
for a lookup it renders a byte-identical heading.
Two pins now carry the separation, and the ablation is why they exist:
- the no-borrowing rule is pinned with a non-empty STRING in the
borrowable field, which no stringify can neutralize;
- the reuse rule is pinned behind the resolver's own `*_name` affix
rung, which the deleted raw chain never looked at.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
The comment claimed an author `title` that is an object reaches the title chain. It does not: `pickLocalized` sits in front of every author-supplied title, is typed to a string, and collapses an object with no string value to `''`. Deleting the backstop leaves every pin green, which is the honest reading and is now written down on both sides instead of implied away. The guard stays — it is defence against a future rung — but it is described as that rather than as a live hazard. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
|
changeset-claim-re-read
|
✅ Console Performance Budget
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
Size Limits
|
ACCEPT — PR #10157, card objectui#10117Reviewed by the ⭐ The diagnosis is sharper than the card's, and this seat re-derived it on
|
Fixes #10117
Clause-②: no — a renderer-internal title resolution, entirely inside
PageHeaderRenderer's record-chrome branch. It adds no export, moves noexported symbol and no key on a published payload, and touches none of the
eight published-contract fields on any
package.json. The two symbols it nowimports,
recordDisplayValueAtandresolveNameField, were already exportedfrom
@object-ui/coreand already have other consumers.Clause-②-correction: this is the declaration the claim comment (id
5751061454) deferred for want of a diff. It is ⛔ not a secondClaim:.Step 1 — the real chooser, and where the card's reading was off
The card's 「Where」 section was explicit that it was read off a minified
plugins-viewschunk, so it was treated as a lead. In source the chooser isthe
resolvedTitlechain insidePageHeaderRenderer's record-chrome branch(the
hasRecord && !disableRecordChromeblock) inpackages/components/src/renderers/layout/containers.tsx. Cited by symbol, notby line address, per AGENTS.md #11.
Two corrections to the relayed reading, both from the source:
nameField/displayNameFieldare not read by the chooser at all. Theyare read one rung above it, inside
getRecordDisplayName— the unifiedADR-0079 resolver in
@object-ui/core(record-title.ts) — which the headercalls with
deriveFromRecordKeys: false.name, full_name, title, subject, display_name; the source also walkedlabel, and below it sat two further rungs the card did not mention:${objectLabel} ${id-prefix}and a bareobjectLabel.⭐ What the header was really doing, which is sharper than "it walks some
candidates": it switched the resolver's record-key rung OFF with
deriveFromRecordKeys: false, and then re-spelled that very rung itself as araw
||chain.recordDisplayValueAt's own header (objectui#8350) says in asmany words not to re-spell that test at a call site. This header was the
remaining re-spelling, and raw
||diverged from it on both of thatfunction's documented clauses:
lookupthe truthy candidate is theexpanded reference object — which went into JSX and took the block down;
record with
name: ' 'rendered a blank H1 — a second live defect inthe same chain, found by the repro pin rather than by the card.
⭐ The control the card supplied was the key, and it held. The breadcrumb
resolves the same record through the same resolver without
deriveFromRecordKeys: false—getRecordDisplayName(objectDef, pageRecord)in
RecordDetailView's record-title effect — so each of its rungs runs throughrecordDisplayValueAtand on intodisplayNameOfEmbeddedObject. That is whythe breadcrumb showed the business unit's name while the header beside it
crashed. The resolution was never missing from this tree; the header simply had
a copy of the walk instead of a call to it.
The fix — all three clauses
safety net now asks
getRecordDisplayNamefor its own step-4b rung. That isthe resolution the breadcrumb and the detail grid use; no second one was
written.
nameField/displayNameField, or a type-aware derivation over itsfields, which is exactly what the exportedresolveNameFieldanswers —has already said which field titles a record. An empty value there is an
empty title, and the header degrades to its existing
${objectLabel} ${id-prefix}placeholder instead of hopping to the nextcandidate. The silent hop was part of the defect, not just its rendering.
The record-key net still runs in full for an object that names no title
field, which is the case it exists for.
reading below.
Pins, and what ablation actually measured
Six pins in
packages/components/src/__tests__/page-header-title.emptyNameLookupFallback-10117.test.tsx.Before the fix, on the base commit: 5 failed, 1 passed — the one that
passed is the control, which is what makes it a control. The failure carried
the card's
args[]key list verbatim.Restoring the raw walk while KEEPING the defensive stringify left "did not
crash" and "the display name is on screen" both green: the backstop
resolves an expanded reference through the same
displayNameOfEmbeddedObject,so for a lookup it renders a byte-identical heading. Two pins were added to
separate the clauses, and they exist because that ablation came back green:
The clause-2 pin puts a non-empty string in the borrowable field, so no
stringify can neutralize it. The clause-1 pin puts the value behind the
resolver's own
*_nameaffix rung, which the deleted raw chain never looked atand which no stringify can reach.
papered over. With clauses 1 and 2 in place no rung of the chain can produce
a non-string:
pickLocalized, which every author-suppliedtitlepassesthrough first, is typed to a string and collapses an object with no string
value to the empty string. So there is no honest fixture for the backstop
through the registered component's public surface. It is kept as a guard
against a future rung and described as exactly that in the code, rather than
as a live hazard. A contrived pin would have asserted nothing.
Every ablation ran through
ablation-replace, so each mutation's landing isproved by an anchor count and a blob-hash change on disk, and each restore by
blob == HEADplus an emptygit diff HEAD— not by an exit code.Scope
Confined to the header's title resolution and its fallback. ⛔ No hands-off path
was touched: no
packages/i18n/src/locales, nopackages/plugin-calendar, nopackages/plugin-formsanitize/autoLayout/masterDetailTx, norecord-quick-actions, no app-shell publish-drafts call site,no
content/docs/releases, and nothing inscripts/check-eager-closure-budget.mjsor any ceiling in it. Theaction-
paramsdialog control worked on objectui#10129 was never reached — thischange does not enter a param or field renderer.
vitest.config.mtsgains one entry: the new pin renders through the REALpage:headerregistration, so it belongs in thedom-heavyproject. Under thelight
domsetup nothing registers that type and the file reportedpage:header not registered.🤖 Generated with Claude Code
https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Generated by Claude Code