Skip to content

Commit dd0f681

Browse files
os-zhuangclaude
andauthored
fix(spec): alias visible/showWhen onto visibleWhen on page:tabs items and screen fields (#8415)
#7832 curated the visible/showWhen action-side spellings onto visibleWhen across six shapes and pinned the inventory in visible-when-alias-guidance.test.ts. Two more visibleWhen shapes were never in that inventory: page:tabs items (ui/component.zod.ts) and the automation screen node's ScreenFieldConfigSchema (builtin-node-config.zod.ts). Both are hand-rolled strictObject calls with exactly one landing key and no boolean sibling, so both get the simple alias-row treatment: visible -> visibleWhen, showWhen -> visibleWhen. Acceptance is unchanged -- an alias row is a message channel, not a parse-time rename. The page:tabs item docblock also flags visibility/visibleOn (the ADR-0089 view/page spellings) as deliberately not accepted here; that is an acceptance statement an alias row does not disturb, so both also get a pointer at visibleWhen while staying rejected. Adds a fifth section to visible-when-alias-guidance.test.ts pinning both shapes' rename messages, canonical-still-parses, aliased-spellings-still- rejected, and the visibility/visibleOn decision, per Fixes #8382. Fixes #8382 Claude-Session: https://claude.ai/code/session_016YBUGvukaeVu9DjKdsHJa9 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2befe60 commit dd0f681

4 files changed

Lines changed: 179 additions & 2 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): alias guidance for `visible` / `showWhen` on the `page:tabs` item and screen-field `visibleWhen` shapes (#8382)
6+
7+
#7832 curated the `visible` / `showWhen` action-side spellings onto `visibleWhen`
8+
across six shapes, pinning the inventory in `visible-when-alias-guidance.test.ts`.
9+
Two more `visibleWhen` shapes in `packages/spec` were never in that inventory:
10+
`page:tabs` items (`ui/component.zod.ts`) and the automation `screen` node's
11+
`ScreenFieldConfigSchema` (`automation/builtin-node-config.zod.ts`). On both, an
12+
author who wrote `visible` or `showWhen` got a rejection naming the surface but
13+
never the key to write instead.
14+
15+
**Nothing changes about what parses.** Every key named here was rejected before
16+
and is rejected after; only the message differs — an alias row is a message
17+
channel, not a parse-time rename.
18+
19+
What each surface says now:
20+
21+
- **`page:tabs` items** rename `visible` and `showWhen` onto `visibleWhen`, the
22+
simple case (one landing key, no boolean sibling). The item's docblock also
23+
states that the deprecated ADR-0089 `visibility` / `visibleOn` spellings are
24+
not accepted here (unlike the view/page shapes that fold them in via
25+
`normalizeVisibleWhen`) — that statement is about **acceptance**, which an
26+
alias row does not disturb, so both now get the same pointer at `visibleWhen`
27+
while staying rejected exactly as before.
28+
- **`ScreenFieldConfigSchema`** (the `screen` automation node's per-field
29+
config) renames `visible` and `showWhen` onto `visibleWhen` the same way. The
30+
shape's pre-existing `visibleIf` prescription (an exact `guidance` entry) is
31+
unaffected — an exact entry wins over the alias table, so that bespoke prose
32+
keeps firing for the four-edit-away typo it was written for.
33+
34+
Both shapes are hand-rolled `strictObject` calls with their own options and
35+
neither spreads `VISIBILITY_STRICT_OPTIONS`, so no shared guidance set answers
36+
these keys ahead of the new alias rows — verified against
37+
`alias-integrity.test.ts`, which fails on a row a guidance set would shadow.

‎packages/spec/src/automation/builtin-node-config.zod.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,19 @@ export const ScreenFieldConfigSchema = lazySchema(() => strictObject({
379379
+ 'typo the whole undeclared-key ladder descends from — three diagnostic passes for a field that silently '
380380
+ 'never hid.',
381381
},
382+
aliases: {
383+
/**
384+
* Action-side spellings (#8382) — the same `visible` / `showWhen` gap
385+
* #7832 closed on six other `visibleWhen` shapes. One landing key here,
386+
* no boolean sibling, so this is the simple rename case per this
387+
* package's alias/guidance rule (`visible-when-alias-guidance.test.ts`
388+
* header). `visibleIf` stays on `guidance` above (an exact match wins
389+
* over `aliases` and keeps its bespoke prose); these two are plain
390+
* renames onto the same target.
391+
*/
392+
visible: 'visibleWhen',
393+
showWhen: 'visibleWhen',
394+
},
382395
}, {
383396
/** Field name — an item with an empty name is dropped. */
384397
name: z.string().describe('Field name (the flow variable the value binds to)'),

‎packages/spec/src/shared/visible-when-alias-guidance.test.ts‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import { RowCrudActionOverrideSchema } from '../data/object.zod';
4444
import { FieldSchema, SelectOptionSchema } from '../data/field.zod';
4545
import { FormFieldSchema, FormSectionSchema } from '../ui/view.zod';
4646
import { PageComponentSchema } from '../ui/page.zod';
47+
import { PageTabsProps } from '../ui/component.zod';
48+
import { ScreenFieldConfigSchema } from '../automation/builtin-node-config.zod';
4749

4850
/**
4951
* The `unrecognized_keys` message for `value`, or a loud failure.
@@ -239,3 +241,104 @@ describe('#7832 — no acceptance change', () => {
239241
expect(FormFieldSchema.safeParse({ ...FORM_FIELD, disabled: true }).success).toBe(false);
240242
});
241243
});
244+
245+
// ===========================================================================
246+
// 5. #8382 — the two `visibleWhen` shapes #7832's inventory never enumerated
247+
// ===========================================================================
248+
//
249+
// `page:tabs` items (`component.zod.ts`) and `ScreenFieldConfigSchema`
250+
// (`builtin-node-config.zod.ts`) both declare `visibleWhen` and were outside
251+
// the six shapes #7832 curated: `visible` / `showWhen` were rejected without
252+
// naming the key to write instead. Both have exactly ONE landing key for the
253+
// visibility intent and no boolean sibling, so per the header's rule this is
254+
// the simple alias case on both: `visible → visibleWhen`, `showWhen →
255+
// visibleWhen`. Neither shape spreads `VISIBILITY_STRICT_OPTIONS` — both are
256+
// hand-rolled `strictObject` calls with their own options (the tab item
257+
// already carried one alias row, `key → value`; the screen field carried
258+
// `guidance` only, keyed to the `visibleIf` typo) — so no guidance set
259+
// consumes these keys before the alias channel does; the assertions below
260+
// confirm the rename fires (a shadowed row would emit the guidance-set
261+
// prescription instead, per section 2's `.not.toContain('Did you mean')`
262+
// pattern), which is exactly what keeps `alias-integrity.test.ts` green.
263+
264+
/** Minimal bodies that reach each new surface's unknown-key path. */
265+
const TAB_ITEM = { label: 'Tab', children: [] } as const;
266+
const SCREEN_FIELD = { name: 'f' } as const;
267+
268+
describe('#8382 — the two shapes #7832 never enumerated', () => {
269+
describe('`page:tabs` item (`PageTabsProps.items`) — `visibleWhen` declared, no alias for the action-side spellings', () => {
270+
it('`visible` renames onto `visibleWhen`', () => {
271+
const m = unknownKeyMessage(PageTabsProps, { items: [{ ...TAB_ITEM, visible: true }] });
272+
expect(m).toContain('Did you mean `visible` → `visibleWhen`?');
273+
});
274+
275+
it('`showWhen` renames onto `visibleWhen`', () => {
276+
const m = unknownKeyMessage(PageTabsProps, { items: [{ ...TAB_ITEM, showWhen: 'record.x' }] });
277+
expect(m).toContain('Did you mean `showWhen` → `visibleWhen`?');
278+
});
279+
280+
it('the canonical `visibleWhen` still parses, unchanged', () => {
281+
// `ExpressionInputSchema` normalizes a bare string into `{ dialect: 'cel',
282+
// source }` — that normalization is pre-existing and untouched by this
283+
// card; what this pins is that the alias rows did not disturb it.
284+
const r = PageTabsProps.safeParse({ items: [{ ...TAB_ITEM, visibleWhen: 'record.x' }] });
285+
expect(r.success).toBe(true);
286+
if (r.success) expect(r.data.items[0]?.visibleWhen).toEqual({ dialect: 'cel', source: 'record.x' });
287+
});
288+
289+
it('`visible` and `showWhen` stay REJECTED — a pointer is not an acceptance', () => {
290+
expect(PageTabsProps.safeParse({ items: [{ ...TAB_ITEM, visible: true }] }).success).toBe(false);
291+
expect(PageTabsProps.safeParse({ items: [{ ...TAB_ITEM, showWhen: 'record.x' }] }).success).toBe(false);
292+
});
293+
294+
// The judgment call: `visibility` / `visibleOn` are the ADR-0089 spellings
295+
// this surface's own docblock says are deliberately NOT folded in (the
296+
// key is new; there is no legacy convention to carry forward here, unlike
297+
// the view/page shapes that fold them via `normalizeVisibleWhen`). That
298+
// sentence is about ACCEPTANCE and an alias row does not disturb it — both
299+
// stay rejected below. But an author who used the ADR-0089 spelling
300+
// correctly on a page component or view form and reaches for the same
301+
// word on a tab item is signalling the identical intent, so #8382 points
302+
// the rejection at `visibleWhen` for these two as well, on the same
303+
// one-landing-key rule as `visible` / `showWhen`. Pinned here so a future
304+
// edit cannot silently drop the pointer OR silently start accepting them.
305+
it('`visibility` / `visibleOn` are POINTED at `visibleWhen` but stay rejected (the #8382 judgment call)', () => {
306+
const mVisibility = unknownKeyMessage(PageTabsProps, { items: [{ ...TAB_ITEM, visibility: true }] });
307+
expect(mVisibility).toContain('Did you mean `visibility` → `visibleWhen`?');
308+
expect(PageTabsProps.safeParse({ items: [{ ...TAB_ITEM, visibility: true }] }).success).toBe(false);
309+
310+
const mVisibleOn = unknownKeyMessage(PageTabsProps, { items: [{ ...TAB_ITEM, visibleOn: 'record.x' }] });
311+
expect(mVisibleOn).toContain('Did you mean `visibleOn` → `visibleWhen`?');
312+
expect(PageTabsProps.safeParse({ items: [{ ...TAB_ITEM, visibleOn: 'record.x' }] }).success).toBe(false);
313+
});
314+
});
315+
316+
describe('`ScreenFieldConfigSchema` — `visibleWhen` declared, no alias for the action-side spellings', () => {
317+
it('`visible` renames onto `visibleWhen`', () => {
318+
const m = unknownKeyMessage(ScreenFieldConfigSchema, { ...SCREEN_FIELD, visible: true });
319+
expect(m).toContain('Did you mean `visible` → `visibleWhen`?');
320+
});
321+
322+
it('`showWhen` renames onto `visibleWhen`', () => {
323+
const m = unknownKeyMessage(ScreenFieldConfigSchema, { ...SCREEN_FIELD, showWhen: 'record.x' });
324+
expect(m).toContain('Did you mean `showWhen` → `visibleWhen`?');
325+
});
326+
327+
it('the canonical `visibleWhen` still parses, unchanged', () => {
328+
const r = ScreenFieldConfigSchema.safeParse({ ...SCREEN_FIELD, visibleWhen: 'record.x' });
329+
expect(r.success).toBe(true);
330+
if (r.success) expect(r.data.visibleWhen).toBe('record.x');
331+
});
332+
333+
it('`visible` and `showWhen` stay REJECTED — a pointer is not an acceptance', () => {
334+
expect(ScreenFieldConfigSchema.safeParse({ ...SCREEN_FIELD, visible: true }).success).toBe(false);
335+
expect(ScreenFieldConfigSchema.safeParse({ ...SCREEN_FIELD, showWhen: 'record.x' }).success).toBe(false);
336+
});
337+
338+
it('the pre-existing `visibleIf` guidance is untouched by the new aliases (`guidance` wins over `aliases`)', () => {
339+
const m = unknownKeyMessage(ScreenFieldConfigSchema, { ...SCREEN_FIELD, visibleIf: 'record.x' });
340+
expect(m).toContain('The visibility predicate is `visibleWhen`');
341+
expect(m).not.toContain('Did you mean');
342+
});
343+
});
344+
});

‎packages/spec/src/ui/component.zod.ts‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,25 @@ export const PageTabsProps = strictObject({
584584
* tokens that move the moment the item list changes.
585585
*/
586586
key: 'value',
587+
/**
588+
* Action-side spellings (#8382) — an author who learned `visible` /
589+
* `showWhen` from `ui/action.zod.ts` and reaches for the same words
590+
* here. One landing key, no boolean sibling, so per this package's
591+
* alias/guidance rule (`visible-when-alias-guidance.test.ts` header)
592+
* this is the simple rename case, not guidance prose.
593+
*/
594+
visible: 'visibleWhen',
595+
showWhen: 'visibleWhen',
596+
/**
597+
* `visibility` / `visibleOn` (#8382) — the ADR-0089 spellings this
598+
* surface deliberately does NOT fold in (see the docblock below): they
599+
* stay rejected, but an author who used them correctly on a page
600+
* component or view form is reaching for the identical intent here, so
601+
* the rejection still points at the one key that lands it. A pointer is
602+
* a message, not acceptance — nothing below changes what parses.
603+
*/
604+
visibility: 'visibleWhen',
605+
visibleOn: 'visibleWhen',
587606
},
588607
}, {
589608
label: I18nLabelSchema,
@@ -596,10 +615,15 @@ export const PageTabsProps = strictObject({
596615
* Binds the same environment as page-component `visibleWhen`: `record` +
597616
* `current_user`, plus page state as `page.<var>` (re-evaluated live).
598617
* Canonical `*When` name per ADR-0089 — this key is new, so the deprecated
599-
* `visibility` / `visibleOn` aliases are NOT accepted on tab items.
618+
* `visibility` / `visibleOn` aliases are NOT ACCEPTED on tab items: unlike
619+
* the view/page surfaces that fold them into `visibleWhen` via
620+
* `normalizeVisibleWhen`, none of `visible` / `showWhen` / `visibility` /
621+
* `visibleOn` parses here — all four are rejected. #8382 gave the
622+
* rejection a pointer at this key for all four spellings (message only:
623+
* being pointed AT `visibleWhen` is not the same as being accepted).
600624
*/
601625
visibleWhen: ExpressionInputSchema.optional().describe(
602-
'Visibility predicate (CEL) — the whole tab (header + panel) is omitted when FALSE; the renderer falls back to the first visible tab when the active one is hidden. Binds `record`, `current_user`, `page.<var>`. ADR-0089 canonical name (`visibility`/`visibleOn` aliases are not accepted here).',
626+
'Visibility predicate (CEL) — the whole tab (header + panel) is omitted when FALSE; the renderer falls back to the first visible tab when the active one is hidden. Binds `record`, `current_user`, `page.<var>`. ADR-0089 canonical name — `visible`/`showWhen`/`visibility`/`visibleOn` are all rejected here (not folded in), each with a pointer at this key.',
603627
),
604628
/**
605629
* Stable URL token for this tab — the value `?tab=` carries and the

0 commit comments

Comments
 (0)