维护者速读
一年前定下的规矩是:一个表示时长的数字,单位要写在键名里,比如 timeoutMs 而不是光叫 timeout——这样 AI 或人照着写配置时,不会把 60000 毫秒当成 60000 秒。
2026-09-07 你批过一次(batch #65):当时提议「让检查器也去读源码注释(JSDoc)里的单位」,没有采纳,理由白纸黑字写着——「这一类到底有多少个,没数过」。
现在数过了。结论有点出人意料:
- 当初被否掉的那个改动,总共只够到 6 个键。小到不值得为它改规则。
- 但普查顺带挖出另一批至少 7 个:它们表示时长或时刻,任何地方都没写单位——注释里没有,描述里也没有。读不读 JSDoc 对它们毫无帮助,因为它们压根没有 JSDoc 单位可读。
- 还有一批 4 个是「时刻」(时间戳),它们该用仓库里已经存在、已经有 6 处在用的那个专用类型,却写成了普通数字。这一批不需要动任何规则,只是没跟上既有约定。
所以真正要你定的不是「要不要读 JSDoc」,而是:这件事的目标到底是哪一个。
我的推荐是 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 (#17635 → 484484041a). 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:
- 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.
- 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 类机械 23 / 语义 6 的边界。17 行的差全是非时间单位的间隔与年龄(
snapshotInterval = 每 N 个事件,reflectionInterval = 每 N 次交互,ageBuckets = 桶数)加 5 个退避倍数。B 类的边界同样悬在这个答案上。
- 要不要扩到所有带单位的数值,而不只是时长? 精确同形 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
维护者速读
一年前定下的规矩是:一个表示时长的数字,单位要写在键名里,比如
timeoutMs而不是光叫timeout——这样 AI 或人照着写配置时,不会把 60000 毫秒当成 60000 秒。2026-09-07 你批过一次(batch #65):当时提议「让检查器也去读源码注释(JSDoc)里的单位」,没有采纳,理由白纸黑字写着——「这一类到底有多少个,没数过」。
现在数过了。结论有点出人意料:
所以真正要你定的不是「要不要读 JSDoc」,而是:这件事的目标到底是哪一个。
我的推荐是 B,理由和回退在下面。请回一个字母:A / B / C / D。
Background
check:duration-unit-keysrefuses 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 (#17635 →484484041a). 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):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: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.z.number()instead ofEpochMsmaxMemoryshape — a size, not a duration)⇒ Option 1 as declined reaches 6 rows (B + C) and cannot reach D at all.
Two measured facts that bear on the choice:
metrics.zod.ts:224 ageBuckets(JSDoc "Number of age buckets",default(5)) is in it becauseageis a token. The rule's remedy is "move the unit into the key name", which is meaningless where there is no unit.plugin-registry.zod.ts:110 downloadsLastMonthhas JSDoc "Downloads in the last 30 days"; the gate'sPROSE_COUNTED_RE(:281) matches30 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
downloadsLastMonthstarts being refused as though it were a durationEpochMs业务含义直译
四维分析(从业务立场)
① 项目长远合理性。 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(比如是相对偏移),那一行退出本批、单独记录,⛔ 不为了凑数改类型。
packages/spec,#15642(扫描子树的范围问题)仍独立开着,别的 workspace 包里同形的键没有数过。裁后执行段
domain:spec实现卡:普查预测入队面packages/spec/scripts/check-duration-unit-keys.ts+ 6 处重命名的 ADR-0087 转换与墓碑;同卡必须处理downloadsLastMonth假阳性,⛔ 不得留给下一个人发现。domain:spec实现卡:4 行(外加 6 行同形)改用EpochMs;这是已发布键的类型收窄,按 clause ② 判定后决定是否需要席内契约复核。needs-user-decision子卡答「什么算时长」(A 类 23 vs 6 的边界),再谈实现。not_planned,在 spec:tenant.zod.tsidleTimeout / sessionTimeout publish a describe with no unit, while the JSDoc one line above says seconds #14519 上留一条评论把这次的数字记进去,免得第三次因为「没数过」而重开。下游问题 —— ⛔ 本卡不问,答完上面那个字母才谈
snapshotInterval= 每 N 个事件,reflectionInterval= 每 N 次交互,ageBuckets= 桶数)加 5 个退避倍数。B 类的边界同样悬在这个答案上。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
EpochMs)是这一轴最干净的一步;downloadsLastMonth),假阳性比漏判更伤信任。推荐: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 atfb29f62cee; every figure quoted here was re-verified by this seat against the tree, not relayed.Generated by Claude Code