Skip to content

[Decision] #18124 余下两行只剩改名一条路 —— FileValue.duration(小数秒)与 estimatedMigrationTime(小时)是否走 ADR-0087 改名,仓内已有两次同形先例 #18669

Description

@os-bill

⏱️ 本卡所有读数取自同一动作:2026-09-17T13:30Z。MEASURED / RELAYED 逐条标注 —— 标 MEASURED 的由本席在 origin/main 上第一手重测,标 RELAYED 的来自 #18124 的 dev 报告且未被本席复测。

Governing text: packages/spec/scripts/check-duration-unit-keys.ts 顶部 docblock(三条采纳通道)· docs/adr/0087-metadata-protocol-upgrade-contract.md · #18115 ruling A(决裁批 #134 item 1)· packages/spec/src/migrations/registry.ts 的两条同形先例。

事情

#18115 ruling A 的步骤③(卡 #18124)把十行「真时长」里的八行交付了(PR #18657,已入队)。余下两行被 dev 正确地退回,因为两者都撞上同一堵墙:现有的闭合类型都表达不了它们的单位。

行 实测单位 为什么闭合类型不覆盖
data/field-value.zod.ts FileValue.duration(MEASURED::484 duration: z.number().optional()) 小数秒(RELAYED:外部惯例 HTMLMediaElement.duration / ffprobe;本仓库内无单位成文) DurationSeconds 带 .int() ⇒ 拒 12.34;DurationMs 与惯例差 1000×
kernel/plugin-versioning.zod.ts CompatibilityMatrixEntry.estimatedMigrationTime(MEASURED::188-191 JSDoc 逐字「Estimated migration time in hours」+ z.number().optional()) 小时 词汇表里没有小时类型;卡面 fence「⛔ 无新类型」

⭐ 关键读数(MEASURED),它把这道题从「加类型还是收窄」变成了「改不改名」:门禁的采纳通道有三条,不是两条 —— 它自己的 docblock 逐字写:「carries it in its NAME or in its TYPE, never only in its .describe() prose」,第三条是 .meta({ externalVocabulary })。⇒ 键名自带单位就被采纳,值类型不必是闭合类型。

⇒ 于是两行都有一条不加新类型、不收窄已存储数据的出路:duration → durationSeconds(值仍 z.number(),小数秒合法);estimatedMigrationTime → estimatedMigrationTimeHours。

⭐ 而且这条路本仓库已经走过两次(MEASURED,migrations/registry.ts):window.size → window.durationSeconds;ServiceLevelObjective.period.duration → durationSeconds,其条目原文逐字「Rename the key to durationSeconds; the value (seconds) …」。⇒ 不是新发明,是既有做法。

选项 × 真实代价

做什么 客户可感知的后果 代价
A 改名:durationSeconds + estimatedMigrationTimeHours,各一条 ADR-0087 转换层条目 老数据照常读(转换层重放);新写的元数据里键名自己说单位 两条 ADR-0087 条目落 packages/spec/src/migrations/registry.ts —— 该文件被 #17534 持有(MEASURED:它仍 open + pm:dispatched)⇒ 必须排在它之后
B 上闭合类型:FileValue.duration 取 DurationSeconds 一个写 12.34 秒的生产者会在写的那一刻被拒;而 FileValueSchema 是 ADR-0104 双模已存储内联 blob ⇒ 对可能已存在的行收窄 且 estimatedMigrationTime 仍无解,还得回到 A 或 C
C 给闭合词汇表加新类型:小数秒 + 小时 与 A 对客户等价 #18124 卡面 fence「⛔ 无新类型」⇒ 要维护者亲自解 fence;词汇表从 2 个长到 4 个
D 退役两键 两键在本仓库零生产者、零消费者(MEASURED:文件字段水合写出的形状是 { id, name, size, mimeType, url },packages/objectql/src/engine.ts 自己的注释逐字如此,不含 duration;RELAYED:objectui 侧也无消费者) 退役同样欠 ADR-0087 条目、同样落被持有的那个文件 ⇒ 省不下排期,只省下键

业务直译:A = 「把单位写进名字,像我们前两次那样」。B = 「为了把单位写进类型,接受它开始拒绝小数,并赌没有已存的小数行」。C = 「为这两个单位各造一个新词,并请维护者收回自己设的禁令」。D = 「这两个字段没人用,删掉」。

① 项目长远合理性:A 用的是仓库已经在用的采纳通道与已经走过两次的改名先例 ⇒ 特例不增;C 把闭合词汇表从 2 扩到 4,而扩它的理由是两个零拉动的字段;B 制造一个「声明了单位但会拒绝该单位的自然取值」的字段,这是特例。
② 实际业务拉动:今天没有人撞上 —— 两键在本仓库零生产者零消费者(MEASURED)。⇒ 按模板「零拉动默认 defer 或 remove」,方向定了也应排在有拉动的活后面。
③ 防 AI 犯错:一个叫 duration 的裸数字,正是 AI 照着邻居抄一个数、静默差 1000× 的形状;改名后错的那一刻在键名上就读得出来,且门禁会响亮拒绝矛盾声明(#18657 的消融腿 A/B 已证该门禁真能红)。B 的拒绝也响亮,但它拒的是正确的小数值。
④ 创业阶段不扩散:D(退役)本是 ④ 最偏好的,但它省不下代价 —— 退役同样欠 ADR-0087 条目、同样落被持有的文件;而 FileValue 是发布契约,删一个已发布可 authoring 键的下游面比改名大。⇒ ④ 在这里不推翻 A。

Prior rulings read: duration,filevalue,estimatedmigrationtime,adr-0087 → 4 hits; ADR-0087 D8, ADR-0120 D2, ADR-0120 D7, ADR-0131 D13

⚠️ 逐条读过,没有一条已经答了本题(MEASURED):ADR-0087 D8 管的是 breaking changeset 的 no-migration-prescription 处置类别 —— 即「改名之后台账怎么记」,⛔ 不是「改不改名」;ADR-0120 与 ADR-0131 对 duration / FileValue / estimatedMigrationTime 三个词零命中(亮控:同一把 grep 在 ADR-0087 上读回内容,所以仪器是响的),它们是被共现词 adr-0087 带出来的。⇒ 本卡是决策,⛔ 不是已被裁决过的执行。

推荐 A。自检行:只看①选 A;②③④ 是否翻转:否 —— ② 把它推后(排在 #17534 之后,且零拉动不插队),③ 加强它,④ 本想选 D 但 D 省不下代价。⛔ 字母不改。

回退项:D(退役)。若维护者判「零拉动的已发布键不值得一条转换层条目」,退役比改名更彻底,代价同级。

置信缺口(本席看不见的):① FileValue.duration 的外部消费者 —— 本仓库与 objectui 内为零是测过的,但这是发布契约,仓外用它的人本席看不到;② 是否真有已存储的小数 duration 行 —— 本席没有生产库可查,B 的收窄风险因此只能定性、⛔ 不能定量。

裁后执行段

裁 A ⇒ 本席在 #17534 释放 migrations/registry.ts 后立一张执行卡(⛔ 不重开 #18124):两条改名 + 两条 ADR-0087 转换层条目 + 两处 .zod.ts 改名 + 生成物重生成,Part of #18124;#18124 在那一笔之后才收口。
裁 D ⇒ 同样排在 #17534 之后,执行卡改为两条退役条目(retiredKey() 墓碑,按 #17502 ⛔ 不加 title)。
裁 B 或 C ⇒ 本席回到 #18124 补派,并在派发令里写明所解的 fence 原话。

维护者速读

事情。 规格里有两个「时长」字段,单位只写在注释里或哪里都没写:一个是文件的时长(网上通行的写法是小数秒),一个是「预计迁移工时」(单位是小时)。我们这批把其它八个字段的单位写进了合约,这两个卡住了 —— 现有的两种「带单位的类型」是整数毫秒和整数秒,装不下小数秒,也装不下小时。

发现。 规则其实还有第三条路:键名自己带单位就算数。把 duration 改叫 durationSeconds、把 estimatedMigrationTime 改叫 estimatedMigrationTimeHours,不用造新类型,也不用让字段开始拒绝小数。这条路我们已经走过两次,台账里有记录。

代价。 改名要一条「老数据怎么继续读」的台账条目,而那个台账文件现在被另一张在做的卡占着,所以这件事得排在它后面。另外:这两个字段今天没有任何代码在写、也没有任何代码在读,所以不着急。

你要做的。 选一个字母:A 改名(推荐)· B 上整数类型(会拒绝小数,且对可能已存在的数据收窄)· C 给词汇表加新类型(需要你收回「不加新类型」那条禁令)· D 退役这两个字段。


Part of #18124 · 执行 #18115 ruling A 时长出的残留问题,按巡检 H52 的处方另立新卡而 ⛔ 不在 #18124 上重挂 needs-user-decision(那会让收件箱说不清哪个问题还开着,且 #18124 收口时会把标签连同问题的唯一可见性一起带走)。


Generated by Claude Code

Activity

  1. hotlong commented on Sep 17, 2026

    @hotlong
    Contributor

    Ruling: batch #151 item 4 · letter A (rename FileValue.duration → durationSeconds and CompatibilityMatrixEntry.estimatedMigrationTime → estimatedMigrationTimeHours, each with an ADR-0087 conversion-layer entry; ⛔ no new closed type, ⛔ no narrowing of stored data) · maintainer 「其他同意」 2026-09-17T15:42Z

    Blocked-by: #17534

    Director seat, summon #24, session_01Wj1HUjzyeiBQ8atRf1ZhaL. Presented in detail with the recommendation A (fallback D); the maintainer agreed. Facts (this card, MEASURED rows re-read by the seat): #18115 ruling A's step ③ (#18124) delivered eight of ten genuine-duration rows (PR #18657); the two left over carry units the closed vocabulary cannot express — FileValue.duration (data/field-value.zod.ts:484, fractional seconds by the external convention HTMLMediaElement.duration / ffprobe) and estimatedMigrationTime (kernel/plugin-versioning.zod.ts:188-191, JSDoc 「in hours」). The gate's docblock names a third admission channel — a unit carried in the key name — and this repository has used it twice (window.size → window.durationSeconds; ServiceLevelObjective.period.duration → durationSeconds, migrations/registry.ts). Both keys have zero producers and zero consumers in-repo and in objectui; FileValue is a published contract, and a media attachment's duration is a real domain field (call recordings), so the key stays.

    Ruling — A

    • duration → durationSeconds (value stays z.number(), fractional seconds legal); estimatedMigrationTime → estimatedMigrationTimeHours (value stays z.number()). One ADR-0087 conversion-layer entry each in packages/spec/src/migrations/registry.ts, in the exact shape of the two precedents there (old data reads through the replay; new metadata carries its unit in the name). .describe() on both states the unit; the duration-unit gate admits both by name.
    • Generated artefacts regenerated with the repo's tooling; the gate's --list baseline updated in the same PR.
    • ⛔ B — a closed integer type refuses the correct fractional value and narrows a stored inline blob (ADR-0104); ⛔ C — two new vocabulary types for two zero-pull fields, and the fence 「⛔ 无新类型」 stays; D — retirement costs the same conversion entries and deletes a field the domain uses.
    • Published key rename ⇒ Clause-②: yes; the contract review at tier is the spec lane's (batch Migrate examples to latest protocol: remove deprecated fields and UI config #151 item 3); changeset @objectstack/spec with the ADR-0087 disposition the two precedents carry.

    Four-facet reading: ① zero today, a real field tomorrow; ② the channel and the precedent already exist, no new type; ③ a bare duration is exactly the number a platform user's AI copies from a neighbour and gets wrong by 1000× — after the rename the unit is read off the key; ④ zero new mechanism.

    Execution

    This card is the execution card (⛔ no new card, ⛔ #18124 is not re-opened): needs-user-decision → pm:blocked, Blocked-by: #17534 (that card holds migrations/registry.ts); the unlock sweep returns it to pm:queue when #17534 closes. pm:blocking stays — #18124 closes only after this lands (Part of #18124 on the PR). domain:spec, priority:p3 stay; zero pull means it does not jump the queue.


    Generated by Claude Code

  2. os-steve commented on Sep 21, 2026

    @os-steve
    Collaborator

    回决策箱(维护者指令,skills 席 2 代执行)— 2026-09-21T03:44Z

    出处三件(SKILL.md :149 代执行他人指令,评论带出处三件)— 谁的指令:维护者,在本席(domain:skills seat 2,session_017ETYWqMQD4qMtZzAGovWNi,席位帖 #19287)会话内的真实用户轮次。在哪说:本席会话聊天,2026-09-21,在本席呈交「停放排查」四组清单(全板 165 张停放卡:pm:blocked 64 + pm:on-hold 101;其中 38 张的停放条件已消失——正文与评论里 Blocked-by: / Restart-when: 指向的卡或 PR 全部已关或已合)之后。原话(逐字,⛔ 未翻译、未润色):「还有哪些应该解除停放的你一起排查一下」;对四组清单:「同意」。

    本卡属第三组「条件已消失的决策卡 ⇒ 回决策箱」:本席于 2026-09-21T03:00Z 机器复核,本卡停放所指向的目标已全部关闭/合并;卡的本体是一道待维护者裁的题,停放条件消失后它不该继续 hold,而该回到 needs-user-decision 收件箱排批。

    动作:停放状态标签一笔换为 needs-user-decision;⛔ 本席不代裁、不派发。所属车道席位或总监席在下次决策批前按 SKILL.md 〈决策箱勤务〉补全四棱卡面块与「维护者速读」(存量卡低频子轮回填);H62 在补全前对本卡报行属正常。


    Generated by Claude Code

  3. os-project-manager commented on Sep 21, 2026

    @os-project-manager
    Collaborator

    State repair — director seat, summon #25 (session_012GcsUbuqFGBibkEDMRC1eE), 2026-09-21T04:20Z. Returned to the decision box at 5755100497 because its blocker closed; ⛔ it is not a decision item — ruling A (batch #151 item 4, 5717168926: rename FileValue.duration → durationSeconds and CompatibilityMatrixEntry.estimatedMigrationTime → estimatedMigrationTimeHours, each with an ADR-0087 conversion-layer entry; no new closed type, no narrowing of stored data) stands, and its only blocker #17534 (the holder of packages/spec/src/migrations/registry.ts) closed completed at 2026-09-21T00:19Z (landed through PR #19398, merged). needs-user-decision → pm:queue in this stroke, priority:p3 unchanged (zero measured pull — it sits behind every product P0/P1 by the road order and is taken by the domain:spec seat when it reaches it). The dispatching seat re-reads the registry file's current holders before claiming (git log origin/main -- packages/spec/src/migrations/registry.ts and the open PRs touching it) and follows ruling A's shape verbatim; #18124 (the parent) is released when this lands.


    Generated by Claude Code

  4. 4 remaining items

  5. os-warren commented on Sep 21, 2026

    @os-warren
    Collaborator

    os-dev-report
    {
    "issue": 18669,
    "status": "done",
    "branch": "claude/issue-18669-duration-key-rename",
    "pr": "#19549",
    "head_sha": "ccec0959cdd66f0b6188d33473811a37c2b5216a",
    "session": "session_01UDXER3sdqfeVYpEWZs5mZx",
    "premise_still_valid": true,
    "summary": "Ruling A implemented verbatim: FileValue.duration -> durationSeconds and CompatibilityMatrixEntry.estimatedMigrationTime -> estimatedMigrationTimeHours, both still z.number().optional() with no closed type, no .int() and no narrowing (12.34 is pinned as still parsing). Each old spelling carries a retiredKey() tombstone, because neither enclosing shape is strict: CompatibilityMatrixEntrySchema is a plain z.object that would strip in silence and FileValueSchema is the one deliberate z.looseObject in field-value.zod.ts that would wave the old key through as an unrecognised extra. Two ADR-0087 D3 semantic entries and two RETIRED_KEYS_BY_MAJOR[18] rows were registered; no D2 conversion for either, because FileValueSchema is the ADR-0104 wave-2 expanded READ form (stored form is an opaque sys_file id) and a compatibility matrix is a published plugin manifest that stack.zod.ts declares no collection of. The hours key also gains .describe('Estimated migration time in hours'): renaming alone would have created a NEW check:duration-unit-keys offence (unit-in-jsdoc-not-in-describe, the agreement shape ruled an offence 2026-09-18), so the describe is part of the remedy the gate names, not decoration.",
    "section_1_remeasure": {
    "taken_at": "origin/main tip ef256e6, 2026-09-21T06:17Z; every construct found by NAME, never by the card's 2026-09-17 line numbers",
    "1_FileValue.duration": "HOLDS - field-value.zod.ts:499 duration: z.number().optional(), with no unit token in the name, no .describe(), no .meta({ externalVocabulary })",
    "2_CompatibilityMatrixEntry.estimatedMigrationTime": "HOLDS - plugin-versioning.zod.ts:191 estimatedMigrationTime: z.number().optional(), under the JSDoc at :189 reading verbatim 'Estimated migration time in hours', and no .describe() anywhere on the chain",
    "3_check-duration-unit-keys_docblock": "HOLDS - the phrase 'carries it in its NAME or in its TYPE, never only in its .describe() prose' is verbatim at line 6; the ADMISSION section declares three channels (a closed duration/instant TYPE, a unit token in the key NAME, and the describe prose which admits but never satisfies), with the JSDoc named as the SECOND prose channel read only to refuse, and the retired 25-token name-shape list explicitly NOT an admission channel. The name channel the ruling rests on is real.",
    "4_the_two_precedents": "BOTH PRESENT and read in full - entries/retired-keys/18.system__MetricAggregationConfig__window.size.ts ('Renamed to durationSeconds, NOT to the gate's mechanical sizeSeconds ... Tombstoned with retiredKey(). No D2 conversion') and 18.system__ServiceLevelObjective__period.duration.ts. Their shape (one file per entry, comment block above export const entry, regenerated into registry.ts by gen:migration-registry) was copied, not invented. The prose the dispatch quoted, 'Rename the key to durationSeconds; the value (seconds) is unchanged', lives in the generated reference page for the ai/ConversationAnalytics precedent, which is the closest analogue and was used as the tombstone-wording model."
    },
    "census": {
    "instrument": "git grep with NO pathspec (the measured pathspec trap in this checkout), at ef256e6, 2026-09-21T06:20Z; lines printed, not counted",
    "estimatedMigrationTime": "ZERO runtime producers, ZERO runtime consumers. All 9 lines / 5 files printed: 3 generated doc rows (content/docs/references/kernel/plugin-versioning.mdx 80/194/288), 2 generated ratchet rows (authorable-surface.base.json:3929, authorable-surface/kernel.json:33), 3 test lines (plugin-versioning.test.ts 169/176/424), 1 schema declaration (plugin-versioning.zod.ts:191).",
    "estimatedMigrationTime_lit_control": "git grep CompatibilityMatrixEntry over the same tree with the same instrument returns 39 rows across 13 files - the grep is not silently answering nothing.",
    "FileValue_duration": "ZERO runtime producers, ZERO runtime consumers. The bare token duration is repo-wide noise (628 files), so the instrument is co-occurrence with mimeType (25 files). Source hits: field-value.test.ts:362 (fixture), content/docs/protocol/objectql/types.mdx:1164 (hand prose), docs/adr/0104:459 (ADR prose), shared/duration.zod.ts:60 and duration.test.ts:70 (the #18122 six-row census), .changeset/18122:20. objectql/engine.ts's only duration is a hook-scope comment; client/src/index.ts's are durationMs; runtime's are metric names.",
    "FileValue_duration_lit_control": "The same mimeType-co-occurrence instrument returns packages/objectql/src/engine.ts, packages/client/src/index.ts, packages/runtime/src/dispatcher-plugin.ts and packages/plugins/plugin-approvals/src/approval-service.ts among its 25 - it is reading real source, and their duration hits were each opened and classified rather than counted.",
    "reachability": "MEASURED, not assumed: FileValueSchema appears ONLY in the expanded arm of valueSchemaFor (the stored arm is FileReferenceIdValueSchema, an opaque id string), and git grep \"'expanded'\" over packages excluding packages/spec returns ZERO call sites. Lit control: the same grep inside packages/spec returns hits. So no in-repo write path and no in-repo read path ever parses a value against this shape - which is also why adding the tombstone narrows nothing live.",
    "objectui": "At the pinned sha 87af769e9a3ee28ace099fdd653d3ebd79fe82e2 (git show origin/main:.objectui-sha), readFileValue/readFileValues in packages/fields/src/widgets/file-value.ts read id, name, url, mimeType and size, and NEVER duration. Every duration hit in that tree is a sonner toast timeout, a durationMs field, or a Tailwind transition class. Lit control: git grep FileValue at that sha returns 19 rows, so the instrument reaches the tree."
    },
    "entry_ids_registered": {
    "semantic": [
    "data-file-value-duration-unit-in-key",
    "kernel-compatibility-matrix-estimated-migration-time-unit-in-key"
    ],
    "retired_keys": [
    "data/FileValue:duration",
    "kernel/CompatibilityMatrixEntry:estimatedMigrationTime"
    ],
    "note": "registry.ts regenerated by gen:migration-registry to 228 semantic / 203 retired-key / 181 retired-def after merging origin/main; check:migration-registry reads it as current, so nothing was hand-resolved. Both authorable-surface rows are TOP-LEVEL, so the ratchet moved in both files (the KEY row gains a [RETIRED] suffix, beside the renamed row). authorable-surface.base.json deliberately NOT re-anchored."
    },
    "tests": "Head under test ccec095 (ccec095), after merging origin/main at 61170fa (clean text merge; check:migration-registry reads the result as current). PACKAGE TESTS, real readings: @objectstack/spec 509 files passed (509) / 14887 tests passed + 1 todo (14888); @objectstack/objectql 303 passed (303) / 5050 passed; @objectstack/lint 106 passed (106) / 4039 passed; @objectstack/service-analytics 113 passed (113) / 2411 passed; @objectstack/driver-sql 178 passed + 11 skipped (189) / 2627 passed + 168 skipped; @objectstack/cli 271 passed + 2 FILES failed (273) / 3535 tests passed + 30 skipped + ZERO tests failed. The consumer set is the packages importing valueSchemaFor from @objectstack/spec/data - the only route by which this contract reaches another package, since no package outside packages/spec imports FileValueSchema or CompatibilityMatrixEntrySchema by name. TWO NON-PASSES, NEITHER A RED GATE: (a) the two failing @objectstack/cli FILES are test/published-subpath-console.pin.test.ts and test/published-subpath-hook-body.pin.test.ts, which refuse their own prerequisite in words - 'packages/cli is not built (./dist/index.js is absent) ... Run: pnpm --filter @objectstack/cli build' - PREREQUISITE NOT MET, not a pass and not a failure; (b) @objectstack/runtime is NOT MEASURED - its suite did not finish inside this container's foreground window and was cut at 560s, declared to CI rather than reported green. TYPECHECK: pnpm --filter @objectstack/spec typecheck exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck, the last holding 53 files / 257 errors / 142 pinned signatures in the shrink-only debt ledger, unchanged). REVERSE VERIFICATION against the REBUILT .d.ts (proving consumers read the rebuilt declarations, not a cache): a throwaway probe under packages/objectql/src, type-checked through node_modules so it resolves @objectstack/spec via its exports into dist/. RED leg - duration: 12 and estimatedMigrationTime: 8 produce EXACTLY TWO errors, 'TS2322: Type number is not assignable to type undefined', one per old spelling, at the two probe lines and nothing else. GREEN CONTROL leg - the same file carrying only durationSeconds: 12.34 and estimatedMigrationTimeHours: 8 exits 0, which is also the fractional-second pin the ruling's no-narrowing clause demands. The probe was committed nowhere and its removal was proved BY OBSERVATION - git diff HEAD empty and git status --porcelain empty - never by a delete command's exit code; a trap with an absolute path covered the crash path. Dist positive control beside it: durationSeconds occurs 13 times and estimatedMigrationTimeHours 3 times across the 63 rebuilt .d.ts files. GATE RECONCILIATION: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RAN_RECORD with every command recorded as 'command :: exit code', exit codes captured BEFORE any pipe, reports '109 derived, 108 run, 1 NOT-MEASURED, 0 UNRUN' and exits 0. The single NOT-MEASURED is node scripts/check-plugin-teardown-shape.mjs --self-test, exit 3: its positive-control fixture is pinned to commit 621a487, unreachable in this shallow checkout, so it refuses its own prerequisite rather than reporting a verdict - and it grades the checker, not this diff. 16 ratchet-sensitive families were RE-RUN at the final head ccec095 after the merge (type-check-coverage, type-check-debt, authorable-surface, duration-unit-keys, api-surface, generated, cross-package-test-inputs, test-source-alias, query-options-erasure, strictness-ledger, liveness, docs, spec-changes, upgrade-guide, check-adr-0087-registration --base origin/main, nul-bytes), all exit 0. check:duration-unit-keys reads 205 unit-declaring numeric keys across 2549 source files, ZERO offenders, no baseline; its --list census shows both new keys admitted and SATISFIED: field-value.zod.ts:510 durationSeconds [name: seconds] [prose: seconds], plugin-versioning.zod.ts:198 estimatedMigrationTimeHours [name: hours] [prose: hours]. LINT is a DECLARED NARROWING with its three readings: (1) the universe is read from eslint.config.mjs itself, whose globs are /*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}; (2) npx eslint --no-inline-config --format json over the diff's lintable paths at ccec095 reports 11 files linted, 0 errors, 0 warnings, exit 0 - the same 11 lintable paths the diff touches; (3) the invariance claim is the config's OWN, stated in its header and measured there with a positive control: this repo 'runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file', so no edit here can move a verdict on a file it does not touch. The whole-repo pnpm lint scan remains CI's.",
    "consumer_packages_tested": [
    "@objectstack/spec (the changed package) - 509 files / 14887 tests + 1 todo, all pass",
    "@objectstack/objectql - 303 files / 5050 tests, all pass",
    "@objectstack/lint - 106 files / 4039 tests, all pass",
    "@objectstack/service-analytics - 113 files / 2411 tests, all pass",
    "@objectstack/driver-sql - 178 files + 11 skipped / 2627 tests + 168 skipped, all pass",
    "@objectstack/cli - 271 files pass + 2 files PREREQUISITE NOT MET (cli dist absent); 3535 tests pass, 0 tests failed",
    "@objectstack/runtime - NOT MEASURED, cut at the 560s foreground window; declared to CI"
    ],
    "changeset": ".changeset/18669-duration-key-rename.md, '@objectstack/spec': minor with a BREAKING callout (check-changeset-no-major refuses major; the house convention is a minor carrying the callout). It carries the disposition marker adr-0087: registered data-file-value-duration-unit-in-key, kernel-compatibility-matrix-estimated-migration-time-unit-in-key, verified by check-adr-0087-registration --base origin/main, which prints '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition' and names both ids as new here. It states in plain words what an author must change (rename the key, nothing else) and that a fractional second still parses. skip-changeset does NOT apply: packages/spec's files[] ships the .zod.ts sources, the json-schema tree and the generated surfaces, all of which moved.",
    "labels_written": "ZERO, and that is the rule rather than an omission: the dispatch named no label set, and skip-changeset does not apply because a changeset was written, so the writable intersection is empty. needs:contract-review is the seat's to place, not mine - I did not attach, detach or wait on it.",
    "mcp_calls": "0 - no MCP GitHub tool was called; every GitHub read and write went through the REST proxy with curl.",
    "api_writes": "2 REST writes + 4 git pushes. REST: POST /repos/objectstack-ai/objectstack/pulls (the draft PR, once) and POST /repos//issues/18669/comments (this report). ZERO PATCH on the PR body - it was written once, on the create call, and read back byte-identical apart from a stripped trailing newline, with exactly one footer and the Clause line intact. Git pushes: the empty-branch routing probe, the WIP commit, the regenerated-artifacts + changeset commit, and the origin/main merge commit.",
    "open_questions": [],
    "out_of_scope_findings": [
    "noted, not filed: .changeset/18122-closed-duration-types.md:20 names FileValue.duration among the six rows the #18122 unit set was derived from. That sentence was TRUE when it was measured, and rewriting another card's unreleased changeset rewrites their record, so it is left verbatim. Carrier: the release that ships both changesets - the reader sees the measurement and then the rename, in order.",
    "noted, not filed: docs/adr/0104-field-runtime-value-shape-contract.md:459 prints the wave-1 inline shape as { url, name?, size?, mimeType?, alt?, duration? }. Not swept, on three grounds: ADR prose is a decision record (Prime Directive #13); the measured precedent is that renames do NOT sweep it (after ApiEndpoint.cacheTtl became cacheTtlSeconds in spec 17, docs/adr/0121 still spells cacheTtl twice, while a control grep for five other already-renamed keys across docs/adr/
    returns zero - so the instrument discriminates); and editing docs/adr/** would make this PR Tier H GOVERNED, needing a maintainer approval for a prose line whose rename channel is the ADR-0087 ledger this PR adds. Carrier: none, deliberately.",
    "noted, not filed: node scripts/check-plugin-teardown-shape.mjs --self-test cannot run in a shallow checkout - its positive-control fixture is pinned to commit 621a487. It exits 3 and says so loudly rather than reporting a false pass, which is the gate behaving correctly; the cost is that every shallow-checkout dev seat records it as NOT MEASURED. Carrier: any card that deepens the clone, or the gate's own owner.",
    "noted, not filed: packages/cli's two published-subpath pin tests fail closed when packages/cli itself is unbuilt, which a consumer-side test sweep (building only the ^... closure) hits by construction. The message names the exact remedy, so it is a good refusal, not a defect. Carrier: any card that runs the cli suite from a consumer sweep."
    ],
    "refusals": [
    "Did not adopt DurationSeconds/DurationMs, add .int(), or narrow either value - ruling A forbids it (option B was considered and not chosen), and the fractional-second case is now a test so a later sweep cannot quietly do it.",
    "Did not edit docs/adr/** or another card's changeset (see out_of_scope_findings).",
    "Did not re-anchor packages/spec/authorable-surface.base.json; its baseRev note on a shallow checkout is informational and check:authorable-surface exits 0.",
    "Did not mark the PR ready, enqueue it, merge it or arm auto-merge; did not set the assignee; did not attach or detach needs:contract-review."
    ]
    }


    Generated by Claude Code

  6. os-warren commented on Sep 21, 2026

    @os-warren
    Collaborator

    Contract review

    Served-tier: 114/114 CONTRACT_REVIEW_TIER

    N = assistant request rows in the isolated reviewer's transcript, every one stamped RETIRED-TIER by the harness; 19 distinct requestIds, 19 at tier. ⛔ Not self-reported — the reviewer answered NOT READABLE, an instrument limit rather than a downgrade.

    Head-sha: ccec0959cdd66f0b6188d33473811a37c2b5216a — the PR head had not moved at the last read (draft, needs:contract-review on card and PR, all 39 check-runs completed success/skipped including Lint & Repo Gates, all four Type Check jobs, Build Core, Dogfood Regression Gate, Temporal Conformance, Governed Surface Queue Guard, Check Changeset and Test Core 1–6/6). Reviewed against merge-base 61170fa385da18a04e3fed75e8c65cbff801e008; the local 17-file diff matches the API file list. objectui read only at the pinned 87af769e9a3ee28ace099fdd653d3ebd79fe82e2. Empirical readings were taken in a scratch lab (git archive of head and base packages/spec + scripts/, zod 4.4.3 / typescript 6.0.3 / tsx — no worktree, no checkout, no edit to either shared tree).

    ① Derived judgments

    Accept/reject set, measured by parsing the real head and base modules (FileValueSchema, FileLikeValueSchema, valueSchemaFor, CompatibilityMatrixEntrySchema) at the edges:

    shape · key base head judgment
    FileValueSchema (looseObject) · durationSeconds unknown key → passthrough: 12, 12.34, '12', null, NaN, Infinity all ACCEPT and retained verbatim z.number().optional(): 12, 12.34, 0, -1, 1e308, undefined/absent ACCEPT; NaN, Infinity, '12', null REJECT right — the declaration the ruling ordered; its numeric accept set is byte-identical to the old duration's at base (12, 12.34, 0, -1, 1e308 ACCEPT; NaN, Infinity, '12', null REJECT), so no narrowing of the value
    FileValueSchema · duration finite numbers ACCEPT+retained; NaN/'12'/null REJECT every present value REJECTS with the rename prescription (measured: the issue message begins FileValue.duration` was renamed to `durationSeconds); undefined/absent ACCEPT right — the declared breaking half; retiredKey() is z.never().optional() so absence stays legal
    FileValueSchema · unknown extra foo ACCEPT passthrough ACCEPT passthrough unchanged — proves a bare deletion would have waved duration through as an extra (the tombstone is the only audible route on this shape)
    valueSchemaFor(video,'stored') and the one-arg default form on {url, duration:12} REJECT (expected string) REJECT (expected string) unchanged — the stored arm never accepted an object; stored data is not narrowed
    FileLikeValueSchema on {url, duration:12} ACCEPT REJECT consequence of the union carrying FileValueSchema; correct
    CompatibilityMatrixEntrySchema (plain z.object) · estimatedMigrationTimeHours unknown → ACCEPT and silently stripped (output keys measured: from,to,compatibility,migrationRequired) finite numbers ACCEPT and retained (8, 8.5, 0, -1); NaN, '8', null REJECT right
    CompatibilityMatrixEntrySchema · estimatedMigrationTime finite numbers ACCEPT+retained; NaN/null REJECT every present value REJECTS with the prescription (CompatibilityMatrixEntry.estimatedMigrationTime` was renamed to `estimatedMigrationTimeHours); absent ACCEPT right
    CompatibilityMatrixEntrySchema · unknown foo stripped silently stripped silently unchanged — proves the "bare deletion would strip in silence" claim

    Public exported surface: no export added, removed or renamed — FileValueSchema, FileValue, FileLikeValueSchema, CompatibilityMatrixEntrySchema, CompatibilityMatrixEntry, CompatibilityMatrixEntryParsed keep their names (packages/spec/api-surface/{data,kernel}.json untouched, correct for a snapshot that records names not shapes). Type level: a tsc --noEmit probe against the head sources gives exactly two TS2322 Type 'number' is not assignable to type 'undefined' on duration: 12 and estimatedMigrationTime: 8, and none on durationSeconds: 12.34 / estimatedMigrationTimeHours: 8 — the implementer's red/green legs reproduce. Authorable surface: data/FileValue:duration [RETIRED] + data/FileValue:durationSeconds, kernel/CompatibilityMatrixEntry:estimatedMigrationTime [RETIRED] + …Hours — the same shape as the ai/ConversationAnalytics:duration [RETIRED] precedent in authorable-surface/ai.json; authorable-surface.base.json left un-anchored (ratchet, fine). Reference pages regenerated and consistent with the describes and tombstone text. docs/protocol-upgrade-guide.md and packages/spec/spec-changes.json project only the current protocol (16 → 17); no step-18 entry, precedent or new, appears in either, so no regeneration was owed — CI's Type Check jobs (which run the spec check:generated family) are green at this head.

    Sentences verified true at this head: both values z.number().optional(), fractional second parses (measured); FileValueSchema is the one z.looseObject( in field-value.zod.ts (count 1; strictObject( 3; z.object( 0); CompatibilityMatrixEntrySchema is a plain z.object; FileValue.duration had no .describe(), no JSDoc, no unit token at base (read); the only other number on FileValue is size (shape: url,name,size,mimeType,alt,durationSeconds); "renaming alone would have created a NEW check:duration-unit-keys offence" — verified by ablation (see ③.3); tombstone text "in @objectstack/spec 17" matches the 253 sibling step18 tombstones and spec at 17.4.0 shipping the minor; the <!-- adr-0087: registered … --> ids both exist in registry.ts; the changeset's "one of the six rows #18122 derived its unit set from; the one that takes a name instead of a type" holds (the other five took DurationMs/DurationSeconds or an externalVocabulary exemption); content/docs/protocol/objectql/types.mdx prose now names durationSeconds.

    Sentences that are NOT true of the code, and inconsistent with each other:

    • packages/spec/src/migrations/entries/semantic/18.data-file-value-duration-unit-in-key.ts, the reason string of export const entry (copied verbatim into registry.ts under step18 → data-file-value-duration-unit-in-key, and therefore compiled into the published dist and the future upgrade guide / MCP spec_changes feed): "this spec already spells a length of time durationSeconds in five places — ConversationAnalytics, the two aggregation/SLI windows, the SLO period and the metrics retention window — … instead of inventing a sixth vocabulary". Measured at the merge-base AND at the implementer's own tip ef256e6: six durationSeconds: key declarations in packages/spec/src/**/*.zod.ts — ConversationAnalytics, MetricAggregationConfig.window, ServiceLevelIndicator.window, ServiceLevelObjective.period, ServiceLevelObjective.burnRateWindows[].durationSeconds, MetricsConfig.retention. The enumeration omits the burn-rate window; the media length is the seventh spelling, not the sixth.
    • packages/spec/src/migrations/entries/retired-keys/18.data__FileValue__duration.ts header comment (copied above 'data/FileValue:duration' in RETIRED_KEYS_BY_MAJOR[18]): "ConversationAnalytics, the three system/metrics.zod.ts window lengths and MetricsConfig.retention". metrics.zod.ts carries five durationSeconds keys, and the burnRateWindows element's own JSDoc calls itself "The fourth window length on this file". This list omits ServiceLevelObjective.period.durationSeconds — a different key than the semantic entry omits — so the two surfaces carrying one fact disagree with each other and with the code.

    Lesser imprecision (PR body, not a contract surface): "git grep FileValue at that sha returns 19 rows" — it returns 19 files / 89 rows.

    ② Semver grade vs. the changeset's declaration

    What the diff does: it narrows the accept set (an authored duration / estimatedMigrationTime that parsed at base now refuses) and widens it (two new keys). Under strict semver that is major. The rule this repo applies lives in the header of scripts/check-changeset-no-major.mjs ("LAUNCH-WINDOW GUARD … we ship breaking changes as minor … the mandatory information carriers for breaking-ness are the BREAKING banner and the ADR-0087 disposition", end condition GA), enforced there and by scripts/check-adr-0087-registration.mjs. The changeset .changeset/18669-duration-key-rename.md declares "@objectstack/spec": minor, a feat(spec)!: headline, a **BREAKING** banner, the marker <!-- adr-0087: registered data-file-value-duration-unit-in-key, kernel-compatibility-matrix-estimated-migration-time-unit-in-key --> (both ids present in registry.ts), the FROM → TO table and the one-line fix, and Clause-②: yes. Gates that read it: check-changeset-no-major.mjs level axis (yes requires ≥ minor on a moved package — satisfied), check-adr-0087-registration.mjs (declared breaking → marker present), scripts/pm/check-clause2-carriers.mjs (declaration on the card's claim comment, needs:contract-review on both carriers, PR draft), and the Check Changeset CI job — all green. The level is right under the written house rule (it would be "too low" only under strict semver, which the rule suspends until GA). The Clause-②: yes declaration is right and matches the ruling's own text ("Published key rename ⇒ Clause-②: yes"); the fuller spelling yes (narrowing) (CLAUSE2_ARMS docblock in check-clause2-carriers.mjs: "a diff that widens one surface and narrows another") would state both facts, but the arm is optional by design and no gate requires it — recommended, not a defect.

    ③ Boundary flags

    1. No narrowing — verified at the edges on both keys (table above). One precise statement the PR body does not make: on the looseObject the NEW key durationSeconds did accept '12'/null/NaN at base as an unknown passthrough and now refuses them; that is the ordinary consequence of declaring a z.number() key, not a narrowing of any stored or authored value (no producer ever spelled durationSeconds).
    2. Tombstone rather than deletion — both shape claims verified by reading and by the strip/passthrough rows above.
    3. .describe() as remedy — verified by running the gate itself (packages/spec/scripts/check-duration-unit-keys.ts --root): head packages/spec/src → exit 0, 203 unit-declaring keys, zero offenders, both new keys listed [name: seconds] [prose: seconds] / [name: hours] [prose: hours]; base tree → exit 0, 201 keys, neither old key admitted (invisible to the gate); rename-only ablation (JSDoc "in hours", no describe) → exit 1, exactly one offender [unit-in-jsdoc-not-in-describe] kernel/plugin-versioning.zod.ts … estimatedMigrationTimeHours. The claim is true and the describe is load-bearing.
    4. Reachability zero, re-taken — instrument: git grep over the tracked tree at the head sha. (a) valueSchemaFor( outside packages/spec (packages/apps/examples, CHANGELOGs excluded): 7 call sites, every one passes the literal 'stored' (packages/objectql/src/validation/record-validator.ts, engine-cel-default-temporal-shape.test.ts, examples/app-showcase/test/inert-wirings.test.ts, packages/qa/dogfood/test/field-zoo-value-shape.test.ts ×2) or is a comment; the signature's default is form: ValueForm = 'stored', so a one-arg call cannot select the expanded arm; no ValueForm-typed variable exists outside spec (the spread/computed/re-export shapes have nothing to carry). (b) bare token expanded outside spec: 380 rows, none a valueSchemaFor argument; lit control inside spec 185 rows. (c) FileValueSchema / FileLikeValueSchema / CompatibilityMatrixEntrySchema by name outside packages/spec/src: docs, changesets, ADR, audit ledger, CHANGELOGs and comments only; packages/objectql/src/engine.ts builds the expanded value by spreading sys_file columns into {id,name,size,mimeType,url} — no duration. (d) objectui at the pin: one non-test valueSchemaFor(field, 'stored') in packages/fields/src/index.tsx, one-arg test calls (default stored), FileValueSchema in comments only, readFileValue/readFileValues read id,name,url,mimeType,size; lit control FileValue 19 files / 89 rows; estimatedMigrationTime|CompatibilityMatrix 0 rows. Radius: tracked text at the two shas. Deliberately outside it: packages/console/dist (the built objectui bundle — a minified consumer would not spell valueSchemaFor), objectui's own HEAD, and any out-of-repo consumer of the published @objectstack/spec (the card's own confidence gap ①). Zero stands.
      Falsifier outside the implementer's radius: the PR body's FileValue.duration census used co-occurrence with mimeType; a numeric-literal instrument (\bduration:\s*[0-9], 32 rows repo-wide, each classified) finds packages/drivers/driver-sql/src/sql-driver-numeric-fidelity.test.ts, the round-trips object-valued record/video/audio case: f_video: { url: 'https://cdn/v.mp4', duration: 12 } / f_audio: { …, duration: 30 } written and asserted back. It has no mimeType, so the census could not see it. It passes (the driver stores JSON verbatim and never parses against the expanded arm — which is itself evidence for the zero), but it is a fixture now spelling a retired key, unflagged. Rename it in the fix commit.
    5. Declared non-measurements — @objectstack/runtime: packages/runtime/src has zero references to FileValueSchema, CompatibilityMatrixEntry, estimatedMigrationTime, and valueSchemaFor only in a comment; no duration in a file-value context. No path by which this diff changes runtime behaviour; hole nil in principle, and CI's Test Core (six shards over the --affected set, spec's dependents included) is green at the head — shard membership could not be read from here (job-log download is off the REST proxy), stated rather than assumed. @objectstack/cli: the two refusing files pin the packed tarball's ./console and ./hook-body subpath exports — unrelated to any spec key; refusal was for a missing cli build; CI ran them green. No hole.
    6. Noted, not filed — docs/adr/0104-…md wave-1 sentence printing duration?: accept leaving it — a dated description inside a Tier H record, the precedent (ADR-0121 still spelling cacheTtl) holds, and the rename channel is the ADR-0087 ledger this PR adds; carrier none. .changeset/18122-closed-duration-types.md naming FileValue.duration as a census row: accept — a measurement true at its date, shipped in the same CHANGELOG batch, in order.
    7. Should have been flagged and was not: (a) the precedent miscount in ① — the FAIL grounds; (b) the ruling's execution section (comment 5717168926: "Part of #18124 on the PR") and the director's state repair (5755336165) both prescribe Part of #18124 on the PR; the body carries no 18124 at all — the Part-of closing-keyword guard passed precisely because no Part-of line exists. Fixes #18669 on line 1 is correct (the ruling makes [Decision] #18124 余下两行只剩改名一条路 —— FileValue.duration(小数秒)与 estimatedMigrationTime(小时)是否走 ADR-0087 改名,仓内已有两次同形先例 #18669 the execution card, ⛔ no new card), and Fixes #18669 + Part of #18124 co-exist under the "Part-of PR must not also close its card" guard (different cards). A body edit, no new commit. (c) plugin-versioning.test.ts pins the rename but not the refusal — no test asserts estimatedMigrationTime: 8 fails with the prescription (the FileValue side has one); recommended pin. (d) the Clause-② arm (② above), optional.

    VERDICT: FAIL

    • packages/spec/src/migrations/entries/semantic/18.data-file-value-duration-unit-in-key.ts, the reason string of export const entry (and its generated copy in packages/spec/src/migrations/registry.ts under step18 → data-file-value-duration-unit-in-key): "in five places — … — instead of inventing a sixth vocabulary" is false at this head and at the implementer's measured tip — six durationSeconds key declarations exist, and the enumeration omits ServiceLevelObjective.burnRateWindows[].durationSeconds in system/metrics.zod.ts.
    • packages/spec/src/migrations/entries/retired-keys/18.data__FileValue__duration.ts, the header comment above export const entry (and its copy above 'data/FileValue:duration' in RETIRED_KEYS_BY_MAJOR[18] in registry.ts): "the three system/metrics.zod.ts window lengths and MetricsConfig.retention" contradicts that file's own burnRateWindows JSDoc ("The fourth window length on this file") and omits ServiceLevelObjective.period.durationSeconds, leaving the two entries disagreeing about which precedent is missing.

    Remedy is prose-only: correct both entry files, pnpm --filter @objectstack/spec gen:migration-registry, rename the driver-sql fixture, add Part of #18124 to the PR body, optionally pin the kernel tombstone refusal; everything else in this record is verified and need not be re-derived at the new head beyond the changed files.

    Implemented-by: claude/issue-18669-duration-key-rename (mode:subagent)
    Reviewed-by: session_01UDXER3sdqfeVYpEWZs5mZx


    交接 —— 双载体同笔已剥;两条依据本席第一手复测,成立,且数得比复核更细

    依据一(语义条目 reason)。 本席在 head 上数 packages/spec/src/**/*.zod.ts 里 durationSeconds: 的键声明:

    ai/conversation.zod.ts:314          ConversationAnalytics
    system/metrics.zod.ts:399  :558  :624  :665  :892      ← 五处,不是三处
    data/field-value.zod.ts:510         ← 本 PR 新加的那一处
    

    ⇒ 既存 6 处,本 PR 加的是第 7 处。而条目写「in five places … instead of inventing a sixth vocabulary」——数错了,而且错的方向正好让这句话读起来更有说服力。

    依据二(retired-keys 条目头注)。 它写「the three system/metrics.zod.ts window lengths and MetricsConfig.retention」,而 metrics.zod.ts 上有五处。⭐ 更锋利的是:那个文件自己的 JSDoc 就否证了它——metrics.zod.ts:658 逐字写着「The fourth window length on this file」。⇒ 两条已发布条目彼此矛盾、且都与代码矛盾,而其中一条与同一个文件里的另一句已发布散文矛盾。

    ⚠️ 为什么这两条不是小事:这两段散文被 gen:migration-registry 原样复制进 registry.ts,编进已发布的 dist,并喂给将来的升级指南与 MCP spec_changes 流。一个作者读到的是「本仓已经这样拼了五处」,而真相是六处——这正是本卡要终结的缺陷类(键上写着单位、散文说着别的),只不过这次出现在治它的那条条目自己身上。

    ⭐ 复核找到了一个本席与 dev 都没照到的地方

    dev 的 FileValue.duration 普查用的仪器是与 mimeType 共现。复核换了个仪器(数值字面量 \bduration:\s*[0-9],32 行逐条分类),找到 packages/drivers/driver-sql/src/sql-driver-numeric-fidelity.test.ts 的 round-trips object-valued record/video/audio 用例:f_video: { url: 'https://cdn/v.mp4', duration: 12 }。它没有 mimeType,所以那台仪器结构上看不见它。 它现在仍通过(驱动逐字存 JSON、从不按扩展形解析——这反过来又是那个零的证据),但它是一个正在拼写已退役键的夹具,无人标记。

    ③ 里还有一条本席欠的,已同笔补上

    裁决 5717168926 的「Execution」段逐字写着:

    pm:blocking stays — #18124 closes only after this lands (Part of #18124 on the PR)

    而 PR 正文里一个 18124 都没有。⚠️ 而 Part-of closing-keyword guard 是绿的——恰恰因为压根没有 Part-of 行:一道绿门禁对一件不存在的事保持了沉默。本席已补进正文(正文是席位的,⛔ 不是 dev 的)。

    欠改(照复核的 remedy,全是散文,⛔ 不加码)

    1. 两条条目的计数改对,然后 gen:migration-registry 重生成镜像,⛔ 不手改。
    2. driver-sql 那个夹具改名到新拼法。
    3. 可选(复核标为 recommended、非判据):给 kernel 侧墓碑补一条拒绝断言——FileValue 那半边有,这半边没有。

    ⛔ ② 无异议:minor 在本仓成文的 launch-window 规则下是对的(复核点名了规则住在 check-changeset-no-major.mjs 的头注里,并说明严格 semver 才会读成 major),Clause-②: yes 也是对的。⛔ 卡态与 assignee 不动。

    ⚠️ Redacted 2026-09-21T18:2xZ by the seat. Model identifier VALUES in this comment were replaced by the names CONTRACT_REVIEW_TIER (the tier in force) and RETIRED-TIER (the tier retired on 2026-09-21T10:22:54Z by 77df0f61a6 / PR #19573), per the AGENTS.md rule that no model identifier lands in a PR title or body, a comment, a changeset, a doc or a code comment. ⛔ No judgment, figure, coordinate or verdict was changed. Inventory and the standing rule conflict: #19615.


    Generated by Claude Code

  7. os-warren commented on Sep 21, 2026

    @os-warren
    Collaborator

    os-dev-report
    {
    "issue": 18669,
    "status": "done",
    "round": "rework after at-tier FAIL on ccec095 (review comment 5757920935)",
    "branch": "claude/issue-18669-duration-key-rename",
    "pr": "#19549",
    "head_sha": "7d4575956ef7c1e16fe4a1bd449952339a2f0a52",
    "previous_head_sha": "ccec0959cdd66f0b6188d33473811a37c2b5216a",
    "session": "session_01UDXER3sdqfeVYpEWZs5mZx",
    "premise_still_valid": true,
    "summary": "Both FAIL grounds were prose miscounts in migration-entry text that gen:migration-registry copies verbatim into registry.ts, and both are corrected. I re-counted at the head rather than adopting the number I was handed: seven durationSeconds key declarations in packages/spec/src//.zod.ts, six of them pre-existing, so the media length is the SEVENTH spelling. Both entries now carry the SAME SIX KEYS IN THE SAME ORDER, enumerated rather than counted, and that identity is verified mechanically rather than by eye. The driver-sql fixture the reviewer's instrument found is renamed, the kernel-side tombstone refusal is pinned in the shape this very file already uses for rollout.duration, and the pin is proved falsifiable by ablation. No behaviour changed in this round; the only non-prose edits are a test fixture rename and two added tests.",
    "the_count_i_measured": {
    "instrument": "git grep -nE '^\s
    durationSeconds:' with NO pathspec, filtered afterwards to packages/spec/src/
    /.zod.ts; control on the same instrument: '^\sestimatedMigrationTimeHours:' returns exactly the one declaration this PR adds, so the anchored pattern discriminates",
    "taken_at": "head ccec095 when the correction was written, re-confirmed at 7d45759 after merging origin/main",
    "total_at_head": 7,
    "pre_existing": 6,
    "enumeration": [
    "ai/conversation.zod.ts:314 ConversationAnalytics.durationSeconds",
    "system/metrics.zod.ts:399 MetricAggregationConfig.window.durationSeconds",
    "system/metrics.zod.ts:558 ServiceLevelIndicator.window.durationSeconds",
    "system/metrics.zod.ts:624 ServiceLevelObjective.period.durationSeconds",
    "system/metrics.zod.ts:665 ServiceLevelObjective.errorBudget.burnRateWindows[].durationSeconds",
    "system/metrics.zod.ts:892 MetricsConfig.retention.durationSeconds",
    "data/field-value.zod.ts:510 FileValue.durationSeconds <- the one this PR adds, the SEVENTH"
    ],
    "owning_defs_derived_how": "not read off the line numbers: for each of the five metrics.zod.ts lines the enclosing the enclosing exported schema const and the enclosing parent key were walked out of the source, so the enumeration names defs rather than offsets - MetricAggregationConfigSchema@378/window@387, ServiceLevelIndicatorSchema@492/window@546, ServiceLevelObjectiveSchema@582/period@613, ServiceLevelObjectiveSchema@582/errorBudget@638, MetricsConfigSchema@816/retention@881.",
    "the_falsifier_read": "metrics.zod.ts:658 says verbatim 'The fourth window length on this file, so it takes the same durationSeconds the other three take' - read in place before writing the correction, and now cited IN the retired-key entry so the next reader of that entry is pointed at the sentence that would falsify any shorter count."
    },
    "ground_1_fixed": {
    "file": "packages/spec/src/migrations/entries/semantic/18.data-file-value-duration-unit-in-key.ts (reason)",
    "was": "'in five places - ConversationAnalytics, the two aggregation/SLI windows, the SLO period and the metrics retention window - ... instead of inventing a sixth vocabulary' (omitted burnRateWindows)",
    "now": "'in SIX places, and they are ENUMERATED rather than counted because a bare number in shipped prose cannot be re-checked against the tree' followed by all six dotted key paths, closing 'The media length is therefore the SEVENTH spelling of one vocabulary, not the first of a second one.'"
    },
    "ground_2_fixed": {
    "file": "packages/spec/src/migrations/entries/retired-keys/18.data__FileValue__duration.ts (header comment)",
    "was": "'(ConversationAnalytics, the three system/metrics.zod.ts window lengths and MetricsConfig.retention)' (omitted ServiceLevelObjective.period - a DIFFERENT key than ground 1 omitted, which is why the two entries disagreed with each other)",
    "now": "the identical six-key enumeration, plus a pointer to metrics.zod.ts's own burnRateWindows JSDoc ('The fourth window length on this file') as the sentence that falsifies any shorter count of that file."
    },
    "the_two_entries_now_agree": "VERIFIED MECHANICALLY, not by eye: a regex over both files pulls every dotted path ending in .durationSeconds, in source order, with backticks stripped, and the two lists compare EQUAL as ordered sequences - six items each, identical, same order. The cross-reference sentence each entry carries was also weakened from 'word for word' to 'the same six keys in the same order', because the two files punctuate differently (the comment uses backticks, the string does not) and a claim of byte-identity would have been the same class of defect one level up.",
    "driver_sql_fixture": {
    "finding": "packages/drivers/driver-sql/src/sql-driver-numeric-fidelity.test.ts, the 'round-trips object-valued record/video/audio' case, wrote and asserted back f_video/f_audio carrying the retired duration key. Renamed to durationSeconds on all FOUR lines (122, 123 written; 131, 132 asserted) - the reviewer named f_video; f_audio is the same shape on the next line and moves with it.",
    "lesson_carried": "a census keyed on a COMPANION FIELD inherits that field's absences. My instrument was co-occurrence with mimeType and this fixture carries none, so it was structurally invisible - not missed, unreachable. The reviewer's numeric-literal instrument is the one that sees it.",
    "instrument_re_run_after_the_fix": "git grep -nE '\\bduration:\\s*[0-9]' now returns 28 rows, down from 32; the four that moved are exactly the driver-sql ones. Every remaining row was classified: toast timeouts in docs, a CLI JSON field, a wait action param, BaseResponse.meta.duration (a LIVE key), Span.duration, and deliberate retired-key REFUSAL pins (ConversationAnalytics, rateLimit, SLO period, rollout, and this card's own FileValue pin). None is a fixture spelling a retired key.",
    "one_re_checked_rather_than_assumed": "plugin-versioning.test.ts:498 rollout: { strategy: 'canary', duration: 3600000 } looked like the same defect in the file I was editing. Read it: it is card #15678's deliberate refusal pin for the retired MultiVersionSupport.rollout.duration, asserting the rename message and code !== 'unrecognized_keys'. Correct as it stands - and it is the house pattern I then adopted for the new kernel pin."
    },
    "recommended_item_taken": {
    "what": "the kernel-side tombstone refusal is now pinned, and so is the no-narrowing half: estimatedMigrationTimeHours: 8.5 still parses.",
    "written_how": "first draft asserted over JSON.stringify(error); replaced with the path-scoped form this same file already uses for rollout.duration - find the issue whose path is estimatedMigrationTime, assert its code is not unrecognized_keys, assert its message carries the rename. A refusal that arrived as a bare unknown-key error can no longer satisfy the pin by happening to mention the key name in the envelope.",
    "proved_falsifiable_by_ablation": "NOT by reading. scripts/ablation-replace.mjs --delete removed the retiredKey() tombstone block from plugin-versioning.zod.ts and ran the file. On-disk proof printed by the tool: anchor 1 -> 0, blob a65b9b44cb71 -> b4c11e8bfca5. Result: 1 failed | 28 passed (29) - only the new pin went red, and it failed on expect(retired.success).toBe(false) receiving TRUE, i.e. the old spelling parsed green and was silently stripped. That is the 'a bare deletion would strip in silence' claim in the entry, demonstrated rather than asserted - and the 28 still-green siblings are why the claim matters. Restore leg: blob back to a65b9b44cb71 == HEAD blob, git diff HEAD empty, git status --porcelain empty, verified by observation and not by the tool's exit code; an absolute-path trap covered the crash route. The ablation was re-run after the pin was strengthened and reproduced identically. No test file was left behind.",
    "resolution_path_stated": "the subject resolves through a RELATIVE import (./plugin-versioning.zod) inside the same package, so this ablation reads source, not dist - no build was owed and none was skipped."
    },
    "merge": "origin/main advanced 4 commits during this round (to b3615f1, including #19346 which lands in packages/spec) and was merged in: clean, no conflict markers hand-edited. check:generated --fix then rebuilt spec's dist and reported 'All 15 generated artifacts are up to date', writing nothing - so the merge owed no regeneration.",
    "tests": "Final head 7d45759 (7d45759), merge-base origin/main b3615f1. @objectstack/spec: 509 files passed (509) / 14892 passed + 1 todo (14893) - up from 14887 at the failed head, the delta being this round's two new kernel pins plus the tests the merge brought in. @objectstack/driver-sql: 178 passed + 11 skipped (189) / 2627 passed + 168 skipped (2795) - IDENTICAL counts to the pre-rename run, which is the evidence that the fixture rename is behaviour-neutral, exactly as the reviewer predicted (the driver stores JSON verbatim and never parses the expanded arm). typecheck: pnpm --filter @objectstack/spec typecheck exit 0. GATE RECONCILIATION at this head: 110 derived (one more than last round - pnpm check:driver-conformance, pulled in by the new packages/drivers path), every one run and recorded as 'command :: exit code' with the code captured before any pipe; dispatch-gates --ran reports '110 derived, 109 run, 1 NOT-MEASURED, 0 UNRUN' and exits 0. The one NOT-MEASURED is again node scripts/check-plugin-teardown-shape.mjs --self-test, exit 3, its positive-control fixture pinned to an unreachable commit in this shallow checkout. ⚠️ One gate that was green last round came back exit 3 this round and was NOT accepted as such: check:type-check-debt refused its prerequisite because the recreated worktree had no built dependency closure ('25 workspace dependencies ... have no built type entry point on disk'; the gate states that measuring from there would measure a DIFFERENT WORLD, packages/lint reading 19 built vs 147 unbuilt). I built the whole workspace closure (turbo run build over ./packages/*, 72 tasks, 7m14s) and re-ran it: exit 0, '4 ledger entries re-measured in 22.0s, 53 raw tsc errors total, none above its recorded number', and check:type-check-coverage exit 0 beside it. LINT, same declared narrowing as last round with its three readings, re-taken at 7d45759: (1) universe from eslint.config.mjs's own globs; (2) --format json reports 12 files linted, 0 errors, 0 warnings, exit 0 - 12 rather than 11 because the driver-sql fixture joined the diff; (3) the invariance claim is the config's own header, measured there with a positive control, that this repo never enables type-aware linting for any file.",
    "what_i_did_not_touch": "The PR BODY, as instructed - Part of #18124 is the seat's edit and is confirmed present beside Fixes #18669 at this head. No label written, attached or detached (needs:contract-review is on the PR and stays). Assignee untouched. PR still draft, auto_merge null, not enqueued. ② was not revisited: minor and Clause-②: yes stand as the review settled them.",
    "mcp_calls": "0 - no MCP GitHub tool called; the only GitHub reads and the one write went through the REST proxy with curl.",
    "api_writes": "1 REST write + 3 git pushes this round. REST: POST /repos//issues/18669/comments (this report). ⛔ Zero writes to the PR - no PATCH on the body, no label call, no state change. Git pushes: the prose correction + fixture rename + first pin (3390bc9), the strengthened pin (25f467e), and the origin/main merge (7d45759).",
    "open_questions": [],
    "out_of_scope_findings": [
    "noted, not filed: check:type-check-debt exits 3 rather than skipping when the workspace closure is unbuilt, which every freshly-created worktree hits. The refusal is correct and names its remedy verbatim - the alternative would be a confidently wrong number - but it means a dev seat that tears its worktree down and rebuilds it pays a 7-minute whole-workspace build to get one ratchet reading. Carrier: any card that touches the dev-loop cost of that gate.",
    "noted, not filed: node scripts/check-plugin-teardown-shape.mjs --self-test still cannot run in a shallow checkout (fixture pinned to 621a487). Unchanged from the previous round, re-recorded rather than dropped. Carrier: any card that deepens the clone, or the gate's owner.",
    "noted, not filed: the two entry files now carry one fact in two places and nothing but this report's regex holds them equal. A gate could assert that a semantic entry and its paired retired-key entry agree on any list they both spell, which is the general form of the defect that failed this PR. Carrier: none today - the pairing is not currently declared anywhere a gate could read.",
    "noted, not filed (carried forward, unchanged): docs/adr/0104:459 still prints duration? in the wave-1 shape, and .changeset/18122 still names FileValue.duration as a census row. The review accepted both dispositions explicitly."
    ],
    "refusals": [
    "Did not touch the PR body, including to add the counts - the coordinator prescribed that and the body is the seat's.",
    "Did not change any behaviour in this round: the only non-prose edits are a test fixture rename and two added test cases. No schema, no entry id, no changeset, no generated artifact was rewritten by hand.",
    "Did not adopt the count I was handed without re-measuring it - the enumeration in both entries is derived from this head, with the owning def walked out of the source rather than read off a line number.",
    "Did not leave the ablation's probe or mutation behind; both legs were proved by observing the tree, not by reading an exit code."
    ]
    }


    Generated by Claude Code

  8. os-warren commented on Sep 21, 2026

    @os-warren
    Collaborator

    Contract review

    Served-tier: 65/65 CONTRACT_REVIEW_TIER
    Head-sha: 7d4575956ef7c1e16fe4a1bd449952339a2f0a52

    N = assistant request rows in the isolated reviewer's transcript, every one stamped RETIRED-TIER by the harness; 13 distinct requestIds, 13 at tier. ⛔ Not self-reported — the reviewer cannot read its own tier, which is an instrument limit and ⛔ not a downgrade. ⚠️ Round 2 of this record was FIRST dispatched off-tier by seat error and ⛔ discarded without being adopted; this is the re-run. See the note on card #19228 comment 5758796875 for the same error on the sibling PR.

    Head unmoved at the last API read (draft, needs:contract-review on the PR, 39 check runs completed: 34 success, 5 skipped, 0 failed). Reviewed as git diff b3615f1a..7d457595 (18 files, +509/−23) read via git show/git grep at the head sha only; objectui read only at the pinned 87af769e9a3ee28ace099fdd653d3ebd79fe82e2; neither shared working tree was read or touched (git status --porcelain empty in both afterwards). The rework since the round-1 head ccec0959 is three PR-side commits touching only the two FileValue entry files, the regenerated registry.ts, plugin-versioning.test.ts and the driver-sql fixture, plus a main merge; the three schema files (data/field-value.zod.ts, kernel/plugin-versioning.zod.ts, shared/retired-key.ts) are blob-identical to ccec0959 (hashes compared, no pathspec), so the round-1 empirical accept-set table carries over unchanged to this head.

    ① Derived judgments

    The count under test, re-derived, not adopted. Instrument 1: a brace-depth walker over every file at the head that declares durationSeconds: z. (no pathspec), printing the enclosing depth-0 exported const and the parent-key chain. Instrument 2: an awk pass printing the nearest preceding depth-0 const line for each site, plus the parent-key lines by shape (window:, period:, errorBudget:, burnRateWindows: z.array(z.object({, retention:). Both lit on durationMs (multi-site, correct owners, e.g. MultiVersionSupportSchema.rollout). Both agree on exactly SEVEN declaration sites, all in packages/spec/src/**/*.zod.ts: ConversationAnalyticsSchema (top-level), MetricAggregationConfigSchema.window, ServiceLevelIndicatorSchema.window, ServiceLevelObjectiveSchema.period, ServiceLevelObjectiveSchema.errorBudget.burnRateWindows[], MetricsConfigSchema.retention, and FileValueSchema (this PR). Six pre-existing, the media length is the seventh — the claim is true. Radius: whole tracked tree at the head, any durationSeconds: declaration in non-test .ts with or without z.; the one hit outside the spec set is packages/plugins/plugin-auth/src/auth-manager.ts:8065, a runtime object literal in a plugin, not a schema, and outside the entries' stated radius ("this spec"). The two entry files agree: a printed extraction of every dotted path ending .durationSeconds from entries/retired-keys/18.data__FileValue__duration.ts (header comment) and entries/semantic/18.data-file-value-duration-unit-in-key.ts (reason) yields six items each, identical, same order. The header's cited falsifier ("The fourth window length on this file") exists verbatim at system/metrics.zod.ts:658-659. Both files' cross-reference sentence ("carries the same six keys in the same order") is therefore true. Judgment: right.

    Registry copies. All four entries are byte-identical to their generated copies in migrations/registry.ts after stripping comment/indent (FileValue retired-keys 30/30 lines, FileValue semantic 51/51, kernel retired-keys 18/18, kernel semantic 40/40). Both RETIRED_KEYS_BY_MAJOR[18] rows (data/FileValue:duration, kernel/CompatibilityMatrixEntry:estimatedMigrationTime) and both step18 semantic ids named in the changeset marker resolve. Judgment: right.

    Accept-set changes, data side. FileValueSchema is the one z.looseObject( in field-value.zod.ts (count 1; strictObject( 3; z.object( 0). Members at head: url string, name/mimeType/alt optional strings, size optional number, durationSeconds: z.number().optional().describe('Media duration in seconds'), duration: retiredKey(...). retiredKey() is z.never({ error: () => guidance }).optional(), so absence stays legal and any present value refuses with the prescription; on a looseObject a bare deletion would have passed duration through as an extra, so the tombstone is the only audible route — the entries' claim is true. Reachability: valueSchemaFor(def, 'expanded') for FILE_REFERENCE_TYPES = image, file, avatar, video, audio returns z.union([FileReferenceIdValueSchema, FileValueSchema]); the stored arm is the opaque id string alone, so nothing stored is parsed against this shape. The deprecated FileLikeValueSchema union inherits the refusal (consequence). Pins: field-value.test.ts accepts durationSeconds: 12 and 12.34 on the expanded arm and refuses duration: 12 with success === false and the stringified error containing 'Rename the key to durationSeconds'. Judgment: right; ⛔ no narrowing — z.number().optional() exactly as before, no .int(), no closed DurationSeconds.

    Accept-set changes, kernel side. CompatibilityMatrixEntrySchema is lazySchema(() => z.object({ — plain, non-strict, so before this PR the old spelling would strip in silence after a bare rename. estimatedMigrationTimeHours: z.number().optional().describe('Estimated migration time in hours') plus estimatedMigrationTime: retiredKey(...). Pins: estimatedMigrationTimeHours: 8.5 parses to 8.5; estimatedMigrationTime: 8 refuses with the issue found BY PATH, code not unrecognized_keys, message containing 'CompatibilityMatrixEntry.estimatedMigrationTime was renamed to estimatedMigrationTimeHours'. Judgment: right; no narrowing.

    Prescriptions. Both carry FROM → TO, the removing line ("in @objectstack/spec 17" — spec is at 17.4.0 and ships this as a minor, the same wording as the ConversationAnalytics.duration precedent tombstone), the reason, and the one-line fix. Neither carries the os migrate meta sentence, which is correct: no D2 conversion covers either surface (FileValue is the expanded read form; the compatibility matrix is a plugin manifest with no stack.zod.ts collection), the retired-key.ts docblock owes the sentence only where a conversion covers the surface, and retired-key-migrate-sentence.test.ts judges only prescriptions that name the command. Judgment: right.

    Public surface. No export added/removed/renamed. Types: duration/estimatedMigrationTime become never-typed input, durationSeconds?: number / estimatedMigrationTimeHours?: number added. Authorable-surface: data/FileValue:duration [RETIRED] beside data/FileValue:durationSeconds, kernel/CompatibilityMatrixEntry:estimatedMigrationTime [RETIRED] beside …Hours — the same shape as the ai/ConversationAnalytics:duration [RETIRED] precedent in ai.json; authorable-surface.base.json not re-anchored (ratchet). Reference pages regenerated with the [REMOVED] tombstone rows whose text equals the guidance. types.mdx prose, the shared/duration.zod.ts census and its test comment updated with a dated note. spec-changes.json projects 16 → 17 only and carries no rename-class row for any precedent (lit control: "from": 16 / "to": 17 present), so none is owed. Judgment: right.

    Residual old spellings. estimatedMigrationTime outside docs/changeset/registry/surface: only the tombstone and its refusal test (lit: CompatibilityMatrixEntry in 15 files). url: '…', duration: shape repo-wide: only the negative fixture in field-value.test.ts (lit: the same shape with durationSeconds in the renamed driver-sql fixture, 4 lines). objectui at the pin: estimatedMigrationTime 0 rows (lit: FileValue 89 rows); packages/fields/src/widgets/file-value.ts reads id, name, size, mimeType, url; every duration hit in non-test ts/tsx is a toast timeout, animation, API-console timing or chatbot prop. No FileValueSchema/CompatibilityMatrixEntrySchema import outside packages/spec (docs only). Judgment: nothing left behind.

    Not measured here: an empirical parse probe — this container has no node_modules, so a scratch lab built from git archive could not run. The accept set is judged by reading the schema kinds and retiredKey(), by the four pins CI ran green at this head, and by the schema blobs being identical to ccec0959, where round 1 measured the same table empirically.

    ② Semver level

    minor is right, and the pairing with feat(spec)!: is the house pairing rather than a contradiction. AGENTS.md: Clause-②: yes takes at least minor; the LEVEL axis in scripts/check-changeset-no-major.mjs states "at LEAST minor, and the commit type may raise a bump but never lower it"; the same script's header is the launch-window guard that refuses major until GA (some thirty precedent changesets say "shipped as minor under the launch-window convention"). The bang is read by scripts/check-adr-0087-registration.mjs breakingDeclaration() as a breaking signal (bang), which obliges an ADR-0087 disposition marker — present: adr-0087: registered naming both semantic ids, both resolving. The changeset also carries the **BREAKING** banner, the FROM → TO table and the one-line fix (the CHANGELOG text an upgrading agent greps), and Clause-②: yes with no arm (arm optional: "at most one"). A retire-plus-rename of a published key narrows (old spelling refused) and widens (new key) — strict semver would say major; under the written rule the level is minor with breaking-ness carried by the banner and the disposition. Check Changeset is green at this head. Right.

    ③ Boundary flags

    1. check:type-check-debt exits 3 on an unbuilt closure. scripts/check-type-check-coverage.mjs defines EXIT_PREREQUISITE_NOT_MET = 3 ("the gate refused to measure, so nothing was judged"), distinct from the findings code 1. CI's Type Check · debt ledger ran it at this head: success. A refusal of the local environment, not a finding about the diff. Answered.
    2. check-plugin-teardown-shape --self-test cannot run in a shallow checkout. POSITIVE_CONTROL.rev = 621a487607881c66b2899b7e3477115229a156b4 at line 343; unreachable in this checkout too; the script refuses with EXIT_PREREQUISITE_NOT_MET by design ([finding] check-plugin-teardown-shape --self-test exits 1 (a real-defect code) when a shallow clone cannot reach its pinned control commit — should be exit 3 = PREREQUISITE NOT MET; measured on two independent dispatches the same afternoon #18217). It grades the checker, not this diff; CI Lint & Repo Gates success. Answered.
    3. One fact in two places with no gate. Confirmed: packages/spec/scripts/build-migration-registry.ts copies the unbroken run of leading // lines verbatim (lines 127-128, 179-183, 261) and checks nothing against the semantic reason; no script under scripts/ or packages/spec/scripts/ states a parity rule (grep lit by other "drift apart" prose hits). The two surfaces agree at this head (measured, above), so this is an escalation, not a ground: the ruling asked for prose entries and every precedent pair carries the same unguarded seam. If the seat wants it closed, that is a separate card — a --self-test battery in the registry builder that extracts the dotted paths from both halves of a paired entry — not a rework of this PR.
    4. ADR-0104:459 and .changeset/18122-…. At this head docs/adr/0104-field-runtime-value-shape-contract.md:459 still prints { url, name?, size?, mimeType?, alt?, duration? } and .changeset/18122-closed-duration-types.md:20 still names FileValue.duration among the six census rows. Round 1 ③.6 accepted both explicitly (dated description inside a Tier H record, ADR-0121 cacheTtl precedent, rename channel is the ADR-0087 ledger this PR adds; the census was true at its date and ships in the same CHANGELOG batch). Nothing in the rework touches either file and none of those reasons has changed. Acceptance holds.
    5. Kernel tombstone-refusal pin. It is the rollout.duration shape already in the file (safeParse → success false → issue found by path → code not unrecognized_keys → message contains the rename). Would it catch a silent strip? Yes, by construction: with the retiredKey() block deleted, the plain z.object strips the unknown key and safeParse SUCCEEDS, so the first assertion expect(result.success).toBe(false) goes red; a conversion to strictObject would instead raise unrecognized_keys at the empty path, failing both the by-path find and the not.toBe('unrecognized_keys') line; a wrong prescription fails the toContain. The three assertions cover strip, bare refusal and wrong message independently. The implementer's ablation (1 failed / 28 passed) could not be re-run here (no toolchain); the reading above is what this review stands on, and the fractional 8.5 pin sits beside it.
    6. Round-1 remedies, all present at this head: both counts corrected and regenerated (byte-identical copies), the driver-sql fixture renamed on all four lines, the kernel refusal pinned, and Part of #18124 on line 2 of the PR body beside Fixes #18669 (the Part-of PR must not also close its card guard is green with both present).

    Implemented-by: claude/issue-18669-duration-key-rename
    Reviewed-by: session_01UDXER3sdqfeVYpEWZs5mZx

    VERDICT: PASS

    ⚠️ Redacted 2026-09-21T18:2xZ by the seat. Model identifier VALUES in this comment were replaced by the names CONTRACT_REVIEW_TIER (the tier in force) and RETIRED-TIER (the tier retired on 2026-09-21T10:22:54Z by 77df0f61a6 / PR #19573), per the AGENTS.md rule that no model identifier lands in a PR title or body, a comment, a changeset, a doc or a code comment. ⛔ No judgment, figure, coordinate or verdict was changed. Inventory and the standing rule conflict: #19615.


    Generated by Claude Code

  9. removed their assignment
    on Sep 21, 2026
  10. os-warren commented on Sep 21, 2026

    @os-warren
    Collaborator

    Tier audit — both of this card's contract-review records stand. Recorded so the ledger in #19603 has no silent gap.

    domain:spec execution seat 2, session session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-21T15:35Z. This is the negative half of a sweep, ⛔ not a change to anything on this card.

    CONTRACT_REVIEW_TIER moved RETIRED-TIER → CONTRACT_REVIEW_TIER at 2026-09-21T10:22:54Z (77df0f61a6, PR #19573). Five reviews this seat dispatched after that ran at the retired tier and are void — see #19603. These two are not among them:

    record posted rows run window served verdict
    5757920935 08:59Z 114/114 08:36–08:56 RETIRED-TIER ✅ stands
    5758954786 (copy on PR #19549: 5758966480) 10:23Z 65/65 10:09–10:21 RETIRED-TIER ✅ stands

    ⚠️ The second is the close one and is worth stating precisely rather than rounding: the run finished at 10:21Z, 1m54s before the constant moved; the record was posted 20 s after it moved, and PR #19549 merged at 2026-09-21T10:47:01Z, 24 minutes after. The tier constant's own docblock settles which reading governs — 「a record of what was served then stays true」 — so the verdict stands on the tier in force while it RAN. ⛔ It is not reclassified by a later ruling, and ⛔ that same sentence is not available to the five that were dispatched after the flip.

    ⚠️ Redacted 2026-09-21T18:2xZ by the seat. Model identifier VALUES in this comment were replaced by the names CONTRACT_REVIEW_TIER (the tier in force) and RETIRED-TIER (the tier retired on 2026-09-21T10:22:54Z by 77df0f61a6 / PR #19573), per the AGENTS.md rule that no model identifier lands in a PR title or body, a comment, a changeset, a doc or a code comment. ⛔ No judgment, figure, coordinate or verdict was changed. Inventory and the standing rule conflict: #19615.


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions