diff --git a/.changeset/flow-edge-condition-evaluated-slot.md b/.changeset/flow-edge-condition-evaluated-slot.md index 37edaaed6f..9b93572b23 100644 --- a/.changeset/flow-edge-condition-evaluated-slot.md +++ b/.changeset/flow-edge-condition-evaluated-slot.md @@ -61,7 +61,10 @@ edges: refused by `evaluateCondition` with the same sentence, instead of answering a silent `false`. An `ast` BESIDE a string `source` is still admitted everywhere. The whitespace-only STRING ruling on `config.condition` (#15662: - consistent `false` on both sides) is untouched. + consistent `false` on both sides) is untouched by this change, but it does not + survive the release that carries it: two sibling notes in that release refuse + the value, at `registerFlow` (#17322, `@objectstack/service-automation`) and + at `objectstack validate` (#17495, `@objectstack/lint`). - **Three doors agree, through the spec.** `registerFlow` refuses the flow at `FlowSchema.parse` (edge) or at its structural pass (`config.condition`); `objectstack validate` refuses it at its `ObjectStackDefinitionSchema` parse @@ -80,7 +83,7 @@ the refusal itself carries the prescription. **A flow ALREADY STORED in `sys_metadata` stops running entirely — the whole flow, not just the edge.** The paragraph above is the author's remedy, at -`objectstack validate` / `POST /flows`; a stored row has no author in front of +`objectstack validate` / `POST /api/v1/automation`; a stored row has no author in front of it. Stored flows are deliberately NOT canonicalized by `applyConversionsToStoredItem` (`spec/src/conversions/stored.ts`, and the same skip in `metadata/src/loaders/database-loader.ts`'s `rowToData`) — flow-node @@ -104,5 +107,9 @@ door, `objectstack validate`, which locates the same edge at judgment for a consumer replaying the chain. Not touched here: `start.config.condition` has no Zod schema to narrow (the -start node's `config` is an open record); its producer-side gate is the -structural refusal above, which this change tightens but does not type. +start node's `config` is an open record). Its producer-side gate is the +structural pass at `registerFlow` and `objectstack validate`: the shape refusal +above, which this change tightens but does not type, and after it a blank-source +check that runs this change's `EvaluatedExpressionInputSchema` on the +condition's `source` (added by #17322 at `registerFlow` and by #17495 at +`objectstack validate`). diff --git a/packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.ts b/packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.ts index b6a058ad53..b8c1cf8c88 100644 --- a/packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.ts +++ b/packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.ts @@ -18,7 +18,7 @@ export const entry: SemanticMigration = { + 'than deriving a second one; it is the same decision reaching the second slot, which is why ' + 'it is named here instead of in an entry of its own. Reachable wherever a flow is authored ' + 'or stored: defineStack({ flows }) sources, an exported stack passed to objectstack validate, ' - + 'a POST /flows body, and a flow row already sitting in sys_metadata', + + 'a POST /api/v1/automation body, and a flow row already sitting in sys_metadata', replacement: 'a non-blank `source` — `{ dialect: \'cel\', source: \'record.amount > 10\' }`, or the bare ' + 'string `\'record.amount > 10\'` — if the edge was meant to branch; or REMOVE the ' @@ -60,7 +60,7 @@ export const entry: SemanticMigration = { acceptanceCriteria: 'Grep every authored structural condition — BOTH `edges[].condition` and a node\'s ' + '`config.condition` (a `decision` node\'s predicate, and on a `start` node the trigger ' - + 'gate) — in `defineStack({ flows })` sources, exported stacks and `POST /flows` bodies, and ' + + 'gate) — in `defineStack({ flows })` sources, exported stacks and `POST /api/v1/automation` bodies, and ' + 'every flow row in `sys_metadata`, for an envelope with no `source` key and for a `source` ' + '(or bare string) that is empty after trimming. ⚠️ Sweeping only the edge key leaves the ' + 'node key unswept, and the node key is the one with no schema in front of it. For each ' diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 30cf33582d..f14b1dffd0 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -9065,7 +9065,7 @@ const step18: MigrationStep = { + 'than deriving a second one; it is the same decision reaching the second slot, which is why ' + 'it is named here instead of in an entry of its own. Reachable wherever a flow is authored ' + 'or stored: defineStack({ flows }) sources, an exported stack passed to objectstack validate, ' - + 'a POST /flows body, and a flow row already sitting in sys_metadata', + + 'a POST /api/v1/automation body, and a flow row already sitting in sys_metadata', replacement: 'a non-blank `source` — `{ dialect: \'cel\', source: \'record.amount > 10\' }`, or the bare ' + 'string `\'record.amount > 10\'` — if the edge was meant to branch; or REMOVE the ' @@ -9107,7 +9107,7 @@ const step18: MigrationStep = { acceptanceCriteria: 'Grep every authored structural condition — BOTH `edges[].condition` and a node\'s ' + '`config.condition` (a `decision` node\'s predicate, and on a `start` node the trigger ' - + 'gate) — in `defineStack({ flows })` sources, exported stacks and `POST /flows` bodies, and ' + + 'gate) — in `defineStack({ flows })` sources, exported stacks and `POST /api/v1/automation` bodies, and ' + 'every flow row in `sys_metadata`, for an envelope with no `source` key and for a `source` ' + '(or bare string) that is empty after trimming. ⚠️ Sweeping only the edge key leaves the ' + 'node key unswept, and the node key is the one with no schema in front of it. For each '