Skip to content

字段多的对象:自适应呈现表面 + 语义 field span(auto/full)+ 表单布局防呆 lint(AI-authored,ADR-0085 对齐) #2578

Description

@os-zhuang

状态:✅ 已实现并浏览器端到端验证(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_zoo57整页/…/showcase_field_zoo/record/<id>,面包屑 + 两列详情,无遮罩
产品 product6抽屉停在列表 ?recordId=<id>,右侧抽屉浮层覆盖虚化的列表

录入表单同时验证多列:Email │ URL 两列、Phone │ Password 两列、Textarea 独占整行(宽控件 span:auto 自动整行)。

用户 / AI 视角

  • AI 建一个几十字段的对象、不写任何表面或列配置 → 记录自动整页 + 表单自动多列 + 长文本自动整行。
  • 想让某个对象也强制整页 → 指派一个 record page(ADR-0085 认定的 per-page 正道),而不是加新 key。

交付与验证

  • frameworkf79263a19: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 全绿
  • objectui97726afa4 + a56cb420f:autoLayout.resolveColSpan(span 感知 / clamp / per-section 密度)+ ObjectForm grouped 路径修 console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515 + FormField.span 透传 + app-shell 记录表面接入 deriveRecordSurfaceautoLayout 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 的两个诉求:

  1. 多列编辑/显示;
  2. 部分对象在新增/编辑/查看详情时自动切到全屏页面,而不是弹窗。

北极星约束:所有元数据都是 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 + collapseFormSection.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;纯函数 + 文档;可单测;无需浏览器;独立可合)

  1. deriveRecordSurface(fieldCount, groups) —— 放 packages/spec/src/data/,与 deriveFieldGroupLayout 并列。从字段数/分组单源派生 page/modal/drawer(移动端强制 page)。是派生,不是 authored key(ADR-0085 §5「one shared derivation, every surface」)。单测覆盖阈值边界、有/无 fieldGroups、mobile。
  2. FormFieldSchema.span: z.enum(['auto','full']).default('auto') —— 加在 packages/spec/src/ui/view.zod.ts。保留 colSpan 兼容。.describe() 写成 AI 引导语:"默认省略(按类型自动);整行用 full;别用绝对列数,表面列数是派生的。"
  3. 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。
  4. ADR-0085 §2 一行澄清:rejected-keys 清单显式加 "modal-vs-page presentation surface"。
  5. changeset(理由写这里,不新开 ADR)。

验收:单测绿;os validate 对种子坏对象吐出两条新 warning;spec 为 additive(不升 major)。

Step 2 — objectui + 浏览器 dogfood(一个 PR)

  1. ObjectView/ObjectForm 消费 deriveRecordSurface();create/edit/detail 统一走派生表面,替掉硬编码的 layout='drawer' 默认。
  2. 字段渲染:span:'auto' → 宽度按 widget 类型 × 派生列数(宽控件自动整行);span:'full' → 整行;遗留 colSpan渲染期 clamp = min(colSpan, 当前列数)(降级不破版,ADR-0078)。
  3. console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515(section.columns 未渲染成多列)作为本 PR 一部分。
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions