|
2136 | 2136 | "toMajor": 18, |
2137 | 2137 | "rationale": "An approval node's executor (`plugin-approvals`) parses `node.config` against `ApprovalNodeConfigSchema` before it does anything else and fails the node on ANY issue. Registration already refused an undeclared key, against the descriptor's published `configSchema`, but a refused value (`timeoutHours: 0.5`) registered and then failed every run that reached the node — the config is metadata, and no rerun could succeed. The build doors asked about neither: `FlowSchema.parse` judged only the builtin node types' executor contracts, and only for a key left out, so `objectstack validate` and `objectstack compile` exited 0 on an `escalation.bogusKey` or a `timeoutHours: 0.5` and compile copied it into the artifact. The contract is the spec's own, so the build can judge it with no plugin loaded: the approval node joins a declared contract map beside the builtin executor contracts, read by the one judge `FlowSchema.parse`, `AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share (`flowNodeConfigRefusals`), and is judged WHOLE — every issue the contract raises is refused, because the executor refuses on every one. An undeclared key or a refused value is `node-config-refused-by-contract`, anchored at the key, in the contract's own sentence (its did-you-mean included); a key left out keeps `node-config-key-missing` or `node-config-key-required-by-rule`. The builtin arm is unchanged and stays presence-only. A plugin node type whose contract the spec does not declare stays outside the build doors, as before. ⚠️ No D2 conversion: the platform cannot know the approvers, the key or the value the author meant, and no value it could write would keep what the flow did. ⚠️ Where such a node already sits the whole flow is refused: registered from the metadata registry or `sys_metadata` at boot it is skipped with a `warn` naming it, its trigger not armed, while the flows beside it register; a `defineStack({ flows })` source throws `StackSchemaInvalidError` for the whole stack; an artifact file is refused whole at load. ADR-0087, ADR-0019." |
2138 | 2138 | }, |
| 2139 | + { |
| 2140 | + "surface": "flows[].nodes[].config.outputVariable of a get_record, create_record, map, script or subflow node, and flows[].nodes[].config.errorVariable of a try_catch node — a variable name that starts with a dollar sign such as $caught, other than the default $error on errorVariable. Reachable wherever a flow is authored or stored: defineStack flows sources, defineFlow, an exported stack passed to objectstack validate or objectstack compile, a flow saved from the Studio flow designer, and a flow row already in sys_metadata", |
| 2141 | + "replacement": "the same name without the dollar sign, read as a hole over that name: errorVariable: 'caught' read as {{ caught.message }}, outputVariable: 'lead' read as {{ lead.name }}. A try_catch may instead drop errorVariable and read the engine's default, {{ $error.message }}. Rename every read of the old name with it — a text-slot hole, a CEL expression, a single-brace token in a value position", |
| 2142 | + "migrationId": "flow-binding-variable-dollar-name-refused", |
| 2143 | + "toMajor": 18, |
| 2144 | + "rationale": "The dollar-named variables are the flow engine's own: it binds $record, $runId, $flowName, $flowLabel and $error, a flat-graph loop binds $loopItems and $loopIndex, and a resume signal may not write any dollar name. A flow text slot refuses a hole whose root is a dollar name the engine does not bind, so a flow that bound $caught as its errorVariable could not read {{ $caught.message }}: the refusal told the author to drop the dollar sign, while the binding key itself took any string. One contract had two answers to whether an author may own a dollar name. The binding keys now give the text slots' answer: each key states the rule as a JSON Schema pattern, so the published schema refuses what the parse refuses, and the node contract, registerFlow, objectstack validate and the run itself refuse such a name at the key, naming the same name without the dollar sign. A binding over an engine name (outputVariable: '$record') would also have overwritten the engine's value for the rest of the run. No D2 conversion exists: the bare name may already be bound in the flow, and the reads of the old name sit in every dialect a flow string speaks, so the rename is the author's. Where such a node already sits the whole flow is refused: registered from the metadata registry or sys_metadata at boot it is skipped with a warn naming it, its trigger not armed, while the flows beside it register; a stack source throws StackSchemaInvalidError for the whole stack. ADR-0087, ADR-0031." |
| 2145 | + }, |
2139 | 2146 | { |
2140 | 2147 | "surface": "a get_record, create_record, update_record, delete_record, notify, http, screen, map, loop or parallel flow node whose config carries a key its executor contract does not declare — a typo (titl), a key the walk at registration already named (fieldValues on a write node, bulk on update_record, visibleIf on a screen field), a key copied from another node type (outputVariable on an http node, flowName on a loop), or a key nothing reads (bogusKey) — at the config itself, or on a screen field or one of its options, a body-less legacy loop included. Never a key inside a free-form map (a filter, fields, headers, defaults, input, payload or templateData key is author data), never a key on a region object (a loop body, a parallel branch) or on its nodes and edges (the region check at registration owns those), and never a try_catch key, which try-catch-and-retry-policy-undeclared-keys-refused covers once the retry policy closed. Reachable wherever a flow is authored or stored: defineStack({ flows }) sources, defineFlow(), an exported stack passed to objectstack validate or objectstack compile, a flow saved from the Studio flow designer, and a flow row already sitting in sys_metadata", |
2141 | 2148 | "replacement": "the key the contract declares, or no key: rename a typo to the declared key it meant (the refusal carries the contract's did-you-mean for a near miss), follow the contract's own prescription for a known slip (`fieldValues` → `fields`, `bulk` / `all` / `multiple` → `multi: true`, `options: { multi }` → a top-level `multi`, a screen field's `visibleIf` → `visibleWhen`, a loop's `itemVariable` → `iteratorVariable`), and delete a key nothing reads (an `http` node's `outputVariable` among them: the http executor binds no output variable)", |
|
5767 | 5774 | "toMajor": 18, |
5768 | 5775 | "rationale": "An approval node's executor (`plugin-approvals`) parses `node.config` against `ApprovalNodeConfigSchema` before it does anything else and fails the node on ANY issue. Registration already refused an undeclared key, against the descriptor's published `configSchema`, but a refused value (`timeoutHours: 0.5`) registered and then failed every run that reached the node — the config is metadata, and no rerun could succeed. The build doors asked about neither: `FlowSchema.parse` judged only the builtin node types' executor contracts, and only for a key left out, so `objectstack validate` and `objectstack compile` exited 0 on an `escalation.bogusKey` or a `timeoutHours: 0.5` and compile copied it into the artifact. The contract is the spec's own, so the build can judge it with no plugin loaded: the approval node joins a declared contract map beside the builtin executor contracts, read by the one judge `FlowSchema.parse`, `AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share (`flowNodeConfigRefusals`), and is judged WHOLE — every issue the contract raises is refused, because the executor refuses on every one. An undeclared key or a refused value is `node-config-refused-by-contract`, anchored at the key, in the contract's own sentence (its did-you-mean included); a key left out keeps `node-config-key-missing` or `node-config-key-required-by-rule`. The builtin arm is unchanged and stays presence-only. A plugin node type whose contract the spec does not declare stays outside the build doors, as before. ⚠️ No D2 conversion: the platform cannot know the approvers, the key or the value the author meant, and no value it could write would keep what the flow did. ⚠️ Where such a node already sits the whole flow is refused: registered from the metadata registry or `sys_metadata` at boot it is skipped with a `warn` naming it, its trigger not armed, while the flows beside it register; a `defineStack({ flows })` source throws `StackSchemaInvalidError` for the whole stack; an artifact file is refused whole at load. ADR-0087, ADR-0019." |
5769 | 5776 | }, |
| 5777 | + { |
| 5778 | + "surface": "flows[].nodes[].config.outputVariable of a get_record, create_record, map, script or subflow node, and flows[].nodes[].config.errorVariable of a try_catch node — a variable name that starts with a dollar sign such as $caught, other than the default $error on errorVariable. Reachable wherever a flow is authored or stored: defineStack flows sources, defineFlow, an exported stack passed to objectstack validate or objectstack compile, a flow saved from the Studio flow designer, and a flow row already in sys_metadata", |
| 5779 | + "replacement": "the same name without the dollar sign, read as a hole over that name: errorVariable: 'caught' read as {{ caught.message }}, outputVariable: 'lead' read as {{ lead.name }}. A try_catch may instead drop errorVariable and read the engine's default, {{ $error.message }}. Rename every read of the old name with it — a text-slot hole, a CEL expression, a single-brace token in a value position", |
| 5780 | + "migrationId": "flow-binding-variable-dollar-name-refused", |
| 5781 | + "toMajor": 18, |
| 5782 | + "rationale": "The dollar-named variables are the flow engine's own: it binds $record, $runId, $flowName, $flowLabel and $error, a flat-graph loop binds $loopItems and $loopIndex, and a resume signal may not write any dollar name. A flow text slot refuses a hole whose root is a dollar name the engine does not bind, so a flow that bound $caught as its errorVariable could not read {{ $caught.message }}: the refusal told the author to drop the dollar sign, while the binding key itself took any string. One contract had two answers to whether an author may own a dollar name. The binding keys now give the text slots' answer: each key states the rule as a JSON Schema pattern, so the published schema refuses what the parse refuses, and the node contract, registerFlow, objectstack validate and the run itself refuse such a name at the key, naming the same name without the dollar sign. A binding over an engine name (outputVariable: '$record') would also have overwritten the engine's value for the rest of the run. No D2 conversion exists: the bare name may already be bound in the flow, and the reads of the old name sit in every dialect a flow string speaks, so the rename is the author's. Where such a node already sits the whole flow is refused: registered from the metadata registry or sys_metadata at boot it is skipped with a warn naming it, its trigger not armed, while the flows beside it register; a stack source throws StackSchemaInvalidError for the whole stack. ADR-0087, ADR-0031." |
| 5783 | + }, |
5770 | 5784 | { |
5771 | 5785 | "surface": "a get_record, create_record, update_record, delete_record, notify, http, screen, map, loop or parallel flow node whose config carries a key its executor contract does not declare — a typo (titl), a key the walk at registration already named (fieldValues on a write node, bulk on update_record, visibleIf on a screen field), a key copied from another node type (outputVariable on an http node, flowName on a loop), or a key nothing reads (bogusKey) — at the config itself, or on a screen field or one of its options, a body-less legacy loop included. Never a key inside a free-form map (a filter, fields, headers, defaults, input, payload or templateData key is author data), never a key on a region object (a loop body, a parallel branch) or on its nodes and edges (the region check at registration owns those), and never a try_catch key, which try-catch-and-retry-policy-undeclared-keys-refused covers once the retry policy closed. Reachable wherever a flow is authored or stored: defineStack({ flows }) sources, defineFlow(), an exported stack passed to objectstack validate or objectstack compile, a flow saved from the Studio flow designer, and a flow row already sitting in sys_metadata", |
5772 | 5786 | "replacement": "the key the contract declares, or no key: rename a typo to the declared key it meant (the refusal carries the contract's did-you-mean for a near miss), follow the contract's own prescription for a known slip (`fieldValues` → `fields`, `bulk` / `all` / `multiple` → `multi: true`, `options: { multi }` → a top-level `multi`, a screen field's `visibleIf` → `visibleWhen`, a loop's `itemVariable` → `iteratorVariable`), and delete a key nothing reads (an `http` node's `outputVariable` among them: the http executor binds no output variable)", |
|
0 commit comments