Filed by the director seat (objectstack #12708, summon #23, session_01WCEaPsmKY4UyoivKkkaUHt) executing decision batch #134 item 1 — maintainer 「决裁批 #134 同意」 to the recommended A on #18115 (declared shape: closed duration types + unit-in-name; the 25-token name list retires from judge to detector), 2026-09-14T03:44Z. This is step ① of three; #18114 (instants → EpochMs) is independent and already queued. domain:spec by landing site; p2; triage may re-rank.
The ruling this card executes (verbatim from #18115, batch #134 item 1)
A 声明式:引入与 EpochMs 同款的闭合时长类型(DurationMs / DurationSeconds,单位集合由 6 个真时长行决定);体检只认「用了时长类型,或键名带单位」的键;17 个无单位数自然出列,6+2+≥2 个真时长各取类型或改名;名字表退为提示。
What to build
Closed duration types beside EpochMs in packages/spec/src/shared/epoch.zod.ts (or a sibling duration.zod.ts re-exported the same way — the dev chooses; both must be reachable from the package entry exactly as EpochMs is):
DurationMs — z.number().int().nonnegative() (or the least-surprising refinement the six genuine rows tolerate; measure their defaults: shutdownTimeout .default(30000), updateAge .default(60*60*24)), .describe('Duration in milliseconds').
DurationSeconds — same shape, seconds.
- ⛔ No speculative third unit:
DurationMinutes/Hours/Days are added only when a real row in step ③ needs one, in that step's PR. The unit set is derived from the conversion population, ⛔ not declared ahead of it.
Mirror EpochMs's discipline verbatim: author state and parsed state coincide (no default, no transform), so there is deliberately no *Parsed synonym; the docblock states the naming convention the gate will read (a Duration* type is one of the two declaration channels — the other is a unit token in the key name).
Acceptance
Clause-②: yes — new exported symbols on the published spec entry; changeset minor.
- A pin that both types accept the six genuine rows' current defaults and refuse a non-integer / negative literal (loud at authoring,
ZodError), with the EpochMs isomorphism pin pattern copied (input type = output type).
tsx scripts/check-duration-unit-keys.ts --self-test still exit 0 (this step changes no gate behaviour; the gate learns to READ the new types in step ②).
- Nothing else moves: ⛔ no key converted here (step ③), ⛔ no gate change here (step ②).
Fences
- ⛔
content/docs/releases/** untouched; ⛔ no key renames; ⛔ no admission-rule change.
Re-check before claiming
git fetch origin main
git grep -n -E 'export const (DurationMs|DurationSeconds)' origin/main -- packages/spec/src # expected 0 today
git grep -n -E 'export const EpochMs' origin/main -- packages/spec/src/shared/epoch.zod.ts # the precedent, expected 1
Downstream (filed alongside, pm:blocked on this card)
Step ② — the gate's admission rule; step ③ — conversion of the genuine duration rows. Both carry Blocked-by: this card; the unlock scan returns them to pm:queue when this lands on origin/main (same repo ⇒ merge is the criterion, no pin lag).
Refs
#18115 (ruling A, batch #134 item 1) · #18080 (ruling C, batch #133 item 4) · #18078 (census) · #18114 (instants tranche, independent) · packages/spec/src/shared/epoch.zod.ts
Dedup terms: DurationMs, DurationSeconds, closed duration type, epoch.zod.ts sibling, unit-in-key declared shape
Generated by Claude Code
Filed by the director seat (objectstack #12708, summon #23,
session_01WCEaPsmKY4UyoivKkkaUHt) executing decision batch #134 item 1 — maintainer 「决裁批 #134 同意」 to the recommended A on #18115 (declared shape: closed duration types + unit-in-name; the 25-token name list retires from judge to detector), 2026-09-14T03:44Z. This is step ① of three; #18114 (instants →EpochMs) is independent and already queued.domain:specby landing site; p2; triage may re-rank.The ruling this card executes (verbatim from #18115, batch #134 item 1)
What to build
Closed duration types beside
EpochMsinpackages/spec/src/shared/epoch.zod.ts(or a siblingduration.zod.tsre-exported the same way — the dev chooses; both must be reachable from the package entry exactly asEpochMsis):DurationMs—z.number().int().nonnegative()(or the least-surprising refinement the six genuine rows tolerate; measure their defaults:shutdownTimeout .default(30000),updateAge .default(60*60*24)),.describe('Duration in milliseconds').DurationSeconds— same shape, seconds.DurationMinutes/Hours/Daysare added only when a real row in step ③ needs one, in that step's PR. The unit set is derived from the conversion population, ⛔ not declared ahead of it.Mirror
EpochMs's discipline verbatim: author state and parsed state coincide (no default, no transform), so there is deliberately no*Parsedsynonym; the docblock states the naming convention the gate will read (aDuration*type is one of the two declaration channels — the other is a unit token in the key name).Acceptance
Clause-②: yes— new exported symbols on the published spec entry; changesetminor.ZodError), with theEpochMsisomorphism pin pattern copied (input type = output type).tsx scripts/check-duration-unit-keys.ts --self-teststill exit 0 (this step changes no gate behaviour; the gate learns to READ the new types in step ②).Fences
content/docs/releases/**untouched; ⛔ no key renames; ⛔ no admission-rule change.Re-check before claiming
Downstream (filed alongside,
pm:blockedon this card)Step ② — the gate's admission rule; step ③ — conversion of the genuine duration rows. Both carry
Blocked-by:this card; the unlock scan returns them topm:queuewhen this lands onorigin/main(same repo ⇒ merge is the criterion, no pin lag).Refs
#18115 (ruling A, batch #134 item 1) · #18080 (ruling C, batch #133 item 4) · #18078 (census) · #18114 (instants tranche, independent) ·
packages/spec/src/shared/epoch.zod.tsDedup terms:
DurationMs,DurationSeconds,closed duration type,epoch.zod.ts sibling,unit-in-key declared shapeGenerated by Claude Code