You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(spec)!: reject unknown keys on the flow-node config contracts (#4001 批 9) (#4925)
Fourteen strip sites across the three automation node-config files close —
the first `automation/` wave of the 2026-08-03 "necessary-and-complete"
ruling. `automation/` remaining-strip: 67 → 53 (authorable 41 → 27).
- io-node-config: NotifyConfig, HttpConfig
- builtin-node-config: the CRUD quartet, Screen (+ field item + its options),
Map
- schemaless-node-config: Script, Subflow, Decision, DecisionCondition
The deliberately-open `FlowNodeSchema.config` SLOT is untouched (ADR-0018
keeps `node.type` open for plugin executors); what closes is the
per-node-type contract inside it.
For `script` / `subflow` / `decision` this is the FIRST unknown-key gate,
not a second one: `registerFlow()`'s #4277 rejection derives its declared
set from a descriptor `configSchema`, and the schemaless class publishes
none — so that walk skips them by construction.
Curation is measured, not assumed. Every guidance/alias entry was checked
against a repo-wide AST scan of 630 real flow-node payloads before it was
written, and the campaign's finding 7 decided the one hard case: on a
`decision`, `config.condition` is NOT renamed to `conditions` (one edit
away, and taking that advice produces the double-declaration #4414 was
filed for) — the mechanism is named instead. On a decision BRANCH the same
word does rename, to `expression`.
ADR-0087: no conversion needed. 160 flow nodes across the three shipped
example apps were parsed against the new contracts — 52 carry one — with 0
unknown-key rejections, and all three `objectstack validate` runs pass.
Co-authored-by: Claude <noreply@anthropic.com>
|`approval.zod.ts`| 4 | authorable |**strict as of #4001 step 3** — all four authoring schemas (node config / approver / escalation / decision-output). The published JSON schema carries `additionalProperties: false` into the Studio form AND `registerFlow()` config validation (#4027/#4040), so an unknown key in an approval node's `config` is rejected at registration too — verified: `z.toJSONSchema` on the strict lazySchema does not throw (#3746 hazard checked) |
|`io-node-config.zod.ts`| 2 | authorable |`NotifyConfigSchema` / `HttpConfigSchema` (#4045) — the sibling contracts that validate the **open**`config` slot on flow `notify` / `http` nodes. Authored per-node, so the open-slot exemption above does not extend to them; candidate once the executors' own drift is verified|
539
-
|`builtin-node-config.zod.ts`| 8 | authorable | Same family (#4045): the CRUD quartet, `screen`, `map`. Written from what the executors read rather than from the descriptors' `configSchema` literals, and reconciled bidirectionally by `builtin-node-form-zod-ledger.test.ts` — so unlike most rows here, this one already has a drift check of its own. Same candidacy note as `io-node-config`|
540
-
|`schemaless-node-config.zod.ts`| 4 | authorable | Same family, third panel (#4278): `script` / `subflow` / `decision` (+ the decision branch item) — the descriptor-schemaless nodes whose form lives in objectui's hand-written table. Written from the executors; the drift check is objectui's `flow-node-config.spec-reconciliation` test (cross-repo, via the published exports). Since #4343`script` and `subflow` ARE parsed at execute time (`parse-config.ts`) — `script` once retiring its `actionType` branches left it flat — so strictness candidacy now follows `io-node-config` on the same terms rather than being moot; `decision` stays export-only |
538
+
|`io-node-config.zod.ts`| 2 | authorable |`NotifyConfigSchema` / `HttpConfigSchema` (#4045) — the sibling contracts that validate the **open**`config` slot on flow `notify` / `http` nodes. Authored per-node, so the open-slot exemption above does not extend to them. **Strict as of #4001 批 9**; the node `config` SLOT itself stays open (ADR-0018 keeps `node.type` open, so the slot cannot be closed without closing the plugin extension point). Five `guidance` entries carry the ADR-0087 notify aliases (`to`/`subject`/`body`/`url`/`source`)|
539
+
|`builtin-node-config.zod.ts`| 8 | authorable | Same family (#4045): the CRUD quartet, `screen`, `map`. Written from what the executors read rather than from the descriptors' `configSchema` literals, and reconciled bidirectionally by `builtin-node-form-zod-ledger.test.ts` — so unlike most rows here, this one already has a drift check of its own. **Strict as of #4001 批 9.** The curated tables are the `FLOW_NODE_UNKNOWN_KEY_GUIDANCE` prose from `service-automation`'s registration door, plus two entries that door never had: `recordId` (measured on CRUD nodes across the repo's own flow fixtures, read by no executor — on `delete_record` that is #3810 wearing a key that looks like a constraint) and `outputVariable` on `update_record` / `delete_record` (a documented ABSENCE, and the likeliest wrong key precisely because five sibling contracts declare it)|
540
+
|`schemaless-node-config.zod.ts`| 4 | authorable | Same family, third panel (#4278): `script` / `subflow` / `decision` (+ the decision branch item) — the descriptor-schemaless nodes whose form lives in objectui's hand-written table. Written from the executors; the drift check is objectui's `flow-node-config.spec-reconciliation` test (cross-repo, via the published exports — it compares `.shape` key sets, so strictness does not move it). Since #4343`script` and `subflow` ARE parsed at execute time (`parse-config.ts`). **Strict as of #4001 批 9 — and this is the one row in the table where strictness is the FIRST unknown-key gate, not a second one**: `registerFlow()`'s #4277 rejection derives its declared set from a descriptor `configSchema`, so it structurally skips the schemaless class. `decision` stays export-only, closed anyway; its `condition` guidance suppresses a one-edit rename to `conditions` that #4414 proves is the worse outcome|
|`time-relative-trigger.zod.ts`| 1 | authorable |**Undeclared until the #4001 re-measurement, and invisible for the worst possible reason**: `TimeRelativeTriggerSchema` is written `z\n .object({`, the old textual counter matched zero sites, and a zero-site file is SKIPPED by the coverage walk as "nothing to classify". So the gate whose whole promise is "no undeclared surface" reported green over an authorable schema — the same shape as `data/driver/`, one layer subtler, because this time the file was not hidden by the walk but by the counter feeding it. Classification is not a guess: the file's own `@example` blocks author it by hand into a flow start node (`config: { timeRelative: { object, dateField, offsetDays, filter } }`), which is the authoring door. A stripped key here means the sweep silently never matches — `offsetDay` for `offsetDays` returns a trigger that never fires, reported as configured |
543
543
|`flow-function.zod.ts`| 1 | authorable |`FlowFunctionDeclarationSchema` (#4396) — the `{ handler, effect }` form of a `defineStack({ functions })` entry. Authored, but note what an undeclared key here would be: a sibling of a **live function**, not data. `defineStack`'s union already rejects a record whose `handler` is not callable, and the boot-path reader is the hand-written `normalizeFlowFunctionEntry` rather than a `.parse()` (re-validating a live handler every boot buys nothing), so strictness would bind at authoring only. Candidate on the same verify-first rule as its `*-node-config` neighbours |
@@ -605,25 +605,31 @@ classes; where it does, the split is stated. **Only the authorable half is in th
605
605
2026-08-03 ruling's forced scope** — wire/open rows are listed so the arithmetic
606
606
is complete and so nobody re-triages them from scratch next batch.
607
607
608
-
#### `automation/` — 67 strip of 75
608
+
#### `automation/` — 53 strip of 75
609
609
610
610
| File | Strip | Sites | Class | Batch |
611
611
|---|---|---|---|---|
612
612
|`execution.zod.ts`| 13 | 13 | wire |**out of scope** — engine-emitted run state; the ledger row already says "never strict" |
|`schemaless-node-config.zod.ts`| 4 | 4 | authorable |`Script` / `Subflow` / `DecisionCondition` / `Decision`; `script` + `subflow` ARE parsed at execute time since #4343|
621
-
|`io-node-config.zod.ts`| 2 | 2 | authorable |`NotifyConfig` / `HttpConfig` — the sibling contracts for the deliberately-open flow node `config` slot |
622
619
|`flow-function.zod.ts`| 1 | 1 | authorable |`FlowFunctionDeclarationSchema`; binds at authoring only (the boot reader is `normalizeFlowFunctionEntry`, not a `.parse()`) |
623
620
|`time-relative-trigger.zod.ts`| 1 | 1 | authorable |`TimeRelativeTriggerSchema` — **newly visible** (see its triage row); a stripped `offsetDay`/`withinDay` yields a trigger that never fires, reported as configured |
0 commit comments