Repository navigation
Commit e0f17a3
Fixes #20026
Clause-②: no
## REWORK round 1 — what changed since the last revision
Per the contract-review FAIL on this PR (comment 5856311938 on this PR,
order of record 5856314403 on #20026), three things were wrong and are
fixed in this round:
1. **The `matches()` example did not parse when copied.** The docblock's
`"^\d{2}-\d{7}$"` reads fine as raw comment text (comments are not
JS-escape-processed), but a reader who pastes that literal into a real
`.ts` string literal gets ONE level of JS unescaping the comment text
never went through: `\\d` in the comment becomes `\d` at runtime, and
cel-js refuses `\d` as an invalid escape sequence (it only accepts `\\ ?
" ' \` a b f n r t v`, `\u`, `\x`, octal). Rewritten to the escape-free
character class `"^[0-9]{2}-[0-9]{7}$"`, which needs no escaping at any
copy depth. **The previous revision's transcript row for this predicate,
and its "20/20 …" claim, were not measured on the copied literal** — see
the corrected measurement methodology below.
2. **Two changeset sentences were false at the reviewed head.**
"`@marcbachmann/cel-js` parses `=` as the start of an assignment" →
cel-js has no assignment operator; it rejects a lone `=` as an
unexpected character, and CEL equality is `==`. The "both branches
evaluate as expected for all 19" claim and the `matches` table row are
now true, after item 1.
3. **Ten more `validation.test.ts` fixtures were left behind under the
already-widened scope.** Each one's surrounding test name and message
text verbatim-copy a Use Case 4/5/6/7 docblock predicate the same way
the original seven did, so the PR's own stated criterion for moving a
fixture applies to them too: `country = "US"` / `state = "CA"` / `tax_id
= null` (`nested_validation`, Use Case 5 — `tax_id` now the full
escape-free `record.tax_id == null || !matches(record.tax_id,
"^[0-9]{2}-[0-9]{7}$")`), `is_taxable = true` / `tax_code = null`
(`tax_validation`, Use Case 6 — `tax_code` now the full `record.tax_code
== null || record.tax_code == ''`), `region = "EU"` /
`gdpr_consent_given = false` / `tos_accepted = false`
(`regional_validation`, Use Case 4), `user_role = "manager"` /
`approval_amount > 50000` (`role_based_validation`, Use Case 7).
Nothing else changed: the other 18 predicates, the Salesforce-formula
side of the comparison, and the `validation.zod.ts`-TSDoc-only / `patch`
/ `Clause-②: no` shape all passed review and are untouched this round.
## What changed overall (both rounds)
Every example predicate in the `ConditionalValidationSchema` TSDoc
docblock (`packages/spec/src/data/validation.zod.ts`, ~lines 362-537) is
rewritten in the CEL the evaluator actually accepts:
- 17 of 19 used `=` for comparison, or `AND`/`NOT`/`REGEX(...)`
word-form operators — cel-js rejects a lone `=` as an unexpected
character (CEL equality is `==`), and CEL has no `AND`/`OR`/`NOT`
keyword form.
- 2 (`order_total > 10000`, `approval_amount > 50000`) named a bare
field with no `record.` root — an unknown-variable type error.
- `REGEX(tax_id, "…")` is rewritten to the evaluator's registered stdlib
function `matches(record.tax_id, "…")`
(`packages/formula/src/stdlib.ts:361`), using the escape-free character
class `[0-9]` rather than `\d` for the reason in item 1 above.
- The **Salesforce Approach** half of the "Salesforce Pattern
Comparison" block (`IF(ISPICKVAL(...), AND(...), FALSE)`) is
deliberately **untouched** — it documents Salesforce's own formula
syntax, not CEL.
`packages/spec/src/data/validation.test.ts`'s
`ConditionalValidationSchema` fixtures whose surrounding test
name/message text verbatim-copy a docblock predicate move with it
(parse-only — `ValidationRuleSchema.parse(...).not.toThrow()`, no CEL
evaluation, so no behaviour changes): the six exact copies of Use Cases
1-3's original seven strings; `manager_approval = null` →
`record.manager_approval == null` (`order_value_validation`, Use Case 3,
one field-name spelling apart); and, from this round, the ten listed
above. **17 fixture strings moved in total.**
No schema, behaviour, or public export changes — TSDoc text only, in the
same two files the strings already lived in, plus the changeset.
## Evaluation transcript — corrected methodology
(`ExpressionEngine.evaluate`, `@objectstack/formula`)
**The previous transcript was measured on hand-typed strings that were
logically equivalent to the CEL source but never simulated what a reader
actually gets by pasting the TSDoc text into real code.** This
transcript is measured differently: each `when:`/`condition:` literal is
extracted from `validation.zod.ts` byte for byte (comment prefix
stripped, quote characters kept), then handed to Node's own parser
(`eval`) to apply the exact JS string-literal unescaping a reader's
paste would trigger — never hand-retyped. That runtime string is what
gets passed to `ExpressionEngine.evaluate`.
| # | key | raw docblock literal (as written) | runtime string after JS
unescape | true-record | true result | false-record | false result |
|--:|:--|:--|:--|:--|:--|:--|:--|
| 1 | when | `"record.account_type == 'enterprise'"` |
`record.account_type == 'enterprise'` | `{"account_type":"enterprise"}`
| `{"ok":true,"value":true}` OK | `{"account_type":"smb"}` |
`{"ok":true,"value":false}` OK |
| 2 | condition | `'record.approval_status == null'` |
`record.approval_status == null` | `{"approval_status":null}` |
`{"ok":true,"value":true}` OK | `{"approval_status":"approved"}` |
`{"ok":true,"value":false}` OK |
| 3 | when | `'record.requires_shipping == true'` |
`record.requires_shipping == true` | `{"requires_shipping":true}` |
`{"ok":true,"value":true}` OK | `{"requires_shipping":false}` |
`{"ok":true,"value":false}` OK |
| 4 | condition | `"record.shipping_address == null \|\|
record.shipping_address == ''"` | `record.shipping_address == null \|\|
record.shipping_address == ''` | `{"shipping_address":null}` |
`{"ok":true,"value":true}` OK | `{"shipping_address":"123 Main St"}` |
`{"ok":true,"value":false}` OK |
| 5 | when | `'record.order_total > 10000'` | `record.order_total >
10000` | `{"order_total":15000}` | `{"ok":true,"value":true}` OK |
`{"order_total":5000}` | `{"ok":true,"value":false}` OK |
| 6 | condition | `'record.manager_approval_id == null'` |
`record.manager_approval_id == null` | `{"manager_approval_id":null}` |
`{"ok":true,"value":true}` OK | `{"manager_approval_id":"user_123"}` |
`{"ok":true,"value":false}` OK |
| 7 | condition | `'record.payment_method == null'` |
`record.payment_method == null` | `{"payment_method":null}` |
`{"ok":true,"value":true}` OK | `{"payment_method":"credit_card"}` |
`{"ok":true,"value":false}` OK |
| 8 | when | `"record.region == 'EU'"` | `record.region == 'EU'` |
`{"region":"EU"}` | `{"ok":true,"value":true}` OK | `{"region":"US"}` |
`{"ok":true,"value":false}` OK |
| 9 | condition | `'record.gdpr_consent_given == false'` |
`record.gdpr_consent_given == false` | `{"gdpr_consent_given":false}` |
`{"ok":true,"value":true}` OK | `{"gdpr_consent_given":true}` |
`{"ok":true,"value":false}` OK |
| 10 | condition | `'record.tos_accepted == false'` |
`record.tos_accepted == false` | `{"tos_accepted":false}` |
`{"ok":true,"value":true}` OK | `{"tos_accepted":true}` |
`{"ok":true,"value":false}` OK |
| 11 | when | `"record.country == 'US'"` | `record.country == 'US'` |
`{"country":"US"}` | `{"ok":true,"value":true}` OK | `{"country":"CA"}`
| `{"ok":true,"value":false}` OK |
| 12 | when | `"record.state == 'CA'"` | `record.state == 'CA'` |
`{"state":"CA"}` | `{"ok":true,"value":true}` OK | `{"state":"NY"}` |
`{"ok":true,"value":false}` OK |
| 13 | condition | `'record.tax_id == null \|\| !matches(record.tax_id,
"^[0-9]{2}-[0-9]{7}$")'` | `record.tax_id == null \|\|
!matches(record.tax_id, "^[0-9]{2}-[0-9]{7}$")` | `{"tax_id":null}` |
`{"ok":true,"value":true}` OK | `{"tax_id":"12-3456789"}` |
`{"ok":true,"value":false}` OK |
| 14 | when | `'record.is_taxable == true'` | `record.is_taxable ==
true` | `{"is_taxable":true}` | `{"ok":true,"value":true}` OK |
`{"is_taxable":false}` | `{"ok":true,"value":false}` OK |
| 15 | condition | `"record.tax_code == null \|\| record.tax_code ==
''"` | `record.tax_code == null \|\| record.tax_code == ''` |
`{"tax_code":null}` | `{"ok":true,"value":true}` OK |
`{"tax_code":"VAT-1"}` | `{"ok":true,"value":false}` OK |
| 16 | when | `"record.user_role == 'manager'"` | `record.user_role ==
'manager'` | `{"user_role":"manager"}` | `{"ok":true,"value":true}` OK |
`{"user_role":"rep"}` | `{"ok":true,"value":false}` OK |
| 17 | condition | `'record.approval_amount > 50000'` |
`record.approval_amount > 50000` | `{"approval_amount":75000}` |
`{"ok":true,"value":true}` OK | `{"approval_amount":10000}` |
`{"ok":true,"value":false}` OK |
| 18 | when | `"record.type == 'enterprise'"` | `record.type ==
'enterprise'` | `{"type":"enterprise"}` | `{"ok":true,"value":true}` OK
| `{"type":"smb"}` | `{"ok":true,"value":false}` OK |
| 19 | condition | `'record.amount > 100000 && record.approval == null'`
| `record.amount > 100000 && record.approval == null` |
`{"amount":150000,"approval":null}` | `{"ok":true,"value":true}` OK |
`{"amount":150000,"approval":"user_1"}` | `{"ok":true,"value":false}` OK
|
**All 19 predicates, extracted from the source and evaluated as the
runtime string a copy-paste produces: true-branch and false-branch both
evaluate as expected.**
### The `matches()` site specifically — old (broken) literal vs. new
(fixed) literal, same simulation
```
old docblock literal (as it was before this round):
'record.tax_id == null || !matches(record.tax_id, "^\\d{2}-\\d{7}$")'
-> runtime string after JS unescape:
"record.tax_id == null || !matches(record.tax_id, \"^\\d{2}-\\d{7}$\")"
-> ExpressionEngine.evaluate:
{"ok":false,"error":{"kind":"parse","message":"Invalid escape sequence: \\d ..."}}
new docblock literal (this round):
'record.tax_id == null || !matches(record.tax_id, "^[0-9]{2}-[0-9]{7}$")'
-> runtime string after JS unescape:
"record.tax_id == null || !matches(record.tax_id, \"^[0-9]{2}-[0-9]{7}$\")"
-> ExpressionEngine.evaluate (true record {tax_id:null}): {"ok":true,"value":true}
-> ExpressionEngine.evaluate (false record {tax_id:'12-3456789'}): {"ok":true,"value":false}
-> ExpressionEngine.evaluate (wrong-shape record {tax_id:'1234'}): {"ok":true,"value":true}
-> ExpressionEngine.evaluate (wrong-shape {tax_id:'not-a-tax-id'}): {"ok":true,"value":true}
```
The old literal reproduces, exactly, the `Invalid escape sequence: \d`
failure the review measured; the new literal evaluates correctly on all
four probes.
## dist confirmation (H2)
Rebuilt `@objectstack/spec` after this round's edit. `grep -c` on
`packages/spec/dist/object.zod-*.d.ts`: every one of the 19 new strings
present exactly once; every one of the 19 original broken strings absent
(checked both `\d` escape variants of the `matches()` predicate — `\\d`
and `\d` — zero occurrences of either).
## Local verification
- `pnpm --filter @objectstack/spec build && pnpm --filter
@objectstack/formula build` — both build clean.
- `pnpm --filter @objectstack/spec check:generated` — 15/15 generated
artifacts up to date.
- `pnpm --filter @objectstack/spec exec vitest run
src/data/validation.test.ts` — 72/72 tests pass.
- `pnpm --filter @objectstack/spec run typecheck` — clean; the 255
pre-existing debt errors are the shrink-only ledger, untouched by this
diff.
- The three changeset gates (`check-empty-changeset`,
`check-adr-0087-registration`, `check-changeset-no-major`) — green
against the corrected changeset.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran ran.list`: **82 derived families, 79 run (all green), 3
NOT-MEASURED** (`check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:type-check-debt` — each refuses its
own prerequisite without the full workspace build closure, out of this
card's local scope, left to CI's Build Core lane). 0 unrun.
## Acceptance notes
- `content/docs/protocol/objectql/index.mdx:186` (`condition:
"account_type = 'Enterprise' AND contract_value = null"`) carries the
same non-CEL spelling in an unrelated docs page; already recorded,
unresolved, in
`docs/audits/2026-06-handwritten-docs-accuracy-followups.md`. Confirmed
still out of scope by this round's review; left untouched.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5983d11 commit e0f17a3
3 files changed
Lines changed: 75 additions & 40 deletions
File tree
- .changeset
- packages/spec/src/data
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
511 | 511 | | |
512 | 512 | | |
513 | 513 | | |
514 | | - | |
| 514 | + | |
515 | 515 | | |
516 | 516 | | |
517 | 517 | | |
518 | 518 | | |
519 | | - | |
| 519 | + | |
520 | 520 | | |
521 | 521 | | |
522 | 522 | | |
| |||
551 | 551 | | |
552 | 552 | | |
553 | 553 | | |
554 | | - | |
| 554 | + | |
555 | 555 | | |
556 | 556 | | |
557 | 557 | | |
558 | 558 | | |
559 | | - | |
| 559 | + | |
560 | 560 | | |
561 | 561 | | |
562 | 562 | | |
563 | 563 | | |
564 | | - | |
| 564 | + | |
565 | 565 | | |
566 | 566 | | |
567 | 567 | | |
| |||
592 | 592 | | |
593 | 593 | | |
594 | 594 | | |
595 | | - | |
| 595 | + | |
596 | 596 | | |
597 | 597 | | |
598 | 598 | | |
599 | 599 | | |
600 | | - | |
| 600 | + | |
601 | 601 | | |
602 | 602 | | |
603 | 603 | | |
| |||
609 | 609 | | |
610 | 610 | | |
611 | 611 | | |
612 | | - | |
| 612 | + | |
613 | 613 | | |
614 | 614 | | |
615 | 615 | | |
616 | 616 | | |
617 | | - | |
| 617 | + | |
618 | 618 | | |
619 | 619 | | |
620 | 620 | | |
621 | 621 | | |
622 | 622 | | |
623 | | - | |
| 623 | + | |
624 | 624 | | |
625 | 625 | | |
626 | 626 | | |
| |||
632 | 632 | | |
633 | 633 | | |
634 | 634 | | |
635 | | - | |
| 635 | + | |
636 | 636 | | |
637 | 637 | | |
638 | 638 | | |
639 | 639 | | |
640 | | - | |
| 640 | + | |
641 | 641 | | |
642 | 642 | | |
643 | 643 | | |
| |||
701 | 701 | | |
702 | 702 | | |
703 | 703 | | |
704 | | - | |
| 704 | + | |
705 | 705 | | |
706 | 706 | | |
707 | 707 | | |
708 | 708 | | |
709 | | - | |
| 709 | + | |
710 | 710 | | |
711 | 711 | | |
712 | 712 | | |
713 | 713 | | |
714 | 714 | | |
715 | | - | |
| 715 | + | |
716 | 716 | | |
717 | 717 | | |
718 | 718 | | |
| |||
724 | 724 | | |
725 | 725 | | |
726 | 726 | | |
727 | | - | |
| 727 | + | |
728 | 728 | | |
729 | 729 | | |
730 | 730 | | |
731 | 731 | | |
732 | | - | |
| 732 | + | |
733 | 733 | | |
734 | 734 | | |
735 | 735 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
375 | 375 | | |
376 | 376 | | |
377 | 377 | | |
378 | | - | |
| 378 | + | |
379 | 379 | | |
380 | 380 | | |
381 | 381 | | |
382 | 382 | | |
383 | 383 | | |
384 | | - | |
| 384 | + | |
385 | 385 | | |
386 | 386 | | |
387 | 387 | | |
388 | | - | |
| 388 | + | |
389 | 389 | | |
390 | 390 | | |
391 | 391 | | |
392 | 392 | | |
393 | 393 | | |
394 | 394 | | |
395 | | - | |
| 395 | + | |
396 | 396 | | |
397 | 397 | | |
398 | 398 | | |
399 | 399 | | |
400 | 400 | | |
401 | | - | |
| 401 | + | |
402 | 402 | | |
403 | 403 | | |
404 | 404 | | |
405 | | - | |
| 405 | + | |
406 | 406 | | |
407 | 407 | | |
408 | 408 | | |
409 | 409 | | |
410 | 410 | | |
411 | 411 | | |
412 | | - | |
| 412 | + | |
413 | 413 | | |
414 | 414 | | |
415 | 415 | | |
416 | 416 | | |
417 | 417 | | |
418 | | - | |
| 418 | + | |
419 | 419 | | |
420 | 420 | | |
421 | 421 | | |
422 | 422 | | |
423 | 423 | | |
424 | | - | |
| 424 | + | |
425 | 425 | | |
426 | 426 | | |
427 | 427 | | |
| |||
432 | 432 | | |
433 | 433 | | |
434 | 434 | | |
435 | | - | |
| 435 | + | |
436 | 436 | | |
437 | 437 | | |
438 | 438 | | |
439 | 439 | | |
440 | 440 | | |
441 | | - | |
| 441 | + | |
442 | 442 | | |
443 | 443 | | |
444 | 444 | | |
445 | 445 | | |
446 | 446 | | |
447 | | - | |
| 447 | + | |
448 | 448 | | |
449 | 449 | | |
450 | 450 | | |
| |||
455 | 455 | | |
456 | 456 | | |
457 | 457 | | |
458 | | - | |
| 458 | + | |
459 | 459 | | |
460 | 460 | | |
461 | 461 | | |
462 | 462 | | |
463 | | - | |
| 463 | + | |
464 | 464 | | |
465 | 465 | | |
466 | 466 | | |
467 | 467 | | |
468 | 468 | | |
469 | | - | |
| 469 | + | |
470 | 470 | | |
471 | 471 | | |
472 | 472 | | |
473 | 473 | | |
474 | | - | |
| 474 | + | |
475 | 475 | | |
476 | 476 | | |
477 | 477 | | |
478 | 478 | | |
479 | 479 | | |
480 | 480 | | |
481 | | - | |
| 481 | + | |
482 | 482 | | |
483 | 483 | | |
484 | 484 | | |
485 | 485 | | |
486 | 486 | | |
487 | | - | |
| 487 | + | |
488 | 488 | | |
489 | 489 | | |
490 | 490 | | |
491 | | - | |
| 491 | + | |
492 | 492 | | |
493 | 493 | | |
494 | 494 | | |
495 | 495 | | |
496 | 496 | | |
497 | 497 | | |
498 | | - | |
| 498 | + | |
499 | 499 | | |
500 | 500 | | |
501 | 501 | | |
502 | 502 | | |
503 | 503 | | |
504 | | - | |
| 504 | + | |
505 | 505 | | |
506 | 506 | | |
507 | 507 | | |
| |||
525 | 525 | | |
526 | 526 | | |
527 | 527 | | |
528 | | - | |
| 528 | + | |
529 | 529 | | |
530 | 530 | | |
531 | 531 | | |
532 | | - | |
| 532 | + | |
533 | 533 | | |
534 | 534 | | |
535 | 535 | | |
| |||
0 commit comments