diff --git a/.changeset/17157-cache-warmup-scheduled-strategy-retired.md b/.changeset/17157-cache-warmup-scheduled-strategy-retired.md new file mode 100644 index 0000000000..6b7a6394c1 --- /dev/null +++ b/.changeset/17157-cache-warmup-scheduled-strategy-retired.md @@ -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) + + + +**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. diff --git a/content/docs/references/system/cache.mdx b/content/docs/references/system/cache.mdx index 9d76ea5187..4663453302 100644 --- a/content/docs/references/system/cache.mdx +++ b/content/docs/references/system/cache.mdx @@ -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 | @@ -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]` @@ -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 | diff --git a/packages/spec/src/cron-typed-positions-retirement.test.ts b/packages/spec/src/cron-typed-positions-retirement.test.ts index 3ce4014c1d..1f4a96f9d8 100644 --- a/packages/spec/src/cron-typed-positions-retirement.test.ts +++ b/packages/spec/src/cron-typed-positions-retirement.test.ts @@ -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 }], diff --git a/packages/spec/src/migrations/entries/semantic/18.cache-warmup-scheduled-strategy-retired.ts b/packages/spec/src/migrations/entries/semantic/18.cache-warmup-scheduled-strategy-retired.ts new file mode 100644 index 0000000000..a828ca5228 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.cache-warmup-scheduled-strategy-retired.ts @@ -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.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 4727e14966..669a3ff9d0 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -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..required on a `master_detail` reference under ' diff --git a/packages/spec/src/system/cache.test.ts b/packages/spec/src/system/cache.test.ts index 3730aa5840..79588e5983 100644 --- a/packages/spec/src/system/cache.test.ts +++ b/packages/spec/src/system/cache.test.ts @@ -238,16 +238,36 @@ describe('CacheWarmupSchema', () => { expect(result.concurrency).toBe(20); }); - it('still accepts the `scheduled` strategy value — the `schedule` cron key beside it is retired', () => { - // `schedule` was deleted outright (#16320); the strip is pinned in - // `cron-typed-positions-retirement.test.ts`. The enum member is a value the - // ruling did not name and stays exactly as inert as it was. - const result = CacheWarmupSchema.parse({ - enabled: true, - strategy: 'scheduled', - }); - expect(result.strategy).toBe('scheduled'); - expect(result).not.toHaveProperty('schedule'); + // ── [#17157] `strategy: 'scheduled'` is RETIRED — the negative leg ───────── + // + // ⚠️ This is the ONLY instrument this retirement has. An enum-VALUE narrowing + // moves no position, no name and no expression-typed slot, so `api-surface/`, + // `authorable-surface/`, `json-schema.manifest/` and the ADR-0058 D7 ledger + // are all byte-identical across it (the `crypto.hash` / #4391 precedent, and + // the retirement playbook's own route table). A green CI run therefore says + // nothing about whether the value is gone. These two assertions say it. + it('refuses `strategy: \'scheduled\'` and answers with the retirement prescription', () => { + expect(() => CacheWarmupSchema.parse({ enabled: true, strategy: 'scheduled' })) + .toThrow(/`CacheWarmup\.strategy: 'scheduled'` was removed.*`'eager'`.*`'lazy'`/s); + // The prescription names the surviving cron slot rather than a replacement + // strategy, because there is no warmup engine to schedule. + expect(() => CacheWarmupSchema.parse({ enabled: true, strategy: 'scheduled' })) + .toThrow(/schedule\.expression/s); + }); + + it('dispatches the prescription on the retired value ONLY — a typo keeps zod\'s own message', () => { + // Lit control for the assertion above: a value that was never legal must + // NOT be told it "was removed", or the error map is matching everything and + // the pin above would pass over a schema that refuses nothing in particular. + const typo = CacheWarmupSchema.safeParse({ enabled: true, strategy: 'sheduled' }); + expect(typo.success).toBe(false); + if (!typo.success) expect(JSON.stringify(typo.error.issues)).not.toContain('was removed'); + }); + + it('the surviving members still parse — the refusal above is the narrowing, not a dead schema', () => { + expect(CacheWarmupSchema.parse({ enabled: true, strategy: 'eager' }).strategy).toBe('eager'); + expect(CacheWarmupSchema.parse({ enabled: true, strategy: 'lazy' }).strategy).toBe('lazy'); + expect(CacheWarmupSchema.parse({ enabled: true }).strategy).toBe('lazy'); }); }); diff --git a/packages/spec/src/system/cache.zod.ts b/packages/spec/src/system/cache.zod.ts index 813cf7828c..31a720e303 100644 --- a/packages/spec/src/system/cache.zod.ts +++ b/packages/spec/src/system/cache.zod.ts @@ -167,6 +167,24 @@ export type CacheAvalanchePrevention = z.input; +// Retired enum-value prescription. Declared with `//` (never `/** */`) so it cannot +// be mistaken for the warmup schema's own doc blurb by build-docs. +// The retirement of the cron key this value selected is #16320; the id stays HERE, +// in an internal comment, and out of the customer-facing string below — that string +// is printed verbatim at an author who has no tracker to resolve it with, and its +// durable reference is ADR-0049 (`check:doc-authoring` Rule 3). +const WARMUP_STRATEGY_SCHEDULED_RETIRED = + "`CacheWarmup.strategy: 'scheduled'` was removed in @objectstack/spec 18 (ADR-0049 " + + 'enforce-or-remove) — the cron key it selected, `CacheWarmup.schedule`, was deleted in ' + + 'this same major, and no cache-warmup engine has ever existed on the platform, ' + + 'so the value named a cadence with nothing to configure it and nothing to run it. Use ' + + "`'eager'` to warm at startup or `'lazy'` to warm on first access. For a warmup on a " + + 'cadence, declare a `job` with `schedule.expression` (`system/job.zod.ts`) whose handler ' + + 'does the warming — that is the one cron slot this platform evaluates. There is no ' + + '`os migrate meta` prescription to replay: `CacheWarmup` is bound to no metadata type ' + + 'and embedded in no stack collection, so no authored source and no stored row has ever ' + + 'carried this value.'; + /** * Cache Warmup Strategy Schema * @@ -176,18 +194,34 @@ export const CacheWarmupSchema = lazySchema(() => z.object({ /** Enable cache warming */ enabled: z.boolean().default(false).describe('Enable cache warmup'), /** Warmup strategy */ - strategy: z.enum(['eager', 'lazy', 'scheduled']).default('lazy') - .describe('Warmup strategy: eager (at startup), lazy (on first access), scheduled (cron)'), + strategy: z.enum(['eager', 'lazy'], { + // Only the value that USED to be legal gets the retirement prescription — + // telling the author of `sheduled` that their value "was removed" would + // misinform. Everything else keeps zod's own enum message, which already + // lists the legal values. (The `managedBy: 'system'` / `crypto.hash` + // precedent — data/object.zod.ts, data/hook-body.zod.ts.) + error: (issue) => (issue.input === 'scheduled' ? WARMUP_STRATEGY_SCHEDULED_RETIRED : undefined), + }).default('lazy') + .describe('Warmup strategy: eager (at startup), lazy (on first access)'), /* * `CacheWarmup.schedule` was DELETED here in @objectstack/spec 18 (ADR-0049 * enforce-or-remove, #16320): declared, parsed into the cron envelope and read by * nothing — `CacheWarmupSchema` has no consumer outside `packages/spec`, so no * warmup ever ran on a schedule. Deleted outright — no `retiredKey()` tombstone, no * D2 conversion, no D3 semantic entry (maintainer ruling 2026-09-10 on the - * retirement PR). The `strategy` enum keeps its `scheduled` member: it is a value, - * not a position this ruling names, and it was exactly as inert before. The one cron - * slot the platform evaluates is `Job.schedule.expression` (`system/job.zod.ts`): a - * warmup on a cadence is a job whose handler you write. + * retirement PR). The one cron slot the platform evaluates is + * `Job.schedule.expression` (`system/job.zod.ts`): a warmup on a cadence is a job + * whose handler you write. + * + * The `strategy` enum's `scheduled` member left in the SAME major, one card later: + * that ruling declined it as "a value, not a position this ruling names", which is + * a statement about the ruling's scope and not a finding that the value was sound. + * With the cron key gone it named a cadence with no key to carry it and no engine + * to run it, while its own `.describe()` still promised "(cron)". An enum-VALUE + * narrowing is invisible to all four surface ratchets (they key on positions, on + * expression-typed slots and on names — never on a def's value set), so the + * prescription hangs on the enum's own `error` map above, and the D3 semantic entry + * `cache-warmup-scheduled-strategy-retired` IS the declaration. */ /** Keys/patterns to warm up */ patterns: z.array(z.string()).optional().describe('Key patterns to warm up (e.g., "user:*", "config:*")'),