Skip to content

Commit f81afe3

Browse files
feat(spec)!: typed expression slots fix their dialect on the envelope arm and refuse a blank string (#15028, #15035) (#16001)
* feat(spec)!: typed expression slots fix their dialect on the envelope arm and refuse a blank string `CronExpressionInputSchema` / `TemplateExpressionInputSchema` narrow their envelope arm to their own dialect literal and refuse a blank (empty or whitespace-only) bare string, each with ONE `invalid_union` at the slot whose message names the dialect and the fix. No cron syntax is judged at parse time; `croner` judges it where a schedule is wired; no grammar is restated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf * spec: spell the typed unions explicitly — a generic literal arm does not typecheck under safeExtend, and the parsed value must stay assignable to the input type Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf * spec: regenerate api-surface, export-origins and the reference pages for the typed-slot narrowing Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf * qa(dogfood): keep the D7 cron row note free of issue ids (doc-authoring guard) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf * spec: regenerate the reference pages after merging origin/main into the typed-slot branch Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c463d03 commit f81afe3

19 files changed

Lines changed: 457 additions & 57 deletions
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
```

content/docs/references/ai/knowledge-source.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const result = FileKnowledgeSourceSchema.parse(data);
6565
| Property | Type | Required | Description |
6666
| :--- | :--- | :--- | :--- |
6767
| **onRecordChange** | `boolean` | optional (default: `true`) | |
68-
| **cron** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron-dialect expression for a periodic full reindex. A bare string is shorthand for `{ dialect: 'cron', source }`; the parse enforces a non-empty string or an expression envelope and normalizes to the envelope — cron syntax (5- or 6-field, or an `@` alias) is the `cron` dialect engine's verdict when the expression is evaluated, not checked here. `service-knowledge` does not schedule it: the value is surfaced so an automation flow / external scheduler can trigger `reindexSource`. |
68+
| **cron** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` | optional | Cron-dialect expression for a periodic full reindex. A bare string is shorthand for `{ dialect: 'cron', source }`; the parse enforces a non-empty string or an expression envelope and normalizes to the envelope — cron syntax (5- or 6-field, or an `@` alias) is the `cron` dialect engine's verdict when the expression is evaluated, not checked here. `service-knowledge` does not schedule it: the value is surfaced so an automation flow / external scheduler can trigger `reindexSource`. |
6969

7070

7171
---
@@ -130,7 +130,7 @@ const result = FileKnowledgeSourceSchema.parse(data);
130130
| Property | Type | Required | Description |
131131
| :--- | :--- | :--- | :--- |
132132
| **onRecordChange** | `boolean` | optional (default: `true`) | |
133-
| **cron** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron-dialect expression for a periodic full reindex. A bare string is shorthand for `{ dialect: 'cron', source }`; the parse enforces a non-empty string or an expression envelope and normalizes to the envelope — cron syntax (5- or 6-field, or an `@` alias) is the `cron` dialect engine's verdict when the expression is evaluated, not checked here. `service-knowledge` does not schedule it: the value is surfaced so an automation flow / external scheduler can trigger `reindexSource`. |
133+
| **cron** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` | optional | Cron-dialect expression for a periodic full reindex. A bare string is shorthand for `{ dialect: 'cron', source }`; the parse enforces a non-empty string or an expression envelope and normalizes to the envelope — cron syntax (5- or 6-field, or an `@` alias) is the `cron` dialect engine's verdict when the expression is evaluated, not checked here. `service-knowledge` does not schedule it: the value is surfaced so an automation flow / external scheduler can trigger `reindexSource`. |
134134

135135

136136
---

content/docs/references/ai/model-registry.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ const result = ModelCapabilitySchema.parse(data);
171171
| **id** | `string` || Unique template identifier |
172172
| **name** | `string` || Template name (snake_case) |
173173
| **label** | `string` || Display name |
174-
| **system** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}}` interpolation |
175-
| **user** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` || User prompt template — supports `{{var}}` interpolation |
174+
| **system** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}}` interpolation |
175+
| **user** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` || User prompt template — supports `{{var}}` interpolation |
176176
| **assistant** | `string` | optional | Assistant message prefix |
177177
| **variables** | `{ name: string; type?: Enum<'string' \| 'number' \| 'boolean' \| 'object' \| 'array'>; required?: boolean; defaultValue?: any; … }[]` | optional | Template variables |
178178
| **modelId** | `string` | optional | Recommended model ID |
@@ -260,8 +260,8 @@ const result = ModelCapabilitySchema.parse(data);
260260
| **id** | `string` || Unique template identifier |
261261
| **name** | `string` || Template name (snake_case) |
262262
| **label** | `string` || Display name |
263-
| **system** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}}` interpolation |
264-
| **user** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` || User prompt template — supports `{{var}}` interpolation |
263+
| **system** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}}` interpolation |
264+
| **user** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` || User prompt template — supports `{{var}}` interpolation |
265265
| **assistant** | `string` | optional | Assistant message prefix |
266266
| **variables** | `{ name: string; type?: Enum<'string' \| 'number' \| 'boolean' \| 'object' \| 'array'>; required?: boolean; defaultValue?: any; … }[]` | optional | Template variables |
267267
| **modelId** | `string` | optional | Recommended model ID |

content/docs/references/api/export.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo
753753

754754
| Property | Type | Required | Description |
755755
| :--- | :--- | :--- | :--- |
756-
| **cronExpression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` || Cron expression for schedule |
756+
| **cronExpression** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` || Cron expression for schedule |
757757
| **timezone** | `string` | optional (default: `"UTC"`) | IANA timezone |
758758

759759
### Nested Shape: `ScheduleExportRequest.delivery`
@@ -831,7 +831,7 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo
831831

832832
| Property | Type | Required | Description |
833833
| :--- | :--- | :--- | :--- |
834-
| **cronExpression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` || Cron expression for schedule |
834+
| **cronExpression** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` || Cron expression for schedule |
835835
| **timezone** | `string` | optional (default: `"UTC"`) | IANA timezone |
836836

837837
### Nested Shape: `ScheduledExport.delivery`

content/docs/references/api/metadata.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ Metadata query with filtering, sorting, and pagination
787787
| **activityMilestones** | `{ field: string; value: string; summary: string; type?: string }[]` | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
788788
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
789789
| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
790-
| **titleFormat** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. |
790+
| **titleFormat** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. |
791791
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
792792
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
793793
| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |

content/docs/references/automation/execution.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ const result = CheckpointSchema.parse(data);
343343
| :--- | :--- | :--- | :--- |
344344
| **id** | `string` || Schedule instance ID |
345345
| **flowName** | `string` || Flow machine name |
346-
| **cronExpression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` || Cron expression — cron`0 9 * * MON-FRI` |
346+
| **cronExpression** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` || Cron expression — cron`0 9 * * MON-FRI` |
347347
| **timezone** | `string` | optional (default: `"UTC"`) | IANA timezone for cron evaluation |
348348
| **status** | `Enum<'active' \| 'paused' \| 'disabled' \| 'expired'>` | optional (default: `"active"`) | Current schedule status |
349349
| **nextRunAt** | `string` | optional | Next scheduled execution timestamp |

content/docs/references/data/object.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ const result = ApiMethod.parse(data);
162162
| **activityMilestones** | `{ field: string; value: string; summary: string; type?: string }[]` | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
163163
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
164164
| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
165-
| **titleFormat** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. |
165+
| **titleFormat** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. |
166166
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
167167
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
168168
| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |

content/docs/references/integration/connector.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ Circuit breaker configuration
280280
| :--- | :--- | :--- | :--- |
281281
| **strategy** | `Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>` | optional (default: `"incremental"`) | Synchronization strategy |
282282
| **direction** | `Enum<'import' \| 'export' \| 'bidirectional'>` | optional (default: `"import"`) | Sync direction |
283-
| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
283+
| **schedule** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
284284
| **realtimeSync** | `boolean` | optional (default: `false`) | Enable real-time sync |
285285
| **timestampField** | `string` | optional | Field to track last modification time |
286286
| **conflictResolution** | `Enum<'source_wins' \| 'target_wins' \| 'latest_wins' \| 'manual'>` | optional (default: `"latest_wins"`) | Conflict resolution strategy |
@@ -626,7 +626,7 @@ Connector type
626626
| :--- | :--- | :--- | :--- |
627627
| **strategy** | `Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>` | optional (default: `"incremental"`) | Synchronization strategy |
628628
| **direction** | `Enum<'import' \| 'export' \| 'bidirectional'>` | optional (default: `"import"`) | Sync direction |
629-
| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
629+
| **schedule** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
630630
| **realtimeSync** | `boolean` | optional (default: `false`) | Enable real-time sync |
631631
| **timestampField** | `string` | optional | Field to track last modification time |
632632
| **conflictResolution** | `Enum<'source_wins' \| 'target_wins' \| 'latest_wins' \| 'manual'>` | optional (default: `"latest_wins"`) | Conflict resolution strategy |
@@ -763,7 +763,7 @@ Connector type
763763
| :--- | :--- | :--- | :--- |
764764
| **strategy** | `Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>` | optional (default: `"incremental"`) | Synchronization strategy |
765765
| **direction** | `Enum<'import' \| 'export' \| 'bidirectional'>` | optional (default: `"import"`) | Sync direction |
766-
| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
766+
| **schedule** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
767767
| **realtimeSync** | `boolean` | optional (default: `false`) | Enable real-time sync |
768768
| **timestampField** | `string` | optional | Field to track last modification time |
769769
| **conflictResolution** | `Enum<'source_wins' \| 'target_wins' \| 'latest_wins' \| 'manual'>` | optional (default: `"latest_wins"`) | Conflict resolution strategy |

0 commit comments

Comments
 (0)