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
@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 4, 2026 07:29
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 8064b07 Aug 4, 2026
25 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-5114-filter-rule-hotfix branch August 4, 2026 07:41
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>
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