状态:✅ 已实现并浏览器端到端验证(2026-07-04)。 分支 feat/adaptive-layout-2578(framework + objectui),单测 / 类型全绿,待提 PR。下方「开发方案」为设计记录,保留。
✅ 开发效果(Delivered) 一句话 字段多 的对象,新增 / 编辑 / 查看详情自动切换到整页窗口 ;字段少 的对象保持抽屉 / 弹窗。全部由平台按字段数自动推断 ,AI 写元数据时零额外配置 ——不暴露 recordSurface 这类可写 key,所以 AI 无从写错。
Before → After 维度 开发前 开发后 字段多对象的记录详情 一律抽屉,几十个字段挤在侧栏 可授权字段数 ≥ 12 → 整页 (有 URL、可后退);< 12 → 抽屉。移动端一律整页录入表单多列(section.columns) type:'simple'忽略 per-section 列数,分组内字段始终单列(#2515 )按声明渲染多列 ,每个分组按自己的列密度在共享网格内排布字段级宽度 只有绝对 colSpan (1–4),换表面(手机 / 弹窗 / 整页)就错位 语义 span(auto / full),与派生列数解耦 ,任何列数都不溢出;遗留 colSpan 渲染期 clamp,永不越界 AI 写错字段引用 / 布局 Zod 合法但静默失效 ,作者发现不了 编写期 warning + 修复提示 (form-field-unknown / absolute-colspan-discouraged),AI 与人手写同一把尺(ADR-0019 / 0078) 协议演进 倾向新增 recordSurface 开关 + 新 ADR 不加 object key、不开新 ADR :字段数机器能推 → 派生;显式覆盖走 assigned page(ADR-0085 §2 准入测试)
浏览器验证(真 console + app-showcase 后端,端到端跑通) 对象 可授权字段 行点击 → 结果 观测到的 URL / 形态 字段动物园 field_zoo 57 整页 /…/showcase_field_zoo/record/<id>,面包屑 + 两列详情,无遮罩产品 product 6 抽屉 停在列表 ?recordId=<id>,右侧抽屉浮层覆盖虚化的列表
录入表单同时验证多列 :Email │ URL 两列、Phone │ Password 两列、Textarea 独占整行(宽控件 span:auto 自动整行)。
用户 / AI 视角 AI 建一个几十字段的对象、不写任何表面或列配置 → 记录自动整页 + 表单自动多列 + 长文本自动整行。 想让某个小 对象也强制整页 → 指派一个 record page(ADR-0085 认定的 per-page 正道),而不是加新 key。 交付与验证 framework f79263a19:deriveRecordSurface(spec/src/data/record-surface.ts)+ FormFieldSchema.span + packages/lint/src/validate-form-layout.ts + ADR-0085 §2 澄清 + changeset。spec 6677 测试、api-surface(regen)、lint 121 全绿 。objectui 97726afa4 + a56cb420f:autoLayout.resolveColSpan(span 感知 / clamp / per-section 密度)+ ObjectForm grouped 路径修 console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515 + FormField.span 透传 + app-shell 记录表面接入 deriveRecordSurface。autoLayout 43 + plugin-form 173 测试、turbo type-check 29/29 全绿 。关键实现点 :console 的记录表面在 packages/app-shell/src/views/ObjectView.tsx 决定(原默认 drawer),已在其 detailNavigation 默认接入 deriveRecordSurface——底层 plugin-view 改动不够,必须在此层接入才在 console 生效。已知边界 / 后续 背景 字段特别多的对象(50+ 字段),来自 #2574 的两个诉求:
多列编辑/显示 ;部分对象 在新增/编辑/查看详情时自动切到全屏页面 ,而不是弹窗。北极星约束:所有元数据都是 AI 写的 ,所以设计的核心目标不是"能配",而是让 AI 无从写错 。下面每个决策都围绕这一点。
关键决策(为什么不 走最直觉的做法) 1. 不新增 recordSurface 之类的对象级 key 按 ADR-0085 §2 的"未来 key 准入测试",呈现表面(modal/page)两条都不过 :
判据是字段数 ,恰恰机器能推 → 机器能推的不该做成 AI 要填的 key; modal-vs-page 是纯重排呈现 ,不是数据的业务事实。ADR-0085 明确 reject 掉这类 per-surface 开关,并规定 per-page 控制走 assigned page 。 → 呈现表面 = 渲染器启发式(auto) + assigned page 覆盖 ,不是 authored key。
2. 不新写 ADR / 设计文档 方案完全落在 ADR-0085 已画的边界内 (auto 派生 + assigned page),理由进 changeset + 代码注释即可(合"fewer ADRs"取向)。唯一 可选动作:往 ADR-0085 §2 的 rejected-keys 清单补一行 "modal-vs-page presentation surface" 例子,防止有人(讨论中我本人就提过又撤回)再提 recordSurface。
3. 绝对 colSpan 在自动适应下 fragile,改语义 span 列数是派生的(手机 1 / modal 2 / 整页 3–4),绝对 colSpan: 3 只在脑补的那个宽度下对,换表面就溢出/错位。
→ 字段级排版原语改为与列数解耦 的 span: 'auto' | 'full'(默认 auto)。
auto:渲染器按 字段类型 × 派生列数 自动排(标量按密度并排,textarea/richtext/子表等宽控件自动整行)——90% 情况 AI 一个都不用写 。full:任何列数下独占整行。half:本期不做 (三者里 AI-safety 最弱:奇数列要取整、很多成对关系渲染器能自己推、多一个可选错的旋钮)。等 dogfood 真的暴露"成对字段在宽页被拆散"再议。4. 多列的地基已经有了 ADR-0085 的 fieldGroups + Field.group + collapse、FormSection.columns(1–4)、RecordDetails.columns 都已在 spec,并已被 validate-semantic-roles.ts 守住字段引用。缺口 :表单视图内 sections[].fields 引用 + colSpan 没有对应交叉校验;renderer 侧 #2515 (section.columns 没渲染成多列)。
贯穿全案的防呆原则(设计脊椎) 闭合枚举(编译期拒) > 智能默认(让 AI 不用写) > 单一派生器(不漂移) > 两级 lint(warning + hint) > schema.describe 即提示词。
依据:ADR-0078(no-silently-inert)、ADR-0019(lint 对 AI 和人手写同一把尺)、ADR-0085(object-level 语义角色 / 准入测试)。
开发计划 Step 1 — framework(一个 PR;纯函数 + 文档;可单测;无需浏览器;独立可合) deriveRecordSurface(fieldCount, groups) —— 放 packages/spec/src/data/,与 deriveFieldGroupLayout 并列。从字段数/分组单源派生 page/modal/drawer(移动端强制 page)。是派生,不是 authored key (ADR-0085 §5「one shared derivation, every surface」)。单测覆盖阈值边界、有/无 fieldGroups、mobile。FormFieldSchema.span: z.enum(['auto','full']).default('auto') —— 加在 packages/spec/src/ui/view.zod.ts。保留 colSpan 兼容。.describe() 写成 AI 引导语:"默认省略(按类型自动);整行用 full;别用绝对列数,表面列数是派生的。"packages/lint/src/validate-form-layout.ts (照 validate-semantic-roles.ts 模子):
form-field-unknown(warning):section 引用了对象上不存在的字段;absolute-colspan-discouraged(warning + hint):用了 colSpan,引导改 span:'full'(自动列数下 overflow 无良定义,故不做比大小)。
导出进 lint index。ADR-0085 §2 一行澄清 :rejected-keys 清单显式加 "modal-vs-page presentation surface"。changeset (理由写这里,不新开 ADR)。验收 :单测绿;os validate 对种子坏对象吐出两条新 warning;spec 为 additive(不升 major)。
Step 2 — objectui + 浏览器 dogfood(一个 PR) ObjectView/ObjectForm 消费 deriveRecordSurface();create/edit/detail 统一走派生表面,替掉硬编码的 layout='drawer' 默认。字段渲染:span:'auto' → 宽度按 widget 类型 × 派生列数(宽控件自动整行);span:'full' → 整行;遗留 colSpan渲染期 clamp = min(colSpan, 当前列数)(降级不破版,ADR-0078)。 修 console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515 (section.columns 未渲染成多列)作为本 PR 一部分。Dogfood 一个 ~60 字段对象(如 contract):AI 元数据里不写任何表面/colSpan 配置 → 自动全屏 + 多列分组 + textarea 自动整行;并验证 assigned page 覆盖能强制全屏。走"prove-it-runs"浏览器验证。Non-goals / 已否决的替代方案(避免重复讨论) ❌ recordSurface(或任何 object 级 modal/page 开关)—— 被 ADR-0085 §2 准入测试否决。 ❌ 新 ADR / 独立设计文档 —— 不必要,落在 ADR-0085 边界内。 ❌ span:'half' —— 暂缓,等 dogfood 暴露真实成对断裂再议。 ❌ 绝对 colSpan 作为主要 排版原语 —— 仅保留兼容,lint 引导迁移。 Refs
✅ 开发效果(Delivered)
一句话
字段多的对象,新增 / 编辑 / 查看详情自动切换到整页窗口;字段少的对象保持抽屉 / 弹窗。全部由平台按字段数自动推断,AI 写元数据时零额外配置——不暴露
recordSurface这类可写 key,所以 AI 无从写错。Before → After
section.columns)type:'simple'忽略 per-section 列数,分组内字段始终单列(#2515)colSpan(1–4),换表面(手机 / 弹窗 / 整页)就错位span(auto/full),与派生列数解耦,任何列数都不溢出;遗留colSpan渲染期 clamp,永不越界form-field-unknown/absolute-colspan-discouraged),AI 与人手写同一把尺(ADR-0019 / 0078)recordSurface开关 + 新 ADR浏览器验证(真 console + app-showcase 后端,端到端跑通)
field_zoo/…/showcase_field_zoo/record/<id>,面包屑 + 两列详情,无遮罩product?recordId=<id>,右侧抽屉浮层覆盖虚化的列表录入表单同时验证多列:
Email │ URL两列、Phone │ Password两列、Textarea独占整行(宽控件span:auto自动整行)。用户 / AI 视角
交付与验证
f79263a19:deriveRecordSurface(spec/src/data/record-surface.ts)+FormFieldSchema.span+packages/lint/src/validate-form-layout.ts+ ADR-0085 §2 澄清 + changeset。spec 6677 测试、api-surface(regen)、lint 121 全绿。97726afa4+a56cb420f:autoLayout.resolveColSpan(span 感知 / clamp / per-section 密度)+ObjectFormgrouped 路径修 console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515 +FormField.span透传 + app-shell 记录表面接入deriveRecordSurface。autoLayout 43 + plugin-form 173 测试、turbo type-check 29/29 全绿。packages/app-shell/src/views/ObjectView.tsx决定(原默认drawer),已在其detailNavigation默认接入deriveRecordSurface——底层 plugin-view 改动不够,必须在此层接入才在 console 生效。已知边界 / 后续
section.columns」路径:现有 showcase 对象都不声明 per-section 列数,该分支由单测覆盖(per-section 密度 4 个用例),无浏览器截图。deriveRecordSurface目前是本地镜像(objectui pin@objectstack/spec@^11.7,早于该导出);待 spec ≥ 11.10 发版后换成@objectstack/spec/data的 import(代码已留 TODO 注释,逻辑与阈值与 spec 一致)。背景
字段特别多的对象(50+ 字段),来自 #2574 的两个诉求:
北极星约束:所有元数据都是 AI 写的,所以设计的核心目标不是"能配",而是让 AI 无从写错。下面每个决策都围绕这一点。
关键决策(为什么不走最直觉的做法)
1. 不新增
recordSurface之类的对象级 key按 ADR-0085 §2 的"未来 key 准入测试",呈现表面(modal/page)两条都不过:
→ 呈现表面 = 渲染器启发式(auto) + assigned page 覆盖,不是 authored key。
2. 不新写 ADR / 设计文档
方案完全落在 ADR-0085 已画的边界内(auto 派生 + assigned page),理由进 changeset + 代码注释即可(合"fewer ADRs"取向)。唯一可选动作:往 ADR-0085 §2 的 rejected-keys 清单补一行 "modal-vs-page presentation surface" 例子,防止有人(讨论中我本人就提过又撤回)再提
recordSurface。3. 绝对
colSpan在自动适应下 fragile,改语义span列数是派生的(手机 1 / modal 2 / 整页 3–4),绝对
colSpan: 3只在脑补的那个宽度下对,换表面就溢出/错位。→ 字段级排版原语改为与列数解耦的
span: 'auto' | 'full'(默认auto)。auto:渲染器按字段类型 × 派生列数自动排(标量按密度并排,textarea/richtext/子表等宽控件自动整行)——90% 情况 AI 一个都不用写。full:任何列数下独占整行。half:本期不做(三者里 AI-safety 最弱:奇数列要取整、很多成对关系渲染器能自己推、多一个可选错的旋钮)。等 dogfood 真的暴露"成对字段在宽页被拆散"再议。4. 多列的地基已经有了
ADR-0085 的
fieldGroups+Field.group+collapse、FormSection.columns(1–4)、RecordDetails.columns都已在 spec,并已被validate-semantic-roles.ts守住字段引用。缺口:表单视图内sections[].fields引用 +colSpan没有对应交叉校验;renderer 侧 #2515(section.columns没渲染成多列)。贯穿全案的防呆原则(设计脊椎)
闭合枚举(编译期拒) > 智能默认(让 AI 不用写) > 单一派生器(不漂移) > 两级 lint(warning + hint) > schema.describe 即提示词。
依据:ADR-0078(no-silently-inert)、ADR-0019(lint 对 AI 和人手写同一把尺)、ADR-0085(object-level 语义角色 / 准入测试)。
开发计划
Step 1 — framework(一个 PR;纯函数 + 文档;可单测;无需浏览器;独立可合)
deriveRecordSurface(fieldCount, groups)—— 放packages/spec/src/data/,与deriveFieldGroupLayout并列。从字段数/分组单源派生 page/modal/drawer(移动端强制 page)。是派生,不是 authored key(ADR-0085 §5「one shared derivation, every surface」)。单测覆盖阈值边界、有/无 fieldGroups、mobile。FormFieldSchema.span: z.enum(['auto','full']).default('auto')—— 加在packages/spec/src/ui/view.zod.ts。保留colSpan兼容。.describe()写成 AI 引导语:"默认省略(按类型自动);整行用full;别用绝对列数,表面列数是派生的。"packages/lint/src/validate-form-layout.ts(照validate-semantic-roles.ts模子):form-field-unknown(warning):section 引用了对象上不存在的字段;absolute-colspan-discouraged(warning + hint):用了colSpan,引导改span:'full'(自动列数下 overflow 无良定义,故不做比大小)。导出进 lint index。
验收:单测绿;
os validate对种子坏对象吐出两条新 warning;spec 为 additive(不升 major)。Step 2 — objectui + 浏览器 dogfood(一个 PR)
ObjectView/ObjectForm消费deriveRecordSurface();create/edit/detail 统一走派生表面,替掉硬编码的layout='drawer'默认。span:'auto'→ 宽度按widget 类型 × 派生列数(宽控件自动整行);span:'full'→ 整行;遗留colSpan渲染期 clamp =min(colSpan, 当前列数)(降级不破版,ADR-0078)。section.columns未渲染成多列)作为本 PR 一部分。Non-goals / 已否决的替代方案(避免重复讨论)
recordSurface(或任何 object 级 modal/page 开关)—— 被 ADR-0085 §2 准入测试否决。span:'half'—— 暂缓,等 dogfood 暴露真实成对断裂再议。colSpan作为主要排版原语 —— 仅保留兼容,lint 引导迁移。Refs