从 #4001 批 15 的门测量中分离出来,未指派。批 15 已按测量把这两个站点留开并三处留痕,本 issue 是它指向的那件事。
测量结果(2026-08-03/04,均带对照)
packages/spec/src/ui/chart.zod.ts 的 7 个 object 站点里,5 个从 dashboard / report 两个 metadata-type root 可达(DashboardWidget.chartConfig、ReportChartSchema),已在批 15 收紧。剩下 2 个 —— ChartAggregateSchema 和 ChartGroupBySchema 的 object 分支 —— 情况不同,而且不是「死槽位」:
| 测量 |
结果 |
| 承载键 |
✅ 活的。aggregate 是 react 层 < ObjectChart objectName aggregate={…} > 的真实可授权 prop(ADR-0081),发布在生成的 react-blocks 契约里,objectui 的 ObjectChart 读 schema.aggregate 去跑查询 |
从 24 个 metadata-type root + ObjectStackSchema 做 BFS |
❌ UNREACHABLE(同一次运行里,上述 5 个是 root-graph,批 13 的 no-door 形状是 UNREACHABLE —— 双向对照都成立) |
三个仓库里的 .parse() / .safeParse() |
❌ 除本文件单测外没有 |
问题
唯一实际审查作者所写 aggregate 的东西是 react 页发布 lint(packages/lint/src/validate-react-page-props.ts),它手写重推了这个 schema 的规则:CHART_FUNCTIONS 复制了函数词表,fn !== 'count' && !field 复制了那条 refinement,结果列命名另算一份。它从不检查未知键。
react-blocks.ts 里这个 prop 是一条手写的类型字符串;旁边的 Zod schema 不是契约的生成来源。
所以今天:
< ObjectChart objectName="invoice" aggregate={{ function: 'sum', field: 'total', groupby: 'status' }} / >
// ^^^^^^^ 静默丢弃
groupby(而非 groupBy)被剥掉 → 聚合退化成无分组、单行返回 → 图表画出坐标轴和一个点,build/validate 全绿。dateGranularty 同理:日期不分桶,趋势线变成一根。这正是 #4001 的原始失效模式,只是发生在 react 层。
为什么批 15 没有顺手收紧
.strict() 是 parse 的属性,这里没有 parse。收紧只会花掉一次 v17 破坏性变更让文件看起来完成,并留下账本自己警告过的东西 —— 「一个被精确校验的死槽位是更有说服力的谎言」(#4583)。而且这里比批 13 的情形更需要小心:这套词汇是活的,所以正确动作不是 ADR-0049 退役,而是把闸门接上。
批 15 因此在账本的分类规则表里加了第四类 no gate,与批 13 的 no door 并列 —— 两者都「没有 parse」,但处方相反:
|
承载键 |
parse |
正确的下一步 |
no door(批 13) |
无 |
无 |
ADR-0049 enforce-or-remove |
no gate(批 15) |
活的 |
无 |
接上 parse;退役会打断一个能用的功能 |
建议(contract-first)
让 validate-react-page-props.ts 用 ChartAggregateSchema.safeParse() 判定 aggregate,而不是重推它的规则:
- 未知键立刻有了闸门,且错误信息可以走本战役的
strictObject 路子(点名 + 改名建议);
CHART_FUNCTIONS、count/field refinement 两份重复实现收敛成一份 —— 目前它们已经能各自漂移;
- 之后这两个站点就是普通的
authorable,收紧是一行改动。
需要先确认的一点:react 页 prop 是静态字面量抽取的结果,NOT_STATIC 的值不能判。所以是「能静态判定时才 parse」,与现有 if (aggregate === undefined || aggregate === NOT_STATIC) return; 的短路保持一致。
参考
从 #4001 批 15 的门测量中分离出来,未指派。批 15 已按测量把这两个站点留开并三处留痕,本 issue 是它指向的那件事。
测量结果(2026-08-03/04,均带对照)
packages/spec/src/ui/chart.zod.ts的 7 个 object 站点里,5 个从dashboard/report两个 metadata-type root 可达(DashboardWidget.chartConfig、ReportChartSchema),已在批 15 收紧。剩下 2 个 ——ChartAggregateSchema和ChartGroupBySchema的 object 分支 —— 情况不同,而且不是「死槽位」:aggregate是 react 层< ObjectChart objectName aggregate={…} >的真实可授权 prop(ADR-0081),发布在生成的 react-blocks 契约里,objectui 的ObjectChart读schema.aggregate去跑查询ObjectStackSchema做 BFSUNREACHABLE(同一次运行里,上述 5 个是root-graph,批 13 的 no-door 形状是UNREACHABLE—— 双向对照都成立).parse()/.safeParse()问题
唯一实际审查作者所写
aggregate的东西是 react 页发布 lint(packages/lint/src/validate-react-page-props.ts),它手写重推了这个 schema 的规则:CHART_FUNCTIONS复制了函数词表,fn !== 'count' && !field复制了那条 refinement,结果列命名另算一份。它从不检查未知键。react-blocks.ts里这个 prop 是一条手写的类型字符串;旁边的 Zod schema 不是契约的生成来源。所以今天:
groupby(而非groupBy)被剥掉 → 聚合退化成无分组、单行返回 → 图表画出坐标轴和一个点,build/validate全绿。dateGranularty同理:日期不分桶,趋势线变成一根。这正是 #4001 的原始失效模式,只是发生在 react 层。为什么批 15 没有顺手收紧
.strict()是 parse 的属性,这里没有 parse。收紧只会花掉一次 v17 破坏性变更让文件看起来完成,并留下账本自己警告过的东西 —— 「一个被精确校验的死槽位是更有说服力的谎言」(#4583)。而且这里比批 13 的情形更需要小心:这套词汇是活的,所以正确动作不是 ADR-0049 退役,而是把闸门接上。批 15 因此在账本的分类规则表里加了第四类
no gate,与批 13 的no door并列 —— 两者都「没有 parse」,但处方相反:no door(批 13)no gate(批 15)建议(contract-first)
让
validate-react-page-props.ts用ChartAggregateSchema.safeParse()判定aggregate,而不是重推它的规则:strictObject路子(点名 + 改名建议);CHART_FUNCTIONS、count/field refinement 两份重复实现收敛成一份 —— 目前它们已经能各自漂移;authorable,收紧是一行改动。需要先确认的一点:react 页 prop 是静态字面量抽取的结果,
NOT_STATIC的值不能判。所以是「能静态判定时才 parse」,与现有if (aggregate === undefined || aggregate === NOT_STATIC) return;的短路保持一致。参考
ui/行 +chart.zod.ts头部注释 +chart.test.ts的常驻 BFS 断言 —— 一旦有人给aggregate加上 metadata 承载键,那条断言会变红并把人指回这里)no door裁决)、datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583(精确校验的死槽位)