|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(spec)!: a duration-shaped `z.number()` key carries its unit in the key name — `hook.timeout` / `job.timeout` / `DriverOptions.timeout` → `timeoutMs`, `MetadataManagerConfig.cache.ttl` → `ttlSeconds`, `cache.databaseLoader.ttl` → `ttlMs`, tenant `idleTimeout` / `sessionTimeout` → `*Seconds`; new gate `check:duration-unit-keys` (#14478, #14519) |
| 6 | + |
| 7 | +<!-- adr-0087: registered hook-timeout-to-timeout-ms, job-timeout-to-timeout-ms, metadata-manager-config-cache-ttl-unit-in-key, driver-options-timeout-to-timeout-ms, tenant-timeouts-unit-in-key --> |
| 8 | + |
| 9 | +**BREAKING** rename of seven published authorable keys, shipped as `minor` under |
| 10 | +the repo's launch-window convention for breaking changes; every rename is |
| 11 | +registered under protocol major 18. Maintainer ruling 2026-09-02 on #14478 |
| 12 | +(director decision batch #14, verbatim 「14461 你不处理,其他同意」): **ruled B** — |
| 13 | +a spec-source gate for duration-shaped number keys **with no grandfathered |
| 14 | +baseline**, plus an ADR-0087 conversion of every offender the ruling named, in |
| 15 | +one PR, on the standing rules 「不考虑存量」 and 「项目在创业阶段,用户也很少,短期不考虑渐进。」. |
| 16 | +⛔ No alias, no transition window: each old spelling is a `retiredKey()` |
| 17 | +tombstone whose rejection names the new key. |
| 18 | + |
| 19 | +## The defect |
| 20 | + |
| 21 | +`kernel/metadata-loader.zod.ts` carried two keys spelled `ttl` fourteen lines |
| 22 | +apart: `cache.ttl` in **seconds** (default 3600) and `cache.databaseLoader.ttl` |
| 23 | +in **milliseconds** (default 60000). Both descriptions named their unit; the |
| 24 | +key names did not. An author who copied the outer number into the inner block |
| 25 | +got a 3.6-second cache and no error anywhere — the number was valid, the type |
| 26 | +was right, the cache was simply cold. `hook.timeout`, `job.timeout` and |
| 27 | +`DriverOptions.timeout` had the same shape (milliseconds, said only in prose) |
| 28 | +beside siblings that spell theirs (`backoffMs`, `intervalMs`, the body-level |
| 29 | +`timeoutMs`). The two tenant keys were worse for the reader who matters most: |
| 30 | +`.describe()` is what `content/docs/references/**` publishes and the JSDoc above |
| 31 | +a key is not, so `idleTimeout` / `sessionTimeout` said "in seconds" in a source |
| 32 | +comment and published a bare `300` / `3600` to the reference page (#14519). |
| 33 | + |
| 34 | +## FROM → TO |
| 35 | + |
| 36 | +| schema | before | after | value | |
| 37 | +|:--|:--|:--|:--| |
| 38 | +| `HookSchema` (`hooks[]`) | `timeout` | `timeoutMs` | unchanged (ms) | |
| 39 | +| `JobSchema` (`jobs[]`) | `timeout` | `timeoutMs` | unchanged (ms) | |
| 40 | +| `DriverOptionsSchema` | `timeout` | `timeoutMs` | unchanged (ms) | |
| 41 | +| `MetadataManagerConfigSchema` | `cache.ttl` | `cache.ttlSeconds` | unchanged (s, default 3600) | |
| 42 | +| `MetadataManagerConfigSchema` | `cache.databaseLoader.ttl` | `cache.databaseLoader.ttlMs` | unchanged (ms, default 60000) | |
| 43 | +| `DatabaseLevelIsolationStrategySchema` | `connectionPool.idleTimeout` | `connectionPool.idleTimeoutSeconds` | unchanged (s, default 300) | |
| 44 | +| `TenantSecurityPolicySchema` | `accessControl.sessionTimeout` | `accessControl.sessionTimeoutSeconds` | unchanged (s, default 3600) | |
| 45 | + |
| 46 | +```ts |
| 47 | +// before |
| 48 | +defineHook({ name: 'audit_order', object: 'order', events: ['afterInsert'], handler: 'auditOrder', timeout: 5000 }); |
| 49 | +defineJob({ name: 'nightly_sweep', schedule: { type: 'cron', expression: '0 1 * * *' }, handler: 'sweep', timeout: 300000 }); |
| 50 | +new MetadataManager({ cache: { ttl: 3600, databaseLoader: { ttl: 60_000 } } }); |
| 51 | + |
| 52 | +// after — rename the key; the number is unchanged |
| 53 | +defineHook({ name: 'audit_order', object: 'order', events: ['afterInsert'], handler: 'auditOrder', timeoutMs: 5000 }); |
| 54 | +defineJob({ name: 'nightly_sweep', schedule: { type: 'cron', expression: '0 1 * * *' }, handler: 'sweep', timeoutMs: 300000 }); |
| 55 | +new MetadataManager({ cache: { ttlSeconds: 3600, databaseLoader: { ttlMs: 60_000 } } }); |
| 56 | +``` |
| 57 | + |
| 58 | +**Migration.** Rename each key; no value changes. Authoring an old spelling |
| 59 | +fails to compile (`tsc`: the input type is `never`) and fails to parse with a |
| 60 | +prescription naming the new key. For `hooks[]` / `jobs[]` the rename is a |
| 61 | +mechanical D2 conversion (`hook-timeout-to-timeout-ms`, |
| 62 | +`job-timeout-to-timeout-ms`, retired from the load path): run |
| 63 | +`os migrate meta --from 17` to list the edits for existing sources and apply |
| 64 | +them by hand; stored `sys_metadata` rows are rehydrated through the same chain. |
| 65 | +The other five keys have no stack seam (runtime config, a per-call options |
| 66 | +argument, cloud tenancy config) and carry a semantic entry each. The |
| 67 | +`JobScheduleOptions` contract key that carries `job.timeoutMs` to the scheduler |
| 68 | +is renamed in lockstep (`timeout` → `timeoutMs`), as is `DatabaseLoaderOptions.cache.ttl` → `ttlMs` in `@objectstack/metadata`. |
| 69 | + |
| 70 | +## The gate |
| 71 | + |
| 72 | +`pnpm --filter @objectstack/spec check:duration-unit-keys` |
| 73 | +(`packages/spec/scripts/check-duration-unit-keys.ts`, wired into `lint.yml`): |
| 74 | +a property whose value is a `z.number()` / `z.int()` / `z.coerce.number()` |
| 75 | +chain and whose `.describe()` names a time unit must carry that unit as a token |
| 76 | +of its key name (`Ms` / `Seconds` / `Minutes` / `Hours` / `Days`, and the |
| 77 | +knex-inherited `Millis`), and the token must agree with the prose — `ttlMs` |
| 78 | +described "in seconds" is refused too. A `{ value, unit }` pair is recognised |
| 79 | +by its sibling `unit` key; duration literals are strings and outside the |
| 80 | +population. Calendar positions ("day of the month") and rates ("requests per |
| 81 | +second") are skipped. There is no baseline and no `gen:`; a red is a rename |
| 82 | +under an ADR-0087 conversion or a describe to fix. |
0 commit comments