Skip to content

spec: the option-1 population is measured — 6 rows, and it cannot reach the ≥7 duration keys that name no unit anywhere. What is the goal? #18080

Description

@claude

维护者速读

一年前定下的规矩是:一个表示时长的数字,单位要写在键名里,比如 timeoutMs 而不是光叫 timeout——这样 AI 或人照着写配置时,不会把 60000 毫秒当成 60000 秒。

2026-09-07 你批过一次(batch #65):当时提议「让检查器也去读源码注释(JSDoc)里的单位」,没有采纳,理由白纸黑字写着——「这一类到底有多少个,没数过」

现在数过了。结论有点出人意料:

  • 当初被否掉的那个改动,总共只够到 6 个键。小到不值得为它改规则。
  • 但普查顺带挖出另一批至少 7 个:它们表示时长或时刻,任何地方都没写单位——注释里没有,描述里也没有。读不读 JSDoc 对它们毫无帮助,因为它们压根没有 JSDoc 单位可读。
  • 还有一批 4 个是「时刻」(时间戳),它们该用仓库里已经存在、已经有 6 处在用的那个专用类型,却写成了普通数字。这一批不需要动任何规则,只是没跟上既有约定。

所以真正要你定的不是「要不要读 JSDoc」,而是:这件事的目标到底是哪一个。

  • A — 就按当初那个提议做:让 JSDoc 算一个单位来源。够到 6 个键。
  • B — 只做「时刻」那一批:让它们改用已有的专用类型。不改任何规则,4 个键(外加 6 个同形的)。
  • C — 改的是「哪些键进入检查范围」本身,让「哪儿都没写单位」的时长也能被点名。改动最大,但只有它够得到那至少 7 个
  • D — 什么都不做,batch Add comprehensive test coverage for ObjectStack spec protocols - 100% coverage achieved #65 维持原判。现在你知道被否掉的那个东西是 6 个键。

我的推荐是 B,理由和回退在下面。请回一个字母:A / B / C / D。


Background

check:duration-unit-keys refuses a duration-shaped numeric key that does not carry its unit in the key name. The #15939 epic closed on 2026-09-13 having remediated 21 such rows and landed the gate widening (#17635484484041a). This card is the follow-on that epic deliberately held back.

Governing text — quoted, ⛔ not paraphrased

Decision batch #65, 2026-09-07, maintainer 「同意」 (#15939 comment 5564447683):

the gate does not start reading JSDoc as a unit channel (option 1 not adopted; its population is unmeasured)

Keys with no unit in either channel stay "listed, not judged" (the #14519 shape) — unchanged.

And the one prior classification of a member of that shape, packages/spec/src/migrations/entries/semantic/18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts:63-65:

The sibling shutdownTimeout on HotReloadConfig is deliberately NOT renamed with them: its JSDoc reads "Graceful shutdown timeout" and names no unit anywhere, so it is the #14519 unit-nowhere shape the #14478 gate leaves outside its verdict, not part of this row set.

⚠️ That sentence classifies one key out of one PR's row set. It does ⛔ not settle the #14519 question, and this card is not overturning it.

Protocol declaration

No protocol change is proposed by this card. Options A and C would change gate behaviour and therefore need their own implementation cards; option B changes no gate at all. ⛔ Nothing here is adopted by filing.

The measured population — the precondition batch #65 named

Census #18078, report-only, at origin/main @ fb29f62cee. Re-check: git fetch origin main && cd packages/spec && tsx scripts/check-duration-unit-keys.ts --list.

class count reachable by option 1?
A unit nowhere, inside the census 23 mechanical → 6 genuine durations no — no JSDoc unit to read
B duration, unit in JSDoc only, outside the census 2 yes
C instant as bare z.number() instead of EpochMs 4 yes
D duration/instant, no unit in any channel and outside the census ≥ 7 no
non-member scoping (the maxMemory shape — a size, not a duration) 6 exact / 58 wider n/a

Option 1 as declined reaches 6 rows (B + C) and cannot reach D at all.

Two measured facts that bear on the choice:

  1. A's 23 is not a population. Only 6 are genuine durations; the other 17 are dimensionless — counts, backoff multipliers, one instant — matching only because the predicate is a 25-token name list. metrics.zod.ts:224 ageBuckets (JSDoc "Number of age buckets", default(5)) is in it because age is a token. The rule's remedy is "move the unit into the key name", which is meaningless where there is no unit.
  2. Adopting A brings a known false positive live. plugin-registry.zod.ts:110 downloadsLastMonth has JSDoc "Downloads in the last 30 days"; the gate's PROSE_COUNTED_RE (:281) matches 30 days. Today the row never enters the census so the misread is inert. On the day JSDoc becomes an admission channel, the gate starts refusing a download count.

Options × real cost

option what happens cost a user or author can feel
A JSDoc becomes a census-admission channel 2 durations + 4 instants become judgeable a gate change plus 6 renames; and downloadsLastMonth starts being refused as though it were a duration
B instants only → EpochMs 4 rows (+6 more of the same shape) adopt an existing convention ⛔ no gate change, no rule change, no migration prescription; it is drift from a live convention with 6 declared users
C widen census admission itself a unit-nowhere duration becomes listable regardless of its name the largest change, and the only one that reaches the ≥7 in class D; it changes what the census is
D do nothing batch #65 stands, now with a number the ≥7 stay invisible; a reader still cannot tell 60000 ms from 60000 s on those keys

业务含义直译

  • A = 「把注释也当成说明书的一部分」。听起来合理,但实测只影响 6 个键,而且会让一个下载计数被当成时长退回——为很小的收益引入一个新的误判面
  • B = 「本来就有的规矩,把漏网的几个补上」。不新增任何义务,只是让既有约定一致。
  • C = 「把体检的名单本身扩大」。这是唯一真正堵住那个洞的做法,也是唯一需要重新想清楚「什么算时长」的做法。
  • D = 「维持原判」。现在这个判断有数字支撑了,不再是因为不知道有多少而搁置。

四维分析(从业务立场)

① 项目长远合理性。 B 缩小特例:它让一个已经存在、已经有 6 处在用的约定被一致地遵守,不新增任何契约表面。A 扩大特例——它让「注释」成为第三个有语义的说明书通道,而这个通道的内容不进入发布产物,读者在参考页上看不到它,于是产生「代码里写了、文档里没有」的长期分裂。C 是三者里唯一减少增生的:它把判断依据从一张 25 个词根的名字表(一个必然长期漂移的清单)换成「有没有单位」这个语义问题。

② 实际业务拉动。 今天真正会撞上的人:拿着参考页照抄配置的作者与 AI。B 的 4+6 行是时间戳,写错的后果是把毫秒当秒传进去,静默地差 1000 倍。A 的 2 行时长同理但只有 2 行。D 类那 ≥7 行是拉动最大的一批——恰恰是三个选项里两个够不到的那批。⛔ 没有任何一类有已发生的事故记录,所以这四项都不是救火。

③ 防 AI 犯错。 这一轴决定推荐。⚠️ 出错时谁看见什么:一个 AI 照着参考页写 timeout: 60000,如果单位只写在源码注释里,参考页上什么都没有,它没有任何线索,写错了也不会被拒绝——静默的 1000 倍偏差,直到运行时行为不对才被发现。这正是 ③ 说的「响亮拒绝优于静默容忍」被违反的形态。B 把这批键换成一个闭合的类型(EpochMs),等于把自由结构换成枚举,是这一轴上最干净的一步。A 只在 2 个时长上做同样的事,但同时引入一个新的静默错误方向:downloadsLastMonth 会被响亮拒绝,而它是对的——假阳性教作者不信任这道门,比漏判更伤。

④ 创业阶段不扩散。 B 不新增任何已声明的键与永久义务,它只是让既有的用起来。A 新增一条规则分支加 6 个重命名的迁移义务,换 6 行。C 的义务最大,但它换来的是唯一一个不会随名字表漂移而失效的判据——如果只做一件长期的事,C 是那件;如果现在只做一件便宜的事,B 是那件。

推荐 + 回退 + 置信缺口

推荐 B,并把 A 与 C 分开处理:B 不改规则、有现成约定、覆盖静默偏差最严重的一批;A 的 2 行时长可以随后单独做,但要连带处理 downloadsLastMonth 这个会被激活的假阳性;C 应当是它自己的卡,因为它改的是普查的定义而不是读散文的方式。

回退:B 若在实施中发现某个时间戳其实不是 wall-clock(比如是相对偏移),那一行退出本批、单独记录,⛔ 不为了凑数改类型。

⚠️ 置信缺口——本分析看不见的:(1) D 类的 ≥7 是下界不是计数——普查明说 "at least 7",要数准需要先答「什么算时长」,而那正是本卡的下游问题;(2) 没有任何事故或用户报告数据,四类的排序完全基于「读者能否在发布面上看到单位」的推理,而不是基于谁真的写错过;(3) 本分析只看 packages/spec,#15642(扫描子树的范围问题)仍独立开着,别的 workspace 包里同形的键没有数过

裁后执行段

  • A ⇒ 一张 domain:spec 实现卡:普查预测入队面 packages/spec/scripts/check-duration-unit-keys.ts + 6 处重命名的 ADR-0087 转换与墓碑;同卡必须处理 downloadsLastMonth 假阳性,⛔ 不得留给下一个人发现。
  • B ⇒ 一张 domain:spec 实现卡:4 行(外加 6 行同形)改用 EpochMs;这是已发布键的类型收窄,按 clause ② 判定后决定是否需要席内契约复核。
  • C ⇒ 先一张 needs-user-decision 子卡答「什么算时长」(A 类 23 vs 6 的边界),再谈实现。
  • D ⇒ 本卡关 not_planned,在 spec: tenant.zod.ts idleTimeout / sessionTimeout publish a describe with no unit, while the JSDoc one line above says seconds #14519 上留一条评论把这次的数字记进去,免得第三次因为「没数过」而重开。

下游问题 —— ⛔ 本卡不问,答完上面那个字母才谈

  1. 什么算「一个时长」? A 类机械 23 / 语义 6 的边界。17 行的差全是非时间单位的间隔与年龄(snapshotInterval = 每 N 个事件,reflectionInterval = 每 N 次交互,ageBuckets = 桶数)加 5 个退避倍数。B 类的边界同样悬在这个答案上。
  2. 要不要扩到所有带单位的数值,而不只是时长? 精确同形 6 行(maxMemory 那种),宽泛同形约 57–58 行(字节、像素、百分比)。

相关单与 PR

#15939(已关,母单与 Ruling A)· #18078(已关,本卡的普查)· #14519(unit-nowhere 形状)· #15676(ruling B,时刻形状)· #18075(不同的洞:经 keyUnits 进普查却逃过判决的 2 行)· #15642(扫描子树,独立)· #17635 / #484484041a(闸门加宽)· batch #65 台账在 #12708


os-decision-facets

  • ① 项目长远合理性:B 让一个已有 6 处在用的约定被一致遵守,⛔ 不新增契约表面;A 让源码注释成为第三个说明书通道(而它不进发布产物),扩大分裂;C 是唯一把判据从一张会漂移的名字表换成语义问题的选项。
  • ② 实际业务拉动:今天撞上的是照着参考页写配置的作者与 AI;拉动最大的是 D 类那 ≥7 行,而 A 与 B 都够不到它;⛔ 四类均无已发生事故记录,都不是救火。
  • ③ 防 AI 犯错:单位只写在源码注释里时,参考页上什么线索都没有,写错静默地差 1000 倍(毫秒/秒);B 把这批换成闭合类型(EpochMs)是这一轴最干净的一步;⚠️ A 会让一个下载计数被响亮误拒(downloadsLastMonth),假阳性比漏判更伤信任。
  • ④ 创业阶段不扩散:B 零新增永久义务;A 用一条新规则分支加 6 个迁移义务换 6 行;C 义务最大但换来唯一不随名字表漂移的判据。

推荐:B(只做时刻那一批,不改规则)。选项:A 采纳 option 1 / B 只做时刻 / C 扩大普查本身 / D 维持 batch #65

置信缺口:D 类的 ≥7 是下界不是计数(要数准需先答「什么算时长」);无任何事故或用户报告数据,排序基于「发布面上看不看得见单位」的推理;只覆盖 packages/spec,别的 workspace 包同形键未数(#15642 独立开着)。

Filed by the epic PM for #15939, session_015c5G6TmpMKgnusmTpD7Ntt, 2026-09-13T23:15Z. Population from census #18078 at fb29f62cee; every figure quoted here was re-verified by this seat against the tree, not relayed.


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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions