You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 569d4d2
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec)!: form layout accepts only vertical | horizontal — the inline and grid arms retired (#20221) (#20262)
Fixes#20221
Clause-②: no (narrowing)
Retires the `inline` and `grid` arms of form `layout` (ADR-0049
enforce-or-remove), per the triage direction on the card (comment
`5855767378`) under the maintainer's family criterion on #18900: the
capability `grid` names already exists under another key (`columns`),
and `inline` is not a record-form layout, so both are redundant
vocabulary and retire with no alias window.
## What changed
- **Both schemas narrowed to `'vertical' | 'horizontal'`.**
`ObjectFormPropsSchema.layout` (the `object-form` page component) and
`FormViewSchema.layout` (`view.form`, `view.formViews.*`, a form view
item's `config`). Each enum carries a per-value error map keyed on the
input (the `record:chatter` `position` precedent): `'grid'` and
`'inline'` are refused with `invalid_value` at `layout` and a
prescription (write `'vertical'` or omit `layout`; for multi-column set
`columns`), while a never-vocabulary value keeps zod's own enum refusal.
- **ADR-0087 D2 conversion `form-layout-inline-grid-to-vertical`**
(protocol 18, `retiredFromLoadPath: true`): rewrites both values to
`'vertical'` and leaves `columns` untouched, on `object-form` page
components (regions, slots, nesting), on every form payload a `views`
entry carries (`mapViewPayloads`, form kind only), and on the
assembled-manifest `viewItems` channel (record `config` or flattened
form overlay). Wired into the protocol-18 chain step; the step's
`rationale` is extended.
- **One D3 entry for the family**, `ui-form-layout-inline-grid-retired`
(ruling B on #17152: one semantic entry per retirement family even when
D2 is lossless). It carries the one judgement the chain cannot make:
whether a form that said `grid` without `columns` wanted more than one
column. `registry.ts` regenerated with `gen:migration-registry`.
- **Descriptions**: the `columns` describe on `object-form` no longer
says "grid layout"; both `layout` describes name `columns` as where
multi-column lives.
- **Regenerated, never hand-edited**:
`content/docs/references/{api/protocol,ui/component,ui/view}.mdx` and
`skills/objectstack-ui/references/react-blocks.md` (the `layout` row of
the `ObjectForm` block).
- **Hand-written**: `content/docs/protocol/objectui/layout-dsl.mdx` (one
sentence listed the four-arm enum), the `form.layout` liveness row in
`packages/spec/liveness/view.json` (still `live`; note re-anchored at
the pin, `verifiedAt` stamped), and the changeset.
- **Pins**:
`packages/spec/src/ui/form-layout-inline-grid-retired.test.ts` (29
tests).
## Mechanism hypotheses, measured
- **H1 — only these two sites declare the four-arm form enum:
CONFIRMED.** `git grep` over `packages/spec/src` at the merge base
`ab820016b3` for a `layout` enum with `'inline'` / `'grid'` arms:
exactly `ui/component.zod.ts` (`ObjectFormPropsSchema.layout`) and
`ui/view.zod.ts` (`FormViewSchema.layout`). The other `'grid'` hits are
the view-type vocabulary (`ListView.type`, `inlineEdit`, the object
designer), left untouched.
- **H1, the flattened overlay: CONFIRMED, it inherits the narrowing.**
`VIEW_METADATA_MEMBERS.formOverlay` is built from `FormViewSchema`
(`.extend` at the base; `.safeExtend` after #20186 landed and was merged
into this branch), so the flattened form overlay refuses both values
with the same prescription. Pinned through
`VIEW_METADATA_MEMBERS.formOverlay`; the union and its members were not
edited.
- **H2 — producers: zero, re-measured with lit controls.** Matcher:
`layout` followed by `grid` or `inline` as a value, quoted or bare,
across every tracked file (TS, JSON, YAML, Markdown), tests excluded. At
`ab820016b3`: 0 hits. Controls on the same tree: the same matcher with
`vertical|horizontal` hits 2 (conversion fixtures); `layout:` under
`examples/**` hits 53; `formViews` under `examples/**` hits 35; the one
authored `object-form` in `examples/` and `apps/` (the showcase create
wizard) authors no `layout`. At the merged head `2d0b7a6bcb` the only
subject hits are this PR's own prose and fixture.
- **Renderer read points, re-measured at the `.objectui-sha` pin
`f8a9d0fb0`** (recorded beside the prescriptions): the simple arm folds
both values to `vertical` (`ObjectForm.tsx:1406-1410`); the drawer/modal
hand-off (`:463`, `:499`), `DrawerForm.tsx:575` and `ModalForm.tsx:597`
pass only `vertical` / `horizontal`; `TabbedForm.tsx:556`,
`SplitForm.tsx:445`, `WizardForm.tsx:1075` hard-code `vertical`. The
pinned sibling imports neither enum for a value assignment, so the pin
build is unaffected.
## Verification (merged head `2d0b7a6bcb`)
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: **550 files passed, 16177 tests passed, 2 todo.**
- `pnpm --filter @objectstack/spec typecheck` (tsc, scripts-typecheck,
test-typecheck): exit 0; test layer "53 file(s) / 255 error(s) / 142
pinned signature(s) held".
- `pnpm --filter @objectstack/spec build` then `check:generated`: **"All
15 generated artifacts are up to date"**. `authorable-surface/`,
`api-surface/` and `json-schema.manifest/` are byte-unchanged, as
expected for an enum-value narrowing (the ratchets record key and export
existence, not value sets). `spec-changes.json` and the upgrade guide
are unchanged because the protocol-18 step is not yet cut.
- Gate families derived by `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `2d0b7a6bcb` (117 families,
the same set as before the merge): **115 run, all exit 0; 2 NOT
MEASURED; 0 unrun** (`--ran` reconciliation). Among them:
`check:adr-0087-registration` ("registered
form-layout-inline-grid-to-vertical, ui-form-layout-inline-grid-retired
(new here: both)"), `check:changeset-no-major`, `check:empty-changeset`,
`check:doc-authoring`, `check:liveness`, `check:objectui-pin-citations`,
`check:react-blocks`, `check:skill-examples`, `check:nul-bytes`,
`check:cross-package-test-inputs`.
- NOT MEASURED: `check:dual-build-cjs-loads` and
`check:type-check-debt`. Both refuse with their own prerequisite (exit
3): every workspace package's built output, which this worktree did not
build on the shared box. The diff adds no import, export or entry point,
and no ledgered package reads or writes the retired values (census
above). CI builds that closure.
- **Ablation** (one-time proof, taken at `41ceb7e5d6` before the merge,
which touched neither enum nor the pin file): both enums re-admitted
`'inline'` and `'grid'` through `scripts/ablation-replace.mjs` (anchor 1
to 0 on each file, blob `b6d256b45a8a` to `0787a83d3806` and
`33d6fd43be7c` to `8514b7afecb3`), then the pin file ran: **14 failed,
15 passed**. Red: all 10 refusal pins (2 values times 5 doors), the
two-surface vocabulary pin, and the 3 conversion pins that assert the
input is refused before the rewrite. Green: the 5
`vertical`/`horizontal` control pairs, the 5 never-vocabulary pins, the
load-path, non-carrier and registration pins. Restore proven: both blobs
equal HEAD and `git diff HEAD` is empty.
- `skills/**` readings:
`skills/objectstack-ui/references/react-blocks.md` 115 lines before and
after (one row regenerated in place); the published catalog's SKILL.md
files, 10 files, 4402 lines before and after.
## Acceptance notes
- **objectui follow-through (on the objectui pin bump, not filed
here):** the page-designer inspector's `object-form` layout select
(still offers Inline and Grid), the `object-form` registry `inputs` enum
(`plugin-form/src/index.tsx:238` at the pin), `ObjectFormSchema.layout`
and its docblock, the fold sites that become plain pass-through, the
`FormSchema` mirror's `'grid'` (its `WiderThanDeclared` row), and the
docs listing four arms. This is objectui#7759 group C.
- **Rewrite, not strip.** The card's own option R proposed stripping the
key; the triage direction and the dispatch rule a rewrite to
`'vertical'`. Both are behaviour-preserving (`'vertical'` is the
renderer default); the rewrite was followed.
- **Clause-② line.** Triage's note 4 wrote `yes`; the claim wrote `no`,
since the accept set only narrows. This PR and its changeset carry `no
(narrowing)`.
- **Governed path.** `skills/objectstack-ui/references/react-blocks.md`
is regenerated output under `skills/**` (Tier H), so this PR waits on a
maintainer approval. It is not split out: CI's react-blocks gate
re-derives it from the schema.
- **Hand-written docs page beyond the claim's file list:**
`content/docs/protocol/objectui/layout-dsl.mdx`, one sentence that
enumerated the four arms.
- **Observation, no carrier:** `horizontal` renders byte-identical to
`vertical` on the drawer, modal and tabbed presentations, and on the
simple form changes only the action row's justification (the card's "Not
in this card" measurement). Not in scope here.
## 维护者速读(草稿)
**改了什么**:表单的 `layout`(字段布局)只保留 `vertical`(纵向)和 `horizontal`(横向)两个取值,删掉
`inline` 和 `grid`。两处都改:页面上的 `object-form`
组件,以及视图里的表单定义。写了这两个值的元数据现在会在保存/校验时被明确拒绝,报错里直接告诉作者该怎么写:改成
`vertical`,想要多列就设 `columns`。已存的旧数据会被自动改写成 `vertical`(`columns`
原样保留),不会因此加载失败。
**为什么改**:这两个值从来没有生效过。前端所有表单形态都把它们当成 `vertical` 渲染,效果完全一样。也就是说,作者(尤其是
AI)写了一个看起来有意义、实际被丢掉的配置,还能通过校验。多列表单本来就由 `columns` 控制,而 `inline`
是工具栏/筛选行的形态,不是记录表单的布局。按您在 #18900 定下的判据,这属于重复词汇,直接退役,不留过渡期。
**风险与代价(含回滚)**:仓库内与示例应用里没有任何地方写过这两个值(已实测,并有对照),所以对现有应用没有影响。外部如有人写了,会在校验时看到明确的修改提示;已存数据由转换层自动改写,渲染结果与之前完全相同。代价是前端(objectui)的设计器下拉框和组件注册里还列着这两个选项,要在下次同步
objectui 版本时一并删掉,本 PR 已在 Acceptance notes 里写明。回滚就是 revert 本
PR,没有数据迁移需要撤销。
**席位意见**:
**你要做的**:批准此 PR。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|**object**|`string`| optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. |
|**layout**|`Enum<'vertical' \| 'horizontal'>`| optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`|
1720
1720
|**columns**|`integer`| optional | Number of columns for the form body |
1721
1721
|**title**|`string`| optional | Form title |
1722
1722
|**description**|`string`| optional | Form description |
@@ -1801,7 +1801,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
|**layout**|`Enum<'vertical' \| 'horizontal'>`| optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`|
1805
1805
|**columns**|`integer`| optional | Number of columns for the form body |
1806
1806
|**title**|`string`| optional | Form title |
1807
1807
|**description**|`string`| optional | Form description |
|**columns**|`number`| optional |Field columns in grid layout |
365
+
|**layout**|`Enum<'vertical' \| 'horizontal'>`| optional | Field layout — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`|
366
+
|**columns**|`number`| optional |Number of field columns (multi-column forms), honoured under either `layout`|
367
367
|**fields**|`any[]`| optional | Limit/order the fields shown |
368
368
|**customFields**|`any`| optional | Custom field definitions merged into the generated set |
|**layout**|`Enum<'vertical' \| 'horizontal'>`| optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`|
405
405
|**columns**|`integer`| optional | Number of columns for the form body |
406
406
|**title**|`string`| optional | Form title |
407
407
|**description**|`string`| optional | Form description |
@@ -1778,9 +1778,9 @@ Tab configuration for multi-tab view interface
|**object**|`string`| optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. |
|**layout**|`Enum<'vertical' \| 'horizontal'>`| optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`|
1854
1854
|**columns**|`integer`| optional | Number of columns for the form body |
1855
1855
|**title**|`string`| optional | Form title |
1856
1856
|**description**|`string`| optional | Form description |
@@ -1935,7 +1935,7 @@ Tab configuration for multi-tab view interface
|**layout**|`Enum<'vertical' \| 'horizontal'>`| optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`|
1939
1939
|**columns**|`integer`| optional | Number of columns for the form body |
1940
1940
|**title**|`string`| optional | Form title |
1941
1941
|**description**|`string`| optional | Form description |
@@ -2187,7 +2187,7 @@ This schema accepts one of the following structures:
|**layout**|`Enum<'vertical' \| 'horizontal'>`| optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`|
2214
2214
|**columns**|`integer`| optional | Number of columns for the form body |
2215
2215
|**title**|`string`| optional | Form title |
2216
2216
|**description**|`string`| optional | Form description |
@@ -2370,7 +2370,7 @@ This schema accepts one of the following structures:
|**layout**|`Enum<'vertical' \| 'horizontal'>`| optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`|
2400
2400
|**columns**|`integer`| optional | Number of columns for the form body |
2401
2401
|**title**|`string`| optional | Form title |
2402
2402
|**description**|`string`| optional | Form description |
"note": "objectui @ f8a9d0fb0: plugin-form ObjectForm.tsx:1408 (simple arm) and :463/:499 (drawer/modal hand-off), DrawerForm.tsx:575, ModalForm.tsx:597 — each reads vertical/horizontal only. The enum was narrowed to those two arms 2026-09-27 (#20221, ADR-0049 enforce-or-remove): every arm folded 'inline' and 'grid' to vertical, so both were a declared value the renderer threw away; multi-column is the sibling `columns` key. The key itself stays live."
0 commit comments