Repository navigation
Audit spec enum ↔ renderer coverage: names that validate but render nothing, and renderer-local dialect #2901
Description
Activity
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
Named enum exports in spec ui/30 …with a real consumer outside packages/types11 …named only in a packages/typesdeny-list or local fork14 …with zero mentions anywhere in the repo 5 Direction A gaps with a user-visible defect 24 …plus inert ones (validate, change nothing) 11 Surfaces carrying renderer-local dialect 10 Enum-value parity guards in the repo 1 The 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.
Tier Count Symptom Issue 1 6 Silently wrong output — worse than #2897. 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.#2941 2 15 Silently absent — the #2897 shape. Includes 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.#2942 3 3 Loud red box. Cheapest to triage precisely because it cannot hide — includes ai:chat_window, which Studio offers in the palette with a config panel and which has no renderer.#2943 4 11 Inert — validates, changes nothing. Live the moment anyone wires the surface up. #2945 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.- Vocabulary consolidation (#2901): what shipped, and why Track C items 1–3 are not planned #2945 — consolidate spec-side vocabularies while 16 is still RC. After GA, retiring a name is breaking.
- Retire the five objectui forks that shadow a @objectstack/spec symbol name (#2901) #2944 — retire the five forks. Mechanical, and inert today.
- Tier 1 (#2901): spec values that render silently WRONG output #2941 (Tier 1), then Tier 2 (#2901): spec values that validate and then render nothing #2942 (Tier 2), guarding each pair as it's fixed.
- Tier 3 (#2901): unregistered PageComponentType values, and the ai:chat_window palette trap #2943 and the enforce-or-remove calls in Vocabulary consolidation (#2901): what shipped, and why Track C items 1–3 are not planned #2945.
- added 9 commits that reference this issue
on Jul 28, 2026 - added a commit that references this issue
on Jul 30, 2026 - added a commit that references this issue
on Jul 30, 2026 - added 4 commits that reference this issue
on 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:
Finding class Outcome Orphan vocabularies with zero consumers deleted — objectstack-ai/objectstack#4061 Enums narrower than what the renderers implement widened — objectstack-ai/objectstack#4070 ( combo,WidgetActionType=ActionType, list-chart subset.extracted, lint's chart set derived)Renderer-local operator dialects all three tables pinned to the spec with parity guards — #2974, #2940, #2989, #3022 A renderer prop the spec never declared InlineActionSchema— objectstack-ai/objectstack#4135, producers migrated in objectstack-ai/cloud#926, Studio can author it in #3028One vocabulary in three unguarded places derived + guarded — objectstack-ai/objectstack#4153 The silent-drop this audit found while tracing operators fixed and verified, objectstack-ai/objectstack#3948 closed Vocabulary narrowing (Track C items 1–3) ⛔ not planned — the harms they were preconditions for are all fixed by cheaper means The 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.
- added a commit that references this issue
on Jul 30, 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)
单 内容 状态 #2941 Tier 1 静默错值(6 条) closed / completed #2942 Tier 2 静默缺失(15 条) closed / completed #2943 Tier 3 红框(3 条) closed / completed #2944 退役五个影子 spec 符号名的 objectui fork closed / completed #2945 spec 侧词汇收敛(含 Track C 1–3 的 not planned 裁决) closed / completed #2997 / #3014 审计衍生的两条独立缺陷 closed / completed ⇒ 七条后继全部闭环,无一悬空。#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:发现的准确性半衰期以天计,囤积是负利息存款):
- 本单已 9 天无新动作,而它记录的读数(行号、枚举取值、fork 清单)在 Tier 1 (#2901): spec values that render silently WRONG output #2941–Vocabulary consolidation (#2901): what shipped, and why Track C items 1–3 are not planned #2945 的修复落地后已被大面积改写 —— 继续持有的是一份已经过时的地图,任何人捡起来都必须重新审计一遍,而重新审计不需要这张旧表;
- 「sweep 比 Vocabulary consolidation (#2901): what shipped, and why Track C items 1–3 are not planned #2945 实际动的范围更广」这句是对的,但它描述的是下一次审计,不是本单的未竟部分。真要做,应基于今天的 main 重新立单,⛔ 不是让一份 7 月 28 日的清单继续挂着冒充待办;
- 两条仍开的窄口径后继(objectstack#4121、objectstack#4157)各自独立在册,不依赖本单存活。
本单最有价值的一条结论(留档,不随关单丢失)
审计推翻了本单自己的前提:
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
The pattern
@objectstack/specdeclares a vocabulary asz.enum([...]). A renderer consumes it as aswitch, a ternary chain, or aRecordlookup. When the two drift, neither side reports anything: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:ColumnSummarySchemaaccepts eleven aggregation names, the hook computed five, and configuringcount_uniquepassed 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
useColumnSummarycase.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/reactexported a hook with adistinctaggregation that is not in the spec vocabulary at all (the spec calls itcount_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.
ChartTypeSchema(ui/chart.zod.ts)bar column line area pie donut funnel(7)plugin-charts/src/AdvancedChartImpl.tsx:95declares 13, includinghorizontal-bar,radar,scatter,combo,treemap,sankey— none in the spec enum. Also notehorizontal-baris 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:272restates all five as a hand-written union; therowHeightCellClasschain at :1564 does cover all five (mediumvia the else branch). In sync today, but mirrored rather than derived.VisualizationTypeSchemagrid kanban gallery calendar timeline gantt map chart tree(9)NavigationModeSchemapage drawer modal split popover new_window none(7)popoverandnew_windoware 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
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.countvscount_filledin feat(grid): compute all eleven spec column summary aggregations (#2890) #2897 needed an actual semantic decision about what the two names mean.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.
Related: #2890, #2897, #2231.