Skip to content

Commit 0add8a4

Browse files
docs(spec): the slot judges' door lists name the parse, and why it is not one of them (#23320) (#23383)
Fixes #23320 Clause-②: no ## What this does The card allowed two outcomes (triage `6152852815`, unlock `6156025101`). Either the value-slot and text-slot judges run in `FlowSchema`'s refinement, or the seat measures why the parse must not run them and the module docs say so. This PR is the second outcome, and it changes comments only: - `packages/spec/src/automation/flow-value-slot-template.ts`: a new module-doc section, "The doors that run it — and why `FlowSchema.parse` is not one". It names every door that runs the judge and the doors that do not: `FlowSchema.parse`, and so `defineStack`, `ObjectStackDefinitionSchema`, the `flow` metadata type's schema and an artifact's parse. It gives the two measured reasons, says what the gap leaves, and states the condition under which the judges can join the parse. - `packages/spec/src/automation/flow-text-slot-template.ts`: the door list is corrected. `HttpConfigSchema` composes the judge too, and the doors are `registerFlow` and the `validateStackExpressions` rule, which `objectstack validate`, `objectstack compile` and the publish gate of an `active` metadata save all run. A new section covers the one text slot the parse does judge: a top-level `end` node's `message`, whose contract has no other door. It points to the reasons above. - `packages/spec/src/automation/flow.zod.ts`: one comment sentence at the contract arm, pointing to those docs. ## The premise, at base `6d96721fa` The premise holds. A throwaway probe used `FlowSchema.safeParse` and `defineStack` with a manifest and one object: | probe | `FlowSchema.parse` | `defineStack` | judge | |:--|:--|:--|:--| | `create_record` `fields.title: '{record.name}'` | accepted | accepted | value-slot judge refuses `fields.title` | | `screen` `recordId: '{x}'` | accepted | accepted | value-slot judge refuses `recordId` | | `notify` `title` template `'{name}'` | accepted | accepted | text-slot judge refuses `title` | | `http` `url: '{id}'` | accepted | accepted | text-slot judge refuses `url` | | controls: a CEL envelope, a `{{ name }}` hole | accepted | accepted | none | PM hypothesis H1 holds. At base, `flow-node-config-refusals.ts` calls both judges only to build `slotJudgeRefusedPaths`, the exemption PR #23319 added. Neither judge adds an issue at the parse. The one exception is already there: a top-level `end` node's `message` (`{x}`) is refused at the parse, because `parseEndNodeConfig` applies `EndConfigSchema`, which composes the text-slot judge. ## The measurement: route (a), applied and reverted Commit `ca829754b` puts both judges in `FlowSchema`'s refinement, as the dispatch's suggested route. It sits beside the `flowNodeConfigRefusals` arm and reports each slot token once at its own path, keeping #23319's exemption and skipping `end`, which its contract already judges. Commit `d5b704543` reverts it. Both stay in this branch's history as the record. Spec `dist/` was rebuilt, and the route's code marker was confirmed in `dist/`, before any consumer reading. - **It works mechanically.** All four probes are refused once each at `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `flows.0.nodes.1.config.SLOT`), and both controls are accepted. - **`@objectstack/spec`:** `src/automation src/conversions src/migrations src/stack`, 74 files, 2877 tests passed. No spec pin depends on the parse accepting a slot token. - **`@objectstack/service-automation`:** the 13 test files that pin a slot refusal: 38 of 378 tests failed, across 11 files. 37 of the failures are `registerFlow`'s located refusal, for example `node 'w' (create_record) create_record field value at config.fields.total: …` followed by `source:`, replaced by the parse's raw issue list (`[{"code":"custom","path":["nodes",1,"config","fields","total"],…}]`). That happens because `registerFlow` parses first and lets the parse's error through. The remaining failure is #23315's pin "a braced operand parses, is not rewritten, and is reported by node": `canonicalizeStoredFlow` throws instead of listing the TODO. - **`@objectstack/lint`:** 19 of 6699 tests failed, across 3 files. 18 failures are `objectstack validate`'s `expression-invalid` finding, preempted because the harness, like `objectstack validate` and `objectstack compile`, parses the stack before the rules run. The remaining failure is a stale fixture (see the Acceptance notes). - **`@objectstack/metadata-protocol`:** 238 files, 28353 tests passed. Its stored-migration pins use a stand-in canonicalizer that does not parse. - **`@objectstack/verify`:** 38 files, 306 tests passed. - **`@objectstack/cli` (unit):** not run under the route. A grep of the CLI's tests finds no flow that carries a slot token. The two brace hits are a dashboard filter placeholder and a connector input. **Re-measured after merging `main` at `dc393266f` (#23356).** Route (a) was applied over this branch's head `28da9bd3e` with `scripts/ablation-replace.mjs` and checked on disk against the head plus the route's patch. Spec was rebuilt, and `ablation-dist-preflight` found the route's marker in 20 built files. In `canonicalize-stored-flow.test.ts`, `builtin/text-slot-template.test.ts` and `builtin/http-slots.test.ts`, 13 of 74 tests failed: - Eight are stored-preview pins: #23315's value-slot TODO, and seven of #23356's `registerFlow` TODO pins for text slots, including "the listing and the door agree in both directions". - Five are `registerFlow`'s located text-slot and http-slot refusals. The source was then restored from `HEAD` (blob equal to HEAD, `git diff HEAD` empty) and spec rebuilt. The marker is absent from all 232 built files, and the tree is clean. Two earlier attempts at this leg are recorded here: - The first was refused by the tool before anything was built. Its anchor reappeared inside the replacement, so the count stayed at 1: an empty operation. - The second landed and built, but stopped at the preflight's tree reading, because the source spells `'end'` and `dist/` spells `"end"`. Its trap restored the source, and the third run's restore leg rebuilt `dist/`. ## Why the parse does not run them 1. **The located refusal.** `registerFlow` and `objectstack validate` / `objectstack compile` each report a slot token with its node, its slot's label, its config path and its remedy. A refusal at the parse comes first at all three, and replaces that report with the parse's issue list. PR #23319 named the same reason for its exemption. 2. **The stored pass.** The flow parse is also `AutomationEngine.canonicalizeStoredFlow`, which `os migrate meta --stored` runs on each flow row. When the parse refuses a row, the row is reported `failed`, and that flips `storedMigrationClean`. The result is a non-zero exit that no run of the command can clear, because no conversion rewrites a slot token (ADR-0087 D2, "refused, not converted"). A refusal made after the parse is listed as a TODO instead, and the verdict does not move. A value-slot site is listed under its step-18 D3 entry `flow-value-slot-template-dialect-refused` (#23315). A text-slot site is listed under `registerFlow` (#23356, which landed while this card was in flight). Every author-facing door already refuses the token: `registerFlow` and every run, `objectstack validate` and `objectstack compile` (`expression-invalid`, `error`, gating), and the publish gate of an `active` metadata save (`INVALID_METADATA`, 422, pinned in `runtime-authoring-gate.flow-filter-operand.test.ts`). **What the gap leaves:** a stack built by `defineStack` that goes through none of those doors. `os dev` and `os serve` load it and boot it, `registerFlow` refuses the flow at boot with its located line, and the flow is not registered. The module doc states when this can change: once `registerFlow` reports a parse refusal as precisely as its own, and once the stored pass lists a slot site the parse refuses instead of failing the row. ## ADR-0087 and stored rows Nothing narrows, so no stored-row disposition and no D3 registration is owed. `node scripts/check-adr-0087-registration.mjs --base origin/main` reads: "this PR adds no declared-breaking changeset (0 non-breaking changeset(s) seen)". ## Changeset: none (`skip-changeset`) The diff is comments only. Measured on a spec build at this head: `dist/` carries none of the new text (the phrase "The doors that run it" is absent, while the control phrase from an exported docblock is present in `dist/automation/index.d.ts`), and the route-(a) code marker is absent from `dist/`. `@objectstack/spec`'s `files[]` ships `src/**/*.zod.ts`, so the tarball's `src/automation/flow.zod.ts` changes by one comment sentence. Comment-only changes are on the no-publish fast track. ## Tests and gates All at head `c7a71df7e`. - `@objectstack/spec`: `vitest run --project local src/automation src/conversions src/migrations` passed 54 files, 2284 tests. `pnpm run typecheck` exited 0: `tsc --noEmit`, `check:scripts-typecheck`, and `check:test-typecheck: OK`. - `node scripts/pm/dispatch-gates.mjs --commands` (no paths) derived 71 commands, the same set as the dispatch's list. Every command was run with its exit code captured before any pipe. 70 exited 0. - `pnpm check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET). It needs every package's `dist/`, and 10 packages are unbuilt in this worktree. Two attempts to build them queued 9 minutes each on the shared verify lock without getting it. So this one is **NOT MEASURED**. The diff changes no built output (see Changeset above). - Reconcile: `✓ dispatch-gates --ran: 71 derived famil(ies) accounted for — 70 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`. - The five spec gates that read `dist/` first refused with exit 3 on a stale digest after the last comment commit. They were run again after a rebuild at `c7a71df7e`, and all exited 0. `check:api-surface` reads "public API surface + factory signatures unchanged". - **NOT MEASURED: type-check-debt.** `--re-measure` re-runs every DEBT and EXEMPT package's `tsc`. This diff is comments in `packages/spec`, whose own typecheck exited 0. CI's lint job runs it. - Consumer readings at this head: the diff carries no built output, so no consumer can read a difference. The consumer readings in "The measurement" above were taken under route (a), where they mean something. ## Acceptance notes - **Stale fixture, not filed (drift, not reached).** `packages/lint/src/lint-flow-patterns.test.ts`, "per-iteration containment … parses under FlowSchema — the docs example is authorable", copies the example from `content/docs/automation/flows.mdx`. The copy writes `recipients: ['{currentCase.owner}']`, while the page itself writes `{ dialect: 'cel', source: 'currentCase.owner' }`. The test passes only because the parse leaves value slots to their judge. Owner: none. - **The stored pass's words for a parse-refused flow row, not filed (read, not reproduced).** A flow row the parse refuses is reported `failed`. Examples today: a top-level `end` `message` carrying `{x}`, a typed-key token (#23260), an undeclared builtin config key. `os migrate meta --stored` then prints that such rows "keep reading canonically through the chain, so nothing is broken". For a flow, the boot refuses that row. Owner: the stored-preview lane (`domain:spec` seat 2). - **The triage's `defineStack` pins are not added.** They belong to outcome (a). Under this outcome they would pin the gap itself. The pins that hold the reasons already exist: `registerFlow`'s located refusal in `service-automation`, `objectstack validate`'s in `@objectstack/lint`, and the stored listing in `canonicalize-stored-flow.test.ts`. Each went red under route (a). --- _Generated by [Claude Code](https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6eb6f7d commit 0add8a4

