Skip to content

fix(spec): 重开 ViewFilterRuleSchema —— 控制台盖的 UI 行 id 让保存筛选条件 422(#5114 热修) - #5154

Merged
xuyushun441-sys merged 3 commits into
mainfrom
claude/issue-5114-filter-rule-hotfix
Aug 4, 2026
Merged

fix(spec): 重开 ViewFilterRuleSchema —— 控制台盖的 UI 行 id 让保存筛选条件 422(#5114 热修)#5154
xuyushun441-sys merged 3 commits into
mainfrom
claude/issue-5114-filter-rule-hotfix

Conversation

@xuyushun441-sys

@xuyushun441-sys xuyushun441-sys commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #5114

p1 热修:ViewFilterRuleSchema 被更早的一批收紧成 strictObject,而 objectui 的筛选构建器给每一行盖 id: crypto.randomUUID()(React 列表键),saveMetaItem 校验后原样存原始 body,所以那个 id 就在 wire 上 —— 闭合的形状把它变成 422。

处理照 #4001 批 18 对 ListView.sort 的既定做法(#5070),不重议:回退该站点为留开 + 三处落锚,⛔ 不声明 id,正解由 #5074 承接。分支基于 #5070 落地后的 origin/main,已 merge 到 bcfebb0

⚠️ dogfood 挖出来的、必须先说的一条:本 PR 必要,但不足以让「控制台保存筛选条件」通。 真浏览器实测:控制台把整个 FilterGroup 对象存进 filter,而 spec 声明的是 ViewFilterRule[] —— 那是外层类型错,挡在 id 前面,属产出端(objectui)缺陷,已另案 #5159。两个缺陷叠在同一份 body 上,本 PR 修掉里层那个。证据见 §2。

改了什么(4 文件)

文件 改动
packages/spec/src/ui/view.zod.ts ViewFilterRuleSchema:strictObject(…)z.object(…),只此一处 + 其 JSDoc
packages/spec/src/ui/view-filter-rule-wire-id.test.ts 新增 pin 测试 11 条
docs/audits/2026-07-unknown-key-strictness-ledger.md ui/ 行 + 表头 + 算术散文,从存活行重算
.changeset/view-filter-rule-console-id-hotfix.md patch

⛔ 为什么不顺手声明 id

它是 React 列表键,不是协议。声明它 = 把 UI 造物放上可授权面,并教 AI 作者给筛选行生成一个 UUID —— 这是 ?? 兜底换了身 schema 外衣。批 18 Q1 的两轴否决在案、PM 已批。重开后 id 是被丢弃(不进 parsed.data),不是被声明;而 saveMetaItem 存原始 body,所以渲染器照样读得到它 —— 两条都钉在测试里。

机理(比这个站点值钱,已写进 JSDoc)

.strip() 不递归,和 .strict() 一样不递归。 ViewMetadataSchema 靠扁平成员上的 .strip() 放行 Studio 往返辅助键,但那只重开顶层;嵌套块仍经由该成员被解析,所以嵌套块里一个控制台盖的键无论成员姿态如何都会 422。这正是本 bug 的成因,也是批 18 在 ListView.sort 上撞到的同一条。

验证

1. 三路径直接 parse —— 先证红,再证绿

修前(分支起点 origin/main):

1 ViewFilterRuleSchema             REJECTED   unrecognized_keys path=[]            `id`
2 ListViewSchema.filter            REJECTED   unrecognized_keys path=["filter",0]  `id`
3 ViewMetadataSchema (overlay)     REJECTED   invalid_union     path=[]            "Invalid input"
ALL THREE ACCEPT: false   (exit 1)

修后:

1 ViewFilterRuleSchema             ACCEPTED
2 ListViewSchema.filter            ACCEPTED
3 ViewMetadataSchema (overlay)     ACCEPTED
ALL THREE ACCEPT: true    (exit 0)

⚠️ 第 3 条修前那句 Invalid input#5014 实测的那个压平:报错里id 两个字都没有。这就是这条 bug 能在 main 上活着没人发现的原因 —— 错误信息读不出病因。

2. dogfood:真浏览器 + 真 /_console bundle

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

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-58c5-4be4-a611-1925181509b0",
                              "field":"title","operator":"equals","value":""}]}

conditions[0] 正是本 issue 那个形状。把这份真 body 三变体回放到两个真实运行的 server(只改 filter):

变体 修前 修后(本 PR)
① 原样(控制台发的 FilterGroup 对象) 422 invalid_union 422 invalid_union#5159
② 拆掉分组 → rule[],保留 UI 的 id 422 ACCEPTED ← 本 PR
③ 拆掉分组 → rule[],去掉 id ACCEPTED ACCEPTED(对照)

读法:两个缺陷叠在同一份 body 上。外层类型错(#5159,产出端)挡在前面;拆开之后,下一道就是本 PR 修的 id。②③ 的对照把变量锁死在 id 上 —— 本 PR 的效果就是把 ② 从 422 翻成通过,一分不多一分不少。

另有一组不经浏览器、直打 PUT /api/v1/meta/view/:name 的对照(扁平 personalization overlay,filter 行带 crypto.randomUUID()):

[PRE-FIX]   A rule[] WITH console id -> REJECTED 422 invalid_union   |  C 无 id -> ACCEPTED
[POST-FIX]  A rule[] WITH console id -> ACCEPTED (seq=1)             |  C 无 id -> ACCEPTED

#5114 正文里「未跑真实 app 端到端」的缺口到此补上 —— 并且正是这一跑,把 #5159 挖了出来。

3. 仪器纪律 —— 断言两向都证过红

破坏 结果
重新关闭 ViewFilterRuleSchema 7 红(三路径 + tab 载体 + id 被丢弃 + 顶层 aux)
台账行 6 → 5 check:strictness-ledger EXIT=1
台账表头 76 → 75 check:strictness-ledger EXIT=1

pin 文件里那两条机理对照(顶层 aux 键随行 / 嵌套已关 emptyState 仍拒)刻意跑在不带 filter 的 overlay 上 —— 所以它们在两种姿态下都绿。断言红了、对照没红,那个落差本身就是「.strip() 不递归」这条发现。

4. 门与套件(合并 origin/main 之后重跑)

packages/spec  vitest run          Test Files 305 passed (305) / Tests 7759 passed (7759)
               tsc --noEmit        EXIT=0
               check:generated     ✓ All 8 generated artifacts are up to date
               check:strictness-ledger
                 ✓ 67 file(s) / 5 director(ies) — site counts match, section totals balance
                 ✓ remaining-strip map: 36 open file(s) / 235 strip site(s)   (批 18 落地时 234,+1 如期)
packages/metadata-protocol  vitest run   Test Files 37 passed (37) / Tests 334 passed (334)

台账合并 —— 第 11 例「两边都对、合并都错」,方向和前十例相反

批 17(#5068,把 component.zod.ts 29 站点整行改判 no gate)如期冲突。按既定纪律:取 main 整段 + 重贴本单编辑 + 逐条断言对方条目存活 + 从存活行重算

前十例都是站点把这行推低;本单是一个站点把它推高 —— 我的分支算 36 of 76(批 17 未落地的树),main 算 6 of 75(本单未落地的树),合并是 7 of 76,两边都没写过。机理已记进散文。

content/docs/releases/ 零触碰;objectui 零改动(只读);os-regen 四步走完(merge → 取 main 生成物 → 整体重生成 → 断言兄弟条目存活),8 件生成物全绿。

顺带发现(⛔ 未在本 PR 修)


🤖 Generated with Claude Code

https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9

…id (#5114)

Saving a filter from the console 422'd on `main`. An earlier strictness wave
closed `ViewFilterRuleSchema` with `strictObject`, and objectui's filter builder
stamps `id: crypto.randomUUID()` on every row it writes
(`components/src/custom/filter-builder.tsx:228`, re-stamped on read-back at
`plugin-view/src/config/view-config-utils.ts:146`/`:160`). `saveMetaItem`
validates the PUT body and then persists the AUTHORED body verbatim, so the `id`
is on the wire — and the closed shape rejected it.

Measured on all three paths before the change, on `origin/main`:

  ViewFilterRuleSchema           -> unrecognized_keys @ []            `id`
  ListViewSchema.filter          -> unrecognized_keys @ ["filter",0]  `id`
  ViewMetadataSchema (overlay)   -> invalid_union     @ []            "Invalid input"

The third is the body the console actually PUTs, and its message is the #5014
flattening: the key that caused the rejection is not in what the author sees,
which is how this sat on `main` unnoticed.

The mechanism governs every nested block in this file and is the opposite of
what the union's comment implies: `.strip()` does NOT recurse, any more than
`.strict()` does. `ViewMetadataSchema` re-opens its flattened members so
Studio's round-trip aux keys ride along — but that re-opens the TOP level only,
so a nested block closed here is still reached through that member and a
console-stamped key inside it 422s regardless of the member's posture. Same
finding 批 18 reached one block over on `ListView.sort` (#5070).

`id` is deliberately NOT declared. It is a React list key, not protocol:
declaring it would put a UI artifact on the authorable surface and tell an AI
author to generate a UUID for a filter rule — a `??` fallback wearing a schema.
Reopening drops it from `parsed.data` instead, and `saveMetaItem` stores the
original body, so the renderer still reads it. The real close is #5074's
authoring/wire split applied to this block, whose scope addendum names this site.

Verified in both directions: re-close the schema and 7 assertions in the new pin
file go red, while its two mechanism CONTROLS (top-level aux key rides, nested
`emptyState` still rejects) stay green either way. Ledger gate proven red on
both the row (6→5) and the header (76→75).

Ledger: ui/ 75 -> 76 strip, authorable 35 -> 36, recomputed from surviving rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 7:12am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling size/m labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…ter-rule-hotfix

# Conflicts:
#	docs/audits/2026-07-unknown-key-strictness-ledger.md
…5114)

Browser verification against the real vendored console found a second,
independent defect stacked on the same request: the list toolbar persists the
filter builder's whole `FilterGroup` object into `filter`, where the spec
declares `ViewFilterRule[]`. That type mismatch rejects before the `id` is ever
reached, so reopening `ViewFilterRuleSchema` does not by itself make "save a
filter from the console" work — it is necessary, not sufficient.

Replaying the captured console body against two running servers separates them:

  variant                                  pre-fix   post-fix
  as sent (FilterGroup object)             422       422        <- producer bug
  unwrapped to rule[] WITH the UI ids      422       ACCEPTED   <- this change
  unwrapped to rule[] without ids          ACCEPTED  ACCEPTED   <- control

The changeset headline said the console save no longer 422s. It does. Release
notes are the one place that claim would have reached users unqualified, so it
now states what actually changed and names the remaining blocker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants