Repository navigation
[Decision] #18124 余下两行只剩改名一条路 —— FileValue.duration(小数秒)与 estimatedMigrationTime(小时)是否走 ADR-0087 改名,仓内已有两次同形先例 #18669
Description
Activity
Ruling: batch #151 item 4 · letter A (rename
FileValue.duration→durationSecondsandCompatibilityMatrixEntry.estimatedMigrationTime→estimatedMigrationTimeHours, each with an ADR-0087 conversion-layer entry; ⛔ no new closed type, ⛔ no narrowing of stored data) · maintainer 「其他同意」 2026-09-17T15:42ZBlocked-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 conventionHTMLMediaElement.duration/ffprobe) andestimatedMigrationTime(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;FileValueis 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 staysz.number(), fractional seconds legal);estimatedMigrationTime→estimatedMigrationTimeHours(value staysz.number()). One ADR-0087 conversion-layer entry each inpackages/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
--listbaseline 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/specwith 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
durationis 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 holdsmigrations/registry.ts); the unlock sweep returns it topm:queuewhen #17534 closes.pm:blockingstays — #18124 closes only after this lands (Part of #18124on the PR).domain:spec,priority:p3stay; zero pull means it does not jump the queue.
Generated by Claude Code
回决策箱(维护者指令,skills 席 2 代执行)— 2026-09-21T03:44Z
出处三件(
SKILL.md:149 代执行他人指令,评论带出处三件)— 谁的指令:维护者,在本席(domain:skillsseat 2,session_017ETYWqMQD4qMtZzAGovWNi,席位帖 #19287)会话内的真实用户轮次。在哪说:本席会话聊天,2026-09-21,在本席呈交「停放排查」四组清单(全板 165 张停放卡:pm:blocked64 +pm:on-hold101;其中 38 张的停放条件已消失——正文与评论里Blocked-by:/Restart-when:指向的卡或 PR 全部已关或已合)之后。原话(逐字,⛔ 未翻译、未润色):「还有哪些应该解除停放的你一起排查一下」;对四组清单:「同意」。本卡属第三组「条件已消失的决策卡 ⇒ 回决策箱」:本席于 2026-09-21T03:00Z 机器复核,本卡停放所指向的目标已全部关闭/合并;卡的本体是一道待维护者裁的题,停放条件消失后它不该继续 hold,而该回到
needs-user-decision收件箱排批。动作:停放状态标签一笔换为
needs-user-decision;⛔ 本席不代裁、不派发。所属车道席位或总监席在下次决策批前按SKILL.md〈决策箱勤务〉补全四棱卡面块与「维护者速读」(存量卡低频子轮回填);H62 在补全前对本卡报行属正常。
Generated by Claude Code
os-project-manager commented
on Sep 21, 2026 CollaboratorMore actionsState 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: renameFileValue.duration→durationSecondsandCompatibilityMatrixEntry.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 ofpackages/spec/src/migrations/registry.ts) closedcompletedat 2026-09-21T00:19Z (landed through PR #19398, merged).needs-user-decision→pm:queuein this stroke,priority:p3unchanged (zero measured pull — it sits behind every product P0/P1 by the road order and is taken by thedomain:specseat 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.tsand 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 remaining items
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:499duration: z.number().optional(),with no unit token in the name, no .describe(), no .meta({ externalVocabulary })",
"2_CompatibilityMatrixEntry.estimatedMigrationTime": "HOLDS - plugin-versioning.zod.ts:191estimatedMigrationTime: 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 aboveexport const entry, regenerated into registry.ts by gen:migration-registry) was copied, not invented. The prose the dispatch quoted, 'Rename the key todurationSeconds; 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 CompatibilityMatrixEntryover 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 tokendurationis repo-wide noise (628 files), so the instrument is co-occurrence withmimeType(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 onlydurationis a hook-scope comment; client/src/index.ts's aredurationMs; 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 theexpandedarm of valueSchemaFor (thestoredarm is FileReferenceIdValueSchema, an opaque id string), andgit 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. Everydurationhit 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: 12andestimatedMigrationTime: 8produce 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 onlydurationSeconds: 12.34andestimatedMigrationTimeHours: 8exits 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 HEADempty andgit status --porcelainempty - 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_RECORDwith 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 jsonover 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-repopnpm lintscan 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 refusesmajor; the house convention is a minor carrying the callout). It carries the disposition markeradr-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, andskip-changesetdoes not apply because a changeset was written, so the writable intersection is empty.needs:contract-reviewis 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 namesFileValue.durationamong 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
Contract review
Served-tier: 114/114
CONTRACT_REVIEW_TIERN = assistant request rows in the isolated reviewer's transcript, every one stamped
RETIRED-TIERby the harness; 19 distinctrequestIds, 19 at tier. ⛔ Not self-reported — the reviewer answeredNOT READABLE, an instrument limit rather than a downgrade.Head-sha:
ccec0959cdd66f0b6188d33473811a37c2b5216a— the PR head had not moved at the last read (draft,needs:contract-reviewon card and PR, all 39 check-runs completedsuccess/skippedincluding 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-base61170fa385da18a04e3fed75e8c65cbff801e008; the local 17-file diff matches the API file list. objectui read only at the pinned87af769e9a3ee28ace099fdd653d3ebd79fe82e2. Empirical readings were taken in a scratch lab (git archiveof head and basepackages/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) ·durationSecondsunknown key → passthrough: 12,12.34,'12',null,NaN,Infinityall ACCEPT and retained verbatimz.number().optional():12,12.34,0,-1,1e308,undefined/absent ACCEPT;NaN,Infinity,'12',nullREJECTright — the declaration the ruling ordered; its numeric accept set is byte-identical to the old duration's at base (12,12.34,0,-1,1e308ACCEPT;NaN,Infinity,'12',nullREJECT), so no narrowing of the valueFileValueSchema·durationfinite numbers ACCEPT+retained; NaN/'12'/nullREJECTevery present value REJECTS with the rename prescription (measured: the issue message begins FileValue.duration` was renamed to `durationSeconds);undefined/absent ACCEPTright — the declared breaking half; retiredKey()isz.never().optional()so absence stays legalFileValueSchema· unknown extrafooACCEPT passthrough ACCEPT passthrough unchanged — proves a bare deletion would have waved durationthrough 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 FileLikeValueSchemaon{url, duration:12}ACCEPT REJECT consequence of the union carrying FileValueSchema; correctCompatibilityMatrixEntrySchema(plainz.object) ·estimatedMigrationTimeHoursunknown → ACCEPT and silently stripped (output keys measured: from,to,compatibility,migrationRequired)finite numbers ACCEPT and retained ( 8,8.5,0,-1);NaN,'8',nullREJECTright CompatibilityMatrixEntrySchema·estimatedMigrationTimefinite numbers ACCEPT+retained; NaN/nullREJECTevery present value REJECTS with the prescription ( CompatibilityMatrixEntry.estimatedMigrationTime` was renamed to `estimatedMigrationTimeHours); absent ACCEPTright CompatibilityMatrixEntrySchema· unknownfoostripped 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,CompatibilityMatrixEntryParsedkeep their names (packages/spec/api-surface/{data,kernel}.jsonuntouched, correct for a snapshot that records names not shapes). Type level: atsc --noEmitprobe against the head sources gives exactly twoTS2322 Type 'number' is not assignable to type 'undefined'onduration: 12andestimatedMigrationTime: 8, and none ondurationSeconds: 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 theai/ConversationAnalytics:duration [RETIRED]precedent inauthorable-surface/ai.json;authorable-surface.base.jsonleft un-anchored (ratchet, fine). Reference pages regenerated and consistent with the describes and tombstone text.docs/protocol-upgrade-guide.mdandpackages/spec/spec-changes.jsonproject 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 speccheck:generatedfamily) are green at this head.Sentences verified true at this head: both values
z.number().optional(), fractional second parses (measured);FileValueSchemais the onez.looseObject(infield-value.zod.ts(count 1;strictObject(3;z.object(0);CompatibilityMatrixEntrySchemais a plainz.object;FileValue.durationhad no.describe(), no JSDoc, no unit token at base (read); the only other number onFileValueissize(shape:url,name,size,mimeType,alt,durationSeconds); "renaming alone would have created a NEWcheck:duration-unit-keysoffence" — verified by ablation (see ③.3); tombstone text "in @objectstack/spec 17" matches the 253 siblingstep18tombstones and spec at 17.4.0 shipping theminor; the<!-- adr-0087: registered … -->ids both exist inregistry.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 tookDurationMs/DurationSecondsor anexternalVocabularyexemption);content/docs/protocol/objectql/types.mdxprose now namesdurationSeconds.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, thereasonstring ofexport const entry(copied verbatim intoregistry.tsunderstep18→data-file-value-duration-unit-in-key, and therefore compiled into the published dist and the future upgrade guide / MCPspec_changesfeed): "this spec already spells a length of timedurationSecondsin 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 tipef256e6: sixdurationSeconds:key declarations inpackages/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.tsheader comment (copied above'data/FileValue:duration'inRETIRED_KEYS_BY_MAJOR[18]): "ConversationAnalytics, the threesystem/metrics.zod.tswindow lengths andMetricsConfig.retention".metrics.zod.tscarries fivedurationSecondskeys, and theburnRateWindowselement's own JSDoc calls itself "The fourth window length on this file". This list omitsServiceLevelObjective.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 FileValueat 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/estimatedMigrationTimethat parsed at base now refuses) and widens it (two new keys). Under strict semver that ismajor. The rule this repo applies lives in the header ofscripts/check-changeset-no-major.mjs("LAUNCH-WINDOW GUARD … we ship breaking changes asminor… the mandatory information carriers for breaking-ness are the BREAKING banner and the ADR-0087 disposition", end condition GA), enforced there and byscripts/check-adr-0087-registration.mjs. The changeset.changeset/18669-duration-key-rename.mddeclares"@objectstack/spec": minor, afeat(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 inregistry.ts), the FROM → TO table and the one-line fix, andClause-②: yes. Gates that read it:check-changeset-no-major.mjslevel axis (yesrequires ≥minoron 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-reviewon both carriers, PR draft), and theCheck ChangesetCI 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). TheClause-②: yesdeclaration is right and matches the ruling's own text ("Published key rename ⇒Clause-②: yes"); the fuller spellingyes (narrowing)(CLAUSE2_ARMSdocblock incheck-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
- 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
durationSecondsdid accept'12'/null/NaNat base as an unknown passthrough and now refuses them; that is the ordinary consequence of declaring az.number()key, not a narrowing of any stored or authored value (no producer ever spelleddurationSeconds). - Tombstone rather than deletion — both shape claims verified by reading and by the strip/passthrough rows above.
.describe()as remedy — verified by running the gate itself (packages/spec/scripts/check-duration-unit-keys.ts --root): headpackages/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.- Reachability zero, re-taken — instrument:
git grepover the tracked tree at the head sha. (a)valueSchemaFor(outsidepackages/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 isform: ValueForm = 'stored', so a one-arg call cannot select the expanded arm; noValueForm-typed variable exists outside spec (the spread/computed/re-export shapes have nothing to carry). (b) bare tokenexpandedoutside spec: 380 rows, none avalueSchemaForargument; lit control inside spec 185 rows. (c)FileValueSchema/FileLikeValueSchema/CompatibilityMatrixEntrySchemaby name outsidepackages/spec/src: docs, changesets, ADR, audit ledger, CHANGELOGs and comments only;packages/objectql/src/engine.tsbuilds the expanded value by spreadingsys_filecolumns into{id,name,size,mimeType,url}— no duration. (d) objectui at the pin: one non-testvalueSchemaFor(field, 'stored')inpackages/fields/src/index.tsx, one-arg test calls (default stored),FileValueSchemain comments only,readFileValue/readFileValuesreadid,name,url,mimeType,size; lit controlFileValue19 files / 89 rows;estimatedMigrationTime|CompatibilityMatrix0 rows. Radius: tracked text at the two shas. Deliberately outside it:packages/console/dist(the built objectui bundle — a minified consumer would not spellvalueSchemaFor), 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'sFileValue.durationcensus used co-occurrence withmimeType; a numeric-literal instrument (\bduration:\s*[0-9], 32 rows repo-wide, each classified) findspackages/drivers/driver-sql/src/sql-driver-numeric-fidelity.test.ts, theround-trips object-valued record/video/audiocase:f_video: { url: 'https://cdn/v.mp4', duration: 12 }/f_audio: { …, duration: 30 }written and asserted back. It has nomimeType, 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. - Declared non-measurements —
@objectstack/runtime:packages/runtime/srchas zero references toFileValueSchema,CompatibilityMatrixEntry,estimatedMigrationTime, andvalueSchemaForonly in a comment; nodurationin 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--affectedset, 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./consoleand./hook-bodysubpath exports — unrelated to any spec key; refusal was for a missing cli build; CI ran them green. No hole. - Noted, not filed —
docs/adr/0104-…mdwave-1 sentence printingduration?: accept leaving it — a dated description inside a Tier H record, the precedent (ADR-0121 still spellingcacheTtl) holds, and the rename channel is the ADR-0087 ledger this PR adds; carrier none..changeset/18122-closed-duration-types.mdnamingFileValue.durationas a census row: accept — a measurement true at its date, shipped in the same CHANGELOG batch, in order. - 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 #18124on the PR") and the director's state repair (5755336165) both prescribePart of #18124on the PR; the body carries no18124at all — thePart-of closing-keyword guardpassed precisely because no Part-of line exists.Fixes #18669on line 1 is correct (the ruling makes [Decision] #18124 余下两行只剩改名一条路 ——FileValue.duration(小数秒)与estimatedMigrationTime(小时)是否走 ADR-0087 改名,仓内已有两次同形先例 #18669 the execution card,⛔ no new card), andFixes #18669+Part of #18124co-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.tspins the rename but not the refusal — no test assertsestimatedMigrationTime: 8fails with the prescription (the FileValue side has one); recommended pin. (d) theClause-②arm (② above), optional.
VERDICT: FAIL
packages/spec/src/migrations/entries/semantic/18.data-file-value-duration-unit-in-key.ts, thereasonstring ofexport const entry(and its generated copy inpackages/spec/src/migrations/registry.tsunderstep18→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 — sixdurationSecondskey declarations exist, and the enumeration omitsServiceLevelObjective.burnRateWindows[].durationSecondsinsystem/metrics.zod.ts.packages/spec/src/migrations/entries/retired-keys/18.data__FileValue__duration.ts, the header comment aboveexport const entry(and its copy above'data/FileValue:duration'inRETIRED_KEYS_BY_MAJOR[18]inregistry.ts): "the threesystem/metrics.zod.tswindow lengths andMetricsConfig.retention" contradicts that file's ownburnRateWindowsJSDoc ("The fourth window length on this file") and omitsServiceLevelObjective.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, addPart of #18124to 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.tswindow lengths andMetricsConfig.retention」,而metrics.zod.ts上有五处。⭐ 更锋利的是:那个文件自己的 JSDoc 就否证了它——metrics.zod.ts:658逐字写着「The fourth window length on this file」。⇒ 两条已发布条目彼此矛盾、且都与代码矛盾,而其中一条与同一个文件里的另一句已发布散文矛盾。⚠️ 为什么这两条不是小事:这两段散文被gen:migration-registry原样复制进registry.ts,编进已发布的 dist,并喂给将来的升级指南与 MCPspec_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:blockingstays — #18124 closes only after this lands (Part of #18124on the PR)而 PR 正文里一个
18124都没有。⚠️ 而Part-of closing-keyword guard是绿的——恰恰因为压根没有 Part-of 行:一道绿门禁对一件不存在的事保持了沉默。本席已补进正文(正文是席位的,⛔ 不是 dev 的)。欠改(照复核的 remedy,全是散文,⛔ 不加码)
- 两条条目的计数改对,然后
gen:migration-registry重生成镜像,⛔ 不手改。 driver-sql那个夹具改名到新拼法。- 可选(复核标为 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 namesCONTRACT_REVIEW_TIER(the tier in force) andRETIRED-TIER(the tier retired on 2026-09-21T10:22:54Z by77df0f61a6/ PR #19573), per theAGENTS.mdrule 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
- added a commit that references this issue
on Sep 21, 2026 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 '^\sdurationSeconds:' 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 samedurationSecondsthe 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 retireddurationkey. 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, awaitaction 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:498rollout: { 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 andcode !== '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.5still 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 isestimatedMigrationTime, assert itscodeis notunrecognized_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 --deleteremoved 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 onexpect(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 HEADempty,git status --porcelainempty, 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 --fixthen 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 --ranreports '110 derived, 109 run, 1 NOT-MEASURED, 0 UNRUN' and exits 0. The one NOT-MEASURED is againnode 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-debtrefused 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 buildover ./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', andcheck:type-check-coverageexit 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 jsonreports 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 #18124is the seat's edit and is confirmed present besideFixes #18669at this head. No label written, attached or detached (needs:contract-reviewis on the PR and stays). Assignee untouched. PR still draft, auto_merge null, not enqueued. ② was not revisited:minorandClause-②: yesstand 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-debtexits 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 printsduration?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
Contract review
Served-tier: 65/65
CONTRACT_REVIEW_TIER
Head-sha:7d4575956ef7c1e16fe4a1bd449952339a2f0a52N = assistant request rows in the isolated reviewer's transcript, every one stamped
RETIRED-TIERby the harness; 13 distinctrequestIds, 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-reviewon the PR, 39 check runs completed: 34success, 5skipped, 0 failed). Reviewed asgit diff b3615f1a..7d457595(18 files, +509/−23) read viagit show/git grepat the head sha only; objectui read only at the pinned87af769e9a3ee28ace099fdd653d3ebd79fe82e2; neither shared working tree was read or touched (git status --porcelainempty in both afterwards). The rework since the round-1 headccec0959is three PR-side commits touching only the two FileValue entry files, the regeneratedregistry.ts,plugin-versioning.test.tsand 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 toccec0959(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-0constline for each site, plus the parent-key lines by shape (window:,period:,errorBudget:,burnRateWindows: z.array(z.object({,retention:). Both lit ondurationMs(multi-site, correct owners, e.g.MultiVersionSupportSchema.rollout). Both agree on exactly SEVEN declaration sites, all inpackages/spec/src/**/*.zod.ts:ConversationAnalyticsSchema(top-level),MetricAggregationConfigSchema.window,ServiceLevelIndicatorSchema.window,ServiceLevelObjectiveSchema.period,ServiceLevelObjectiveSchema.errorBudget.burnRateWindows[],MetricsConfigSchema.retention, andFileValueSchema(this PR). Six pre-existing, the media length is the seventh — the claim is true. Radius: whole tracked tree at the head, anydurationSeconds:declaration in non-test.tswith or withoutz.; the one hit outside the spec set ispackages/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.durationSecondsfromentries/retired-keys/18.data__FileValue__duration.ts(header comment) andentries/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 atsystem/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.tsafter stripping comment/indent (FileValue retired-keys 30/30 lines, FileValue semantic 51/51, kernel retired-keys 18/18, kernel semantic 40/40). BothRETIRED_KEYS_BY_MAJOR[18]rows (data/FileValue:duration,kernel/CompatibilityMatrixEntry:estimatedMigrationTime) and bothstep18semantic ids named in the changeset marker resolve. Judgment: right.Accept-set changes, data side.
FileValueSchemais the onez.looseObject(infield-value.zod.ts(count 1;strictObject(3;z.object(0). Members at head:urlstring,name/mimeType/altoptional strings,sizeoptional number,durationSeconds: z.number().optional().describe('Media duration in seconds'),duration: retiredKey(...).retiredKey()isz.never({ error: () => guidance }).optional(), so absence stays legal and any present value refuses with the prescription; on a looseObject a bare deletion would have passeddurationthrough as an extra, so the tombstone is the only audible route — the entries' claim is true. Reachability:valueSchemaFor(def, 'expanded')forFILE_REFERENCE_TYPES=image, file, avatar, video, audioreturnsz.union([FileReferenceIdValueSchema, FileValueSchema]); thestoredarm is the opaque id string alone, so nothing stored is parsed against this shape. The deprecatedFileLikeValueSchemaunion inherits the refusal (consequence). Pins:field-value.test.tsacceptsdurationSeconds: 12and12.34on the expanded arm and refusesduration: 12withsuccess === falseand the stringified error containing 'Rename the key todurationSeconds'. Judgment: right; ⛔ no narrowing —z.number().optional()exactly as before, no.int(), no closedDurationSeconds.Accept-set changes, kernel side.
CompatibilityMatrixEntrySchemaislazySchema(() => 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')plusestimatedMigrationTime: retiredKey(...). Pins:estimatedMigrationTimeHours: 8.5parses to 8.5;estimatedMigrationTime: 8refuses with the issue found BY PATH,codenotunrecognized_keys, message containing 'CompatibilityMatrixEntry.estimatedMigrationTimewas renamed toestimatedMigrationTimeHours'. 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.durationprecedent tombstone), the reason, and the one-line fix. Neither carries theos migrate metasentence, which is correct: no D2 conversion covers either surface (FileValue is the expanded read form; the compatibility matrix is a plugin manifest with nostack.zod.tscollection), theretired-key.tsdocblock owes the sentence only where a conversion covers the surface, andretired-key-migrate-sentence.test.tsjudges only prescriptions that name the command. Judgment: right.Public surface. No export added/removed/renamed. Types:
duration/estimatedMigrationTimebecomenever-typed input,durationSeconds?: number/estimatedMigrationTimeHours?: numberadded. Authorable-surface:data/FileValue:duration [RETIRED]besidedata/FileValue:durationSeconds,kernel/CompatibilityMatrixEntry:estimatedMigrationTime [RETIRED]beside…Hours— the same shape as theai/ConversationAnalytics:duration [RETIRED]precedent inai.json;authorable-surface.base.jsonnot re-anchored (ratchet). Reference pages regenerated with the[REMOVED]tombstone rows whose text equals the guidance.types.mdxprose, theshared/duration.zod.tscensus and its test comment updated with a dated note.spec-changes.jsonprojects 16 → 17 only and carries no rename-class row for any precedent (lit control:"from": 16/"to": 17present), so none is owed. Judgment: right.Residual old spellings.
estimatedMigrationTimeoutside docs/changeset/registry/surface: only the tombstone and its refusal test (lit:CompatibilityMatrixEntryin 15 files).url: '…', duration:shape repo-wide: only the negative fixture infield-value.test.ts(lit: the same shape withdurationSecondsin the renamed driver-sql fixture, 4 lines). objectui at the pin:estimatedMigrationTime0 rows (lit:FileValue89 rows);packages/fields/src/widgets/file-value.tsreadsid, name, size, mimeType, url; everydurationhit in non-test ts/tsx is a toast timeout, animation, API-console timing or chatbot prop. NoFileValueSchema/CompatibilityMatrixEntrySchemaimport outsidepackages/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 fromgit archivecould not run. The accept set is judged by reading the schema kinds andretiredKey(), by the four pins CI ran green at this head, and by the schema blobs being identical toccec0959, where round 1 measured the same table empirically.② Semver level
minoris right, and the pairing withfeat(spec)!:is the house pairing rather than a contradiction. AGENTS.md:Clause-②: yestakes at leastminor; the LEVEL axis inscripts/check-changeset-no-major.mjsstates "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 refusesmajoruntil GA (some thirty precedent changesets say "shipped asminorunder the launch-window convention"). The bang is read byscripts/check-adr-0087-registration.mjsbreakingDeclaration()as a breaking signal (bang), which obliges an ADR-0087 disposition marker — present:adr-0087: registerednaming 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), andClause-②: yeswith 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 saymajor; under the written rule the level isminorwith breaking-ness carried by the banner and the disposition.Check Changesetis green at this head. Right.③ Boundary flags
check:type-check-debtexits 3 on an unbuilt closure.scripts/check-type-check-coverage.mjsdefinesEXIT_PREREQUISITE_NOT_MET = 3("the gate refused to measure, so nothing was judged"), distinct from the findings code 1. CI'sType Check · debt ledgerran it at this head: success. A refusal of the local environment, not a finding about the diff. Answered.check-plugin-teardown-shape --self-testcannot run in a shallow checkout.POSITIVE_CONTROL.rev = 621a487607881c66b2899b7e3477115229a156b4at line 343; unreachable in this checkout too; the script refuses withEXIT_PREREQUISITE_NOT_METby 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; CILint & Repo Gatessuccess. Answered.- One fact in two places with no gate. Confirmed:
packages/spec/scripts/build-migration-registry.tscopies the unbroken run of leading//lines verbatim (lines 127-128, 179-183, 261) and checks nothing against the semanticreason; no script underscripts/orpackages/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-testbattery in the registry builder that extracts the dotted paths from both halves of a paired entry — not a rework of this PR. - ADR-0104:459 and
.changeset/18122-…. At this headdocs/adr/0104-field-runtime-value-shape-contract.md:459still prints{ url, name?, size?, mimeType?, alt?, duration? }and.changeset/18122-closed-duration-types.md:20still namesFileValue.durationamong the six census rows. Round 1 ③.6 accepted both explicitly (dated description inside a Tier H record, ADR-0121cacheTtlprecedent, 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. - Kernel tombstone-refusal pin. It is the
rollout.durationshape already in the file (safeParse →successfalse → issue found by path →codenotunrecognized_keys→ message contains the rename). Would it catch a silent strip? Yes, by construction: with theretiredKey()block deleted, the plainz.objectstrips the unknown key andsafeParseSUCCEEDS, so the first assertionexpect(result.success).toBe(false)goes red; a conversion tostrictObjectwould instead raiseunrecognized_keysat the empty path, failing both the by-pathfindand thenot.toBe('unrecognized_keys')line; a wrong prescription fails thetoContain. 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 fractional8.5pin sits beside it. - 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 #18124on line 2 of the PR body besideFixes #18669(thePart-of PR must not also close its cardguard is green with both present).
Implemented-by:
claude/issue-18669-duration-key-rename
Reviewed-by:session_01UDXER3sdqfeVYpEWZs5mZxVERDICT: PASS
⚠️ Redacted 2026-09-21T18:2xZ by the seat. Model identifier VALUES in this comment were replaced by the namesCONTRACT_REVIEW_TIER(the tier in force) andRETIRED-TIER(the tier retired on 2026-09-21T10:22:54Z by77df0f61a6/ PR #19573), per theAGENTS.mdrule 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
Tier audit — both of this card's contract-review records stand. Recorded so the ledger in #19603 has no silent gap.
domain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-21T15:35Z. This is the negative half of a sweep, ⛔ not a change to anything on this card.CONTRACT_REVIEW_TIERmovedRETIRED-TIER→CONTRACT_REVIEW_TIERat 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 575792093508: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 namesCONTRACT_REVIEW_TIER(the tier in force) andRETIRED-TIER(the tier retired on 2026-09-21T10:22:54Z by77df0f61a6/ PR #19573), per theAGENTS.mdrule 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
⏱️ 本卡所有读数取自同一动作: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.tsFileValue.duration(MEASURED::484duration: z.number().optional())HTMLMediaElement.duration/ffprobe;本仓库内无单位成文)DurationSeconds带.int()⇒ 拒12.34;DurationMs与惯例差 1000×kernel/plugin-versioning.zod.tsCompatibilityMatrixEntry.estimatedMigrationTime(MEASURED::188-191JSDoc 逐字「Estimated migration time in hours」+z.number().optional())⭐ 关键读数(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 todurationSeconds; the value (seconds) …」。⇒ 不是新发明,是既有做法。选项 × 真实代价
durationSeconds+estimatedMigrationTimeHours,各一条 ADR-0087 转换层条目packages/spec/src/migrations/registry.ts—— 该文件被 #17534 持有(MEASURED:它仍 open +pm:dispatched)⇒ 必须排在它之后FileValue.duration取DurationSeconds12.34秒的生产者会在写的那一刻被拒;而FileValueSchema是 ADR-0104 双模已存储内联 blob ⇒ 对可能已存在的行收窄estimatedMigrationTime仍无解,还得回到 A 或 C{ id, name, size, mimeType, url },packages/objectql/src/engine.ts自己的注释逐字如此,不含 duration;RELAYED:objectui 侧也无消费者)业务直译: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 D13no-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