Skip to content

[#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

Closed

Description

@os-project-manager

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

  1. 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
  2. .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:
    site key standard
    src/api/auth-endpoints.zod.ts:287 interval OAuth 2.0 device authorization (RFC 8628)
    src/api/http-cache.zod.ts:71 / :72 / :73 maxAge / staleWhileRevalidate / staleIfError HTTP Cache-Control (RFC 9111)
    src/shared/http.zod.ts:139 maxAge CORS Access-Control-Max-Age
    src/system/object-storage.zod.ts:258 maxAge CORS on bucket rules
    src/system/auth-config.zod.ts:308 / :330 / :550 resetPasswordTokenExpiresIn / expiresIn / expiresIn better-auth option names
    src/api/storage.zod.ts:44 expiresIn S3 presigned-URL option
    src/system/object-storage.zod.ts:197 expiresIn S3 presigned-URL option
    src/data/driver/postgres.zod.ts:265 statementTimeout PostgreSQL statement_timeout
    src/system/disaster-recovery.zod.ts:127 ttl DNS 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).
  3. 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.

Execution notes

Related: #14478 · PR #15626 · #15642 (folded into card 6/6) · #14519 (rides with #15626).

Activity

  1. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · pm:queue / domain:spec / priority:p2 / enhancement

    ⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。本卡的方向已由维护者裁决 B(#14478 5548763981,batch #43,「同意」)定下,⛔ 本席位不重开、不改写、不「优化」那条裁决——只做锚定、定级与排序。

    ⭐ 一次可做的复核:整条栈的算术自洽

    本卡自称「本卡之后在叠加 head 上的预期 offender 数 = 65 − 6 − 13 = 46(佐证,门的数字才是读数)」。把 2/6–5/6 四张目录卡各自标题里的数字加起来:

    2/6 `api/`                     12
    3/6 `kernel/`                  14
    4/6 `system/`                  13
    5/6 `data/ ui/ ai/ integration/` 7
                                  ───
                                    46
    

    46 = 46。 ⇒ 六张卡的人口切分没有重叠、没有缺口,也没有哪张卡把别人的键算进自己名下。这是本席位在不重跑门的前提下能做的最有力的一次交叉验证,记录在此。

    ⚠️ 但请照裁决办:门自己的谓词才是人口的定义。上面这条只是佐证;⛔ 任何人都不要用另一套规则(正则普查、手数)去重数,裁决 (1) 明令如此。

    ⚠️ 一条必须点破的前提:这道门今天不在 main 上

    本席位实测(origin/main = 95d5cbb):

    git ls-tree -r --name-only origin/main | grep -i "duration-unit-keys"        → 0
    git grep -n "duration-unit-keys" -- packages/spec/package.json package.json  → 0
    

    阳性对照(同一次读):packages/spec/scripts 下有 125 个文件 ⇒ 树可读、pathspec 有效,零是真的零。

    ⇒ check-duration-unit-keys.ts、它的 SRC_ROOT 与自测,全部只存在于 PR #15626 的草稿分支上。所以:

    • 本卡正文里那些 e68ae2b58 行号,⛔ 在 main 上一个也验不了——不是卡片写错,是它引用的就是分支。
    • 接手者的第一步不是写代码,而是把分支取到手、在其 head 上跑 pnpm --filter @objectstack/spec check:duration-unit-keys 拿到当下的读数。⛔ 不要把本卡正文里的两张表当作现成清单直接照抄——卡片自己也警告了(见下)。

    为什么锚定 domain:spec

    落点:packages/spec/src/** 的 schema(新的共享 EpochMs、13 处 .meta({ externalVocabulary }) 标记)、packages/spec/scripts/check-duration-unit-keys.ts 的门与自测、以及 docs 生成器那一半。按车道表:packages/spec 整包唯一契约 + packages/spec/scripts/** + 围着 spec 契约转的工具链(门禁/生成器)⇒ 全部 domain:spec。整条栈同车道。

    priority:p2,且六张同级

    给同级是刻意的:裁决 (2) 明写「the set lands together when check:duration-unit-keys is green on the stacked head」⇒ 这是一个落地单元,给它们不同优先级会制造一个不存在的取舍。

    选 p2 而非 p1:今天没有任何东西是坏的——ttl/timeout 这些键在运行时行为正确,问题是同一个块里两个 ttl 单位不同、timeout 裸名这类可读性与误用风险。
    选 p2 而非 p3:裁决已下、PR 已在飞、且 #15626 以红门状态挂着草稿——这条栈是当前正在阻塞一条已开工链路的东西,不是可以无限期躺着的清理。

    为什么是 enhancement

    机械边界判据:本栈重命名已发布的键并新增一个导出 schema 与一条 .meta() 约定 ⇒ 改变可授权表面 ⇒ Feature 侧、manual floor。卡片自陈 Clause-②: yes 与此一致。

    ⛔ 本席位不代贴 needs:contract-review:卡片明写它在 PR 创建时同时贴到 PR 与卡上(dual carrier, one stroke)。记录在此供派发/执行席按流程处置。

    交给接手者的四条(全部出自卡片与裁决,本席位只是加重)

    1. ⛔ 表里的「标准」名是读来的,不是验过的。 卡片自陈:13 条 external-vocabulary 的标准归属是该席对 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 正文的阅读,dev 必须逐条对照 schema 自己的 docblock 核实;核实下来不是镜像某标准的键,属于它所在目录卡的一次重命名,⛔ 不是一个 marker。⇒ 这条一旦搞错,会把一个本该改名的键永久豁免掉。
    2. ⛔ 绝不 Fixes #14478。 PR 正文首行 Part of #14478,第二行指名本卡且不带关闭关键字。整条栈同此。
    3. 叠加而非平行:分支从 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)切出,PR base 指向它,draft。⛔ 不要直接对 main 开 PR。
    4. 自测要钉三种情形(卡片已列):marked 键豁免;裸 EpochMs 键豁免;marked 键的 describe 与它自称的单位矛盾时仍然被拒。⭐ 第三条是防止 marker 变成万能免死金牌的那一条,⛔ 不能省。

    ⚠️ packages/spec/src/migrations/registry.ts 由本栈独占写入(座位贴 §3 已为其保留)⇒ ⛔ 其它卡不要在同期改它。

    分诊席位 · claude-opus-5 · 本轮 R+156


    Generated by Claude Code

  2. claude commented on Sep 5, 2026

    @claude
    Contributor

    ⛔ ORDERING GUARD — do not cut this card until PR #15626's state-counts.md conflict is resolved

    From os-sales / session_01G4138K1EG7kQ81FNba5Kp4, 2026-09-05T05:18Z. ⛔ No label, no assignee, no state change — this is a comment only. I am off-shift and this card is the domain:spec seat's (session_01M59rPZZFzqhfMUPFqqZTkf); I am posting it because the maintainer has made me accountable for #15626 reaching merge, and the risk it names is concrete and ordered.

    The maintainer's instruction, on PR #15626 at 5549281398: the conflict fix "必须在切 1/6(#15676)之前做,否则整条栈带着冲突" — it must land before 1/6 is cut, or the whole stack carries the conflict.

    State as measured at 05:17Z: PR #15626 head is still e68ae2b582 (5 commits, unchanged), mergeable_state=dirty. ⇒ The fix has not been pushed yet. This card is still pm:queue and unassigned, so the ordering has not been violated — the window is open, not missed.

    What the fix is

    Exactly one file conflicts against main — packages/spec/liveness/state-counts.md; the other 8 auto-merge. The remedy is one command on the merged tree, named by the merge driver itself:

    pnpm --filter @objectstack/spec gen:liveness-counts
    

    ⚠️ And there is a trap in verifying it. .gitattributes maps that file to merge=os-regen, and scripts/setup-git-hooks.mjs (run by pnpm install) registers the driver per clone. So in an installed clone a plain git merge-tree exits 0 and reports clean — a false negative. To see what GitHub sees:

    git -c merge.os-regen.driver=false merge-tree --write-tree --name-only <main> <head>   # exit 1
    

    Full measurement on the PR at 5549574898. ⛔ I got this wrong myself first and recorded a wrong cause; the correction is in that comment.

    ⇒ Sequence

    1. Regenerate + push 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 first.
    2. Then cut this card (1/6) and the rest of the stack ([#14478 stack 2/6] api/: the 12 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers, ApiError.retryAfter with its own BREAKING note #15677 · [#14478 stack 3/6] kernel/: the 14 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers (runtime-emitted measurements included) #15678 · [#14478 stack 4/6] system/: the 15 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers; metrics.zod.ts size needs an honest name, not the mechanical one #15679 · [#14478 stack 5/6] data/ · ui/ · ai/ · integration/: the 7 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers #15680 · [#14478 stack 6/6] widen check:duration-unit-keys from packages/spec/src/** to every workspace package's zod schemas (folds #15642) and convert the one turso offender it finds #15682).

    ⛔ I am not claiming, labelling or dispatching anything here — the lap and this card are yours. If you have already done the regeneration and this comment is stale, ignore it; the PR head SHA is the check.


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim: epic PM for the #14478 subtree, session session_01G4138K1EG7kQ81FNba5Kp4 (os-sales), 2026-09-05T07:55Z.
    Branch: claude/issue-15676-epoch-ms-and-external-vocabulary-exemptions
    Clause-②: yes

    ⚠️ Authorisation — I am not the domain:spec seat. This subtree was delegated to me under the maintainer's direct-dispatch channel, in-session: 「你继续负责跟进这个任务到合并」, 「15626你应该负责修复红啊」, 「我驱动整条栈」, 「用epic 卡派发」. Parent #14478 now carries pm:epic with the session and declared file territory in its body; the announcement and the measurements behind it are on post #6017 at 5550404209. ⛔ If the seat wakes, the subtree goes straight back.

    Why Clause-②: yes, judged from content: this card renames authorable published keys under ADR-0087 conversions (the ruling's own words for the bare-named instants), so the accepted key set moves. That is the accepted-value test #14406 met and #14586 did not.

    ⭐ Branch base — the one thing easy to get wrong here

    ⛔ Do NOT branch from main. The seat's own sequencing on this card: "branches stacked on #15626's; the set lands together when check:duration-unit-keys is green on the stacked head". So this branch starts from claude/issue-14478-duration-unit-in-key-name at head 140e0b266, not from main. Branching from main would produce a PR that cannot be evaluated against the gate this card is teaching.

    State the dev inherits, measured rather than recalled

    ⛔ The predicate is the gate's, not anyone's regex

    Run pnpm --filter @objectstack/spec check:duration-unit-keys on the stacked head. ⛔ Never recount by another rule — the ruling says so explicitly, and this lane has already been burned by a hand census (a 6-line lookahead window that bled across declaration boundaries and inflated the count).

    Sequencing after this card

    2/6–5/6 (#15677–#15680) are defined against what remains after these two exemption classes are declared, and 6/6 (#15682) widens the gate's population beyond packages/spec/src/**. All five are claimed and held at pm:blocked — their true state — and unblock when this card's PR exists.


    Generated by Claude Code

  4. os-sales commented on Sep 5, 2026

    @os-sales
    Collaborator

    Dev claim — 1/6 of the #14478 stack

    Session session_01G4138K1EG7kQ81FNba5Kp4, dispatched by the epic PM for the #14478 subtree, 2026-09-05.

    Branch: claude/issue-15676-epoch-ms-and-external-vocabulary-exemptions
    Clause-②: yes

    Branch base — measured, not assumed. This branch is cut from claude/issue-14478-duration-unit-in-key-name (PR #15626) at head 140e0b266578c1325d96d95294513232520c72ae, ⛔ not from main. Verified: git rev-parse origin/claude/issue-14478-duration-unit-in-key-name = 140e0b266578c1325d96d95294513232520c72ae, and the new worktree's HEAD is the same sha. The empty branch is already pushed as a write-path probe (git push -u exit 0).

    File territory I am taking (the two exemption classes plus the gate that honours them; the directory populations of cards 2/6–5/6 are ⛔ not mine):

    • packages/spec/src/shared/** — the new shared EpochMs primitive schema.
    • packages/spec/src/api/websocket.zod.ts, packages/spec/src/kernel/context.zod.ts, packages/spec/src/kernel/service-registry.zod.ts, packages/spec/src/kernel/startup-orchestrator.zod.ts — the six epoch instants only.
    • packages/spec/src/api/auth-endpoints.zod.ts, packages/spec/src/api/http-cache.zod.ts, packages/spec/src/api/storage.zod.ts, packages/spec/src/shared/http.zod.ts, packages/spec/src/system/object-storage.zod.ts, packages/spec/src/system/auth-config.zod.ts, packages/spec/src/data/driver/postgres.zod.ts, packages/spec/src/system/disaster-recovery.zod.ts — the external-vocabulary markers only.
    • packages/spec/scripts/check-duration-unit-keys.ts and its self-test — the gate learning both classes.
    • the docs generator half, packages/spec/src/migrations/registry.ts (held for this stack), a changeset, and whatever check:generated regenerates.

    ⚠️ Cards 2/6–5/6 touch several of the same files for other keys. I am editing only the keys this card names in those files.

    What I will report: the gate's own before/after offender counts from pnpm --filter @objectstack/spec check:duration-unit-keys on the stacked head, ⛔ never a regex recount. Baseline inherited from the PM claim at 5550415354: 65 offender(s) among 215 duration-shaped numeric key(s) in 762 source file(s); I re-read it on my own branch point before touching anything.


    Generated by Claude Code

  5. 2 remaining items

  6. claude commented on Sep 5, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 15676,
      "status": "done",
      "branch": "claude/issue-15676-epoch-ms-and-external-vocabulary-exemptions",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15814",
      "premise_still_valid": true,
      "summary": "BRANCH BASE, as instructed and verified: cut from `claude/issue-14478-duration-unit-in-key-name` (PR #15626) at `140e0b266578c1325d96d95294513232520c72ae`, never from `main`; the new worktree's HEAD was the same sha and the PR base is that branch. GATE'S OWN BEFORE/AFTER: before `65 offender(s) among 215 duration-shaped numeric key(s) in 762 source file(s)` (identical to the inherited baseline); after `48 offender(s) among 215 duration-shaped numeric key(s) in 769 source file(s) (6 declared EpochMs instant(s), 11 declared externalVocabulary mirror(s))`. Both verbatim from `pnpm --filter @objectstack/spec check:duration-unit-keys`; never recounted by another rule. The denominator holds at 215 on purpose: an exempted key stays in the census and the verdict line counts it, so the exemption is auditable rather than a silent subtraction. 48 AND NOT THE CARD'S PROJECTED 46 -- this is the card's own invited correction, measured. Two of the 13 attributed external-vocabulary keys do not survive verification against their own schema and are left as offenders for their directory cards: (1) `api/DeviceRequestResponse.interval` -- the schema does not mirror RFC 8628 as a set (`code` is not `device_code`, `verificationUrl` is not `verification_uri`, `expiresAt` is not `expires_in` -- different name AND different type), so it already renames RFC fields to house style and the 'the standard fixes this name' argument is falsified by its own sibling; goes to card 2/6 (#15677). (2) `system/AccessControlConfig.maxAge` -- its siblings are the bucket-CORS-rule vocabulary, and every bucket-CORS standard spells that field WITH the unit (S3 `MaxAgeSeconds`, GCS `maxAgeSeconds`, Azure `MaxAgeInSeconds`), so marking it would exempt a deviation FROM the cited standard rather than a mirror of it; goes to card 4/6 (#15679) as a rename to `maxAgeSeconds`. I demoted rather than marked because the risk is asymmetric and the card says so: a wrongly-marked key is exempted permanently and silently, a wrongly-demoted key is just renamed by its directory card. STACK ARITHMETIC, measured per directory on the final head: api/ 12, kernel/ 14, system/ 15, data+ui+ai+integration 7 = 48. Cards 2/6, 3/6 and 5/6 are UNCHANGED at exactly their stated 12 / 14 / 7. ONLY CARD 4/6 (#15679, system/) MOVES: 13 -> 15. Corroboration for the first demotion comes from the stack's own numbers -- had `interval` been marked, api/ would be 11, one BELOW the 12 card 2/6 carries in its title. WHAT LANDED: (i) `packages/spec/src/shared/epoch.zod.ts` exports `EpochMs` (a `z.number().int()` epoch-millisecond instant) and the gate recognises a key whose value IS that schema as an instant. All six instants moved onto it; the four bare-named ones were renamed to the `*At` convention -- WebSocketEvent.timestamp -> occurredAt, SimplePresenceState.lastSeen -> lastSeenAt, KernelContext.startTime -> startedAt (inherited by TenantRuntimeContext), HealthStatus.timestamp -> checkedAt. `*At` and not `*Ms` was measured, not chosen: on this package's own authorable surface all 51 distinct `*Ms` keys are DURATIONS and all 51 distinct `*At` keys are INSTANTS, so `*Ms` would have moved an instant into the family the rule exists to separate it from. ServiceMetadata.registeredAt and ScopeInfo.createdAt were already correct and changed schema only. (ii) 11 keys carry `.meta({ externalVocabulary })`, each verified against its own docblock; two attributions were CORRECTED -- `stale-while-revalidate` and `stale-if-error` are RFC 5861, not the RFC 9111 the card named (RFC 9111 defines neither). (iii) The docs generator publishes the marker: the reference page now prints '(unit per THE_STANDARD)' -- 23 notes across 9 pages. DISPOSITION: all four renames are tombstoned with `retiredKey()`, registered in RETIRED_KEYS_BY_MAJOR[18] (5 entries, the TenantRuntimeContext walked-shape copy separately) plus ONE D3 semantic entry `epoch-instant-keys-renamed` -- a SEMANTIC entry and not a D2 conversion because all four are runtime-emitted (wire payloads, a host-constructed kernel context, an emitted health report), which is the disposition `kernel/KernelContext:previewMode` already carries on one of these very defs and exactly what ruling B prescribes. Changeset: `@objectstack/spec` minor with a BREAKING banner and disposition `registered epoch-instant-keys-renamed`. NEITHER EXEMPTION IS A GET-OUT-OF-JAIL CARD: a marked key still fails `name-unit-contradicts-prose`, a new `instant-unit-contradicts-schema` refuses an `EpochMs` key whose describe names a non-ms unit, an empty or non-literal marker exempts nothing, and the gate now reads `description` out of `.meta()` so moving a describe there cannot take a key out of the population silently (measured: adds no offender today). ONE TIGHTENING, declared: WebSocketEvent.timestamp and SimplePresenceState.lastSeen were bare `z.number()` and `EpochMs` is `z.number().int()`, so a fractional epoch is now refused at those two sites. LOUD, PER THE DISPATCH: this PR IS GOVERNED. `check-governed-merges.mjs --test` on the final 52-path list exits 3 -- `skills/objectstack-api/references/_index.md`, ONE GENERATED LINE that `gen:skill-refs` adds to index the new `shared/epoch.zod.ts` module, and `check:generated` is red without it. The #11705 generated-surface exception does not lift it because this PR also edits generator scripts. That is the same human-merge regime PR #15626 is already under, so the stack's posture is unchanged, but you should know it: if you want the PR ungoverned, the only lever is putting `EpochMs` inside an existing shared module instead of a new one, and I judged a dedicated module the right home (the card says 'exported from the shared layer beside the other primitive schemas'). Your call -- say the word and I will move it. `needs:contract-review` posted on BOTH carriers (PR #15814 and this card) at PR creation, dual carrier one stroke, via the additive REST endpoint with a comparison read-back: nothing stripped on either. PR is a DRAFT and stays one -- not flipped ready, not enqueued, no auto-merge, not approved. PR #15626's diff, draft flag, labels and body were not touched, nor were the directory populations of cards 2/6-5/6 or the gate's declared population (card 6/6).",
      "tests": "All on final head `c1949b9ac` (`git rev-parse --short HEAD` from the run itself), exit codes captured before any pipe (`cmd > file 2>&1; EXIT=$?`), never read through `| head`/`| tail`. Heavy runs went through `scripts/pm/os-verify-lock.sh -c` with slot `dev15676`; every VERDICT line read command-exit, waited 0s. (1) `pnpm --filter @objectstack/spec test` -- VERDICT command-exit 0, `Test Files 473 passed (473) / Tests 12731 passed (12731)`. An earlier run on an intermediate head had 2 failures, both mechanical and both fixed: the ADR-0122 pin-count assertion (825 -> 826 for the new Iso868) and a stale `export-origins/shared.json`. (2) `pnpm --filter @objectstack/spec typecheck` -- exit 0 (`tsc --noEmit` + check:scripts-typecheck + check:test-typecheck). Its FIRST run was exit 1 with 7 errors in `src/contracts/startup-orchestrator.test.ts` (`Type 'number' is not assignable to type 'undefined'`) -- the `retiredKey()` tombstone refusing the old key at the construction site, i.e. the audible-removal property working on the first consumer it met, and a consumer my `git grep` had missed because it sits under `src/contracts/` not `src/kernel/`. Fixed, re-run green. (3) Gate union on the final head, each exit code read from its own capture: check:generated 0, check:authorable-surface 0, check:docs 0, check:api-surface 0, check:export-origins 0, check:declaration-map 0, check:spec-parsed-alias 0, check:nul-bytes 0, check:migration-registry 0, check:spec-changes 0, check:upgrade-guide 0, check:liveness 0, check:dual-source-exports 0, check:entry-nameability 0, check:exported-any 0, check:declared-population-live 0, check:watch-hint-literal 0, check:self-test-wired 0, check:cross-package-test-inputs 0, check:test-source-alias 0, check:skill-compatibility 0, check:skill-identifier-liveness 0, check:skill-frame-sync 0, check:pm-skill-ratchet 0, check:published-files 0, check-adr-0087-registration 0 (its own verdict line: `registered epoch-instant-keys-renamed (new here: epoch-instant-keys-renamed)`), check-changeset-no-major 0, check-empty-changeset 0. `check:duration-unit-keys` is exit 1 BY DESIGN -- 48 offenders that cards 2/6-5/6 own; the ruling's sequencing is that the set lands together when it reaches zero on the stacked head. NOT MEASURED, not green: I ran `pnpm check:skills-token-ratchet` and got exit 254 `ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command not found` -- no such script exists; that is a typo of mine, not a failed measurement. The real skill gates are the five listed above, all 0. (4) Gate self-test: 37 cases, all pass (17 new), via `check:duration-unit-keys`'s own `--self-test` leg. (5) Derived family: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` exit 0, first line naming this checkout and commit; it derived 165 gate commands because the three-dot change set against `origin/main` necessarily includes PR #15626's own 9 files (this is a stacked branch). I did not run all 165 -- CI runs the farm exactly once. I ran the card-named family plus every gate my own 52-path diff implicates, listed above. ABLATION -- both halves, each done FROM A COMMITTED STATE so the restore leg had a real restore point, each with an absolute-path `trap '...' EXIT INT TERM`, each mutation PROVEN ON DISK by `git hash-object` against the `HEAD` blob before any measurement, and each restore PROVEN by `git diff HEAD` empty PLUS a hash match -- never by an exit code. NO REBUILD WAS NEEDED and here is why, stated rather than assumed: both mutated files (`scripts/lib/schema-section.ts`, `scripts/check-duration-unit-keys.ts`) are loaded from SOURCE by vitest and by `tsx` respectively, never through a package `exports` map into `dist/`, so there is no stale-artifact leg for a false green to hide in. Ablation A (docs half): deleting the `externalVocabularyNote(prop)` term from the description cell -- PREDICTED before running that 3 of the 5 new cases would go red and 2 would stay green; MEASURED exactly that (`Tests 3 failed | 34 passed`), the 2 survivors being the ones that assert the note's ABSENCE. Ablation B (gate half): breaking `INSTANT_ROOT` and deleting the marker branch -- 6 self-test cases red including the `EpochMs`-is-exported liveness pin, and the tree reading moved 48 -> 59 offenders among 215 -> 209 sites. The DIRECTION is worth recording because it is not the obvious one: breaking the instant root does NOT turn the six instants into offenders, it makes them INVISIBLE (they leave the population as an unresolvable root). That silent-failure mode is precisely why the export-liveness self-test case exists, and it is the case that went red. DECLARED NARROWING (one, and it is a measurement rather than a skip): downstream typecheck was scoped to `@objectstack/spec` rather than the ~70 consumers of `...@objectstack/spec`. Three pieces of evidence, not an assertion: (a) every consumer of the four renamed keys was enumerated with `git grep` over `packages apps examples` and each hit was READ, not counted -- the only matches outside `packages/spec` are unrelated local symbols in `packages/core` (`PluginStartupResult.startTime`, `PluginHealthReport.timestamp`, both local interfaces with their own shapes); (b) the renames are TOMBSTONED, so a missed consumer is a compile error and not a silent strip -- demonstrated, since that is exactly how the one consumer I had missed surfaced; (c) CI runs the full typecheck farm regardless. I did NOT run the repo-wide `pnpm lint`.",
      "mcp_calls": "3 -- one claim comment, one create_pull_request, one report comment. Every read (card body, comments, labels, PR read-back) and the label writes went through the repo-scoped REST channel, which I probed first (`GET /repos/...` HTTP 200, `GET /user` HTTP 200 for the write leg).",
      "open_questions": [
        {
          "question": "This PR is GOVERNED by one generated line -- `skills/objectstack-api/references/_index.md`, which `gen:skill-refs` adds to index the new `shared/epoch.zod.ts` module, and which `check:generated` refuses to be green without. Accept it, or move `EpochMs` into an existing shared module so no new source file exists and the skill index does not move?",
          "options": [
            "A -- Accept the governance. The stack is already under human merge (PR #15626 is governed on skills/** too and the ruling keeps it a draft for human merge), so this costs the stack nothing new, and `shared/epoch.zod.ts` is the home the card names ('exported from the shared layer beside the other primitive schemas'). One generated line, in the same PR as its cause, is what check:generated asks for.",
            "B -- Co-locate `EpochMs` in an existing shared module (no new file, no skill-index line, PR ungoverned). Costs: no shared module here is a natural home for a numeric time primitive -- identifiers.zod is identifier grammars, value-domain.zod is closed STRING domains (timezone/currency/country) -- so the schema would live somewhere a reader would not look for it, purely to dodge a governance bit.",
            "C -- Split into two PRs as the gate's own message suggests (generator change, then artifact regeneration). Costs: a second PR for one generated line, and it breaks this stack's 'lands together' sequencing for no review benefit that the human merge does not already provide."
          ],
          "recommendation": "A. On the four axes: real business need -- the governance bit changes nothing about how this stack lands, since human merge is already its regime; long-term soundness -- putting a shared primitive in the module a reader expects is the contract-first answer and B is a workaround aimed at a gate rather than at a problem; making it structurally harder for an AI to get this wrong -- a discoverable `shared/epoch.zod.ts` beside the other primitives is exactly what makes an author reach for the declared instant instead of writing a bare `z.number()` again; startup-stage non-proliferation -- A adds one generated line and no process, C adds a whole PR. I am flagging it rather than deciding it because the dispatch told me to say so loudly if the card required a governed surface, and it did."
        }
      ],
      "out_of_scope_findings": [
        "NOT FILED, reported to you instead, because both fall inside an already-queued card's declared scope and the rule is to hang findings on the card that owns them rather than scatter new ones: (1) `packages/core/src/kernel.ts` `PluginStartupResult.startTime` is assigned a DURATION (`startTime: duration` where `duration = Date.now() - startTime`, twice, lines ~694 and ~704) -- the exact defect class #14478 is about, wearing an instant's name while holding an elapsed count. It is outside `packages/spec/src/**` so today's gate cannot see it, and card 6/6 (#15682) is the card that widens the population beyond that subtree, so it is #15682's to catch. Say the word if you want it filed separately. (2) Card 4/6 (#15679) needs its title count corrected from 13 to 15 -- measured, and the reason is in the PR body's 'Two attributions that did not survive verification'. That is a PM edit on a card I do not own, so I did not touch it."
      ]
    }

    Generated by Claude Code

  7. claude commented on Sep 5, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 15676,
      "status": "done",
      "branch": "claude/issue-15676-epoch-ms-and-external-vocabulary-exemptions",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15814",
      "premise_still_valid": true,
      "summary": "Follow-up lap on the existing PR — no new card, no second PR: pushed c1949b9ac..960580850 onto the existing branch. Removed the '(#14478 ruling B)' parenthetical from the four retiredKey() prescription strings check:doc-authoring flagged, keeping each sentence actionable (the FROM to TO rename, that the value is unchanged, and the surface caveats it already carried: the PresenceState.lastSeen neighbour note and the '*At' versus 'startTimeMs' rationale), matching the shape hook.timeout / job.timeout / DriverOptions.timeout already use. The #14478 anchor is untouched in the adjacent // and /** */ comments — the id count in tracked sources went 95 to 91, exactly the four flagged strings and no comment. check:generated then proved content/docs/references/** stale, so it was regenerated with the repo's own tooling (pnpm --filter @objectstack/spec gen:docs, never hand-edited) and the regenerated diff is exactly the six prescription rows. Final diff is 6 files, content/docs/releases/ contributes zero, and no key name, .describe(), .meta(), schema shape, tombstone target, ADR-0087 disposition, RETIRED_KEYS_BY_MAJOR row, conversion or changeset moved. PR left as draft — not flipped ready, not enqueued, not auto-merged, not approved: it is a governed-surface PR a human lands.",
      "tests": "Prerequisites first, so no reading below is a prerequisite-not-met void: pnpm install (exit 0) then pnpm --filter \"@objectstack/spec...\" build (exit 0, check-dts-emitted 34/34 declaration files present) in a dedicated worktree on the branch at c1949b9ac. Every exit code captured BEFORE any pipe (cmd > out 2>&1; EXIT=$?), never through head or tail. || check:doc-authoring BEFORE (installed and built tree at c1949b9ac) — exit 1, 4 findings, verbatim: [packages/spec/src/api/websocket.zod.ts:484  #14478  [retiredKey()] -> '(#14478 ruling B) — the event INSTANT now carries the shared `EpochMs` schema, '] ; [packages/spec/src/api/websocket.zod.ts:523  #14478  [retiredKey()] -> '(#14478 ruling B) — the last-activity INSTANT now carries the shared `EpochMs` '] ; [packages/spec/src/kernel/context.zod.ts:33  #14478  [via START_TIME_RENAMED] -> '(#14478 ruling B) — the boot INSTANT now carries the shared `EpochMs` schema, which '] ; [packages/spec/src/kernel/startup-orchestrator.zod.ts:107  #14478  [retiredKey()] -> '(#14478 ruling B) — the instant the check RAN now carries the shared `EpochMs` ']; summary line: 4 string(s): tombstone 4. || check:doc-authoring AFTER (committed head 960580850) — exit 0, verbatim: [doc authoring guard: 394 files clean, no bare metadata literals] ; [doc authoring guard: 46 published skill files clean, no internal issue-id references] ; [doc authoring guard: 14871 customer-facing string(s) across 768 spec sources clean, no internal issue-id references (message 1191, strictObject 3535, tombstone 992, describe 8732, functionBuilt 254, functionDeclared 167)] ; [doc authoring guard: sibling-package prose ids hold the baseline, 831 pinned site(s) across 231 file(s), 85224 string(s) read in 1155 parsed source(s), no growth, no burn-down unrecorded]. || check:duration-unit-keys — UNMOVED. Its own verdict line at 960580850, verbatim: [✗ check:duration-unit-keys — 48 offender(s) among 215 duration-shaped numeric key(s) in 769 source file(s) (6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)):]. Exit 1 is the epic's remaining work on cards 2 through 6, not a regression; the pin is the two counts and the exemption header, and all of it matches the expected reading exactly. || pnpm --filter @objectstack/spec test — exit 0: Test Files 473 passed (473), Tests 12731 passed (12731). Run through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 400s, waited 0s). || pnpm --filter @objectstack/spec typecheck — exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck; VERDICT command-exit 0, held 65s). || check:generated — first run exit 1: 1 of 15 artifact(s) stale, content/docs/references/**. Regenerated with the repo tooling pnpm --filter @objectstack/spec gen:docs (exit 0, Generated 231 files); the resulting git diff is exactly 3 mdx files carrying exactly the 6 prescription rows, so the staleness was caused entirely by this edit and no pre-existing churn rode along. Re-run: exit 0, All 15 generated artifacts are up to date. No generated artifact was hand-edited. || check:skill-examples — the first run was a NON-READING, neither pass nor finding: the gate refused to reach a verdict because packages/client-react/dist held no .d.ts declarations. Built that closure (exit 0), re-ran: exit 0, 257 prose examples type-check across 3 surface(s), every marked block parsed so tsc ran the SEMANTIC pass on all of them. || check:nul-bytes — exit 0, scanned 7632 text file(s), no raw ASCII control bytes; plus a direct grep -naP control-byte scan over all 6 edited files, zero hits. || Substitution safety: applied by an anchored script that asserts exactly 1 match per replacement and writes NOTHING if any count differs — all four reported \"ok 1 match\". Mutation then confirmed ON DISK rather than from the editor exit code: per-file #14478 counts went websocket 6 to 4, context 3 to 2, startup-orchestrator 3 to 2, repo-wide tracked sources 95 to 91, and every surviving occurrence in those three files was re-listed and is a // or /** */ comment. || Diff confinement: git diff --name-only c1949b9ac lists exactly packages/spec/src/api/websocket.zod.ts, packages/spec/src/kernel/context.zod.ts, packages/spec/src/kernel/startup-orchestrator.zod.ts plus the 3 regenerated content/docs/references pages; content/docs/releases/ contributes 0 files; nothing on card 2/6 (claude/issue-15677-api-duration-unit-in-key-name) or any other PR was touched. || SCOPE DECLARED, not silently narrowed: heavy CI does not report on this PR (lint.yml and ci.yml both declare pull_request branches [main] while this PR's base is claude/issue-14478-duration-unit-in-key-name), so these local readings are the only evidence and I did not wait on CI. I ran the derivation (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, exit 0): it reports 151 families for the PR's full 114-path change set, which is the prior lap's already-measured surface. This lap's working-tree delta is 3 spec sources plus their generated mirrors, so I ran the prescription-reading gates named above instead of re-running the whole family. The derivation also printed a STALE TREE warning (this tree is at least 34 commits behind origin/main), expected for a PR stacked on a feature branch.",
      "mcp_calls": "0 — every GitHub read and write this lap went through repo-scoped REST (probe: GET /repos/objectstack-ai/objectstack returned HTTP 200 with the container token), plus git for the branch and the push. No MCP GitHub call was made.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED, handed to PM with the reasoning rather than manufactured into a card: pnpm --filter @objectstack/spec gen:docs prints \"Warning: Schema directory packages/spec/json-schema/{conversions,meta-spelling,migrations} does not exist\" on every run. It is pre-existing on this head, independent of this change, the generator still exits 0 and check:docs is green — those source dirs simply emit no JSON schemas. I judge it cosmetic generator noise rather than a defect; say the word and I will file it as a finding-labelled card."
      ]
    }

    Generated by Claude Code

  8. os-sales commented on Sep 5, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
      "issue": 15676,
      "status": "done",
      "branch": "claude/issue-15676-epoch-ms-and-external-vocabulary-exemptions",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15814",
      "premise_still_valid": true,
      "summary": "Follow-up lap on the existing card-1/6 branch, pushed 960580850 -> 5b1b5308e; no new PR, no ready flip, no label/state/auto-merge change. Two hand-kept count claims that epoch.zod.ts and its generated page had moved are corrected WITH the prose beside them (defect #15904): packages/spec/llms.txt shared row 13->14 with 'Epoch Instant (EpochMs)' added to that row's Key Schemas characterisation, llms.txt inventory heading 207->208, and content/docs/getting-started/quick-reference.mdx 'Shared Protocol' (5 of 8)->(5 of 9) — M only, N stays 5 and no row was added because that page declares its tables a curated subset and the gate calls closing the gap a decision, not a fix. Plus the PM's added scope: four JSDoc @example blocks that still taught keys this branch tombstoned, each classified in its own context. Four files, seven lines; content/docs/releases/ contributes zero; no key name, describe, meta, schema shape, tombstone, disposition, RETIRED_KEYS_BY_MAJOR row, conversion or changeset was touched, and nothing on cards 2/6 or 3/6's branches. All edits were anchored substitutions with a per-anchor expected occurrence count, aborting before any write on a mismatch, then re-asserted on disk.",
      "example_sites": [
        "packages/spec/src/api/websocket.zod.ts:450 — was 'timestamp: Date.now()', now 'occurredAt: Date.now()'. In WebSocketEventSchema's own docblock (@example 'Subscribe to channel'); that schema's timestamp is a retiredKey tombstone and the live key is occurredAt.",
        "packages/spec/src/api/websocket.zod.ts:460 — was 'timestamp: Date.now()', now 'occurredAt: Date.now()'. Same docblock, @example 'Data change notification'; same schema, same rename.",
        "packages/spec/src/kernel/startup-orchestrator.zod.ts:84 — was '\"timestamp\": 1706659200000,', now '\"checkedAt\": 1706659200000,'. HealthStatusSchema's @example; live key checkedAt at :102, tombstone at :105.",
        "packages/spec/src/kernel/startup-orchestrator.zod.ts:140 — was '\"timestamp\": 1706659200000', now '\"checkedAt\": 1706659200000'. CHANGED, and here is the check the PM asked for: it is PluginStartupResultSchema's @example, nested under \"health\", and that field is declared 'health: HealthStatusSchema.optional()' at :176 — so it denotes the same renamed key. Its sibling \"duration\" in that example is a live key on this branch and was left untouched.",
        "No fifth site: re-scanning this branch's own changed sources for comment-position occurrences of timestamp / lastSeen / startTime / ttl / timeout / duration found no other @example teaching a retired key (the remaining hits are tombstone prescriptions and rename notes, correct as written); SimplePresenceState's example already read lastSeenAt."
      ],
      "tests": "BUILT FIRST (stated because exit 3 is a void reading): pnpm --filter '@objectstack/spec^...' build matched no projects (spec has no workspace deps), then pnpm --filter @objectstack/spec build EXIT=0, 'check-dts-emitted: 34/34 declared declaration file(s) present', re-run after the source edits. Every exit code captured before any pipe (cmd redirected to a file, then EXIT=$?); no reading taken through head or tail. BEFORE, on 960580850: check:llms-txt EXIT=1 — 'packages/spec/llms.txt:94  [count] domain `shared` declares 13 schemas; src/shared/ holds 14' and 'packages/spec/llms.txt:80  [count] heading declares 207 schemas; packages/spec/src/ holds 208'; check:quick-reference-counts EXIT=1 — 'content/docs/getting-started/quick-reference.mdx:209  [total] section \"Shared Protocol\" declares \"of 8 schemas\" but content/docs/references/shared/ publishes 9 page(s)'. AFTER, on 5b1b5308e (same tree, clean working dir): check:llms-txt EXIT=0 — 'OK packages/spec/llms.txt: 97 claim(s) re-derived — every advertised symbol resolves against api-surface/ (17 entry points), every subpath is a real `exports` key, the inventory matches src/ (14 domains, 208 schemas) and the package table matches the workspace (68 published).'; check:quick-reference-counts EXIT=0 — 'OK content/docs/getting-started/quick-reference.mdx: 13 section(s), every \"(N of M schemas)\" heading matches its table AND content/docs/references/ (14 categories, all sectioned or declared).' check:duration-unit-keys EXIT=1, unchanged and expected (the campaign's remaining backlog), verbatim census line: 'check:duration-unit-keys — 48 offender(s) among 215 duration-shaped numeric key(s) in 769 source file(s) (6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)):'. check:doc-authoring EXIT=0 (394 files clean; 14871 customer-facing strings across 768 spec sources clean; sibling baseline holds — not regressed). check:generated EXIT=0 — 'All 15 generated artifacts are up to date', so the docblock edits staled nothing and nothing was hand-edited. check:nul-bytes EXIT=0 (7632 text files). Gates added by grepping for the scripts that actually read my two doc files: check:doc-anchors EXIT=0 (307 fragment links across 411 sources resolve — the edited heading's slug moves with its text and nothing links to it), check:docs-single-h1 EXIT=0, check:published-files EXIT=0, check:error-status-conformance EXIT=0, check:docs-locale-catch-all EXIT=0. pnpm --filter @objectstack/spec typecheck EXIT=0 (incl. check:test-typecheck: 54 files / 261 errors / 145 pinned signatures held). pnpm --filter @objectstack/spec test EXIT=0 — Test Files 473 passed (473), Tests 12731 passed (12731). All heavy runs went through scripts/pm/os-verify-lock.sh (VERDICT lines read, never a bare $?; waited 0s each time). NOT MEASURED, stated rather than implied: no ablation was run this lap; heavy CI does not run on this stacked PR (lint.yml and ci.yml both declare pull_request branches main), so none of the 7 required contexts reports on this head and these local readings are the only evidence. scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack EXIT=0 derives the family for the card's whole 116-path change set rather than this lap's 4 files, and warns its own tree is at least 34 commits behind origin/main. git diff --name-only 960580850..HEAD lists exactly content/docs/getting-started/quick-reference.mdx, packages/spec/llms.txt, packages/spec/src/api/websocket.zod.ts, packages/spec/src/kernel/startup-orchestrator.zod.ts; the same diff filtered to content/docs/releases/ is 0 files.",
      "mcp_calls": "6 — issue_read #15904, search_issues (dedupe, 0 hits), search_issues (control term, 2 hits, so the empty read is real), issue_write (filed #15919), add_issue_comment on PR #15814, add_issue_comment on #15904; this report comment and its readback make 8.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #15919: api-surface/ records a name declared as BOTH a const and a type under '(type)' only — TypeScript merges the two declarations into one symbol and build-api-surface.ts's kindOf tests TypeAlias before Variable — so deleting the value half of any of the 132 such exports in packages/spec/src leaves the shard byte-identical and check:api-surface green. Measured statically (3 shard entries spot-checked, 132-name population, zero duplicate names across all 17 shards, the source ordering quoted); explicitly NOT ablated, and the card says so."
      ]
    }

    Generated by Claude Code

  9. os-sales commented on Sep 6, 2026

    @os-sales
    Collaborator

    Delivered by PR #15814 and landed on main at e9fcd6bbd (via the stack's trunk PR #15626, merged 2026-09-06T03:17Z).

    Verified on main rather than assumed: packages/spec/src/shared/epoch.zod.ts exists, and check:duration-unit-keys reports the two declared exemption classes this card created — 6 declared EpochMs instant(s), 11 declared externalVocabulary mirror(s) — as part of a green verdict.

    Closing as completed; pm:dispatched removed in the same write. needs:contract-review is left as it stands — a gate label is not mine to clear.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions