Repository navigation
[#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
Description
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 sharedEpochMsschema; (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
EpochMsschema and a rename to*At/*Mswhere 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 whencheck:duration-unit-keysis 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 (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):site (line numbers from e68ae2b58)key 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:site key standard src/api/auth-endpoints.zod.ts:287intervalOAuth 2.0 device authorization (RFC 8628) src/api/http-cache.zod.ts:71/:72/:73maxAge/staleWhileRevalidate/staleIfErrorHTTP Cache-Control (RFC 9111) src/shared/http.zod.ts:139maxAgeCORS Access-Control-Max-Agesrc/system/object-storage.zod.ts:258maxAgeCORS on bucket rules src/system/auth-config.zod.ts:308/:330/:550resetPasswordTokenExpiresIn/expiresIn/expiresInbetter-auth option names src/api/storage.zod.ts:44expiresInS3 presigned-URL option src/system/object-storage.zod.ts:197expiresInS3 presigned-URL option src/data/driver/postgres.zod.ts:265statementTimeoutPostgreSQL statement_timeoutsrc/system/disaster-recovery.zod.ts:127ttlDNS record TTL ⚠️ The standard named beside each key is the seat's reading of 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's body; the dev verifies each against the schema's own docblock and corrects the attribution where the schema says otherwise (a key that turns out NOT to mirror a standard is a rename for its directory card, ⛔ not a marker).- Gate + self-test cases for both classes (a marked key is exempt; a bare
EpochMskey 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
- Stacked: branch off
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).- Changeset:
@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).- Expected offender count after this card on the stacked head: 65 − 6 − 13 = 46 (corroboration; the gate's number is the reading).
Related: #14478 · PR #15626 · #15642 (folded into card 6/6) · #14519 (rides with #15626).
Activity