Repository navigation
Commit 5556455
fix(service-automation)!: a flow's record nodes treat a computed filter operand as a value, never as a query operator (#29404)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9K5fJRZk7J3wEEMXeLi47fH)_
Refs #29050
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: follow-up #29418 — content/docs/automation/flows.mdx,
skills/objectstack-automation/SKILL.md
## 维护者速读
- 改了什么:流程的记录节点(`get_record` / `update_record` /
`delete_record`)过滤条件里由表达式算出来的值,现在只当作“值”来比较:必须是字符串、数字(含 bigint)、布尔或日期,在
`$in` / `$nin` / `$between`
处可以是它们的列表(不得含空项);算出别的东西(对象、该放单个值的位置上的列表、含非值成员或空项的列表)时,节点在查询前拒绝运行(守卫失败,不可被
fault 边吞掉),提示里点名节点、表达式和它在过滤条件里的位置。作者手写的过滤结构与字面值不变。
- 为什么改:按维护者裁决「A+C 同意」的 C
半,规则是:记录节点把过滤条件操作数位置上的求值结果当作值,绝不当作查询运算符;不是标量(或运算符接受列表时不是标量列表)的值在查询运行前即被拒绝。
- 风险与回滚:收窄(BREAKING):把对象或列表算进过滤操作数的流程会在该节点失败。仓内示例、showcase、dogfood
夹具与各包测试流程普查下来,过滤操作数的表达式全是标量 id 或字段值,无此用法;全量 dogfood 结果见下方
Verification。回滚即撤销本 PR。
- 先看:`packages/services/service-automation/src/builtin/crud-nodes.ts` 的
`resolveNodeFilter` 与新函数
`operandValueRefusal`;`crud-filter-guard.test.ts` 新增的 describe「a
computed filter operand is a value, never filter structure」。
- 两分钟验证:`pnpm --filter @objectstack/service-automation exec vitest run
src/builtin/crud-filter-guard.test.ts`,预期 1 个测试文件、66 个用例全部通过。
- 需要你做的:无,席位落地。
## What changes
The rule: **a flow's record nodes treat an evaluated value in a filter's
operand position as a value, never as a query operator: a value that is
not a scalar (or a list of scalars where the operator takes a list) is
refused before the query runs.**
- **`resolveNodeFilter`**
(`packages/services/service-automation/src/builtin/crud-nodes.ts`), the
one place `get_record`, `update_record` and `delete_record` resolve
their filter, now judges each operand an envelope evaluates to, at every
position `flowFilterOperands` locates (a field's value under any key, an
operator's comparand, a list element, a nested-relation value, a
combinator member, `$not`), before writing it into the copied filter.
- **A value** is a string, number, bigint, boolean or date: the
platform's accepted comparand set (`isAcceptedFilterComparand`,
`@objectstack/spec/data`) less `null`, which the nothing guard owns. At
the comparand of an operator whose comparand is a list (`$in`, `$nin`,
`$between`; the operand's path ends in that operator), a list whose
every member is such a value is one too; a list with an entry that is
nothing is refused, as an operand that is nothing is.
- **Anything else is refused** with a guard (`refuseNode`), as the
existing nothing guard in the same function is: the message names the
node type, each refused operand's expression and its place in the filter
(`filter.owner.$ne`), and what kind of thing it evaluated to (`an
object`, `a list`, `a list whose entry [1] is an object`, `a list whose
entry [1] is nothing`). The value itself is never echoed. The data
engine is not called.
- **Unchanged:** the structure the author wrote (fields, operators,
`$and` / `$or` / `$not`, nested relations), every literal operand, query
placeholders, and the nothing guard (an operand that evaluates to `null`
keeps its message and wins when both apply).
- **Docblock:** `storedMetadataFilterRefusal`'s note on reading the
resolved filter is reworded, since an operand can no longer evaluate to
a field reference.
- **Changeset:** `.changeset/29050-record-node-operand-value.md`, patch
for `@objectstack/service-automation`, BREAKING (narrowing), ADR-0087
`not-required (no-migration-prescription)`.
The list-operator set is local to the executor
(`LIST_COMPARAND_OPERATORS`): the spec's own split is deliberately kept
out of its published barrel, and no `packages/spec` edit is in scope
here. A test reconciles it against `FieldOperatorsSchema` behaviourally,
so a list operator added to the grammar reds this package's suite.
Reading the set from spec instead is #29419.
## Tests
`packages/services/service-automation/src/builtin/crud-filter-guard.test.ts`,
new describe "a computed filter operand is a value, never filter
structure" (29 cases), on the file's existing data double:
- Each record node: an evaluated non-scalar operand is refused before
the query, naming the node and the operand; the engine is never called.
- `delete_record`, at twelve positions (any key, an operator's
comparand, a list element, a nested relation, a combinator member's
value, a combinator member, a combinator's list, `$not`, a list where
one value belongs, a list at a list element, a list holding a non-value,
a list holding an empty entry): refused, located.
- Several refused operands are all named in the one refusal.
- Each record node: an evaluated list at `$in` / `$nin` / `$between`
passes; an evaluated string, number, boolean and date pass; the
structure the author wrote reaches the engine unchanged.
- Reconciliation: every operator `FieldOperatorsSchema` declares takes
an evaluated list exactly when its enforced slot takes a list.
- Executor level, each record node: the refusal is a guard and nothing
is queried.
**Forced test edits:** none. Each pin goes red with its check removed
(four ablations, each restored to the HEAD blob).
## Acceptance notes
- **Guard, not a routable failure.** The refusal is classed like the
nothing guard beside it: the filter the author wrote is not the filter
that would run, so a `fault` edge does not route it; a `try_catch`
region still catches it.
- **Docs and skill follow-up: #29418.**
`content/docs/automation/flows.mdx` (the CRUD filter paragraph and the
guard list) and `skills/objectstack-automation/SKILL.md` (Filter
operands, the does-not-route table) describe operands as value slots but
do not yet say a non-value is refused. Both are outside this card's file
surface; `skills/**` is a governed surface.
- `get-record-stored-metadata-filter-refusal.integration.test.ts`: one
of its cases is now refused by this rule before the stored-metadata
check is reached; the case still holds (refused, the engine never asked)
and is unchanged.
## Verification
Every reading below was taken on head `cf1d52e8` (round 2 on top of
`37f60fd6`, which merged origin/main `eef116e4`), except the first three
ablations, taken on `04dd52eb` (the fix commit, before round 2).
<details><summary>Tests, typecheck and lint</summary>
- Under the verify lock: `turbo run build
--filter='@objectstack/service-automation^...'` 31/31 tasks, exit 0;
`vitest run` in `packages/services/service-automation`: 260 files / 3369
tests passed; `pnpm --filter @objectstack/service-automation typecheck`
exit 0.
- `eslint --no-inline-config` over
`packages/services/service-automation`: exit 0. The repo-wide run is
CI's.
- `test:repo` is not run locally (no file of that project changed); it
is CI's `Test Core`.
</details>
<details><summary>Ablations</summary>
Each through `scripts/ablation-replace.mjs` (anchor hit once, blob
changed, restored to the HEAD blob with `git diff HEAD` empty), running
`crud-filter-guard.test.ts`; the test imports the executor from source,
so no `dist` leg applies.
- The value check short-circuited: 19 failed / 46 passed of 65 (every
refusal pin red; the passing pins stay green).
- The list-position check removed: 3 failed / 62 passed of 65.
- The list-member check removed: 2 failed / 63 passed of 65.
- The empty-entry check removed (on `cf1d52e8`): 1 failed / 65 passed of
66, the new pin.
</details>
<details><summary>Full dogfood battery</summary>
- `cf1d52e8` · `os-verify-lock: VERDICT command-exit 0` (closure build
`turbo run build --filter='@objectstack/dogfood^...'` exit 0, then `pnpm
--filter @objectstack/dogfood test`) · Test Files 393 passed, 3 skipped
(396) · Tests 3243 passed, 28 skipped (3271) · no failures.
</details>
<details><summary>Dispatch gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived the same 60 commands (3 paths, 306
changed lines, under the 3000-line threshold). All 60 run with stdin
from /dev/null and the exit captured before any pipe: 60 exit 0.
`check:type-check-debt` first read exit 3 (its closure build queued
behind the dogfood run on the lock and timed out, nothing measured);
re-run under the lock it read exit 0 (`126/129 workspace packages
type-checked … 0 in the DEBT ledger`).
- `--ran`: 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero).
- CI check-runs on `cf1d52e8`: not read at the time of writing.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No schema touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (eslint over
`packages/services/service-automation`; the repo-wide run is CI's.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (The 维护者速读 section is
Chinese by the repo's maintainer rule.)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmR4vTcUse1dqBKZNXq)_
<!-- signed: domain:server#5 · os-steve ·
session_01ADHkmR4vTcUse1dqBKZNXq -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e8b10a6 commit 5556455
3 files changed
Lines changed: 300 additions & 6 deletions
File tree
- .changeset
- packages/services/service-automation/src/builtin
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
286 | 287 | | |
287 | 288 | | |
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 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
289 | 477 | | |
290 | 478 | | |
291 | 479 | | |
| |||
0 commit comments