Skip to content

finding(core): rowHeightToDensityMode coerces an off-spec rowHeight to comfortable, the opposite of what the spec bridge now does with the same input #4440

Description

@yinlianghui

Found while implementing #4352 (PR #4439). Observation-class: nothing a user hits today, and no fix is proposed here.

What

Two surfaces narrow a list view's rowHeight onto the renderer's three-step density vocabulary, and after #4352 they answer differently for the same off-spec input.

packages/core/src/utils/normalize-list-view.ts:62:

export function rowHeightToDensityMode(rowHeight: unknown): DensityMode {
  if (typeof rowHeight === 'string' && rowHeight in ROW_HEIGHT_TO_DENSITY_MODE) {
    return ROW_HEIGHT_TO_DENSITY_MODE[rowHeight as RowHeight];
  }
  return 'comfortable';
}

Its doc comment states the intent plainly: "View metadata is user-authored, so rowHeight is not guaranteed to be one of the five spec values — an unknown one lands on comfortable rather than rendering an undefined density."

packages/react/src/spec-bridge/bridges/list-view.ts, after #4352, takes the other branch of the same question: an off-spec rowHeight yields no density at all, so the renderer's own default applies. That was the #4352 ruling, on AGENTS.md #0.1 grounds — one strict contract beats N dialects, and a bad rowHeight gets fixed at the producer.

So @object-ui/plugin-list's ListView (the rowHeightToDensityMode caller, ListView.tsx:877) resolves a garbage rowHeight to comfortable, while the spec bridge resolves the same string to nothing. Neither is obviously wrong in isolation; what is odd is that one metadata-driven system holds both answers.

Why it is observation-class and not a defect today

The empirical sweep run for #4352 found zero authored off-spec rowHeight values anywhere reachable: this repo (examples/, apps/ including the console's view metadata, all packages/, every JSON/YAML), the objectstack example apps (showcase and CRM), and one real downstream app. Every authored value is one of the five RowHeightSchema admits. The legacy densityMode alias cannot produce an off-spec value either — DENSITY_MODE_TO_ROW_HEIGHT is typed Record< DensityMode, RowHeight >, so its codomain is compact / medium / tall.

The divergence is therefore reachable only by metadata that does not exist, which is exactly why #4352 could delete its four dead spellings without a conversion card.

The decision, when someone takes it

Two readings, and they are not equally cheap:

  1. Align on strictness — drop the 'comfortable' fallback so an off-spec rowHeight resolves to nothing on both surfaces, matching #0.1. Note this fallback is not the same shape as finding(react): bridgeListView's mapDensity carries four rowHeight spellings no spec-valid list view can hold #4352's dead keys: it is a catch-all, not four named aliases, so it does not fossilize any particular misspelling into a second vocabulary. It also affects a live render path rather than an unreachable one, so unlike finding(react): bridgeListView's mapDensity carries four rowHeight spellings no spec-valid list view can hold #4352 it would want its own empirical check on what ListView actually receives at runtime, including from stored per-user view overrides.
  2. Leave it and write down why — the two surfaces genuinely differ in position: the bridge is a translation boundary that can decline to answer, while rowHeightToDensityMode returns a non-optional DensityMode and has no "no answer" to give without changing its signature. If that asymmetry is the intended design, the doc comment should say so and name the bridge's opposite choice, so the next reader does not file this twice.

Worth deciding deliberately rather than by whichever surface someone reads first.


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 12, 2026
  2. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    CollaboratorAuthor

    CLAIM + RULING — PM seat session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4440-rowheight-one-answer, one dev agent, one PR, Fixes #4440. finding → pm:queue.

    Ruling (delegated, veto window open): one system, one answer for off-spec input — and the strict answer wins, per #0.1 and the just-landed #4352 precedent: rowHeightToDensityMode's coerce-unknown-to-'comfortable' branch retires; an off-spec rowHeight yields the same no-mapping outcome react's bridge now gives (undefined → the consumer's default), so the two surfaces agree. Conditions binding on the dispatch:

    1. Empirical check FIRST, per the card's own caution — the coercion sits on a live render path: enumerate rowHeightToDensityMode's callers, and for each, measure what an off-spec input renders TODAY through the coercion vs AFTER (the consumer's default when the mapping abstains). PR refactor(react): bridgeListView maps only the five rowHeight spellings the spec admits (#4352) #4439's three-repo sweep already proves no off-spec rowHeight is authored anywhere, so the expected reading is "unreachable, zero rendered change" — but the reading governs; a reachable path whose rendered output changes → report the delta as a measured, documented consequence (likely acceptable — it aligns with the bridge — but it must be seen, not assumed).
    2. The surviving map gains the same Record<RowHeight, …> typing refactor(react): bridgeListView maps only the five rowHeight spellings the spec admits (#4352) #4439 gave its react twin (the upstream-enum-growth build-failure property), if it lacks it.
    3. Pin the agreement: one test asserting both surfaces give the same answer class for an off-spec input, so the disagreement cannot regrow silently.
    4. Changeset graded by the refactor(react): bridgeListView maps only the five rowHeight spellings the spec admits (#4352) #4439 precedent (behavior surface changes for off-spec input on a published function → minor with semantics; if the function is module-private, patch — measure), never major.

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions