|
| 1 | +--- |
| 2 | +"@objectstack/spec": major |
| 3 | +--- |
| 4 | + |
| 5 | +feat(spec)!: reject unknown keys on the flow-node config contracts (#4001 批 9) |
| 6 | + |
| 7 | +The first `automation/` wave of the 2026-08-03 "necessary-and-complete" |
| 8 | +ruling. Fourteen strip sites across three files close, and `automation/`'s |
| 9 | +remaining-strip count drops 67 → 53 (authorable 41 → 27). |
| 10 | + |
| 11 | +- **`automation/io-node-config.zod.ts`** — `NotifyConfigSchema`, |
| 12 | + `HttpConfigSchema`. |
| 13 | +- **`automation/builtin-node-config.zod.ts`** — the CRUD quartet |
| 14 | + (`get_record` / `create_record` / `update_record` / `delete_record`), |
| 15 | + `ScreenConfigSchema`, `ScreenFieldConfigSchema` and its `options` item, |
| 16 | + `MapConfigSchema`. |
| 17 | +- **`automation/schemaless-node-config.zod.ts`** — `ScriptConfigSchema`, |
| 18 | + `SubflowConfigSchema`, `DecisionConfigSchema`, `DecisionConditionSchema`. |
| 19 | + |
| 20 | +The deliberately-open `FlowNodeSchema.config` SLOT is unchanged — ADR-0018 |
| 21 | +keeps `node.type` open so plugins contribute their own executors, and closing |
| 22 | +the slot would close that extension point. What is closed is the per-node-type |
| 23 | +contract *inside* it. |
| 24 | + |
| 25 | +**Why the third file is different.** `registerFlow()` already hard-rejects |
| 26 | +undeclared config keys against a node's descriptor `configSchema` (#4277), and |
| 27 | +`script` / `subflow` / `decision` publish no descriptor `configSchema` — so |
| 28 | +that walk skips them by construction. Until now those three had **no** |
| 29 | +unknown-key enforcement at any layer. For them this is the first gate, not a |
| 30 | +second one. |
| 31 | + |
| 32 | +**Migration.** Every key now rejected was previously stripped and had no |
| 33 | +runtime effect, so removing or renaming one never changes behaviour. All three |
| 34 | +shipped example apps were re-validated after the change and no stored shape |
| 35 | +needed an ADR-0087 conversion (160 flow nodes walked, 52 carrying one of these |
| 36 | +contracts, 0 rejections). The rejections carry their own prescriptions: |
| 37 | + |
| 38 | +- `notify`: `to` → `recipients`, `subject` → `title`, `body` → `message`, |
| 39 | + `url` → `actionUrl`, `source: { object, id }` → `sourceObject` + `sourceId`. |
| 40 | +- CRUD: `object` → `objectName`, `filters` → `filter`, |
| 41 | + `fieldValues` → `fields`, `recordId` → a filter VALUE |
| 42 | + (`filter: { id: '{record.id}' }` — no CRUD executor has ever read a |
| 43 | + `recordId` key), and on `update_record` / `delete_record` `outputVariable` |
| 44 | + is a documented absence, not a typo — read the row back with a following |
| 45 | + `get_record`. |
| 46 | +- `screen`: `object` → `objectName`, and on a field item |
| 47 | + `visibleIf` → `visibleWhen`. |
| 48 | +- `map` / `subflow`: `flow` → `flowName`. `subflow`'s `timeoutMs` belongs on |
| 49 | + the NODE (`FlowNodeSchema.timeoutMs`), not in its config. |
| 50 | +- `script`: `functionName` → `function`, `input` → `inputs` (the singular |
| 51 | + stays canonical on `connector_action`'s `connectorConfig.input` — do not |
| 52 | + "fix" that one). The five `actionType`-branch keys keep their existing |
| 53 | + `retiredKey()` tombstones. |
| 54 | +- `decision`: `config.condition` (singular) is **not** renamed to |
| 55 | + `conditions`. Nothing reads it on a decision — it is the trigger gate on a |
| 56 | + `start` node and inert everywhere else (#4414) — and declaring branches here |
| 57 | + *and* on the out-edges is the double-declaration #4414 was filed for. |
| 58 | + Branching lives on the out-edges. On a decision BRANCH the predicate slot is |
| 59 | + `expression`, so `condition` → `expression` there. |
| 60 | +- decision branch `target`: a VIRTUAL designer column projected from the |
| 61 | + node's out-edges, never stored — route by matching the branch `label` to an |
| 62 | + out-edge `label`. |
| 63 | + |
| 64 | +For a key rewritten at load by an ADR-0087 D2 conversion, reaching this |
| 65 | +rejection means the config carries BOTH spellings: `renameConfigKey` leaves a |
| 66 | +shadowed alias in place rather than clobbering the canonical winner, so the |
| 67 | +retired twin is dead weight and should be deleted. |
0 commit comments