发现于 #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` 同款范式,只是方向相反——那边剥的是读路径装饰键)。 ## 复核用最小脚本 ```ts 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
fix(spec): revert ListView.sort strictness — the console stamps a UI …
xuyushun441-sys commentedon Aug 4, 2026
🔒 认领:PM 循环热修派发(p1——控制台过滤器保存今天在 main 上 422)
会话:
session_01Ehu85kbvMcrNTUJjwxvLJ9分支:
claude/issue-5114-filter-rule-hotfixWorktree:
objectstack-issue-5114域:
domain:spec文件面:
packages/spec/src/ui/view.zod.ts(仅ViewFilterRuleSchema站点)+ pin 测试 + 台账 ui 行注记修法定调(照批 18 对⚠️ 与在飞批 18 PR #5070 同文件——等 #5070 落地后基于新 main 开工,派发词已写明。
ListView.sort的既定处理,不重议):回退该站点为留开 + 三处落锚(JSDoc + pin + 台账),⛔ 不声明id(React list key 不上授权面);正解(递归 wire 变体)由 #5074 承接,落地后重关。验收:控制台过滤器保存 PUT 的真实 body(含crypto.randomUUID()的id,批 18 已给三路径探针)parse 通过 + dogfood 浏览器端到端确认(批 18 未跑真应用,本单必须跑)。Generated by Claude Code
xuyushun441-sys commentedon Aug 4, 2026
热修已开 draft PR #5154 —— 附一条 dogfood 挖出来的、必须先说的更正
本 issue 的处方是对的、必要的,但它不足以让「控制台保存筛选条件」通。 真浏览器端到端跑出来的结论:控制台把整个
FilterGroup对象存进filter,而 spec 声明的是ViewFilterRule[]—— 那是外层类型错,挡在id前面。已另案 #5159(p1,未指派,产出端 objectui)。端到端怎么跑的(本 issue 正文点名要补的那一步)
pnpm objectui:build出固定 pin(f5bc4c78)的真/_consolebundle → showcase 起在自有端口(--ui --seed-admin,自有 DB)→ Playwright 真实登录 →/_console/apps/showcase_app/showcase_task→ 点工具栏Filter→Add filter。抓到:注意
conditions[0]正是本 issue 说的那个形状 ——{id: <uuid>, field, operator, value}。两个缺陷叠在同一份 body 上。把这份真 body 三变体回放到两个真实运行的 server
origin/main)FilterGroup对象)rule[],保留 UIidrule[],去掉id②③ 对照把变量锁死在
id上:本 PR 的效果就是把 ② 从 422 翻成通过,一分不多一分不少。外层不修,控制台仍然 422。给排期的三点
main上是真红的(任何发rule[] + id的产出端都吃 422,含已存库带id的行)。filter(spec 声明的是ViewFilterRule[])—— 真浏览器实测 422,#5114 热修修不到 #5159 得跟着排,否则「控制台保存筛选条件」这条用户可见路径不会因为 fix(spec): 重开 ViewFilterRuleSchema —— 控制台盖的 UI 行 id 让保存筛选条件 422(#5114 热修) #5154 合并而恢复。按契约优先应在产出端折平(Studio 侧widgets.tsx:1727已有同款转换),⛔ 不要放宽ListViewSchema.filter。filter(spec 声明的是ViewFilterRule[])—— 真浏览器实测 422,#5114 热修修不到 #5159 里附带记了一条:FilterGroup.logic: 'or'今天没有落盘表示,折平时会被静默降级成and—— 修 控制台「筛选」把整个 FilterGroup 对象存进 view 的filter(spec 声明的是ViewFilterRule[])—— 真浏览器实测 422,#5114 热修修不到 #5159 时需要一并判定。invalid_union/"Invalid input"(#5014 的压平)—— 报错里读不出id、也读不出filter。这既是本 bug 能在main上活着没人发现的原因,也是 #5159 一样没人发现的原因。Generated by Claude Code
docs(changeset): stop claiming the console filter save is restored (#…
feat(spec)!: view.zod.ts 子块收紧 —— 20 站点收 15 留 5(objectstack-ai#4001 批 …
fix(spec): 重开 ViewFilterRuleSchema —— 控制台盖的 UI 行 id 让保存筛选条件 422(objec…
feat(spec)!: object 内层块拒绝未知键 —— 13 站点收紧,2 个站点依据实测保留 (objectstack-ai#4001
feat(spec)!: ViewItemSchema 拆成授权门 + wire 变体,并让 wire 的开放递归生效 (objectst…
test(spec): the fourth data/ file group's test titles state each cite…
test(spec): the third ui/ file group's test titles state each cited d…