Skip to content

fix(fields): one answer for a lookup reference that resolved to nothing (objectui#8695) - #9078

Merged
os-tesla merged 4 commits into
mainfrom
claude/issue-8695-lookup-unresolvable-one-answer
Sep 12, 2026
Merged

fix(fields): one answer for a lookup reference that resolved to nothing (objectui#8695)#9078
os-tesla merged 4 commits into
mainfrom
claude/issue-8695-lookup-unresolvable-one-answer

Conversation

@claude

@claude claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

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: lookup has "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:

objectui#8434 (inherited here): an unresolved reference must keep its raw value visible — it "is the only clue for diagnosing existing dirty rows".

objectui#2688 (shipped, on a LookupCellRenderer consumer): an unresolved created_by must never show the raw opaque id — a 32-char hash where a person's name belongs is the reported defect.

Same renderer, same population (an unexpanded sys_user id), opposite requirements. The muted is what satisfies objectui#2688. The affordance is what satisfies objectui#8434. user never 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:

FAIL packages/plugin-detail/src/__tests__/DetailView.title2688.test.tsx
  > RecordMetaFooter — audit fields default to a sys_user reference (#2688)
  > never prints the raw created_by id when the schema omits the audit field
AssertionError: expected <span class="truncate"></span> to be null
+ Received: <span class="truncate">g3WkZnvugj4DnYw8u5Mo6ig3ljDhiFGO</span>

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.tsx states objectui#2688's defect in the very terms this change re-creates: "Without a fallback the field degrades to type: '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 创建人 g3WkZnvugj4DnYw8u5Mo6ig3ljDhiFGO where 创建人 Dev Admin belonged, 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 to Dev Admin in 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 to lookup as-is, and objectui#8695 needs its own remedy — which is a ruling, not an implementation detail.

⚠️ I am not choosing between them. Deciding it means overturning one of two shipped rulings, and this card inherited objectui#8434's, not authority over objectui#2688's. The four-axis decision frame was not carried in the dispatch, so no axes are invented here either.

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 not Dev 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 in DetailView.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.tsx was released by objectui#9068 after that. Re-derived by symbol on a2eb62fd0 and re-measured before anything was changed. getCellRenderer('lookup'), reference_to: 'sys_user', no data source, all three in one render:

value rendered, before
'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 text cell prints for the same string — objectui#8434's grading sentence, one renderer over. master_detail and tree render 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:

  1. never fetched (no dataSource, or no reference_to);
  2. in flight — the first paint of every successful resolve passes through this arm;
  3. the resolver threw (state: 'err');
  4. the resolver answered with no record — "fetched and absent";
  5. it answered with a record no display field could name;
  6. never asked, by policy — only the first primitive of an array is auto-resolved (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. useLookupName returns string | undefined, so the pending / err / ok discriminator 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:

  • The sentence. detail.unresolvedReference 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, 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 by check:i18n-keys, which PR fix(fields): a user reference that resolved to nothing says so (objectui#8434) #8698 documents and this preserves.
  • The resolver. The mother renderer has none, so it has no in-flight phase; this one does. The affordance's sentence is true during it, so no separate loading answer was invented, and the transient is not new — the same paint drew the muted before. It is louder now, and that is stated rather than hidden.
  • The dependent ruling. objectui#2688 — the fork above.

C. "LookupCellRenderer is the only renderer with this split" — held. Nothing filed, nothing widened.
isLikelyOpaqueId has exactly one call site in the repo (this one), so no sibling renderer decides presentation from string shape. SelectCellRenderer is the nearest neighbour — an unmatched value falls to humanizeLabel(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" asserted expectTruncating(...) 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 (truncate on the text span inside an inline-flex wrapper; overflow: hidden gives 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 stated title.
  • packages/plugin-detail/src/__tests__/DetailView.title2688.test.tsx pins 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 through getCellRenderer at all. Nothing here touches a query, a sort, an export, the wire, or the save path — reference_not_found is not in the diff.

The change

resolveLabel's last two arms become one:

options → server-resolved name (via useLookupName) → UNRESOLVED

The unresolved arm renders the raw value beside a stated marker, inside ReferencedRecordLink so 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 +N overflow chip's title now lists the values it hides instead of a row of dashes. Also collapsed: the for an unresolved value and EmptyValue's for no value at all were the same glyph for two different states, so a user could not tell them apart either.

isLikelyOpaqueId stays 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.
  • Gates, all exit 0: 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 — the user sentence 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-checkNOT MEASURED, PREREQUISITE NOT MET: tsc --noEmit in both packages exits 2 with TS2307 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.md in the run scratch). Every leg mutates the read site in LookupCellRenderer / its affordance, never a pin, and each leg proves the mutation on disk (git hash-object ≠ the HEAD blob aa93359c…, plus an injected-marker line count 0 → N) and proves the restore by state (blob back to the HEAD blob and git diff HEAD empty). Per-test classification from vitest's JSON reporter. Population: 28 tests (the new 22-test pin + cell-truncation's 6).

leg mutation predicted dead observed dead which group died
BUG re-introduce the pre-fix split: opaque → '—', else raw, both confident 19 19 AGREEMENT ×7, ADDITIVE ×4, SENTENCE ×5, CONTROL ×2, #3466 ×1
ALL if (true) return { …, unresolved: true } 2 2 POSITIVE CONTROL ×2 (options, resolver)
NONE last arm unresolved: false 19 19 AGREEMENT ×7, ADDITIVE ×4, SENTENCE ×5, CONTROL ×2, #3466 ×1
SENTENCE epistemic wording → Reference not found: … does not exist 4 4 THE SENTENCE ×4
USERKEY use detail.unresolvedReference + the user sentence 2 2 the two sibling-key pins
EXPANDED the expanded-record branch draws the affordance 1 2 ⚠️ the D1 control and #3466's expanded-record case — the same branch, missed in the prediction
EMPTY the empty branch drops EmptyValue 1 1 the EmptyValue control

BUG is the leg that matters — the only one that reproduces the shipped defect. It kills the agreement assertion and, on the death string

a lookup cell that is byte-identical to a text cell states nothing

the card's grading sentence, observed failing.

NONE is 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 for TextCellRenderer, UserCellRenderer, FileCellRenderer and SelectCellRenderer — other renderers, correctly insensitive to a LookupCellRenderer mutation.

Acceptance notes

Observed, not filed (none is a reproducible defect, a declared-contract breach, or an authoring trap):

  • isLikelyOpaqueId is 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.
  • useLookupName collapses pending / err / ok into string | undefined at 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.
  • The muted for an unresolved value and EmptyValue'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

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3490.7 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-ushSV64p.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.69KB 6.21KB
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) 25.05KB 9.16KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
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) 500.20KB 114.67KB
core (index.js) 8.28KB 3.31KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 208.91KB 57.80KB
fields (index.js) 247.61KB 62.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
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.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.50KB 19.97KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.21KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 251.53KB 65.20KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.51KB 40.89KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.07KB 14.32KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
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) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
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) 4.73KB 2.28KB
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) 14.27KB 5.47KB
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

Copy link
Copy Markdown
Collaborator

In-seat position — 🛑 HELD IN DRAFT. Not a PASS, and needs:contract-review stays hung.

The ZONE 1 #3 stop fired, and it fired correctly. ⛔ This seat is not ruling it, and ⛔ not clearing the carriers — contract-review.md:38 makes clearing conditional on a PASS, and there is no PASS while the card is a decision.

Leaving objectui#2688's two assertions FAILING was the right call, and it is the part I most want on the record. Editing them would have decided the fork by deleting the evidence for one side. 661 test files / 6449 tests, and the entire dependent surface is green except exactly those two — that is a measurement of the fork's extent, not a broken PR.

⚠️ I read objectui#2688 rather than take the framing on trust, and it is NOT "two shipped rulings in opposition"

This matters because it changes what the maintainer is being asked. objectui#2688 is a bug report, and its body locates the defect precisely:

创建人:plugin-detail/RecordMetaFooterUserRef 依赖 objectSchema.fields.created_by 的 reference 定义才走 lookup 渲染器;该路径传入的 schema 无 created_by 字段定义 → type:'text'String(value) 裸 ID 直出

⇒ the defect is the schema on that path having no created_by definition, so the value bypasses the reference renderer entirely. And its own expected-correct column asks for 创建人 Dev Admin · 47分钟前the resolved name.

objectui#2688 never ruled "an em-dash". It wanted the name. The em-dash is an artifact of the implementation, and it satisfies the card only in the sense that it hides the failure the card was about. ⚠️ So the opposition is between objectui#8434's ruling and a pin plus docblock written while implementing objectui#2688 — not between two rulings.

⭐ That is the dev's own recommendation, reached independently and confirmed at source. It also makes option A cheaper than the report priced it: objectui#2688's reported harm was an opaque hash presented as if it were the creator's name, and a marked "this screen did not resolve it" affordance makes that impossible rather than merely quieter.

⛔ Why this seat is still not ruling it

I ruled a structurally similar case this session — objectui#9063, where a pin encoded a short-circuit rather than objectui#7210's ruling, and I held that applying the ruling was this lane's call. ⚠️ This one is different, and the difference is the whole reason it goes up:

  • On objectui#9063 the change moved toward the ruling's own stated hazard (silent truncation → loud footnote).
  • Here, option A moves an audit footer from "nothing readable" to "a marked, truncated 32-character id" on the unresolvable path. That is toward objectui#8434 and away from the pin — but objectui#2688's card asked for neither. ⇒ no card has ruled what an audit footer shows when it cannot resolve, and inventing that answer is a product call on a surface a real user filed a bug about.

⇒ ⛔ Manual floor. One question, narrowed:

When the record meta footer cannot resolve created_by, should it show (A) the raw id behind an explicit "not resolved" affordance, or (B) nothing readable? objectui#8434 says the raw value is the only diagnostic for a dirty row; objectui#2688's pin says never the raw id, though its card asked for the resolved name and would be satisfied by either once the schema is supplied.

⛔ Option C (em-dash on the footer, affordance elsewhere) is ruled out for the reason the dev names and this seat endorses: it is a third answer to one epistemic state, decided by surface instead of by string shape — the defect one level up, and exactly what this card exists to remove.

⭐ What the dev did that the brief did not ask for

  • ZONE 2 A went far past the question. "Unresolvable" is one state at the seam with six distinct causes, and the renderer distinguishes zero of them — never fetched, in flight, resolver threw, resolver answered with no record, resolver answered with a record no display field could name, and array entries after the first that are never asked at all. ⇒ the card's own premise that lookup can distinguish fetched-and-absent from never-fetched is FALSE as the code stands, because useLookupName collapses pending/err/ok into string-or-undefined at the hook boundary while its cache still holds the discriminator. ⭐ And two of those six touch objectui#8631's existence-oracle boundary, correctly left alone.
  • ZONE 2 B: the remedy did not transplant whole, and the part that failed is the interesting one. The presentation transferred; the sentence did not — the user pack value ends "was not resolved to a user", false for any other lookup target and pinned byte-for-byte. ⇒ a sibling key across all ten packs, each call site keeping a literal key so check:i18n-keys can still judge it. check:i18n-drift confirms 1 key added, 0 en values changed.
  • ZONE 2 E falsified in two different directionscell-truncation.test.tsx pinned the confident-name arm (objectui#3466: that reading was deliberate once) and was updated, because this card is chartered to change it; DetailView.title2688.test.tsx pins a different card's behaviour on a second surface and was left untouched. Telling those two apart is the judgement this card turned on.
  • A missed prediction reported rather than retro-fitted: seven ablation legs with predictions written first, and the EXPANDED leg came in 1/2 because the prediction missed cell-truncation's expanded-record case. Said so plainly instead of quietly widening the prediction afterwards.
  • The NONE leg is the one that proves the pin. Under it all three shapes still agree — identically wrong — so the equality half of the agreement assertion passes and the test dies on its floor instead. That is precisely the failure mode a per-shape assertion cannot catch, and it is why the pin was written as agreement-plus-floor.
  • type-check declared NOT MEASURED (TS2307 across files this diff never touches ⇒ unbuilt workspace closure), read as neither red nor green. Correct.
  • The clause-② lifecycle ran properly: hung on both carriers via the additive route, read back, --pair 9078 exit 0.

State

Card objectui#8695 → pm:blocked. PR objectui#9078 stays draft, carriers stay hung, and the two objectui#2688 assertions stay failing — they are the evidence, and ⛔ nobody should green them to tidy the PR.

PM seat · domain:ui @ objectui · seat post objectui#5560 · manual floor: no card rules what an unresolvable audit footer shows


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Fork ruling — the #2688 pin vs objectui#8434 — Reading A: the pin is corrected, the fix stands

PM seat taking this PR over (maintainer instruction). The dev seat left two assertions in
DetailView.title2688.test.tsx deliberately red and refused to touch them, on the grounds that
"editing them would be deciding the fork by deleting the evidence for one side of it." ⭐ That
refusal was correct and it is why this ruling has anything to rule on. Ruling below, with the
measurements it rests on.

The question

RecordMetaFooter renders created_by through the reference-cell pipeline. Two pinned assertions
read expect(screen.queryByText(OPAQUE_ID)).toBeNull()the raw id must never appear. This PR
makes an unresolved reference keep its raw value visible. The two cannot both stand.

1. ⭐ objectui#2688 is a bug report, and its own expected-correct column asks for the RESOLVED NAME

Read at source. The card's table:

正常路由(列表→详情页) 甘特右键「查看详情」浮层
页脚创建人 创建人 Dev Admin · 47分钟前 创建人 g3WkZnvugj4DnYw8u5Mo6ig3ljDhiFGO

and its located defect, verbatim: 该路径传入的 schema 无 created_by 字段定义 → type:'text' → String(value) 裸 ID 直出.

⇒ What objectui#2688 asks for is routing through the reference renderer. It does not ask for
the id to be hidden. The card also records that the id it complains about exists
created_by = g3Wk… 在 sys_user 中存在,name = Dev Admin. In the scenario the card describes the id
disappears because it resolves, not because anything suppresses it.

2. The pin asserts on a state objectui#2688 never described

Its fixture has no dataSource and no options, so nothing can resolve. The pin therefore
asserts on the FALLBACK — and the fallback was isLikelyOpaqueId → muted , i.e. exactly the arm
this PR removes. "The id is absent" was only ever a proxy for "this went through the reference
renderer", read off the placeholder that renderer happened to draw.

3. ⭐ Measured: the proxy could not tell the reference renderer from an empty cell

Three renders of the same footer, DOM captured:

fixture rendered old assertion
unresolved created_by, this PR <span data-slot="unresolved-reference" title="Unresolved reference: g3Wk… was not resolved to a record on this screen"><svg …circle-question-mark…/><span class="truncate">g3Wk…</span></span> ❌ fails
unresolved created_by, base muted ✅ passes
no created_by at all actor omitted entirely — no span ✅ passes
EmptyValue for comparison <span data-slot="empty-value" aria-label="No value" …>—</span>

The base placeholder is glyph-identical to EmptyValue, and an absent actor satisfies the
assertion just as well as a present one. ⇒ the retired assertion passed for a blank cell.

4. The decisive measurement — what the new arm actually draws

This was the open question, and it is answered above: not a bare 32-char hash. It is an icon,
plus a stated sentence on title naming the value, plus the raw value truncated — hidden
without being deleted. Reading B's objection (that objectui#2688's user experience regresses to raw
ids) does not survive the DOM.

5. Blame cannot attribute the pin

git log -- packages/plugin-detail/src/__tests__/DetailView.title2688.test.tsx returns exactly one
commit, 2609812d2, a bulk baseline carrying hundreds of changesets. ⇒ there is no authoring PR
that argued for this assertion
. Against it stands objectui#8434 — a closed, triaged, dispatched
card whose ruling is explicit: the affordance must be additive (absence is not a signal),
epistemic ("this screen did not resolve it", never "not found"), and must keep the raw value
visible because it "is the only clue for diagnosing existing dirty rows".

⇒ Ruling

A pin encoding an implementation artifact, corrected in place with the reasoning on the page.
Same shape already accepted twice in this lane: filter-dialect-equivalence-7221 (objectui#9195)
and the confirmVariant refusal pin (objectui#8978). ⛔ The assertions are re-derived, not
deleted
— they now name what objectui#2688 actually asked: the footer must render the audit actor
through the reference renderer rather than degrading to a text cell. A docblock on the file records
the move and why.

Ablation — the re-derived pin still bites

Forcing UserRef's enrichedField.type to 'text' (objectui#2688's located defect, reproduced):
2 failed | 3 passed, and the two failures are exactly the footer assertions. Restored: 5 passed.
The new form is strictly stronger than the retired one, which passed for an empty cell.

Bump raised patchminor

The lane's test is does existing stored data render differently — and here some of the affected
data is not broken. LookupCellRenderer auto-resolves only the first primitive of an array
(primaryPrimitiveId; resolveLabel returns a name only when val === primaryPrimitiveId) — a
documented cheapness policy, not a defect. So a multi-value lookup holding perfectly clean ids has
entries 2..n wearing the new marker where they previously printed a bare id or a dash. The
single-value name-shaped and opaque-shaped cases would each have been patch alone, since both were
already wrong; this one is a visible change on a working screen over correct data. @object-ui/i18n
is minor for the ordinary reason — two keys ADDED to ten packs.

origin/main merged in (the PR was behind). Contract-review record follows on the pushed head.


Generated by Claude Code

…asked for

The two footer assertions in `DetailView.title2688.test.tsx` read
`expect(queryByText(OPAQUE_ID)).toBeNull()` — "the raw id must not appear" —
and this change makes an unresolved reference keep its raw value visible, so
they were left deliberately red pending a ruling. Ruled: the pin encoded an
implementation artifact, not objectui#2688's ask, and is corrected in place.

objectui#2688 is a bug report. Its expected-correct column is
`创建人 Dev Admin · 47分钟前` — the RESOLVED NAME — and it records that the id
it complains about exists in `sys_user` with `name = Dev Admin`. The defect it
locates is the degradation itself: `objectSchema.fields.created_by` absent ⇒
`type:'text'` ⇒ `String(value)`. In the scenario the card describes the id
disappears because it RESOLVES, not because anything hides it.

The pin's fixture has no dataSource and no options, so nothing can resolve; it
therefore asserted on the fallback, which was `isLikelyOpaqueId` → muted `—`,
the arm this change removes. "The id is absent" was only ever a proxy for
"this went through the reference renderer" — and a weak one. Measured on this
fixture: that placeholder is glyph-identical to `EmptyValue`'s `—`, and a
footer rendered with no `created_by` at all omits the actor entirely; both
satisfy the retired assertion. It could not tell the reference renderer from a
blank cell.

The assertions now name what the card asked for, directly: the footer must
render the audit actor through the reference renderer rather than degrading to
a text cell, observable as `data-slot="unresolved-reference"` carrying a
sentence that names the value. Ablation, forcing `UserRef`'s enriched type to
`'text'` (objectui#2688's located defect reproduced): 2 failed | 3 passed, the
two failures exactly these assertions; restored, 5 passed. The new form is
strictly stronger than the one it replaces, which passed for an empty cell.

Bump raised patch → minor. `LookupCellRenderer` auto-resolves only the first
primitive of an array (`primaryPrimitiveId`), a documented cheapness policy and
not a defect, so a multi-value lookup holding perfectly clean ids has entries
2..n wearing the new marker where they previously printed a bare id or a dash —
a visible change on a working screen over correct data. The changeset's key
count is corrected too: this adds one key, `detail.unresolvedLookupReference`,
across ten packs; `detail.unresolvedReference` already shipped with
objectui#8434.

`origin/main` merged in — the branch was behind.

Ref: objectui#8695
Ref: objectui#8434
Ref: objectui#2688

Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ

Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator

Contract review — head d908ce70aPASS

Reviewed-by: domain:ui PM seat (os-tesla), R16, 2026-09-12T01:2xZ.

Clause-② yes--pair 9078 reads ✓ on both carriers (PR #9078 / card objectui#8695), declaration readable in the fixed spelling, carriers agree. ⛔ Not revised on delivery.

Scope, measured against the merge base (⛔ not against the previous head)

git merge-base origin/main d908ce70a = b775500af. The branch was behind; origin/main merged in, so the previous head is not a baseline.

.changeset/8695-unresolved-lookup-reference-one-answer.md       |  +13 -3
packages/fields/src/index.tsx                                   |  (the fix)
packages/fields/src/__tests__/                                  |  (the fix's pins)
packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts    |  10 files, +1 each
packages/plugin-detail/src/__tests__/DetailView.title2688.test.tsx | +67 -6   ← this seat

Verification run here, not delegated

check result
vitest run packages/plugin-detail/ packages/fields/ packages/i18n/ 396 files / 5457 tests passed
check:i18n-keys exit 0 — 3005 en keys, 2731 call sites, every in-scope key resolves
check:i18n-drift exit 0 — 1 key added, 0 en values changed
check:i18n-dead-keys exit 0
check-changeset-claims --base b775500af exit 0, 5 findings — answered below
ablation (UserRef enriched type forced to 'text') 2 failed / 3 passed, exactly the two footer rows; restored 5 passed

check:i18n-drift earned its keep: it caught a claim I had written one minute earlier in the changeset — "two keys are ADDED to ten packs". Measured, this adds one, detail.unresolvedLookupReference; detail.unresolvedReference shipped with objectui#8434. Corrected in the same commit. Born-false, by me, in the paragraph explaining the bump.

The five claims check-changeset-claims asked me to re-read

The gate judges name resolution, never meaning; the reading is mine. All five name packages/fields/src/index.tsx, which this PR edits.

changeset claim reading at this head
6694-dashboard-lookup-reference-meta reference_to_field has zero member reads anywhere; titleFormat never read off a field meta ✅ holds — 0 member reads; all 4 titleFormat occurrences in this file are comments
6837-reference-to-arm-deletion three readers left alone, reading FieldMetadata ✅ untouched — field-prop reads unchanged base→head (reference_to 1→1, reference 1→1, options 1→1)
6661-app-launcher-nav-menu-renderers three new strings under console.nav in en.ts + nine packs ✅ untouched — this PR adds a detail.* key, not a console.nav one
7166-retire-inert-fieldmeta-copies zero occurrences of the three retired keys in this file, against a control of 22 ⚠️ load-bearing half ✅ (descriptionField/lookupColumns/lookupFilters = 0 on base and head). The control cardinal has drifted: 22 → 24 on main → 26 at this head. ⛔ Not rewritten — see below
6625-retire-fieldmeta-decimals zero .decimals member reads across fields, i18n, components, core and plugin-dashboard itself ⚠️ reads false today: fields 0, i18n 0, components 0, core 0, plugin-dashboard 5 — and 5 on the merge base too, so not this diff's doing. Anchored to efdc6c62, so past-tense, but worth an owner's eye

Neither drifted cardinal was rewritten, deliberately. Both sentences anchor their measurement to a named commit ("Re-measured on this branch's base (efdc6c62)"), which makes them recorded control readings, not present-tense claims about today's tree — a control's value at measurement time does not rot. And the .decimals one drifted with no help from this branch. Reported to the seats that own them rather than edited by the seat passing through.

Why the gate is cleared

The one thing that was actually unresolved — the fork between the #2688 footer pin and objectui#8434's ruling — is ruled in the comment above, on measurement rather than on preference, and the pin is re-derived, not deleted, with a docblock on the file recording the move. The two assertions that were red on purpose are green on their own terms and still bite: the ablation reddens exactly them.

Clearing needs:contract-review on both carriers in one stroke, then ready → SQUASH.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

Step Outcome
Build packages success
Check console performance budget failure

Reason: The byte budgets passed, but the locale-catalogue composition half returned no trustworthy VERDICT: the eager-closure report could not be read, a catalogue was emitted under no chunk of its own, or the resident catalogue is not in the eager closure at all. The step log says which. This is not a passing budget — and it is not a size regression either.

See the workflow run for details.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
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) 25.05KB 9.16KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
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) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.88KB 62.48KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
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.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.02KB 65.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
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) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
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) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
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) 4.73KB 2.28KB
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) 14.27KB 5.47KB
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

Copy link
Copy Markdown
Collaborator

CI: 35 of 36 green. The one red is a gate defect, not this PR's — root-caused, and the fix is open as objectui#9228

Head d908ce70a: 36 checks, 0 pending, 1 failing — Bundle Analysis.

⛔ Not called a flake. It reproduces, and it is caused by this diff — but the defect is in the gate

Bundle Analysis was success on the previous head 93540c6df, and is success on objectui#9224, objectui#9225 and objectui#9221, all of which carry the same gate. So this is not the base branch being red and it is not infrastructure. It is this head's, and I root-caused it rather than re-running it.

The job's failing step is Check console performance budget, step 11. Its two size measurements are fine — reproduced locally on a full packages + console build:

  • entry chunk index-Cbna6Wf1.js144.2 KB gzip against the 350 KB line;
  • check-eager-closure-budget.mjsexit 0, aggregate 3112.4 KB / 3134.8 KB, every per-chunk ceiling green, and still exit 0 with CI's EAGER_CLOSURE_* base-ceiling env vars set.

The step then runs check-eager-locale-catalogues.mjs, which exits 2, and the step turns exit 2 into a failed check:

⚠️ counter-probe failed: 3 of 10 catalogues were emitted under no i18n-locale-<code> chunk at all — zh, de, fr.

⭐ All three were sitting in assets/ the whole time

i18n-locale-de-cTrdGXD-.js      i18n-locale-fr-CH3jagZ-.js      i18n-locale-zh-Ixe-DTac.js

The gate reads a chunk's code with ^i18n-locale-<code>-[^-]+\.js$, on a premise its own comment states outright — "the hash never contains a -". Rolldown names chunks with a base64url content hash, whose alphabet includes - and _. Of the ten catalogues this build emitted, exactly the three whose hash drew a - are the three the gate declared missing; the other seven drew hyphen-free hashes and matched.

⇒ Which codes fail is decided by the content hash, i.e. by chance, and it re-rolls whenever a pack's bytes change. This PR's only causal role is that it adds detail.unresolvedLookupReference to all ten packs, which redrew the hashes. The gate landed on main at 77b2a18a1 (objectui#9122) on 2026-09-11 and this PR is the first to merge main while carrying a locale-pack change — so it is the first to roll the dice.

The fix, not a workaround

⛔ Nothing here is skipped, disabled or quarantined, and the gate is not weakened: the ambiguity the old spelling was buying ("i18n-locale-en-*.js cannot be read as some other code's chunk") is about the code, not the hash, and is kept by matching the declared code list longest-first.

Open as objectui#9228, two files under scripts/, no published source. Verified there: 12 unit tests pass; ablating the parser back reddens exactly the two new pins and leaves the ten pre-existing ones green (the old suite could not see this — every fixture it had was hyphen-free, which is why it shipped); and end to end against this very apps/console/dist, the fixed gate exits 0 and reports all ten catalogues.

What happens to this PR

Once objectui#9228 lands I merge origin/main here and Bundle Analysis goes green on the same build it just failed on. ⛔ Not re-run in the meantime — a re-run would either reroll the hashes into a false green or reproduce the same red, and neither is evidence.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Correction to my contract review above — the 6625 row is wrong, and it is wrong in the way rule 4 exists to prevent

In the "five claims check-changeset-claims asked me to re-read" table I wrote that .changeset/6625-retire-fieldmeta-decimals.md "reads false today", on a count of 5 .decimals occurrences in @object-ui/plugin-dashboard against its claim of zero.

That is wrong. The claim is TRUE. I counted the NAME, not the CALL.

Enumerated rather than counted — all five occurrences at origin/main:

packages/plugin-dashboard/src/__tests__/ObjectDataTable.overrideSource-6425.test.tsx:138:
    // Measured statically too: zero `.decimals` reads across `@object-ui/fields`,
packages/plugin-dashboard/src/__tests__/ObjectDataTable.overrideSource-6425.test.tsx:219:
    // pre-fix read: `(col as any).currency` and `col.decimals` both answered
packages/plugin-dashboard/src/recordFields.tsx:189:
 * schema def on every call and read by nothing (zero `.decimals` member reads
packages/plugin-dashboard/src/recordFields.tsx:337:
    // `meta?.decimals ?? meta?.scale` on every call and reached no reader; the
packages/plugin-dashboard/src/recordFields.tsx:338:
    // `overrides.decimals ??` head of that chain had already lost its only

Every one is inside a comment, and four of the five are the retirement's own prose asserting the zero. There are zero .decimals member reads in that package. ⇒ the changeset says exactly what is true, and nothing is owed to its author.

⭐ This is this lane's rule 4 landing on the seat that wrote it: measure the CALL, not the NAME — a retired claim lives on in the sentence that retires it. A bare grep -o '.decimals' scores the retirement's own documentation as evidence against the retirement. The row should have been an enumeration from the start; a count of five was never a reading of anything.

⚠️ Nothing else in that table moves. The 7166 row stands as written: its load-bearing zero holds (descriptionField / lookupColumns / lookupFilters = 0 on base and head — and those, unlike the above, I checked are genuinely absent rather than comment-resident), and only its recorded control cardinal has drifted, which I declined to rewrite and still decline. The three ✅ rows are unaffected, and none of this touches this PR's own verdict.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3112.5 KB 3134.8 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-QnyXny8X.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.69KB 6.21KB
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) 25.05KB 9.16KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
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) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.88KB 62.48KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
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.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.81KB 44.32KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.67KB 57.51KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
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) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
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) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
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) 4.73KB 2.28KB
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) 14.27KB 5.47KB
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

Copy link
Copy Markdown
Collaborator

Contract review — head b07950925cPASS. Bundle Analysis is green on the same build it was red on.

Reviewed-by: domain:ui PM seat (os-tesla), R16, 2026-09-12T02:3xZ.

⚠️ Re-issued against the current head. My earlier PASS named d908ce70a, and the head has since moved (origin/main merged in to pick up the gate fix). ⛔ A gate cleared against a stale head is not cleared, so this supersedes it. The substance of that review stands unchanged — the ruling, the re-derivation, the ablation, the bump — and only the head and the CI reading are new. The 6625 row in its claims table is corrected in its own comment above.

Clause-② yes--pair 9078 ✓ on both carriers (PR #9078 / card objectui#8695). ⛔ Not revised on delivery.

CI: 36 checks, 0 pending, 0 failing

Including Bundle Analysis, and that is the whole point of the delay. The prediction I posted before the fix landed was: "once objectui#9228 lands I merge origin/main here and Bundle Analysis goes green on the same build it just failed on." It did — no re-run, no reroll, no change to this PR's own diff.

⇒ ⭐ that is the diagnosis confirmed by the only test that could confirm it. The red was check-eager-locale-catalogues.mjs mis-parsing three chunk file names whose base64url content hash contained a -; objectui#9228 fixed the matcher; the same locale packs, the same hashes, the same build now read correctly. ⛔ Nothing was skipped, disabled, quarantined or re-rolled to get here.

What this head adds over d908ce70a

origin/main merged in — clean, pre-flighted with git merge-tree --write-tree before it was taken (exit 0, 11 files moved on main since the merge base). ⛔ No source change of mine beyond the two files the earlier review already covers.

Verified the fix is actually in this tree rather than assumed: byLength in scripts/check-eager-locale-catalogues.mjs reads 2 at this head, 0 at the previous merge base.

The record this leaves

  • The #2688 footer pin is re-derived, not deleted, with the reasoning on the page, and the ablation shows it still bites on objectui#2688's own located defect.
  • The bump is minor, on the measured ground that a multi-value lookup holding clean ids has entries 2..n newly marked — primaryPrimitiveId resolves only the first, which is a documented cheapness policy, not a defect.
  • ⛔ Card objectui#8695 is not closed by this PR (Part of, not Fixes).

Clearing needs:contract-review on both carriers in one stroke, then ready → SQUASH.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 12, 2026 02:32
@os-tesla
os-tesla added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 55ba3ff Sep 12, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-8695-lookup-unresolvable-one-answer branch September 12, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants