Skip to content

Commit f6981bd

Browse files
committed
docs(spec): the migration text offers a {token} only where http interpolates before its parse
The changeset's FROM -> TO rows and the D3 entry's replacement told an author to write a {token} template in limit or maxIterations, but get_record and loop parse their config as authored, so such a value passes the build doors and fails every run. A number or boolean slot outside http now takes a literal only, http's slots keep the sole-token form, and the "still accepted" token bullet says the hold-back is no promise the value runs. The step-18 fragment says the same; the registry region is regenerated. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 22f54b0 commit f6981bd

3 files changed

Lines changed: 46 additions & 25 deletions

File tree

‎.changeset/21898-flow-builtin-node-config-values-refused.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ Clause-②: yes (narrowing)
1414

1515
**What is refused.** A node of any builtin type (`get_record`, `create_record`, `update_record`, `delete_record`, `notify`, `http`, `screen`, `script`, `subflow`, `map`, `loop`, `parallel`, `try_catch`), at any depth, whose present config value its executor contract refuses — a wrong type, a value outside the declared set or range, an empty `function` / `flowName`, or a rule finding on present keys (a `notify` `template` beside an inline `title`). The refusal is the existing closed-set code `node-config-refused-by-contract`, `params: { nodeType, key }`, anchored at the key (`nodes.N.config.outputVariable`, `nodes.N.config.fields.0.min`), from the one judge `flowNodeConfigRefusals` that `FlowSchema.parse`, `AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share. The issue's `code` is `custom`. That covers `FlowSchema`, `defineFlow()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `flows.N.nodes.M.config.<key>`), `os validate`, `os compile`, an artifact's parse, `registerFlow` and the metadata save door (`422 INVALID_METADATA`).
1616

17-
**What stays accepted, byte for byte.** Every value its contract accepts, and the values the build cannot know the run will parse:
17+
**What the build doors still accept, byte for byte.** Every value its contract accepts, and the values this arm holds back:
1818

19-
- a value carrying a `{token}` (also spelled with double braces or a leading `$`) — never refused for its pre-interpolation type;
20-
- on `http`, which parses after interpolating its whole config, any value with a token inside it, and `signingSecret` (the credential channel may supply it);
19+
- a value carrying a `{token}` (also spelled with double braces or a leading `$`) — never refused at the build doors for its pre-interpolation type. That is not a promise it runs: only `http` interpolates its config before it parses, so only an `http` slot sees the token's resolved value. Every other builtin parses its config as authored, so a token in one of its number or boolean slots (`limit: '{n}'`, `maxIterations: '{cap}'`, a screen field `min: '{m}'`, `multi: '{bulk}'`) still fails at its first run, exactly as before — write a literal there;
20+
- on `http`, any value with a token inside it, and `signingSecret` (the credential channel may supply it);
2121
- a `loop` with no `body` (its executor does not parse it), and the region slots of `loop`, `parallel` and `try_catch`;
2222
- an undeclared or retired key, a screen field's `visibleWhen` and a CRUD `fields` value — each keeps the judge it had.
2323

@@ -27,14 +27,15 @@ Clause-②: yes (narrowing)
2727
|:--|:--|
2828
| `outputVariable: 42` | `outputVariable: 'taskId'` — the variable's name |
2929
| a screen field `min: '1'`, `max: '10'` | `min: 1`, `max: 10` |
30-
| `limit: '10'`, `timeoutMs: '5000'`, `maxIterations: '5'` | `limit: 10`, `timeoutMs: 5000`, `maxIterations: 5` — or a `{token}` template computed per run |
31-
| `multi: 'true'`, `durable: 'yes'`, a screen field `required: 'yes'` | `multi: true`, `durable: true`, `required: true` |
30+
| `limit: '10'`, `maxIterations: '5'` (any number slot outside `http`) | `limit: 10`, `maxIterations: 5` — a literal number only: these executors parse the config as authored, so a `{token}` here passes the build and fails every run |
31+
| `multi: 'true'`, a screen field `required: 'yes'` (any boolean slot outside `http`) | `multi: true`, `required: true` — a literal boolean only, for the same reason |
32+
| `http` `timeoutMs: '5000'`, `durable: 'yes'` | `timeoutMs: 5000`, `durable: true` — or, on `http` alone, a sole-token template such as `timeoutMs: '{timeout}'`: `http` interpolates before it parses, so the token resolves to its value's type first |
3233
| `severity: 'loud'`, `mode: 'view'` | one of the declared values (`'info'` / `'warning'` / `'critical'`; `'create'` / `'edit'`) |
3334
| a `notify` with both `template` and `title` | one content path, as the refusal's sentence says |
3435

3536
**The one-line fix: write the value the contract declares at the key the refusal names.** The runtime never ran such a node, so the fix changes nothing a working flow does.
3637

37-
**Who is affected, measured.** At `833d57c9cf`, every builtin node `config` authored in this repository's examples, docs, skills and `packages/qa` fixtures (96 nodes), and every one in hotcrm at `4054ec2680` (138 nodes), parses under this arm. The one real writer found to store a refused value is the Studio flow designer, which saved a screen field's Min / Max as strings until objectui `5ba255538a`. Deployed metadata, and other repositories, were not measured. Where such a node already sits in a stored flow, the whole flow is refused at registration: at boot it is skipped with a warn naming it, its trigger not armed, while the flows beside it register.
38+
**Who is affected, measured.** At `833d57c9cf`, every builtin node `config` authored in this repository's examples, docs, skills and `packages/qa` fixtures (96 nodes), and every one in hotcrm at `4054ec2680` (138 nodes), parses under this arm. A second census at `d1c7d8d392` that also reads helper calls, same-file constants and assignments into a node config (1065 configs in this repository, 138 in hotcrm) found no other real writer; 64 configs here take a value from an import, a call or a spread that no static reading evaluates, and are not counted either way. The one real writer found to store a refused value is the Studio flow designer, which saved a screen field's Min / Max as strings until objectui `5ba255538a`. Deployed metadata, and other repositories, were not measured. Where such a node already sits in a stored flow, the whole flow is refused at registration: at boot it is skipped with a warn naming it, its trigger not armed, while the flows beside it register.
3839

3940
### The kit
4041

‎packages/spec/src/migrations/entries/semantic/18.flow-builtin-node-config-values-refused.ts‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,14 @@ export const entry: SemanticMigration = {
3030
replacement:
3131
'the value the contract declares, written at the key the refusal names: a string where it wants a '
3232
+ 'string (`outputVariable: \'taskId\'`), a number where it wants a number (`min: 1`, `limit: 10`, '
33-
+ '`timeoutMs: 5000`), a boolean where it wants a boolean (`multi: true`, `durable: true`), one of the '
34-
+ 'declared values (`severity: \'warning\'`, `mode: \'edit\'`), or a value inside the declared range. '
35-
+ 'A value meant to be computed per run is written as a `{token}` template (`limit: \'{page.size}\'`) — '
36-
+ 'the build never judges a token\'s type. For a rule finding, follow the rule\'s own sentence (keep '
37-
+ '`template` or the inline `title` / `message`, not both)',
33+
+ '`maxIterations: 5`, `timeoutMs: 5000`), a boolean where it wants a boolean (`multi: true`, '
34+
+ '`durable: true`), one of the declared values (`severity: \'warning\'`, `mode: \'edit\'`), or a value '
35+
+ 'inside the declared range. Outside `http`, a number or boolean slot takes a LITERAL only: those '
36+
+ 'executors parse the config as authored, so a `{token}` template there (`limit: \'{page.size}\'`, '
37+
+ '`maxIterations: \'{cap}\'`) passes the build doors and still fails every run. Only `http` '
38+
+ 'interpolates its config before it parses, so only an `http` slot may also take a sole-token template '
39+
+ 'that resolves to the declared type (`timeoutMs: \'{timeout}\'`, `durable: \'{durable}\'`). For a rule '
40+
+ 'finding, follow the rule\'s own sentence (keep `template` or the inline `title` / `message`, not both)',
3841
reason:
3942
'Every builtin executor (`service-automation` `builtin/`) parses its node\'s `config` against the '
4043
+ 'contract `getBuiltinNodeConfigContracts()` names before it acts, and refuses the node on any '
@@ -46,8 +49,11 @@ export const entry: SemanticMigration = {
4649
+ '`AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share '
4750
+ '(`flowNodeConfigRefusals`) now refuses such a value as `node-config-refused-by-contract`, anchored '
4851
+ 'at the key, in the contract\'s own words — the code the approval contract already uses. It judges '
49-
+ 'only what the build can know the run will parse: a value carrying a `{token}` is never refused for '
50-
+ 'its pre-interpolation type; `http` parses after interpolating its whole config, so only token-free '
52+
+ 'only what the build can know the run will parse, and holds one more class back by ruling: a value '
53+
+ 'carrying a `{token}` is never refused at the build doors for its pre-interpolation type — which is '
54+
+ 'no promise it runs, since every builtin but `http` parses its config as authored and so still '
55+
+ 'refuses a token in a number or boolean slot at its first run; `http` parses after interpolating its '
56+
+ 'whole config, so only token-free '
5157
+ 'values are judged there and never `signingSecret`, which the credential channel may supply; a '
5258
+ '`loop` with no `body` is not parsed by its executor and is judged for nothing; the region slots of '
5359
+ '`loop`, `parallel` and `try_catch` are judged as graphs of their own and by `validateControlFlow`. '
@@ -67,6 +73,9 @@ export const entry: SemanticMigration = {
6773
+ 'For each hit write the value the contract declares, per the replacement. Two proofs. (1) For a '
6874
+ 'stack authored in config files, `objectstack validate` is clean. (2) Boot the stack and confirm '
6975
+ 'each flow REGISTERS: no `failed to register flow` warn for it — that warn line is the locator for '
70-
+ 'a row that exists only in `sys_metadata`. A node whose values its contract accepts, and any value '
71-
+ 'written as a `{token}` template, parses and registers byte-identically to before.',
76+
+ 'a row that exists only in `sys_metadata`. A node whose values its contract accepts parses and '
77+
+ 'registers byte-identically to before. A `{token}` template parses and registers as before too, and '
78+
+ 'runs only where the run parses it after interpolation (`http`) or where the slot takes a string; in a '
79+
+ 'number or boolean slot of any other builtin it fails at its first run exactly as it did, so write a '
80+
+ 'literal there.',
7281
};

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5605,7 +5605,9 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [
56055605
+ 'contract before it acts, so a `create_record` `outputVariable: 42` or a screen field `min: \'1\'` '
56065606
+ 'used to pass `objectstack validate` and `objectstack compile`, register, and fail every run that '
56075607
+ 'reached the node. The arm judges only what the build can know the run will parse: never a value '
5608-
+ 'carrying a `{token}`, whatever its slot\'s type; on `http`, which parses after interpolating, only '
5608+
+ 'carrying a `{token}`, whatever its slot\'s type (held back by ruling, not admitted: outside `http` '
5609+
+ 'such a token in a number or boolean slot still fails at its first run, so those slots take a '
5610+
+ 'literal); on `http`, which parses after interpolating, only '
56095611
+ 'token-free values and never the credential-held `signingSecret`; on a `loop`, only one with a '
56105612
+ '`body`; on the region containers, never the region slots. Key membership is untouched. No key '
56115613
+ 'is removed, so there is no tombstone, and no D2 conversion exists: the platform cannot know the '
@@ -13184,11 +13186,14 @@ const step18: MigrationStep = {
1318413186
replacement:
1318513187
'the value the contract declares, written at the key the refusal names: a string where it wants a '
1318613188
+ 'string (`outputVariable: \'taskId\'`), a number where it wants a number (`min: 1`, `limit: 10`, '
13187-
+ '`timeoutMs: 5000`), a boolean where it wants a boolean (`multi: true`, `durable: true`), one of the '
13188-
+ 'declared values (`severity: \'warning\'`, `mode: \'edit\'`), or a value inside the declared range. '
13189-
+ 'A value meant to be computed per run is written as a `{token}` template (`limit: \'{page.size}\'`) — '
13190-
+ 'the build never judges a token\'s type. For a rule finding, follow the rule\'s own sentence (keep '
13191-
+ '`template` or the inline `title` / `message`, not both)',
13189+
+ '`maxIterations: 5`, `timeoutMs: 5000`), a boolean where it wants a boolean (`multi: true`, '
13190+
+ '`durable: true`), one of the declared values (`severity: \'warning\'`, `mode: \'edit\'`), or a value '
13191+
+ 'inside the declared range. Outside `http`, a number or boolean slot takes a LITERAL only: those '
13192+
+ 'executors parse the config as authored, so a `{token}` template there (`limit: \'{page.size}\'`, '
13193+
+ '`maxIterations: \'{cap}\'`) passes the build doors and still fails every run. Only `http` '
13194+
+ 'interpolates its config before it parses, so only an `http` slot may also take a sole-token template '
13195+
+ 'that resolves to the declared type (`timeoutMs: \'{timeout}\'`, `durable: \'{durable}\'`). For a rule '
13196+
+ 'finding, follow the rule\'s own sentence (keep `template` or the inline `title` / `message`, not both)',
1319213197
reason:
1319313198
'Every builtin executor (`service-automation` `builtin/`) parses its node\'s `config` against the '
1319413199
+ 'contract `getBuiltinNodeConfigContracts()` names before it acts, and refuses the node on any '
@@ -13200,8 +13205,11 @@ const step18: MigrationStep = {
1320013205
+ '`AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share '
1320113206
+ '(`flowNodeConfigRefusals`) now refuses such a value as `node-config-refused-by-contract`, anchored '
1320213207
+ 'at the key, in the contract\'s own words — the code the approval contract already uses. It judges '
13203-
+ 'only what the build can know the run will parse: a value carrying a `{token}` is never refused for '
13204-
+ 'its pre-interpolation type; `http` parses after interpolating its whole config, so only token-free '
13208+
+ 'only what the build can know the run will parse, and holds one more class back by ruling: a value '
13209+
+ 'carrying a `{token}` is never refused at the build doors for its pre-interpolation type — which is '
13210+
+ 'no promise it runs, since every builtin but `http` parses its config as authored and so still '
13211+
+ 'refuses a token in a number or boolean slot at its first run; `http` parses after interpolating its '
13212+
+ 'whole config, so only token-free '
1320513213
+ 'values are judged there and never `signingSecret`, which the credential channel may supply; a '
1320613214
+ '`loop` with no `body` is not parsed by its executor and is judged for nothing; the region slots of '
1320713215
+ '`loop`, `parallel` and `try_catch` are judged as graphs of their own and by `validateControlFlow`. '
@@ -13221,8 +13229,11 @@ const step18: MigrationStep = {
1322113229
+ 'For each hit write the value the contract declares, per the replacement. Two proofs. (1) For a '
1322213230
+ 'stack authored in config files, `objectstack validate` is clean. (2) Boot the stack and confirm '
1322313231
+ 'each flow REGISTERS: no `failed to register flow` warn for it — that warn line is the locator for '
13224-
+ 'a row that exists only in `sys_metadata`. A node whose values its contract accepts, and any value '
13225-
+ 'written as a `{token}` template, parses and registers byte-identically to before.',
13232+
+ 'a row that exists only in `sys_metadata`. A node whose values its contract accepts parses and '
13233+
+ 'registers byte-identically to before. A `{token}` template parses and registers as before too, and '
13234+
+ 'runs only where the run parses it after interpolation (`http`) or where the slot takes a string; in a '
13235+
+ 'number or boolean slot of any other builtin it fails at its first run exactly as it did, so write a '
13236+
+ 'literal there.',
1322613237
},
1322713238
// The absent half of the decision-branch predicate rule. A SEPARATE entry from
1322813239
// `flow-predicate-slot-blank-string-refused` on purpose: that one keeps the

0 commit comments

Comments
 (0)