Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Page 块检查器提供的 hidden 条件框写的是 spec 不认识的键(canonical 是 visibleWhen) #3229

Closed

Description

@os-zhuang

做 #3218(表达式信封普查)时越界发现,未在该 PR 里改 —— 它不是信封读法那一族,是「declared ≠ enforced」的另一族。

缺陷

packages/app-shell/src/views/metadata-admin/inspectors/PageBlockInspector.tsx:553-558 给选中的 page 块渲染了一个条件构建器:

<ConditionBuilder
  label={t('engine.inspector.pageBlock.hidden', locale)}
  value={block.hidden ?? ''}
  onCommit={(v) => patch({ hidden: v || undefined })}
  ...
/>

而 @objectstack/spec 的 PageComponentSchema(ui/page.zod.ts)是 .strict() 的,键集合里没有 hidden:条件可见性在这里的 canonical 名字是 visibleWhen(ADR-0089),外加一个已废弃别名 visibility。

实测(@objectstack/spec@17.0.0-rc.1):

PageSchema.parse({
  name: 'p1', label: 'P', type: 'record', template: 'default',
  regions: [{ name: 'main', components: [{ type: 'text', id: 'b1', hidden: 'record.x == 1' }] }],
});
ZodError: unrecognized_keys, path: regions.0.components.0, keys: ["hidden"]
message: Unrecognized key(s) on this view/page schema: `hidden`. Before ADR-0089 D3a these
were dropped silently, shipping inert metadata; a mis-layered or stale key is now a loud
parse error. If this is the conditional-visibility predicate, the canonical key is
`visibleWhen` (ADR-0089) — `visibleOn` (view form) and `visibility` (page component) are
still accepted as deprecated aliases.

spec 的报错自己把修法写出来了。

为什么是真 bug

previews/PageBlockCanvas.tsx 的文件头写明它「Supports only the canonical Page shape (regions[].components[])」,也就是被选中的块就是 PageComponent。所以这个输入框提供的是一个作者填了就存不进去的键:

  1. 作者在检查器里填 hidden,草稿保存时 page 解析在 ADR-0089 D3a 之后是响亮失败(不再静默丢弃),而错误信息指向的是一个作者没听说过的键名;
  2. 语义还是反的 —— hidden 为真时隐藏,visibleWhen 为真时显示。就算把键名改过去也不能直接搬值,取反才对;
  3. 反过来,一个合法的块(带 visibleWhen)在这个检查器里没有任何控件可编辑它的可见性谓词 —— 唯一露出来的可见性控件写的是错键。

这是 Prime Directive #10 的形态:界面上宣传了一个运行时/spec 并不接受的能力。

建议修法(需要决策,不宜由实现者猜)

至少两种走法,取舍不同:

  • A:把控件改成编辑 visibleWhen,并在 UI 上把语义从「hidden when」翻成「visible when」(标签、i18n key、以及 engine.inspector.pageBlock.hidden 文案都要动)。历史草稿里已存的 hidden 要不要读进来取反,是附带问题。
  • B:先把这个控件摘掉(它今天写的是必然存不进去的键),等 page 检查器统一按 ADR-0089 的 *When 词汇重做时一起补。

倾向 A,但翻转语义会改变作者已经形成的心智,且涉及 i18n 双语文案,应由维护者定。

相关

Activity

  1. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    认领:PM 循环第 7 轮 · v17 发布窗口:协议类优先
    会话:session_01NVPjPzmmAJ2Ngtvgg5MSRa(objectui 分片 PM,账号 xuyushun441-sys)
    分支:claude/issue-3229-page-block-visible-when
    Worktree:objectui-issue-3229

    裁定:方案 A(控件改写 visibleWhen),维护者已同意

    原单把 A/B 标为"需要决策,不宜由实现者猜"。已就两条评估轴与维护者综合评估并取得同意,裁定 A。

    平台长远合理性:恢复「设计器只产出 spec 接受的键」这条不变量,同时补上原单指出的第三个缺口 —— 一个合法的块(带 visibleWhen)今天在检查器里没有任何控件能编辑它的可见性。B(先摘掉控件)不再教错,但那个缺口继续存在,是能力回退。

    防 AI 写错:这是决定性的一轴。检查器是生产者,它今天在批量制造必被拒的草稿 —— 这比消费端宽容更糟一档:宽容是"错的东西能跑",这是"对的工具产出错的东西"。A 之后设计器产出的就是 canonical 词汇。

    一个我核实后已解除的顾虑

    原单说「历史草稿里已存的 hidden 要不要读进来取反,是附带问题」。这不构成阻塞:ADR-0089 D3a 之后 hidden 在保存时就是响亮解析失败,所以不存在带该键的合法存量;D3a 之前的键在 parse 时被静默剥掉,同样进不了已发布的元数据。

    派发范围

    1. PageBlockInspector.tsx(约 :553-558)的 ConditionBuilder 改为读写 visibleWhen;
    2. 语义翻转必须落在 UI 文案上 —— 标签、i18n key(engine.inspector.pageBlock.hidden → 对应 visibleWhen 的新 key)、以及中英双语文案,全部从「hidden when」翻成「visible when」;
    3. 同文件 :193 的本地类型里 hidden?: string 一并跟随。

    ⛔ 明确不要做的两件

    不要自动把旧 hidden 表达式取反塞进 visibleWhen。 对任意 CEL 表达式做文本取反是典型的"聪明但危险"变换(a && b 的否定不是 !a && !b);而 spec 的报错信息本身就是迁移指引(它明确指名 visibleWhen)。让稀有的陈旧草稿走响亮报错,比静默改写作者的谓词安全得多。

    不要只换键名而留下 hidden 语义的标签。 键换了、标签还写「隐藏条件」,作者会把条件写反 —— 那正好制造出 objectui#3276 那种「能 parse 但语义反了」的元数据,而那是 #3257 的守卫结构上抓不到的一类(它只问能不能 parse)。本仓今天刚为此修过一个样例,不要在检查器里再造一个。

    验收:测试必须钉住方向

    ⚠️ scope fence:packages/app-shell/src/views/metadata-admin/inspectors/。同期另有 agent 在同包的 views/metadata-admin/previews/(#3275)与 views/RecordDetailView.tsx(#3268),文件不相交,但 i18n 语言包可能是共享面 —— 若你要动 packages/i18n 或 app-shell 的语言包,只加你自己的 key,不要重排既有条目。


    Generated by Claude Code

  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    PM 验收:ACCEPT — PR #3286,已入合并队列

    项 结果
    改动范围 ✅ 4 个文件,全在 views/metadata-admin/(inspector + i18n + 测试 + changeset)
    键改为 visibleWhen ✅
    语义翻转落在 UI 文案上 ✅ 英文 Hidden (CEL) → Visible when (CEL),中文「隐藏条件」→「显示条件」
    未自动取反迁移 ✅
    方向测试 ✅ 且不空转
    CI 已完成项全绿零失败;4 个 shard 与 Type Check 在跑,auto-merge 会等它们

    一处我和原单都没指定、而它做对了的事

    visibleWhen 是 ExpressionInputSchema,所以持久化的块携带的是 { dialect, source } 信封而不是作者写的裸字符串。它没有直接写字符串,而是走 #3218 已有的 expressionSource / writeExpressionSource 这对读写器 —— 编辑会保留 dialect 与 meta、丢弃过期的 ast。

    这条不是锦上添花:天真地写回裸字符串会把信封拍平,并静默切换求值引擎。测试里专门有一例钉住带 meta 的信封在编辑后仍然完整。

    测试的两处设计值得记

    1. fixture 纪律:没有一个信封是手写的。 每个 fixture 都是"作者写的输入 → 过 PageSchema.parse",所以 fixture 结构上不可能与 spec 漂移。它还额外钉住了平台实际存储的形状:

    expect(draft…visibleWhen).toEqual({ dialect: 'cel', source: 'record.amount > 10' });

    理由写在注释里:如果哪天 spec 不再做这个归一化,这条会响亮地红,而不是让一个过期假设继续传下去。

    2. 反证防空转:

    expect(() => pageDraft({ hidden: 'record.amount > 20' })).toThrow(/hidden/);

    这条证明"被测的严格性是真的"—— 否则那句"提交的草稿能 parse"可能只是因为 schema 根本不严格,断言就成了摆设。

    方向测试钉的是渲染结果,不是键名

    按我的硬要求,它把检查器自己提交的那个块交给 SchemaRenderer(即 PageBlockCanvas 用的渲染器)去渲染,谓词为真 → 块在;为假 → container.innerHTML === '';外加"完全没有谓词 → 块在"。

    它还指出两个方向是互相加强的:evaluateCondition 在失败时软退化为"可见",所以一个不起区分作用的谓词会让 FALSE 那一例变红。这就排除了"两条都过是因为谓词根本没生效"的可能。

    changeset 把这单的性质说清楚了

    这不是一个宽容的消费者在接受草率的输入 —— 是生产者在发出契约拒收的键。填了那个框的作者会收到一个指名他从未输入过的键的保存失败,因为是检查器替他输入的。

    以及为什么不能只改键名:

    键换了而留着 hide 味道的文案,会比留着这个 bug 更糟 —— 作者会把谓词写反,产出能 parse 却含义相反的元数据,也就是 #3276 那一类,而 #3257 的守卫对它结构上是盲的。

    不迁移旧值的理由也写全了:!(a && b) 不等于 !a && !b,且 ADR-0089 D3a 之后 hidden 在保存时就是响亮失败、D3a 之前该键被静默丢弃,两种情况下都不存在需要迁移的已发布元数据。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions