Repository navigation
Commit 3e72f93
fix(spec/automation)!: refuse a
Fixes #22502
Clause-②: no (narrowing)
## What this does
The `$` names are the flow engine's at the binding keys too, so a flow
can no longer bind a `$` name that a text slot then refuses to read.
- A node's `outputVariable` (`get_record`, `create_record`, `map`,
`script`, `subflow`) refuses a name that starts with `$`, the engine's
own names included (`outputVariable: '$record'` would overwrite the
trigger record).
- A `try_catch` node's `errorVariable` refuses every `$` name except
`$error`, its default.
- The refusal names the remedy: the same name without the `$`, read as
`{{ name }}`. For `errorVariable: '$caught'` that is `caught`, read as
`{{ caught.message }}`, or deleting the key and reading the default `{{
$error.message }}`.
One rule, composed into all six keys: `flowBoundVariableNameSchema` in
the new package-internal leaf
`packages/spec/src/automation/flow-bound-variable-name.ts` (not in
`automation/index.ts`, so there is no new public export;
`check:api-surface` is green).
- It is a `regex` check, not a `.refine()`. `z.toJSONSchema()` emits it
as `pattern`, so the published `json-schema/**` refuses exactly what the
parse refuses, and `dropped-refinements.baseline.json` gains no row.
- Each executor parses its config against the same contract. So
`FlowSchema.parse`, `registerFlow`, `objectstack validate`,
`defineStack` and the run itself all refuse such a name through
`flowNodeConfigRefusals`, anchored at `nodes.N.config.outputVariable` /
`nodes.N.config.errorVariable`, region bodies included.
- It reads the `$` prefix the engine's closed list implies, never a copy
of the list. `FLOW_ENGINE_VARIABLES` stays package-internal in
`flow-text-slot-template.ts`, untouched. A binding must not claim any
name in the engine's namespace, whichever name that is.
- ⛔ The text-slot judge is not widened (triage `6084430227` rules that
option out).
## The PM's mechanism assumptions, measured at `b53b949a15`
1. **`try_catch.errorVariable`** was `z.string().default('$error')` at
`control-flow.zod.ts:329` and accepted any string. Confirmed.
2. **`outputVariable` is declared at five sites, each its own
`z.string().optional()`. They share no schema:**
- `builtin-node-config.zod.ts:445` (`get_record`), `:471`
(`create_record`), `:991` (`map`);
- `schemaless-node-config.zod.ts:305` (`script`), `:384` (`subflow`).
- `flow.zod.ts` and `flow-function.zod.ts` only mention it in comments.
Measured with `git grep -nE '^\s+[a-zA-Z]*(Variable|Var)s?\s*:\s*z\.' --
packages/spec/src`.
3. **The closed list** is `FLOW_ENGINE_VARIABLES` in
`packages/spec/src/automation/flow-text-slot-template.ts`. It is
unexported on purpose, and the module is `export *`-ed from the barrel.
So this card reads the prefix rule it implies rather than publishing the
list.
4. **In-repo authors** of a `$`-named `errorVariable` / `outputVariable`
other than `$error`, measured with `git grep -nP
'Variable\W{0,8}\$[a-zA-Z_]' -- .` over the whole tree (13 hits, 10 of
them `$error`). All three non-`$error` hits are test fixtures, and all
three are renamed here:
- `packages/spec/src/automation/region-normalization.test.ts:127`:
`'$err'` to `'err'`;
-
`packages/spec/src/automation/flow-builtin-node-config-keys.test.ts:344`:
`'$err'` to `'err'`;
-
`packages/services/service-automation/src/throw-arm-error-refresh.test.ts:77`:
`'$caught'` to `'caught'`.
- Every authored `errorVariable` in `examples/app-showcase` (2),
`content/docs` (2) and the `service-automation` README (1) is `$error`,
which stays legal.
- No example or doc binds a `$`-named `outputVariable`.
- The pinned objectui (`47b1f0bb71`) authors `errorVariable: '$error'`
only (`apps/console/src/preview-samples.ts:244`).
5. **Engine readers.** No engine reader breaks.
- `try-catch-node.ts:105` (`cfg.errorVariable || '$error'`) still gets
`$error`.
- The executors write the author's `outputVariable` verbatim with
`variables.set` (`crud-nodes.ts`, `map-node.ts`, `screen-nodes.ts`,
`subflow-node.ts`).
- No engine code authors a `$`-named binding.
- The only `packages/services/**` edit is the one fixture above. It is
not an engine change: under the new contract `registerFlow` refused that
flow (reverse check below).
## ADR-0087 disposition
- **D3 entry:**
`packages/spec/src/migrations/entries/semantic/18.flow-binding-variable-dollar-name-refused.ts`
(protocol 18), plus its step-18 rationale fragment (`order: 92`).
- **Projections regenerated:** `gen:migration-registry`,
`gen:spec-changes`, `gen:upgrade-guide`.
- **No D2 conversion:** the bare name may already be bound in the flow,
and the reads of the old name sit in every dialect a flow string speaks.
- **Guidance:** the printed guidance carries no tracker number.
`test/migrate-meta-engine-guidance.test.ts` is green (3/3).
- **Changeset:**
`.changeset/22502-flow-binding-variable-dollar-name-refused.md`,
`@objectstack/spec` `major` in pre mode, with `Clause-②: no (narrowing)`
and the `registered flow-binding-variable-dollar-name-refused` marker.
`check-adr-0087-registration` reads `[major+BREAKING+clause-②-narrowing]
registered flow-binding-variable-dollar-name-refused (new here)`.
## Tests (at `ff2832a7bb`, after the one `origin/main` merge through
`os-regen-merge.sh` and its regeneration commit)
- **New:**
`packages/spec/src/automation/flow-bound-variable-name.test.ts`, 26
cases.
- `$x` is refused with the remedy on each of the five `outputVariable`
contracts, and every `$` name with it (`$record`, `$error`, `$`, `$$x`).
- Controls: `x`, `a$b` and an absent key are accepted. A non-string
keeps its type refusal.
- `errorVariable: '$caught'` is refused with the remedy, and so are
`$record`, `$errors`, `$error.x` and `$`. Controls: the default
`$error`, an explicit `$error` and `caught` are accepted.
- `FlowSchema` refuses at `nodes.1.config.errorVariable`, and inside a
region at `nodes.1.config.try.nodes.0.config.outputVariable`.
- `defineStack` refuses with `{ code: 'STACK_SCHEMA_INVALID', status:
422 }`, and the save door at the key.
- `textSlotTemplateRefusal('Failed: {{ caught.message }}')` passes, and
a flow binding `caught` with that text in its catch region parses clean.
- The published JSON Schema carries a `pattern` on all six keys that
accepts and refuses the same names.
- The D3 entry and its rationale fragment are registered.
- `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2
src/automation src/migrations`: **39 files, 1416 tests passed**.
- `pnpm --filter @objectstack/service-automation exec vitest run
--maxWorkers=2` (whole suite): **180 files, 2259 tests passed**.
- `pnpm --filter @objectstack/cli exec vitest run --project integration
test/migrate-meta-engine-guidance.test.ts` (after its closure build):
**3 passed**. The file lives in the `integration` project. A first
`--project unit` call selected nothing; that call is not counted as a
measurement.
- `pnpm --filter @objectstack/spec typecheck` and `pnpm --filter
@objectstack/service-automation typecheck` both exit 0,
`check:test-typecheck: OK`.
- `pnpm --filter @objectstack/spec check:generated`: **all 15 generated
artifacts up to date**.
**Ablation** (committed fix, mutation and restore through
`scripts/ablation-replace.mjs`, wrap mode). The spec tests import `src`
directly, so no dist leg applies.
- The rule's regex was replaced with one that accepts everything. The
anchor fell from 1 to 0, and the blob moved from `5211426e99fb` to
`d0cb32780409`.
- Result: **16 failed, 10 passed** of 26. Red: every refusal, every door
and the `pattern` pin. Green: the controls, the remedy-reads pin, the
non-string case and the ledger pins.
- Restore was proven: blob equals HEAD `5211426e99fb`, and `git diff
HEAD` is empty.
**Reverse check of the services fixture:**
- `'caught'` was put back to `'$caught'`, then
`throw-arm-error-refresh.test.ts` was run against the rebuilt spec.
- Result: **1 failed**, a `ZodError` at `nodes.3.config.errorVariable`
thrown from `AutomationEngine.canonicalizeStoredFlow` inside
`registerFlow`. So the fixture rename was owed.
- Restore was proven: blob equals HEAD `b4f38da11225`.
## Gates
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths, merge base `6a3f82efa`) derived
116 commands, reconciled with `--ran`.
- `pnpm check:dual-build-cjs-loads` is **NOT MEASURED** (reason: it
needs a whole-workspace build, which this dispatch rules out). It is
declared to CI.
- Full readings, including the artifact-roster block, are in the report
on #22502.
## Acceptance notes
- **The same class, on sibling binding keys, is not in this PR.**
`FlowSchema.parse` at this head still accepts a `$` name on:
- `loop` / `map` `iteratorVariable` and `indexVariable`;
- a `screen` `idVariable`;
- a declared flow variable's `name`;
- an `assignment` target key.
`NotifyConfigSchema` refuses a read of one (`{{ $row.name }}`). The
triage ruling scoped this card to `errorVariable` and `outputVariable`,
so the siblings are reported for the family close-out rather than
widened here.
- `service-automation`'s executor descriptors (`configSchema` on
`crud-nodes.ts`, `map-node.ts`, `try-catch-node.ts`) still describe
these keys as plain strings. The spec contract is the judge at every
door, and the descriptor walk stands aside for builtins. Noted, not
changed.
- `engine.ts`'s `buildSubflowResumeSignal` comment calls the
reserved-name check a false positive "on an oddly-named"
`outputVariable`. After this narrowing such a name cannot be authored.
Whether `engineBuilt` is still needed there for another reason was not
measured. Noted, not changed.
- The hand-written `content/docs/automation/flows.mdx` gains one
paragraph beside the `try_catch` example saying where the `$` names
belong.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>$-named outputVariable, and a $-named errorVariable other than $error, at authoring (#22569)1 parent 86f53a4 commit 3e72f93
17 files changed
Lines changed: 535 additions & 29 deletions
File tree
- .changeset
- content/docs
- automation
- references/automation
- docs
- packages
- services/service-automation/src
- spec
- src
- automation
- migrations
- entries/semantic
Lines changed: 40 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
858 | 858 | | |
859 | 859 | | |
860 | 860 | | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
861 | 867 | | |
862 | 868 | | |
863 | 869 | | |
| |||
Lines changed: 3 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
151 | | - | |
| 151 | + | |
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
| |||
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
198 | | - | |
| 198 | + | |
199 | 199 | | |
200 | 200 | | |
201 | 201 | | |
| |||
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
215 | | - | |
| 215 | + | |
216 | 216 | | |
217 | 217 | | |
218 | 218 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
244 | 244 | | |
245 | 245 | | |
246 | 246 | | |
247 | | - | |
| 247 | + | |
248 | 248 | | |
249 | 249 | | |
250 | 250 | | |
| |||
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
167 | | - | |
| 167 | + | |
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
| |||
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
185 | | - | |
| 185 | + | |
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
| |||
Large diffs are not rendered by default.
Lines changed: 4 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
| 44 | + | |
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
| |||
74 | 75 | | |
75 | 76 | | |
76 | 77 | | |
77 | | - | |
| 78 | + | |
78 | 79 | | |
79 | 80 | | |
80 | 81 | | |
| |||
0 commit comments