Skip to content

[Decision] DatasetSelection is a published wire shape with no Zod schema, so four of its members have no door at any layer — where should DatasetSelectionSchema live, and who authors it? #17551

Description

@os-justin

维护者速读

我们的分析接口对外公布了一个「选数据集」的请求形状,一共 11 个字段。去年补的那道校验门只管住了其中 7 个,剩下 4 个(runtimeFilter、dateGranularity、compareTo、totals)在 TypeScript 里声明了、在对外接口清单里公布了、但运行时没有任何一层拦它。

这不是理论风险,已经有一张卡在记它的后果:客户端传 compareTo: { kind: 'nonsense' },服务端不报错,返回 200,并且悄悄给了一个「上一周期」的对比数字。看报表的人看不出这个数字是瞎编的。四个字段里这只是第一个。

三条路:

  • A 在 packages/spec 里把这 4 个字段的校验补齐,放在接口声明旁边,让那道门管住整个形状。—— 一处声明,一次收口。代价是要动 packages/spec 并重新生成产物。
  • B 不补,以后每撞上一次算一张缺陷卡,从已有的那张开始。—— 单张便宜,但那四个字段会长期停在「声明了却不兑现」的状态,每个用它的界面自己猜一套答案。
  • C 不动 packages/spec,在 packages/rest 里用现成零件拼一个。—— 不用改 spec,但等于给同一个对外形状写了第二份声明。

我荐 A。请回一个字母:A / B / C

os-decision-facets

  • ① 项目长远合理性:A 缩小特例 —— 一个对外形状一份声明,补的 4 个字段是已经公布过的文字的转写,不是新契约。C 明确扩大:同一个形状两份声明,两份就会漂。B 把「声明了不兑现」固化成常态。
  • ② 实际业务拉动:有,且已实测一例 —— #17550:shiftRange 只分支 previousYear,其余落到 previousPeriod 臂,于是 compareTo: { kind: 'nonsense' } 在 200 下悄悄返回一个上一周期对比。今天看分析报表的业务人员就会撞上,而且看不出来。四个字段里这是第一个。
  • ③ 防 AI 犯错:⭐ 这一棱是决定性的。北极星〈优先级〉4:「错的必须被响亮拒绝并给处方,永不静默落库」。B 恰恰保留一个「写错了照样 200、并且给出一个不同的答案」的面 —— 那就是静默落库的定义。AI 写元数据撞上它时,既不会被拒绝,也拿不到处方。⇒ B 直接违反第 4 条,A 正面兑现它(闭合枚举 + 响亮拒绝)。
  • ④ 创业阶段不扩散:A 不新增任何已声明的键,只是把已公布的 4 个成员写成可执行的形;C 新增一份同形状的声明(永久义务 ×2);B 留下 4 个永久未兑现的声明。⇒ A 最小,C 最大。

Prior rulings read: DatasetSelectionSchema,DatasetSelection,AnalyticsQuerySchema → 0 hits(除本卡外);ADR-0021 named by the card, ⛔ not re-read by this seat; thread: read to end(1 comment,5749446800)

推荐:A。自检行:「只看①选 A;②③④ 是否翻转:否 —— ③ 与 ① 同向且最强,②给出已实测的拉动,④同向。」
回退项:B,仅当您判断分析面近期整体要重做、现在补门是白补。
置信缺口两条,都据实写:

  1. 立卡席把 A 标成 Clause-②: yes(收窄接受集)。但 references/lanes/spec.md:21 原话是「收窄仍是契约面,不触条款②」。⇒ 这个自评可能偏严。⛔ 本席不代 spec 席改这个申报,也不让推荐依赖它 —— 无论条款②算不算命中,字母都是 A。
  2. 卡上点名的 Prime Directive 10 / 12 本席 ⛔ 没有重读,因此推荐不建立在它们之上;上面第 ③ 棱只用北极星原文,自身成立。

Governing text:docs/NORTH-STAR.md〈优先级〉4「元数据既要 AI 能写——错的必须被响亮拒绝并给处方,永不静默落库」·references/lanes/spec.md:21「放宽接受集或扩大公开面的卡,不论多小,即条款②;收窄仍是契约面,不触条款②。」

⬆️ 本决策卡面由分诊席于 2026-09-20T12:07Z 补齐,同笔答 pm:retriage。以下为原始卡文,⛔ 未改一字。


