Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .changeset/17157-cache-warmup-scheduled-strategy-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
"@objectstack/spec": minor
---

feat(spec)!: retire the `scheduled` cache-warmup strategy — the cron it selected left in this same major, and nothing ever warmed on a cadence (ADR-0049)

<!-- adr-0087: registered cache-warmup-scheduled-strategy-retired -->

**BREAKING** in the accept-set sense, landing in the launch window as `minor` (the
lockstep convention: `major` is refused by `check-changeset-no-major`, and breaking-ness
is carried by this banner plus the ADR-0087 disposition above).

`CacheWarmup.strategy` no longer accepts `'scheduled'`.

| | before | after |
|:--|:--|:--|
| accept set | `'eager' \| 'lazy' \| 'scheduled'` | `'eager' \| 'lazy'` |
| describe | `… lazy (on first access), scheduled (cron)` | `… lazy (on first access)` |
| a document writing it | parsed green | **refused**, with the prescription |

**The one-line fix:** write `strategy: 'eager'` (warm at startup) or `strategy: 'lazy'`
(warm on first access). For a warmup on a **cadence**, declare a `job` — that is the one
cron slot this platform evaluates:

```ts
defineStack({
jobs: [{ name: 'warm_config_cache', schedule: { expression: '0 * * * *' }, handler: 'warmConfigCache' }],
});
```

## Why

`cron-typed-positions-retired` (17.x → 18, #16320) deleted `CacheWarmup.schedule`, the
cron key this enum member selected, and left the member standing on the reading that it is
"a value, not a position the ruling names". That was a statement about that ruling's
**scope**, not a finding that the value was sound. After the deletion the member declared a
warmup cadence with **no key left to configure it and no engine that has ever run one**,
while its own `.describe()` still promised `(cron)` — ADR-0049 declared-not-enforced, in
the form Prime Directive 10 names outright: a capability advertised that the runtime does
not deliver.

Nothing on the platform reads `CacheWarmupSchema`: outside its declaring file it resolves
to the generated reference page's import line, the `declaration-map` / `export-origins`
catalogues, the ADR-0058 D7 ledger comment and two of this package's own test files — zero
runtime consumers, measured beside a lit control (`ConnectorSchema`, 46 files, same sweep).
So **no runtime behaviour changes**: no warmup has ever run on a schedule, before or after.
What changes is that the contract stops promising it.

## The retirement kit

- the member leaves `z.enum(['eager','lazy','scheduled'])` and the `.describe()` stops
saying `(cron)` (`system/cache.zod.ts`)
- the prescription hangs on **the enum's own `error` map, dispatched by `issue.input`** —
the established route for an enum-VALUE retirement (`crypto.hash` on
`HookBodyCapability`, `object.managedBy: 'system'`, `HotReloadConfig.stateStrategy`).
There is no value-level analogue of `retiredKey()` and none is invented here. Only the
value that **used to be legal** gets the "was removed" sentence; `strategy: 'sheduled'`
keeps zod's own enum message, which already lists the legal values
- an **ADR-0087 D3 semantic entry**, `cache-warmup-scheduled-strategy-retired` — a semantic
entry rather than a D2 conversion because there is **no source to rewrite**: `CacheWarmup`
is bound to no metadata type and embedded in no stack collection, so no authored document
and no stored row has ever carried this value, and `os migrate meta` has nothing to list.
That is also why the prescription carries **no `os migrate meta` sentence** — it would
promise a listing the tool cannot produce, which is the very defect this card is about
- **nothing in `RETIRED_KEYS_BY_MAJOR`** — no authorable *key* changed — and **no
`retiredKey()` tombstone**, which tombstones keys, not values
- pin tests (`system/cache.test.ts`): the refusal and its prescription, a **lit control**
that a typo is *not* told it "was removed", and that the surviving members and the
`'lazy'` default still parse. `cron-typed-positions-retirement.test.ts`'s warmup fixture
moves to `'eager'`, since a fixture must be well-formed under the current schema

## ⚠️ The four surface ratchets are byte-identical across this change, and that is correct

An enum-VALUE narrowing moves no position, no exported name and no expression-typed slot:
`authorable-surface/` keys on **positions** (`system/CacheWarmup:strategy` stays — the key
is untouched), the ADR-0058 D7 ledger on **expression-typed slots**, and `api-surface/` /
`json-schema.manifest/` on **names**. None of them reads a def's *value set*, so none of
them can fail on this change — the `crypto.hash` precedent measured exactly this. The pin
tests above are therefore not a formality: they are the only instrument this retirement
has, and a green CI run on its own says nothing about whether the value is gone.
6 changes: 3 additions & 3 deletions content/docs/references/system/cache.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Cache warmup strategy
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | optional (default: `false`) | Enable cache warmup |
| **strategy** | `Enum<'eager' \| 'lazy' \| 'scheduled'>` | optional (default: `"lazy"`) | Warmup strategy: eager (at startup), lazy (on first access), scheduled (cron) |
| **strategy** | `Enum<'eager' \| 'lazy'>` | optional (default: `"lazy"`) | Warmup strategy: eager (at startup), lazy (on first access) |
| **patterns** | `string[]` | optional | Key patterns to warm up (e.g., "user:*", "config:*") |
| **concurrency** | `number` | optional (default: `10`) | Maximum concurrent warmup operations |

Expand All @@ -220,7 +220,7 @@ Distributed cache configuration with consistency and avalanche prevention
| **encryption** | `boolean` | optional (default: `false`) | Enable encryption for cached data |
| **consistency** | `Enum<'write_through' \| 'write_behind' \| 'write_around' \| 'refresh_ahead'>` | optional | Distributed cache consistency strategy |
| **avalanchePrevention** | `{ jitterTtl?: object; circuitBreaker?: object; lockout?: object }` | optional | Cache avalanche and stampede prevention |
| **warmup** | `{ enabled: boolean; strategy: Enum<'eager' \| 'lazy' \| 'scheduled'>; patterns?: string[]; concurrency: number }` | optional | Cache warmup strategy |
| **warmup** | `{ enabled: boolean; strategy: Enum<'eager' \| 'lazy'>; patterns?: string[]; concurrency: number }` | optional | Cache warmup strategy |

### Nested Shape: `DistributedCacheConfig.tiers[number]`

Expand Down Expand Up @@ -260,7 +260,7 @@ Rule defining when and how cached entries are invalidated
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | optional (default: `false`) | Enable cache warmup |
| **strategy** | `Enum<'eager' \| 'lazy' \| 'scheduled'>` | optional (default: `"lazy"`) | Warmup strategy: eager (at startup), lazy (on first access), scheduled (cron) |
| **strategy** | `Enum<'eager' \| 'lazy'>` | optional (default: `"lazy"`) | Warmup strategy: eager (at startup), lazy (on first access) |
| **patterns** | `string[]` | optional | Key patterns to warm up (e.g., "user:*", "config:*") |
| **concurrency** | `number` | optional (default: `10`) | Maximum concurrent warmup operations |

Expand Down
6 changes: 5 additions & 1 deletion packages/spec/src/cron-typed-positions-retirement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ const EXPORT_WELL_FORMED = {
const STATE_WELL_FORMED = { id: 'sched_001', flowName: 'daily_report', createdAt: '2026-01-01T00:00:00Z' };
const SYNC_WELL_FORMED = { strategy: 'incremental' as const, direction: 'bidirectional' as const, batchSize: 500 };
const CONNECTOR_WELL_FORMED = { name: 'sap_erp', label: 'SAP ERP', type: 'saas' as const, syncConfig: SYNC_WELL_FORMED };
const WARMUP_WELL_FORMED = { enabled: true, strategy: 'scheduled' as const, patterns: ['config:*'] };
// [#17157] was `strategy: 'scheduled'` — that enum member was itself retired one card
// later, in this same major, precisely because the cron key stripped below was its
// only referent. A fixture must be well-formed under the CURRENT schema, so it now
// carries `eager`; what this file pins is the absence of `schedule`, unchanged.
const WARMUP_WELL_FORMED = { enabled: true, strategy: 'eager' as const, patterns: ['config:*'] };
const CACHE_WELL_FORMED = {
enabled: true,
tiers: [{ name: 'l1', type: 'memory' as const }],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import type { SemanticMigration } from '../../types.js';

export const entry: SemanticMigration = {
id: 'cache-warmup-scheduled-strategy-retired',
// No backticks in `surface` — build-upgrade-guide.ts renders it inside a code
// span AND a table cell.
surface:
"CacheWarmup.strategy — the value 'scheduled' left the warmup-strategy enum "
+ '(packages/spec/src/system/cache.zod.ts), and the enum describe stopped promising '
+ '"scheduled (cron)". The key itself, DistributedCacheConfig.warmup.strategy, is '
+ 'unchanged and still authorable',
replacement:
"'eager' to warm at startup or 'lazy' to warm on first access — the two strategies "
+ 'the vocabulary ever described without pointing outside itself. There is no '
+ 'replacement for the cadence: a warmup on a schedule is a job. Declare a `job` with '
+ 'schedule.expression (system/job.zod.ts) whose handler does the warming — that is '
+ 'the one cron slot this platform evaluates, and it is the slot #16320 deliberately '
+ 'kept when it deleted the other seven',
reason:
'ADR-0049 enforce-or-remove, closing the residue #16320 left inside the schema it had '
+ 'just edited. That card deleted CacheWarmup.schedule — the cron key this enum member '
+ 'selected — and declined the member itself on the reading that it is "a value, not a '
+ "position this ruling names\". That is a statement about the ruling's SCOPE, not a "
+ 'finding that the value was sound: after the deletion the member declared a warmup '
+ 'cadence with no key left to configure it, no engine that has ever run one, and a '
+ '.describe() still promising "(cron)" — ADR-0049 declared-not-enforced in the form '
+ 'Prime Directive 10 names outright, a capability advertised that the runtime does '
+ 'not deliver. Re-measured on main at 690f083f83 with a lit control rather than '
+ 'inherited from the card: CacheWarmupSchema has zero runtime consumers outside its '
+ 'declaring file (six files reference it — the generated reference page import, the '
+ 'declaration-map and export-origins catalogues, the ADR-0058 D7 ledger comment and '
+ 'two spec test files — while the control, ConnectorSchema, resolves to 46 files), '
+ 'and no cache-warmup engine exists anywhere on the platform. Bookkeeping follows the '
+ "hot-reload-inert-state-strategies-retired and crypto.hash precedents: an enum-VALUE "
+ 'narrowing puts nothing in RETIRED_KEYS_BY_MAJOR (no authorable KEY changed) and '
+ 'leaves the four surface ratchets byte-identical (no def changed, and they key on '
+ "positions and names, never on a def's value set), so the prescription hangs on the "
+ "enum's own error map dispatched by issue.input — telling the author of a TYPO that "
+ 'their value "was removed" would misinform. It is a SEMANTIC entry rather than a D2 '
+ 'conversion because there is no source to rewrite: CacheWarmup is bound to no '
+ 'metadata type and embedded in no stack collection, so no authored document and no '
+ 'stored row has ever carried this value, and os migrate meta has nothing to list. '
+ 'Route 3 of the retirement playbook, the #4834 / #11825 shape: this entry IS the '
+ 'declaration. ADR-0049, ADR-0087, #17157, #16320.',
acceptanceCriteria:
"No configuration passes strategy: 'scheduled' to CacheWarmupSchema or to "
+ 'DistributedCacheConfigSchema.warmup. TypeScript callers cannot: '
+ "CacheWarmup['strategy'] is now 'eager' | 'lazy', so the literal is a compile error "
+ 'at the authoring site. Callers that arrive as JSON get a parse REFUSAL — not the '
+ 'silent strip #16320 left for the schedule key beside it, because a narrowed enum '
+ 'rejects rather than drops — carrying the prescription, which names the job route. '
+ 'Concretely, check two places. (1) Any host or deployment config embedding a '
+ 'DistributedCacheConfig: a warmup block selecting the retired strategy now fails to '
+ 'parse where it previously parsed green; change it to eager or lazy. (2) Anything '
+ 'that was waiting on the cadence to take effect: it never did. No warmup has ever '
+ 'run on a schedule on this platform, so migrating the value changes no runtime '
+ 'behaviour whatsoever — what changes is that the contract stops promising it. If a '
+ 'scheduled warmup is genuinely wanted, it comes back through the ENFORCE leg of '
+ 'ADR-0049: the engine first, the declaration with it, never as a bare enum row '
+ 'again.',
};
59 changes: 59 additions & 0 deletions packages/spec/src/migrations/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5940,6 +5940,65 @@ const step18: MigrationStep = {
+ 'value, so no source rewrite ships and `objectstack migrate meta` has '
+ 'nothing to visit.',
},
{
id: 'cache-warmup-scheduled-strategy-retired',
// No backticks in `surface` — build-upgrade-guide.ts renders it inside a code
// span AND a table cell.
surface:
"CacheWarmup.strategy — the value 'scheduled' left the warmup-strategy enum "
+ '(packages/spec/src/system/cache.zod.ts), and the enum describe stopped promising '
+ '"scheduled (cron)". The key itself, DistributedCacheConfig.warmup.strategy, is '
+ 'unchanged and still authorable',
replacement:
"'eager' to warm at startup or 'lazy' to warm on first access — the two strategies "
+ 'the vocabulary ever described without pointing outside itself. There is no '
+ 'replacement for the cadence: a warmup on a schedule is a job. Declare a `job` with '
+ 'schedule.expression (system/job.zod.ts) whose handler does the warming — that is '
+ 'the one cron slot this platform evaluates, and it is the slot #16320 deliberately '
+ 'kept when it deleted the other seven',
reason:
'ADR-0049 enforce-or-remove, closing the residue #16320 left inside the schema it had '
+ 'just edited. That card deleted CacheWarmup.schedule — the cron key this enum member '
+ 'selected — and declined the member itself on the reading that it is "a value, not a '
+ "position this ruling names\". That is a statement about the ruling's SCOPE, not a "
+ 'finding that the value was sound: after the deletion the member declared a warmup '
+ 'cadence with no key left to configure it, no engine that has ever run one, and a '
+ '.describe() still promising "(cron)" — ADR-0049 declared-not-enforced in the form '
+ 'Prime Directive 10 names outright, a capability advertised that the runtime does '
+ 'not deliver. Re-measured on main at 690f083f83 with a lit control rather than '
+ 'inherited from the card: CacheWarmupSchema has zero runtime consumers outside its '
+ 'declaring file (six files reference it — the generated reference page import, the '
+ 'declaration-map and export-origins catalogues, the ADR-0058 D7 ledger comment and '
+ 'two spec test files — while the control, ConnectorSchema, resolves to 46 files), '
+ 'and no cache-warmup engine exists anywhere on the platform. Bookkeeping follows the '
+ "hot-reload-inert-state-strategies-retired and crypto.hash precedents: an enum-VALUE "
+ 'narrowing puts nothing in RETIRED_KEYS_BY_MAJOR (no authorable KEY changed) and '
+ 'leaves the four surface ratchets byte-identical (no def changed, and they key on '
+ "positions and names, never on a def's value set), so the prescription hangs on the "
+ "enum's own error map dispatched by issue.input — telling the author of a TYPO that "
+ 'their value "was removed" would misinform. It is a SEMANTIC entry rather than a D2 '
+ 'conversion because there is no source to rewrite: CacheWarmup is bound to no '
+ 'metadata type and embedded in no stack collection, so no authored document and no '
+ 'stored row has ever carried this value, and os migrate meta has nothing to list. '
+ 'Route 3 of the retirement playbook, the #4834 / #11825 shape: this entry IS the '
+ 'declaration. ADR-0049, ADR-0087, #17157, #16320.',
acceptanceCriteria:
"No configuration passes strategy: 'scheduled' to CacheWarmupSchema or to "
+ 'DistributedCacheConfigSchema.warmup. TypeScript callers cannot: '
+ "CacheWarmup['strategy'] is now 'eager' | 'lazy', so the literal is a compile error "
+ 'at the authoring site. Callers that arrive as JSON get a parse REFUSAL — not the '
+ 'silent strip #16320 left for the schedule key beside it, because a narrowed enum '
+ 'rejects rather than drops — carrying the prescription, which names the job route. '
+ 'Concretely, check two places. (1) Any host or deployment config embedding a '
+ 'DistributedCacheConfig: a warmup block selecting the retired strategy now fails to '
+ 'parse where it previously parsed green; change it to eager or lazy. (2) Anything '
+ 'that was waiting on the cadence to take effect: it never did. No warmup has ever '
+ 'run on a schedule on this platform, so migrating the value changes no runtime '
+ 'behaviour whatsoever — what changes is that the contract stops promising it. If a '
+ 'scheduled warmup is genuinely wanted, it comes back through the ENFORCE leg of '
+ 'ADR-0049: the engine first, the declaration with it, never as a bare enum row '
+ 'again.',
},
{
id: 'cbp-master-detail-required-forced',
surface: 'object.fields.<master>.required on a `master_detail` reference under '
Expand Down
Loading
Loading