|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(spec)!: a typed expression slot fixes its dialect on the envelope arm too, and refuses a blank string (#15028, #15035) |
| 6 | + |
| 7 | +<!-- adr-0087: not-required (no-migration-prescription) No authorable key is renamed, retired or re-typed: every one of the twelve cron- and template-typed keys still takes the bare string and the same-dialect envelope it took before. The two newly refused shapes — an envelope naming a foreign dialect, and a blank string — were measured against every author value in this repo, the examples, the docs, the skills and the objectui pin (32 cron and 14 template values; #15035 census comment 5552934813): zero real authors write either, so the ledger has nothing to rewrite and this changeset carries no rewrite instruction. The remedy is authoring intent: declare the slot's own dialect, or write the bare string. --> |
| 8 | + |
| 9 | +**BREAKING** accept-set narrowing on the twelve authorable keys typed |
| 10 | +`CronExpressionInputSchema` (`system/CronSchedule:expression`, |
| 11 | +`ai/KnowledgeRefreshPolicy:cron`, `api/ScheduledExport` and |
| 12 | +`api/ScheduleExportRequest` `schedule.cronExpression`, |
| 13 | +`automation/ScheduleState:cronExpression`, `integration/DataSyncConfig:schedule`, |
| 14 | +`system/CacheWarmup:schedule`, `system/BackupConfig:schedule`, |
| 15 | +`system/DisasterRecoveryPlan` `testing.schedule`) and |
| 16 | +`TemplateExpressionInputSchema` (`ai/PromptTemplate:system`, |
| 17 | +`ai/PromptTemplate:user`, `data/Object:titleFormat`). Shipped as `minor` under |
| 18 | +the repo's launch-window convention for breaking changes. Measured cost: zero |
| 19 | +— of the 46 author values probed across the repo, the examples, the docs, the |
| 20 | +skills and the objectui pin, every one is a bare string or a same-dialect |
| 21 | +envelope. |
| 22 | + |
| 23 | +**What changes** (`packages/spec/src/shared/expression.zod.ts`): |
| 24 | + |
| 25 | +- The envelope arm of each typed schema is `ExpressionSchema` narrowed to that |
| 26 | + one dialect literal. A cron-typed slot accepts a bare string or |
| 27 | + `{ dialect: 'cron', source }` only; a template-typed slot likewise for |
| 28 | + `template`. An envelope naming any other dialect — `cel` or `template` on a |
| 29 | + cron slot, `cel` or `cron` on a template slot, or the retired `js` — is |
| 30 | + refused with ONE `invalid_union` at the slot whose message is the slot's |
| 31 | + dialect-only sentence (`TYPED_EXPRESSION_DIALECT_ONLY[dialect]`, exported). |
| 32 | + Before, the arm was the unrestricted `ExpressionSchema`, so a cron slot |
| 33 | + parsed a `cel` envelope green and whatever read it received an expression it |
| 34 | + could not schedule — a copy-paste artifact of the untyped schema, never a |
| 35 | + decision. |
| 36 | +- The bare-string arm refuses a blank string — empty or whitespace-only, the |
| 37 | + notion of blank `EvaluatedExpressionSchema` already applies (`source.trim()`) |
| 38 | + — with ONE `invalid_union` at the slot whose message is the slot's |
| 39 | + source-required sentence (`TYPED_EXPRESSION_SOURCE_REQUIRED[dialect]`, |
| 40 | + exported). Before, `.min(1)` did not trim, so `' '` normalized to |
| 41 | + `{ dialect: 'cron', source: ' ' }` on every typed slot. |
| 42 | +- The author type narrows with it: `CronExpressionInput` / |
| 43 | + `TemplateExpressionInput` no longer admit a foreign-dialect envelope, and the |
| 44 | + published JSON Schema and the generated reference page declare the envelope's |
| 45 | + `dialect` as that one literal. `TypedExpressionDialect` names the pair. |
| 46 | + |
| 47 | +**What does NOT change.** No cron syntax is judged at parse time; `croner` |
| 48 | +judges it where a schedule is wired (`CronSchedule.expression`, the one cron |
| 49 | +slot with a reader); no grammar is restated in spec. `'not a cron'` still |
| 50 | +normalizes to `{ dialect: 'cron', source: 'not a cron' }`, deliberately: the |
| 51 | +repo's two cron grammars already disagree on 5 of 32 probed patterns, and a |
| 52 | +restatement would be a third. `ExpressionInputSchema` and `ExpressionSchema` |
| 53 | +are untouched — the untyped envelope still takes every declared dialect, and an |
| 54 | +envelope with neither `source` nor `ast` is refused exactly as before. |
| 55 | + |
| 56 | +```ts |
| 57 | +// a cron-typed slot, e.g. defineStack({ jobs: [{ schedule: { type: 'cron', expression } }] }) |
| 58 | +expression: '0 9 * * 1-5' // accepted, normalized to { dialect: 'cron', source } |
| 59 | +expression: { dialect: 'cron', source: '0 9 * * 1-5' } // accepted verbatim |
| 60 | +expression: { dialect: 'cel', source: 'now()' } // refused at jobs.0.schedule.expression |
| 61 | +expression: ' ' // refused at jobs.0.schedule.expression |
| 62 | +expression: 'not a cron' // accepted — syntax is croner's verdict at schedule time |
| 63 | +``` |
0 commit comments