You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[#14478 stack 1/6] declare the two exemption classes ON THE SCHEMA — a shared EpochMs for the 6 epoch instants and a .meta({ externalVocabulary }) marker on the 13 external-standard keys, honoured by check:duration-unit-keys and printed by the docs generator #15676
Part of #14478 — follow-on card 1 of 6, filed by the domain:spec seat (session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T02:55Z) under the maintainer ruling B recorded on #14478 (5548763981, decision batch #43, 「同意」). This is the FIRST card of the stack: every directory card (2–5) applies the rule to what remains after these two classes are declared, so they wait for this one's PR to exist.
The ruling (verbatim, the parts this card executes)
the rule (a z.number() duration key carries its unit in its name) governs every authored duration and every runtime-emitted duration, minus two structural classes declared ON THE SCHEMA, never in a gate ledger: (i) epoch instants move to a shared EpochMs schema; (ii) keys mirroring an external standard carry a .meta({ externalVocabulary: '…' }) marker the gate honours and the docs generator publishes.
Exempt by declaration, not by list: the 6 epoch instants (a different confusion; one shared EpochMs schema and a rename to *At/*Ms where the name is bare) and the ~12 external-vocabulary keys (marker on the key; the reference page prints "unit per the named standard").
(1) the gate's own predicate is the population's definition — the 70/65 reading is authoritative, the seat's regex census is corroboration only; write the predicate into the follow-on cards so nobody recounts by another rule. (2) sequencing: PR #15626 stays a draft (governed — skills/** — human merge) and is ⛔ not landed with the gate red or unwired; the remainder is dispatched as follow-on cards per directory […], branches stacked on #15626's; the set lands together when check:duration-unit-keys is green on the stacked head.
The predicate (the gate's, from packages/spec/scripts/check-duration-unit-keys.ts on PR #15626's head e68ae2b58; run pnpm --filter @objectstack/spec check:duration-unit-keys on the stacked head — ⛔ never recount by another rule)
A property whose value is a z.number() / z.int() / z.coerce.number() chain and whose .describe() names a time unit must carry that unit as a token of its key name (Ms / Seconds / Minutes / Hours / Days, plus the knex-inherited Millis), and the token must agree with the prose. { value, unit } pairs are recognised structurally by the sibling unit key; duration literals are strings and outside the population. Calendar positions and rates are skipped. Declared population today: packages/spec/src/** (card 6/6 widens it).
What this card changes
EpochMs — one shared schema (a z.number() epoch-milliseconds instant, described as such, exported from the shared layer beside the other primitive schemas) and the gate learns to recognise a key whose value IS that schema as an instant, not a duration. The six instants the gate lists on e68ae2b58 move onto it; where the key name is bare the ruling renames it to *At / *Ms (ADR-0087 conversion where the key is authorable; a semantic entry where it is runtime-emitted):
site (line numbers from e68ae2b58)
key
src/api/websocket.zod.ts:473
timestamp
src/api/websocket.zod.ts:502
lastSeen
src/kernel/context.zod.ts:106
startTime
src/kernel/service-registry.zod.ts:69
registeredAt
src/kernel/service-registry.zod.ts:266
createdAt
src/kernel/startup-orchestrator.zod.ts:98
timestamp
.meta({ externalVocabulary: '…' }) — the marker convention (a .meta() key the gate reads: a marked key is exempt from the rename rule; the describe still states the unit) and its docs-generator half (the reference page prints the unit as "per the named standard"). Applied to the thirteen external-standard keys the gate lists on e68ae2b58:
Gate + self-test cases for both classes (a marked key is exempt; a bare EpochMs key is exempt; a marked key whose describe contradicts its own stated unit is still refused); docs generator test for the printed phrase; check:generated followers regenerated.
Clause-②: yes (renames on published keys; a new exported schema) ⇒ needs:contract-review on the PR and on this card at PR creation (dual carrier, one stroke).
Changeset: @objectstack/specminor with a BREAKING banner for the renamed instants and adr-0087: registered naming every conversion id; adr-0087 disposition per scripts/check-adr-0087-registration.mjs.
packages/spec/src/migrations/registry.ts is written by this stack only (held for it; see the seat post §3).
Expected offender count after this card on the stacked head: 65 − 6 − 13 = 46 (corroboration; the gate's number is the reading).
Part of #14478 — follow-on card 1 of 6, filed by the
domain:specseat (session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T02:55Z) under the maintainer ruling B recorded on #14478 (5548763981, decision batch #43, 「同意」). This is the FIRST card of the stack: every directory card (2–5) applies the rule to what remains after these two classes are declared, so they wait for this one's PR to exist.The ruling (verbatim, the parts this card executes)
The predicate (the gate's, from
packages/spec/scripts/check-duration-unit-keys.tson PR #15626's heade68ae2b58; runpnpm --filter @objectstack/spec check:duration-unit-keyson the stacked head — ⛔ never recount by another rule)A property whose value is a
z.number()/z.int()/z.coerce.number()chain and whose.describe()names a time unit must carry that unit as a token of its key name (Ms/Seconds/Minutes/Hours/Days, plus the knex-inheritedMillis), and the token must agree with the prose.{ value, unit }pairs are recognised structurally by the siblingunitkey; duration literals are strings and outside the population. Calendar positions and rates are skipped. Declared population today:packages/spec/src/**(card 6/6 widens it).What this card changes
EpochMs— one shared schema (az.number()epoch-milliseconds instant, described as such, exported from the shared layer beside the other primitive schemas) and the gate learns to recognise a key whose value IS that schema as an instant, not a duration. The six instants the gate lists one68ae2b58move onto it; where the key name is bare the ruling renames it to*At/*Ms(ADR-0087 conversion where the key is authorable; a semantic entry where it is runtime-emitted):e68ae2b58)src/api/websocket.zod.ts:473timestampsrc/api/websocket.zod.ts:502lastSeensrc/kernel/context.zod.ts:106startTimesrc/kernel/service-registry.zod.ts:69registeredAtsrc/kernel/service-registry.zod.ts:266createdAtsrc/kernel/startup-orchestrator.zod.ts:98timestamp.meta({ externalVocabulary: '…' })— the marker convention (a.meta()key the gate reads: a marked key is exempt from the rename rule; the describe still states the unit) and its docs-generator half (the reference page prints the unit as "per the named standard"). Applied to the thirteen external-standard keys the gate lists one68ae2b58:src/api/auth-endpoints.zod.ts:287intervalsrc/api/http-cache.zod.ts:71/:72/:73maxAge/staleWhileRevalidate/staleIfErrorsrc/shared/http.zod.ts:139maxAgeAccess-Control-Max-Agesrc/system/object-storage.zod.ts:258maxAgesrc/system/auth-config.zod.ts:308/:330/:550resetPasswordTokenExpiresIn/expiresIn/expiresInsrc/api/storage.zod.ts:44expiresInsrc/system/object-storage.zod.ts:197expiresInsrc/data/driver/postgres.zod.ts:265statementTimeoutstatement_timeoutsrc/system/disaster-recovery.zod.ts:127ttlEpochMskey is exempt; a marked key whose describe contradicts its own stated unit is still refused); docs generator test for the printed phrase;check:generatedfollowers regenerated.Execution notes
claude/issue-14478-duration-unit-in-key-name(PR feat(spec)!: duration-shaped number keys carry their unit in the key name — no-baseline gate + seven ADR-0087 renames (timeoutMs, ttlSeconds/ttlMs, *TimeoutSeconds) #15626, heade68ae2b58at filing time), PR base = that branch, draft, body first linePart of #14478and a second line naming this card without a closing keyword. ⛔ NeverFixes #14478. The set lands together (human merge on feat(spec)!: duration-shaped number keys carry their unit in the key name — no-baseline gate + seven ADR-0087 renames (timeoutMs, ttlSeconds/ttlMs, *TimeoutSeconds) #15626 after the stacked head greens the gate).Clause-②: yes(renames on published keys; a new exported schema) ⇒needs:contract-reviewon the PR and on this card at PR creation (dual carrier, one stroke).@objectstack/specminor with a BREAKING banner for the renamed instants andadr-0087: registerednaming every conversion id;adr-0087disposition perscripts/check-adr-0087-registration.mjs.packages/spec/src/migrations/registry.tsis written by this stack only (held for it; see the seat post §3).Related: #14478 · PR #15626 · #15642 (folded into card 6/6) · #14519 (rides with #15626).