|
| 1 | +--- |
| 2 | +"@objectstack/service-automation": minor |
| 3 | +"@objectstack/spec": minor |
| 4 | +--- |
| 5 | + |
| 6 | +feat(automation,spec): flow executors `parse()` their config, and undeclared config keys reject at registration (#4277) |
| 7 | + |
| 8 | +The #4045 reconciliation left every flat builtin with a Zod config contract that |
| 9 | +nothing enforced, and #4059 left `registerFlow` warning about undeclared keys it |
| 10 | +could not yet safely reject. #4277 installs both halves of the enforcement: |
| 11 | + |
| 12 | +**1. Executors parse their config (execute time).** The 12 contract-carrying |
| 13 | +builtins — `get_record` / `create_record` / `update_record` / `delete_record`, |
| 14 | +`screen`, `map`, `notify`, `http`, `loop` / `parallel` / `try_catch` — now run |
| 15 | +`node.config` through their Zod contract before executing |
| 16 | +(`service-automation/builtin/parse-config.ts`). A type or missing-`required` |
| 17 | +violation refuses the node as a **guard** (`errorClass: 'guard'`, not routable |
| 18 | +via `fault` edges — config is metadata; re-running changes nothing), naming |
| 19 | +every violated path. `{token}` templates stay legal: string-typed slots parse |
| 20 | +the raw template, and `http` — whose executor reads the interpolated config — |
| 21 | +parses POST-interpolation, where a whole-token template has already resolved to |
| 22 | +its value's real type. Exemption: a legacy flat-graph `loop` (no `config.body`) |
| 23 | +predates the ADR-0031 construct and is not parsed. |
| 24 | + |
| 25 | +**2. Undeclared config keys are rejected at `registerFlow` (registration |
| 26 | +time).** The #4059 warning is now an error: a config key the node type's |
| 27 | +descriptor `configSchema` does not declare fails registration, with the exact |
| 28 | +path, the declared key set, a did-you-mean, and — for keys with documented |
| 29 | +history (`screen.visibleIf`, `create_record`/`update_record.fieldValues`) — a |
| 30 | +per-key tombstone (the `UNKNOWN_KEY_GUIDANCE` pattern). Unchanged exemptions: |
| 31 | +`assignment` is exempt wholesale (its top-level keys ARE the author's variable |
| 32 | +names), schemaless types (`decision`/`script`/`wait`/`subflow`/ |
| 33 | +`connector_action`) declare nothing so nothing can be undeclared, and keyValue |
| 34 | +maps stop the walk (their keys are author data). Every `registerFlow` call site |
| 35 | +already try/catches per flow, so a bad stored flow is skipped loudly at boot, |
| 36 | +never a crashed kernel. |
| 37 | + |
| 38 | +**Contract fix folded in:** `LoopConfigSchema.collection` is now |
| 39 | +`z.union([z.string().min(1), z.array(z.unknown())])` — the executor has always |
| 40 | +accepted an inline array (shared resolve logic with `map.collection`, which |
| 41 | +already declared the union), so the string-only declaration under-declared what |
| 42 | +it reads. |
| 43 | + |
| 44 | +**Migration.** If a flow stops registering: the error names the undeclared key |
| 45 | +and its path — rename it to the declared key it meant (`visibleIf` → |
| 46 | +`visibleWhen`, `fieldValues` → `fields`), or delete it (an undeclared key was |
| 47 | +never read, so removing it changes no behavior). If an executor of yours |
| 48 | +genuinely reads the key, declare it on the node type's descriptor |
| 49 | +`configSchema`. If a node starts refusing at run time: the refusal names each |
| 50 | +violated path against the contract — fix the value's type or supply the missing |
| 51 | +required key (e.g. `get_record` `limit` must be a number; `screen` |
| 52 | +`fields[].options` entries are `{ value, label }` objects; `notify` requires |
| 53 | +`recipients` + `title`). Retry-policy defaults now come from the contract: a |
| 54 | +`try_catch` `retry` block that omits `retryDelayMs` gets the documented 1000ms |
| 55 | +base delay where the executor historically used 0. |
0 commit comments