维护者速读
我们的分析接口对外公布了一个「选数据集」的请求形状,一共 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,仅当您判断分析面近期整体要重做、现在补门是白补。
置信缺口两条,都据实写:
- 立卡席把 A 标成
Clause-②: yes(收窄接受集)。但 references/lanes/spec.md:21 原话是「收窄仍是契约面,不触条款②」。⇒ 这个自评可能偏严。⛔ 本席不代 spec 席改这个申报,也不让推荐依赖它 —— 无论条款②算不算命中,字母都是 A。
- 卡上点名的 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
维护者速读
我们的分析接口对外公布了一个「选数据集」的请求形状,一共 11 个字段。去年补的那道校验门只管住了其中 7 个,剩下 4 个(
runtimeFilter、dateGranularity、compareTo、totals)在 TypeScript 里声明了、在对外接口清单里公布了、但运行时没有任何一层拦它。这不是理论风险,已经有一张卡在记它的后果:客户端传
compareTo: { kind: 'nonsense' },服务端不报错,返回 200,并且悄悄给了一个「上一周期」的对比数字。看报表的人看不出这个数字是瞎编的。四个字段里这只是第一个。三条路:
packages/spec里把这 4 个字段的校验补齐,放在接口声明旁边,让那道门管住整个形状。—— 一处声明,一次收口。代价是要动packages/spec并重新生成产物。packages/spec,在packages/rest里用现成零件拼一个。—— 不用改 spec,但等于给同一个对外形状写了第二份声明。我荐 A。请回一个字母:A / B / C
os-decision-facets
#17550:shiftRange只分支previousYear,其余落到previousPeriod臂,于是compareTo: { kind: 'nonsense' }在 200 下悄悄返回一个上一周期对比。今天看分析报表的业务人员就会撞上,而且看不出来。四个字段里这是第一个。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,仅当您判断分析面近期整体要重做、现在补门是白补。
置信缺口两条,都据实写:
Clause-②: yes(收窄接受集)。但references/lanes/spec.md:21原话是「收窄仍是契约面,不触条款②」。⇒ 这个自评可能偏严。⛔ 本席不代 spec 席改这个申报,也不让推荐依赖它 —— 无论条款②算不算命中,字母都是 A。Governing text:
docs/NORTH-STAR.md〈优先级〉4「元数据既要 AI 能写——错的必须被响亮拒绝并给处方,永不静默落库」·references/lanes/spec.md:21「放宽接受集或扩大公开面的卡,不论多小,即条款②;收窄仍是契约面,不触条款②。」⬆️ 本决策卡面由分诊席于 2026-09-20T12:07Z 补齐,同笔答
pm:retriage。以下为原始卡文,⛔ 未改一字。⛔ Filed by the
domain:cliexecution PM seat (#6024), sessionsession_01DapQyvYrFb1MxSYe7BL2nt, landing point measured aspackages/spec⇒ routeddomain: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-devon #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
DatasetSelectionis 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 membersDatasetSelectionshares withAnalyticsQuery, parsed asAnalyticsQuerySchema.pick(…). That was deliberate and it is why that card gradedClause-②: 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.AnalyticsQueryRequestSchemarequirescube(a dataset selection carries none) and is.strict(), so a legalDatasetSelectionfails it oncubeplus 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 onpreviousYearand falls through to thepreviousPeriodarm, socompareTo: { 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
⚠️ Costs a
DatasetSelectionSchemainpackages/specbeside the interface (contracts/analytics-service.tsorapi/analytics.zod.ts), and have the door parse the whole selection against it.⇒ One declaration, Zod-First, closes the class.
packages/specchange 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.
⚠️ It is the declared-not-enforced posture Prime Directive 10 names, and each face gets to invent its own answer.
⇒ Cheap per card.
C — assemble the missing members from spec-exported parts (
FilterConditionSchema,DateGranularity, the widgetcompareTostrictObject) insidepackages/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-②: yesis arguable — the accept set on the wire narrows, but only onto textpackages/specalready 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./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 forDatasetSelectionSchemais 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.kindfall-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