Skip to content

控制台保存筛选条件会 422:ViewFilterRuleSchema 拒绝 filter-builder 盖的 id,而 wire 成员的 .strip() 救不到嵌套块 #5114

Description

@xuyushun441-sys

发现于 #4001 批 18,现存于 main,不是本批引入的。批 18 在自己的 ListView.sort 上撞到同一机制并及时回退,顺手把这条已落地的实测出来。

事实(实测,非阅读推断)

ViewFilterRuleSchema 在批 18 之前的某一批已收紧为 strictObject。objectui 的筛选构建器每行都盖一个 id:

  • objectui/packages/components/src/custom/filter-builder.tsx:228 —— id: crypto.randomUUID()

在 origin/main 上直接 parse:

ViewFilterRuleSchema.safeParse({ id:'c0ffee00-…', field:'stage', operator:'equals', value:'won' })
  => REJECTED  unrecognized_keys: ["id"]
     "Unrecognized key(s) on this view filter rule: `id`. …"

ListViewSchema.safeParse({ columns:['name'], filter:[<同一行>] })            => REJECTED
ViewMetadataSchema.safeParse({ type:'grid', columns:['name'], filter:[…],
                               name:'o.default', viewKind:'list', object:'o' }) => REJECTED

第三行是关键:那是扁平化 personalization overlay 的形状,也就是控制台 PUT 真正发出的 body。它命中 ViewMetadataSchema 的扁平成员——那个成员是刻意 .strip() 的——然而依旧被拒。

机制(这条比这个 bug 本身更值钱)

.strip() 不递归,和 .strict() 一样不递归。

ViewMetadataSchema 用扁平成员上的 .strip() 来放行 Studio 的往返辅助键,view.zod.ts 里那段块注释也是这么写的。但 .strip() 只重新打开顶层。任何在 ListViewSchema 内部被收紧的嵌套块,仍然经由那个成员被解析——所以嵌套块里一个控制台盖的键,无论成员姿态如何都会 422。

这意味着这个战役在 view.zod.ts(以及任何有 wire overlay 成员的形状)上收紧嵌套块时,.strip() 的救援是够不到的,而块注释读起来像是够得到。批 18 已就地把那段注释改成实测描述,并在 view-strictness-batch18.test.ts 里钉住了这条递归性质。

影响

控制台改一次筛选条件并保存 → saveMetaItem 的 safeParse 失败 → 422。凡是经由 filter-builder 落盘的 view personalization 都受影响。

⚠️ 我没有跑起真实 app 点一次筛选去端到端确认(批 18 的范围不在这)。上面是 schema 层的直接 parse 证据 + 产出端的 randomUUID 站点;端到端复现请按 .claude/skills/dogfood-verification 走一遍再动手。

处方(倾向,不代劳裁定)

与 #5074 同族,别就地声明 id 了事:它是 React 列表键,不是协议。声明它等于把 UI 造物放进可授权面,并且会教 AI 作者去生成一个 UUID —— 正是本战役要防的那类「声明即鼓励」。

方向应当与 #5074 一致:授权形状收紧、wire 形状单独开放,且开放必须能递归到嵌套块(这正是当前 .strip() 做不到的那一半)。可选做法之一是让 personalization 写路径在校验前剥掉这类 UI 造物(与 stripReadDecorations 同款范式,只是方向相反——那边剥的是读路径装饰键)。

复核用最小脚本

const V = await import('@objectstack/spec/ui');
const row = { id: 'c0ffee00-dead-beef-cafe-000000000000', field: 'stage', operator: 'equals', value: 'won' };
console.log(V.ViewFilterRuleSchema.safeParse(row).success);            // false
console.log(V.ListViewSchema.safeParse({ columns:['name'], filter:[row] }).success); // false

Activity

  1. xuyushun441-sys commented on Aug 4, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    🔒 认领:PM 循环热修派发(p1——控制台过滤器保存今天在 main 上 422)
    会话:session_01Ehu85kbvMcrNTUJjwxvLJ9
    分支:claude/issue-5114-filter-rule-hotfix
    Worktree:objectstack-issue-5114
    域:domain:spec
    文件面:packages/spec/src/ui/view.zod.ts(仅 ViewFilterRuleSchema 站点)+ pin 测试 + 台账 ui 行注记

    修法定调(照批 18 对 ListView.sort 的既定处理,不重议):回退该站点为留开 + 三处落锚(JSDoc + pin + 台账),⛔ 不声明 id(React list key 不上授权面);正解(递归 wire 变体)由 #5074 承接,落地后重关。验收:控制台过滤器保存 PUT 的真实 body(含 crypto.randomUUID() 的 id,批 18 已给三路径探针)parse 通过 + dogfood 浏览器端到端确认(批 18 未跑真应用,本单必须跑)。⚠️ 与在飞批 18 PR #5070 同文件——等 #5070 落地后基于新 main 开工,派发词已写明。


    Generated by Claude Code

  2. xuyushun441-sys commented on Aug 4, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    热修已开 draft PR #5154 —— 附一条 dogfood 挖出来的、必须先说的更正

    本 issue 的处方是对的、必要的,但它不足以让「控制台保存筛选条件」通。 真浏览器端到端跑出来的结论:控制台把整个 FilterGroup 对象存进 filter,而 spec 声明的是 ViewFilterRule[] —— 那是外层类型错,挡在 id 前面。已另案 #5159(p1,未指派,产出端 objectui)。

    端到端怎么跑的(本 issue 正文点名要补的那一步)

    pnpm objectui:build 出固定 pin(f5bc4c78)的真 /_console bundle → showcase 起在自有端口(--ui --seed-admin,自有 DB)→ Playwright 真实登录 → /_console/apps/showcase_app/showcase_task → 点工具栏 Filter → Add filter。抓到:

    PUT /api/v1/meta/view/showcase_task.default  ->  422
    {"code":"INVALID_METADATA","issues":[{"path":"","message":"Invalid input","code":"invalid_union"}]}
    
    body.filter = {"id":"root","logic":"and",
                   "conditions":[{"id":"712135fb-…","field":"title","operator":"equals","value":""}]}
    

    注意 conditions[0] 正是本 issue 说的那个形状 —— {id: <uuid>, field, operator, value}。两个缺陷叠在同一份 body 上。

    把这份真 body 三变体回放到两个真实运行的 server

    变体 修前(origin/main) 修后(#5154)
    ① 原样(FilterGroup 对象) 422 422 ← #5159,本 PR 修不到
    ② 拆掉分组 → rule[],保留 UI id 422 ACCEPTED ← 本 issue
    ③ 拆掉分组 → rule[],去掉 id ACCEPTED ACCEPTED(对照)

    ②③ 对照把变量锁死在 id 上:本 PR 的效果就是把 ② 从 422 翻成通过,一分不多一分不少。外层不修,控制台仍然 422。

    给排期的三点

    1. fix(spec): 重开 ViewFilterRuleSchema —— 控制台盖的 UI 行 id 让保存筛选条件 422(#5114 热修) #5154 该合 —— 它是必要的一半,且 ② 那一格今天在 main 上是真红的(任何发 rule[] + id 的产出端都吃 422,含已存库带 id 的行)。
    2. 控制台「筛选」把整个 FilterGroup 对象存进 view 的 filter(spec 声明的是 ViewFilterRule[])—— 真浏览器实测 422,#5114 热修修不到 #5159 得跟着排,否则「控制台保存筛选条件」这条用户可见路径不会因为 fix(spec): 重开 ViewFilterRuleSchema —— 控制台盖的 UI 行 id 让保存筛选条件 422(#5114 热修) #5154 合并而恢复。按契约优先应在产出端折平(Studio 侧 widgets.tsx:1727 已有同款转换),⛔ 不要放宽 ListViewSchema.filter。
    3. 控制台「筛选」把整个 FilterGroup 对象存进 view 的 filter(spec 声明的是 ViewFilterRule[])—— 真浏览器实测 422,#5114 热修修不到 #5159 里附带记了一条:FilterGroup.logic: 'or' 今天没有落盘表示,折平时会被静默降级成 and —— 修 控制台「筛选」把整个 FilterGroup 对象存进 view 的 filter(spec 声明的是 ViewFilterRule[])—— 真浏览器实测 422,#5114 热修修不到 #5159 时需要一并判定。

    ⚠️ 另外,三条路径里第 3 条(扁平 overlay)的 422 消息是 invalid_union / "Invalid input"(#5014 的压平)—— 报错里读不出 id、也读不出 filter。这既是本 bug 能在 main 上活着没人发现的原因,也是 #5159 一样没人发现的原因。


    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