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
63 changes: 63 additions & 0 deletions .changeset/typed-expression-envelope-dialect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
"@objectstack/spec": minor
---

feat(spec)!: a typed expression slot fixes its dialect on the envelope arm too, and refuses a blank string (#15028, #15035)

<!-- 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. -->

**BREAKING** accept-set narrowing on the twelve authorable keys typed
`CronExpressionInputSchema` (`system/CronSchedule:expression`,
`ai/KnowledgeRefreshPolicy:cron`, `api/ScheduledExport` and
`api/ScheduleExportRequest` `schedule.cronExpression`,
`automation/ScheduleState:cronExpression`, `integration/DataSyncConfig:schedule`,
`system/CacheWarmup:schedule`, `system/BackupConfig:schedule`,
`system/DisasterRecoveryPlan` `testing.schedule`) and
`TemplateExpressionInputSchema` (`ai/PromptTemplate:system`,
`ai/PromptTemplate:user`, `data/Object:titleFormat`). Shipped as `minor` under
the repo's launch-window convention for breaking changes. Measured cost: zero
— of the 46 author values probed across the repo, the examples, the docs, the
skills and the objectui pin, every one is a bare string or a same-dialect
envelope.

**What changes** (`packages/spec/src/shared/expression.zod.ts`):

- The envelope arm of each typed schema is `ExpressionSchema` narrowed to that
one dialect literal. A cron-typed slot accepts a bare string or
`{ dialect: 'cron', source }` only; a template-typed slot likewise for
`template`. An envelope naming any other dialect — `cel` or `template` on a
cron slot, `cel` or `cron` on a template slot, or the retired `js` — is
refused with ONE `invalid_union` at the slot whose message is the slot's
dialect-only sentence (`TYPED_EXPRESSION_DIALECT_ONLY[dialect]`, exported).
Before, the arm was the unrestricted `ExpressionSchema`, so a cron slot
parsed a `cel` envelope green and whatever read it received an expression it
could not schedule — a copy-paste artifact of the untyped schema, never a
decision.
- The bare-string arm refuses a blank string — empty or whitespace-only, the
notion of blank `EvaluatedExpressionSchema` already applies (`source.trim()`)
— with ONE `invalid_union` at the slot whose message is the slot's
source-required sentence (`TYPED_EXPRESSION_SOURCE_REQUIRED[dialect]`,
exported). Before, `.min(1)` did not trim, so `' '` normalized to
`{ dialect: 'cron', source: ' ' }` on every typed slot.
- The author type narrows with it: `CronExpressionInput` /
`TemplateExpressionInput` no longer admit a foreign-dialect envelope, and the
published JSON Schema and the generated reference page declare the envelope's
`dialect` as that one literal. `TypedExpressionDialect` names the pair.

**What does NOT change.** No cron syntax is judged at parse time; `croner`
judges it where a schedule is wired (`CronSchedule.expression`, the one cron
slot with a reader); no grammar is restated in spec. `'not a cron'` still
normalizes to `{ dialect: 'cron', source: 'not a cron' }`, deliberately: the
repo's two cron grammars already disagree on 5 of 32 probed patterns, and a
restatement would be a third. `ExpressionInputSchema` and `ExpressionSchema`
are untouched — the untyped envelope still takes every declared dialect, and an
envelope with neither `source` nor `ast` is refused exactly as before.

```ts
// a cron-typed slot, e.g. defineStack({ jobs: [{ schedule: { type: 'cron', expression } }] })
expression: '0 9 * * 1-5' // accepted, normalized to { dialect: 'cron', source }
expression: { dialect: 'cron', source: '0 9 * * 1-5' } // accepted verbatim
expression: { dialect: 'cel', source: 'now()' } // refused at jobs.0.schedule.expression
expression: ' ' // refused at jobs.0.schedule.expression
expression: 'not a cron' // accepted — syntax is croner's verdict at schedule time
```
4 changes: 2 additions & 2 deletions content/docs/references/ai/knowledge-source.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const result = FileKnowledgeSourceSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **onRecordChange** | `boolean` | optional (default: `true`) | |
| **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`. |
| **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`. |


---
Expand Down Expand Up @@ -130,7 +130,7 @@ const result = FileKnowledgeSourceSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **onRecordChange** | `boolean` | optional (default: `true`) | |
| **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`. |
| **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`. |


---
Expand Down
8 changes: 4 additions & 4 deletions content/docs/references/ai/model-registry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ const result = ModelCapabilitySchema.parse(data);
| **id** | `string` | ✅ | Unique template identifier |
| **name** | `string` | ✅ | Template name (snake_case) |
| **label** | `string` | ✅ | Display name |
| **system** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}}` interpolation |
| **user** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | User prompt template — supports `{{var}}` interpolation |
| **system** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}}` interpolation |
| **user** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | ✅ | User prompt template — supports `{{var}}` interpolation |
| **assistant** | `string` | optional | Assistant message prefix |
| **variables** | `{ name: string; type?: Enum<'string' \| 'number' \| 'boolean' \| 'object' \| 'array'>; required?: boolean; defaultValue?: any; … }[]` | optional | Template variables |
| **modelId** | `string` | optional | Recommended model ID |
Expand Down Expand Up @@ -260,8 +260,8 @@ const result = ModelCapabilitySchema.parse(data);
| **id** | `string` | ✅ | Unique template identifier |
| **name** | `string` | ✅ | Template name (snake_case) |
| **label** | `string` | ✅ | Display name |
| **system** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}}` interpolation |
| **user** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | User prompt template — supports `{{var}}` interpolation |
| **system** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}}` interpolation |
| **user** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | ✅ | User prompt template — supports `{{var}}` interpolation |
| **assistant** | `string` | optional | Assistant message prefix |
| **variables** | `{ name: string; type?: Enum<'string' \| 'number' \| 'boolean' \| 'object' \| 'array'>; required?: boolean; defaultValue?: any; … }[]` | optional | Template variables |
| **modelId** | `string` | optional | Recommended model ID |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/api/export.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo

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

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

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

### Nested Shape: `ScheduledExport.delivery`
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ Metadata query with filtering, sorting, and pagination
| **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). |
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
| **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. |
| **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. |
| **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. |
| **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. |
| **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). |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/automation/execution.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ const result = CheckpointSchema.parse(data);
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Schedule instance ID |
| **flowName** | `string` | ✅ | Flow machine name |
| **cronExpression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Cron expression — cron`0 9 * * MON-FRI` |
| **cronExpression** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` | ✅ | Cron expression — cron`0 9 * * MON-FRI` |
| **timezone** | `string` | optional (default: `"UTC"`) | IANA timezone for cron evaluation |
| **status** | `Enum<'active' \| 'paused' \| 'disabled' \| 'expired'>` | optional (default: `"active"`) | Current schedule status |
| **nextRunAt** | `string` | optional | Next scheduled execution timestamp |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/data/object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ const result = ApiMethod.parse(data);
| **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). |
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
| **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. |
| **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. |
| **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. |
| **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. |
| **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). |
Expand Down
6 changes: 3 additions & 3 deletions content/docs/references/integration/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ Circuit breaker configuration
| :--- | :--- | :--- | :--- |
| **strategy** | `Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>` | optional (default: `"incremental"`) | Synchronization strategy |
| **direction** | `Enum<'import' \| 'export' \| 'bidirectional'>` | optional (default: `"import"`) | Sync direction |
| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
| **schedule** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
| **realtimeSync** | `boolean` | optional (default: `false`) | Enable real-time sync |
| **timestampField** | `string` | optional | Field to track last modification time |
| **conflictResolution** | `Enum<'source_wins' \| 'target_wins' \| 'latest_wins' \| 'manual'>` | optional (default: `"latest_wins"`) | Conflict resolution strategy |
Expand Down Expand Up @@ -626,7 +626,7 @@ Connector type
| :--- | :--- | :--- | :--- |
| **strategy** | `Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>` | optional (default: `"incremental"`) | Synchronization strategy |
| **direction** | `Enum<'import' \| 'export' \| 'bidirectional'>` | optional (default: `"import"`) | Sync direction |
| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
| **schedule** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
| **realtimeSync** | `boolean` | optional (default: `false`) | Enable real-time sync |
| **timestampField** | `string` | optional | Field to track last modification time |
| **conflictResolution** | `Enum<'source_wins' \| 'target_wins' \| 'latest_wins' \| 'manual'>` | optional (default: `"latest_wins"`) | Conflict resolution strategy |
Expand Down Expand Up @@ -763,7 +763,7 @@ Connector type
| :--- | :--- | :--- | :--- |
| **strategy** | `Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>` | optional (default: `"incremental"`) | Synchronization strategy |
| **direction** | `Enum<'import' \| 'export' \| 'bidirectional'>` | optional (default: `"import"`) | Sync direction |
| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
| **schedule** | `string \| { dialect: 'cron'; source?: string; ast?: any; meta?: object }` | optional | Cron expression for scheduled sync — cron`0 */15 * * *` |
| **realtimeSync** | `boolean` | optional (default: `false`) | Enable real-time sync |
| **timestampField** | `string` | optional | Field to track last modification time |
| **conflictResolution** | `Enum<'source_wins' \| 'target_wins' \| 'latest_wins' \| 'manual'>` | optional (default: `"latest_wins"`) | Conflict resolution strategy |
Expand Down
Loading
Loading