Repository navigation
Commit 6a06e3b
Part of #19939
Clause-②: no (narrowing)
Pass 2 of #19939: the run-user token `{$User.*}` leaves the flow value
slots, and the flow CEL scope binds `current_user`. This executes the
maintainer's ruling `6063191653` (Q1 A, Q2 A; maintainer 「同意」
2026-10-08). The date macros `{NOW()}` / `{TODAY() ± N}` stay with pass
3 (#19939 remains open), so this PR uses `Part of`.
> Q1 — A. The flow CEL scope binds `current_user` to the run's
`EvalUser` (id from `userId`, positions, organization from `tenantId`)
when the run has a user, and to `null` when it has none, never a
pseudo-user (ADR-0118 D1, D4). `{$User.*}` is refused in value slots at
registration, the remedy naming `current_user.id`; for a flow that can
run without a user the remedy names the guard `current_user != null ?
current_user.id : null` and states that the guarded form writes `null`
where the template wrote nothing, which on `update_record` clears a
stored value the template left alone. The step-18 D3 entry
`flow-value-slot-template-dialect-refused` is amended; no D2 conversion.
>
> Q2 — A. `current_user` carries only what the run holds: id, positions,
organization, platform-admin flag. `{$User.Email}` and every
`{$User.PATH}` other than `Id` retire with a remedy that says they never
resolved in any shipped run and names the read of the user record
through `current_user.id` for email or name.
## What lands
**The binding (`@objectstack/service-automation`).**
`AutomationEngine.celScope` binds `current_user` from the run context,
through `createEvalUser`: `id` from `userId`, `positions`,
`organizationId` from `tenantId`, and the derived `isPlatformAdmin`. A
run with no user gets `null`. Every CEL site of a flow now receives the
run context: the start-node condition (`runContext`), edge conditions, a
screen field's `visibleWhen` on resume (`run.context`), and the
`decision`, `assignment`, `create_record` and `update_record` executors.
`evaluateCondition` and `evaluateValueEnvelope` take the context as an
optional last argument, and without one `current_user` is `null`. `vars`
and `current_user` are bound after the variables are spread, so each
wins over a flow variable of the same name.
**The refusal (`@objectstack/spec`).** `valueSlotTemplateRefusals` no
longer keeps the `user` token kind; only the date macros are kept.
- `{$User.Id}`: the refusal names `{ dialect: 'cel', source:
'current_user.id' }` and the guard `current_user != null ?
current_user.id : null`, with its `update_record` consequence.
- Every other `{$User.PATH}`: the refusal says it never resolved in any
shipped run and names the read of the user record (`current_user.id`
into a variable, a `get_record` on `sys_user`, then `me.email` /
`me.name`).
- Text with holes: the run user's id becomes `current_user.id` in the
concatenation, with the hole's guard `(current_user != null ?
current_user.id : '')`. Any other `$User` path is left out of the
concatenation, as the template rendered nothing for it, followed by the
Q2 sentence.
- The text-slot judge's run-user remedy now computes the id with the CEL
envelope. It previously named `assignments: { v: '{$User.Id}' }`, which
this change refuses.
**H4, the expression remedy (carry `6087210395`).** `celExpression` now
tokenises the expression and writes every variable path by `celPath`'s
rule, not only the divisors: `{int * 2}` → `vars["int"] * 2`, `{items.0
* 2}` → `items[0] * 2`. Calls, keywords, quoted strings and members of a
call are left as written, and the divisor rule is byte-identical.
**H5, the `vars` head (carry `6088133752`).**
`FLOW_SCOPE_CLAIMED_IDENTIFIERS = { vars, current_user }`, measured from
`celScope`. A path whose head is either name is printed through `vars`:
`vars["vars"][0]`, `vars["current_user"].name`, with
`has(vars.vars.tags)` guards.
**The ledger.** The step-18 D3 entry
`flow-value-slot-template-dialect-refused` is amended (surface,
replacement, reason and acceptance criteria name `{$User.*}` and both
remedies). The two text-slot entries,
`flow-text-slot-single-brace-refused` and
`flow-text-slot-unbound-dollar-root-refused`, are amended where their
replacement computed the run user through the refused spelling.
`registry.ts` was regenerated (`gen:migration-registry`), and the
hand-kept step-18 rationale fragment for the value-slot entry was
updated. No D2 conversion and no new entry.
**Sites, docs, descriptions.**
- `examples/app-todo/src/flows/task.flow.ts`: `owner` uses the bare
`current_user.id`, because a screen flow always has a user.
- `packages/cli/src/commands/explain.ts` (`os explain flow`): the
example is an `update_record` under `record-after-create`, which a
system write fires with no user. It now writes `current_user.id` and
gates on `condition: 'current_user != null'`, so a user-less run leaves
the stored value alone, as the template did. Its test is updated.
- `examples/app-showcase/src/automation/flows/index.ts:1936`
(`recipients: ['{record.assignee}', '{$User.Id}']`) is classified, not
migrated. `recipients` is neither a text slot (#22110's
`FLOW_NODE_TEXT_SLOTS`) nor a ledger value slot (the `value` role is
only `assignment.assignments.*` and `create_record` / `update_record`
`fields.*`). The judge does not refuse it, and the interpolator still
resolves it.
- `content/docs/automation/flows.mdx`: the FROM → TO table gains the two
run-user rows, the text-slot table gains the CEL remedy, the dialect
table names `current_user`, and a paragraph defines it (user-less
`null`, no email or name, the shadowing of a variable named
`current_user` / `vars`).
- `FlowValueSlotSchema` / `AssignmentValueSchema` `.describe()`: "the
date macros are kept for now". The references were regenerated
(`gen:docs`, `builtin-node-config.mdx`).
**Patch round 1 (seat answers `6092066429`): two remedies this card made
false.**
- `flow-bare-dollar-reference`
(`packages/lint/src/lint-flow-patterns.ts`). The hint now depends on the
slot the string sits in. In a value slot it names the CEL envelope the
spec's value-slot judge writes for the referenced token:
`current_user.id` and its guard for `$User.Id`, the `sys_user` read for
any other `$User` path, `vars["$error"].message` for an engine-bound
root, and the variable without the `$` for any other root. Whether a
position is a value slot is asked of the judge with a probe token at the
string's path, so there is no second list. In a text slot, and at every
other position, the hint is unchanged. Each prescribed envelope, put
back in its slot, passes the value-slot judge,
`validateStackExpressions`, `FlowSchema` and the #1315 lint (0 findings
each).
- `packages/services/service-automation/README.md` "Expressions"
(shipped in `files`) is restated as this pass leaves the dialect. A
value slot takes the CEL envelope and reads a plain string as its
literal text, `current_user` is the run user (null with no user), and
`{$User.*}` is refused with its remedy. The date macros stay read until
pass 3, and the other value-like positions keep the single brace.
- The changeset gains `'@objectstack/lint': patch`.
## Measured premises (H1–H5, on `86bf9ed7d5`)
- **H1.** `current_user` was not bound: `celScope` returned `{ ...vars,
vars }` only, and neither #22110 nor #22319 bound it. Bound here per Q1
and pinned with a user and without one
(`flow-cel-current-user.test.ts`).
- **H2.** Two value-slot sites, migrated as above. The showcase
`recipients` site is classified "neither".
- **H3.** Built on the judge as #22315 left it.
- **Riders.** The six rider positions (`subflow.input`, `map.input`,
`script.inputs`, `screen.defaults`, a screen field's `defaultValue`,
`http`) are value-like positions that keep the single-brace dialect by
#22110's own classification. None is a ledger `value` slot, and none
evaluates a CEL envelope, so a refusal there would have no remedy to
name. They are not brought under the judge here (see Acceptance notes).
- **Kept-spelling leak.** `{{ }}` does not apply to value slots. A
run-user token beside a date macro (`'Due {TODAY()} by {$User.Id}'`) is
still kept whole (pinned) until pass 3 retires the macros.
- **#22319** had already landed on the base. `main` (`4638625e07`) was
merged cleanly.
- **H4.** Both measured cases are pinned with the evaluated remedy
through `evaluateValueEnvelope`: `{int * 2}` evaluates to 10, the value
the interpolator computed. `{items.0 * 2}` evaluates to 6; the
interpolator computed nothing there.
- **H5.** `vars` is a claimed head, with the reason and the pin.
`current_user` joins it for the same reason, since this change binds it
after the spread.
## Surface crossings (the claim's file list, and why each crossing was
needed)
The claim names the judge, the token module, `builtin/template.ts`,
`engine.ts`, `validate-expressions.ts` (where the value-slot refusal
surfaces), the three sites, the value-slot D3 entry and `registry.ts`,
`flows.mdx` and one changeset, each with its tests. These files are
outside that list. Each one was either needed for the change to work or
would have stated something false after it:
- **`service-automation/src/builtin/crud-nodes.ts`, `logic-nodes.ts`.**
One argument each: the run context passed to `evaluateValueEnvelope` /
`evaluateCondition`. Without it, `fields` / `assignments` envelopes and
`decision` conditions would see `current_user` as `null` in every run.
Their docblocks also said `{$User.*}` was kept.
- **`spec/src/automation/flow-text-slot-template.ts`** (and its test).
Its run-user remedy named `assignments: { v: '{$User.Id}' }`, a spelling
this change refuses.
- **`spec/src/automation/builtin-node-config.zod.ts`** (and its test),
with the regenerated
`content/docs/references/automation/builtin-node-config.mdx`. The two
value-slot `.describe()` strings said the `$User` paths are kept.
- **`18.flow-text-slot-single-brace-refused.ts`,
`18.flow-text-slot-unbound-dollar-root-refused.ts`**, and the hand-kept
step-18 rationale fragment in `registry.ts`. Their replacement text
computed the run user through the refused spelling.
- **Tests that pinned the kept spelling or the old remedy.** In lint:
`lint-flow-patterns.test.ts` and
`validate-expressions.text-slot.test.ts`. In service-automation:
`text-slot-template.test.ts`, `logic-nodes.test.ts` and
`crud-fields-value-envelope.test.ts`. One new test file:
`flow-cel-current-user.test.ts`.
`packages/lint` changes `lint-flow-patterns.ts` in patch round 1
(above), the files the seat's answer `6092066429` added, with
`service-automation/README.md`. No governed surface is touched
(`skills/**`, `.claude/**`, `docs/adr/**`, `AGENTS.md`).
## Tests and gates (head `caeaab982f`)
Head is `caeaab982f`, a merge of `main` at `4638625e07`. The build is
the dependency closures of service-automation, lint and cli (58 tasks),
then the whole workspace minus docs (72 tasks).
- `@objectstack/spec` local project: 635 files, 18974 passed and 1 todo.
Repo project: 54 files, 915 passed. Both ran at `caeaab982f`.
- `@objectstack/service-automation` full suite: 181 files, 2284 passed
at `caeaab982f`. Typecheck exit 0, with `check:test-typecheck` OK.
- `@objectstack/lint` full suite: 133 files, 6092 passed.
`@objectstack/cli` unit `test/commands.test.ts` and
`test/explain-rule-id.test.ts`: 62 passed. Typecheck exit 0 for spec,
lint and cli. All of these ran at `a86164b899`; the only later commit
changes three service-automation test files.
- Gates: `dispatch-gates --commands` re-derived on the actual diff gives
122 commands, a superset of the 93 at dispatch (the added families
include `check:engine-double-contract`, `check:type-check-coverage`, the
docs families and `check:generated`). All 122 ran at `caeaab982f`, and
`dispatch-gates --ran` reports 122 derived, 122 run, 0 NOT-MEASURED, 0
UNRUN.
- Five exited 3 (PREREQUISITE NOT MET) on the first pass:
`check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n`,
`check:i18n-coverage` and `check:i18n-walk-parity`. All five read built
output that did not exist yet. Each exited 0 after the workspace build.
- `check:empty-changeset`: green. No pending changeset of another change
is edited (see Acceptance notes).
- Repo lint, narrowed and proven:
- ① The population is `eslint . --no-inline-config` over
`eslint.config.mjs`.
- ② `--format json` over the 26 changed `.ts` files reports 26 linted, 0
ignored, 0 errors and 0 warnings.
- ③ That config enables no type-aware linting (no
`parserOptions.project`, no typed rules; `eslint.config.mjs` says so at
its type-aware note), so this diff cannot move any verdict on a file it
does not touch. The full `pnpm lint` is CI's.
**Patch round 1 at `c5a343c32a`** (no merge of `main`; the PR reads
mergeable): `@objectstack/lint` full suite 133 files, 6102 passed; lint
typecheck exit 0. `dispatch-gates --commands` re-derives the same 122,
all exit 0 at `c5a343c32a`, and `--ran` reports 0 NOT-MEASURED and 0
UNRUN. `check:published-readme-links`, `check:published-readme-exports`,
`check:nul-bytes` and `check:doc-authoring` exit 0. Narrowed lint over
the 27 changed `.ts` files: 0 errors, 0 warnings. Spec,
service-automation and cli code is untouched by the round, so their
`caeaab982f` readings stand.
## Ablations
Three ablations, one per behaviour, each through
`scripts/ablation-replace.mjs` in WRAP mode under the verify lock, from
the committed state `caeaab982f`. Each mutation is verified on disk
(anchor x1 → x0, blob changed), and each restore is proven blob == HEAD
with an empty `git diff HEAD`.
- **A1, the old judge branch.** `KEPT_KINDS` back to `['date-macro',
'user']`. `flow-value-slot-template.test.ts` goes red: 8 failed, 93
passed. Every run-user refusal pin fails. Restored to blob
`8ee782870cd2`.
- **A2, the divisor-only `celExpression`.** The old
`inner.replace(/\/\s*(\d+).../g, '/ $1.0')` body put back. The same file
goes red: 9 failed, 92 passed. The H4 pins `{int * 2}` and `{items.0 *
2}` fail, as do the call/keyword/quoted-string controls. Restored to
blob `8b5be3d9cad1`.
- **A3, the binding removed.** `celScope` back to `{ ...vars, vars }`.
`flow-cel-current-user.test.ts` goes red: 13 failed, 1 passed. The one
pass is the control that a bare `current_user.id` faults without a user,
which holds unbound too. Restored to blob `d17b32e3e677`.
- **A4, the old lint hint** (patch round 1, from committed
`a7d0cb1b34`): the firing site's hint put back for every position.
`lint-flow-patterns.test.ts` goes red, 7 failed and 191 passed, exactly
the 7 value-slot pins. The text-slot pin and both controls stay green.
Restored to blob `ee30b6ea9c22`.
A1 and A2 resolve the judge from `src` (spec's own tests), and A3
resolves `engine.ts` by a relative import, so no `dist/` sits on any
ablation's path.
## Acceptance notes
Nothing here is filed. Each item is for the seat:
- **Pending release notes this change makes false.** Three pending
changesets still state what this change retires:
- pass 1's
`.changeset/19939-flow-value-slot-template-dialect-refused.md` lists
`{$User.PATH}` as still accepted;
- #22110's `.changeset/22110-flow-text-slot-double-brace.md` has the row
`'By {$User.Id}'` → an `assignment` whose value slot still reads it;
- #22477's `.changeset/22477-flow-text-slot-dollar-root-refused.md` has
`assignments: { by: '{$User.Id}' }`.
This PR's changeset states that it supersedes those lines, and it
carries the corrected FROM → TO row. Correcting them in place is
`check:empty-changeset`'s "DELIBERATE CORRECTION" path, which keeps the
required gate red until a person confirms. That is a release decision,
so it was not taken here.
- **The other `current_user` aliases.** The ruling binds `current_user`
only. `user.id`, `ctx.user.id` and `os.user.id` in a flow condition pass
`validateStackExpressions` with 0 issues and fault at run time with
`Unknown variable: user` / `ctx` / `os` (measured at `caeaab982f`).
ADR-0068 mounts one EvalUser under all four names elsewhere. → filed as
#22565.
- **The six rider positions** (`subflow.input`, `map.input`,
`script.inputs`, `screen.defaults`, a screen field's `defaultValue`,
`http`) keep the single-brace dialect. None is a ledger value slot or
evaluates a CEL envelope, so retiring the dialect there needs a
value-slot declaration and executor evaluation per position. That is a
separate decision, not this pass.
- **The kept-spelling leak.** `'Due {TODAY()} by {$User.Id}'` is kept
whole until pass 3 retires the date macros (pinned as kept).
- **`skills/objectstack-automation/SKILL.md`** (Tier H; #22260 is
closed, so the seat files a skills-lane card when this PR lands) still
teaches `{$User.PATH}` as kept and the text-slot run-user remedy through
the value slot.
- **HotCRM's 5 `{$User.Id}` sites** (pass 1's census) are in another
repository, on the seat's handover ledger.
- **The judge's remedy at positions where an envelope is literal data**
(carried to pass 3 on #19939). At an element of the legacy `assignments`
array (`assignments[0].value`), and at a string nested inside an object
literal in a value slot, the judge refuses `{$User.Id}` and names the
CEL envelope. The prescribed metadata then passes the build door and
`registerFlow`, and the run stores the envelope OBJECT as the value. The
canonical map form evaluates correctly. This has held since pass 1 for
every token at those positions. The lint hint inherits it by design,
because it asks the judge, so one fix in the judge reaches both.
- **`flow-double-brace-interpolation`'s hint in a value slot** says `Use
{var} (e.g. {record.title})`, which the value-slot judge refuses. False
since pass 1; the slot-aware remedy is a design choice. Carried to pass
3 with the item above.
- Two #1315 false-positive guards in `lint-flow-patterns.test.ts` are
titled as endorsing spellings the value-slot judge refuses ("correct
single-brace interpolation", "a braced $User reference"). Their
assertions hold; only the titles mislead.
- **`Clause-②`.** The claim line reads `no`, and the dispatch prescribes
`no (narrowing)`. This PR uses the dispatch's spelling, which is the
same value with its direction arm stated.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5495331 commit 6a06e3b
31 files changed
Lines changed: 1378 additions & 207 deletions
File tree
- .changeset
- content/docs
- automation
- references/automation
- examples/app-todo/src/flows
- packages
- cli
- src/commands
- test
- 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 | + | |
| 44 | + | |
| 45 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
271 | 271 | | |
272 | 272 | | |
273 | 273 | | |
| 274 | + | |
| 275 | + | |
274 | 276 | | |
275 | | - | |
| 277 | + | |
276 | 278 | | |
277 | 279 | | |
278 | | - | |
| 280 | + | |
279 | 281 | | |
280 | 282 | | |
281 | 283 | | |
| |||
325 | 327 | | |
326 | 328 | | |
327 | 329 | | |
328 | | - | |
| 330 | + | |
| 331 | + | |
329 | 332 | | |
330 | 333 | | |
331 | 334 | | |
| |||
1990 | 1993 | | |
1991 | 1994 | | |
1992 | 1995 | | |
1993 | | - | |
| 1996 | + | |
1994 | 1997 | | |
1995 | | - | |
1996 | | - | |
1997 | | - | |
| 1998 | + | |
| 1999 | + | |
| 2000 | + | |
1998 | 2001 | | |
1999 | 2002 | | |
2000 | 2003 | | |
2001 | 2004 | | |
2002 | 2005 | | |
2003 | 2006 | | |
2004 | | - | |
2005 | | - | |
2006 | | - | |
2007 | | - | |
| 2007 | + | |
| 2008 | + | |
| 2009 | + | |
| 2010 | + | |
| 2011 | + | |
| 2012 | + | |
| 2013 | + | |
| 2014 | + | |
| 2015 | + | |
| 2016 | + | |
| 2017 | + | |
| 2018 | + | |
| 2019 | + | |
2008 | 2020 | | |
2009 | 2021 | | |
2010 | 2022 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
138 | | - | |
| 138 | + | |
139 | 139 | | |
140 | 140 | | |
141 | 141 | | |
| |||
180 | 180 | | |
181 | 181 | | |
182 | 182 | | |
183 | | - | |
| 183 | + | |
184 | 184 | | |
185 | 185 | | |
186 | 186 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
472 | 472 | | |
473 | 473 | | |
474 | 474 | | |
475 | | - | |
476 | | - | |
477 | | - | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
478 | 479 | | |
479 | 480 | | |
480 | 481 | | |
481 | 482 | | |
482 | 483 | | |
483 | 484 | | |
484 | | - | |
| 485 | + | |
485 | 486 | | |
486 | 487 | | |
487 | 488 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
155 | 155 | | |
156 | 156 | | |
157 | 157 | | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
158 | 161 | | |
159 | | - | |
160 | | - | |
161 | | - | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
162 | 166 | | |
163 | 167 | | |
164 | 168 | | |
165 | 169 | | |
166 | | - | |
| 170 | + | |
167 | 171 | | |
168 | 172 | | |
169 | 173 | | |
| |||
0 commit comments