Repository navigation
Commit 243dd3c
fix(service-automation)!: flow CEL
Fixes #22642
Clause-②: no (narrowing)
In flow CEL, `record` is now the record the run was handed, or unbound.
It is never the run's variables map.
`AutomationEngine.celScope` handed the formula engine `record: vars`.
`buildScope` assigns `extra` after `record`, so a `record` variable
already won. With no record in hand, though, CEL `record` was the
variables map, and `record.assignee` silently read a flow variable named
`assignee`. The builder now passes no `record` slot. `record` resolves
through the variable spread like any other name:
- it is bound when an entrance handed the run a record
(`seedRunVariables` binds `context.record` as `record`), or when the
flow binds a `record` variable itself;
- otherwise `record.X` faults `Unknown variable: record`, with the
source, as every other unbound root does.
Bare names (`assignee`) and `vars.assignee` resolve as before. The diff
is one statement and its docblock in
`packages/services/service-automation/src/engine.ts`, one new test file,
and the changeset.
## The open decision: this PR is NOT ready until it is settled
`check-adr-0087-registration` is red on one cause: the changeset claims
`registered flow-cel-record-variables-alias-retired`, and that D3 entry
does not exist yet. I stopped before editing the migrations registry, as
the dispatch asks. The two routes and the evidence are below. The full
analysis is in the `os-dev-report` comment on #22642.
- **The conflict.** The dispatch says "zero reach, no migration entry
owed", and it also asks the changeset to carry the FROM → TO remedy.
With the remedy in the body, the gate's own detector reads a
prescription. `hasMigrationPrescription` on this changeset answers
`true`; the control, `.changeset/15206-managed-content-sealed.md`, which
validly holds `no-migration-prescription`, answers `false`. So
`no-migration-prescription` is refused, and an honest `registered` needs
a ledger entry. PR #22609 (landed) took the same route at zero measured
reach.
- **Route A (recommended): register a D3 semantic entry.** Two files in
`packages/spec`, outside this claim's surface:
- new:
`packages/spec/src/migrations/entries/semantic/18.flow-cel-record-variables-alias-retired.ts`;
- regenerated by `pnpm --filter @objectstack/spec
gen:migration-registry`: `packages/spec/src/migrations/registry.ts`.
- **Route B: drop the remedy from the changeset** and claim
`no-migration-prescription`. This leaves a breaking runtime narrowing
with no FROM → TO, against AGENTS.md's Post-Task Checklist step 3 and
the dispatch.
## Step 1: the census (measured before any edit)
I used a static TypeScript-AST pass that executes nothing from the
corpus. It reads every flow CEL slot: `condition`, `expression`,
`visibleWhen`, and the `{ dialect: 'cel', source }` value envelopes
(tagged templates included). Sources it cannot resolve statically are
listed and resolved by hand, never dropped. Recall control: every file
carrying start-node text yielded a flow literal (0 misses on both
trees).
| tree | flows | CEL slots | read `record` | of those, with no record
entrance |
|:--|--:|--:|--:|--:|
| this repo at `0ec4268972`: `examples/**`, `packages/platform-objects`
(no flows), `packages/qa/dogfood` | 64 | 51 | 2
(`showcase_inbound_task_webhook`, an `api` hook; a dogfood
`record-after-update` flow) | **0** |
| `objectstack-ai/hotcrm` at `f0afcbda07` (`src/`, `test/`) | 45 | 57 (5
resolved by hand: `vars.*` only) | 12, all `record_change` | **0** |
**Reach: zero**, so no flow is rewritten. One cron string read as an
`expression` was excluded from the 51. Deployed metadata and other
repositories were not measured.
## The entrance map: how each door hands a record today
Every entrance hands its record through `AutomationContext.record`, and
`seedRunVariables` binds it as `record` and `$record`. Nothing else
binds `record` except the flow's own variables.
| entrance | what it hands | where |
|:--|:--|:--|
| record-change trigger | the written row | `trigger-record-change` |
| time-relative sweep | each matched row | `trigger-schedule`
`time-relative-trigger.ts` |
| inbound hook | the request body | `trigger-api` |
| `type: 'flow'` action (REST `/actions`, MCP `run_action`) | the loaded
row, or an empty record carrying at most the given `id` | `runtime`
`dispatchFlowAction` / `loadActionSubjectRecord` |
| `subflow` parent | the parent's `context.record` (the child context
spreads the parent's) | `subflow-node.ts` |
| `map` item | the item, when it carries a string `id`; otherwise the
parent's | `map-node.ts` |
| REST trigger route, declared endpoint, cron schedule | **none** |
`buildAutomationContext`; `schedule-trigger.ts` |
## Pins and the ablation
`packages/services/service-automation/src/flow-cel-record-binding.test.ts`
has 17 cases, through `registerFlow` + `execute` and the two primitives:
- **The defect:** `record.assignee`, and the card's
`has(record.assignee) ? record.assignee : null`, with no record and an
`assignee` variable: the run fails (`success: false`, `status:
'failed'`), naming `Unknown variable: record` and the source, and never
answers `u9`. An edge predicate and `evaluateValueEnvelope` /
`evaluateCondition` refuse it too.
- **Controls:** a record-triggered run reads `record.assignee` from its
record over a same-named variable. Bare `assignee` and `vars.assignee`
still read the variable. A flow-declared `record` variable is read.
- **Each entrance:** record-change, time-relative, inbound hook, flow
action on a row; an object-less action's empty record faults on the key
and never reads the variable; a `subflow` child reads its parent's
record, and a record-less parent hands none; a `map` item with an `id`
is the child's record, and an id-less item leaves the parent's.
**Ablation** (`scripts/ablation-replace.mjs`, WRAP mode, run from the
committed state): `record: vars` put back. The anchor hit 1 → 0, and the
blob went `69bb14b848c5` → `0451cf389cbd`. Result: **5 failed / 12
passed**, exactly the five no-record pins (both value pins, the edge
predicate, the primitives, and the record-less subflow parent). Controls
and entrances stayed green. Restore proven: the blob after restore
equals HEAD (`69bb14b848c5`), `git diff HEAD` is empty, and `git status
--porcelain` shows 0 lines.
## Verification
Every reading below was taken at HEAD `e87a793ce6` (this branch merged
with `origin/main` `5fb1746611`) unless it says otherwise. Each exit
status was captured before any pipe.
- **Build** (the closure, `turbo run build
--filter='@objectstack/service-automation...' --concurrency=1`): 30/30
tasks, exit 0.
- **`@objectstack/service-automation` tests** (`vitest run
--maxWorkers=2`): 184 files, **2348 passed**, exit 0. The new file
alone: 17/17.
- **`@objectstack/service-automation` typecheck** (`tsc --noEmit` and
`check:test-typecheck`): exit 0. `tsc --listFiles` counts the new test
in the program (1 hit; 184 test files; 0 `error TS`).
- **`@objectstack/spec` `test:repo`**: 54 files, **915 passed**, exit 0.
Its repo tests walk the tree, which includes the new file and the edited
docblock.
- **Derived gates** (`node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`, no paths): 65 commands. Reconciled
with `--ran` and exit codes recorded: **65 derived, 64 run, 1
NOT-MEASURED, 0 unrun**.
- 63 exit 0;
- **red, the named cause above:** `node
scripts/check-adr-0087-registration.mjs --base origin/main` exits 1
because the claimed id `flow-cel-record-variables-alias-retired` does
not exist in the registries;
- **NOT MEASURED:** `pnpm check:dual-build-cjs-loads` exits 3 with
PREREQUISITE NOT MET, because it needs every package's `dist` and only
the `service-automation` closure is built here. Declared narrowing: the
one package this diff changes loads under `require` (`node -e
"require('./dist/index.cjs')"` in
`packages/services/service-automation`, exit 0, `AutomationEngine` is a
function). The full gate is CI's.
- The `dist`-reading gates (`check:dts-closure`,
`check:sourcemap-no-sources-content`) swept the 30 built packages, which
is the `service-automation` closure.
## Acceptance notes
- **The lint twin, a follow-up the seat files.**
`packages/lint/src/flow-cel-root-scope.ts` (landed in #22609) lists
`record` in `ENGINE_BOUND_ROOTS`, the always-bound set. After this
lands, `record` is entrance-derived, so `objectstack validate` still
passes a `record.X` that now faults at run time when the flow has no
record entrance. That PR's D3 entry `flow-cel-unbound-root-refused` also
says `record` is "bound by the engine". This PR does not touch
`packages/lint` or that entry, per the dispatch.
- **Not in scope, not measured:** the `{var}` template dialect (`loop` /
`map` `collection`, text slots) is interpolated from the variables map,
not evaluated through `celScope`, so this change does not reach it. What
`{record.x}` does on a run with no record was not measured here.
- The census tool lived in the session scratchpad and is not committed.
## Seat's append: patch round 1 (head `e4977aa6d2`)
Appended by `domain:services` seat 1
(`session_013j5gkUCpqQiti4GgPqqmnt`) at 2026-10-10T11:23Z, from the
dev's round-1 report on #22642. The open decision above is settled:
**A**. The seat answered in-seat (`6096468599`, which also amends the
claim's file surface), and the spec lane was told on #6017
(`6096471320`). The changeset is unchanged: it keeps its FROM → TO
remedy and `registered flow-cel-record-variables-alias-retired`.
- **The D3 entry.** NEW
`packages/spec/src/migrations/entries/semantic/18.flow-cel-record-variables-alias-retired.ts`,
in the shape of its sibling `flow-cel-unbound-root-refused`.
- **Surface:** every flow CEL slot reading `record.X`, or bare `record`,
on a run that holds no record, where X names a flow variable. The slots
are node and edge `condition`, a decision branch `expression`, a screen
field `visibleWhen`, and the `assignment` and `create_record` /
`update_record` value envelopes.
- **Replacement:** the variable by its name, or `vars.X`.
- **Reason:** the entrance map, and why no D2 conversion exists: a
flow's record entrances are not visible from the flow alone.
- **Acceptance:** list the flow's entrances, then rewrite where an
entrance hands no record. Re-run each path, once started with no record
and once started with a record.
- No `conversionIds` and no `relevantWhen`.
- **The registry.** `packages/spec/src/migrations/registry.ts` is
regenerated by `gen:migration-registry` only (+39, −0).
`check:migration-registry` exits 0 (418 semantic).
- **Merges.** Both go through `bash scripts/pm/os-regen-merge.sh`:
- `d9ebbee173`, from origin/main `ee3ae0360d`;
- `e4977aa6d2`, from origin/main `6a3fe2517b`, which carries #22647's
registry entry. The post-merge regeneration reproduced the merged bytes
exactly.
- **Head `e4977aa6d2`.** The delta against origin/main `6a3fe2517b` is 5
files, +418 / −2.
**Gate exits at `e4977aa6d2`**, each captured before any pipe:
- `check-adr-0087-registration --base origin/main`: exit 0 (registered,
new here).
- spec `check:migration-registry`: exit 0. `check:generated`: exit 0,
all 15 current.
- `service-automation`: 184 files, 2348 passed. Typecheck exit 0.
- spec local tests (where `migrations.test.ts` lives): 642 files, 19180
passed. `test:repo`: 54 files, 915 passed.
- Derived gates: 92 derived, 92 run, 0 NOT-MEASURED, all exit 0.
`check:dual-build-cjs-loads` is included.
---
_Generated by [Claude
Code](https://claude.ai/code/session_013j5gkUCpqQiti4GgPqqmnt)_
---------
Co-authored-by: Claude <noreply@anthropic.com>record is the record the run was handed, or unbound (#22674)1 parent a00cf99 commit 243dd3c
5 files changed
Lines changed: 418 additions & 2 deletions
File tree
- .changeset
- packages
- services/service-automation/src
- spec/src/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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12097 | 12097 | | |
12098 | 12098 | | |
12099 | 12099 | | |
| 12100 | + | |
| 12101 | + | |
| 12102 | + | |
| 12103 | + | |
| 12104 | + | |
| 12105 | + | |
| 12106 | + | |
| 12107 | + | |
| 12108 | + | |
| 12109 | + | |
| 12110 | + | |
| 12111 | + | |
| 12112 | + | |
| 12113 | + | |
| 12114 | + | |
| 12115 | + | |
| 12116 | + | |
| 12117 | + | |
| 12118 | + | |
| 12119 | + | |
| 12120 | + | |
| 12121 | + | |
| 12122 | + | |
| 12123 | + | |
| 12124 | + | |
| 12125 | + | |
| 12126 | + | |
| 12127 | + | |
| 12128 | + | |
12100 | 12129 | | |
12101 | 12130 | | |
12102 | 12131 | | |
12103 | 12132 | | |
12104 | | - | |
| 12133 | + | |
12105 | 12134 | | |
12106 | 12135 | | |
12107 | 12136 | | |
| |||
12115 | 12144 | | |
12116 | 12145 | | |
12117 | 12146 | | |
12118 | | - | |
| 12147 | + | |
| 12148 | + | |
| 12149 | + | |
12119 | 12150 | | |
12120 | 12151 | | |
12121 | 12152 | | |
| |||
Lines changed: 265 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 | + | |
| 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 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
0 commit comments