⛔ Filed by the domain:cli execution PM seat (#6024), session session_01DapQyvYrFb1MxSYe7BL2nt, landing point measured as packages/spec ⇒ routed domain:spec. Grading, type and whether this needs a maintainer are triage's. ⛔ Not claimed, ⛔ not dispatched, and ⛔ this seat is not choosing between the options below — it is out of its lane by landing point and it shapes a public contract.

Raised as an open question by the os-dev on #17058, which measured the gap while building that card's door and correctly declined to guess. The three options and the recommendation below are its analysis; I verified the measurements and am routing it.

The measurement

DatasetSelection is a TypeScript interface — packages/spec/src/contracts/analytics-service.ts:177-232, exported in the api-surface — and there is no Zod schema for it anywhere in the repo.

PR #17548 (card #17058) put a door on POST /api/v1/analytics/dataset/query, but only over the seven members DatasetSelection shares with AnalyticsQuery, parsed as AnalyticsQuerySchema.pick(…). That was deliberate and it is why that card graded Clause-②: no — the refusal set equals the published interface.

⇒ The other four members — runtimeFilter, dateGranularity, compareTo, totals — are declared in TypeScript, published in the api-surface, and enforced by nothing on the wire.

⚠️ Reusing the siblings' schema for the whole selection is ⛔ not available, and this was measured rather than assumed: AnalyticsQueryRequestSchema requires cube (a dataset selection carries none) and is .strict(), so a legal DatasetSelection fails it on cube plus all four members above. That would 400 every real dashboard widget.

The concrete consequence already on file

#17550 — shiftRange (dataset-executor.ts:568-580) branches only on previousYear and falls through to the previousPeriod arm, so compareTo: { kind: 'nonsense' } silently returns a previous-period comparison under a 200. That is one member of four; the same shape is available to the other three.

The three options, as the dev framed them

A — author DatasetSelectionSchema in packages/spec beside the interface (contracts/analytics-service.ts or api/analytics.zod.ts), and have the door parse the whole selection against it.
⇒ One declaration, Zod-First, closes the class.
⚠️ Costs a packages/spec change with generated-artifact regeneration, and it is a real acceptance-surface narrowing — Clause-②: yes — because the four members become refusable for the first time.

B — leave the four undoored and treat each concrete consequence as its own defect card, starting with #17550.
⇒ Cheap per card.
⚠️ It is the declared-not-enforced posture Prime Directive 10 names, and each face gets to invent its own answer.

C — assemble the missing members from spec-exported parts (FilterConditionSchema, DateGranularity, the widget compareTo strictObject) inside packages/rest.
⇒ Needs no spec change.
⛔ It is exactly the second declaration of a spec-owned wire shape Prime Directive 12 forbids — named, not inferred.

The dev's recommendation: A — 「the members are already declared in TypeScript and published in the api-surface, so the schema is a transcription of existing published text, not a new contract」, and C is barred by name while B leaves a wire surface where compareTo: { kind: 'nonsense' } silently returns a comparison under a 200.

What this seat adds, and what it deliberately does not

⭐ The 「transcription, not a new contract」 argument is the load-bearing one and it deserves testing rather than adoption: if the schema really is a transcription, then Clause-②: yes is arguable — the accept set on the wire narrows, but only onto text packages/spec already publishes. That is the same shape this round settled twice (「拉回已声明契约不触它」), and it is a judgment for the spec seat, which owns both the file and the clause-② review tier. ⛔ Not mine.

⚠️ Dedup is NOT MEASURED and I am saying so rather than implying a clean zero. /search/* is 403 on this session's egress. What I verified is the mechanism: the interface exists at the cited lines, carries no Zod schema, and #17548's door covers seven members and not these four. Whether an existing card already asks for DatasetSelectionSchema is unchecked — worth one query from a seat with a working search channel before this is dispatched.

Refs

#17058 / PR #17548 (the seven-member door, and why the siblings' schema could not be reused) · #17550 (the compareTo.kind fall-through — one instance of this class) · packages/spec/src/contracts/analytics-service.ts:177-232 · ADR-0021 (the semantic dataset layer this selection belongs to).

派发席位 · session_01DapQyvYrFb1MxSYe7BL2nt · R72 · 2026-09-10T21:55Z(读表) · 本评论来自 domain:cli 派发座位


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions