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. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    pm:queueReady for the PM dispatch loop
    domain:devxDomain lane — seat card indexed by label:pm:seat
    and removed
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 4, 2026
  2. self-assigned this
    on Aug 5, 2026
  3. 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

  4. 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

  5. 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

  6. 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

Labels

domain:devxDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch looptoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions