Skip to content

Commit e0859b1

Browse files
os-zhuangclaude
andauthored
fix(formula): retire the js expression dialect (#3278) (#3291)
* chore: bump objectui to 2e7d7f0f7ee7 feat(evaluator): route CEL-dialect component/action predicates to the canonical engine (#2664) objectui@2e7d7f0f7ee76b838f580e8a36b74f1309b204fc * fix(formula): retire the `js` expression dialect (#3278) `js` was declared in `ExpressionDialect` but never shipped — only a registry stub with no engine and no author helper (`cel`/`F`/`P` → CEL, `tmpl` → template, `cron` → cron; nothing ever emitted `js`). Remove it from the enum (now `{cel, cron, template}`) per ADR-0049 (enforce-or-remove), and fix a latent `hasDialect` bug that reported the stub as a real engine (a false-positive `true` — the stub-detection checked for a `stub:` prefix that no registered engine ever carries). Procedural JS is unaffected: it remains the L2 `ScriptBody { language: 'js' }` sandboxed, capability-gated surface — a separate enum, not an expression dialect. - spec: narrow `ExpressionDialect`; regenerate reference docs (js drops from the ~50 Expression-holding declarations — the only generated churn) - formula: drop the stub + dead stub-detection; `hasDialect` now reports only registered real engines; registry test asserts `hasDialect('js') === false` so the gate can actually go red - ADR-0058: addendum + inline amendments recording the retire No runtime behavior changes for any valid persisted artifact — no producer ever emitted `dialect: 'js'` (open-core, cloud: 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent da58467 commit e0859b1

45 files changed

Lines changed: 210 additions & 158 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/formula': patch
4+
---
5+
6+
fix(formula): retire the `js` expression dialect and fix the `hasDialect` false-positive (#3278)
7+
8+
The `js` **expression** dialect was declared in `ExpressionDialect` but never
9+
shipped — it existed only as a registry stub with no engine and no author helper
10+
(`cel`/`F`/`P` → CEL, `tmpl` → template, `cron` → cron; nothing ever emitted
11+
`js`). Per ADR-0049 (enforce-or-remove) it is removed from the enum; the set is
12+
now `{cel, cron, template}`.
13+
14+
Procedural JavaScript is unaffected: it remains the **L2** authoring surface —
15+
the sandboxed, capability-gated `ScriptBody { language: 'js' }` in hook/action
16+
bodies — which is a separate enum (`hook-body.zod.ts`), not an expression
17+
dialect.
18+
19+
Also fixes a latent bug in `hasDialect`: it detected stubs via
20+
`dialect.startsWith('stub:')`, but stubs were registered under their real name,
21+
so the check was dead code and `hasDialect('js')` returned a false-positive
22+
`true`. With the stub removed, `hasDialect` reports only registered real
23+
engines, and the registry test now asserts the negative case (`hasDialect('js')
24+
=== false`) so the gate can actually go red.
25+
26+
No runtime behavior changes for any valid persisted artifact — no producer ever
27+
emitted `dialect: 'js'`. See the ADR-0058 addendum.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ const result = ModelCapability.parse(data);
121121
| :--- | :--- | :--- | :--- |
122122
| **name** | `string` || Registry name |
123123
| **models** | `Record<string, { model: object; status?: Enum<'active' \| 'deprecated' \| 'experimental' \| 'disabled'>; priority?: integer; fallbackModels?: string[]; … }>` || Model entries by ID |
124-
| **promptTemplates** | `Record<string, { id: string; name: string; label: string; system?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }>` | optional | Prompt templates by name |
124+
| **promptTemplates** | `Record<string, { id: string; name: string; label: string; system?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }>` | optional | Prompt templates by name |
125125
| **defaultModel** | `string` | optional | Default model ID |
126126
| **enableAutoFallback** | `boolean` | optional | Auto-fallback on errors |
127127

@@ -168,8 +168,8 @@ const result = ModelCapability.parse(data);
168168
| **id** | `string` || Unique template identifier |
169169
| **name** | `string` || Template name (snake_case) |
170170
| **label** | `string` || Display name |
171-
| **system** | `string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}`} interpolation |
172-
| **user** | `string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` || User prompt template — supports `{{var}`} interpolation |
171+
| **system** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | System prompt — supports `{{var}`} interpolation |
172+
| **user** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` || User prompt template — supports `{{var}`} interpolation |
173173
| **assistant** | `string` | optional | Assistant message prefix |
174174
| **variables** | `{ name: string; type?: Enum<'string' \| 'number' \| 'boolean' \| 'object' \| 'array'>; required?: boolean; defaultValue?: any; … }[]` | optional | Template variables |
175175
| **modelId** | `string` | optional | Recommended model ID |

content/docs/references/api/automation-api.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ const result = AutomationApiErrorCode.parse(data);
106106
| **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` || Flow type |
107107
| **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean }[]` | optional | Flow variables |
108108
| **nodes** | `{ id: string; type: string; label: string; config?: Record<string, any>; … }[]` || Flow nodes |
109-
| **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` || Flow connections |
109+
| **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` || Flow connections |
110110
| **active** | `boolean` | optional | Is active (Deprecated: use status) |
111111
| **runAs** | `Enum<'system' \| 'user'>` | optional | Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A schedule-triggered run has no trigger user, so under user it runs UNSCOPED (elevated) — declare system to make that explicit. |
112112
| **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; retryDelayMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration |

content/docs/references/api/export.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo
533533
| **fields** | `string[]` | optional | Fields to include |
534534
| **filter** | `Record<string, any>` | optional | Record filter criteria |
535535
| **templateId** | `string` | optional | Export template ID for field mappings |
536-
| **schedule** | `{ cronExpression: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; timezone?: string }` || Schedule timing configuration |
536+
| **schedule** | `{ cronExpression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; timezone?: string }` || Schedule timing configuration |
537537
| **delivery** | `{ method: Enum<'email' \| 'storage' \| 'webhook'>; recipients?: string[]; storagePath?: string; webhookUrl?: string }` || Export delivery configuration |
538538

539539

@@ -567,7 +567,7 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo
567567
| **fields** | `string[]` | optional | Fields to include |
568568
| **filter** | `Record<string, any>` | optional | Record filter criteria |
569569
| **templateId** | `string` | optional | Export template ID for field mappings |
570-
| **schedule** | `{ cronExpression: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; timezone?: string }` || Schedule timing configuration |
570+
| **schedule** | `{ cronExpression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; timezone?: string }` || Schedule timing configuration |
571571
| **delivery** | `{ method: Enum<'email' \| 'storage' \| 'webhook'>; recipients?: string[]; storagePath?: string; webhookUrl?: string }` || Export delivery configuration |
572572
| **enabled** | `boolean` | optional | Whether the scheduled export is active |
573573
| **lastRunAt** | `string` | optional | Last execution timestamp |

content/docs/references/api/graphql.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ const result = FederationEntity.parse(data);
361361
| **fields** | `{ required?: string[]; selectable?: string[] }` | optional | Field selection configuration |
362362
| **authRequired** | `boolean` | optional | Require authentication |
363363
| **permissions** | `string[]` | optional | Required permissions |
364-
| **cache** | `{ enabled?: boolean; ttl?: integer; key?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } }` | optional | Query caching |
364+
| **cache** | `{ enabled?: boolean; ttl?: integer; key?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } }` | optional | Query caching |
365365

366366

367367
---
@@ -408,7 +408,7 @@ const result = FederationEntity.parse(data);
408408
| :--- | :--- | :--- | :--- |
409409
| **path** | `string` || Resolver path (Type.field) |
410410
| **type** | `Enum<'datasource' \| 'computed' \| 'script' \| 'proxy'>` || Resolver implementation type |
411-
| **implementation** | `{ datasource?: string; query?: string; expression?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; dependencies?: string[]; … }` | optional | Implementation configuration |
411+
| **implementation** | `{ datasource?: string; query?: string; expression?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; dependencies?: string[]; … }` | optional | Implementation configuration |
412412
| **cache** | `{ enabled?: boolean; ttl?: integer; keyArgs?: string[] }` | optional | Resolver caching |
413413

414414

content/docs/references/automation/connector.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,16 @@ const result = Connector.parse(data);
7676
| **label** | `string` | optional | Sync display name |
7777
| **description** | `string` | optional | Sync description |
7878
| **source** | `{ object?: string; filters?: any; fields?: string[]; connectorInstanceId?: string; … }` || Data source |
79-
| **destination** | `{ object?: string; connectorInstanceId?: string; operation: Enum<'insert' \| 'update' \| 'upsert' \| 'delete' \| 'sync'>; mapping?: Record<string, string> \| { source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record<string, any> }; defaultValue?: any }[]; … }` || Data destination |
79+
| **destination** | `{ object?: string; connectorInstanceId?: string; operation: Enum<'insert' \| 'update' \| 'upsert' \| 'delete' \| 'sync'>; mapping?: Record<string, string> \| { source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record<string, any> }; defaultValue?: any }[]; … }` || Data destination |
8080
| **direction** | `Enum<'push' \| 'pull' \| 'bidirectional'>` | optional | Sync direction |
8181
| **syncMode** | `Enum<'full' \| 'incremental' \| 'realtime'>` | optional | Sync mode |
8282
| **conflictResolution** | `Enum<'source_wins' \| 'destination_wins' \| 'latest_wins' \| 'manual' \| 'merge'>` | optional | Conflict resolution |
83-
| **schedule** | `string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron schedule |
83+
| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron schedule |
8484
| **enabled** | `boolean` | optional | Sync enabled |
8585
| **changeTrackingField** | `string` | optional | Field for change tracking |
8686
| **batchSize** | `integer` | optional | Batch size for processing |
8787
| **retry** | `{ maxAttempts?: integer; backoffMs?: integer }` | optional | Retry configuration |
88-
| **validation** | `{ required?: string[]; unique?: string[]; custom?: { name: string; condition: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; message: string }[] }` | optional | Validation rules |
88+
| **validation** | `{ required?: string[]; unique?: string[]; custom?: { name: string; condition: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; message: string }[] }` | optional | Validation rules |
8989
| **errorHandling** | `{ onValidationError?: Enum<'skip' \| 'fail' \| 'log'>; onSyncError?: Enum<'skip' \| 'fail' \| 'retry'>; notifyOnError?: string[] }` | optional | Error handling |
9090
| **optimization** | `{ parallelBatches?: boolean; cacheEnabled?: boolean; compressionEnabled?: boolean }` | optional | Performance optimization |
9191
| **audit** | `{ logLevel?: Enum<'none' \| 'error' \| 'warn' \| 'info' \| 'debug'>; retainLogsForDays?: integer; trackChanges?: boolean }` | optional | Audit configuration |

content/docs/references/automation/control-flow.mdx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const result = FlowRegion.parse(data);
9696
| Property | Type | Required | Description |
9797
| :--- | :--- | :--- | :--- |
9898
| **nodes** | `{ id: string; type: string; label: string; config?: Record<string, any>; … }[]` || Region body nodes (single-entry/single-exit sub-graph) |
99-
| **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` | optional | Region body edges |
99+
| **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` | optional | Region body edges |
100100

101101

102102
---
@@ -111,7 +111,7 @@ const result = FlowRegion.parse(data);
111111
| **iteratorVariable** | `string` | optional | Loop variable holding the current item |
112112
| **indexVariable** | `string` | optional | Optional loop variable holding the current index |
113113
| **maxIterations** | `integer` | optional | Hard cap on iterations (clamped to the engine ceiling) |
114-
| **body** | `{ nodes: { id: string; type: string; label: string; config?: Record<string, any>; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }` | optional | Loop body region (omit for legacy flat-graph loops) |
114+
| **body** | `{ nodes: { id: string; type: string; label: string; config?: Record<string, any>; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }` | optional | Loop body region (omit for legacy flat-graph loops) |
115115

116116

117117
---
@@ -124,7 +124,7 @@ const result = FlowRegion.parse(data);
124124
| :--- | :--- | :--- | :--- |
125125
| **name** | `string` | optional | Branch label |
126126
| **nodes** | `{ id: string; type: string; label: string; config?: Record<string, any>; … }[]` || Branch body nodes |
127-
| **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` | optional | Branch body edges |
127+
| **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` | optional | Branch body edges |
128128

129129

130130
---
@@ -135,7 +135,7 @@ const result = FlowRegion.parse(data);
135135

136136
| Property | Type | Required | Description |
137137
| :--- | :--- | :--- | :--- |
138-
| **branches** | `{ name?: string; nodes: { id: string; type: string; label: string; config?: Record<string, any>; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }[]` || Branch regions executed concurrently; implicit join at block end |
138+
| **branches** | `{ name?: string; nodes: { id: string; type: string; label: string; config?: Record<string, any>; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }[]` || Branch regions executed concurrently; implicit join at block end |
139139

140140

141141
---
@@ -146,8 +146,8 @@ const result = FlowRegion.parse(data);
146146

147147
| Property | Type | Required | Description |
148148
| :--- | :--- | :--- | :--- |
149-
| **try** | `{ nodes: { id: string; type: string; label: string; config?: Record<string, any>; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }` || Protected region |
150-
| **catch** | `{ nodes: { id: string; type: string; label: string; config?: Record<string, any>; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }` | optional | Handler region run when the try region fails |
149+
| **try** | `{ nodes: { id: string; type: string; label: string; config?: Record<string, any>; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }` || Protected region |
150+
| **catch** | `{ nodes: { id: string; type: string; label: string; config?: Record<string, any>; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }` | optional | Handler region run when the try region fails |
151151
| **errorVariable** | `string` | optional | Variable holding the caught error in the catch region |
152152
| **retry** | `{ maxRetries?: integer; retryDelayMs?: integer; backoffMultiplier?: number; maxRetryDelayMs?: integer; … }` | optional | Optional retry policy for the try region |
153153

content/docs/references/automation/etl.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ const result = ETLDestination.parse(data);
191191
| **destination** | `{ type: Enum<'database' \| 'api' \| 'file' \| 'stream' \| 'object' \| 'warehouse' \| 'storage' \| 'spreadsheet'>; connector?: string; config: Record<string, any>; writeMode?: Enum<'append' \| 'overwrite' \| 'upsert' \| 'merge'>; … }` || Data destination |
192192
| **transformations** | `{ name?: string; type: Enum<'map' \| 'filter' \| 'aggregate' \| 'join' \| 'script' \| 'lookup' \| 'split' \| 'merge' \| 'normalize' \| 'deduplicate'>; config: Record<string, any>; continueOnError?: boolean }[]` | optional | Transformation pipeline |
193193
| **syncMode** | `Enum<'full' \| 'incremental' \| 'cdc'>` | optional | Sync mode |
194-
| **schedule** | `string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron schedule expression |
194+
| **schedule** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Cron schedule expression |
195195
| **enabled** | `boolean` | optional | Pipeline enabled status |
196196
| **retry** | `{ maxAttempts?: integer; backoffMs?: integer }` | optional | Retry configuration |
197197
| **notifications** | `{ onSuccess?: string[]; onFailure?: string[] }` | optional | Notification settings |

content/docs/references/automation/execution.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ const result = Checkpoint.parse(data);
166166
| :--- | :--- | :--- | :--- |
167167
| **id** | `string` || Schedule instance ID |
168168
| **flowName** | `string` || Flow machine name |
169-
| **cronExpression** | `string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` || Cron expression — cron`0 9 * * MON-FRI` |
169+
| **cronExpression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` || Cron expression — cron`0 9 * * MON-FRI` |
170170
| **timezone** | `string` | optional | IANA timezone for cron evaluation |
171171
| **status** | `Enum<'active' \| 'paused' \| 'disabled' \| 'expired'>` | optional | Current schedule status |
172172
| **nextRunAt** | `string` | optional | Next scheduled execution timestamp |

0 commit comments

Comments
 (0)