spec: closed duration types DurationMs / DurationSeconds beside EpochMs (step 1 of ruling A on #18115) - #18238
Conversation
… EpochMs Step 1 of ruling A on the unit-in-key census: a duration declares its unit either on its VALUE (one of these two types) or as a token in its key name. Both are z.number().int().nonnegative() -- the refinement measured against the six genuine duration rows the ruling derives the unit set from. Declared in shared/duration.zod.ts and re-exported from shared/index.ts exactly as epoch.zod is, so both reach consumers on the @objectstack/spec/shared subpath and neither widens the root entry. No key is converted and no gate behaviour changes here; the gate learns to read the new channel in step 2 and the rows convert in step 3. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…ocabulary api-surface, export-origins, declaration-map and the reference docs, all four proved stale by check:generated and regenerated by its --fix on a fresh build. Sharded per entry, so only shared/ moves: 4 rows added on the ./shared surface (DurationMs and DurationSeconds, const and type each), 0 removed, and no other entry point gains a row. Also narrows the two ZodIssue assertions in duration.test.ts on `code` instead of casting -- $ZodIssue is a discriminated union and `expected` / `minimum` exist only on their own members, which check:test-typecheck caught. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
… holds check:llms-txt went red on two claims this branch made false: the shared domain row said 14 schemas against 15 on disk, and the section heading said 199 against 200. The file is hand-kept with no generator and ships to AI consumers inside the npm tarball, so the row's PROSE is corrected alongside its number -- the shared row now names the duration vocabulary beside the epoch instant it sits next to, rather than carrying a count nothing in the row accounts for. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
check:quick-reference-counts went red on the [total] half of the "Shared Protocol (5 of 9 schemas)" heading: M counts the pages content/docs/references/shared/ publishes, and gen:docs added a tenth. Only M moves. The table is a curated subset -- N < M is its normal state, and the gate is explicit that adding a row to close the gap is a decision rather than a fix, so the duration page is left unlisted exactly as the epoch page it sits beside already is. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 103f906216416b80480853f281c2f3b82e0d5299 && git checkout 103f906216416b80480853f281c2f3b82e0d5299
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b 4a04e07eac974a218d1b087d1e8994be9b028e78 && git checkout -B drift-repro b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b && git merge --no-ff 4a04e07eac974a218d1b087d1e8994be9b028e78
node scripts/docs-audit/affected-docs.mjs --json b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b |
Contract reviewServed-tier: First review of this PR. Head confirmed via ① Derived judgmentsSize and base. Published surface — precedent verified, no widening. Root entry The refinement — six rows derived, not accepted. Can every row adopt Author state = parsed state. Both exports are No gate behaviour moved — and what the identical output does and does not establish. ② Semver level
③ Boundary flags1. Ratchet 783 → 785 — registration of two new rows, not a weakening. In 2. Published claims — both new numbers true at this head, and the prose beside them true. 3. Test-layer type error — fixed by narrowing, not by a cast or a suppression. 4. The two gates the dev could not measure — measured from CI, not from the report. Other measurements. Local at head, each exit 0: Implemented-by: VERDICT: PASS Generated by Claude Code |
席位采纳达档记录
|
| 段落 | 原文 |
|---|---|
| 维护者速读(维护者作答前读的那段) | 「名字表退休」(3 处 退休) |
| 执行形状段 | 「名字表降为提示」(1 处 提示) |
「退休」与「降为提示」不是一回事,而 #18123(步骤 ②)正是改 admission 规则的那张卡 —— 它必须二选一。⛔ 这不是本席能替维护者裁的。已在 #18123 上落一条,交由它的派发席在起工前解决。
落地前检
- ① 达档 PASS 在案并点名本 head
4a04e07eac974a218d1b087d1e8994be9b028e78✅(记录5673894386) - ②
check-clause2-carriers --pair 18238清标前读 ✅ 双载体一致 - ③ 待清标后重取 ——
⚠️ 本席自己的标签写入会重新触发Check Changeset,fix(spec): guard four prototype fall-through lookups with own-property checks #18233 上量过,所以 ③ 必须在清标之后取,⛔ 不复用清标前的读数
⏳ 清标、provenance 评论、③ 复检、翻 ready、入队,按序进行。
Generated by Claude Code
Provenance — carriers cleared against the at-tier PASS, and the three landing pre-checks, each with the time it was taken
Record of the clearing: comment
Carriers cleared in one stroke and read back: PR at
⏳ Flipping ready and enqueuing. Landing will be confirmed git-side on Generated by Claude Code |
Fixes #18122
Step ① of three on ruling A of #18115 (decision batch #134 item 1): the declared shape the unit-in-key census will read. It adds the two closed duration types and converts nothing — step ② (#18123) teaches the gate to read them, step ③ (#18124) converts the rows.
Where the types landed, and why a sibling file
packages/spec/src/shared/duration.zod.ts, a new sibling re-exported fromsrc/shared/index.tsexactly asepoch.zodis — not appended toepoch.zod.ts. That file is the INSTANT file: its whole doc block is the instant-vs-duration distinction, and the gate's own source calls its exportINSTANT_ROOT. Putting durations inside it would make the module's name false about its contents. The barrel re-export is what makes the two files equivalent in published surface, which is the property the card asks to mirror.Reachability — measured, not asserted
The card's "reachable from the package entry exactly as
EpochMsis" means the./sharedsubpath, not the root entry. Four readings on this branch, the last two taken from the regenerated artifact:The
api-surface/shared.jsondelta is 4 rows added, 0 removed —DurationMs (const),DurationMs (type),DurationSeconds (const),DurationSeconds (type). No rootsrc/index.tsre-export was added.The refinement, and the defaults that chose it
z.number().int().nonnegative()for both, measured against the six genuine duration rows — the 23 census rows that carry a unit in neither channel, minus the 17 counts wearing a duration's vocabulary:kernel/plugin-lifecycle-advanced.zod.tsshutdownTimeoutz.number().int().min(0).default(30000)kernel/plugin-security-advanced.zod.tscors.maxAgez.number().int().optional()system/metrics.zod.tsslideIntervalz.number().int().positive().optional()system/auth-config.zod.tssession.updateAgez.number().default(60 * 60 * 24)api/contract.zod.tsmeta.durationz.number().optional()data/field-value.zod.tsFileValue.durationz.number().optional().int()— three of the six already declare it, and both rows that carry a default default to an integer (30000,60 * 60 * 24). The three barez.number()rows are tightened by adopting it, which is the same tighteningepoch.zod.tsrecords for the sites that adoptedEpochMs..nonnegative()— the weakest floor every declared floor implies. One row declares.min(0), one.positive(), none a negative floor..positive()would be too strong in the other direction:.min(0)admits0, and a zero timeout means "do not wait".DurationMinutes/DurationHours/DurationDaysare absent on purpose — the unit set is derived from the conversion population, never declared ahead of it.Author state and parsed state coincide (no
.default(), no.transform()on the types themselves), so there is deliberately no*Parsedsynonym, and the isomorphism is pinned as ADR-0122 requires:Iso873/Iso874intype-alias-convention.pin.test.ts(count 783 -> 785, both prose statements moved with it).check:spec-parsed-aliasreads that file as its exemption registry, so the pins are load-bearing rather than decorative.The pin can actually fail — ablation
Dropping
.nonnegative()fromDurationMsonly, proven on disk before the run (the anchored line count went1 -> 0, the mutated spelling-> 1) rather than trusted to the editor's exit code:Restore proven by blob identity, not by an exit code:
git hash-objectreturnedf02731a63112ffbee7f8263fc2fea36ec0dfc27f, equal toHEAD:packages/spec/src/shared/duration.zod.ts, withgit diff HEADempty.Two published claims this branch made false, both repaired
Neither is scope creep — both are existing published artifacts that my new module invalidated, and both gates went red:
packages/spec/llms.txt(hand-kept, no generator, ships inside the npm tarball): thesharedrow said 14 schemas against 15 on disk and the section heading said 199 against 200. The row's prose was corrected alongside its number — it now names the duration vocabulary beside the epoch instant — because the gate is explicit that rewriting a count without re-reading its row turns a loud staleness into a silent lie.content/docs/getting-started/quick-reference.mdx: the[total]half of "Shared Protocol (5 of 9 schemas)". Only M moved, to 10. The table is a curated subset and N < M is its normal state, so the duration page is left unlisted exactly as the epoch page beside it already is — the gate names adding a row as a decision rather than a fix.Local verification — every command, at
4a04e07eac, worktree cleanpnpm linteslint . --no-inline-config— not a narrowingpnpm --filter @objectstack/spec testTest Files 478 passed (478)·Tests 13622 passed (13622)pnpm --filter @objectstack/spec typecheckpnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to datepnpm --filter @objectstack/spec check:duration-unit-keyspnpm check:spec-parsed-alias1446 bare z.input aliases, 785 pinned isomorphicpnpm --filter @objectstack/spec check:llms-txt97 claim(s) re-derivedpnpm check:quick-reference-counts13 section(s), every heading matchespnpm check:nul-bytesThe gate families were derived mechanically rather than guessed —
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived after each commit until it stopped growing at 109 commands, all 109 run. 107 green. The two not measured arecheck:dual-build-cjs-loadsandcheck:type-check-debt: both refused withPREREQUISITE NOT METbecause they read every workspace package'sdist/, which needsturbo run build --filter='./packages/*' --filter='./packages/*/*'— a whole-farm build this diff cannot make relevant, since it touches onlypackages/spec. Five further gates that first refused the same way were converted to real green readings by building@objectstack/formula,@objectstack/lintand@objectstack/client-react.This step changes no gate behaviour, and the gate says so itself:
check:duration-unit-keysreports the same 211 duration-shaped keys, 6EpochMsinstants and 11externalVocabularymirrors as onorigin/main. It recognises exactly one identifier root today —EpochMs— so a key typedDurationMsis outside its population rather than exempted by it, which is precisely what step ② changes.Changeset
minor, notpatch:Clause-②: yesand the./sharedentry gains exported symbols. Notmajor— breaking changes ship asminoruntil GA (#14043), and nothing here is breaking in any case: no key is converted, renamed or refused, and the six rows still declare exactly what they declared before.Out of scope, noted not filed
packages/spec/src/data/driver.zod.ts:414idlesits in the unitless census but is a pool-stat count ({ total, idle, active, waiting }fromgetPoolStats), not a duration — worth knowing for step ③'s triage, since its name reads like a duration and its neighbours in the census are. Carrier: spec: the genuine duration rows adopt a declaration —DurationMs/DurationSecondsor a unit-suffixed name (census #18078 classes A-true ×6, B ×2, D ×≥2) — step ③ of ruling A on #18115 #18124, which reads this same census.packages/spec/src/shared/connector-auth.zod.ts:38tokenExpirydescribes itself as a "Token expiry timestamp" — an instant, soEpochMsrather than a duration type. Same carrier, spec: the genuine duration rows adopt a declaration —DurationMs/DurationSecondsor a unit-suffixed name (census #18078 classes A-true ×6, B ×2, D ×≥2) — step ③ of ruling A on #18115 #18124.Neither is a defect, a contract violation or an authoring trap, so neither is filed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Generated by Claude Code