3 files changed

Lines changed: 70 additions & 4 deletions

File tree

‎packages/spec/src/automation/flow-text-slot-template.ts‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,32 @@
2626
* dialect: a single-brace `{…}` token is refused, and the refusal names the
2727
* `{{ }}` spelling of each token or, for a token no hole can spell, where to
2828
* compute it. The contracts compose it (`NotifyConfigSchema`,
29-
* `ScreenConfigSchema`, `EndConfigSchema`), and `AutomationEngine.registerFlow`
30-
* and `objectstack validate` call it on every slot
29+
* `HttpConfigSchema`, `ScreenConfigSchema`, `EndConfigSchema`), and
30+
* `AutomationEngine.registerFlow` and the `validateStackExpressions` rule
31+
* (`objectstack validate`, `objectstack compile`, the runtime publish gate of
32+
* an `active` metadata save) call it on every slot
3133
* {@link flowNodeTextSlotSources} finds — ⛔ never a second reading of the
32-
* dialect anywhere. Those two doors then compile each slot's `{{ }}` holes
34+
* dialect anywhere. Those doors then compile each slot's `{{ }}` holes
3335
* (`validateExpression('template', …)`), which the spec cannot do itself: the
3436
* template engine is a runtime.
3537
*
38+
* ## Not at `FlowSchema.parse`, but for an `end` node (#23320)
39+
*
40+
* The flow parse runs this judge on one slot: a top-level `end` node's
41+
* `message`, because `end` has no executor, so its contract, `EndConfigSchema`,
42+
* has no door but the parse (`parseEndNodeConfig` in `flow.zod.ts`). On every
43+
* other text slot the parse leaves the token to the doors above, as it leaves
44+
* a value-slot token to the value-slot judge's: `flowNodeConfigRefusals`'
45+
* value arm stands aside at the paths this judge refuses
46+
* (`slotJudgeRefusedPaths`, #23260), and the parse calls no slot judge of its
47+
* own. The reasons are measured once, for both judges, in
48+
* `flow-value-slot-template.ts` ("The doors that run it"): a refusal at the
49+
* parse would replace `registerFlow`'s located line and `objectstack
50+
* validate`'s `expression-invalid` finding with the parse's issue list, and
51+
* would fail the row in the stored pass (`os migrate meta --stored`), which
52+
* lists a text-slot refusal made after the parse as a TODO under
53+
* `registerFlow` and leaves its verdict unmoved.
54+
*
3655
* ## Refused, not converted (ADR-0087 D2)
3756
*
3857
* D2 lets a conversion rewrite only what maps LOSSLESSLY. Every token spelling

‎packages/spec/src/automation/flow-value-slot-template.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,50 @@
2828
* `AutomationEngine.registerFlow`, `objectstack validate` and the executors
2929
* call it — ⛔ never a second reading of the dialect anywhere.
3030
*
31+
* ## The doors that run it — and why `FlowSchema.parse` is not one (#23320)
32+
*
33+
* The judge runs at every door that reports a flow's refusals per slot:
34+
* `AutomationEngine.registerFlow`, after its parse; the `validateStackExpressions`
35+
* rule (`expression-invalid` at `error`), which `objectstack validate`,
36+
* `objectstack compile` and the runtime publish gate of an `active` metadata
37+
* save run; and every run, through the node contracts that compose
38+
* `FlowValueSlotSchema` and the `assignment` executor's own call. The flow
39+
* parse does NOT run it: `FlowSchema.parse`, and so `defineStack`,
40+
* `ObjectStackDefinitionSchema`, the `flow` metadata type's schema and an
41+
* artifact's parse, accept a value-slot token. The parse
42+
* parses the node contracts too (`flowNodeConfigRefusals`' value arm), and
43+
* that arm stands aside at exactly the paths this judge refuses
44+
* (`slotJudgeRefusedPaths`, #23260), so the parse leaves the token to the
45+
* doors above, on purpose. Two measured reasons, taken by running this judge
46+
* and the text-slot judge in `FlowSchema`'s refinement and reverting it:
47+
*
48+
* - **The located refusal.** `registerFlow` parses first, and a parse
49+
* refusal reaches its caller as the parse's own issue list. A token
50+
* refused at the parse would replace `registerFlow`'s located line — the
51+
* node's id and type, the slot's label, the config path, the remedy and the
52+
* `source:` — at registration and in the boot's log, and `objectstack
53+
* validate` and `objectstack compile`, which parse the stack before their
54+
* rules run, would stop at the parse before the `expression-invalid`
55+
* finding. Those doors' pins of the located refusal went red.
56+
* - **The stored pass.** The parse is also the stored-flow canonicalization
57+
* (`AutomationEngine.canonicalizeStoredFlow`) that `os migrate meta
58+
* --stored` runs on each flow row, and a parse refusal reports the row
59+
* `failed`, which flips `storedMigrationClean`: a non-zero exit that no run
60+
* of the command can clear, because no conversion rewrites the token (the
61+
* next section). A refusal after the parse is listed instead, as a TODO —
62+
* a value-slot site under the step-18 D3 entry
63+
* `flow-value-slot-template-dialect-refused` (`conversions/refused-shapes.ts`),
64+
* a text-slot site under `registerFlow` — and the verdict does not move.
65+
* The pins that a braced slot parses and is listed went red.
66+
*
67+
* What this leaves: a stack built by `defineStack` and handed to none of those
68+
* doors — `os dev` and `os serve` load it and boot — reaches `registerFlow` at
69+
* boot, which refuses the flow with its located line and does not register it.
70+
* The judge can join the parse once `registerFlow` reports a parse refusal as
71+
* located as its own and the stored pass lists a slot the parse refuses
72+
* rather than failing its row; the contract arm's exemption then keeps each
73+
* token reported once.
74+
*
3175
* ## Refused, not converted (ADR-0087 D2)
3276
*
3377
* D2 lets a conversion rewrite only what maps LOSSLESSLY. Every token spelling

‎packages/spec/src/automation/flow.zod.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1503,7 +1503,10 @@ export const FlowSchema = lazySchema(() => strictObject(
15031503
// value carrying a `{token}` included since #23260 (`get_record`
15041504
// `limit: '{x}'`), never a region slot, a ledger predicate or value slot,
15051505
// a value or text slot whose own judge refuses its token, or `http`'s
1506-
// run-resolved `signingSecret`. A KEY its
1506+
// run-resolved `signingSecret`. (#23320) This parse runs neither slot
1507+
// judge itself, but for an `end` node's `message` through its contract:
1508+
// the doors that do, and the measured reasons the parse is not one, are
1509+
// in `flow-value-slot-template.ts` ("The doors that run it"). A KEY its
15071510
// contract does not declare is refused too (#21982), for every builtin
15081511
// `builtinNodeConfigKeysJudged` names (every one, `try_catch` included
15091512
// since #22343), a body-less legacy `loop` included. The one plugin node contract the spec declares, `approval`

0 commit comments

Comments
 (0)