Skip to content

docs(core): drop the rendering claim EXPANDABLE_FIELD_TYPES docblock could not check, and correct its consumer list by four - #8717

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-8693-expand-fields-rendering-claim
Sep 9, 2026
Merged

os-justin merged 1 commit into
mainfrom
claude/issue-8693-expand-fields-rendering-claim

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8693

Comment-only. One file of published source in @object-ui/core, plus an empty-frontmatter changeset. No behaviour.

The measurement (arm B was chosen because of it, not before it)

Four families x three unexpanded primitives, resolved through getCellRenderer and rendered, on da5e4f69e:

unexpanded value lookup / master_detail / tree user
'u_1' u_1 — span class="block max-w-full truncate" the same span, u_1
'01HQZX9K2M4N6P8R' muted em-dash — the same span plus text-muted-foreground the same span, the raw ULID
'Ada Lovelace' Ada Lovelace, plain span the same span, the raw string

isLikelyOpaqueId measured alongside: false / true / false for the three values. lookup, master_detail and tree all resolve to LookupCellRenderer; user resolves to UserCellRenderer.

So the sentence was wrong twice over, and the second way is the one that matters:

  • user never drew the em-dash at all. UserCellRenderer has no resolver — no useLookupName, no isLikelyOpaqueId.
  • The three families that do draw it draw it only for values isLikelyOpaqueId accepts. That qualifier is the entire content of the behaviour, and the sentence never carried it. A reader was told a human-readable id would degrade visibly; it printed as if it were a name.

The opening paragraph carried the same claim in different words ("instead of a bare id placeholder") and is corrected with it — same defect, same docblock, one edit.

The instrument was observed to move. The same harness, same command shape, run against PR #8698's head 3692f7218: the user row changes to a data-slot="unresolved-reference" affordance that keeps the raw value (all three primitives), and the other three families come back byte-identical to the table above. A table that changes in exactly one column when exactly one renderer changes is measuring the tree, not printing a constant.

Premise check: PR #8698 has NOT landed

The dispatch said objectui#8434 / PR #8698 "has gone through the merge queue". Measured: state: open, merged: false, merged_at: null; timeline shows added_to_merge_queue at 2026-09-08T23:28:00Z with no removal and no merge. Content check agrees — origin/main at da5e4f69e contains no 8434 reference under the packages' sources, and packages/fields' newest commit is da5e4f69e itself.

It is in the queue, not through it. That does not change the ruling; it sharpens it. The table above is main today, and it is about to stop being main.

Why B (drop the rendering claim), not A (correct it)

The brief asked, if arm A were taken, what would make the new sentence checkable. The answer is nothing, and it is mechanical rather than a matter of taste:

  • @object-ui/core's package.json lists @object-ui/types, @objectstack/formula, @objectstack/spec in dependencies; peerDependencies and devDependencies contain no @object-ui/fields. The arrow runs the other way: @object-ui/fields depends on @object-ui/core. No test that can reach this file can render the cell a corrected sentence would describe.
  • Nothing in this repository compares docblock prose to rendered output. A replacement would be checked by whatever checked the last one.
  • Its shelf life was measured, not guessed: PR fix(fields): a user reference that resolved to nothing says so (objectui#8434) #8698 is sitting in the queue rewriting the user column of that very table. A sentence written from the table above would have been stale before it landed.

So the rendering claim is dropped and the storage claim kept — the half the set actually depends on, and the half __tests__/expand-fields.test.ts and the identity pins can check. Where an unexpanded reference prints belongs next to the renderers in @object-ui/fields, where a test can fail. The docblock now says that in place of the claim, with the measurement, so the next reader inherits the evidence rather than the conclusion.

The consumer list was stale by four — verified with a lit control

The docblock's enumeration of consumers is the argument for user's membership, and it already carries two recorded falsifications of its own counts. It has a third.

Re-derived over the whole tree with no pathspec:

git grep -lE 'EXPANDABLE_FIELD_TYPES|isExpandableFieldType|buildExpandFields'

96 files on da5e4f69e. Dropping tests, changesets and changelogs, the production readers fall into three radii: this file plus 8 naming the set directly, plus 4 calling isExpandableFieldType, plus 13 calling buildExpandFields.

Four live readers were missing from the list, and they share one cause: commit 7c96c9420 — "converge four private copies of the reference-bearing field family, and correct the 'fourth and last' claim" (objectui#5874 / #6064) — converged four more copies while correcting the previous count, and never added its own conversions here.

  • packages/app-shell/src/utils/resolveActionParams.ts — reads the set directly
  • packages/plugin-detail/src/HeaderHighlight.tsx — reads the set directly
  • packages/plugin-detail/src/RecordDetailDrawer.tsx — through isExpandableFieldType
  • packages/plugin-kanban/src/ObjectKanban.tsx — through buildExpandFields

All four are in the same commit, all four carry identity pins (expandableFamily.identity-5874.test.* in three packages). Added to the list in the same commit as the rendering fix — same defect class, same docblock, comment-only, no new verification surface.

Lit control (a zero-hit region is otherwise indistinguishable from a blind spot):

  • Population control: apps/ has 265 tracked files and zero hits for these names. The same no-pathspec command shape, run for a token known to live there (createRoot), fires on apps/console/src/main.tsx and four more. apps/ is inside the swept population.
  • Planted-consumer control: a file reading EXPANDABLE_FIELD_TYPES was written to apps/console/src/, the identical command re-run, and the delta was exactly one file — the plant. It was then removed and the sweep re-run: byte-identical to the pre-plant listing, no residue under apps/.
  • The control script hard-fails if the baseline sweep returns zero files. That guard fired for real: the first run had its cwd outside the repo, every git grep failed, and the before/after comparison "passed" on 0 == 0. The recorded reading is from the run after that was fixed.

Other claims in the same docblock that nothing can check

Listed, not fixed — the list is the deliverable. Filed as objectui#8716 for triage.

  1. "the server resolves it through the same expand path as lookup / master_detail" — a claim about objectql, a different repository. expand-fields.test.ts asserts what goes into $expand, never what a server does with it. Load-bearing in a way the removed sentence was not: it is the stated reason user is a member. Wants an anchor, probably not deletion.
  2. The tree note's "requesting it is harmless and forward-compatible" — same cross-repo class, plus a prediction about servers that do not exist yet. It carries a membership decision.
  3. "That was true for 15 days." — a hand-kept duration with no keeper. Weak, listed for completeness.
  4. The object-ref / filter-condition / recipient-picker exclusion — nearly fine, and the contrast is instructive: it is anchored (widgetHintOnly: true in app-shell/src/utils/paramValueShape.ts, measured present). What is missing is only the reverse link.

Verification

All at 1750d7710 unless stated. Verdict lines quoted from the tools, not from exit codes.

  • pnpm exec vitest run packages/core/ — Test Files 132 passed (132), Tests 2788 passed (2788).
  • Identity pins across five packages, run as one invocation (app-shell, plugin-detail, plugin-kanban, plugin-dashboard, components, plugin-grid) — Test Files 6 passed (6), Tests 78 passed (78).
  • pnpm --filter @object-ui/core^... build then pnpm --filter @object-ui/core run type-check — exit 0. The dependency closure was built first; an unbuilt tree would have reported TS2307 for every workspace import.
  • node scripts/check-changeset-presence.mjs — before the changeset: 1 source file(s) of 1 released package(s) changed, and this change adds no changeset. After: 1 source file(s) ... declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate. Its own verdict line, per the brief; the skip-changeset label is not used and reads nothing here.
  • node scripts/check-changeset-no-major.mjs — No changeset declares a major bump.
  • pnpm check:control-bytes — OK (scanned 6882 tracked text file(s); skipped 85 binary). Additional sweep of the diff for control bytes outside the NUL class: zero hits.
  • pnpm check:comment-mask-corpus — 1 file(s) disagree, within the residue objectui#7882 is holding open (ceiling: 1 file(s), 1517 over-masked bytes, 0 fabricated). Unchanged, and the one disagreeing file is not this one.
  • node scripts/check-governed-queue-guard.mjs --test packages/core/src/utils/expand-fields.ts — NOT GOVERNED.
  • ESLint: the whole of @object-ui/core via its own lint script with --format json — 231 files, 0 errors, 532 pre-existing warnings; the single warning in the edited file is @typescript-eslint/no-explicit-any at the buildExpandFields signature, untouched by this change. The narrowing to one package is safe to state rather than assume: eslint.config.js configures no project, projectService or parserOptions, so no type-aware rule exists and this diff cannot move the verdict on any file outside it.

Reverse verification

tsc was proved to actually read this file, so the green type-check is a measurement:

  • Mutation leg. The docblock now warns that writing a certain two-character sequence literally would end the comment. That warning was earned — the first draft of this edit contained it and would have shipped a syntax error. Re-injected deliberately: the anchor text count went 1 to 0 and the injected text count to 1 (proof it reached disk, not an editor exit code), the worktree blob hash diverged from the HEAD blob, and type-check went to exit 2 with 132 error TS lines, the first at expand-fields.ts(115,65) — the injection site.
  • Restore leg. git checkout HEAD -- packages/core/src/utils/expand-fields.ts, pointed explicitly at HEAD rather than a bare checkout from the index. Restoration proved by content: the worktree blob hash is byte-identical to the HEAD blob, git diff HEAD is empty, and type-check is back to exit 0. The script carries a trap ... EXIT INT TERM with an absolute path, and it ran only after the work was committed — a restore against uncommitted edits would have deleted the change itself and left every check green.

No pin was added, deliberately

This is a comment-only change; there is nothing to pin that would not be the same defect one level up. A test asserting "this docblock does not contain the string renders as" would be a pin over a claim nobody reads, and a test asserting what UserCellRenderer prints belongs in @object-ui/fields next to the renderer — where PR #8698 is already changing it, and where such a test can fail. The assertion that was observed to fail is the reverse verification above.

In-flight neighbours checked

PR #8684 (normalize-list-view.ts) and PR #8656 (record-title tests) are both open on packages/core. Neither touches expand-fields.ts or .changeset/8693-*; no overlap.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

… could not check, and correct its consumer list by four

The docblock over `EXPANDABLE_FIELD_TYPES` said an unexpanded `user` id
"renders as `—`". Measured on `da5e4f69e` by resolving each family through
`getCellRenderer` and rendering three unexpanded primitives:

| unexpanded value     | lookup / master_detail / tree | user             |
|----------------------|-------------------------------|------------------|
| 'u_1'                | u_1, plain truncated span     | u_1, same span   |
| '01HQZX9K2M4N6P8R'   | muted em-dash                 | the raw ULID     |
| 'Ada Lovelace'       | Ada Lovelace, plain span      | the raw string   |

Wrong twice over. `user` never drew the em-dash at all — `UserCellRenderer`
has no resolver, no `useLookupName`, no `isLikelyOpaqueId`. And the three
families that do draw it draw it only for values `isLikelyOpaqueId` accepts,
a qualifier the sentence never carried and the whole content of the
behaviour. The opening paragraph carried the same claim ("a bare id
placeholder") and is corrected with it.

The claim is DROPPED rather than rewritten. `@object-ui/core` does not depend
on `@object-ui/fields` in dependencies, peerDependencies or devDependencies,
so no test that can reach this file can render the cell a rewrite would
describe, and nothing here compares a docblock to rendered output: a
replacement would be checked by whatever checked the last one. Its shelf life
was measured too — PR #8698 sits in the merge queue rewriting the `user`
column of that table (on its head `3692f7218` all three primitives render an
unresolved-reference affordance that keeps the raw value; the other three
families are byte-identical on both trees), so a corrected sentence would
have been stale before it landed. The storage claim, which is the half this
set actually depends on and the half its tests can check, is kept.

The consumer list in the same docblock is a hand-kept claim of the same
class, and it was stale by four: objectui#5874 / #6064 converged four more
private copies — resolveActionParams, HeaderHighlight, RecordDetailDrawer,
ObjectKanban — while correcting the previous "fourth and last" claim, and
never added them here. Added, plus a re-derivation recipe: one `git grep`
over the whole tree with no pathspec, its measured counts, and the note that
the sweep was controlled by planting a reader under apps/console/src/ and
confirming the same command found it.

Comment-only; no behaviour. The changeset declares an empty frontmatter —
`check-changeset-presence` requires a declaration for published source even
when nothing releases.

Part of objectui#8693

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3477.6 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BkOatu4S.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) 15.67KB 5.75KB
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) 499.42KB 114.32KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 244.36KB 61.61KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 251.25KB 65.00KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.18KB 56.62KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.73KB 27.69KB
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.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
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.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
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) 13.64KB 4.59KB
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

@os-justin
os-justin added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 2f881a9 Sep 9, 2026
35 checks passed
@os-justin
os-justin deleted the claude/issue-8693-expand-fields-rendering-claim branch September 9, 2026 01:28
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.

expand-fields.ts documents a rendering behaviour UserCellRenderer has never had — an unexpanded user id printed as bare text, not as "—"

2 participants