Repository navigation
Commit 3073b72
fix(spec/automation): refuse a text-slot
Fixes #22477
Clause-②: no (narrowing)
## What this does
A `{{ }}` hole in a flow text slot (a `notify` `title` / `message`, a
`screen` `title` / `description`, a refusing `end` `message`) whose root
is a `$` name the flow engine does not bind is now refused by the one
text-slot judge, `textSlotTemplateRefusal` in
`packages/spec/src/automation/flow-text-slot-template.ts`. Every door
that already calls that judge refuses it with no edit of its own:
`NotifyConfigSchema`, `ScreenConfigSchema`, `EndConfigSchema`,
`AutomationEngine.registerFlow`, and `objectstack validate`
(`expression-invalid`, `error`).
- `'By {{ $User.Id }}'` is refused with the very remedy `'By
{$User.Id}'` already gets ("compute it into a variable with an
`assignment` node, whose value slot still reads it (`assignments: { v:
'{$User.Id}' }`), and write `{{ v }}` here"). The test asserts the hole
refusal ends with the single-brace remedy byte for byte, so the two
spellings answer alike.
- Any other `$` root (`{{ $User }}`, `{{ $Error.message }}`, `{{ $org.id
}}`) gets a remedy that names the root and the variables the engine does
bind.
- A single-brace path token over such a root (`'Failed:
{$caught.message}'`) used to be told to write `{{ $caught.message }}`,
which would now be refused in turn. It gets the same remedy instead of a
rewrite.
- Controls hold: `{{ $error.message }}`, `{{ record.name }}`, a node
output `{{ lookup.result }}`, and every hole over an engine-bound `$`
variable are unchanged.
- ⛔ The template engine binds no new variable (triage ruling).
## The `$` roots are one enumerated list (H1, measured)
The runtime has no single declaration of its `$` variables. They are
bound by literal name in three places:
| root | where `service-automation` binds it (at `dee7692f0b`) |
|:--|:--|
| `$record`, `$runId`, `$flowName`, `$flowLabel` | `engine.ts`
`seedRunVariables` (every run attempt; `$record` when there is a trigger
record) |
| `$error` | `engine.ts`, the throw arm and the returned-failure arm of
node execution (also the `try_catch` `errorVariable` default) |
| `$loopItems`, `$loopIndex` | `builtin/loop-node.ts`, the legacy
flat-graph `loop` with no `body` |
The spec has no home for this either. `contracts/automation-service.ts`
states only that `$` names are reserved, in its `INVALID_SIGNAL` prose.
So this follows H1's fallback. The list `FLOW_ENGINE_VARIABLES` sits
beside the judge in `packages/spec`. A parity pin in
`service-automation`'s `text-slot-template.test.ts` scans that package's
non-test sources for every `.set('$name'` literal and asserts the public
judge admits a hole over each one. Today the scan finds exactly those
seven, listed with their files. The scan also carries a floor that fails
if one of the seven stops being bound, so a removal is caught too.
The list is **module-private**, not exported. A new public export would
enlarge the public surface, and that is the question `Clause-②` answers;
it is ruled `no`. The parity pin reaches the list through
`textSlotTemplateRefusal`, which is already public. `check:api-surface`
and `check:export-origins` are unchanged and green.
**H2 (node outputs):** a node output is addressed by its node id (`{{
lookup.result }}`). The engine writes it as `NODEID.KEY`. `git grep`
finds no node id starting with `$`. The map node's `.$mapState` /
`.$mapItemDone` are `.$` segments under a node-id root, not `$` roots.
So no node output joins the list.
**H3 (the validate door):** `objectstack validate`'s text-slot check is
`validate-expressions.ts`, and it calls
`textSlotTemplateRefusal(slot.source)` before compiling the slot. The
refusal therefore reaches `objectstack validate` through the same judge,
and **no lint source edit** is needed. `validate-flow-template-paths.ts`
does skip `$` roots, but that is the record-field-path warning rule, not
the text-slot door. The validate-door pin is in
`validate-expressions.text-slot.test.ts`.
**H4:** no `skills/**` edit.
## Boundary, stated
The triage ruling says nothing outside the list is admitted. So a
variable an author binds under a `$` name, such as `try_catch`
`errorVariable: '$caught'` read as `{{ $caught.message }}`, is refused
in a text slot, and the remedy says to name it without the `$`.
Measured: `$`-named `errorVariable`s other than `$error` occur only in
tests (`$caught` in `throw-arm-error-refresh.test.ts`, `$err` in two
spec tests). None of them is read in a text slot.
## Changeset: `@objectstack/spec` minor, `@objectstack/lint` patch,
`Clause-②: no (narrowing)`, **BREAKING**
Patch round 1 (REWORK `6083820886` on #22477, after contract review FAIL
`6083797903`) corrected the grade:
- **A published accept set narrows.** At tag `@objectstack/spec@17.7.0`
(npm `latest`), `NotifyConfigSchema.title` / `.message`,
`ScreenConfigSchema.title` / `.description` and
`EndConfigSchema.message` are plain `z.string()`, and no text-slot judge
exists. So `'By {{ $User.Id }}'` is accepted by the published contract
and refused after this PR. The unreleased part is the hole semantics of
#22110, not the acceptance of the string.
- **The disposition:** a new step-18 D3 semantic entry,
`flow-text-slot-unbound-dollar-root-refused` (`504b61ad21`), with the
changeset's marker `adr-0087: registered
flow-text-slot-unbound-dollar-root-refused`. After PR #22215 (protocol
18) landed, the re-sync regenerated `spec-changes.json` and the upgrade
guide in their own commit (`52c005e2b1`): step 18 has 330 semantic
entries, up from 329, the one added being this entry.
- `@objectstack/lint` patch: the `flow-bare-dollar-reference` hint now
asks the judge, so for a `$` root the engine does not bind it prescribes
the judge's remedy, not a refused hole (`d9a5ebbb6f`).
## Re-sync after PR #22215 (head `52c005e2b1`)
- `origin/main` `e148ca9842` merged in patch round 1 (`b3482cb096`), and
`origin/main` `4e9fe9ff6a` (PR #22215) merged in the re-sync
(`6e65fb1747`). Both went through `os-regen-merge.sh`, and neither merge
commit adds content of its own.
- At `52c005e2b1`: spec `local` 630 files / 18,810, spec `repo` 54 /
915; lint 131 / 5,983; service-automation and the three packages'
typechecks exit 0. 94 derived gate families run, 0 not measured.
- The lint-hint ablation: restoring the old static hint turns exactly
the `$User.Id` pin and the mixed-slot pin red, and the `$error.message`
control stays green.
## Tests, round 0 (head `f0b39b02de`)
- `packages/spec` `flow-text-slot-template.test.ts`: 26 passed. It holds
the card's pins at the judge and at the three node contracts
(`NotifyConfigSchema` with a bare string and with a template envelope,
`ScreenConfigSchema`, `EndConfigSchema`; each refused at the key with
`code: custom`), plus the controls.
- `packages/services/service-automation` `text-slot-template.test.ts`:
18 passed. It holds the `registerFlow` pin, the parity scan, and a run
that renders `{{ $flowName }} / {{ $flowLabel }} / {{ $record.name }}`
as `roots / roots / Acme Corp`.
- `packages/lint` `validate-expressions.text-slot.test.ts`: 6 passed. It
holds `os validate`'s own sequence (normalize, parse,
`runAuthoringRules('validate')`): `By {{ $User.Id }}` in a notify
`message` and a screen `title` is one `error` finding carrying the
remedy. The control `{{ $error.message }}` / `{{ record.name }}` gives
`[]`.
- Package suites and typechecks: every run below is on head `f0b39b02de`
and went through `os-verify-lock.sh`:
- `@objectstack/spec` `vitest run --project local`: 630 files, 18805
passed, 1 todo;
- `@objectstack/spec` `vitest run --project repo` (the corpus walk over
other packages' sources): 54 files, 915 passed;
- `@objectstack/service-automation` `test`: 179 files, 2197 passed;
- `@objectstack/lint` `vitest run`: 130 files, 5944 passed;
- `typecheck` (`tsc --noEmit` plus `check:test-typecheck`) is green on
spec, service-automation and lint.
## Ablation (one-shot, nothing left in the tree)
Every leg ran through `scripts/ablation-replace.mjs` (anchor must hit,
blob hash proven, restore trap armed). The fix was committed first.
Service-automation and lint resolve `@objectstack/spec` through `dist/`,
so spec was rebuilt inside each leg, and
`scripts/ablation-dist-preflight.mjs` proved the mutation reached
`dist/`.
- **Leg A**, the hole refusal removed. `[singleBraceRefusal(text),
unboundRootHoleRefusal(text)]` became `[singleBraceRefusal(text)]`:
anchor 1 → 0, blob `b022c9d7bcda` → `1d140ae58225`, marker absent from
all 98 built files.
- spec: **7 failed** / 19 passed (every hole pin and every schema pin);
- service-automation: **2 failed** / 16 passed (the parity control and
`registerFlow`);
- lint: **1 failed** / 5 passed (the validate door).
- **Leg B**, `'$loopIndex'` deleted from the list: anchor 1 → 0, blob →
`8679f7dab66e`, `"$loopIndex"` absent from dist.
- spec: **1 failed** (engine-bound roots admitted);
- service-automation: **1 failed**, with the message `$loopIndex, bound
in builtin/loop-node.ts`. This is the parity pin firing;
- lint: 6 passed (not its subject).
- **Restore**: each leg's blob equals HEAD `b022c9d7bcda` and `git diff
HEAD` is empty. After a full spec rebuild, the preflight in default mode
finds both markers back in `dist/`, `git status --porcelain` is empty
before and after the build, and the three files show 26 / 18 / 6 passed.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands` derived **88** families
from this diff: the dispatch list plus 6 the test edits added
(`check:engine-double-contract`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt`, `check:where-matcher`). The derivation was
re-run on a throwaway tree at `origin/main` `35ef501e13` with this diff
applied, and it printed the identical 88.
`--ran` reconciliation: 88 accounted for, **87 run with exit 0**, 0
unrun, 1 NOT MEASURED. Among the 87: `check:api-surface`,
`check:export-origins`, `check:authorable-surface`, `check:docs`,
`check-adr-0087-registration` ("1 non-breaking changeset"),
`check-changeset-no-major`, `check-empty-changeset`,
`check:published-files`, `check:nul-bytes`, `check:doc-authoring` and
`check:cross-package-test-inputs`.
- NOT MEASURED: `pnpm check:dual-build-cjs-loads`. Reason: PREREQUISITE
NOT MET (exit 3). The gate reads every package's built `dist/`, and 36
packages are unbuilt in this worktree. This diff changes no build
config, no `exports` and no entry point. Left to CI.
## Acceptance notes
- ~~The `flow-bare-dollar-reference` hint prescribed a refused hole for
a bare `$User.Id`.~~ Fixed in patch round 1 (`d9a5ebbb6f`).
- The published skill `skills/objectstack-automation` on `main` says
"`{{ $User.Id }}` renders blank: assign them to a variable first". Once
this lands, the parenthetical is outdated: the hole is refused at `os
validate` / `registerFlow` / the node contract. The instruction itself
is still right. `skills/**` is governed and outside this card. Carrier:
the skills seat.
- `content/docs/automation/flows.mdx`'s "you wrote / write instead"
table could gain a `{{ $User.Id }}` row. Nothing on the page is made
false by this change: it already says holes read "the engine-set
`$`-named ones". Carrier: none.
- `try_catch`'s `errorVariable` / `outputVariable` still accept a
`$`-named variable that a text slot now refuses to read: filed as
#22502.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>{{ $… }} hole whose root the flow engine does not bind (#22499)1 parent db9cf80 commit 3073b72
11 files changed
Lines changed: 632 additions & 23 deletions
File tree
- .changeset
- docs
- packages
- lint/src
- services/service-automation/src/builtin
- spec
- src
- automation
- migrations
- entries/semantic
| 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 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
2533 | 2533 | | |
2534 | 2534 | | |
2535 | 2535 | | |
| 2536 | + | |
| 2537 | + | |
| 2538 | + | |
| 2539 | + | |
| 2540 | + | |
| 2541 | + | |
| 2542 | + | |
| 2543 | + | |
| 2544 | + | |
| 2545 | + | |
| 2546 | + | |
| 2547 | + | |
| 2548 | + | |
| 2549 | + | |
| 2550 | + | |
| 2551 | + | |
| 2552 | + | |
| 2553 | + | |
| 2554 | + | |
| 2555 | + | |
| 2556 | + | |
| 2557 | + | |
| 2558 | + | |
| 2559 | + | |
| 2560 | + | |
| 2561 | + | |
| 2562 | + | |
| 2563 | + | |
2536 | 2564 | | |
2537 | 2565 | | |
2538 | 2566 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
| 162 | + | |
162 | 163 | | |
163 | 164 | | |
164 | 165 | | |
| |||
669 | 670 | | |
670 | 671 | | |
671 | 672 | | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
672 | 700 | | |
673 | 701 | | |
674 | 702 | | |
| |||
1759 | 1787 | | |
1760 | 1788 | | |
1761 | 1789 | | |
1762 | | - | |
| 1790 | + | |
| 1791 | + | |
| 1792 | + | |
1763 | 1793 | | |
1764 | 1794 | | |
1765 | 1795 | | |
1766 | 1796 | | |
1767 | 1797 | | |
1768 | 1798 | | |
1769 | | - | |
1770 | | - | |
1771 | | - | |
| 1799 | + | |
1772 | 1800 | | |
1773 | 1801 | | |
1774 | 1802 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
19 | 23 | | |
20 | 24 | | |
21 | 25 | | |
| |||
117 | 121 | | |
118 | 122 | | |
119 | 123 | | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
120 | 140 | | |
121 | 141 | | |
122 | 142 | | |
| |||
Lines changed: 74 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
16 | 18 | | |
17 | 19 | | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
18 | 23 | | |
19 | 24 | | |
20 | | - | |
| 25 | + | |
21 | 26 | | |
22 | 27 | | |
23 | 28 | | |
| |||
262 | 267 | | |
263 | 268 | | |
264 | 269 | | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
0 commit comments