Skip to content

fix(plugin-dashboard): converge the two private reference-bearing field tables onto core's published family (#5692) - #5888

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5692-dashboard-expandable-family
Aug 23, 2026
Merged

fix(plugin-dashboard): converge the two private reference-bearing field tables onto core's published family (#5692)#5888
os-zhuang merged 1 commit into
mainfrom
claude/issue-5692-dashboard-expandable-family

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#5692

packages/plugin-dashboard held two hand-maintained copies of the reference-bearing field family — LOOKUP_TYPES in recordFields.tsx and an inline disjunction inside computeLookupExpand in ObjectDataTable.tsx. Neither derived from nor pinned against @object-ui/core's published EXPANDABLE_FIELD_TYPES. Both now read core's object and carry the identity pin the earlier conversions carry (#4770 / #4790 / #4815 / #5312).

Convergence is ruled (PM, domain:ui seat, dispatching this card). The membership question the card left open was decided by measurement, not by preference — also the PM's instruction, and the measurement is below with its controls.


The membership measurement: is reference a live field type, or a dead spelling?

The card noted the sets are not in a subset relation either way, so convergence moves behaviour in two directions. The reference direction had a prior factual question, and the probe ran subject and controls on the same read, so a probe that had lost hold of the vocabulary fails as a broken probe rather than reporting the subject absent.

Leg A — the spec's closed FieldType vocabulary.Leg B — the real parse-time producer gate, FieldSchema.safeParse.

PROBE-A spec FieldType vocabulary size = 49
PROBE-A SUBJECT FieldType.includes('reference') = false (subject)
PROBE-A control+ FieldType.includes('lookup') = true OK
PROBE-A control+ FieldType.includes('master_detail') = true OK
PROBE-A control+ FieldType.includes('user') = true OK
PROBE-A control+ FieldType.includes('tree') = true OK
PROBE-A control- FieldType.includes('owner') = false OK
PROBE-A control- FieldType.includes('zzz_not_a_field_type') = false OK
PROBE-B SUBJECT FieldSchema.safeParse({type:'reference'}).success = false <- "Invalid option: expected one of \"text\"|…
PROBE-B control+ FieldSchema.safeParse({type:'lookup'}).success = true
PROBE-B control+ FieldSchema.safeParse({type:'master_detail'}).success = true
PROBE-B control+ FieldSchema.safeParse({type:'user'}).success = true
PROBE-B control+ FieldSchema.safeParse({type:'tree'}).success = true
PROBE-B control- FieldSchema.safeParse({type:'owner'}).success = false
PROBE-B control- FieldSchema.safeParse({type:'zzz_not_a_field_type'}).success = false
VERDICT: controls all held. `reference` is NOT A member of the spec's closed FieldType vocabulary.

Four positive controls held, two negative controls held, and both legs agree. reference is a dead alias spelling, not a live stored field type. It sits at exactly the level owner sat at before #4814 retired it, and at the level of pure nonsense.

The card's triage comment asked whether the PARAM_TYPE_ALIASES fold seen on a sibling surface (#5654) holds here. It does not fold on this road — the dashboard predicates read objectSchema.fields[…].type raw — but that strengthens rather than weakens the reading: the one surface where the spelling is live at all declares it a legacy dialect alias and folds it to lookup before any field-type data is read (PARAM_TYPE_ALIASES in packages/app-shell/src/utils/paramToField.ts). On the dashboard road the spelling was simply answered for, un-aliased and unreachable.

So EXPANDABLE_FIELD_TYPES is unchanged

The "two independent consumers reached for it" argument does not survive the measurement: they reached for a spelling no producer can emit. Per the PM's fence, a member is not added to a published shared set on plausibility — so packages/core/src/utils/expand-fields.ts gains prose only, not a member. Its Set literal is byte-identical.


The two behaviour directions, stated plainly

beforeafter
tree column on the dashboard tablenot $expand-ed$expand-ed
reference column on the dashboard table$expand-ednot $expand-ed
lookup / master_detail / user$expand-edunchanged
  • tree gains expansion — accepted.EXPANDABLE_FIELD_TYPES is the published authority and tree is a member; a self-referencing hierarchy column being $expand-ed on the dashboard road is the same treatment the form/grid road already gives it. Its cell renders the parent record's display name instead of a bare id.
  • reference loses expansion — a no-op on real data, on the measurement above. Nothing spec-compliant can produce such a column.

Where the behaviour actually lands. Only computeLookupExpand is on a live path (ObjectDataTable.tsx:338, every dashboard table fetch). isLookupType is exported from recordFields.tsx but has no production consumer in packages/ or apps/, and is not re-exported from the package entry — so its convergence is a contract change, not a behaviour change. Stated rather than implied, because a PR claiming two behaviour directions across both faces would be overclaiming. Filed separately as #5876.


Verification

All legs on the final commit 91c3f6b3f, tree clean (git status --porcelain = 0 lines). Heavy legs through scripts/pm/os-verify-lock.sh; exit codes captured before any pipe, and each gate quoted by its own printed verdict line.

Tests

pnpm exec vitest run packages/plugin-dashboard/ packages/core/src/utils/__tests__/ --maxWorkers=2
Test Files 112 passed (112)
Tests 1338 passed (1338)
os-verify-lock: VERDICT command-exit 0 · held the lock 180s (3m00s)

Type-check (dependency closures built first)

pnpm --workspace-concurrency=2 --filter "@object-ui/plugin-dashboard^..." build
os-verify-lock: VERDICT command-exit 0 · held the lock 71s
pnpm --filter @object-ui/plugin-dashboard --filter @object-ui/core type-check
Scope: 2 of 47 workspace projects
packages/core type-check$ tsc --noEmit && tsc -p tsconfig.test.json
packages/core type-check: Done
packages/plugin-dashboard type-check$ tsc --noEmit && tsc -p tsconfig.test.json
packages/plugin-dashboard type-check: Done
os-verify-lock: VERDICT command-exit 0

Scope: 2 of 47 with both script names echoed — not a zero-match silent green.

Gates

gateits own verdict line
check-changeset-presence.mjs✅ 5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs✅ No changeset declares a major bump.
check-changeset-fixed.mjs✅ All workspace packages are in the changeset fixed group.
check-control-bytes.mjs✅ check-control-bytes: OK (scanned 4890 tracked text file(s); skipped 85 binary).
check-phantom-dependencies.mjs✅ Every in-scope import is declared by the package that publishes it.
check:spec-symbols✅ spec symbol derivation: 1300 files scanned against 4959 spec export names
check:self-import✅ No package names itself inside its own src/.
check:esm-specifiersSpecifier leg: no un-ledgered package emits an extensionless relative specifier.

@object-ui/core was already a declared dependency of @object-ui/plugin-dashboard, so the new import declares nothing new — check-phantom-dependencies confirms it.

Lint — narrowed, and the narrowing declared

Ran the two touched packages' own lint rather than the repo-scale pnpm lint (a CI-owned run):

pnpm --filter @object-ui/plugin-dashboard --filter @object-ui/core lint
Scope: 2 of 47 workspace projects
packages/core lint: ✖ 510 problems (0 errors, 510 warnings)
packages/plugin-dashboard lint: ✖ 381 problems (0 errors, 381 warnings)
os-verify-lock: VERDICT command-exit 0

0 errors. The warning populations are the repo's standing @typescript-eslint/no-explicit-any backlog on lines this diff does not touch; the diff adds no any and no new export.

The narrowing is sound rather than merely convenient: eslint.config.js's languageOptions sets only ecmaVersion and globalsno project / projectService / parserOptions.project, so type-aware linting is not enabled and this diff cannot move the verdict on any file it does not touch.

check:eager-closure was not run: it reads apps/console/dist/eager-closure.json, emitted during a vite build of the console, and refuses as a broken gauge when absent. That build is CI's. Structurally the delta is nil anyway — the only new cross-package import is @object-ui/core into recordFields.tsx, and ObjectDataTable.tsx, the file that imports recordFields.tsx, already imported @object-ui/core before this change.


Reverse verification — four legs, all discriminating

Every leg carried a trap … EXIT INT TERM restore, and each mutation was confirmed on disk before its reading was accepted (removed text at 0 occurrences and injected text present — an editor's exit code proves neither).

No rebuild leg is owed, and here is why: the root vitest.config.mts aliases '@object-ui/core' → packages/core/src, and the tests import the subjects by relative path inside the same package's src. No dist/ is on the pins' resolution path, so a stale build cannot fake either colour. The green run's identity pin passing is itself proof that test and subject hold the same Set instance.

legmutationresult
1restore recordFields' original private copy4 red — its identity pin, its tree pin, its reference pin, and lookupTypes.retiredFieldType.test.ts's LIVE_RELATIONS control. 14 passed.
1brestore a member-identical private copy new Set(['lookup','master_detail','tree','user'])exactly 1 red — the identity pin. 17 passed.
2restore ObjectDataTable's original inline disjunction3 red — its identity pin, its tree expand pin, its reference pin. 15 passed.
2bmember-identical inline copyexactly 1 red — the identity pin. 17 passed.
restoregit checkout <branch> -- <both files>git diff HEAD --statempty, porcelain 0, all three mutation markers absent from disk, 18/18 green

Legs 1b and 2b are the point: every membership assertion in the suite is satisfied by a private set holding the same four strings, so a value check would pass on the defect. Only the spy on EXPANDABLE_FIELD_TYPES.has catches it.

Legs that do NOT discriminate, named as required:

  • In legs 1b and 2b, lookupTypes.retiredFieldType.test.ts stayed fully green. It cannot detect a member-identical fork, by construction — which is exactly the gap the new identity pin exists to close, not a defect in that file.
  • Legs 1/1b leave computeLookupExpand's pins green and legs 2/2b leave isLookupType's pins green. That per-face isolation is why these are two pins rather than one.
  • The ordinary-relation regression controls (lookup / master_detail / user, both column modes) stayed green in all four legs — which is what makes them controls: the convergence did not take the whitelist with it.

Scope

#4914's landed retirement gate is untouched and still runs ahead of both predicates. EXPANDABLE_FIELD_TYPES' Set literal is unchanged. No other package's copy was re-homed.

The card also asked that #5312's false "LAST private copy" claim be corrected. It is corrected in the family's canonical home, packages/core/src/utils/expand-fields.ts, whose hand-kept "four consumers" count was itself already stale by two. The sentence also stands verbatim in two app-shell files, which are outside this card's fence — filed as #5875 rather than reached into.

Out-of-scope findings (filed unassigned, finding + domain:ui, no pm:queue, after a duplicate search)


Generated by Claude Code

…ld tables onto core's published family (#5692)
`packages/plugin-dashboard` held two hand-maintained copies of the
reference-bearing field family — `LOOKUP_TYPES` in `recordFields.tsx` and an
inline disjunction inside `computeLookupExpand` in `ObjectDataTable.tsx`.
Neither derived from nor pinned against `@object-ui/core`'s published
`EXPANDABLE_FIELD_TYPES`, and both predate objectui#5312, whose docblock
recorded `paramToField` as the LAST private copy of the rule. Both now read
core's object and carry the identity pin the earlier conversions carry, so a
member-identical private copy fails rather than quietly re-forking the table.
The copies were not in a subset relation with the shared family in either
direction, so converging changes behaviour twice:
- `tree` is GAINED — a self-referencing hierarchy column is now `$expand`-ed on
the dashboard road, the treatment the form and grid roads already gave it.
- `reference` is DROPPED, measured rather than preferred: the spelling is absent
from `@objectstack/spec`'s closed `FieldType` vocabulary and refused by
`FieldSchema.safeParse` — probed with `lookup` / `master_detail` / `user` /
`tree` as live controls and the retired `owner` plus a nonsense spelling as
dead ones — so no spec-compliant object schema can declare a field whose
stored type is `reference`. The measurement is carried forward as an
executable pin, so the day the spec adds the spelling this goes red and the
membership question reopens.
`EXPANDABLE_FIELD_TYPES` itself is unchanged: the measurement did not license
widening a published shared set. Core's docblock ledger is corrected — its
hand-kept "four consumers" count was already stale by two, and objectui#5312's
"LAST private copy" claim was false by two more.
objectui#4914's retirement gate is untouched and still runs ahead of both
predicates.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3229.3 KB3990.2 KB
Main entry chunk (gzip)153.4 KB350 KB
Entry fileindex-DD6EH8pE.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.13KB3.77KB
app-shell (runtime-config.js)13.57KB4.78KB
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)504.16KB114.08KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
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)186.91KB44.22KB
plugin-dashboard (index.js)133.32KB34.42KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.34KB60.99KB
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.93KB14.60KB
plugin-list (index.js)111.80KB27.20KB
plugin-map (index.js)20.06KB6.62KB
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)3.77KB1.33KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
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)0.20KB0.18KB
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 (index.js)3.88KB1.85KB
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

@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 17:42
@os-zhuang
os-zhuang added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 3d79455Aug 23, 2026
23 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5692-dashboard-expandable-family branch August 23, 2026 17:42
Sign up for freeto 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.

finding(plugin-dashboard): two more private copies of the reference-bearing field family — #5312's "LAST private copy" claim is false

2 participants

@os-zhuang@claude