Repository navigation
Commit 0add8a4
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
30 | | - | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
31 | 33 | | |
32 | | - | |
| 34 | + | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
36 | 55 | | |
37 | 56 | | |
38 | 57 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
31 | 75 | | |
32 | 76 | | |
33 | 77 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1503 | 1503 | | |
1504 | 1504 | | |
1505 | 1505 | | |
1506 | | - | |
| 1506 | + | |
| 1507 | + | |
| 1508 | + | |
| 1509 | + | |
1507 | 1510 | | |
1508 | 1511 | | |
1509 | 1512 | | |
| |||
0 commit comments