Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

The legacy list-view vocabulary normalizeListViewSchema exists to accept has no declared type — every fixture exercising the fold must cast #4337

Closed as not planned

Description

@yinlianghui

Observation-class finding, surfaced while wiring @object-ui/plugin-list's tests into tsc for #4040 (tranche 3). Nothing a user meets today; it costs type safety in exactly the tests that cover the fold.

What

normalizeListViewSchema (packages/core/src/utils/normalize-list-view.ts) is the vocabulary bridge #2890 introduced: it folds objectui's historical list-view spellings onto @objectstack/spec's canonical ones — fields → columns, densityMode → rowHeight, filters → filter, the show* flags → userActions, aria: { label, describedBy } → { ariaLabel, ariaDescribedBy }, sharing: { visibility, enabled } → { type }, and the per-column identity keys.

Its signature is:

export function normalizeListViewSchema< T >(schema: T): T

Fully generic. There is no type for what it ACCEPTS. ListViewSchema describes the canonical surface — the fold's OUTPUT, which is all ListView reads, because ListView.tsx:581 is normalizeListViewSchema(propSchema) before anything else. The legacy INPUT vocabulary is documented only in that function's prose comment.

That is fine for production code, which passes stored metadata straight through. It bites the tests that exist to prove the fold still happens, because those must author the legacy spelling in a position typed as the canonical one:

src/__tests__/ListView.test.tsx(561,9): error TS2353: Object literal may only specify known
  properties, and 'label' does not exist in type '{ ariaLabel?: ...; ariaDescribedBy?: ...;
  role?: ...; live?: ... }'.
src/__tests__/ListView.test.tsx(579,9): error TS2353: Object literal may only specify known
  properties, and 'enabled' does not exist in type '{ type?: "personal" | "collaborative";
  lockedBy?: string }'.

Both compiler verdicts are correct, and neither fixture is wrong — they are pre-fold input, and the two "obvious" fixes are both worse than the cast:

PR (this tranche) therefore casts through a named local helper (authored(...)) with the reasoning written next to it. Three test files now carry that helper or an equivalent.

Worth knowing: the filter key is a third case, and a sharper one

The normalizer's own contract note already records it:

filters → filter (#2890 scope A step 4). A key rename only: BOTH keys carry an ObjectQL FilterNode array ([['stage','=','won']]) everywhere in objectui — every consumer passes the value straight to $filter. The spec types filter as ViewFilterRule[] ({field, operator, value} objects), so objectui's field is typed from the spec but used as something else. That mismatch is real and out of scope here; converting formats inside a vocabulary fold would change what reaches the data source.

So ListViewSchema.filter is declared as one shape and carries another in production, not only in tests — the declaration and every real value disagree. ListView.emptyStateFilteredCopy.test.tsx authors the ObjectQL form because that is the form the renderer receives, and needs the same cast for it.

Why it is finding and not queued

The fold is permanent by design — the normalizer's own comment says stored view metadata in user databases still carries the legacy keys, so the renderer cannot stop accepting them. That means this is not absorbed by #2890 finishing; it outlives it.

Resolving it is one of two shapes, and picking between them is a contract call:

  • Declare the legacy input surface next to the vocabulary it bridges — a ListViewSchemaInput (or similar) in @object-ui/core beside normalizeListViewSchema, with normalizeListViewSchema(schema: ListViewSchemaInput): ListViewSchema as the signature. Fixtures then say what they are with no cast, and the fold's accepted vocabulary becomes checkable rather than prose. This is the shape [P2] objectui: every package type-checks its tests or declares why not — and guards must prove they can fail (measured: 355 errors across 19 packages) #4040 itself prescribes for the dialect problem ("declare the dialect next to the vocabulary it extends").
  • Leave it undeclared and accept the casts as the marker of pre-fold input, on the grounds that the legacy vocabulary is frozen and should not gain a maintained type.

The filter mismatch above is a separate, larger question (it is a real production divergence, not a test-only one) and may want its own card whichever way this one goes.

Filed standalone rather than as a sub-issue of #2890 for the reason given above — #2890 completing does not close this.

Refs #4040, #2890, #2231.


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Findings-triage route B: held — finding stays.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. added theissue type on Aug 17, 2026
  3. os-steve commented on Aug 17, 2026

    @os-steve
    Collaborator

    Concentrated finding round (unified triage seat, 2026-08-17): closed not planned. The card itself establishes that both "obvious" fixes are worse than the status quo (re-spelling fixtures deletes the fold's only coverage; widening ListViewSchema re-forks the vocabulary #2890 unified), and the landed resolution — a named authored(...) cast helper with its reasoning written beside it — is the accepted answer. Declaring a legacy-input type would mean maintaining a second public vocabulary surface forever, for test ergonomics only.

    Reopen trigger: the legacy fold gains new spellings or the cast helper spreads past test files into production code — either changes the cost/benefit. Maintainer veto: reopen is free.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions