fix(spec): 重开 ViewFilterRuleSchema —— 控制台盖的 UI 行 id 让保存筛选条件 422(#5114 热修) - #5154
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…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
xuyushun441-sys
marked this pull request as ready for review
August 4, 2026 07:29
This was referenced Aug 4, 2026
This was referenced Aug 4, 2026
Closed
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…正读它的那一层 (objectstack-ai#5022) (objectstack-ai#5221) * wip(spec): objectstack-ai#5022 measurement checkpoint — drillDown carrier evidence (no code yet) 断点位置:测量完成,**尚未写任何 spec 代码**。测量结果与裁决前提冲突, 倾向 needs_decision(见下)。分支上目前是空提交,仅承载证据。 ## 已完成的测量(objectui @91757a7 + objectstack @41e605ec70) ### A. ObjectChart 实际读取词汇(反推自 packages/plugin-charts/src/ObjectChart.tsx) 读到的键(:602 起,经 @object-ui/core drill-down.ts): enabled? boolean isDrillEnabled: 无 config=关;{}=开;enabled:false=关 filter? Record<string,unknown> computeDrillFilter,支持 ${event.*} 插值 title? string resolveDrillTitle,支持 ${event.*} 插值 target? 'drawer'|'dialog' :755 — **只分支 'dialog',其余一律 Sheet** columns? string[] :762 → accessorKey/header maxRows? number :761 → pageSize ObjectChart **不读**:mode / view / report / sort,且 **不实现 target:'navigate'** (navigate 只在 DrillDownDrawer.tsx:84 实现,ObjectChart 自绘 Sheet/Dialog,不走它)。 objectui 侧已有 TS 接口 DrillDownConfig(packages/types/src/data-display.ts:897), 是 5 个 widget 共享的更宽形状:{enabled,mode,target(含 navigate),filter,title, view,report,columns,sort,maxRows}。其中 view/sort 无任何渲染器读取。 ### B. 载体测量 —— 与裁决前提("渲染器已在读的 drillDown,objectui 零改动,挂 chart config")冲突 1. widget.chartConfig.drillDown → **无人读**。 DashboardRenderer.tsx 全文 0 处 chartConfig;DatasetWidget.tsx:595-605 只取 showLegend 一个键,注释自陈 "the rest of chartConfig stays unforwarded"; :632 构造的 chart schema 里没有 drillDown。 2. widget.options.drillDown → DashboardRenderer.tsx:494/:570/:531 确实读,但**全部在 isObjectProvider(widgetData) 的 legacy inline 分支内**;而 DashboardWidgetSchema 要求 dataset(dashboard.zod.ts:427,非 optional),datasetBound 恒真, DashboardRenderer.tsx:691/:720 恒走 <DatasetWidget>,componentSchema 被丢弃。 ⇒ 对 v17 合法 widget,options.drillDown 永不被读。 3. ADR-0021 正路 DatasetWidget 的 drill 是**从 dataset 结果派生、不可配置**的: canDrill 由 dimensionFields/drillRanges 决定,DrillDownDrawer 只收 {objectName, filter, title, dataSource} —— 上述词汇一个都不认。 4. 唯一真活的面是 **react tier**:ChartConfigSchema 的键在 react 块上是**扁平 prop** (react-blocks.ts:226-247 dataProps),所以 <ObjectChart drillDown={…}> 确实生效; 但 drillDown 既不在 spec dataProps/interactions,也不在 objectui registry inputs (ObjectChart.tsx:822-830),SDUI save gate 会报 unknown-prop(warning,不拦)。 ⇒ 结论:挂 chart config 在 **react tier 交付、dashboard 元数据面不交付**; 挂 options 在两面都不交付。任一选择都会造出 objectstack-ai#5011 "same key, two fates" 的形状。 ## 剩余步骤(若维护者裁定后继续) 1. [待裁决] 定载体:(A) 只挂 react 块契约 /(B) 挂 ChartConfigSchema 并明写 dashboard 面不转发 /(C) 挂 options 并在 objectui DatasetWidget 落地实现(非小改动)。 2. [待裁决] 定形状:chart 诚实子集 {enabled,filter,title,target:'drawer'|'dialog', columns,maxRows} vs objectui 共享 DrillDownConfig 全集。 3. 写 ChartDrillDownSchema(strictObject,surface/history/guidance),JSDoc 与 ReportSchema.drilldown 双向点名消歧。 4. 逐键验收测试 + 调和 chart.test.ts:464 的批 15 钉子。 5. liveness/dashboard.json 分类 + check:strictness-ledger 台账行。 6. changeset(minor,v17 pre 模式);objectui 侧把 (schema as any).drillDown 换成声明类型。 7. 合 origin/main(objectstack-ai#5154/objectstack-ai#5162 已入队)后重跑 check:strictness-ledger + check:generated。 ## 顺带发现(超范围,待立 issue) - widget.chartConfig 在 dashboard 面只有 showLegend 落地,其余 11 个已声明键全惰性; liveness/dashboard.json 的 chartConfig 行写 "chart-config bag forwarded",高估。 - objectui DrillDownConfig.view / .sort 无渲染器读取;ObjectChart 忽略 target:'navigate'。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * feat(spec,lint): declare the chart segment drill as ChartDrillDownSchema, on the tier that reads it (objectstack-ai#5022) `drillDown` drove a real capability with no protocol declaration behind it: objectui's ObjectChart read `(schema as any).drillDown` and really did open a filtered record drawer from it, while `grep drillDown packages/spec` found only a comment — the one objectstack-ai#3752 wrote prescribing the key as a migration target. That inverted Prime Directive objectstack-ai#10: delivered, never declared. Declared now, additively, at the one surface measured to read it. ## The shape is the six keys ObjectChart reads `{ enabled?, filter?, title?, target?: 'drawer' | 'dialog', columns?, maxRows? }`, each reverse-engineered from a read point: `isDrillEnabled` (absent = off, `{}` = on), `computeDrillFilter`, `resolveDrillTitle`, the `'dialog'` branch, the column map, the page size. objectui's own `DrillDownConfig` is wider because it is shared with the table / pivot / metric widgets, and copying that union would have promoted four keys a chart ignores — two of which NO widget reads (objectui#3354) — into protocol-declared capabilities. Each absent key is a `guidance` entry naming where it does apply, not a rename. ## The carrier is the react block, NOT chart config or the widget options bag Both candidates in the issue measured dead on the dashboard metadata path: `DashboardRenderer` never reads `chartConfig`, and `DatasetWidget` forwards exactly one key out of it (`showLegend`); `options.drillDown` is read only in `DashboardRenderer`'s legacy `isObjectProvider` branch, which a spec-legal v17 widget cannot reach, because `dataset` is required and `datasetBound` therefore discards that component schema unrendered. An ADR-0021 dataset-bound widget drills through the semantic layer and honours no drill config at all — which `content/docs/ui/dashboards.mdx` already said. So the declaration rides `react-blocks.ts`'s interaction overlay, deliberately not `ChartConfigSchema`, whose members a dashboard widget's `chartConfig` also parses. A member there would have been authorable, parse-clean and unread. The three places an author reaches for it now answer instead of shrugging: `chartConfig.drillDown` → the react prop; `widget.drillDown`/`widget.drilldown` → dashboard drill is AUTOMATIC, plus both configurable drills named; `report.drillDown` → back to the chart prop. ## Two-way disambiguation with ReportSchema.drilldown Same word, one letter apart, two capabilities. Edit distance actively gets this wrong — the spellings are distance 1, so a bare "did you mean" sends an author to the boolean slot where their config object fails a second time. Both gates name the TYPE difference, not just the spelling. ## Declared = enforced, in the same commit `.strict()` is a property of a parse, and nothing on the react surface called one. `validate-react-page-props.ts` now PARSES the schema against a static `drillDown={{…}}` literal (`react-chart-drilldown-invalid`) instead of re-deriving the rules the way `CHART_FUNCTIONS` does for `aggregate` beside it — the fix the strictness ledger's `chart.zod.ts` row has been waiting on, shown on one key. A value from React state is skipped: unresolvable is not wrong (ADR-0072 D1). ## Instruments - 批 15's pin (`chart.test.ts`) is RECONCILED, not relaxed: the ChartInteraction prescription still must not say a bare `drillDown`, now because that schema is reached from both tiers and cannot tell which, so the advice would be a dead end for half its readers. - 20 new assertions: per-key acceptance, `{}` enables, absent stays optional, unknown key rejected with the surface named, each non-chart key rejected with its reason, `target:'navigate'` prescribed (and a plain wrong value NOT given that text), the carrier verdict pinned in both directions, and the published react type string derived-checked against the schema shape. - 10 lint-gate tests; ledger row and site counts updated (`ui/` 198 → 199). Filed, not fixed here: objectstack-ai#5175 (`chartConfig` delivers 1 of 12 keys on the dashboard path; the liveness row's evidence overstates it) and objectui#3354 (`DrillDownConfig.view`/`.sort` read by nothing; `navigate` silently degrades). Fixes objectstack-ai#5022 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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。改了什么(4 文件)
packages/spec/src/ui/view.zod.tsViewFilterRuleSchema:strictObject(…)→z.object(…),只此一处 + 其 JSDocpackages/spec/src/ui/view-filter-rule-wire-id.test.tsdocs/audits/2026-07-unknown-key-strictness-ledger.mdui/行 + 表头 + 算术散文,从存活行重算.changeset/view-filter-rule-console-id-hotfix.mdpatch⛔ 为什么不顺手声明
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):修后:
Invalid input是 #5014 实测的那个压平:报错里连id两个字都没有。这就是这条 bug 能在main上活着没人发现的原因 —— 错误信息读不出病因。2. dogfood:真浏览器 + 真
/_consolebundlepnpm objectui:build出固定 pin(f5bc4c78)的真控制台 → showcase 起在自有端口(--ui --seed-admin,自有 DB)→ Playwright 真实登录 →/_console/apps/showcase_app/showcase_task→ 点工具栏Filter→Add filter。抓到的真实 PUT:conditions[0]正是本 issue 那个形状。把这份真 body 三变体回放到两个真实运行的 server(只改filter):FilterGroup对象)invalid_unioninvalid_union← #5159rule[],保留 UI 的idrule[],去掉id读法:两个缺陷叠在同一份 body 上。外层类型错(#5159,产出端)挡在前面;拆开之后,下一道就是本 PR 修的
id。②③ 的对照把变量锁死在id上 —— 本 PR 的效果就是把 ② 从 422 翻成通过,一分不多一分不少。另有一组不经浏览器、直打
PUT /api/v1/meta/view/:name的对照(扁平 personalization overlay,filter 行带crypto.randomUUID()):#5114 正文里「未跑真实 app 端到端」的缺口到此补上 —— 并且正是这一跑,把 #5159 挖了出来。
3. 仪器纪律 —— 断言两向都证过红
ViewFilterRuleSchemaid被丢弃 + 顶层 aux)check:strictness-ledgerEXIT=1check:strictness-ledgerEXIT=1pin 文件里那两条机理对照(顶层 aux 键随行 / 嵌套已关
emptyState仍拒)刻意跑在不带 filter 的 overlay 上 —— 所以它们在两种姿态下都绿。断言红了、对照没红,那个落差本身就是「.strip()不递归」这条发现。4. 门与套件(合并
origin/main之后重跑)台账合并 —— 第 11 例「两边都对、合并都错」,方向和前十例相反
与 批 17(#5068,把
component.zod.ts29 站点整行改判no gate)如期冲突。按既定纪律:取 main 整段 + 重贴本单编辑 + 逐条断言对方条目存活 + 从存活行重算。component.zod.ts行取 main(批 17 的no gate),view.zod.ts行取本单(批 18 文字的超集 + 控制台保存筛选条件会 422:ViewFilterRuleSchema拒绝 filter-builder 盖的id,而 wire 成员的.strip()救不到嵌套块 #5114)。PageComponent.properties是开放 record,ComponentPropsMap的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068、批 18 的ListView.sort回退段、widgetno-door 行、i18n5/6 split、ADR-0049 enforce-or-remove: ui/widget.zod.ts 全文件 + ui/i18n.zod.ts 五个形状实测无门(#4001 批 16 改判) #5055/#4001 战役的门测量 BFS 会误报「可达」:zod.describe()共享 def 对象,任意单属性 bridge 把无关形状连起来 #5056、「批 16 is the eighth instance」—— 全在。ui/76 strip of 198;29+6+9+2+7+5+4+4+4+3+1+1+1 = 76,其中 69 属两类 no-parse(38no door+ 31no gate),可授权半边 =view6 +app1 = 7 of 76。前十例都是关站点把这行推低;本单是开一个站点把它推高 —— 我的分支算
36 of 76(批 17 未落地的树),main 算6 of 75(本单未落地的树),合并是7 of 76,两边都没写过。机理已记进散文。⛔
content/docs/releases/零触碰;objectui 零改动(只读);os-regen 四步走完(merge → 取 main 生成物 → 整体重生成 → 断言兄弟条目存活),8 件生成物全绿。顺带发现(⛔ 未在本 PR 修)
filter(spec 声明的是ViewFilterRule[])—— 真浏览器实测 422,#5114 热修修不到 #5159(p1,已立案,未指派)—— 控制台运行时工具栏把整个FilterGroup对象存进filter,spec 声明的是ViewFilterRule[]。产出端(objectui)缺陷,按契约优先应在产出端折平(Studio 侧widgets.tsx:1727已有同款转换),⛔ 不要放宽ListViewSchema.filter。本 PR 合并后控制台保存筛选条件仍会 422,直到 控制台「筛选」把整个 FilterGroup 对象存进 view 的filter(spec 声明的是ViewFilterRule[])—— 真浏览器实测 422,#5114 热修修不到 #5159 落地。filter(spec 声明的是ViewFilterRule[])—— 真浏览器实测 422,#5114 热修修不到 #5159):FilterGroup.logic: 'or'今天没有落盘表示,折平时会被静默降级成and,修 控制台「筛选」把整个 FilterGroup 对象存进 view 的filter(spec 声明的是ViewFilterRule[])—— 真浏览器实测 422,#5114 热修修不到 #5159 时需一并判定。🤖 Generated with Claude Code
https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9