From 438c1df00a3c4af5c22b9c8091d158a7b1f234a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 1 Jun 2026 22:25:23 +0000 Subject: [PATCH 1/2] docs(automation): document ADR-0031 control-flow; fix dangling ref card - guide (content/docs/guides/metadata/flow.mdx): document the structured loop container, parallel block (implicit join), and try/catch/retry constructs with config examples and the region/DAG-invariant model; update the Node Types table. - doc generator (packages/spec/scripts/build-docs.ts): only emit index Cards for reference pages that were actually generated. control-flow's schemas embed CEL-expression transforms (like Flow/FlowEdge) so produce no JSON-Schema page; the index previously carded every .zod.ts file, yielding a dangling "Control Flow" 404 link. Cards now match meta.json. https://claude.ai/code/session_012ti8cx3TkdiQdjCnZXZg2Q --- .changeset/docs-adr0031-control-flow.md | 15 ++++ content/docs/guides/metadata/flow.mdx | 97 ++++++++++++++++++++++++- packages/spec/scripts/build-docs.ts | 7 ++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 .changeset/docs-adr0031-control-flow.md diff --git a/.changeset/docs-adr0031-control-flow.md b/.changeset/docs-adr0031-control-flow.md new file mode 100644 index 000000000..e4ae16dd1 --- /dev/null +++ b/.changeset/docs-adr0031-control-flow.md @@ -0,0 +1,15 @@ +--- +"@objectstack/spec": patch +--- + +docs(automation): document ADR-0031 control-flow constructs; fix dangling reference card + +- **guide**: `content/docs/guides/metadata/flow.mdx` now documents the structured + control-flow constructs — the `loop` container, `parallel` block (implicit + join), and `try_catch` (try/catch/retry) — with config examples and the + region/DAG model. The Node Types table is updated accordingly. +- **doc generator**: `build-docs.ts` now cards only reference pages that were + actually generated. Control-flow's schemas embed CEL-expression transforms + (like `Flow`/`FlowEdge`) and so have no JSON-Schema page; the index previously + carded every `.zod.ts`, producing a dangling "Control Flow" 404 link. Cards + now align with `meta.json` (generated pages only). diff --git a/content/docs/guides/metadata/flow.mdx b/content/docs/guides/metadata/flow.mdx index be94c263a..8d5aa3099 100644 --- a/content/docs/guides/metadata/flow.mdx +++ b/content/docs/guides/metadata/flow.mdx @@ -101,7 +101,9 @@ Each node performs a specific action in the flow. | `end` | Flow termination | | `decision` | Conditional branching (if/else) | | `assignment` | Set variable values | -| `loop` | Iterate over a collection | +| `loop` | Structured iteration **container** — runs a body region once per item (ADR-0031) | +| `parallel` | Structured **parallel block** — runs N branch regions concurrently, implicit join (ADR-0031) | +| `try_catch` | Structured **try/catch/retry** error handling (ADR-0031) | | `create_record` | Create a new record | | `update_record` | Update existing records | | `delete_record` | Delete records | @@ -201,6 +203,99 @@ Each node performs a specific action in the flow. } ``` +## Structured control flow (ADR-0031) + +`loop`, `parallel`, and `try_catch` are **structured control-flow constructs** — +the native, AI-authored model for iteration, concurrency, and error handling. +Unlike free-form BPMN gateways (kept in the protocol for import/export interop +only), these constructs are **well-formed by construction**: each owns its body +as a self-contained, single-entry/single-exit **region** carried in `config` +(representation **B** — a nested sub-graph). Ordinary step-to-step edges stay +acyclic — iteration and concurrency are scoped containers, not back-edges — so +the **DAG invariant is preserved** and termination stays analyzable. Regions are +validated at `registerFlow()` (single-entry/single-exit, acyclic, bounded loop); +a malformed construct is rejected before the flow can run. + +A region runs in the **enclosing variable scope** (the iterator value and any +body mutations are visible to the surrounding flow) — it is *not* a separate +`subflow` invocation. The container node's ordinary out-edges are the +"after-loop / after-block" continuation. + +### Loop container + +Runs its `body` region once per item of a collection, binding the current item +(and optionally its index) as flow variables, under a hard max-iteration guard. + +```typescript +{ + id: 'notify_each', + type: 'loop', + label: 'For each task', + config: { + collection: '{tasks}', // template/variable resolving to an array + iteratorVariable: 'task', // current item, visible inside the body + indexVariable: 'i', // optional zero-based index + maxIterations: 500, // hard cap (clamped to the engine ceiling) + body: { // single-entry/single-exit region + nodes: [ + { id: 'send', type: 'script', label: 'Notify', config: { /* … */ } }, + ], + edges: [], + }, + }, +} +``` + +A `loop` node with **no `body`** keeps the legacy flat-graph behavior — the +container is additive. + +### Parallel block + +Declares N branch regions that run **concurrently** and **join implicitly** when +all complete — there is no author-visible split/join gateway. Branches should +write distinct variables (last-writer-wins on collision); a failing branch fails +the block. + +```typescript +{ + id: 'fan_out', + type: 'parallel', + label: 'Notify in parallel', + config: { + branches: [ // ≥ 2 regions + { name: 'Email', nodes: [{ id: 'email', type: 'script', label: 'Email', config: { /* … */ } }], edges: [] }, + { name: 'Slack', nodes: [{ id: 'slack', type: 'script', label: 'Slack', config: { /* … */ } }], edges: [] }, + ], + }, +} +``` + +### Try / catch / retry + +Wraps a protected `try` region; on failure runs the optional `catch` region +(with the caught error bound to `errorVariable`), optionally retrying the `try` +region first with exponential backoff. This surfaces the engine's existing +`fault` + retry semantics as a structured construct (rather than BPMN boundary +events). + +```typescript +{ + id: 'guarded', + type: 'try_catch', + label: 'Charge with fallback', + config: { + try: { nodes: [{ id: 'charge', type: 'http_request', label: 'Charge', config: { /* … */ } }], edges: [] }, + catch: { nodes: [{ id: 'flag', type: 'update_record', label: 'Flag failure', config: { /* … */ } }], edges: [] }, + errorVariable: '$error', + retry: { maxRetries: 3, retryDelayMs: 1000, backoffMultiplier: 2 }, + }, +} +``` + +> BPMN `parallel_gateway` / `join_gateway` / `boundary_event` remain in the +> protocol as the **interop** representation and map onto these constructs on +> import/export — they are not the native authoring model. + ## Edges Edges connect nodes and define the execution path: diff --git a/packages/spec/scripts/build-docs.ts b/packages/spec/scripts/build-docs.ts index 61e484d91..c93e0c3a6 100644 --- a/packages/spec/scripts/build-docs.ts +++ b/packages/spec/scripts/build-docs.ts @@ -386,6 +386,13 @@ Object.entries(CATEGORIES).forEach(([category, title]) => { mdx += `\n`; Array.from(zodFiles).sort().forEach(zodFile => { + // Only card zod files that actually produced a reference page. A + // `.zod.ts` whose schemas are all unrepresentable in JSON Schema — e.g. + // they embed a transform (the ADR-0031 control-flow constructs and the + // Flow edge schema carry CEL-expression transforms) — generates no page, + // so carding it would be a dangling 404 link. This aligns the index with + // `meta.json`, which already lists only generated pages. + if (!fs.existsSync(path.join(DOCS_ROOT, category, `${zodFile}.mdx`))) return; const fileTitle = zodFile.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); // Link relative to the category folder (where index.mdx lives) mdx += ` \n`; From a17911ebe8f52242f32ab8420e7fdb39c92f5945 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 00:14:41 +0000 Subject: [PATCH 2/2] docs(generator): escape lone `<`/`{` in descriptions to fix docs build The docs site build (Vercel apps/docs) was failing on every branch off main: ./content/docs/references/kernel/manifest.mdx Unexpected character `5` (U+0035) before name (MDX) A zod `.describe()` carries a SemVer range `">=4.0 <5"`; the generator's `escapeMdxDescription` only wraps `<`/`{` when it finds a matching `>`/`}`, so the lone `<5` slipped through raw and MDX parsed it as the start of a JSX tag, failing `next build` for the whole site. Escape a lone `<`/`{` (no matching close) as inline code. Verified: the full `apps/docs` build (gen:schema && gen:docs && next build) now compiles and prerenders all 1089 pages. https://claude.ai/code/session_012ti8cx3TkdiQdjCnZXZg2Q --- .changeset/docs-adr0031-control-flow.md | 5 +++++ packages/spec/scripts/build-docs.ts | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/.changeset/docs-adr0031-control-flow.md b/.changeset/docs-adr0031-control-flow.md index e4ae16dd1..3ea9fe8c7 100644 --- a/.changeset/docs-adr0031-control-flow.md +++ b/.changeset/docs-adr0031-control-flow.md @@ -13,3 +13,8 @@ docs(automation): document ADR-0031 control-flow constructs; fix dangling refere (like `Flow`/`FlowEdge`) and so have no JSON-Schema page; the index previously carded every `.zod.ts`, producing a dangling "Control Flow" 404 link. Cards now align with `meta.json` (generated pages only). +- **doc generator (build fix)**: `escapeMdxDescription` now escapes a lone `<` + or `{` that has no matching close (e.g. a SemVer range like `">=4.0 <5"` in a + `.describe()`). MDX parsed the bare `<5` as a JSX tag and failed the entire + docs build (`Unexpected character` in `kernel/manifest.mdx`); the docs site now + builds again. diff --git a/packages/spec/scripts/build-docs.ts b/packages/spec/scripts/build-docs.ts index c93e0c3a6..2139e33ee 100644 --- a/packages/spec/scripts/build-docs.ts +++ b/packages/spec/scripts/build-docs.ts @@ -185,6 +185,12 @@ function generateMarkdown(schemaName: string, schema: any, category: string, zod i = end; continue; } + // No matching close — a lone `<` / `{` (e.g. a SemVer range like + // ">=4.0 <5" in a `.describe()`). MDX would parse it as the start of a + // JSX tag / expression and fail the whole docs build, so escape the + // single character as inline code. + out += '`' + ch + '`'; + continue; } out += ch; }