Skip to content

Commit fc28c1d

Browse files
claude[bot]claude
andauthored
feat(spec)!: tenant schemaCacheTTL carries its unit in the key name (#17784) (#17954)
Fixes #17784 Clause-②: yes Executes director-seat **ruling A on #15939** (2026-09-11, maintainer 「同意」, decision batch #115) — the per-file remediation of the #14478 duration-unit rule. This card owns exactly one row, in `packages/spec/src/system/tenant.zod.ts`. The gate PR (#17635) is sequenced to land LAST and is untouched here. ⚠️ The declaration reads `yes`, not the `no` ruling A wrote: the PM corrected it on the card (comment `5652103109`) under the mechanical floor in `references/contract-review.md` — a new key on a published payload is always `yes` — and a rename is a removal plus a new key. `needs:contract-review` is hung on card and PR; the changeset is graded `minor` accordingly, matching the four sibling duration-rename changesets of this same rule (#15677 · #15678 · #15679 · #15680), every one of which shipped as `feat(spec)!` under `## 17.4.0`. ## What changed `SchemaLevelIsolationStrategy.performance.schemaCacheTTL` named its unit in a source JSDoc — "Schema cache TTL in seconds" — and nowhere else. The `.describe()` that `content/docs/references/system/tenant.mdx` renders said "Schema cache TTL" and named no unit at all, so the reader of the published reference page could not tell 3600 seconds from 3600 milliseconds. | | before | after | |:--|:--|:--| | authored key | `performance.schemaCacheTTL: 3600` | `performance.schemaCacheTtlSeconds: 3600` | | published describe | `Schema cache TTL` | `Schema cache TTL in seconds` | | value + default | seconds, `3600` | unchanged | The new spelling is `Ttl`, not `TTL`, derived from how the suffixed family already spells itself on this tree rather than from the dispatch: `cacheTtlSeconds` 11, `ttlSeconds` 3, `defaultCacheTtlSeconds` 1, and no key-position `TTLSeconds` variant anywhere. The kit, following the #15678 / #15679 shape the ruling names: - a `retiredKey()` tombstone on the old spelling — `tsc` types it `never` and a value reaching the parse raises the rename prescription instead of being silently stripped (the nested `performance` object is not `.strict()`) - ADR-0087 **D3** semantic entry `tenant-schema-cache-ttl-unit-in-key` and the `RETIRED_KEYS_BY_MAJOR[18]` row `system/SchemaLevelIsolationStrategy:performance.schemaCacheTTL`, both as `migrations/entries/` files with `registry.ts` regenerated by `gen:migration-registry` (never hand-merged) - **no D2 conversion**: `stack.zod.ts` declares no tenancy collection and a tenant isolation strategy is not a stored metadata row, so the chain has no seam that runs on it — the same reading `tenant-timeouts-unit-in-key` recorded for the two sibling keys on this same file - pin tests on `SchemaLevelIsolationStrategySchema`: the refusal carries the rename prescription, the suffixed key parses at the magnitude the retired one carried with the same 3600 default, and the describe publishes the unit - `content/docs/references/system/tenant.mdx` regenerated by `gen:docs` — all four rows move and the tombstone prescription renders in place of the old describe - a `minor` changeset carrying the FROM to TO mapping and the ADR-0087 disposition ## The measurement this card was dispatched to produce This is the first of six renames, so the mechanical clause-② reading matters for the other five. **`node scripts/pm/check-widening-tells.mjs --declaration no` over this PR's own diff — exit 4, exactly one tell:** ``` ✗ T1 packages/spec/src/system/tenant.zod.ts:454 — a new key on a Zod object schema — the accept set gains a spelling an author may now write + schemaCacheTTL: retiredKey( ``` The tell is **not** on the rename. It is on the **tombstone**. Two synthetic probes separate the two halves on the same instrument: | probe | exit | tells | |:--|--:|:--| | the rename alone (one key line removed, one added, one change block) | 0 | none — the #16943 REPLACEMENT budget absorbs it | | an added `retiredKey(` key line with no paired removal | 4 | T1 on the tombstone line | So the accounting is exact: the removed `schemaCacheTTL:` line buys one T1 unit, the added `schemaCacheTtlSeconds:` line spends it, and the added `schemaCacheTTL: retiredKey(` line is the surplus. Every `retiredKey()` retirement raises this, and the line it raises on is the one that makes the accept set strictly **narrower** — the key becomes `never`. That is the same inversion this file's own header records for `#17300` (T2 on the retirement ledger's rows) and `#17618` (T1 on a narrowed in-shape key), one door further along. Filed as **#17955**; the diff was **not** reshaped to silence it and no gate was weakened. ⚠️ Read the two readings apart, because they agree on the verdict for different reasons: the mechanical tell is a **false positive on the tombstone line**, while the `yes` this PR now declares rests on the contract-review floor (the rename really does add a key an author may write). `check-clause2-carriers --pair 17954` read exit 4 / C5 against the original `no` and reads **exit 0** against the corrected `yes`. ## Verification Heavy runs through `scripts/pm/os-verify-lock.sh`, verdicts read from its own `VERDICT` line; every gate's exit code captured before any pipe. - `pnpm --filter '@objectstack/spec^...' build` — exit 0, **empty closure** (`No projects matched`): `packages/spec` has no workspace dependencies, so step ① is a documented no-op - `pnpm --filter @objectstack/spec build` — `VERDICT command-exit 0`, run before every dist-reading gate and re-run after the last source edit - `pnpm --filter @objectstack/spec test` — **475 test files / 13507 tests passed** - `pnpm --filter @objectstack/spec typecheck` — exit 0 (`tsc --noEmit` + `check:scripts-typecheck` + `check:test-typecheck`) - `pnpm --filter @objectstack/spec check:generated` — **all 15 generated artifacts up to date** after `gen:docs`; `check:authorable-surface`, `check:api-surface`, `check:migration-registry`, `check:spec-changes` and `check:upgrade-guide` among them - `check:authorable-surface` does not move for this key and that is correct: the ratchet records top-level keys per def and this one is nested under `performance` (0 hits for the key across `authorable-surface/` and `authorable-surface.base.json`, against 4 for the `system/MigrationPlan:` lit control) - `check:duration-unit-keys` (the gate as it stands on `main`, not #17635's) — exit 0 - `check:objectui-pin-citations` — exit 0, 16 asserting pin citations match `.objectui-sha` - cross-repo, per the ruling's execution note and AGENTS.md post-task step 4: `git grep` of the **pinned** objectui checkout at `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` (re-read from this tree) — `schemaCacheTTL` **0** occurrences across 6409 tracked files, beside lit controls `TTL` 112, `Ttl` 11, `tenant` 819, `cacheTTL` 1 and a dark control at 0, so the zero is a reading Gate set derived at the actual change set with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`; full per-gate exit codes, including the ones that could not run in this container, are in the round report on #17784. ## Acceptance notes - noted, not filed: the two in-file sibling renames landed under #14478 / #14519 registered a D3 semantic entry but **no** `RETIRED_KEYS_BY_MAJOR` row, while #15679's nested duration renames on `system/metrics.zod.ts` and `system/cache.zod.ts` registered both. This card follows #15679 (the shape ruling A names) and registers both. Successor: whoever lands the remaining four rename cards in this epic, who meets the same choice. - ⛔ Not touched, deliberately: `packages/spec/scripts/check-duration-unit-keys.ts` and its self-test (PR #17635, sequenced last, red by construction), the five sibling files owned by #17780 #17781 #17782 #17783 #17785, and `content/docs/releases/`. ⛔ Draft on purpose: the in-seat clause-② review this PR now declares is owed before it may turn ready or enqueue. That is the PM's step, not this round's. Round report, with every reading above and its exit code: the `os-dev-report` comment on #17784. --- _Generated by [Claude Code](https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 65767d2 commit fc28c1d

7 files changed

Lines changed: 220 additions & 8 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: the `system/tenant.zod.ts` schema-cache TTL key carries its unit in the key name (#17784, ruling A on #15939)
6+
7+
<!-- adr-0087: registered tenant-schema-cache-ttl-unit-in-key -->
8+
9+
**BREAKING** — the schema-cache TTL on the `isolated_schema` tenant isolation strategy carries
10+
its unit in the key name.
11+
12+
| | before | after |
13+
|:--|:--|:--|
14+
| authored key | `performance.schemaCacheTTL: 3600` | `performance.schemaCacheTtlSeconds: 3600` |
15+
| published describe | `Schema cache TTL` | `Schema cache TTL in seconds` |
16+
| value + default | seconds, `3600` | **unchanged** |
17+
18+
## Migration
19+
20+
```diff
21+
performance: {
22+
- schemaCacheTTL: 3600,
23+
+ schemaCacheTtlSeconds: 3600,
24+
}
25+
```
26+
27+
Rename the key. The value is the same number of seconds it always was, and the `3600` default is
28+
unchanged; nothing else on `SchemaLevelIsolationStrategy` moves.
29+
30+
## Why
31+
32+
The key named its unit in a source JSDoc — "Schema cache TTL in seconds" — and nowhere else. The
33+
`.describe()` that `content/docs/references/system/tenant.mdx` renders said "Schema cache TTL" and
34+
named no unit at all, so the one reader who most needs it, the reader of the published reference
35+
page, was the only reader who never saw it: `3600` is a plausible number of seconds and a plausible
36+
number of milliseconds, and nothing on the page decided between them. Executes director-seat ruling
37+
A on #15939 (2026-09-11, maintainer 「同意」, decision batch #115), the per-file remediation of the
38+
#14478 rule — under that rule, moving the unit into the describe alone is itself a violation (unit
39+
in prose, none in the name), so the key is renamed and the describe is corrected together.
40+
41+
The new spelling is `Ttl`, not `TTL`: counted on this tree, every member of the suffixed family
42+
already spells it that way — `cacheTtlSeconds` (11), `ttlSeconds` (3), `defaultCacheTtlSeconds` (1).
43+
44+
## The kit
45+
46+
- a `retiredKey()` tombstone on the old spelling, so `tsc` types it `never` and a value reaching the
47+
parse raises the rename prescription instead of being silently stripped (the nested `performance`
48+
object is not `.strict()`)
49+
- the ADR-0087 D3 semantic entry `tenant-schema-cache-ttl-unit-in-key` and the
50+
`RETIRED_KEYS_BY_MAJOR[18]` row `system/SchemaLevelIsolationStrategy:performance.schemaCacheTTL`.
51+
No D2 conversion: `stack.zod.ts` declares no tenancy collection and a tenant isolation strategy is
52+
not a stored metadata row, so the chain has no seam that runs on it — the same reading
53+
`tenant-timeouts-unit-in-key` recorded for the two sibling keys on this file
54+
- pin tests on `SchemaLevelIsolationStrategySchema`: the refusal carries the rename prescription, the
55+
suffixed key parses at the magnitude the retired one carried with the same `3600` default, and the
56+
describe publishes the unit
57+
- no authorable-surface row moves — that ratchet records top-level keys per def, and this one is
58+
nested under `performance` (measured: 0 hits for the key across `authorable-surface/` and
59+
`authorable-surface.base.json`, against 4 for the `system/MigrationPlan:` control)

content/docs/references/system/tenant.mdx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Quota enforcement check result
151151
| **strategy** | `'isolated_schema'` || Schema-level isolation strategy |
152152
| **schema** | `{ namingPattern: string; includePublicSchema: boolean; sharedSchema: string; autoCreateSchema: boolean }` | optional | Schema configuration |
153153
| **migrations** | `{ strategy: Enum<'parallel' \| 'sequential' \| 'on_demand'>; maxConcurrent: integer; rollbackOnError: boolean }` | optional | Migration configuration |
154-
| **performance** | `{ poolPerSchema: boolean; schemaCacheTTL: integer }` | optional | Performance settings |
154+
| **performance** | `{ poolPerSchema: boolean; schemaCacheTtlSeconds: integer }` | optional | Performance settings |
155155

156156
### Nested Shape: `SchemaLevelIsolationStrategy.schema`
157157

@@ -175,7 +175,8 @@ Quota enforcement check result
175175
| Property | Type | Required | Description |
176176
| :--- | :--- | :--- | :--- |
177177
| **poolPerSchema** | `boolean` | optional (default: `false`) | Separate pool per schema |
178-
| **schemaCacheTTL** | `integer` | optional (default: `3600`) | Schema cache TTL |
178+
| **schemaCacheTtlSeconds** | `integer` | optional (default: `3600`) | Schema cache TTL in seconds |
179+
| **schemaCacheTTL** | `never` | optional | [REMOVED] `performance.schemaCacheTTL` was renamed to `schemaCacheTtlSeconds` on `SchemaLevelIsolationStrategy` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Its unit (seconds) lived in a source comment only and the published description named none, so a reader of the reference page could not tell 3600 seconds from 3600 milliseconds. Rename the key to `schemaCacheTtlSeconds`; the value (seconds) is unchanged. |
179180

180181

181182
---
@@ -278,7 +279,7 @@ This schema accepts one of the following structures:
278279
| **strategy** | `'isolated_schema'` || Schema-level isolation strategy |
279280
| **schema** | `{ namingPattern: string; includePublicSchema: boolean; sharedSchema: string; autoCreateSchema: boolean }` | optional | Schema configuration |
280281
| **migrations** | `{ strategy: Enum<'parallel' \| 'sequential' \| 'on_demand'>; maxConcurrent: integer; rollbackOnError: boolean }` | optional | Migration configuration |
281-
| **performance** | `{ poolPerSchema: boolean; schemaCacheTTL: integer }` | optional | Performance settings |
282+
| **performance** | `{ poolPerSchema: boolean; schemaCacheTtlSeconds: integer }` | optional | Performance settings |
282283

283284
### Nested Shape: `TenantIsolationConfig[strategy='isolated_schema'].schema`
284285

@@ -302,7 +303,8 @@ This schema accepts one of the following structures:
302303
| Property | Type | Required | Description |
303304
| :--- | :--- | :--- | :--- |
304305
| **poolPerSchema** | `boolean` | optional (default: `false`) | Separate pool per schema |
305-
| **schemaCacheTTL** | `integer` | optional (default: `3600`) | Schema cache TTL |
306+
| **schemaCacheTtlSeconds** | `integer` | optional (default: `3600`) | Schema cache TTL in seconds |
307+
| **schemaCacheTTL** | `never` | optional | [REMOVED] `performance.schemaCacheTTL` was renamed to `schemaCacheTtlSeconds` on `SchemaLevelIsolationStrategy` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Its unit (seconds) lived in a source comment only and the published description named none, so a reader of the reference page could not tell 3600 seconds from 3600 milliseconds. Rename the key to `schemaCacheTtlSeconds`; the value (seconds) is unchanged. |
306308

307309
---
308310

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// #15939 ruling A (per-file remediation of #14478). `performance.schemaCacheTTL`
4+
// said "Schema cache TTL in seconds" in a source JSDoc and "Schema cache TTL" in
5+
// the `.describe()` the reference pages publish, so the published channel named
6+
// no unit at all. Renamed to `schemaCacheTtlSeconds` — `Ttl`, not `TTL`, because
7+
// that is how every member of the suffixed family on this tree already spells it
8+
// (`cacheTtlSeconds`, `ttlSeconds`, `defaultCacheTtlSeconds`). The value and the
9+
// 3600 default are unchanged. Tombstoned with `retiredKey()`: the nested
10+
// `performance` object is not strict, so a bare deletion would silently strip the
11+
// key. No D2 conversion: not a stack collection member, not a stored row. See
12+
// `tenant-schema-cache-ttl-unit-in-key`.
13+
export const entry = 'system/SchemaLevelIsolationStrategy:performance.schemaCacheTTL';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
export const entry: SemanticMigration = {
6+
id: 'tenant-schema-cache-ttl-unit-in-key',
7+
surface: 'SchemaLevelIsolationStrategy `performance.schemaCacheTTL` (system/tenant.zod.ts)',
8+
replacement: '`performance.schemaCacheTtlSeconds` (default 3600) — rename the key; the value '
9+
+ '(seconds) is unchanged',
10+
reason:
11+
'Director-seat ruling A on #15939, 2026-09-11, carrying the maintainer\'s 「同意」 (decision '
12+
+ 'batch #115), executing the #14478 rule per file. The key carried its unit (seconds) in a '
13+
+ 'source JSDoc only — "Schema cache TTL in seconds" — while `.describe()`, the text '
14+
+ '`content/docs/references/**` publishes, said "Schema cache TTL" and named no unit at all. '
15+
+ 'So the reader who most needs the unit, the reader of the published reference page, was the '
16+
+ 'only reader who never saw it: 3600 is a plausible number of seconds and a plausible number '
17+
+ 'of milliseconds, and nothing on the page decided it. Under the #14478 gate, moving the unit '
18+
+ 'into the describe alone is itself a violation (unit in prose, none in the name), so the key '
19+
+ 'is renamed and the describe is corrected in the same stroke. Spelled `Ttl` and not `TTL`: '
20+
+ 'counted on this tree, the suffixed family already spells it that way in every member '
21+
+ '(`cacheTtlSeconds` 11, `ttlSeconds` 3, `defaultCacheTtlSeconds` 1) and no key-position '
22+
+ '`TtlSeconds` variant spells it otherwise. Tombstoned with `retiredKey()` because the nested '
23+
+ '`performance` object is not strict, so a bare deletion would silently strip the key. Why a '
24+
+ 'semantic entry and not a D2 conversion: `stack.zod.ts` declares no tenancy collection and a '
25+
+ 'tenant isolation strategy is not a stored metadata row (it describes cloud tenancy '
26+
+ 'configuration), so the chain has no seam that runs on it — the same reading '
27+
+ '`tenant-timeouts-unit-in-key` recorded for the two sibling keys on this file. Measured on '
28+
+ 'bd25e897dc: no in-repo runtime reads the key — outside `packages/spec/src/system/tenant.zod.ts` '
29+
+ 'and its test the only occurrences are the four generated rows in '
30+
+ '`content/docs/references/system/tenant.mdx`, which this rename regenerates; and the pinned '
31+
+ 'objectui checkout — `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` — spells it 0 '
32+
+ 'times across 6409 tracked files, against lit controls `TTL` 112 and `tenant` 819 on the '
33+
+ 'same corpus.',
34+
acceptanceCriteria:
35+
'Every schema-level tenant isolation source spells `performance.schemaCacheTtlSeconds`; '
36+
+ 'authoring `performance.schemaCacheTTL` fails to compile and fails to parse with the rename '
37+
+ 'prescription naming the suffixed key; the parsed default is 3600 as before, and the '
38+
+ 'published describe reads "Schema cache TTL in seconds".',
39+
};

packages/spec/src/migrations/registry.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10479,6 +10479,41 @@ const step18: MigrationStep = {
1047910479
+ '{ max: 100, duration: 60000 } was, and the positive-integer bound rides along with the '
1048010480
+ 'renamed key. The sibling max is a COUNT and keeps its name — it has no unit to carry.',
1048110481
},
10482+
{
10483+
id: 'tenant-schema-cache-ttl-unit-in-key',
10484+
surface: 'SchemaLevelIsolationStrategy `performance.schemaCacheTTL` (system/tenant.zod.ts)',
10485+
replacement: '`performance.schemaCacheTtlSeconds` (default 3600) — rename the key; the value '
10486+
+ '(seconds) is unchanged',
10487+
reason:
10488+
'Director-seat ruling A on #15939, 2026-09-11, carrying the maintainer\'s 「同意」 (decision '
10489+
+ 'batch #115), executing the #14478 rule per file. The key carried its unit (seconds) in a '
10490+
+ 'source JSDoc only — "Schema cache TTL in seconds" — while `.describe()`, the text '
10491+
+ '`content/docs/references/**` publishes, said "Schema cache TTL" and named no unit at all. '
10492+
+ 'So the reader who most needs the unit, the reader of the published reference page, was the '
10493+
+ 'only reader who never saw it: 3600 is a plausible number of seconds and a plausible number '
10494+
+ 'of milliseconds, and nothing on the page decided it. Under the #14478 gate, moving the unit '
10495+
+ 'into the describe alone is itself a violation (unit in prose, none in the name), so the key '
10496+
+ 'is renamed and the describe is corrected in the same stroke. Spelled `Ttl` and not `TTL`: '
10497+
+ 'counted on this tree, the suffixed family already spells it that way in every member '
10498+
+ '(`cacheTtlSeconds` 11, `ttlSeconds` 3, `defaultCacheTtlSeconds` 1) and no key-position '
10499+
+ '`TtlSeconds` variant spells it otherwise. Tombstoned with `retiredKey()` because the nested '
10500+
+ '`performance` object is not strict, so a bare deletion would silently strip the key. Why a '
10501+
+ 'semantic entry and not a D2 conversion: `stack.zod.ts` declares no tenancy collection and a '
10502+
+ 'tenant isolation strategy is not a stored metadata row (it describes cloud tenancy '
10503+
+ 'configuration), so the chain has no seam that runs on it — the same reading '
10504+
+ '`tenant-timeouts-unit-in-key` recorded for the two sibling keys on this file. Measured on '
10505+
+ 'bd25e897dc: no in-repo runtime reads the key — outside `packages/spec/src/system/tenant.zod.ts` '
10506+
+ 'and its test the only occurrences are the four generated rows in '
10507+
+ '`content/docs/references/system/tenant.mdx`, which this rename regenerates; and the pinned '
10508+
+ 'objectui checkout — `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` — spells it 0 '
10509+
+ 'times across 6409 tracked files, against lit controls `TTL` 112 and `tenant` 819 on the '
10510+
+ 'same corpus.',
10511+
acceptanceCriteria:
10512+
'Every schema-level tenant isolation source spells `performance.schemaCacheTtlSeconds`; '
10513+
+ 'authoring `performance.schemaCacheTTL` fails to compile and fails to parse with the rename '
10514+
+ 'prescription naming the suffixed key; the parsed default is 3600 as before, and the '
10515+
+ 'published describe reads "Schema cache TTL in seconds".',
10516+
},
1048210517
{
1048310518
id: 'tenant-timeouts-unit-in-key',
1048410519
surface: 'DatabaseLevelIsolationStrategy `connectionPool.idleTimeout` / TenantSecurityPolicy '
@@ -13198,6 +13233,17 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
1319813233
// `api/BatchEndpointsConfig:operations.upsertMany` are.
1319913234
// D3 semantic entry: `change-management-duration-keys-retired`.
1320013235
'system/RollbackPlan:steps.estimatedMinutes',
13236+
// #15939 ruling A (per-file remediation of #14478). `performance.schemaCacheTTL`
13237+
// said "Schema cache TTL in seconds" in a source JSDoc and "Schema cache TTL" in
13238+
// the `.describe()` the reference pages publish, so the published channel named
13239+
// no unit at all. Renamed to `schemaCacheTtlSeconds` — `Ttl`, not `TTL`, because
13240+
// that is how every member of the suffixed family on this tree already spells it
13241+
// (`cacheTtlSeconds`, `ttlSeconds`, `defaultCacheTtlSeconds`). The value and the
13242+
// 3600 default are unchanged. Tombstoned with `retiredKey()`: the nested
13243+
// `performance` object is not strict, so a bare deletion would silently strip the
13244+
// key. No D2 conversion: not a stack collection member, not a stored row. See
13245+
// `tenant-schema-cache-ttl-unit-in-key`.
13246+
'system/SchemaLevelIsolationStrategy:performance.schemaCacheTTL',
1320113247
// #15679 (stack card 4/6 of #14478) — ruling B. The second of the two
1320213248
// byte-identical `window.size` declarations in `metrics.zod.ts`; it carries the
1320313249
// same prose and takes the same new name, `durationSeconds`, for the reason

packages/spec/src/system/tenant.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ describe('SchemaLevelIsolationStrategySchema', () => {
271271
},
272272
performance: {
273273
poolPerSchema: false,
274-
schemaCacheTTL: 3600,
274+
schemaCacheTtlSeconds: 3600,
275275
},
276276
};
277277

@@ -754,3 +754,41 @@ describe('tenant idleTimeout / sessionTimeout → *Seconds (#14478, #14519)', ()
754754
expect(access.description).toBe('Session timeout in seconds');
755755
});
756756
});
757+
758+
// #15939 ruling A (executing #14478) — the third duration key on this file to
759+
// carry its unit in a source JSDoc only. `.describe()` said "Schema cache TTL",
760+
// so the reference-page reader could not tell 3600 seconds from 3600
761+
// milliseconds. Renamed with the unit in the key; the old spelling is a
762+
// retiredKey tombstone (the nested `performance` object is not strict).
763+
describe('tenant schemaCacheTTL → schemaCacheTtlSeconds (#15939, #14478)', () => {
764+
it('REFUSES `performance.schemaCacheTTL` with a rename naming `schemaCacheTtlSeconds`', () => {
765+
const result = SchemaLevelIsolationStrategySchema.safeParse({
766+
strategy: 'isolated_schema',
767+
performance: { schemaCacheTTL: 3600 },
768+
});
769+
expect(result.success).toBe(false);
770+
const issue = result.error!.issues.find((i) => i.path.join('.') === 'performance.schemaCacheTTL');
771+
expect(issue).toBeDefined();
772+
expect(issue!.message).toMatch(
773+
/`performance\.schemaCacheTTL` was renamed.*Rename the key to `schemaCacheTtlSeconds`/s,
774+
);
775+
});
776+
777+
it('accepts the suffixed key at the magnitude the retired key carried, with the same default', () => {
778+
const parsed = SchemaLevelIsolationStrategySchema.parse({
779+
strategy: 'isolated_schema',
780+
performance: { schemaCacheTtlSeconds: 7200 },
781+
});
782+
expect(parsed.performance?.schemaCacheTtlSeconds).toBe(7200);
783+
expect(parsed.performance).not.toHaveProperty('schemaCacheTTL');
784+
expect(
785+
SchemaLevelIsolationStrategySchema.parse({ strategy: 'isolated_schema', performance: {} })
786+
.performance?.schemaCacheTtlSeconds,
787+
).toBe(3600);
788+
});
789+
790+
it('publishes the unit in the describe — the text the reference pages render', () => {
791+
const cache = SchemaLevelIsolationStrategySchema.shape.performance.unwrap().shape.schemaCacheTtlSeconds;
792+
expect(cache.description).toBe('Schema cache TTL in seconds');
793+
});
794+
});

packages/spec/src/system/tenant.zod.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,24 @@ export const SchemaLevelIsolationStrategySchema = lazySchema(() => z.object({
441441
poolPerSchema: z.boolean().default(false).describe('Separate pool per schema'),
442442

443443
/**
444-
* Schema cache TTL in seconds
445-
*/
446-
schemaCacheTTL: z.number().int().positive().default(3600).describe('Schema cache TTL'),
444+
* Schema cache TTL in seconds.
445+
*
446+
* Renamed from `schemaCacheTTL` (#15939 ruling A, executing #14478): the unit
447+
* lived in this JSDoc only, and `.describe()` — the text the reference pages
448+
* publish — carried none. Spelled `Ttl`, not `TTL`, because that is how the
449+
* suffixed family already spells it (`cacheTtlSeconds`, `ttlSeconds`,
450+
* `defaultCacheTtlSeconds`). Tombstoned rather than deleted because this
451+
* nested object is not `.strict()`.
452+
*/
453+
schemaCacheTtlSeconds: z.number().int().positive().default(3600).describe('Schema cache TTL in seconds'),
454+
schemaCacheTTL: retiredKey(
455+
'`performance.schemaCacheTTL` was renamed to `schemaCacheTtlSeconds` on ' +
456+
'`SchemaLevelIsolationStrategy` in @objectstack/spec 17 — the unit of a duration-shaped ' +
457+
'number lives in the key name, not only in the describe prose. Its unit (seconds) lived in a ' +
458+
'source comment only and the published description named none, so a reader of the reference ' +
459+
'page could not tell 3600 seconds from 3600 milliseconds. Rename the key to ' +
460+
'`schemaCacheTtlSeconds`; the value (seconds) is unchanged.',
461+
),
447462
}).optional().describe('Performance settings'),
448463
}));
449464

0 commit comments

Comments
 (0)