Skip to content

refactor(plugin-dashboard): one relation predicate, not two that agree by coincidence - #6099

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-5876-one-lookup-predicate
Aug 24, 2026
Merged

refactor(plugin-dashboard): one relation predicate, not two that agree by coincidence#6099
yinlianghui merged 2 commits into
mainfrom
claude/issue-5876-one-lookup-predicate

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#5876

Option B, triage's preference: computeLookupExpand now calls isLookupType
instead of carrying its own copy of it. The package holds one relation predicate.

What was there

ObjectDataTable.tsx held a local isLookup inside computeLookupExpand that was
identical to the exported isLookupType in recordFields.tsx — same retirement
gate, same EXPANDABLE_FIELD_TYPES.has, verified line-for-line on the merge-base
(7c96c9420). They agreed because #5692 had just pointed both at the same set, and
nothing kept them agreeing afterwards.

computeLookupExpand now calls isLookupType at both call sites, and the module no
longer imports EXPANDABLE_FIELD_TYPES, isRetiredFieldType or
reportRetiredFieldType at all. isLookupType gains its first production consumer,
so #4914's item-9 coverage in lookupTypes.retiredFieldType.test.ts keeps working
untouched — the asymmetry that makes B cheaper than A.

Behaviour-preserving, and that is measured, not assumed

Triage called this "behaviour-affecting". With identical bodies it is not, and both
candidate deltas were checked:

  • Boolean answers. Identical bodies give identical answers. tree is still
    expanded, reference is still refused, ordinary relations are still carried.
  • Warning emission count.reportRetiredFieldType dedupes per spelling, in
    one module-level Set inside @object-ui/core
    (retired-field-types.ts: "the dedupe is per SPELLING, not per face"). Both bodies
    already shared that Set, so routing two callers through one function cannot move
    the count. If the key had been per call site, this would have been a real delta.

Nothing published moves: isLookupType is not re-exported from index.tsx, so it is
not on @object-ui/plugin-dashboard's published surface. patch, not minor.
renderFieldValue was deliberately not moved (#6014 reads that seam).

Why the pin is identity, not behaviour

A refactor with no observable delta cannot be pinned by a behavioural test. Every
assertion in the three pre-existing describes — tree expands, reference does not,
ordinary relations do — passes identically on a revert. They are controls for this
change, not pins of it.

The new pin in expandableFamily.identity-5692.test.ts has two halves that fail for
different reasons:

  • `computeLookupExpand` CALLS `isLookupType` — in BOTH column modes — a vi.doMock
    double (surgical: not hoisted, so the rest of the file stays on the real module)
    records the call in the explicit-whitelist and auto-derive paths.
  • `ObjectDataTable.tsx` holds no second predicate body — source read with comments
    stripped, so the convergence note that names those symbols cannot fake a hit.

Ablation, direction predicted before running: restore the merge-base
ObjectDataTable.tsx and both new pins go RED while everything else stays GREEN.
Observed exactly that — 2 failed | 18 passed, failing on
explicit whitelist answered the relation question without asking isLookupType and
computeLookupExpand stopped calling the shared predicate. Mutation was proven on
disk before the run (local body restored: 1 hit; isLookupType(: 0 hits; control
computeLookupExpand: 5 hits), and restored under trap … EXIT INT TERM with
git diff HEAD --stat confirmed empty afterwards.

The first ablation leg found a real flaw and it was fixed in a second commit: the
source pin failed on its control line ("probe stripped the code away"), the same
message a broken probe would give — control and subject were indistinguishable. The
controls are now text present in both versions of the file.

Gates — all at 42c38c2a4, the final commit

gateexit
pnpm --filter @object-ui/plugin-dashboard type-check0 (script name echoed; both tsc --noEmit and tsc -p tsconfig.test.json)
root vitest over packages/plugin-dashboard/src0 — Test Files 80 passed (80), Tests 754 passed (754)
pnpm --filter '...@object-ui/plugin-dashboard' type-check (downstream)0 — Scope: 7 of 47, all 7 Done
pnpm lint (repo-wide eslint ., no narrowing)0 — 47 successful, 47 total
node scripts/check-changeset-presence.mjs0
node scripts/check-changeset-no-major.mjs0
node scripts/check-control-bytes.mjs0
pnpm check:esm-specifiers0

The downstream sweep is the prefix form — the package and its dependents
(@object-ui/app-shell, apps/console, apps/site, three examples), i.e. the
contract direction. It went red once on Cannot find module '@object-ui/example-schema-catalog' — the unbuilt-closure class, not this diff:
building examples/** flipped it green with the diff unchanged.

check:esm-specifiers was run because an import was added to ObjectDataTable.tsx;
it is a step inside the Type Check job, derived by enumerating that job's own step
list rather than top-level script names.


Generated by Claude Code

…e by coincidence
`computeLookupExpand` in `ObjectDataTable.tsx` carried its own `isLookup`,
byte-identical to the exported `isLookupType` in `recordFields.tsx` once
objectui#5692 had pointed both at `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES`.
Nothing kept them aligned. `computeLookupExpand` now calls `isLookupType` — its
first production consumer — and the module no longer imports the shared family
or the retirement gate.
Behaviour-preserving, measured rather than assumed: identical bodies give
identical boolean answers, and `reportRetiredFieldType` dedupes per SPELLING in
one module-level set in `@object-ui/core` that both bodies already shared, so
the warning count cannot move either.
A refactor with no observable delta cannot be pinned by a behavioural test, so
the pin is identity: `computeLookupExpand` is observed CALLING `isLookupType`,
and `ObjectDataTable.tsx` is read at source level to confirm no second body
survives for it to call instead.
…r its own ablation
Measured on the first ablation leg: a genuine revert failed the pin on its
CONTROL line ('probe stripped the code away'), which is the message a broken
probe would give. Control and subject were indistinguishable. The controls are
now text present in BOTH versions of the file, and the isLookupType call count
moved to the subject where it belongs.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3220.9 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-CmPttahD.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.15KB114.53KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)166.86KB46.08KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.04KB61.85KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 24, 2026 17:09
@yinlianghui
yinlianghui added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit a100f77Aug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5876-one-lookup-predicate branch August 24, 2026 17:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude