|
| 1 | +--- |
| 2 | +'@objectstack/spec': minor |
| 3 | +'@objectstack/service-automation': patch |
| 4 | +--- |
| 5 | + |
| 6 | +feat(spec)!: `FlowNodeSchema` parses its own ADR-0031 regions — the post-parse pass retires (#4415) |
| 7 | + |
| 8 | +`FlowSchema.parse` normalized a flow's own `nodes[]` / `edges[]` but could not reach a |
| 9 | +**region**, because a region lives inside `FlowNodeSchema.config` — a deliberately open |
| 10 | +`z.record` (ADR-0018). #4381 closed the resulting gap with a **post-parse pass**, |
| 11 | +`normalizeControlFlowRegions`, that every caller had to remember to run: |
| 12 | + |
| 13 | +```ts |
| 14 | +const flowShell = FlowSchema.parse(converted); |
| 15 | +validateControlFlow(flowShell); |
| 16 | +const parsed = normalizeControlFlowRegions(flowShell); // ← had to remember |
| 17 | +``` |
| 18 | + |
| 19 | +That is an unwritten rule on top of a parse, and it is exactly the condition the #4347 |
| 20 | +family of defects grows in: a new consumer — a Studio publish path, an MCP tool, a bulk |
| 21 | +validation script — takes a `FlowParsed` and uses it, holding a **half-parsed flow that |
| 22 | +looks finished**. Nested edge predicates were still bare strings, nested nodes had not been |
| 23 | +through `.strict()`, and nothing said so. |
| 24 | + |
| 25 | +Now the schema does it. `FlowNodeSchema` carries a `.transform()` that parses each declared |
| 26 | +region slot — `loop.config.body`, `parallel.config.branches[]`, `try_catch.config.try` / |
| 27 | +`.catch` — through the schema that slot's value *is*. Nesting needs no manual recursion: a |
| 28 | +region's `nodes` are `z.array(FlowNodeSchema)`, so Zod re-enters the transform on the way |
| 29 | +down. **"Parsed" now means parsed at every depth** (Prime Directive #1), from any entry |
| 30 | +point — including `FlowNodeSchema.parse(node)` on a single node, which the old whole-flow |
| 31 | +pass could not serve at all. |
| 32 | + |
| 33 | +## Migration |
| 34 | + |
| 35 | +**`normalizeControlFlowRegions` is removed from `@objectstack/spec/automation`.** Delete the |
| 36 | +call; the parse above it already did the work: |
| 37 | + |
| 38 | +```diff |
| 39 | + const parsed = FlowSchema.parse(converted); |
| 40 | + validateControlFlow(parsed); |
| 41 | +- const normalized = normalizeControlFlowRegions(parsed); |
| 42 | +``` |
| 43 | + |
| 44 | +Its replacement, `parseFlowNodeRegions(node)`, is exported for the same purpose one node at |
| 45 | +a time, but you should not normally need it — it is the transform's own body. |
| 46 | + |
| 47 | +**`FlowNodeSchema` is now a `ZodPipe`, not a `ZodObject`,** so it no longer has `.shape` / |
| 48 | +`.extend()` / `.pick()`. `z.infer` / `z.input` / `.parse` / `.safeParse` and |
| 49 | +`z.toJSONSchema` are unaffected, and the authorable key set is byte-identical (verified by |
| 50 | +`check:authorable-surface`). If you were reaching for the object half, read it from the |
| 51 | +pipe's input side — `FlowNodeSchema.def.in` — which is also what the repo's own generators |
| 52 | +do (`pipeAuthorableSide` in `scripts/lib/zod-graph.ts`). |
| 53 | + |
| 54 | +One visible consequence in the generated reference: `content/docs/references/automation/flow.mdx` |
| 55 | +now renders FlowNode's **input** shape, so keys carrying a `.default()` (`boundaryConfig.interrupting`, |
| 56 | +`inputSchema[].required`) show as optional. That is what an author actually writes, which is |
| 57 | +what an authoring reference should say. |
0 commit comments