From 32ce7645227c065d7fcb319a2558d440a6de3f18 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 14:37:45 +0000 Subject: [PATCH 1/2] docs(spec): describe the form-section collapse pair on both keys `FormSectionSchema.collapsible` and `.collapsed` were two published, authorable booleans with no `.describe()` at all, sitting between neighbours that have one, so the generated reference page printed two empty Description cells and the dependency between them reached nobody. Both now carry contract text, plus one TSDoc block that states the pair once. Every sentence is measured against the built package: parse does NOT normalize the pair in either direction (`{ collapsed: true }` parses to `{ collapsible: false, collapsed: true }`, and `safeParseAsync` agrees), so the implication `collapsed` -> `collapsible` is a renderer rule read from the declaration and has to be stated on the declaration or it reaches no author. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- packages/spec/src/ui/view.zod.ts | 52 ++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index ce2ae04cde..2ecea023f2 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -3312,8 +3312,56 @@ export const FormSectionSchema = lazySchema(() => strictObject({ name: z.string().optional().describe('Stable section identifier for i18n lookup (snake_case)'), label: I18nLabelSchema.optional(), description: z.string().optional().describe('Optional description rendered under the section header.'), - collapsible: z.boolean().default(false), - collapsed: z.boolean().default(false), + /** + * ## The collapse pair, declared once for both members + * + * Two independent booleans, both `.default(false)`, and the dependency + * between them is a RENDERER rule rather than anything parse does — so it + * has to be stated on the declaration or it reaches nobody. `collapsed` + * IMPLIES `collapsible`: a section that starts closed always carries the + * disclosure control that reopens it, `collapsed: true` needs no + * `collapsible` beside it, and it outranks an explicit `collapsible: false` + * (maintainer ruling 2026-09-18, letter A). Letter B — refusing the + * combination at the declaration — and letter C — a dev-only warning — were + * both REFUSED, so ⛔ neither this schema nor a lint rule may grow one: + * `collapsed: true` alone is a CORRECT spelling of "collapsed by default", + * which is why the ruling made the renderer widen instead. + * + * ⚠️ The pair is NOT normalized at parse, in either direction — measured + * against the built package: `{ collapsed: true }` parses to + * `{ collapsible: false, collapsed: true }`, verbatim, and + * `safeParseAsync` agrees. So a consumer reading the parsed `collapsible` + * is reading what the author typed, NOT whether a control renders; it + * applies the implication itself, from the declaration and never from live + * collapse state (deriving it from the latter deletes the control the + * moment the reader opens the section). That is the opposite of the + * `fieldGroups` pair on `ObjectSchema`, which a parse-time mapping folds + * onto the `collapse` enum — ⛔ do not carry a reading across. + * + * Only `true` is refused on a wizard step and beside `group`; `false` is + * accepted in both, because it declares exactly what those surfaces already + * deliver (see `trueOnlyDerivedKeys` below and the FormViewSchema wizard + * refinement). + */ + collapsible: z.boolean().default(false).describe( + 'Whether the section renders a disclosure control, so a reader can close it and open it again. ' + + 'Default `false`: a section declaring neither collapse key is always open and shows no control. ' + + '⚠️ `collapsed: true` IMPLIES this key — an explicit `collapsible: false` beside it does NOT take ' + + 'the control away (ruled 2026-09-18). The renderer resolves that from the DECLARATION; parse never ' + + 'rewrites the pair, so a parsed section still reports the `false` that was authored. Only `true` is ' + + 'refused on a wizard step and beside `group`; `false` is accepted in both, because it declares ' + + 'exactly what those surfaces already deliver.', + ), + collapsed: z.boolean().default(false).describe( + 'Whether the section starts closed. Default `false`. ⚠️ `collapsed: true` IMPLIES `collapsible` and is ' + + 'sufficient ON ITS OWN — a section that starts closed always carries the disclosure control that ' + + 'reopens it, and it outranks an explicit `collapsible: false` (ruled 2026-09-18; refusing the ' + + 'combination at the declaration, and warning on it, were both rejected — nobody can depend on a ' + + 'section that cannot be opened). The implication is a renderer rule, never a parse-time rewrite: ' + + '`{ collapsed: true }` still parses to `collapsible: false, collapsed: true`, so the parsed ' + + '`collapsible` must never be read as "a control renders". Only `true` is refused on a wizard step (steps do not ' + + 'collapse) and beside `group`, whose field group declares the pair.', + ), /** * Conditional-visibility predicate (CEL) — the whole section is shown only * when TRUE (ADR-0089, canonical `*When` name). Same per-layer binding root as From 8ea5a49fe145f095658eefa3478a9afa5a26abdf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 14:44:21 +0000 Subject: [PATCH 2/2] chore(spec): regenerate the form-section reference rows, add the changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gen:schema && gen:docs` after the describe edit. Exactly six rows move in `content/docs/references/ui/view.mdx` — the collapse pair in the three places that page projects `FormSectionSchema` — and nothing else under `content/docs/references/**` differs from `origin/main`. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../19311-form-section-collapse-describes.md | 22 +++++++++++++++++++ content/docs/references/ui/view.mdx | 12 +++++----- 2 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 .changeset/19311-form-section-collapse-describes.md diff --git a/.changeset/19311-form-section-collapse-describes.md b/.changeset/19311-form-section-collapse-describes.md new file mode 100644 index 0000000000..eec9c7ea1f --- /dev/null +++ b/.changeset/19311-form-section-collapse-describes.md @@ -0,0 +1,22 @@ +--- +"@objectstack/spec": patch +--- + +`FormSection.collapsible` / `FormSection.collapsed` — both keys now carry a `.describe()`, so the published reference page no longer prints two empty Description cells for two authorable booleans (#19311). + +They were the only keys in `FormSectionSchema` with no contract text at all, sitting between neighbours that have it, and the dependency between them was published nowhere. **Both** are described rather than only `collapsed`: the sibling silence is what made the gap ambiguous in the first place, and describing one of a pair recreates it one key over. + +Measured against the built package, `FormSectionSchema.safeParse` on one section: + +| authored on the section | `collapsible` after parse | `collapsed` after parse | +| :--- | :--- | :--- | +| neither | `false` | `false` | +| `collapsible: true` | `true` | `false` | +| `collapsed: true` | `false` | `true` | +| `collapsed: true` + `collapsible: false` | `false` | `true` | +| both `true` | `true` | `true` | +| both `false` | `false` | `false` | + +- **Parse does NOT normalize the pair, in either direction.** `{ collapsed: true }` parses to `{ collapsible: false, collapsed: true }` verbatim, and `safeParseAsync` agrees. So the implication `collapsed` ⇒ `collapsible` — ruled 2026-09-18, letter A — is a **renderer** rule applied from the declaration, and the describes say exactly that rather than implying a fold the schema does not perform. A consumer reading the parsed `collapsible` is reading what the author typed, never whether a disclosure control renders. +- **This is the opposite of the `ObjectFieldGroup` pair**, where a parse-time mapping really does fold the old booleans onto the ADR-0085 `collapse` enum. The two surfaces share key names and share nothing else; the describes say so. +- **Text only.** No key is added, removed or re-typed, no accept set moves and no refinement changes: `check:authorable-surface` and `check:api-surface` are both green with no delta, and the wizard-step and `group` co-declaration refusals parse identically before and after (only `true` is refused in either place; `false` is accepted in both). diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index ea800bd3ae..b48d558ff2 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -331,8 +331,8 @@ View filter rule | **name** | `string` | optional | Stable section identifier for i18n lookup (snake_case) | | **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **description** | `string` | optional | Optional description rendered under the section header. | -| **collapsible** | `boolean` | optional (default: `false`) | | -| **collapsed** | `boolean` | optional (default: `false`) | | +| **collapsible** | `boolean` | optional (default: `false`) | Whether the section renders a disclosure control, so a reader can close it and open it again. Default `false`: a section declaring neither collapse key is always open and shows no control. ⚠️ `collapsed: true` IMPLIES this key — an explicit `collapsible: false` beside it does NOT take the control away (ruled 2026-09-18). The renderer resolves that from the DECLARATION; parse never rewrites the pair, so a parsed section still reports the `false` that was authored. Only `true` is refused on a wizard step and beside `group`; `false` is accepted in both, because it declares exactly what those surfaces already deliver. | +| **collapsed** | `boolean` | optional (default: `false`) | Whether the section starts closed. Default `false`. ⚠️ `collapsed: true` IMPLIES `collapsible` and is sufficient ON ITS OWN — a section that starts closed always carries the disclosure control that reopens it, and it outranks an explicit `collapsible: false` (ruled 2026-09-18; refusing the combination at the declaration, and warning on it, were both rejected — nobody can depend on a section that cannot be opened). The implication is a renderer rule, never a parse-time rewrite: `{ collapsed: true }` still parses to `collapsible: false, collapsed: true`, so the parsed `collapsible` must never be read as "a control renders". Only `true` is refused on a wizard step (steps do not collapse) and beside `group`, whose field group declares the pair. | | **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — section shown only when TRUE. Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in metadata forms. `current_user` (and the ADR-0068 aliases `user` / `ctx.user` / `os.user`) resolves here too — CLIENT-SIDE only: nothing server-side evaluates a form-view section `visibleWhen`, so a role test here hides the controls and protects no data (declare permission-set field-level security for that), and on the public `/f/:slug` route no host publishes a scope, so the root is unbound and the predicate faults open. No `features.*` on ANY form-view predicate — refused at parse (ruled 2026-08-27): unbound on the standalone form routes, where the predicate would fault open. | | **visibleOn** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Hides the whole section when false. Normalized to `visibleWhen` at parse. | | **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional (default: `1`) | | @@ -463,8 +463,8 @@ Form-view select option — the object-field option shape minus the per-option ` | **name** | `string` | optional | Stable section identifier for i18n lookup (snake_case) | | **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **description** | `string` | optional | Optional description rendered under the section header. | -| **collapsible** | `boolean` | optional (default: `false`) | | -| **collapsed** | `boolean` | optional (default: `false`) | | +| **collapsible** | `boolean` | optional (default: `false`) | Whether the section renders a disclosure control, so a reader can close it and open it again. Default `false`: a section declaring neither collapse key is always open and shows no control. ⚠️ `collapsed: true` IMPLIES this key — an explicit `collapsible: false` beside it does NOT take the control away (ruled 2026-09-18). The renderer resolves that from the DECLARATION; parse never rewrites the pair, so a parsed section still reports the `false` that was authored. Only `true` is refused on a wizard step and beside `group`; `false` is accepted in both, because it declares exactly what those surfaces already deliver. | +| **collapsed** | `boolean` | optional (default: `false`) | Whether the section starts closed. Default `false`. ⚠️ `collapsed: true` IMPLIES `collapsible` and is sufficient ON ITS OWN — a section that starts closed always carries the disclosure control that reopens it, and it outranks an explicit `collapsible: false` (ruled 2026-09-18; refusing the combination at the declaration, and warning on it, were both rejected — nobody can depend on a section that cannot be opened). The implication is a renderer rule, never a parse-time rewrite: `{ collapsed: true }` still parses to `collapsible: false, collapsed: true`, so the parsed `collapsible` must never be read as "a control renders". Only `true` is refused on a wizard step (steps do not collapse) and beside `group`, whose field group declares the pair. | | **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — section shown only when TRUE. Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in metadata forms. `current_user` (and the ADR-0068 aliases `user` / `ctx.user` / `os.user`) resolves here too — CLIENT-SIDE only: nothing server-side evaluates a form-view section `visibleWhen`, so a role test here hides the controls and protects no data (declare permission-set field-level security for that), and on the public `/f/:slug` route no host publishes a scope, so the root is unbound and the predicate faults open. No `features.*` on ANY form-view predicate — refused at parse (ruled 2026-08-27): unbound on the standalone form routes, where the predicate would fault open. | | **visibleOn** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Hides the whole section when false. Normalized to `visibleWhen` at parse. | | **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional (default: `1`) | | @@ -479,8 +479,8 @@ Form-view select option — the object-field option shape minus the per-option ` | **name** | `string` | optional | Stable section identifier for i18n lookup (snake_case) | | **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **description** | `string` | optional | Optional description rendered under the section header. | -| **collapsible** | `boolean` | optional (default: `false`) | | -| **collapsed** | `boolean` | optional (default: `false`) | | +| **collapsible** | `boolean` | optional (default: `false`) | Whether the section renders a disclosure control, so a reader can close it and open it again. Default `false`: a section declaring neither collapse key is always open and shows no control. ⚠️ `collapsed: true` IMPLIES this key — an explicit `collapsible: false` beside it does NOT take the control away (ruled 2026-09-18). The renderer resolves that from the DECLARATION; parse never rewrites the pair, so a parsed section still reports the `false` that was authored. Only `true` is refused on a wizard step and beside `group`; `false` is accepted in both, because it declares exactly what those surfaces already deliver. | +| **collapsed** | `boolean` | optional (default: `false`) | Whether the section starts closed. Default `false`. ⚠️ `collapsed: true` IMPLIES `collapsible` and is sufficient ON ITS OWN — a section that starts closed always carries the disclosure control that reopens it, and it outranks an explicit `collapsible: false` (ruled 2026-09-18; refusing the combination at the declaration, and warning on it, were both rejected — nobody can depend on a section that cannot be opened). The implication is a renderer rule, never a parse-time rewrite: `{ collapsed: true }` still parses to `collapsible: false, collapsed: true`, so the parsed `collapsible` must never be read as "a control renders". Only `true` is refused on a wizard step (steps do not collapse) and beside `group`, whose field group declares the pair. | | **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — section shown only when TRUE. Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in metadata forms. `current_user` (and the ADR-0068 aliases `user` / `ctx.user` / `os.user`) resolves here too — CLIENT-SIDE only: nothing server-side evaluates a form-view section `visibleWhen`, so a role test here hides the controls and protects no data (declare permission-set field-level security for that), and on the public `/f/:slug` route no host publishes a scope, so the root is unbound and the predicate faults open. No `features.*` on ANY form-view predicate — refused at parse (ruled 2026-08-27): unbound on the standalone form routes, where the predicate would fault open. | | **visibleOn** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Hides the whole section when false. Normalized to `visibleWhen` at parse. | | **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional (default: `1`) | |