Blocked-by: #18669
⏱️ PM 改写本行(巡检 H28 的处方,2026-09-17T14:02Z;原行 Blocked-by: #18122 留在编辑历史里)。 #18122 已于 2026-09-15 关闭,本卡等的早已不是它:步骤③的十行里八行已随 PR #18657 落地(c049e74679 单亲),余下两行等的是决策卡 #18669 的裁定。⇒ 按 H28「把正文那行改写成点名活着的那个阻塞源」,让下一个读者(人或巡检)在正文这个正典位置读到当前的等待。
⚠️ 本阻塞按构造是无限期的(巡检 H26):解锁谓词是「Blocked-by: 目标关闭」,而 #18669 带 needs-user-decision —— 那是一张开着的时候才待在那个状态的卡。⇒ 释放只能来自目标自身状态的改变(裁定被答),⛔ 不会有任何机制自动放行,得有人想要它。⭐ 裁 A(改名)之后还有第二道:两条 ADR-0087 转换层条目落 packages/spec/src/migrations/registry.ts,该文件由席 1 的 #17534 持有 —— 那一张能关,所以那半程是有机制的。
ℹ️ 巡检 H52 的处置记录在此,免得下一轮重问:终报的三个 open_questions 已在 5714587850 逐条答 A;而执行裁决时长出的两条残留按 H52 的 RESIDUAL 分支另立 #18669,⛔ 未在本卡重挂 needs-user-decision。
Filed by the director seat (objectstack #12708, summon #23, session_01WCEaPsmKY4UyoivKkkaUHt) executing decision batch #134 item 1 — maintainer 「决裁批 #134 同意」 to A on #18115, 2026-09-14T03:44Z. Step ③ of three: the genuine duration rows adopt a declaration. domain:spec; p2; pm:blocked until step ① is on origin/main. Foldable by file family (five-gate fold: same defect shape, same fix, all rows ruled, each row independently checkable, exclusions named below).
The population — census #18078 @ fb29f62cee; re-verify each row on today's origin/main (lines drift, key + file is the identity)
Genuine durations, unit in no channel (class A-true, 6):
| file:line |
key |
reading |
likely unit (dev measures the producer, ⛔ not this table) |
packages/spec/src/api/contract.zod.ts:311 |
duration |
BaseResponseSchema.meta.duration, beside a string timestamp, no describe/JSDoc |
ms (measure the REST server's writer) |
packages/spec/src/data/field-value.zod.ts:424 |
duration |
FileValueSchema, media length |
seconds? (measure the upload/metadata writer) |
packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts:413 |
shutdownTimeout |
.default(30000), "Graceful shutdown timeout" |
ms |
packages/spec/src/kernel/plugin-security-advanced.zod.ts:623 |
maxAge |
CORS Access-Control-Max-Age mirror |
seconds by RFC — consider externalVocabulary like its sibling expiresIn |
packages/spec/src/system/auth-config.zod.ts:563 |
updateAge |
.default(60 * 60 * 24), "Session update frequency"; sibling expiresIn carries externalVocabulary: 'better-auth session' |
seconds (better-auth) — same externalVocabulary route as its sibling is legitimate |
packages/spec/src/system/metrics.zod.ts:412 |
slideInterval |
sibling durationSeconds in the same window object |
seconds |
Unit only in JSDoc (class B, 2): packages/spec/src/kernel/plugin-versioning.zod.ts:191 estimatedMigrationTime · packages/spec/src/system/metrics.zod.ts:847 resolution (sibling afterSeconds).
Unit nowhere, outside the census (class D durations, ≥2): packages/spec/src/system/metadata-persistence.zod.ts:259 loadTime, :287 saveTime — bare z.number().optional(), no describe, no JSDoc, beside size and etag.
Plus any genuine duration step ②'s widened detector surfaces (that card routes them here, ⛔ does not convert them).
What each row does — one of two declarations, per the ruling
⚠️ The unit of each row is a measurement of its producer/consumer (who writes the number, in what unit), ⛔ never a guess from the name — a wrong unit declaration is the exact silent ×1000 error this whole program exists to prevent. Print the reading per row in the PR body.
Exclusions (named so the fold is honest)
Acceptance
- After this PR and step ②,
--list shows zero undeclared time-shaped refusals on packages/spec/src; the summary line's declared-duration count equals the rows converted by type.
- Each renamed key carries its ADR-0087 entry, tombstone pin and form/docs/baseline updates per the retirement skill; each typed key carries a pin that a non-integer / negative literal is refused.
Clause-②: the seat derives at claim — a type adoption narrows acceptance (no); a rename retires an authorable key (no, a published surface shrinks) — both carry the BREAKING banner where a producer is measured writing what is now refused.
- Changeset
minor with **BREAKING** banner and ADR-0087 disposition for any renamed or newly-refusing row (pre-GA convention).
Fences
- ⛔ No gate change (step ②); ⛔ no new type (step ①; if a row needs a unit neither type covers, STOP and report — the ruling adds units only on a measured need); ⛔
content/docs/releases/** untouched.
Re-check before claiming
git fetch origin main
git grep -n -E 'export const (DurationMs|DurationSeconds)' origin/main -- packages/spec/src # step ① landed? must be ≥1
cd packages/spec && tsx scripts/check-duration-unit-keys.ts --list | grep -c '\[name: -\] \[prose: -\]' # A-class count today (23; 22 after #18114)
Refs
#18115 (ruling A) · #18080 (ruling C) · #18078 (census §A-true, §B, §D) · #15939 (the parent epic that renamed 21 sibling rows — the precedent for the rename route) · step ① / step ② cards · .claude/skills/spec-property-retirement
Dedup terms: shutdownTimeout DurationMs, loadTime saveTime unit, estimatedMigrationTime, slideInterval, genuine duration conversion, unit-nowhere durations
Generated by Claude Code
Blocked-by: #18669
Filed by the director seat (objectstack #12708, summon #23,
session_01WCEaPsmKY4UyoivKkkaUHt) executing decision batch #134 item 1 — maintainer 「决裁批 #134 同意」 to A on #18115, 2026-09-14T03:44Z. Step ③ of three: the genuine duration rows adopt a declaration.domain:spec; p2;pm:blockeduntil step ① is onorigin/main. Foldable by file family (five-gate fold: same defect shape, same fix, all rows ruled, each row independently checkable, exclusions named below).The population — census #18078 @
fb29f62cee; re-verify each row on today'sorigin/main(lines drift, key + file is the identity)Genuine durations, unit in no channel (class A-true, 6):
packages/spec/src/api/contract.zod.ts:311durationBaseResponseSchema.meta.duration, beside a stringtimestamp, no describe/JSDocpackages/spec/src/data/field-value.zod.ts:424durationFileValueSchema, media lengthpackages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts:413shutdownTimeout.default(30000), "Graceful shutdown timeout"packages/spec/src/kernel/plugin-security-advanced.zod.ts:623maxAgeAccess-Control-Max-AgemirrorexternalVocabularylike its siblingexpiresInpackages/spec/src/system/auth-config.zod.ts:563updateAge.default(60 * 60 * 24), "Session update frequency"; siblingexpiresIncarriesexternalVocabulary: 'better-auth session'externalVocabularyroute as its sibling is legitimatepackages/spec/src/system/metrics.zod.ts:412slideIntervaldurationSecondsin the samewindowobjectUnit only in JSDoc (class B, 2):
packages/spec/src/kernel/plugin-versioning.zod.ts:191 estimatedMigrationTime·packages/spec/src/system/metrics.zod.ts:847 resolution(siblingafterSeconds).Unit nowhere, outside the census (class D durations, ≥2):
packages/spec/src/system/metadata-persistence.zod.ts:259 loadTime,:287 saveTime— barez.number().optional(), no describe, no JSDoc, besidesizeandetag.Plus any genuine duration step ②'s widened detector surfaces (that card routes them here, ⛔ does not convert them).
What each row does — one of two declarations, per the ruling
DurationMs/DurationSeconds(step ①), keep the key name; describe gains the unit. Preferred where the key name is a domain word (duration,resolution,maxAge).shutdownTimeoutMs,loadTimeMs…). This is a RETIREMENT of an authorable key:retiredFromLoadPathconversion + ADR-0087 chain entry + tombstone +**BREAKING**banner atminor(launch-window convention, ADR-0087 amended 2026-09-13), following.claude/skills/spec-property-retirement. Preferred where the name is already unit-suggestive (*Timeout,*Time) and the census's own finding: check:duration-unit-keys reads .describe() but not JSDoc — a duration key documenting its unit only in JSDoc never enters the population, and one card already recorded a wrong reason because of it #15939 sibling rows were renamed that way.externalVocabularymeta stays a legitimate third route where the value mirrors an external contract's unit (maxAge,updateAge) — it is already an exemption class the gate declares.Exclusions (named so the fold is honest)
z.number()wall-clock instants toEpochMs(census #18078 class C ×4 + unit-nowhere instants ×6) — tranche 1 of ruling C on #18080 #18114's, ⛔ not touched here.lamportTimestamp/lamportClock— logical clocks, correctly not time.Acceptance
--listshows zero undeclared time-shaped refusals onpackages/spec/src; the summary line's declared-duration count equals the rows converted by type.Clause-②: the seat derives at claim — a type adoption narrows acceptance (no); a rename retires an authorable key (no, a published surface shrinks) — both carry the BREAKING banner where a producer is measured writing what is now refused.minorwith**BREAKING**banner and ADR-0087 disposition for any renamed or newly-refusing row (pre-GA convention).Fences
content/docs/releases/**untouched.Re-check before claiming
Refs
#18115 (ruling A) · #18080 (ruling C) · #18078 (census §A-true, §B, §D) · #15939 (the parent epic that renamed 21 sibling rows — the precedent for the rename route) · step ① / step ② cards ·
.claude/skills/spec-property-retirementDedup terms:
shutdownTimeout DurationMs,loadTime saveTime unit,estimatedMigrationTime,slideInterval,genuine duration conversion,unit-nowhere durationsGenerated by Claude Code