Skip to content

fix(app-shell): read view-column list identity in the canonical spelling only - #6098

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-5725-view-column-io-alias-reads
Aug 24, 2026
Merged

fix(app-shell): read view-column list identity in the canonical spelling only#6098
yinlianghui merged 2 commits into
mainfrom
claude/issue-5725-view-column-io-alias-reads

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#5725

view-column-io's colLabel and colFieldName carried the same
undeclared-alias read ViewColumnInspector retired in #5344, one file over.
FieldsListEditor — the sole importer — renders inside the inspector's own
panel
, so after #5344 the two surfaces gave the author two different answers
about what one column is called. This is the sixth site in that family and the
second editor-side one; it reuses #5344's retirement shape as triage directed.

The two defects that were not in dispute

1. The label chain was INVERTED, not merely tolerant. It read
label ?? header ?? field ?? accessorKey, preferring the undeclared header
over the declared field. A canonical column carrying a stray header key
displayed the alias' value instead of its own declared identity.

2. The field-name read backs more than a label. It feeds usedFieldNames(),
which the Add-field picker consults, so a spec-refused column reserved a field
name.

Both now read field / label only. The write path is untouched — the raw
columns array is still reordered and spliced without normalising, so no stored
document is rewritten.

The open question, and what was measured

The card declined to rule on whether colLabel should drop its alias fallback
outright or keep a best-effort human label, naming the asymmetry: an empty
field-key box invites re-authoring, an empty list row would leave the author
with nothing to click. The PM's lean was to drop the aliases and let the
positional fallback col N — already the last link in that chain — be the
something-to-click, conditional on that fallback actually producing a clickable
row.

Measured, not assumed.FieldsListEditor.retiredAliases.test.tsx mounts the
real editor, asserts the legacy row renders as col 1, and then clicks it
and asserts onSelectionChange fires with that label. FieldListRow is a
role="button" div carrying a grip, an icon, the label span and a remove
control, so it does not collapse on an empty label either — and the positional
fallback means the label is never empty. No escalation needed.

Verification

Reverse-verified as two separate legs, direction predicted before running,
each mutation proved on disk by grepping the injected text and separately the
removed text (landing site printed, anchor uniqueness asserted first), restored
under trap … EXIT INT TERM:

  • Leg A (restore the inverted label chain only): 5 red — the three
    label/row assertions, the declared-outranks-alias assertion, and the panel
    agreement pin. Both picker-reservation tests stayed green.
  • Leg B (restore the colFieldName alias only): 4 red — both field-name
    binding assertions, the usedFieldNames counter-probe, and the picker "Added"
    assertion. Every label/row test and the inspector pin stayed green.

The clean split is the point: the two defects are independent, and neither leg
can be satisfied by the other's fix.

Counter-probes are load-bearing and included: a canonical {field, label}
column still shows its declared label in the list and still reserves its name
in the picker ("Added" badge present), and a bare-string column is untouched.
Otherwise "the aliases are gone" would be satisfiable by breaking the list and
the picker outright.

One assertion was strengthened after leg A came back 4 red where 5 were
predicted
: a {field, label, header} row is green against both sources
(the old chain returns label too), so it could never reverse-verify. It is now
{field, header} — the shape the inversion actually bit on.

The flipped pin

#5344 committed a pin documented to flipViewColumnInspector.identityRead.test.tsx,
the test named "records the residue this card is fenced out of", asserting
getAllByText('Name') had length 1. It is flipped, not deleted: it now
asserts queryAllByText('Name') has length 0 over the whole panel, that the
row is named col 1, and that the identity controls are still empty — so the
"the panel agrees" claim is two-sided. The count goes 2 → 1 → 0 across the two
cards.

Gates (union run on 27d56b063, clean tree)

gateexit
pnpm --filter '@object-ui/app-shell^...' build0
pnpm --filter @object-ui/app-shell type-check0
path-filtered vitest run (3 files, 17 tests)0 — 17 passed
pnpm --filter @object-ui/app-shell lint0 errors (2659 pre-existing warnings; none in the new files)
node scripts/check-changeset-presence.mjs0
node scripts/check-control-bytes.mjs0
node scripts/check-type-check-coverage.mjs0
node scripts/check-lint-coverage.mjs0
pnpm check:esm-specifiers0
pnpm check:i18n-keys0

Whole-package app-shell vitest was not run — it is ~784 s and holds the
shared verify lock; CI runs the farm exactly once regardless.

Note on the size of defect 2

The card priced it as "hiding a field the author is entitled to add". Measured in
AddFieldPopover's own source: usedNames does not filter the option list
(only the search query does) — it renders an "Added" badge and the option stays
clickable. So a spec-refused column mislabelled a field as taken; it never
blocked adding it. Real and reachable, smaller than priced. Recorded in the test
docblock so the pin says what the mechanism does.


Generated by Claude Code

…ing only
`view-column-io`'s `colLabel` and `colFieldName` carried the same
undeclared-alias read `ViewColumnInspector` retired in objectui#5344, one file
over. `FieldsListEditor` — the sole importer — renders inside the inspector's
own panel, so after objectui#5344 the two surfaces gave the author two
different answers about what one column is called.
Two consequences the list surface had that the inspector did not:
- `colLabel`'s chain was INVERTED, not merely tolerant:
`label ?? header ?? field ?? accessorKey` preferred the undeclared `header`
over the declared `field`, so a canonical column carrying a stray `header`
displayed the alias instead of its own identity.
- `colFieldName` feeds `usedFieldNames()`, which the Add-field picker reads,
so a spec-refused column reserved a field name and the picker tagged that
field "Added" for a column no accepted document binds.
Both now read `field` / `label` only. A column the canonical keys cannot name
falls through to the positional label `col N` that already sat at the end of
that chain, so the row is named and still selectable — measured, not assumed.
The write path is untouched: no stored document is rewritten.
Fixes#5725
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
…,header} row is green against both sources
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@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-D4gavI7R.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.44KB
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

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