Skip to content

Audit spec enum ↔ renderer coverage: names that validate but render nothing, and renderer-local dialect #2901

Description

@os-zhuang

The pattern

@objectstack/spec declares a vocabulary as z.enum([...]). A renderer consumes it as a switch, a ternary chain, or a Record lookup. When the two drift, neither side reports anything:

  • the author's metadata validates, because the schema accepts the name;
  • the renderer hits its default branch and draws nothing, or silently ignores the prop.

The only symptom is a blank cell that nobody knows should be full. No test fails, no console warning fires, no type error appears — the renderer's own union type is usually hand-written, so TypeScript agrees with the narrower set.

This is not hypothetical. #2897 found it in useColumnSummary: ColumnSummarySchema accepts eleven aggregation names, the hook computed five, and configuring count_unique passed validation and then rendered an empty footer cell. That one is fixed and guarded. The question this issue asks is where else the same shape exists.

Both directions matter

A. Spec wider than renderer — a published name renders nothing. This is the useColumnSummary case.

B. Renderer wider than spec — the renderer accepts names the protocol does not define. This is renderer-local dialect, the second de-facto contract Commandment #0.1 warns about. #2897 found this too: @object-ui/react exported a hook with a distinct aggregation that is not in the spec vocabulary at all (the spec calls it count_unique).

A third, milder shape is worth recording where it appears: the renderer restates the spec's values as a hand-written TS union instead of deriving them. Currently in sync means nothing — there is no mechanism keeping it that way.

Leads from a shallow pass

These came out of a quick grep, not an investigation. Treat them as starting points to verify, not as confirmed defects.

Enum Spec values Observation
ChartTypeSchema (ui/chart.zod.ts) bar column line area pie donut funnel (7) plugin-charts/src/AdvancedChartImpl.tsx:95 declares 13, including horizontal-bar, radar, scatter, combo, treemap, sankey — none in the spec enum. Also note horizontal-bar is kebab-case where the spec uses snake_case. Direction B lead. Worth checking whether this surface is meant to be spec-driven at all before concluding anything.
RowHeightSchema (ui/view.zod.ts) compact short medium tall extra_tall (5) ObjectGrid.tsx:272 restates all five as a hand-written union; the rowHeightCellClass chain at :1564 does cover all five (medium via the else branch). In sync today, but mirrored rather than derived.
VisualizationTypeSchema grid kanban gallery calendar timeline gantt map chart tree (9) Not yet checked against the view-type dispatch.
NavigationModeSchema page drawer modal split popover new_window none (7) Not yet checked. popover and new_window are the ones to look at first.

Other named enum schemas worth sweeping, roughly in descending likelihood of having a renderer counterpart: ui/view.zod.ts (32 enum declarations — the densest file), ui/component.zod.ts (22), ui/dashboard.zod.ts, ui/animation.zod.ts, ui/notification.zod.ts, ui/offline.zod.ts, ui/page.zod.ts. Filter operators are also worth including even though they live outside this list.

Suggested approach

  1. Enumerate the pairs. Find spec z.enum exports that have a renderer counterpart. grep -rn "export const [A-Za-z]*Schema = lazySchema(() => z.enum" over packages/spec/src/ gives the left side; the right side is a switch/ternary/Record keyed on the same concept.
  2. Diff each pair in both directions. Produce a table: enum, spec values, renderer values, missing, extra.
  3. Triage before fixing. Direction A splits into "should render, implement it" and "genuinely not applicable here, and the schema should say so". Direction B splits into "promote into the spec" and "delete the dialect". Neither is mechanical — count vs count_filled in feat(grid): compute all eleven spec column summary aggregations (#2890) #2897 needed an actual semantic decision about what the two names mean.
  4. Guard what gets fixed. packages/plugin-grid/src/__tests__/summary-spec-parity.test.ts is the template: it asserts the renderer's supported set equals the spec enum in both directions and fails the moment either side moves. It requires @objectstack/spec as a devDependency of the consuming package — six plugin-* packages already have it.

The deliverable for a first pass is the table, not the fixes. Coverage gaps and dialect are different problems with different owners, and the triage in step 3 is where the judgment is.

Why it is worth doing now

These gaps are invisible by construction — they will not surface through normal testing or bug reports, only through someone configuring the affected value and quietly getting nothing. Two instances turned up in a single PR that was not looking for them. Spec 16 is still RC, which makes both fixing coverage and retiring dialect cheap; after GA, removing a renderer-local name is a breaking change.

Related: #2890, #2897, #2231.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions