Repository navigation
Audit spec enum ↔ renderer coverage: names that validate but render nothing, and renderer-local dialect #2901
Description
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
defaultbranch 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
- Enumerate the pairs. Find spec
z.enumexports that have a renderer counterpart.grep -rn "export const [A-Za-z]*Schema = lazySchema(() => z.enum"overpackages/spec/src/gives the left side; the right side is aswitch/ternary/Recordkeyed on the same concept. - Diff each pair in both directions. Produce a table: enum, spec values, renderer values, missing, extra.
- 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 —
countvscount_filledin feat(grid): compute all eleven spec column summary aggregations (#2890) #2897 needed an actual semantic decision about what the two names mean. - Guard what gets fixed.
packages/plugin-grid/src/__tests__/summary-spec-parity.test.tsis 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/specas a devDependency of the consuming package — sixplugin-*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.
Activity
os-zhuang commentedon Jul 28, 2026
First pass done. Full audit doc in #2937; the deliverable table is below.
Five parallel auditors covered
ui/view,ui/chart+dashboard/report,ui/page+ui/component, field types + filter operators, and the peripheral UI enums. Every row was read off the dispatch code; the highest-stakes claims were then independently re-verified in source. Nothing was exercised at runtime, so symptoms are read off the code path, not observed.The premise was inverted
This issue described
ChartTypeSchemaas a 7-value spec enum thatplugin-chartshad outgrown with 13 renderer-local names. It's the reverse. The spec enum has 19 values (ui/chart.zod.ts:18). The 7-value list is an objectui fork —packages/types/src/zod/data-display.zod.ts:209— re-exported from@object-ui/typesunder the same symbol name the spec uses. Two packages exportChartTypeSchema; they differ by twelve values.That inversion turned out to be the audit's most useful result, because it generalises: the failure is rarely "renderer forgot a name". It is usually "a second definition of the vocabulary exists, and the renderer is faithful to the wrong one." Five such forks found — tracked in #2944.
Numbers
ui/packages/typespackages/typesdeny-list or local forkThe middle number is the structural one: 19 of 30 UI vocabularies have nothing mechanically tying them to the spec. That measures derivation, not coverage —
VisualizationTypeSchemaobviously has a renderer, it just restates the nine values instead of importing them. Drift there is unpoliced by construction, which explains why these went unreported rather than why they exist.Triage: by runtime symptom, not by direction
Direction A/B turned out to be the wrong axis for sequencing. What the user sees orders the work by how long a defect survives undetected.
PivotTable.tsx:114returns a sum forcount_distinct;DatasetReportRenderer.tsx:370silently draws a bar chart for 10 of 19 types. A blank cell is detectable; a plausible wrong number is not.element: 'toggle'deleting the entire filter bar, and 6 of 9 animation presets doing nothing becauseuseAnimation.ts:64keys its lookup in hyphens where the spec enum uses underscores.ai:chat_window, which Studio offers in the palette with a config panel and which has no renderer.Two upstream tracks: #2944 (retire the five forks — all inert today, and already-settled #2231 policy) and #2945 (consolidate the spec-side vocabularies: nine disagreeing filter-operator enums, two chart-type enums,
ActionTypevsWidgetActionTypeSchema). #2945 is upstream of most of the renderer work — no filter-builder fix is durable until there is one answer to "what is a filter operator".The sharpest single finding
ui/view.zod.ts:315says oftoggle: "Kept in the enum so existing configs keep rendering; do not author newtogglefilters."plugin-list/src/UserFilters.tsx:162returnsnullfor it — the whole filter bar vanishes. The component's own docstring at:122advertises "toggle: on/off toggle buttons per field".Three artifacts describe one value. The only one that executes is the one that says nothing.
Already fixed
#2940 ships the two items that needed no design decision:
$ncontains—FilterConditionField.tsx:114emitted a token appearing zero times in the spec, which objectui's ownconvertFiltersToASTthrows on while naming the correct spelling ($notContains) in its error message. Every "does not contain" sharing rule validated in the UI and was rejected downstream.secretinline-editable as plaintext — in neitherEDIT_WIDGETSnorINLINE_EXCLUDED_FIELD_TYPES, two lines frompassword, which is correctly excluded. Per ADR-0100 asecretis encrypted-at-rest and fail-closed.Adding
secrettripped the exclusion-set staleness guard, which is itself mis-specified in the way this issue predicts: it treatsObject.keys(fieldWidgetMap)as the set of real field types, but spec spellings reaching a widget through the alias table (secret→field:password) aren't keys.Why the existing guards can't see any of this
The repo has a mature drift-guard doctrine for schema shape (#2231 — three tests in
packages/types) and exactly one guard for enum-value renderer coverage (summary-spec-parity.test.ts, from #2897).list-view-spec-parity.test.tsasserts field keys, not enum values, so none of the drift above trips it.Two tests that look like coverage guards aren't:
fields/src/field-type-coverage.test.ts:19hard-codes its expected list instead of importingFieldType— which is exactly why theautonumberdisplay-map gap survived.block-config.test.ts:26asserts palette exclusions by hand rather than deriving inclusion fromPageComponentType, locking drift in instead of detecting it.The #2897 template ports without new machinery —
.optionsreads cleanly through thelazySchemawrapper on every enum tested. One prerequisite: of the 13 packages that would need a guard, 6 cannot import the spec today (plugin-list,plugin-charts,plugin-dashboard,plugin-report,components,mobilelack the@objectstack/specdevDependency).Suggested sequencing
The two clear bugs— fix(fields): emit the spec's$notContains, and keepsecretout of inline edit (#2901) #2940.docs(audit): spec enum ↔ renderer coverage audit for #2901 (#2937)
fix(fields): emit the spec's `$notContains`, and keep `secret` out of…
fix(list,data): bridge every spec view operator onto the filter AST (#…
docs(audit): re-validate the #2901 audit against spec 17, with three …
docs(audit): the devDep count is 6, not 5 (#2978)
refactor(types): retire the five forks that shadowed a @objectstack/s…
fix(spec-parity): render the six Tier-1 spec values right instead of …
fix(view,components): the spec→FilterBuilder operator table covers th…
refactor(spec)!: retire three orphan operator vocabularies (objectui#…
feat(spec,lint): the ui vocabularies admit what the renderers impleme…
feat(spec,lint): the `ui` vocabularies admit what the renderers imple…
refactor(types)!: the action sub-vocabularies derive from `@objectsta…
fix(spec-parity): the Tier-2 spec values render instead of validating…
fix(spec-parity): the Tier-2 spec values render instead of validating…
fix(spec-parity): the Tier-3 spec values render instead of red-boxing (…
feat(metadata): saveMeta persists the operator spellings the spec nor…
build(types): type-check this package's tests, so the spec-derivation…
os-zhuang commentedon Jul 30, 2026
The consolidation issue this audit spawned is closed — #2945
Leaving a pointer here since #2945 was the vehicle for acting on this audit's findings, and it is now fully dispositioned.
What the audit's findings turned into:
combo,WidgetActionType=ActionType, list-chart subset.extracted, lint's chart set derived)InlineActionSchema— objectstack-ai/objectstack#4135, producers migrated in objectstack-ai/cloud#926, Studio can author it in #3028The audit's own most useful output, in hindsight, was not the enum inventory — it was the discovery that a name validating but not rendering is only half the problem. The other half, which cost more to fix, was names the renderer implements that the spec never admits (
combo,formon widget actions,element:button.action), because those fail in the direction nothing checks: valid metadata rejected at save, or authored metadata silently stripped.Two follow-ups still open, both narrower than this audit: objectstack-ai/objectstack#4121 and objectstack-ai/objectstack#4157.
Whether this audit issue should stay open is a call for whoever owns it — the enum ↔ renderer sweep it describes is broader than what #2945 acted on.
refactor(types)!: rename the 15 objectui symbols that only collided w…
os-zhuang commentedon Aug 6, 2026
分诊:关闭(completed)—— 本单的交付物已经交付,且全部后继单已闭环。 维护者可否决重开(否决窗口,同 step 8 模式)。
依据:本单自己定义的交付物是「表,不是修复」
正文原话:「The deliverable for a first pass is the table, not the fixes」。那张表已在 2026-07-28 的评论里交付(审计文档 #2937,五个并行审计员覆盖
ui/view、ui/chart+dashboard/report、ui/page+ui/component、字段类型+过滤算子、外围 UI 枚举),并给出了完整读数:30 个 specui/具名枚举、11 个有packages/types之外的真实消费方、19/30 与 spec 之间没有任何机械绑定、24 条有用户可见缺陷的 Direction A 缺口、10 个带渲染器方言的面。正文第 3 步要求的分级(triage before fixing)也做了,而且换了更好的轴:按运行期症状(错值 / 静默缺失 / 红框 / 惰性)而不是按 A/B 方向排序。
后继单状态(本轮逐条核过 —— ⛔ 不取最后一条评论的字面,自己查了 state)
⇒ 七条后继全部闭环,无一悬空。#2940 早已带走两个无需设计决策的即时修复(
$ncontains拼写、secret内联明文可编辑)。为什么现在关而不是继续持有
最后一条评论把这个问题明确交给了 owner:「Whether this audit issue should stay open is a call for whoever owner it — the enum ↔ renderer sweep it describes is broader than what #2945 acted on」。分诊的判断是关,理由是发现分诊轮的纪律(#4949:发现的准确性半衰期以天计,囤积是负利息存款):
本单最有价值的一条结论(留档,不随关单丢失)
审计推翻了本单自己的前提:
ChartTypeSchema不是「7 值 spec 被 13 值渲染器超出」,而是反过来 —— spec 有 19 值,那份 7 值清单是 objectui 的 fork(packages/types/src/zod/data-display.zod.ts:209),用与 spec 相同的符号名再导出。由此得出的一般化结论值得比这张表活得更久:以及那条最锋利的观察:
toggle一个值被三份工件描述(spec 说保留兼容、组件 docstring 说提供开关按钮、代码return null),唯一会执行的是什么都不说的那一份。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code