Summary
One intent — "show this thing only when the predicate holds" — is spelled two different ways depending on which schema you are authoring, and the guard that catches the wrong spelling only runs in one direction.
The action schemas already recognise the other spelling and rename it: actionObject() registers visibleWhen → visible, showWhen → visible, disabledWhen → disabled (action.zod.ts:776), and ACTION_PARAM_KEY_ALIASES maps visiblewhen/visibleon/visibility → visible (L74). The comment there states the motive exactly:
ADR-0089 made visibleWhen the canonical predicate on view/page schemas. An author who learned it there would silently lose a param's capability gate here.
The reverse alias does not exist. An author who learns visible on an action and writes it on a field or on userActions.delete gets a bare unknown-key error that never names the key they should have used.
Measured (spec dist on main, ObjectSchema.create)
ACCEPTED field.visibleWhen (canonical)
REJECTED field.visible
Unrecognized key(s) on this field: `visible`. Until #4001 closed this shape
these were dropped silently — …
REJECTED userActions.delete.visible
Unrecognized key: "visible"
REJECTED userActions.delete.disabled
Unrecognized key: "disabled"
Neither message mentions visibleWhen / disabledWhen. RowCrudActionOverrideSchema is a plain z.object({…}).strict() — it has no aliases / guidance map at all, so it cannot say anything beyond zod's default.
The arms accepted also differ, which matters for any unification:
| surface |
key |
false literal |
CEL string |
{dialect, source} |
| action / action param |
visible |
✅ (#5970) |
✅ |
✅ |
field / section / component / userActions.* |
visibleWhen |
❌ |
✅ |
✅ |
On userActions.edit/delete the boolean lives on a sibling key (enabled: false), so the same "settled at authoring time" case is spelled differently again.
Why raise it
The asymmetry argument the repo already accepts, applied to itself. From action.zod.ts (#5970, on visible vs disabled before they were unified):
An asymmetry between two keys that mean the same kind of thing is a dialect nursery: it teaches each consumer to keep its own widening … and every one of those is a second de-facto contract (Prime Directive #12).
Two spellings for one intent across neighbouring schemas is the same nursery one level up. Today it costs authors a failed parse and a search; the aliases already carry the admission that authors DO move between these surfaces.
Asks (in order of cost)
- Symmetry, no behaviour change — give
RowCrudActionOverrideSchema (and the field / section / component shapes) the same aliases treatment the action shapes have: visible → visibleWhen, showWhen → visibleWhen, disabled → disabledWhen. Purely a better error; nothing that parses today changes. Note the boolean case on userActions.* should point at enabled, not at visibleWhen, or the hint will just move the confusion.
- A platform decision on ONE canonical, recorded rather than left implicit.
visibleWhen is the majority surface and already ADR-0089's canonical, so converging there (with visible demoted to a registered alias on the action shapes) is the smaller move — but it needs a call on:
Item 1 stands on its own even if item 2 lands as "keep both" — right now the guard protects one direction of a two-way street.
Summary
One intent — "show this thing only when the predicate holds" — is spelled two different ways depending on which schema you are authoring, and the guard that catches the wrong spelling only runs in one direction.
visible/disabled—ActionSchema(ui/action.zod.ts:1131),ActionParamSchema(L383).visibleWhen/disabledWhen— fields (data/field.zod.ts:814), form sections (ui/view.zod.ts:1757, L1858), page components (ui/page.zod.ts:150), per-option predicates (field.zod.ts:176), and the built-in row CRUD overridesuserActions.edit/delete(data/object.zod.ts:1079).The action schemas already recognise the other spelling and rename it:
actionObject()registersvisibleWhen → visible,showWhen → visible,disabledWhen → disabled(action.zod.ts:776), andACTION_PARAM_KEY_ALIASESmapsvisiblewhen/visibleon/visibility → visible(L74). The comment there states the motive exactly:The reverse alias does not exist. An author who learns
visibleon an action and writes it on a field or onuserActions.deletegets a bare unknown-key error that never names the key they should have used.Measured (spec
distonmain,ObjectSchema.create)Neither message mentions
visibleWhen/disabledWhen.RowCrudActionOverrideSchemais a plainz.object({…}).strict()— it has noaliases/guidancemap at all, so it cannot say anything beyond zod's default.The arms accepted also differ, which matters for any unification:
falseliteral{dialect, source}visibleuserActions.*visibleWhenOn
userActions.edit/deletethe boolean lives on a sibling key (enabled: false), so the same "settled at authoring time" case is spelled differently again.Why raise it
The asymmetry argument the repo already accepts, applied to itself. From
action.zod.ts(#5970, onvisiblevsdisabledbefore they were unified):Two spellings for one intent across neighbouring schemas is the same nursery one level up. Today it costs authors a failed parse and a search; the aliases already carry the admission that authors DO move between these surfaces.
Asks (in order of cost)
RowCrudActionOverrideSchema(and the field / section / component shapes) the samealiasestreatment the action shapes have:visible → visibleWhen,showWhen → visibleWhen,disabled → disabledWhen. Purely a better error; nothing that parses today changes. Note the boolean case onuserActions.*should point atenabled, not atvisibleWhen, or the hint will just move the confusion.visibleWhenis the majority surface and already ADR-0089's canonical, so converging there (withvisibledemoted to a registered alias on the action shapes) is the smaller move — but it needs a call on:ExpressionInputSchemahas none, andvisible: falseis common enough that spec:ActionSchema的visible/disabled统一为boolean | string(CEL) | {dialect,source}(#4075 step 3 前置,维护者 2026-08-06 已裁决) #5970 added it deliberately. Converging means either widening the target schema or telling authors to switch key;visible, and whatever the retirement process (ADR-0087) requires;Item 1 stands on its own even if item 2 lands as "keep both" — right now the guard protects one direction of a two-way street.