Repository navigation
Description
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/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 *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; changesetminor.- A pin that both types accept the six genuine rows' current defaults and refuse a non-integer / negative literal (loud at authoring,
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 ②).- 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
Activity
os-warren commentedon Sep 15, 2026
Claim:
domain:specexecution PM seat —session_01KB5PFtxuy1x3dcR5gxudx6, claimed 2026-09-15T01:14Z.Branch:
claude/issue-18122-closed-duration-typesClause-②: yes
Dispatched to an
os-devsubagent in a dedicated worktree. Labels written first (pm:dispatchedon,pm:queueoff, assignee set) and read back at01:14:17Z; this comment is the second half of the atomic pair.Clause-② declaration.
yes— this card introduces new exported symbols on the published@objectstack/specsurface, so the delivering PR and this card both carryneeds:contract-reviewfor the duration, and the gate clears from both carriers in one stroke only against an at-tier PASS naming the landed head.Premise re-check, run before the claim, on
origin/main@8c657f7dd0(2026-09-15T01:12Z). Every row is the card's own re-check command or a work item it lists:export const DurationMs|DurationSecondsinpackages/spec/srcexport const EpochMsinpackages/spec/src/shared/epoch.zod.ts:51shutdownTimeout .default(30000)kernel/plugin-lifecycle-advanced.zod.ts:413updateAgedefault60*60*24system/auth-config.zod.ts, pinned atauth-config.test.ts:109pm:queue, unassignedpm:blocked,Blocked-by: #18122bothpackages/spec/src/shared/epoch.zod.tsTwo card imprecisions corrected here rather than left for the dev to trip on. Both are mine to state, because a dispatch order that repeats an under-specified sentence is how the last two rounds lost time:
The gate script's path. The card writes
tsx scripts/check-duration-unit-keys.ts --self-test. There is no such file at the repo root. The real one ispackages/spec/scripts/check-duration-unit-keys.ts, run aspnpm --filter @objectstack/spec check:duration-unit-keys(that script runs--self-testfirst and then the gate, perpackages/spec/package.json:291). The acceptance criterion is unchanged; only the spelling was wrong.What "reachable from the package entry exactly as
EpochMsis" actually names. Measured, because "the entry" is ambiguous and the two readings differ in public surface:⇒
EpochMsis not on the root.entry. Its channel issrc/shared/index.ts'sexport *, published as the@objectstack/spec/sharedsubpath. So mirroring the precedent means landing the new types whereexport * from './epoch.zod'already carries them, and ⛔ not adding a rootsrc/index.tsre-export — a root export would be a wider published surface than the precedent, which is the exact widening an at-tier review is looking for. The Acceptance line's "published spec entry" reads as "published surface", and./sharedis one.Maintainer ruling carried into the order verbatim, untranslated (#18115, decision batch #134 item 1, agreed 2026-09-14T03:44Z):
The fences ride with it: this is step ① of three. ⛔ No key converted (that is #18124), ⛔ no gate behaviour changed (that is #18123), ⛔ no speculative third unit — the unit set is derived from the conversion population, not declared ahead of it.
Concurrency. Two other rounds are live in this lane (#18230/card #17598 on the analytics
dateRangearm andpackages/types; #18231/card #17502 onpackages/metadata-protocoland the repeater tombstones). Neither touchespackages/spec/src/shared/**, so the file surfaces are disjoint.os-verify-lock.sh --statusread arrival depth 1 againstLOCK_DEPTH_HOLD2 at01:13Z, so this is the third concurrent round and it is under the ceiling.Generated by Claude Code
os-warren commentedon Sep 15, 2026
os-dev-report
{ "issue": 18122, "status": "done", "branch": "claude/issue-18122-closed-duration-types", "pr": "https://github.com/objectstack-ai/objectstack/pull/18238", "premise_still_valid": true, "summary": "Added packages/spec/src/shared/duration.zod.ts declaring DurationMs and DurationSeconds, both z.number().int().nonnegative(), each with its bare type alias, re-exported from src/shared/index.ts exactly as epoch.zod is. A sibling file rather than an addition to epoch.zod.ts: that file IS the instant file (the gate's own source names its export INSTANT_ROOT), so durations inside it would make the module name false. Nothing is converted and no gate behaviour moves. Both PM corrections re-measured and confirmed on my own base. PREMISE RE-MEASUREMENTS on origin/main 8c657f7dd0 (my base, matching the PM's): (1) `git grep -n -E 'export const (DurationMs|DurationSeconds)' -- packages/spec/src` -> zero hits, exit 1. (2) `git grep -n -E 'export const EpochMs' -- packages/spec/src/shared/epoch.zod.ts` -> 1 hit at :51, exit 0. (3) CORRECTION 2 reachability: `git grep 'EpochMs' -- packages/spec/src/index.ts` -> zero hits; `packages/spec/src/shared/index.ts:40: export * from './epoch.zod';`; package.json exports carries './shared'. After landing, `grep -l 'DurationMs' packages/spec/api-surface/*.json` returns ONLY shared.json, and `grep -c 'EpochMs|DurationMs' api-surface/root.json` returns 0 (control: the precedent is absent from root too). api-surface/shared.json delta = 4 rows added, 0 removed. No root src/index.ts re-export was added. (4) CORRECTION 1 confirmed: the gate is at packages/spec/scripts/check-duration-unit-keys.ts and `pnpm --filter @objectstack/spec check:duration-unit-keys` runs --self-test then the gate; there is no repo-root copy. (5) shutdownTimeout is z.number().int().min(0).default(30000); session.updateAge is z.number().default(60*60*24). REFINEMENT: .int() because three of the six genuine rows already declare it and both rows carrying a default default to an integer (30000, 60*60*24); .nonnegative() because it is the weakest floor every declared floor implies -- one row declares .min(0), one .positive(), none a negative floor -- and .positive() would refuse the 0 that .min(0) already admits. The six genuine rows were derived, not assumed: the gate's --list census has 23 rows with a unit in neither channel, 17 of which are counts wearing duration vocabulary, leaving exactly the 6 the ruling names (23-6=17 matches the ruling's own arithmetic). ADR-0122: Iso873/Iso874 added to type-alias-convention.pin.test.ts, count 783->785 with both prose statements moved, because check:spec-parsed-alias reads that file as its exemption registry and would otherwise refuse the two bare aliases. TWO PUBLISHED CLAIMS my new module made false were repaired in-scope, both caught by red gates: packages/spec/llms.txt (hand-kept, no generator, ships in the npm tarball) said 14 shared schemas against 15 and 199 total against 200 -- number AND row prose corrected; and quick-reference.mdx's 'Shared Protocol (5 of 9 schemas)' [total] half moved to 10, with no row added because the gate states that closing the curated gap is a decision rather than a fix. ASSIGNEE was already set by the PM (os-warren) and I did not write it; the newest Claim comment 5673171906 names my branch.", "tests": "All at final HEAD 4a04e07eac, worktree clean, each exit code captured before any pipe. `pnpm lint` -> 0 (WHOLE repo, eslint . --no-inline-config; not a narrowing, so no narrowing evidence is owed). `pnpm --filter @objectstack/spec test` -> 0: 'Test Files 478 passed (478) / Tests 13622 passed (13622)'. `pnpm --filter @objectstack/spec typecheck` -> 0: 'check:test-typecheck: OK -- @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json'; the new test file is NOT in the debt ledger (check:test-typecheck first caught one real type error in it -- $ZodIssue is a discriminated union, so `expected` needs narrowing on `code` rather than a cast -- which I fixed rather than ledgering, since the ledger is maintainer-only and shrink-only). `pnpm --filter @objectstack/spec check:generated` -> 0: 'All 15 generated artifacts are up to date' (4 were proved stale and regenerated by --fix on a fresh build: api-surface, export-origins, declaration-map, docs). ACCEPTANCE CRITERION `pnpm --filter @objectstack/spec check:duration-unit-keys` -> 0: '211 duration-shaped numeric key(s) across 2490 source file(s) all carry their unit in the key name ...; 6 declared EpochMs instant(s), 11 declared externalVocabulary mirror(s); zero offenders, no baseline' -- the same 211/6/11 as origin/main, which is the measured proof that no gate behaviour moved. `pnpm check:spec-parsed-alias` -> 0: '1446 bare z.input aliases, 785 pinned isomorphic, 661 paired with an XParsed'. `pnpm --filter @objectstack/spec check:llms-txt` -> 0: '97 claim(s) re-derived'. `pnpm check:quick-reference-counts` -> 0. `pnpm check:nul-bytes` -> 0 (8668 files, no raw control bytes); I also ran the out-of-gate scan `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` over my five files -> exit 1, clean. GATE FAMILIES were derived mechanically with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (never a hand-rolled git diff), re-derived after each commit until it stopped growing: 84 -> 109 -> 109 stable. ALL 109 RUN. 107 green. The 2 NOT MEASURED are `pnpm check:dual-build-cjs-loads` and `pnpm check:type-check-debt`, both refusing with PREREQUISITE NOT MET because they read EVERY workspace package's dist/ (they name `turbo run build --filter='./packages/*' --filter='./packages/*/*'`); a whole-farm build is CI's run, and this diff touches only packages/spec. Five further gates first refused the same way and were CONVERTED to real green readings by building @objectstack/formula, @objectstack/lint and @objectstack/client-react: check:doc-formula-expressions, check:lean-entry-closure, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift. ABLATION (direction predicted before the run: turns red). Dropped .nonnegative() from DurationMs only. On-disk proof BEFORE the run rather than the editor's exit code: the anchored line count went 1 -> 0 and the mutated spelling -> 1. MUTATED run: exit 1, 'Tests 1 failed | 10 passed (11)', the failure being exactly 'DurationMs refuses a negative literal'. RESTORED leg: `git checkout HEAD --` against the ABSOLUTE target path (never a bare checkout), proven by blob identity -- git hash-object returned f02731a63112ffbee7f8263fc2fea36ec0dfc27f, equal to HEAD's blob, with `git diff HEAD` empty -- then re-run: 'Tests 11 passed (11)', exit 0. The script carried a restore trap armed on EXIT, INT and TERM with absolute paths throughout. The test resolves duration.zod.ts by RELATIVE source import, not through the package exports/dist, so no dist preflight applies to this ablation. All heavy runs went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-18122; every verdict read from the wrapper's own 'VERDICT command-exit' line, never a bare $?.", "mcp_calls": "0 - no MCP GitHub tool was called at any point; every GitHub read and write went through the REST proxy with curl.", "api_writes": "2 REST writes: POST /repos/objectstack-ai/objectstack/pulls (the draft PR) and POST /repos/objectstack-ai/objectstack/issues/18122/comments (this report). Plus 5 git pushes, all to my own branch claude/issue-18122-closed-duration-types (the mandatory empty-branch probe, then one per commit). NO labels write was spent: needs:contract-review is the PM's dual carrier and explicitly fenced off from me, and every other label on this PR is applied by the repo's own labeler (it had already put size/m on #18238 by the time I read it back), whose whole-set PUT would strip anything I added by hand. Reads not counted.", "open_questions": [ { "question": "The dispatch's write budget includes a labels POST and says to label the PR the moment it opens, but this card leaves no label for me to apply: needs:contract-review is explicitly yours, and documentation / tests / tooling / protocol:* / size/* are all applied by the repo's labeler workflow. I applied none. Is that right, or does this seat owe a label I have not identified?", "options": [ "A - correct as delivered: no dev-owned label exists on this card, the PM hangs needs:contract-review on both carriers", "B - a dev-owned label does exist (name it) and I should POST it to the additive labels endpoint and read it back" ], "recommendation": "A, because the only label this card names is the contract-review carrier the dispatch fences off from me, and hand-adding a labeler-owned label invites exactly the whole-set-PUT strip the dispatch warns about. Flagged rather than silently skipped." } ], "out_of_scope_findings": [ "noted, not filed: packages/spec/src/data/driver.zod.ts:414 `idle` appears in the unitless duration census but is a pool-stat COUNT (the getPoolStats output object is { total, idle, active, waiting }), not a duration -- its name reads like one and its census neighbours are, so step 3's triage could mis-sort it. Not a defect, contract violation or authoring trap, so not filed. Carrier: #18124, which reads this same census.", "noted, not filed: packages/spec/src/shared/connector-auth.zod.ts:38 `tokenExpiry` describes itself as a 'Token expiry timestamp' -- an INSTANT, so EpochMs rather than a duration type, again relevant to step 3's sorting of the same 23 unitless rows. Not a defect, contract violation or authoring trap, so not filed. Carrier: #18124.", "noted, not filed: the PR body write was mutated by the platform on create -- it appended a blank line, a rule and the session-URL footer AFTER the harness-mandated footer my body already carried, so the stored body ends with two attribution blocks. My body survived byte-exact as a prefix (8844 of 8934 stored bytes; the 90 appended bytes are exactly that block). Left alone deliberately: AGENTS.md forbids re-sending a body that already carries an appended footer. Carrier: none -- this is a known measured platform behaviour recorded in pm-dispatch references, not a defect." ] }Generated by Claude Code
os-warren commentedon Sep 15, 2026
席位裁定 —— 唯一的
open_question答 A;两条 PM 更正逐条复验通过;双载体已同笔挂上domain:spec执行席,session_01KB5PFtxuy1x3dcR5gxudx6,2026-09-15T02:2xZ。⛔ 不上交维护者。open_question—— 「这张卡有没有一个 dev 该打的标签?」 裁 A:没有,交付正确推荐成立,理由比它给的更硬一条:本卡唯一点名的标签就是
needs:contract-review,而派发令把它明确围给了席位;其余(documentation/tests/tooling/size/*/protocol:*)全部由仓里的 labeler workflow 产出,它以整集 PUT 写入 —— 手加一个进去,下一次 labeler 跑就把它剥掉,留下一个「加了标、没人知道为什么没了」的半状态。⭐ 把这个疑问摆出来而不是默默跳过是对的。 派发令的写预算里确实列了一次 labels POST —— 那是令的模板惯性,不是本卡的事实。令写错了,dev 照着现实办并申报,处置正确。本班第六次 dev 把冲突摆上台面。
两条 PM 更正,本席独立复验(⛔ 不采信 dev 自述)
读数取
origin/main与分支 ref,2026-09-15T02:20Z:更正 1(门禁路径) —— 令里改写的
packages/spec/scripts/check-duration-unit-keys.ts+pnpm --filter @objectstack/spec check:duration-unit-keys成立;仓根无同名副本。✅更正 2(「reachable from the package entry exactly as
EpochMsis」到底指哪条通道) —— 这条是本席当初怕它踩坑才逐字量的,现在逐字复验:控制词同主体:根
index.ts里EpochMs同样是 0 命中,而export {在同一文件 36 次 ⇒ 文件读得到、模式匹配得上,那个 0 是真缺席,不是坏仪器。⇒ 新符号走的是
./shared子路径,与先例逐字同形,⛔ 没有新增根src/index.ts再导出 —— 那会是比先例更宽的已发布面,正是达档复核要抓的那种放宽。围栏守住了。✅双载体 —— 同笔挂上,⛔ 这次没有重蹈 #17502 的覆辙
⏳ 已派达档复核,并把三件本席认为会藏在绿里的事点名交给它
⛔ 本席不自判(服务档位低于
CONTRACT_REVIEW_TIER),但交办时把判据写清:type-alias-convention.pin.test.ts由 783 → 785。抬 ratchet 上限原属人工地板,判别点是有没有既有行被挪动 —— 若只是两个真新行的登记,那是注册不是削弱。packages/spec/llms.txt(手工维护、无生成器、随 npm tarball 发布)14→15、199→200;quick-reference.mdx的 total 改 10。要求复核核新数在本 head 上为真,并且数字旁边那句话也要为真 —— 改对了数、留错了句,仍是缺陷。check:dual-build-cjs-loads/check:type-check-debt)。另:dev 报告里两条
out_of_scope_findings(driver.zod.ts:414的idle其实是池计数不是时长;connector-auth.zod.ts:38的tokenExpiry是瞬时该用EpochMs)—— 两条都正确地没立卡,承接者都点名了 #18124(步骤③,读同一份普查)。⭐ 这正是「进 Acceptance notes 前先问:哪一个 PR 会碰到它」该有的答法:说得出具体卡。Generated by Claude Code
os-warren commentedon Sep 15, 2026
✅ 落地 —— git 侧核实,⛔ 不取 API 的
merged字段。两张下游卡已解锁domain:spec执行席,session_01KB5PFtxuy1x3dcR5gxudx6,2026-09-15T03:22Z。本班第二个落地,也是本班唯一一张从零派发到落地的卡(另一张是接手的返工)。落地读数
main是 03:21Z 这次 fetch 才读到的 —— 相差约 29 分钟。 台账记的是后者。这是本班第二次量到同一件事(#18233 差约 31 分钟)。pm:dispatched已同笔摘掉并回读:priority:p2,domain:spec,assignee 留着(归属不是状态)。解锁扫描 —— 两张卡回队列,先在合并后的 ref 上重验
「上游关单放回解锁卡」,并按 放行双查 逐条过:
pm:blocked是立卡时打的),条件即正文Blocked-by: #18122—— 已满足 ✅68fea8bcac上重验工作项仍未完成check-duration-unit-keys.ts:649仍是site.durationShaped && …,DURATION_SHAPED_TOKENS仍在(3 处)⇒ admission 未动 ✅shutdownTimeout仍是z.number().int().min(0).default(30000)⇒ 未转换 ✅git grep DurationMs -- packages/spec/src | grep -v duration.zod.ts,它回了几条 —— 但那些全是totalDurationMs这类键名,不是该类型的使用。模式太松。正确的读法是这条零命中不能靠那个 grep 下结论;真正成立的判据是上表那条:目标行本身仍是裸z.number()。⛔ 不把一个太松的模式的输出写成干净。DurationMs/DurationSecondsor a unit-suffixed name (census #18078 classes A-true ×6, B ×2, D ×≥2) — step ③ of ruling A on #18115 #18124 →pm:queue(步骤 ③,转换那些真时长行)check-duration-unit-keysadmits by DECLARATION (Duration*/EpochMs type or unit-in-name), the name list becomes a detector that refuses undeclared time-shaped numerics, dimensionless rows declare it in-schema — step ② of ruling A on #18115 #18123 →pm:queue+pm:retriage⛔ 为什么 #18123 同时挂
pm:retriage因为它照现状派不出去:它执行的裁决对「名字表何去何从」在同一张 #18115 里说了两次且不一样 —— 维护者速读段说 「名字表退休」(
退休3 处),执行形状段说 「名字表降为提示」(提示1 处)。而 #18123 正是改 admission 规则的那张卡,必须二选一。我在 02:49Z 已把这条落在 #18123 上(
5673919233),但那时它只是一条建议性的注记。挂pm:retriage才让它有约束力 —— 「带本标签的pm:queue卡跳过派发」,于是这个问题必须被回答,而不是被某个派发席顺手略过。⛔ 本席不代裁:这改的是门禁行为。Refs
#18115(裁决 A,批 #134 item 1)· #18238(本卡的交付,已合)· #18123 / #18124(步骤 ②③)· #18075(同一分支的另一处漏判,已定级
pm:queuep2Bug,与 #18123 改同一个分支 —— 两者谁先动都要读对方)Generated by Claude Code
spec: closed duration types DurationMs / DurationSeconds beside Epoch…
fix(pm): bind H31's carrier pair through the closing keyword (objects…
spec(gate): check-duration-unit-keys admits by declaration — retire t…
feat(spec)!: FileValue.duration and CompatibilityMatrixEntry.estimate…
test(spec): shared test titles state each cited decision in words ins…