diff --git a/packages/spec/src/automation/flow-text-slot-template.ts b/packages/spec/src/automation/flow-text-slot-template.ts index 3bc4d46552b..bbe4135ef7e 100644 --- a/packages/spec/src/automation/flow-text-slot-template.ts +++ b/packages/spec/src/automation/flow-text-slot-template.ts @@ -26,13 +26,32 @@ * 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`), 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) * * 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..dbff1e7810d 100644 --- a/packages/spec/src/automation/flow-value-slot-template.ts +++ b/packages/spec/src/automation/flow-value-slot-template.ts @@ -28,6 +28,50 @@ * `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` 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 + * 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. 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 + * `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 — + * 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 + * 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`