Repository navigation
Commit 534ca13
Fixes #23604
Clause-②: no
## What this carries
The two `domain:services` rows split out of #15953's classification
table (comment `6115219467`, rows as of slice 6's landing `6144431383`).
Tests only, in `packages/services/service-automation/src/builtin/`. No
production file changes. Each row is carried by the existing test file
that already pinned part of it (ruling: the missing cases go in that
file, not in a second file), and each file's header now names the row,
#15953 and the hotcrm file it lets hotcrm retire
(objectstack-ai/hotcrm#1583).
| row | hotcrm file it lets retire | carried by |
|:--|:--|:--|
| 8 | `test/readonly-write-semantics.test.ts` (its flow-writer rows:
flow node, screen flow, subflow) | `crud-runas.test.ts` |
| 11 | `test/flow-filter-today-token.test.ts` |
`crud-filter-guard.test.ts` |
### Row 8: a flow's `runAs` decides `isSystem` for its writes, per flow
writer kind
- **Flow node: already pinned exactly.** `crud-runas.test.ts`'s first
describe (`flow.runAs identity enforcement at the data layer (#1888)`)
asserts that every data op of a `runAs:'system'`, `'user'` and default
flow receives `isSystem` true, false and false. No new case. The header
line names the hotcrm file.
- **Screen flow: added** (3 cases, one `it.each`). The screen parks the
run, and nothing is written before it is answered. The `update_record`
on the RESUMED leg receives `isSystem` from the flow's `runAs`:
`'system'` gives true, `'user'` and omitted give false. The triggering
user and org ride that write. The resume call carries no identity, so
the write's context is the one the suspension persisted.
- **Subflow: added** (2 cases). The child is its own run, so the CHILD
flow's `runAs` decides its writes, whichever way the two flows differ:
- a `'system'` child under a default parent writes `isSystem: true`
(actor `svc:flow:sys_child`), while the parent's own write stays
`false`;
- a default child under a `'system'` parent writes `isSystem: false` as
the triggering user. The caller's elevation does not carry into the
child.
- **Not this package's:** the PATCH and `beforeUpdate` writer kinds, and
the objectql strip rule (an update strips `readonly` fields iff its
context is not system). Those belong to the engine card.
- `runas-system-stamping.integration.test.ts`,
`runas-attribution-contract.test.ts` and
`runas-grant-resolution.integration.test.ts` pin attribution, row
stamping and grant resolution. None of them pins the writer-kind switch,
so none carries this row.
### Row 11: a flow filter's date value, in the dialect filters use now
The row names `interpolateFilter` resolving `TODAY()`. That function no
longer exists: S6 (#19939) retired the template dialect from filter
operands, the date macros included. This is what is true on `main`,
measured and now pinned:
- **A CEL date envelope is evaluated before the query: added** (3 cases,
one per CRUD verb). With the clock pinned at `2026-03-14T12:00:00Z`
(only `Date` is faked), `{ $lt: { dialect: 'cel', source:
'isoDate(today())' } }` reaches the data engine as `{ $lt: '2026-03-14'
}`, and `isoDate(daysAgo(30))` reaches it as `'2026-02-12'`.
- **`{today}` is passed through untouched: one operand added to the
existing placeholder case** (`a query placeholder reaches the engine
untouched, beside a literal`). That case already covered
`{30_days_ago}`, `{current_year_start}` and `{current_user_id}`.
`{today}` is a word of the query engine's vocabulary
(`isKnownFilterToken`), which objectql resolves. The executor does not
resolve it.
- **The retired `{TODAY()}` macro is refused before any query: added**
(6 cases). At `registerFlow` the refusal points to the operand
(`config.filter.close_date.$lt`) and names the CEL spelling
`isoDate(today())`, and the flow is not registered. The executor's
contract parse refuses it as a guard (`errorClass: 'guard'`), and the
data engine receives nothing.
- **Not this package's:** objectql's `resolveFilterTokens`, which
resolves `{today}` and refuses `{TODAY()}`. That belongs to the engine
card.
The difference from the row's wording is the retired mechanism only. The
date is still resolved before the query. It is now written as a CEL
envelope instead of a `{TODAY()}` token, and `{TODAY()}` is now refused
instead of resolved. No spelling was restored.
## Ablations (one per row)
Both mutations were temporary and were restored, with the restore
proven, before any commit. Each went through
`scripts/ablation-replace.mjs`: anchor count 1 to 0, replacement count 0
to 1, blob changed. A trap restored the file, and the restore was proven
by the blob hash equalling HEAD's and an empty `git diff HEAD`. Both
mutated files are imported by these tests through relative `./` paths,
so vitest runs them from `src/` and no `dist/` build sits between the
mutation and the run.
**Row 8.** `packages/services/service-automation/src/engine.ts:6351`,
the run context's construction point:
```text
- runAs: flow.runAs ?? 'user',
+ runAs: 'user',
```
`crud-runas.test.ts`: `Tests 8 failed | 21 passed (29)`. The new cases
that went red:
- `screen flow, runAs:'system': the write on the resumed leg is isSystem
true`: `AssertionError: the screen flow's runAs did not decide the
resumed write: expected false to be true`
- `subflow: a 'system' child called by a 'user' parent writes elevated;
...`: `AssertionError: the 'system' child's runAs did not decide its
write: expected false to be true`
- `subflow: a child with no runAs called by a 'system' parent writes as
the user ...`: `AssertionError: the 'system' parent's own write was not
elevated: expected false to be true`
- plus the five existing elevation cases, e.g. `REGRESSION: a
runAs:'system' flow must reach the data layer elevated (fails if runAs
is ignored)`: `runAs:system did not elevate the data op (#1888
regressed): expected false to be true`.
Stayed green, as predicted before the run: the `'user'` and omitted
screen cases, because the mutation produces exactly their answer. This
mutation reddens the second subflow case at its parent assertion, so it
never reaches that case's leak assertion. A mutation that makes the
child inherit the caller's `runAs` would redden the leak assertion. It
was not run, because the ruling allows one ablation per row.
**Row 11.**
`packages/services/service-automation/src/builtin/crud-nodes.ts:88`, in
`resolveNodeFilter`:
```text
- if (!isExpressionEnvelopeShaped(operand.value)) continue;
+ if (true) continue;
```
`crud-filter-guard.test.ts`: `Tests 20 failed | 17 passed (37)`. The new
cases that went red: `get_record`, `update_record` and `delete_record: a
CEL date envelope is evaluated before the query — the engine receives
the date`, each with:
```text
- "$lt": "2026-03-14",
+ "$lt": { "dialect": "cel", "source": "isoDate(today())" },
- "$gte": "2026-02-12",
+ "$gte": { "dialect": "cel", "source": "isoDate(daysAgo(30))" },
```
Stayed green, as predicted: the six `{TODAY()}` refusal cases and the
placeholder case, which do not depend on evaluation. The refusal is the
spec's judge, `classifyFlowFilterOperandString`, composed into the node
contract. That judge lives in `packages/spec` and is not this row's
ablation.
## Tests (head `aed1c7c3fd`)
- Dependency closure built first: `turbo run build
--filter='@objectstack/service-automation^...' --concurrency=2`, 29 of
29 tasks successful.
- `pnpm --filter @objectstack/service-automation exec vitest run
--maxWorkers=2`: `Test Files 186 passed (186)`, `Tests 2500 passed
(2500)`.
- The two edited files alone: `Test Files 2 passed (2)`, `Tests 66
passed (66)`. The slowest new case took 12 ms.
- `pnpm --filter @objectstack/service-automation run typecheck` exited
0. The test program (`tsconfig.test.json`) lists both edited files, and
`check:test-typecheck` reported 0 errors.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (2 paths, +260/-3) derived 52 commands. 51
exited 0. `pnpm check:dual-build-cjs-loads` exited 3 (`PREREQUISITE NOT
MET`: it reads the built output of every workspace package, and only
this package's closure is built here), so it is NOT MEASURED locally and
CI's run is the measurement. `--ran` reconciliation: `52 derived
famil(ies) accounted for — 51 run, 1 NOT-MEASURED`.
## Acceptance notes
- The card body's row 11 names a retired mechanism (`interpolateFilter`,
`TODAY()`). The semantics it stood for are pinned in today's dialect,
described above. The premise survives, with the mechanism updated.
- The PM's lead for row 8 named the three `runas-*` files. The flow-node
switch is actually pinned in `builtin/crud-runas.test.ts`, which is why
the new writer-kind cases live there.
- `registerFlow` refuses a value-slot token by throwing a bare `Error`
whose message is the located refusal, with no `code`. The existing pins
and the new ones therefore assert the message's located prefix (built
from the exported `VALUE_SLOT_TEMPLATE_REFUSAL`) and the named CEL
spelling. This is an observation about an in-process API, not a finding:
carrier none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9bb9370 commit 534ca13
2 files changed
Lines changed: 260 additions & 3 deletions
File tree
- packages/services/service-automation/src/builtin
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
31 | 40 | | |
32 | 41 | | |
33 | 42 | | |
| |||
126 | 135 | | |
127 | 136 | | |
128 | 137 | | |
129 | | - | |
| 138 | + | |
130 | 139 | | |
131 | 140 | | |
132 | 141 | | |
133 | 142 | | |
134 | 143 | | |
| 144 | + | |
135 | 145 | | |
136 | | - | |
| 146 | + | |
137 | 147 | | |
138 | 148 | | |
139 | 149 | | |
| |||
326 | 336 | | |
327 | 337 | | |
328 | 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
13 | 22 | | |
14 | 23 | | |
15 | 24 | | |
16 | 25 | | |
| 26 | + | |
| 27 | + | |
17 | 28 | | |
18 | 29 | | |
19 | 30 | | |
| |||
499 | 510 | | |
500 | 511 | | |
501 | 512 | | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
0 commit comments