Repository navigation
Commit e462186
feat(spec,automation): create_record / update_record fields.* accept the CEL value envelope — declared and evaluated together (#20205)
Fixes #19938
Fixes #11182
Clause-②: yes
One branch, one PR, spec commit first, one merge: maintainer ruling B on
#19938 (record `5816929495`, 「19938 同意」), which folds the #11182 engine
half into this change. #11182 ruling D (`5805777944`, 「11182 D 其他同意」)
governs the content.
## What changes
A value in a `create_record` or `update_record` node's `fields` map may
now be a CEL value envelope, `{ dialect: 'cel', source: '…' }`, under
the same shape and dialect rules the `assignment` node's `assignments`
map already has. The slot is declared in the expression ledger and
evaluated by the executor in the same merge, so it is never declared
without being evaluated. A plain string in `fields.*` is still a
`{token}` template and means what it meant in 17.x. No spelling changes
meaning (ruling D).
### Commit 1 (`de7c28942`): spec, the contract half (#19938)
- **Expression ledger** (`flow-node-expression-paths.ts`): two new
`value` rows, `create_record.fields.*` and `update_record.fields.*`. The
census grows from five rows to seven. The prose that called the
assignment map the only value slot is corrected, and so is the shipped
`predicateSlotRefusal` sentence that named it as the only `value`-role
spelling.
- **Value contract** (`builtin-node-config.zod.ts`):
`CreateRecordConfigSchema` / `UpdateRecordConfigSchema` `fields` values
take `FlowValueSlotSchema`. One factory builds every value slot's
contract, so the shape rule is stated once. It is declared above the
CRUD schemas: `OS_EAGER_SCHEMAS=1` runs every lazy factory at module
load, and a schema declared further down would be in its temporal dead
zone there.
- **Q2, the seat's reading**: the refusal sentence is slot-neutral.
`VALUE_ENVELOPE_REFUSAL` reads "A value carrying a `dialect` key is read
as an expression envelope, and this one is not a valid CEL value
envelope." The published `ASSIGNMENT_VALUE_ENVELOPE_REFUSAL` is kept and
is the same string, and `AssignmentExpressionValueSchema`'s dialect
message is neutral too. A refused field value is no longer told it is
"an assignment value".
- **Ratchet channel**: `LEDGER_DECLARED_NODE_CONFIG_SCHEMAS` carries
both CRUD contracts. Their descriptors publish `fields` as
`additionalProperties: true`, exactly like `assignment`, so the marker
rides the spec Zod.
- `resolveFlowNodeValueSlots`: every authored value of a value slot,
strings included, located by the ledger's own walk. The lint hint uses
it, so lint never re-spells the path shapes.
- Generated artefacts are regenerated, never hand-edited: api-surface,
export-origins, declaration-map, json-schema manifest and reference
docs. The three new dropped-refinement sites (`CreateRecordConfig`,
`UpdateRecordConfig`, `FlowValueSlot`) are declared, with the ledger
header totals 207 → 210 and 574 → 577, the build's own reading.
- Pins: the census, the fields entries, the value-slot resolver, the
CRUD value contract (accept, preserve, refuse at the field's path,
slot-neutral sentence), and the ratchet pin in
`config-expression-ledger.test.ts`.
### Commit 2 (`8e37b80ff`): engine, the executor half (#11182)
- **Executor** (`crud-nodes.ts`, the `resolveFieldValues` function):
each top-level `fields` value that is envelope-shaped goes through
`AutomationEngine.evaluateValueEnvelope`, the call the `assignment`
executor makes (one evaluator, one CEL scope, one notion of malformed).
Every other value goes through `interpolate()` exactly as the whole map
used to. A malformed envelope, or one that faults on the live values,
fails the node and writes nothing.
- **Consumers**: `engine.ts` `valueEnvelopeRefusals` and lint
`checkDeclaredValue` read the slot-neutral `FlowValueSlotSchema` /
`VALUE_ENVELOPE_REFUSAL`. Comments in `engine.ts`, `logic-nodes.ts`,
`node-executor.zod.ts` and `validate-expressions.ts` no longer call
`assignments.*` the only value slot.
- **Author-time hint** (ruling D point 1): `objectstack validate` warns
(never errors) when any value slot holds a `{…}` template expression,
meaning arithmetic or a call to one of the six functions, and points it
at the envelope. The warning states the one conversion trap: `/ 100`
becomes `/ 100.0`.
- **Wrong guidance fixed** (ruling D point 2): the `template.ts`
docblock and the shipped `round()` arity refusal now prescribe `round(x
* 100) / 100.0`. Measured on this tree: CEL answers `1234` for `round(x
* 100) / 100` and `1234.57` for `/ 100.0` at `x = 1234.5678`, while the
template dialect answers `1234.57` for both. `/ 100.0` is right in both
dialects. The arity pin in `template-functions.test.ts` asserts the new
prescription and the measured reason.
- **Docs** (`flows.mdx`): value slots, the envelope in the Create Record
example, the three refusal doors (the stale "faults at run time, tracked
in #15430" paragraph is corrected), the dialect table's scale-2 row, and
a CEL-envelope row.
- **Changeset**: minor for `@objectstack/spec`,
`@objectstack/service-automation` and `@objectstack/lint`, with
`Clause-②: yes (widening)` (Q3, the seat's reading). It names what newly
passes, what newly refuses, and the nested and literal rule.
- `skills/objectstack-automation/SKILL.md` (Tier H) is untouched, per
ruling D point 2.
The merge of `origin/main` (`94216fb72`) went through
`scripts/pm/os-regen-merge.sh`. It resolved without conflicts,
`check:generated` stayed at 15/15 current afterwards, and the branch's
delta against main contains only this change.
## Why the hint covers only template expressions
The hint covers arithmetic and the six functions, where CEL is a strict
superset and the only conversion trap is stated in the warning itself.
It does not cover:
- a plain `{var}` / `{var.path}` reference: CEL adds nothing, and an
absent key would flip from `undefined` to a fault;
- `NOW()` / `TODAY()`: CEL's `now()` / `today()` are timestamps with no
`string(timestamp)`, and the string form is the v18 carrier's (#19939)
to add;
- `$User.*`: the flow's CEL scope binds no user.
Hinting any of these would steer authors toward metadata that the
runtime honours but that makes the value worse. Census, a heuristic scan
of object-literal `fields` / `assignments` blocks: the hint fires on 0
flow sites outside tests in objectstack (`94216fb72`) and on 2 in HotCRM
(`2f7b232`, the two `quote-generation.flow.ts` money fields #11182
measured).
## Measured: the 42-row probe grid, base against after
The grid is 7 values × {create_record, update_record} × {number, text,
json} columns. The doors are `FlowSchema.safeParse`, the `os validate`
pipeline (`normalizeStackInput` → unknown-key lints →
`ObjectStackDefinitionSchema` → `runAuthoringRules('validate')`),
`registerFlow`, and a run over a real ObjectQL with a recording driver.
- **Base (`455dcc060`)** reproduces the prior report exactly. Every
envelope passes every door and is written as a literal object: text and
JSON columns report success, and the number column is refused by the
data engine.
- **After**:
- The 18 template and literal rows are byte-identical to base.
- The 6 valid-envelope rows are evaluated (`price * 2` writes `42` on
the number, text and JSON columns).
- The 18 malformed rows (no `source`, non-parsing CEL, a `template`
dialect) are refused at validate (`error` / `expression-invalid`) and at
`registerFlow`, located at `config.fields.FIELD` with the slot-neutral
sentence.
- `FlowSchema.parse` is unchanged, because node config is an open
record.
- **Runtime publish gate (`metadata-protocol`)**, not measured before
and measured now through `saveMetaItem` over the protocol's stub-engine
harness:
- A malformed `fields.*` envelope goes from SAVED to `422
INVALID_METADATA` with `expression-invalid` at the node.
- A valid envelope still saves.
- A `{round(price * 100) / 100}` template saves with the hint as an
advisory, in all three value slots.
- The `assignment` control rows were refused before and after; only the
sentence changed.
- **Nested values** (mechanism assumption 3): only the top-level value
of a field is judged. An envelope-shaped object nested in a JSON value
or an array is data, is interpolated as before, and is written verbatim,
which the executor test pins. A top-level JSON value that is itself an
object with a string `dialect` is now an envelope; the changeset states
the rule and the escape (bind it to a variable and write
`'{thatVariable}'`). No flow in this repo or HotCRM writes an
envelope-shaped object into `fields`. The heuristic scan found 0 outside
tests; its control leg found 14 envelope-shaped `assignments` values in
objectstack tests.
## Reverse verification (one-off, from the committed state)
- The executor half was reverted to the old whole-map `interpolate()` at
both sites through `scripts/ablation-replace.mjs`: anchor 2 → 0, blob
`a292a5ac9181` → `1d6328c8d76d`. The executor test went from 26/26 to 8
failed / 18 passed: the six evaluation pins and two run-time-fault pins
went red, and the preservation and registration pins stayed green
because registration is ledger-driven. The restore was proven: the blob
equals HEAD and `git diff HEAD` is empty.
- The lint hint was short-circuited. The lint test went to 4 failed / 22
passed, all four of them the hinted cases, and the restore was proven.
- The two ledger rows were deleted. The spec pins went to 6 failed / 127
passed (census, the two slot declarations, the two field resolutions,
the value-slot resolver), and the restore was proven.
## Verification at `94216fb72`
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 110 commands. `--ran` reports 110
derived, 110 run, 0 NOT-MEASURED, 0 UNRUN. Three gates first refused
with exit 3 (prerequisite not met) and passed after the builds they
asked for: `check:skill-examples`, `check:dual-build-cjs-loads` and
`check:type-check-debt`, the last one re-run after direct rebuilds
because the ablation restores had touched mtimes.
- Package suites, all command-exit 0:
- `@objectstack/spec`: 541 files, 15924 tests
- `@objectstack/service-automation`: 146 files, 1757 tests
- `@objectstack/lint`: 109 files, 4209 tests
- `@objectstack/metadata-protocol`: 189 files passed, 3 skipped (2700
tests)
- `typecheck` is green for all four.
- The other direct consumers of the changed exports are green:
`examples/app-showcase/test/predicate-write-bulk-intent.test.ts` (17/17,
parses with `UpdateRecordConfigSchema`) and
`packages/cli/src/flow-node-undeclared-field-write.integration.test.ts`
(7/7, drives `registerCrudNodes`).
## Acceptance notes
- **`registerFlow` has no ADR-0112 envelope.** Every expression refusal
there throws one aggregated plain `Error` with no `code` or `status`,
and this is pre-existing and door-wide. The registration pins therefore
assert the located message substance (node, slot, path, sentence). The
`os validate` pins assert the rule id `expression-invalid` and severity
`error`, and the publish gate answers `422` / `INVALID_METADATA`.
- **One excuse was added to the `validate-expressions.test.ts`
meta-guard: `grammar`.** It is an import-specifier artefact, not a
receiver. The guard's scan
`RULE_CODE.matchAll(/\b([a-z][\w$]*)\??\.[A-Za-z_$]/g)` reads
`grammar.js` inside `'./flow-template-grammar.js'` as a receiver, the
same artefact it already excuses as `scope`, `fields` and `guards`. The
import is already a named import, so the construct the scanner misreads
is the module path itself. My new locals were renamed instead of
excused: the value-slot loop reuses the excused `found` (the same
resolver-result shape), and the token scan uses a `RegExp.exec` loop
with no lowercase receiver. What the guard checks for real receivers is
unchanged. The engine commit was amended so that this fix sits inside it
and each commit is green on its own.
- **File surface beyond the claims' lists**, each a test of a claimed
file:
- `template-functions.test.ts`: the arity-refusal prescription pin for
`template.ts`;
- `validate-expressions.test.ts`: the meta-guard entry above;
- two new test files: `crud-fields-value-envelope.test.ts` and
`validate-expressions.fields-value-slot.test.ts`.
- **Observations, not filed:**
- `flow-field-expression-scale.integration.test.ts:23` still calls
`round(x * 100) / 100` "the CEL-identical authoring pattern" in a test
comment. The oracle it runs is the template dialect, where that is
correct. Carrier: #19939.
- `dropped-refinements.baseline.json`'s unpinned
`measured.refinementSitesThatDidProject` reads 369 while the build
measures 409. It was already stale before this change and is left as
found.
- A number written into a text column is accepted by ObjectQL, for
literal `42` at base as well. This carries the prior report's
observation forward.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2bcd5cf commit e462186
25 files changed
Lines changed: 1223 additions & 215 deletions
File tree
- .changeset
- content/docs
- automation
- references
- automation
- packages
- lint/src
- services/service-automation/src
- builtin
- spec
- api-surface
- declaration-map
- export-origins
- json-schema.manifest
- src/automation
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
200 | 200 | | |
201 | 201 | | |
202 | 202 | | |
203 | | - | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
208 | 214 | | |
209 | 215 | | |
210 | 216 | | |
211 | 217 | | |
212 | | - | |
213 | | - | |
214 | | - | |
215 | | - | |
216 | | - | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
217 | 223 | | |
218 | 224 | | |
219 | 225 | | |
220 | | - | |
221 | | - | |
222 | | - | |
223 | | - | |
224 | | - | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
225 | 232 | | |
226 | 233 | | |
227 | 234 | | |
| |||
240 | 247 | | |
241 | 248 | | |
242 | 249 | | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
243 | 253 | | |
244 | 254 | | |
245 | 255 | | |
| |||
1785 | 1795 | | |
1786 | 1796 | | |
1787 | 1797 | | |
1788 | | - | |
| 1798 | + | |
| 1799 | + | |
| 1800 | + | |
| 1801 | + | |
| 1802 | + | |
| 1803 | + | |
| 1804 | + | |
| 1805 | + | |
| 1806 | + | |
| 1807 | + | |
1789 | 1808 | | |
1790 | 1809 | | |
1791 | 1810 | | |
| |||
Lines changed: 17 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
72 | 77 | | |
73 | 78 | | |
74 | 79 | | |
| |||
86 | 91 | | |
87 | 92 | | |
88 | 93 | | |
89 | | - | |
90 | | - | |
| 94 | + | |
| 95 | + | |
91 | 96 | | |
92 | 97 | | |
93 | 98 | | |
| |||
108 | 113 | | |
109 | 114 | | |
110 | 115 | | |
111 | | - | |
| 116 | + | |
112 | 117 | | |
113 | 118 | | |
114 | 119 | | |
| |||
136 | 141 | | |
137 | 142 | | |
138 | 143 | | |
139 | | - | |
| 144 | + | |
140 | 145 | | |
141 | 146 | | |
142 | 147 | | |
| |||
165 | 170 | | |
166 | 171 | | |
167 | 172 | | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
168 | 180 | | |
169 | 181 | | |
170 | 182 | | |
| |||
272 | 284 | | |
273 | 285 | | |
274 | 286 | | |
275 | | - | |
| 287 | + | |
276 | 288 | | |
277 | 289 | | |
278 | 290 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
| 36 | + | |
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
| 108 | + | |
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
116 | | - | |
| 116 | + | |
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
| |||
0 commit comments