From ca829754be9e470f68a98fbc8054ee57478ae9db Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 00:50:28 +0000 Subject: [PATCH 1/5] wip(spec): FlowSchema runs the value-slot and text-slot judges (measurement) Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude --- packages/spec/src/automation/flow.zod.ts | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/spec/src/automation/flow.zod.ts b/packages/spec/src/automation/flow.zod.ts index 4dee4a1dcc8..3f668071f9a 100644 --- a/packages/spec/src/automation/flow.zod.ts +++ b/packages/spec/src/automation/flow.zod.ts @@ -26,6 +26,11 @@ import { strictObject } from '../shared/strict-object'; import { collectFlowGraphs, parseFlowNodeRegions } from './control-flow.zod'; import { predicateSlotRefusal, resolveFlowNodeExpressions } from './flow-node-expression-paths'; import { flowNodeConfigRefusals } from './flow-node-config-refusals'; +// [#23320] The two slot judges, asked here as at every other door — never a +// second reading of the dialect. `flow-node-config-refusals.ts` already reads +// both (its "reported once" exemption), so the import graph gains nothing. +import { flowNodeTextSlotSources, textSlotTemplateRefusal } from './flow-text-slot-template'; +import { flowNodeValueTemplateRefusals } from './flow-value-slot-template'; import { EndConfigSchema } from './builtin-node-config.zod'; // [#22572] The `$` names are the flow engine's at every binding door — the one // rule, composed into a declared variable's `name` and judging an `assignment` @@ -1534,6 +1539,31 @@ export const FlowSchema = lazySchema(() => strictObject( }); } + // [#23320] A `{…}` token at a VALUE slot or a TEXT slot — the two slot + // judges, `flowNodeValueTemplateRefusals` and `textSlotTemplateRefusal` over + // `flowNodeTextSlotSources`, the same calls `registerFlow` and `objectstack + // validate` make — refused here too, at the path the author wrote, each + // once: the contract arm above stands aside at exactly these paths + // (`slotJudgeRefusedPaths`, #23260). An `end` node's `message` is judged by + // `parseEndNodeConfig` (its contract composes the text-slot judge), so it is + // not judged twice here. + for (const graph of collectFlowGraphs(flow)) { + graph.nodes.forEach((node, index) => { + const type: unknown = (node as { type?: unknown } | null)?.type; + if (typeof type !== 'string') return; + const config = (node as { config?: unknown }).config; + const at = [...graph.path, 'nodes', index, 'config']; + for (const refusal of flowNodeValueTemplateRefusals(type, config)) { + ctx.addIssue({ code: 'custom', path: [...at, ...ledgerPathSegments(refusal.path)], message: refusal.message }); + } + if (type === 'end') return; + for (const slot of flowNodeTextSlotSources(type, config)) { + const message = textSlotTemplateRefusal(slot.source); + if (message !== undefined) ctx.addIssue({ code: 'custom', path: [...at, ...ledgerPathSegments(slot.path)], message }); + } + }); + } + // An `assignment` node's targets are bindings (#22572), and the `$` names are // the flow engine's at every binding door — `assignments: { $record: … }` // would overwrite the trigger record for the rest of the run. No executor From d5b704543aac13ae05f6d121600d934df8c15f27 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 01:27:15 +0000 Subject: [PATCH 2/5] =?UTF-8?q?revert(spec):=20the=20parse=20does=20not=20?= =?UTF-8?q?run=20the=20slot=20judges=20=E2=80=94=20the=20measurement=20of?= =?UTF-8?q?=20route=20(a)=20is=20recorded,=20not=20landed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route (a) (ca829754b) put flowNodeValueTemplateRefusals and the text-slot judge in FlowSchema's refinement. Measured against it: registerFlow's located slot refusal and objectstack validate's expression-invalid finding are both preempted by the raw schema issue list, and the stored flow canonicalization fails a row the v18 stored pass lists as a TODO today. Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude --- packages/spec/src/automation/flow.zod.ts | 30 ------------------------ 1 file changed, 30 deletions(-) diff --git a/packages/spec/src/automation/flow.zod.ts b/packages/spec/src/automation/flow.zod.ts index 3f668071f9a..4dee4a1dcc8 100644 --- a/packages/spec/src/automation/flow.zod.ts +++ b/packages/spec/src/automation/flow.zod.ts @@ -26,11 +26,6 @@ import { strictObject } from '../shared/strict-object'; import { collectFlowGraphs, parseFlowNodeRegions } from './control-flow.zod'; import { predicateSlotRefusal, resolveFlowNodeExpressions } from './flow-node-expression-paths'; import { flowNodeConfigRefusals } from './flow-node-config-refusals'; -// [#23320] The two slot judges, asked here as at every other door — never a -// second reading of the dialect. `flow-node-config-refusals.ts` already reads -// both (its "reported once" exemption), so the import graph gains nothing. -import { flowNodeTextSlotSources, textSlotTemplateRefusal } from './flow-text-slot-template'; -import { flowNodeValueTemplateRefusals } from './flow-value-slot-template'; import { EndConfigSchema } from './builtin-node-config.zod'; // [#22572] The `$` names are the flow engine's at every binding door — the one // rule, composed into a declared variable's `name` and judging an `assignment` @@ -1539,31 +1534,6 @@ export const FlowSchema = lazySchema(() => strictObject( }); } - // [#23320] A `{…}` token at a VALUE slot or a TEXT slot — the two slot - // judges, `flowNodeValueTemplateRefusals` and `textSlotTemplateRefusal` over - // `flowNodeTextSlotSources`, the same calls `registerFlow` and `objectstack - // validate` make — refused here too, at the path the author wrote, each - // once: the contract arm above stands aside at exactly these paths - // (`slotJudgeRefusedPaths`, #23260). An `end` node's `message` is judged by - // `parseEndNodeConfig` (its contract composes the text-slot judge), so it is - // not judged twice here. - for (const graph of collectFlowGraphs(flow)) { - graph.nodes.forEach((node, index) => { - const type: unknown = (node as { type?: unknown } | null)?.type; - if (typeof type !== 'string') return; - const config = (node as { config?: unknown }).config; - const at = [...graph.path, 'nodes', index, 'config']; - for (const refusal of flowNodeValueTemplateRefusals(type, config)) { - ctx.addIssue({ code: 'custom', path: [...at, ...ledgerPathSegments(refusal.path)], message: refusal.message }); - } - if (type === 'end') return; - for (const slot of flowNodeTextSlotSources(type, config)) { - const message = textSlotTemplateRefusal(slot.source); - if (message !== undefined) ctx.addIssue({ code: 'custom', path: [...at, ...ledgerPathSegments(slot.path)], message }); - } - }); - } - // An `assignment` node's targets are bindings (#22572), and the `$` names are // the flow engine's at every binding door — `assignments: { $record: … }` // would overwrite the trigger record for the rest of the run. No executor From 92aefd3f3da338fb4b885148275c3844f57194ff Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 01:28:59 +0000 Subject: [PATCH 3/5] docs(spec): the slot judges' door lists name the parse and why it is not one of them The value-slot and text-slot judges run at registerFlow, at the validateStackExpressions rule (objectstack validate, objectstack compile, the runtime publish gate of an active save) and at every run. FlowSchema.parse runs neither, but for an end node's message through EndConfigSchema. Both module docs now say so, with the two measured reasons: a parse refusal would replace registerFlow's and the validate rule's located refusal with the parse's issue list, and would fail the row in the stored pass, whose verdict a refusal after the parse leaves unmoved. Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude --- .../src/automation/flow-text-slot-template.ts | 24 +++++++++-- .../automation/flow-value-slot-template.ts | 42 +++++++++++++++++++ packages/spec/src/automation/flow.zod.ts | 5 ++- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/packages/spec/src/automation/flow-text-slot-template.ts b/packages/spec/src/automation/flow-text-slot-template.ts index 3bc4d46552b..c90448d8068 100644 --- a/packages/spec/src/automation/flow-text-slot-template.ts +++ b/packages/spec/src/automation/flow-text-slot-template.ts @@ -26,13 +26,31 @@ * dialect: a single-brace `{…}` token is refused, and the refusal names the * `{{ }}` spelling of each token or, for a token no hole can spell, where to * compute it. The contracts compose it (`NotifyConfigSchema`, - * `ScreenConfigSchema`, `EndConfigSchema`), and `AutomationEngine.registerFlow` - * and `objectstack validate` call it on every slot + * `HttpConfigSchema`, `ScreenConfigSchema`, `EndConfigSchema`), and + * `AutomationEngine.registerFlow` and the `validateStackExpressions` rule + * (`objectstack validate`, `objectstack compile`, the runtime publish gate of + * an `active` metadata save) call it on every slot * {@link flowNodeTextSlotSources} finds — ⛔ never a second reading of the - * dialect anywhere. Those two doors then compile each slot's `{{ }}` holes + * dialect anywhere. Those doors then compile each slot's `{{ }}` holes * (`validateExpression('template', …)`), which the spec cannot do itself: the * template engine is a runtime. * + * ## Not at `FlowSchema.parse`, but for an `end` node (#23320) + * + * The flow parse runs this judge on one slot: a top-level `end` node's + * `message`, because `end` has no executor, so its contract, `EndConfigSchema`, + * has no door but the parse (`parseEndNodeConfig` in `flow.zod.ts`). On every + * other text slot the parse leaves the token to the doors above, as it leaves + * a value-slot token to the value-slot judge's: `flowNodeConfigRefusals`' + * value arm stands aside at the paths this judge refuses + * (`slotJudgeRefusedPaths`, #23260), and the parse calls no slot judge of its + * own. The reasons are measured once, for both judges, in + * `flow-value-slot-template.ts` ("The doors that run it"): a refusal at the + * parse would replace `registerFlow`'s located line and `objectstack + * validate`'s `expression-invalid` finding with the parse's issue list, and + * would fail the row in the stored pass (`os migrate meta --stored`), whose + * verdict a refusal after the parse leaves unmoved. + * * ## Refused, not converted (ADR-0087 D2) * * D2 lets a conversion rewrite only what maps LOSSLESSLY. Every token spelling diff --git a/packages/spec/src/automation/flow-value-slot-template.ts b/packages/spec/src/automation/flow-value-slot-template.ts index eb213bb7bd3..fbbc14a222d 100644 --- a/packages/spec/src/automation/flow-value-slot-template.ts +++ b/packages/spec/src/automation/flow-value-slot-template.ts @@ -28,6 +28,48 @@ * `AutomationEngine.registerFlow`, `objectstack validate` and the executors * call it — ⛔ never a second reading of the dialect anywhere. * + * ## The doors that run it — and why `FlowSchema.parse` is not one (#23320) + * + * The judge runs at every door that reports a flow's refusals per slot: + * `AutomationEngine.registerFlow`, after its parse; the `validateStackExpressions` + * rule (`expression-invalid` at `error`), which `objectstack validate`, + * `objectstack compile` and the runtime publish gate of an `active` metadata + * save run; and every run, through the node contracts that compose + * `FlowValueSlotSchema`. The flow parse does NOT run it: `FlowSchema.parse`, + * and so `defineStack`, `ObjectStackDefinitionSchema`, the `flow` metadata + * type's schema and an artifact's parse, accept a value-slot token. The parse + * parses the node contracts too (`flowNodeConfigRefusals`' value arm), and + * that arm stands aside at exactly the paths this judge refuses + * (`slotJudgeRefusedPaths`, #23260), so the parse leaves the token to the + * doors above, on purpose. Two measured reasons, taken by running this judge + * and the text-slot judge in `FlowSchema`'s refinement and reverting it: + * + * - **The located refusal.** `registerFlow` parses first, and a parse + * refusal reaches its caller as the parse's own issue list. A token + * refused at the parse would replace `registerFlow`'s located line — the + * node's id and type, the slot's label, the config path, the remedy and the + * `source:` — at registration and in the boot's log, and `objectstack + * validate` and `objectstack compile`, which parse the stack before their + * rules run, would stop at the parse before the `expression-invalid` + * finding. Every pin of either located refusal went red. + * - **The stored pass.** The parse is also the stored-flow canonicalization + * (`AutomationEngine.canonicalizeStoredFlow`) that `os migrate meta + * --stored` runs on each flow row, and a parse refusal reports the row + * `failed`, which flips `storedMigrationClean`: a non-zero exit that no run + * of the command can clear, because no conversion rewrites the token (the + * next section). A refusal after the parse is listed instead, as a TODO + * under the step-18 D3 entry `flow-value-slot-template-dialect-refused` + * (`conversions/refused-shapes.ts`), and the verdict does not move. The + * pin that a braced filter operand parses and is listed went red. + * + * What this leaves: a stack built by `defineStack` and handed to none of those + * doors — `os dev` and `os serve` load it and boot — reaches `registerFlow` at + * boot, which refuses the flow with its located line and does not register it. + * The judge can join the parse once `registerFlow` reports a parse refusal as + * located as its own and the stored pass lists a slot the parse refuses + * rather than failing its row; the contract arm's exemption then keeps each + * token reported once. + * * ## Refused, not converted (ADR-0087 D2) * * D2 lets a conversion rewrite only what maps LOSSLESSLY. Every token spelling diff --git a/packages/spec/src/automation/flow.zod.ts b/packages/spec/src/automation/flow.zod.ts index 4dee4a1dcc8..7d9189d0da3 100644 --- a/packages/spec/src/automation/flow.zod.ts +++ b/packages/spec/src/automation/flow.zod.ts @@ -1503,7 +1503,10 @@ export const FlowSchema = lazySchema(() => strictObject( // value carrying a `{token}` included since #23260 (`get_record` // `limit: '{x}'`), never a region slot, a ledger predicate or value slot, // a value or text slot whose own judge refuses its token, or `http`'s - // run-resolved `signingSecret`. A KEY its + // run-resolved `signingSecret`. (#23320) This parse runs neither slot + // judge itself, but for an `end` node's `message` through its contract: + // the doors that do, and the measured reasons the parse is not one, are + // in `flow-value-slot-template.ts` ("The doors that run it"). A KEY its // contract does not declare is refused too (#21982), for every builtin // `builtinNodeConfigKeysJudged` names (every one, `try_catch` included // since #22343), a body-less legacy `loop` included. The one plugin node contract the spec declares, `approval` From 28da9bd3e94291cb137b752f1d5bf6b634d54215 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 02:16:33 +0000 Subject: [PATCH 4/5] =?UTF-8?q?docs(spec):=20the=20stored-pass=20reason=20?= =?UTF-8?q?names=20both=20listings=20=E2=80=94=20a=20value-slot=20site=20u?= =?UTF-8?q?nder=20its=20D3=20entry,=20a=20text-slot=20site=20under=20regis?= =?UTF-8?q?terFlow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude --- packages/spec/src/automation/flow-text-slot-template.ts | 5 +++-- packages/spec/src/automation/flow-value-slot-template.ts | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/spec/src/automation/flow-text-slot-template.ts b/packages/spec/src/automation/flow-text-slot-template.ts index c90448d8068..bbe4135ef7e 100644 --- a/packages/spec/src/automation/flow-text-slot-template.ts +++ b/packages/spec/src/automation/flow-text-slot-template.ts @@ -48,8 +48,9 @@ * `flow-value-slot-template.ts` ("The doors that run it"): a refusal at the * parse would replace `registerFlow`'s located line and `objectstack * validate`'s `expression-invalid` finding with the parse's issue list, and - * would fail the row in the stored pass (`os migrate meta --stored`), whose - * verdict a refusal after the parse leaves unmoved. + * would fail the row in the stored pass (`os migrate meta --stored`), which + * lists a text-slot refusal made after the parse as a TODO under + * `registerFlow` and leaves its verdict unmoved. * * ## Refused, not converted (ADR-0087 D2) * diff --git a/packages/spec/src/automation/flow-value-slot-template.ts b/packages/spec/src/automation/flow-value-slot-template.ts index fbbc14a222d..d347f29a451 100644 --- a/packages/spec/src/automation/flow-value-slot-template.ts +++ b/packages/spec/src/automation/flow-value-slot-template.ts @@ -57,10 +57,11 @@ * --stored` runs on each flow row, and a parse refusal reports the row * `failed`, which flips `storedMigrationClean`: a non-zero exit that no run * of the command can clear, because no conversion rewrites the token (the - * next section). A refusal after the parse is listed instead, as a TODO - * under the step-18 D3 entry `flow-value-slot-template-dialect-refused` - * (`conversions/refused-shapes.ts`), and the verdict does not move. The - * pin that a braced filter operand parses and is listed went red. + * next section). A refusal after the parse is listed instead, as a TODO — + * a value-slot site under the step-18 D3 entry + * `flow-value-slot-template-dialect-refused` (`conversions/refused-shapes.ts`), + * a text-slot site under `registerFlow` — and the verdict does not move. + * The pins that a braced slot parses and is listed went red. * * What this leaves: a stack built by `defineStack` and handed to none of those * doors — `os dev` and `os serve` load it and boot — reaches `registerFlow` at From c7a71df7ecce8d8b9c6ff83bca00bfacd7cf2f4a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 02:50:11 +0000 Subject: [PATCH 5/5] docs(spec): the value-slot door list names the assignment executor's own call, and the measured pins as those doors' located refusals Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude --- packages/spec/src/automation/flow-value-slot-template.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/spec/src/automation/flow-value-slot-template.ts b/packages/spec/src/automation/flow-value-slot-template.ts index d347f29a451..dbff1e7810d 100644 --- a/packages/spec/src/automation/flow-value-slot-template.ts +++ b/packages/spec/src/automation/flow-value-slot-template.ts @@ -35,9 +35,10 @@ * rule (`expression-invalid` at `error`), which `objectstack validate`, * `objectstack compile` and the runtime publish gate of an `active` metadata * save run; and every run, through the node contracts that compose - * `FlowValueSlotSchema`. The flow parse does NOT run it: `FlowSchema.parse`, - * and so `defineStack`, `ObjectStackDefinitionSchema`, the `flow` metadata - * type's schema and an artifact's parse, accept a value-slot token. The parse + * `FlowValueSlotSchema` and the `assignment` executor's own call. The flow + * parse does NOT run it: `FlowSchema.parse`, and so `defineStack`, + * `ObjectStackDefinitionSchema`, the `flow` metadata type's schema and an + * artifact's parse, accept a value-slot token. The parse * parses the node contracts too (`flowNodeConfigRefusals`' value arm), and * that arm stands aside at exactly the paths this judge refuses * (`slotJudgeRefusedPaths`, #23260), so the parse leaves the token to the @@ -51,7 +52,7 @@ * `source:` — at registration and in the boot's log, and `objectstack * validate` and `objectstack compile`, which parse the stack before their * rules run, would stop at the parse before the `expression-invalid` - * finding. Every pin of either located refusal went red. + * finding. Those doors' pins of the located refusal went red. * - **The stored pass.** The parse is also the stored-flow canonicalization * (`AutomationEngine.canonicalizeStoredFlow`) that `os migrate meta * --stored` runs on each flow row, and a parse refusal reports the row