Repository navigation
Commit 731f020
Fixes #18669
Part of #18124
Clause-②: yes
Maintainer ruling **A** (comment 5717168926, decision batch #151 item 4,
「其他同意」 2026-09-17T15:42Z):
> 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
Options B (adopt a closed type), C (new closed types) and D (retire both
keys) were not chosen and are not implemented here.
| def | before | after |
|:--|:--|:--|
| `data/FileValue` | `duration: 12` | `durationSeconds: 12` |
| `kernel/CompatibilityMatrixEntry` | `estimatedMigrationTime: 8` |
`estimatedMigrationTimeHours: 8` |
Both stay `z.number().optional()`. `durationSeconds: 12.34` still parses
— the closed `DurationSeconds`
(`z.number().int().nonnegative()`, #18122) and a bare `.int()` were both
refused by the ruling, and the
fractional case is pinned as a test.
## 1. Premise re-measured on `origin/main` before any edit
Taken at tip `ef256e6a8f9bf5744e071631f4337ded9c3b3b76`,
2026-09-21T06:17Z. Every construct was found by
NAME, never by the card's 2026-09-17 line numbers.
| what the card asserts | answer |
|:--|:--|
| `FileValue.duration` is still `z.number().optional()`, no unit in the
name, no `externalVocabulary` | **holds** — `field-value.zod.ts` line
499, bare, no `.describe()`, no `.meta()` |
| `CompatibilityMatrixEntry.estimatedMigrationTime` is still
`z.number().optional()` with the JSDoc "Estimated migration time in
hours" | **holds** — `plugin-versioning.zod.ts` line 191, JSDoc verbatim
at line 189, no `.describe()` |
| `check-duration-unit-keys.ts` still names three admission channels and
the phrase "carries it in its NAME or in its TYPE, never only in its
`.describe()` prose" | **holds** — verbatim at line 6; the ADMISSION
section declares TYPE, NAME and prose (describe + JSDoc as the second
prose channel), with the retired name-shape list explicitly not an
admission channel |
| the two precedents in `migrations/registry.ts` | **both present** —
`system/MetricAggregationConfig:window.size` and
`system/ServiceLevelObjective:period.duration`, read in full; their
entry shape is copied rather than invented |
The premise holds in all four places, so the implementation was built on
it.
## 2. Producer / consumer census, each zero with a lit control
Instrument: `git grep` with **no pathspec** (the measured pathspec trap
in this checkout), at
`ef256e6a8f9bf5744e071631f4337ded9c3b3b76`, 2026-09-21T06:20Z.
**`estimatedMigrationTime` — 9 lines / 5 files**, printed rather than
counted: 3 generated doc rows
(`content/docs/references/kernel/plugin-versioning.mdx`), 2 generated
ratchet rows
(`authorable-surface.base.json`, `authorable-surface/kernel.json`), 3
test lines
(`plugin-versioning.test.ts` 169/176/424) and the schema declaration
itself. **Zero runtime producers,
zero runtime consumers.** Lit control, same instrument:
`CompatibilityMatrixEntry` returns 39 rows across
13 files, so the grep is not silently answering nothing.
**`FileValue.duration`** — the bare token `duration` is repo-wide noise
(628 files), so the instrument is
co-occurrence with `mimeType`: 25 files, of which the source hits are
`packages/spec/src/data/field-value.test.ts:362` (a fixture),
`content/docs/protocol/objectql/types.mdx:1164`
(hand-written prose), `docs/adr/0104-...md:459` (ADR prose),
`packages/spec/src/shared/duration.zod.ts:60`
and `duration.test.ts:70` (the #18122 six-row census), and
`.changeset/18122-closed-duration-types.md:20`.
**Zero runtime producers, zero runtime consumers.** Lit control: the
same instrument returns
`packages/objectql/src/engine.ts`, `packages/client/src/index.ts` and
three more for the pair, so it is
reading real files; those files' `duration` hits are a hook-scope
comment, `durationMs` and Tailwind
`duration-200` classes, none of them a file value.
**Reachability, measured rather than 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, with the
same grep inside `packages/spec` returning hits as the lit control. So
no write path and no read path in
this repo ever parses a value against this shape.
**objectui at the pinned sha**
`87af769e9a3ee28ace099fdd653d3ebd79fe82e2` (`.objectui-sha` on
`origin/main`):
`readFileValue` / `readFileValues`
(`packages/fields/src/widgets/file-value.ts`) read `id`, `name`, `url`,
`mimeType`, `size` and **never** `duration`. Every `duration` hit in
that tree is a 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.
## 3. What landed
- `packages/spec/src/data/field-value.zod.ts` — `durationSeconds:
z.number().optional().describe('Media duration in seconds')`, plus a
`retiredKey()` tombstone on `duration`
- `packages/spec/src/kernel/plugin-versioning.zod.ts` —
`estimatedMigrationTimeHours: z.number().optional().describe('Estimated
migration time in hours')`, plus a `retiredKey()` tombstone on
`estimatedMigrationTime`
- two ADR-0087 D3 semantic entries:
**`data-file-value-duration-unit-in-key`** and
**`kernel-compatibility-matrix-estimated-migration-time-unit-in-key`**
- two `RETIRED_KEYS_BY_MAJOR[18]` rows: `data/FileValue:duration` and
`kernel/CompatibilityMatrixEntry:estimatedMigrationTime`
- prose sweep: `content/docs/protocol/objectql/types.mdx`, the
`shared/duration.zod.ts` six-row census and its test comment
- regenerated: `authorable-surface/data.json`,
`authorable-surface/kernel.json`, `migrations/registry.ts`,
`content/docs/references/{data/field-value,kernel/plugin-versioning}.mdx`
Both authorable-surface rows are **top-level**, so the ratchet moves in
both files:
`data/FileValue:duration [RETIRED]` beside
`data/FileValue:durationSeconds`, and the same shape on
`kernel/CompatibilityMatrixEntry`. `authorable-surface.base.json` is
deliberately **not** re-anchored.
### Why the describe on the hours key is not cosmetic
Renaming `estimatedMigrationTime` alone would have produced a NEW gate
failure, not a fix: the key name
would carry `Hours`, the JSDoc would name hours, and the `.describe()`
would name nothing —
`check:duration-unit-keys` refuses exactly that as
`unit-in-jsdoc-not-in-describe` (the agreement shape,
ruled an offence 2026-09-18, decision batch #158 item 5, letter A). The
remedy the gate names is to state
the unit in the describe, which is what this does.
`FileValue.durationSeconds` gains one for the same
reader.
### Why no D2 conversion for either key
`FileValueSchema` is the ADR-0104 D3 wave-2 **expanded read** form,
derived at read time from a `sys_file`
id; the stored form is an opaque string. A plugin compatibility matrix
is a published version manifest and
`stack.zod.ts` declares no collection of them. Neither is ever a stored
`sys_metadata` row, so the
conversion chain has no seam that would see one — the same disposition
`ai-conversation-analytics-duration-unit-in-key` and the two metrics
entries already took.
## 4. Verification
Head under test: **`ccec095`**
(`ccec0959cdd66f0b6188d33473811a37c2b5216a`), after merging
`origin/main`
at `61170fa38`. The merge of `migrations/registry.ts` was a clean text
merge of two sorted-union
insertions; `check:migration-registry` reads the result as current (228
semantic, 203 retired-key, 181
retired-def), so nothing was hand-resolved.
**Reverse verification against the rebuilt `.d.ts`** — a throwaway probe
in `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** `TS2322 Type 'number' is not assignable to type
'undefined'` errors, one per old spelling, and nothing else
- green control leg: the same file with only `durationSeconds: 12.34`
and `estimatedMigrationTimeHours: 8` exits **0**
- the probe was removed and the removal proved by observation: `git diff
HEAD` empty and `git status --porcelain` empty, not by the delete
command's exit code
**Package tests** (each a real reading, not a summary):
| package | files | tests |
|:--|:--|:--|
| `@objectstack/spec` (the changed package) | 509 passed (509) | 14887
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 failed (273) | 3535 passed, 30
skipped, **0 failed** |
The consumer set is the packages that import `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 honest non-passes, neither a red gate:
- `@objectstack/cli` — the two failing FILES are
`test/published-subpath-console.pin.test.ts` and
`test/published-subpath-hook-body.pin.test.ts`, both of which refuse
their own prerequisite in words:
*"packages/cli is not built (./dist/index.js is absent) … Run: pnpm
--filter @objectstack/cli build"*.
Zero individual tests failed. **PREREQUISITE NOT MET**, not a pass and
not a failure.
- `@objectstack/runtime` — **NOT MEASURED**: its suite did not finish
inside this container's foreground
window and was cut at 560s. Declared to CI rather than reported as
green.
**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:
```
Run reconciliation — 109 derived, 108 run, 1 NOT-MEASURED, 0 UNRUN.
```
The one NOT-MEASURED is `node scripts/check-plugin-teardown-shape.mjs
--self-test`, exit **3**: its
positive-control fixture is pinned to commit
`621a487607881c66b2899b7e3477115229a156b4`, unreachable in
this shallow checkout. It refuses its own prerequisite rather than
reporting a verdict, and it grades the
checker, not this diff.
Sixteen ratchet-sensitive families were re-run at the final head
`ccec095` after the merge — `check:type-check-coverage`,
`check:type-check-debt`, `check:authorable-surface`,
`check:duration-unit-keys`, `check:api-surface`,
`check:generated`, `check:cross-package-test-inputs`,
`check:test-source-alias`, `check:query-options-erasure`,
`check:strictness-ledger`, `check:liveness`, `check:docs`,
`check:spec-changes`, `check:upgrade-guide`,
`check-adr-0087-registration --base origin/main`, `check:nul-bytes` —
all exit 0.
`check:duration-unit-keys` reads **205 unit-declaring numeric keys
across 2549 source files, zero
offenders, no baseline**, and its `--list` census shows both new keys
admitted and satisfied:
```
packages/spec/src/data/field-value.zod.ts:510 durationSeconds [name: seconds] [prose: seconds] "Media duration in seconds"
packages/spec/src/kernel/plugin-versioning.zod.ts:198 estimatedMigrationTimeHours [name: hours] [prose: hours] "Estimated migration time in hours"
```
**Lint — a declared narrowing, with its three readings.** `pnpm lint` is
`eslint . --no-inline-config`, a
whole-repo scan CI owns. Run here instead over the diff's lintable files
at `ccec095`:
(1) the universe is read from `eslint.config.mjs` itself, whose file
globs are
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`; (2) `--format json` 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 in this diff can
move a verdict on a file it does
not touch.
## Acceptance notes
Noted, not filed — each is prose about a measurement taken before this
rename, not a defect:
- `.changeset/18122-closed-duration-types.md` 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 would rewrite their record, so it is left verbatim.
Carrier if it ever matters: the
release that ships both changesets.
- `docs/adr/0104-field-runtime-value-shape-contract.md:459` prints the
wave-1 inline shape as
`{ url, name?, size?, mimeType?, alt?, duration? }`. ADR prose is a
decision record, and the measured
precedent is that renames do not sweep it — after `ApiEndpoint.cacheTtl`
became `cacheTtlSeconds` in spec
17, `docs/adr/0121-...md` still spells `cacheTtl` twice, and a control
grep for five other already-renamed
keys across `docs/adr/**` returns zero. Editing it would also make this
PR Tier H governed for a prose
line whose rename channel is the ADR-0087 ledger this PR adds. Carrier:
none — deliberately.
## Refused, and why
- ⛔ Did not adopt `DurationSeconds`/`DurationMs`, add `.int()`, or
narrow either value — ruling A forbids it, and the fractional-second
case is pinned so a later sweep cannot quietly do it.
- ⛔ Did not edit `docs/adr/**` or `.changeset/18122-...md` (above).
- ⛔ Did not re-anchor `packages/spec/authorable-surface.base.json`; its
`baseRev` note on a shallow checkout is informational and
`check:authorable-surface` is green.
- ⛔ Did not mark this ready, enqueue, merge or arm auto-merge.
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 77df0f6 commit 731f020
18 files changed
Lines changed: 509 additions & 23 deletions
File tree
- .changeset
- content/docs
- protocol/objectql
- references
- data
- kernel
- packages
- drivers/driver-sql/src
- spec
- authorable-surface
- src
- data
- kernel
- migrations
- entries
- retired-keys
- semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1161 | 1161 | | |
1162 | 1162 | | |
1163 | 1163 | | |
1164 | | - | |
| 1164 | + | |
1165 | 1165 | | |
1166 | 1166 | | |
1167 | 1167 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
123 | | - | |
| 123 | + | |
| 124 | + | |
124 | 125 | | |
125 | 126 | | |
126 | 127 | | |
| |||
145 | 146 | | |
146 | 147 | | |
147 | 148 | | |
148 | | - | |
| 149 | + | |
| 150 | + | |
149 | 151 | | |
150 | 152 | | |
151 | 153 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
80 | | - | |
| 80 | + | |
| 81 | + | |
81 | 82 | | |
82 | 83 | | |
83 | 84 | | |
| |||
191 | 192 | | |
192 | 193 | | |
193 | 194 | | |
194 | | - | |
| 195 | + | |
| 196 | + | |
195 | 197 | | |
196 | 198 | | |
197 | 199 | | |
| |||
285 | 287 | | |
286 | 288 | | |
287 | 289 | | |
288 | | - | |
| 290 | + | |
| 291 | + | |
289 | 292 | | |
290 | 293 | | |
291 | 294 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
119 | 119 | | |
120 | 120 | | |
121 | 121 | | |
122 | | - | |
123 | | - | |
| 122 | + | |
| 123 | + | |
124 | 124 | | |
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
131 | | - | |
132 | | - | |
| 131 | + | |
| 132 | + | |
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
443 | 443 | | |
444 | 444 | | |
445 | 445 | | |
446 | | - | |
| 446 | + | |
| 447 | + | |
447 | 448 | | |
448 | 449 | | |
449 | 450 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
| 34 | + | |
34 | 35 | | |
35 | 36 | | |
36 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
359 | 359 | | |
360 | 360 | | |
361 | 361 | | |
362 | | - | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
363 | 376 | | |
364 | 377 | | |
365 | 378 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| 43 | + | |
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
| |||
496 | 497 | | |
497 | 498 | | |
498 | 499 | | |
499 | | - | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
500 | 519 | | |
501 | 520 | | |
502 | 521 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
166 | 166 | | |
167 | 167 | | |
168 | 168 | | |
169 | | - | |
| 169 | + | |
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
173 | 173 | | |
174 | 174 | | |
175 | 175 | | |
176 | | - | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
177 | 216 | | |
178 | 217 | | |
179 | 218 | | |
| |||
421 | 460 | | |
422 | 461 | | |
423 | 462 | | |
424 | | - | |
| 463 | + | |
425 | 464 | | |
426 | 465 | | |
427 | 466 | | |
| |||
0 commit comments