Skip to content

ChartAggregateSchema 有活的承载键却没有任何 parse:react 页发布闸门手写重推了它的规则,未知键从不检查(#4001 批 15 的 no gate 首例) #5020

Closed

Description

@xuyushun441-sys

从 #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; 的短路保持一致。

参考

Activity

  1. self-assigned this
    on Aug 5, 2026
  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    认领:PM 循环第 5 轮(devx 车道)
    会话:session_01GX3sL71LFq8m2usg6VqTSE
    分支:claude/issue-5020-chart-aggregate-gate
    Worktree:objectstack-issue-5020
    域:domain:devx
    文件面:packages/lint/src/validate-react-page-props.ts + 其测试(既有文件,不新增规则注册)。⛔ 不触 packages/spec/src/ui/chart.zod.ts 本体(#4001 战役属地,本单只消费其导出)。

    前提已核:origin/main 该文件 :301 仍手写 CHART_FUNCTIONS、:356 仍手推 refinement;同文件 :288 已有 ChartDrillDownSchema.safeParse 同款先例 —— 修法有仓内锚,照该形状把 aggregate/groupBy 接上 ChartAggregateSchema/ChartGroupBySchema。NOT_STATIC 短路语义保持。


    Generated by Claude Code

  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    裁定(devx 车道 PM,session_01GX3sL71LFq8m2usg6VqTSE,2026-08-05):dev 的 needs_decision 停车完全正确 —— 它实测证伪了正文「未知键立刻有了闸门」的主张(两 schema 均 STRIP 姿态,仓内常驻 pin 自证),并找到了同类兄弟单 #5068 今日的维护者裁决。三问均落在该同日先例的射程内,按先例直接裁定(否决窗口,维护者可推翻):

    Q1 → A(两步走,#5068 方向 A 照抄):本单只做 packages/lint —— 接 ChartAggregateSchema.safeParse、删 CHART_FUNCTIONS 与手推 refinement、解包 invalid_union 保留具名 arm 消息;strictObject 收紧归 #4001 后续批,跟进单本轮已立(见下),过渡期缺口用 pin 测试 + 注释点名跟进单钉住。C(消费端复刻 strictObject)按防 AI 犯错轴否决 —— 契约知识进消费者正是 #4001 要删的形状。

    Q2 → A 类逐字执行 + groupBy 缺席单独 warning:schema、已发布 react-blocks 类型、渲染器三方一致的类(function 必填/枚举、field 字符串、aggregate 对象)按 error 执行,declared = enforced;groupBy 缺席这一类降 warning —— 它是唯一有活渲染路径(ObjectChart.tsx:640 的 || xAxisKey 兜底)兑现的形状,恰是 #5068 分级条款针对的「设闸打断能用的东西」情形。「无分组单值图是否受支持形状」(决定 schema 声明 optional 还是收紧渲染器)是 spec 面产品问题,写进跟进单交战役/维护者,本 PR 不拍。

    Q3 → A(三处记录随 parse PR 一起翻):chart.zod.ts 仅头部散文、chart.test.ts 仅 pin 注释(断言本体不动 —— STRIP 姿态未变,pin 应保持绿)、台账 :636/:849 行 no gate → authorable 并指向跟进单。账本自己的「三处一起改或都不改」纪律 + #5068 把行翻转计入 parse PR 完成定义,共同支持 A;schema 本体零改动,#4001 属地未破。⚠️ .counts.md 是生成物:若 gen:strictness-ledger 有 diff 按 os-regen 规程整体重生成,禁手改。

    同一认领续派新 dev 执行,派发词含全部三裁。


    Generated by Claude Code

  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    实施完成(续派 dev,session_01GX3sL71LFq8m2usg6VqTSE,分支 claude/issue-5020-chart-aggregate-gate):draft PR #5608。

    三裁全部按裁定落地,前提已对 origin/main 复核且无冲突(渲染器兑现 groupBy 缺席、react-blocks.ts:245 声明必填、两 schema 均 STRIP、zod 4 union 塌缩,四项均实测)。

    • R1 —— ChartAggregateSchema.safeParse() 接上,CHART_FUNCTIONS 与手推 count/field refinement 删除;invalid_union 解包,各 arm 具名消息进同一条 finding(spec: ChartAggregateSchema 与 ChartGroupBySchema object 分支转 strictObject —— #5020 parse 接通后的收紧半边(#4001 后续批) #5583 的 strict unrecognized_keys 会以同样方式塌缩,靠这段解包送达作者)。额外补了 zod 不给的 received 值回显(按 issue path 从输入取回,不在消费端复述契约)。
    • R2 —— function 必填/枚举、field 字符串、aggregate 对象、sum/avg/min/max 缺 field 一律 error;groupBy 缺席单独 warning 不阻断,分级按 issue 而非整个 aggregate({ function: 'median' } 同时得一条 error + 一条 warning)。
    • R3 —— chart.zod.ts 仅散文、chart.test.ts 仅 pin 注释(两条 STRIP 断言本体不动、保持绿)、台账 :636/:849 no gate → authorable;.counts.md 走 gen:strictness-ledger 整体重生成,no gate 31 → 29、authorable 11 → 13,正好这 2 个站点,未手改数字。

    ⚠️ 过渡期缺口按裁定钉住并做了判别性加固(不靠空结果蒙绿):pin 在未知键旁种一个只有 parse 报得出的违规,断言恰好 1 条来自 schema 措辞的 finding 且不含 Unrecognized key。反向验证方向先判后跑:预判「放回手写限枝 → 新 pin 红」,实跑 11 条红与预判一致;同时如实记录两条 STRIP 容忍断言的容忍那半在新旧代码下都绿(两版都不查未知键),没有把它算作本 PR 的成果。

    验证:lint 包 1335 passed / typecheck 绿、chart.test.ts 75 passed、spec check:generated 10/10、NUL 与控制字符自查干净、eslint 干净。无 out-of-scope 新发现需另立单。


    Generated by Claude Code

  5. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    验收(devx 车道 PM,session_01GX3sL71LFq8m2usg6VqTSE,2026-08-05):ACCEPT → PR #5608,转 ready 并挂 auto-merge。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions