…ng (objectui#8695)
`LookupCellRenderer` split ONE epistemic state — a reference this screen did
not resolve — by the SHAPE of the string. `isLikelyOpaqueId` sent
opaque-looking ids to a muted `—` and everything else to confident bare text.
Re-measured on this base with `reference_to: 'sys_user'`: `'Ada Lovelace'`
rendered byte-identically to what a `text` cell prints for the same string, so
the screen stated a confident fact it did not have and a dirty row read exactly
like a clean one; `'01HQZX9K2M4N6P8R'` rendered a muted `—`, destroying the raw
id that objectui#8434's triage named as the only clue for diagnosing an
existing dirty row. Two opposite failures, one state.
Both arms now render objectui#8434's remedy: the raw value kept visible beside
a stated, epistemic marker. `master_detail`, `tree` and the multi-value chip
shape route through the same answer.
The sentence is a SIBLING i18n key, not the `user` one: that pack value ends
"was not resolved to a user", false on a lookup pointing at any other object
and pinned byte-for-byte by two existing tests.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Part of #8695 — ⛔ deliberately not a closing keyword. The remaining half is a ruling, named below, and this PR must not silently close a card that is waiting for one.
🛑 The stop-and-report fork FIRED. This PR stays in draft.
The card raises the fork against itself:
lookuphas "its own already-shipped affordance and its own trade-off", and if objectui#8434's remedy would break that affordance, the mother reasoning is branch-specific and the card returns for its own ruling.It does, and the break is not the em-dash going away — that was always what a transplant means. It is that a second, independently-shipped ruling depends on the em-dash and points the other way:
Same renderer, same population (an unexpanded
sys_userid), opposite requirements. The muted—is what satisfies objectui#2688. The affordance is what satisfies objectui#8434.usernever had this second ruling on it, which is exactly why the mother card landed with every dependent green and this one cannot.The whole dependent surface is green except those two assertions — 661 test files, 6449 tests, 1 file failed / 2 tests failed, and they are precisely objectui#2688's:
⛔ Those two are left failing on purpose. Editing them would be deciding the fork by deleting the evidence for one side of it. They are not a pin on this card's subject (the confident-name arm) — they are a pin on the affordance the fork is about.
⛔ No third behaviour was invented. The obvious "split the difference" — keep
—in the audit footer, show the raw id in grids — is a third answer to the same epistemic state, decided by surface instead of by string shape. That is the defect one level up, which is what the card and the claim both fence off.The ruling this needs
RecordMetaFooter.tsxstates objectui#2688's defect in the very terms this change re-creates: "Without a fallback the field degrades totype: 'text'and the footer prints the raw user id (objectui#2688)." Two honest readings of that card, and they ship different products:Reading A — objectui#2688's pin is incidental; this PR is right and the pin is a pin to UPDATE. The card's reported defect (原文, 未译) is
创建人 g3WkZnvugj4DnYw8u5Mo6ig3ljDhiFGOwhere创建人 Dev Adminbelonged, and its located cause is that the footer bypassed the reference renderer (type:'text'→String(value)). Its remedy was to route it through the renderer. This PR does not un-route it; it changes what that renderer draws when nothing resolves. In the real product — with a data source — the footer still resolves toDev Adminin both worlds, so objectui#2688's actual complaint is untouched. And the new rendering is further from that complaint than—was: the reported harm was an opaque hash presented as if it were the creator's name, which a stated "this screen did not resolve it" makes impossible.Reading B — objectui#2688's pin is its ruling, and the
—is load-bearing. The pin's docblock says "never the raw opaque id" twice, in normative voice, and someone wrote it deliberately. An audit footer showing a 32-char hash to every user on every record is a real, shipped regression that no amount of marker glyph makes acceptable, and it is precisely the "already-shipped behaviour that people may rely on" the card warned about. Under this reading objectui#8434's remedy is not transplantable tolookupas-is, and objectui#8695 needs its own remedy — which is a ruling, not an implementation detail.My recommendation, offered as input to the ruling and not acted on: Reading A, on the grounds that objectui#2688's own body locates its defect at bypassing the renderer, and that the
—never satisfied objectui#2688 either (—is notDev Admin) — it merely hid the failure the card was really about. If Reading A is taken, this PR is complete and the only remaining edit is the two-line pin update inDetailView.title2688.test.tsx, with an updated docblock recording that objectui#8695 moved it.Everything below is the work as it stands, so that either ruling is cheap to execute.
Step one: both readings re-taken on today's tree
The card measured at
da5e4f69e;packages/fields/src/index.tsxwas released by objectui#9068 after that. Re-derived by symbol ona2eb62fd0and re-measured before anything was changed.getCellRenderer('lookup'),reference_to: 'sys_user', no data source, all three in one render:'Ada Lovelace'span class="block max-w-full truncate" title="Ada Lovelace"→Ada Lovelace'wo_1'span class="block max-w-full truncate" title="wo_1"→wo_1'01HQZX9K2M4N6P8R'span class="block max-w-full truncate text-muted-foreground" title="—"→—Both readings reproduce verbatim, and worse than the card states: the first two are byte-identical to what a
textcell prints for the same string — objectui#8434's grading sentence, one renderer over.master_detailandtreerender identically. The disagreement between the shapes is the finding; a one-sided reading could not tell "the renderer is confident" from "this value happens to resolve", which is why every reading here is two-sided and carries its own resolved control.ZONE 2 — the PM's assumptions, each falsified or held
A. "unresolvable is one state" — falsified in the card's own direction, and it is a load-bearing finding.
It is one state at this seam and six distinct causes behind it, and the renderer distinguishes none:
dataSource, or noreference_to);state: 'err');primaryPrimitiveId), so entries 2..n never reach the resolver.Measured: causes 1, 3 and 4 render byte-identically, and cause 2 renders identically to them on first paint.
useLookupNamereturnsstring | undefined, so thepending/err/okdiscriminator its own cache stores is dropped before any caller sees it. ⇒ the card's premise that this renderer "can genuinely distinguish 'fetched and absent' from 'never fetched'" is false as the code stands — and that premise was the one asymmetry offered as a reason the mother ruling might not transfer. Even surfacing the discriminator could not upgrade the sentence: (3) and (4) also cover a record the viewer may not read, and "cannot read" vs "does not exist" is an existence-oracle boundary this lane does not cross (objectui#8631). ⛔ No distinction the data does not carry was added.B. "objectui#8434's remedy is transplantable" — held for the presentation, falsified for the sentence, and falsified at the level the fork is about.
The drawing transplants unchanged (same
data-slot="unresolved-reference", same glyph, same muted treatment, raw value kept). Two adaptations were needed, and one of them is the fork:detail.unresolvedReferenceends "was not resolved to a user", false on a lookup pointing at any other object, and pinned byte-for-byte by two existing tests, so it could not be widened in place. This adds a sibling key,detail.unresolvedLookupReference, in all ten locale packs. Each call site keeps its literal key rather than taking one as a prop — a key read from a constant is downgraded to report-only bycheck:i18n-keys, which PR fix(fields): auserreference that resolved to nothing says so (objectui#8434) #8698 documents and this preserves.—before. It is louder now, and that is stated rather than hidden.C. "
LookupCellRendereris the only renderer with this split" — held. Nothing filed, nothing widened.isLikelyOpaqueIdhas exactly one call site in the repo (this one), so no sibling renderer decides presentation from string shape.SelectCellRendereris the nearest neighbour — an unmatched value falls tohumanizeLabel(String(val))— but that is one answer, not a shape-decided pair, and its option set is the whole oracle. Not this defect.D. "the two readings still reproduce" — held, re-measured above on
a2eb62fd0, after objectui#9068.E. "no existing test pins the current confident-name rendering" — FALSIFIED, and it turned out to be two different pins with two different dispositions.
packages/fields/src/__tests__/cell-truncation.test.tsx→ "LookupCellRenderer: primitive non-opaque value" assertedexpectTruncating(...)on the confident-name arm. So objectui#3466 pinned the confident bare-text rendering of an unresolved reference — that reading was deliberate once. This card is chartered to change exactly that, so the case is UPDATED, not routed around: it now asserts what 记录详情页字段值超长时尾部无声消失:span.truncate 容器缺 min-width:0,ellipsis 永不触发,且无 title 兜底全文 #3466 actually contracted for and what still holds — a single-line value must not expand its column and its full text must stay reachable. The shape that meets it moved (truncateon the text span inside aninline-flexwrapper;overflow: hiddengives a flex item an automatic minimum size of zero, so it shrinks instead of pushing the row wider) and the full value is exposed through the wrapper's statedtitle.packages/plugin-detail/src/__tests__/DetailView.title2688.test.tsxpins the—, on a second surface, in normative voice, from a different card. That is the fork's fence, not this card's subject ⇒ left failing, untouched.F. "presentational only" — held. Cell renderers are consumed only inside React render trees (
ObjectGrid,RelatedList,DetailSection,HeaderHighlight,RecordMetaFooter,ObjectKanban, …); no caller reads a renderer's output back into data, and the grid's export path does not go throughgetCellRendererat all. Nothing here touches a query, a sort, an export, the wire, or the save path —reference_not_foundis not in the diff.The change
resolveLabel's last two arms become one:The unresolved arm renders the raw value beside a stated marker, inside
ReferencedRecordLinkso the reference stays navigable (objectui#4336) — the value is the foreign key, so an unresolved reference is still worth being able to open. The multi-value chip keeps its muted background and gains the affordance inside it; the+Noverflow chip'stitlenow lists the values it hides instead of a row of dashes. Also collapsed: the—for an unresolved value andEmptyValue's—for no value at all were the same glyph for two different states, so a user could not tell them apart either.isLikelyOpaqueIdstays exported — published surface, and retiring it is a breaking change no display card is entitled to make — but nothing calls it. Its docblock now says why re-introducing a presentation caller would re-open this card.Verification
Assertions are on rendered output, and the load-bearing ones assert agreement, not per-shape strings: each shape's markup is normalised by its own raw value and the three must collapse to one string. A per-shape assertion would go green on a repair that made both shapes wrong in a new matching way; the agreement assertion plus a floor (the affordance is present, the raw value survives, the sentence is epistemic) is what closes that.
packages/fields: 158 files / 2750 tests passed,VERDICT command-exit 0.packages/i18n+plugin-detail+plugin-grid+plugin-dashboard+plugin-list+plugin-kanban+plugin-gantt+plugin-tree: 661 files / 6449 tests — 1 file failed, 2 tests failed,VERDICT command-exit 1. Both failures are objectui#2688's, quoted at the top; nothing else moved.check:i18n-keys(it judges the new call site — "every in-scope call-site key resolves against the en pack (3005 keys) … every call site passes exactly the arguments that value has holes for"),check:i18n-drift(1 key added, 0 en values changed — theusersentence did not move),check:i18n-dead-keys,check:control-bytes(7270 files) plus a manual control-byte scan of every changed file,check:icon-record-names,check:new-line-citations,check:unreferenced-sources,check-changeset-presence(12 source files of 2 released packages, 1 changeset),check-governed-queue-guard --test(NOT GOVERNED).type-check— NOT MEASURED,PREREQUISITE NOT MET:tsc --noEmitin both packages exits 2 withTS2307 Cannot find module '@object-ui/types' | '@object-ui/core' | '@object-ui/components'across files this diff never touches — the workspace dependency closure is not built in this container. A missing prerequisite, not a red gate. ⛔ Not read as green either. Declared to CI.Ablation — read site mutated, seven legs, all from the committed fix
Predictions were written down before running (
PREDICTIONS.mdin the run scratch). Every leg mutates the read site inLookupCellRenderer/ its affordance, never a pin, and each leg proves the mutation on disk (git hash-object≠ the HEAD blobaa93359c…, plus an injected-marker line count 0 → N) and proves the restore by state (blob back to the HEAD blob andgit diff HEADempty). Per-test classification from vitest's JSON reporter. Population: 28 tests (the new 22-test pin +cell-truncation's 6).'—', else raw, both confidentif (true) return { …, unresolved: true }options, resolver)unresolved: falseReference not found: … does not existdetail.unresolvedReference+ theusersentenceEmptyValueEmptyValuecontrolBUGis the leg that matters — the only one that reproduces the shipped defect. It kills the agreement assertion and, on the death stringthe card's grading sentence, observed failing.
⭐
NONEis the leg that proves the floor is load-bearing. Under it all three shapes still agree — they are identically wrong — so the equality half of the agreement pin passes; the test dies on its floor (data-slot="unresolved-reference"absent). That is exactly the failure mode a per-shape assertion could not catch and the reason the floor is there.All 22 assertions in the new pin were observed failing on at least one leg. The four in the population that never failed are
cell-truncation's cases forTextCellRenderer,UserCellRenderer,FileCellRendererandSelectCellRenderer— other renderers, correctly insensitive to aLookupCellRenderermutation.Acceptance notes
Observed, not filed (none is a reproducible defect, a declared-contract breach, or an authoring trap):
isLikelyOpaqueIdis now an exported helper with no caller in this repo. Dead-ish code behind a published export; retiring it is a breaking change and belongs to whoever next revisits@object-ui/fields' published surface, not to a display card. Successor: none currently queued.useLookupNamecollapsespending/err/okintostring | undefinedat the hook boundary while its cache holds the discriminator. That is what makes cause (2) above indistinguishable from (4). Not a defect — nothing today is entitled to say more than the epistemic sentence — but a future "loading" affordance would start there. Successor: none currently queued.—for an unresolved value andEmptyValue's—for no value were the same glyph for two different states. Folded into this change rather than filed, since the arm was being rewritten anyway.🤖 Generated with Claude Code
https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Generated by Claude Code
Generated by Claude Code