Commit Graph
10 Commits
Author SHA1 Message Date
Antonio PallaresandCursor 343ba2221e Name dropping build metadata unvalidated as a concession (#7502)
Raised in review on the Android port: everything from the first + on is
discarded without being checked, so "1.0.0++junk" compares as "1.0.0".
That is intended, but it sat outside the list of concessions and read as
an oversight. A fixture now pins it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 16:45:18 +02:00
Antonio PallaresandCursor bc1186542e Refresh two doc comments that outlived their examples (#7498)
rc.length justifies throwing with a missing var, which cannot reach the
operator any more: the lookup raises first, as
length_missing_variable_fails_at_the_lookup pins. The reason now rests on
a key that is present but holds null, the case that does reach it.

objectList tells providers a record must carry everything a predicate
about it needs. rc.rootVar reaches the snapshot from inside an iteration
scope, so that is no longer the only option.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 11:41:23 +02:00
Antonio PallaresandCursor f9a42756ee Add rc.semverCompare custom operator (#7472)
Version targeting is the most common audience condition, and the built-in
< / > compare version strings lexicographically, ranking "10.0.0" below
"9.0.0".

rc.semverCompare returns -1, 0, or 1 so rules compose it with the existing
comparison operators. Precedence follows semver 2.0.0: numeric core first,
then prerelease identifiers, with build metadata excluded. Unparseable
input and inexact arity throw typeMismatch rather than reporting "equal",
which would quietly match every rule built on it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 11:26:54 +02:00
Antonio PallaresandCursor 267322158d Add rc.entries and rc.fromEntries custom operators (#7435)
* Add Scope plumbing and custom operator extension point

Thread evaluation scope through the rules engine and delegate unknown
operators to an empty CustomOperators dispatcher, without changing behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix project.pbxproj: restore RewardedAdTrackingMetadata entries

The RulesEngine registration edit accidentally dropped four
RewardedAdTrackingMetadata.swift entries from the Xcode project.
Restore them and normalize CustomOperators identifiers to 24 hex chars.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add rc.entries and rc.fromEntries for keyed object iteration

Introduce pair conversion operators so filter/map/some can iterate
subscriptions, entitlements, and other keyed objects via existing iteration ops.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Use firstArgEvaluated for rc.entries argument convention

Promote the shared unary-arg helper to Operators and align rc.entries
and rc.fromEntries with json-logic-js spread semantics. Update fixtures
for wrapped array operands and add a multi-arg interop case.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Promote firstArgEvaluated to shared Operators helpers

Move the unary-arg helper from LogicOperators to Operators so custom
operators can use the same json-logic-js spread convention.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address review comments on custom operator extension point

Drop the cross-engine registration note from CustomOperators, and restore
the single-line var extra-args warning that was reflowed without changing
its text.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Reformat rc.entries fixtures so subexpressions read as units

The one-token-per-line formatting spread predicates over dozens of lines,
so a comparison like {"===": [{"var": "0"}, "a"]} took seven lines and its
shape was impossible to see at a glance. Subtrees that fit within 120
columns are now inlined, which keeps each operator and its operands
together on one line while deeply nested structures still expand.

No fixture content changed; the document is byte-identical after parsing,
key order included.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim EntriesOperators docs of cross-repo references

Drops the cross-engine contract paragraph and the Audience.swift pointer.
Both described obligations outside this file, which will drift as those
places change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Make rc.entries throw typeMismatch on non-object, non-array input

Returning [] with a warning let a misaimed rule keep evaluating: a some
over an empty list is simply false, so a rule pointed at a string or a
missing variable reported a confident answer rather than a failure. Null,
undefined, bool, number and string now throw, matching rc.length.

Strings in particular do not yield the character pairs JS Object.entries
produces. This is JSON, not JS, and a string here means the rule targets
the wrong field.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Make rc.fromEntries throw typeMismatch on malformed input

Skipping bad entries produced a partial object indistinguishable from a
legitimate one, letting a rule match on whichever keys happened to survive.
Non-array arguments and entries that are not two-element [key, value] pairs
now throw, so rc.fromEntries fails the same way as rc.entries.

Entry arity is checked exactly: a one-element entry no longer defaults its
value to null, and an over-long entry no longer silently ignores the extra
elements the way JS Object.fromEntries does.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 10:57:04 +00:00
Antonio PallaresandCursor 66659ab8ef Raise an error for unresolved variables instead of degrading to null (#7460)
* Raise an error for unresolved variables instead of degrading to null

A `var` lookup that found nothing returned `null`, following the JSON
Logic spec. That null then coerces: `0` in a numeric comparison, `""`
in a string one. A rule that reads a dimension the SDK never supplied
therefore still produces an answer, and often the wrong one — a missing
`now` turns `{">": [expires, now]}` into `expires > 0`, which is true
for every subscription that ever existed.

An absent variable means "unknown", and unknown is not the same answer
as "no". The lookup now throws `EvaluationError.unresolvedVariable`
carrying the path, so the rule fails loudly and the SDK decides what an
unanswerable rule means. `LocalRulesEvaluator` already distinguishes an
error from a non-match: it keeps walking later rules and only throws if
none matched.

Absence a rule genuinely tolerates is opted into with the spec's own
escape hatch, `{"var": ["path", default]}`. A key that is present but
holds `null` is a known value and still resolves. `missing` and
`missing_some` exist to probe for absence and keep reporting it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Stop pinning the rendered path for the oversized float fixture

iOS renders `1e19` as `1e+19` and Android as `1.0E19`, so pinning the
path would keep this fixture from being shared verbatim between the two
engines. The fixture is about the formatter not trapping on a value past
Int64.max, not about how that magnitude is spelled.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply the unresolved-variable contract to rc.lower and rc.upper

`rc.lower` arrived on main while this branch was open, carrying a
fixture that pinned a missing variable as a `typeMismatch` from the
operator. The lookup now fails first, so the error names the unresolved
path instead. Its companion pins that an explicit null still reaches
`rc.lower` and is refused there.

The operator's rationale for not coercing cited a missing `var`
resolving to null, which is no longer how that ends. It refuses
non-strings because an explicit null would lower to "null" and compare
equal to it, which is the part that still holds.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim the warn doc comment

Co-authored-by: Cursor <cursoragent@cursor.com>

* Name the expected-error path field for the case it belongs to

`path` sits next to `kind` and `operator` with nothing to say which
error it qualifies. The JSON key stays `path`, since there it appears
alongside `"error": "unresolvedVariable"` and reads unambiguously.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Describe the rc.rootVar fixture by what it does today

The description narrated the previous null-degrading behavior instead of
the case the fixture pins.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Give the absent-attribute predicates a default

Two non-matching predicates read attribute paths that resolve to nothing,
which is now an error rather than a false-y null. The Android counterpart
already carried this change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Report an unanswerable audience as unavailable rather than no match

Two checkpoint tests pinned the null-degrading behavior: an audience
reading a dimension no provider supplies was merely false, so it read as
noMatch, and under negation it matched everyone.

The lookup throws now, which routes both through the resolver's existing
decision that an audience the SDK failed to evaluate is not the same
answer as an audience the customer falls outside of.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 12:38:49 +02:00
Antonio PallaresandCursor ee1e91f0aa Add rc.split custom operator (#7473)
Splits a string into an array of fields, keeping empty ones, so rules can
target locale subtags, product-id namespaces and similar composite values.

A var path cannot index the result of an expression, so a positional field
is read by wrapping the array in a one-element list and letting an
iteration operator rebind scope to it. The array is also usable directly
by in and by the iteration operators.

Fields are never coerced to numbers, and a non-string operand or empty
separator throws typeMismatch rather than splitting into characters.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 09:47:39 +02:00
Antonio PallaresandCursor 8ba422da6a Add rc.lower and rc.upper custom operators (#7436)
* Add Scope plumbing and custom operator extension point

Thread evaluation scope through the rules engine and delegate unknown
operators to an empty CustomOperators dispatcher, without changing behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix project.pbxproj: restore RewardedAdTrackingMetadata entries

The RulesEngine registration edit accidentally dropped four
RewardedAdTrackingMetadata.swift entries from the Xcode project.
Restore them and normalize CustomOperators identifiers to 24 hex chars.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Promote firstArgEvaluated to shared Operators helpers

Move the unary-arg helper from LogicOperators to Operators so custom
operators can use the same json-logic-js spread convention.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add rc.lower and rc.upper for case-insensitive rules

Locale-independent case conversion via jsString coercion, with predicate
fixtures covering coercion, Unicode behavior, and motivating match cases.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address review comments on custom operator extension point

Drop the cross-engine registration note from CustomOperators, and restore
the single-line var extra-args warning that was reflowed without changing
its text.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Pin locale-independent uppercasing of ASCII i

rc.lower already had a fixture asserting the Unicode default mapping for
the Turkish I, but rc.upper had no equivalent. The uppercase direction is
the more consequential one: Turkish maps i to U+0130, a scalar with no
ASCII counterpart, so a locale-sensitive implementation would silently
change what rules match.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Require string input for rc.lower and rc.upper

Both operators coerced non-strings through jsString, mirroring JS
String(x).toLowerCase(). That makes absent data look like real data: a
missing var resolves to null, so comparing a lowercased path against
"null" would quietly match.

Throw typeMismatch instead, matching rc.length and rc.entries. The four
coercion fixtures are replaced by nine type-error fixtures covering null,
undefined, numbers, booleans, arrays, objects and missing vars, with
rc.upper cases pinning that it shares the guard.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Correct the jsNumberString divergence note

The note claimed the JS mismatch only surfaces through var path coercion
and looseEq, and only for large magnitudes. cat, substr and in stringify
numbers through the same helper, and small values diverge too: 1e-6
renders as "1e-06" rather than "0.000001".

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 07:41:56 +02:00
Antonio PallaresandCursor 2a59bec6e0 Add rc.length operator for strings and arrays (#7437)
* Add Scope plumbing and custom operator extension point

Thread evaluation scope through the rules engine and delegate unknown
operators to an empty CustomOperators dispatcher, without changing behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix project.pbxproj: restore RewardedAdTrackingMetadata entries

The RulesEngine registration edit accidentally dropped four
RewardedAdTrackingMetadata.swift entries from the Xcode project.
Restore them and normalize CustomOperators identifiers to 24 hex chars.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Promote firstArgEvaluated to shared Operators helpers

Move the unary-arg helper from LogicOperators to Operators so custom
operators can use the same json-logic-js spread convention.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add rc.length operator for string and array lengths

UTF-16 code-unit count for strings (JS String.length parity), element
count for arrays, typeMismatch for other inputs. Thirteen new fixtures.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address review comments on custom operator extension point

Drop the cross-engine registration note from CustomOperators, and restore
the single-line var extra-args warning that was reflowed without changing
its text.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim rc.length doc comment

Drop the rationale contrasting rc.length's throwing behavior with
rc.entries' lenient empty result.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Pin that rc.length ignores extra arguments

The doc comment claimed extra args are silently ignored but no fixture
covered it. Under single-operand semantics ["abc", "ignored"] would
measure the arg list itself and yield 2, so the fixture fails if the
spread convention regresses.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 07:10:49 +00:00
Antonio PallaresandCursor ad09c032c9 Add rc.rootVar operator for root scope in RulesEngine (#7432)
* Add Scope plumbing and custom operator extension point

Thread evaluation scope through the rules engine and delegate unknown
operators to an empty CustomOperators dispatcher, without changing behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add rc.rootVar operator for nested predicate root access

Introduce the rc.rootVar custom operator and shared resolveVar lookup,
with fixture coverage including falsy-item empty-path discrimination.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix project.pbxproj: restore RewardedAdTrackingMetadata entries

The RulesEngine registration edit accidentally dropped four
RewardedAdTrackingMetadata.swift entries from the Xcode project.
Restore them and normalize CustomOperators identifiers to 24 hex chars.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Promote firstArgEvaluated to shared Operators helpers

Move the unary-arg helper from LogicOperators to Operators so custom
operators can use the same json-logic-js spread convention.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address review comments on custom operator extension point

Drop the cross-engine registration note from CustomOperators, and restore
the single-line var extra-args warning that was reflowed without changing
its text.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 18:07:53 +02:00
Antonio PallaresandCursor 2deacfd190 Add Scope plumbing and custom operator extension point (#7433)
* Add Scope plumbing and custom operator extension point

Thread evaluation scope through the rules engine and delegate unknown
operators to an empty CustomOperators dispatcher, without changing behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix project.pbxproj: restore RewardedAdTrackingMetadata entries

The RulesEngine registration edit accidentally dropped four
RewardedAdTrackingMetadata.swift entries from the Xcode project.
Restore them and normalize CustomOperators identifiers to 24 hex chars.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Promote firstArgEvaluated to shared Operators helpers

Move the unary-arg helper from LogicOperators to Operators so custom
operators can use the same json-logic-js spread convention.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address review comments on custom operator extension point

Drop the cross-engine registration note from CustomOperators, and restore
the single-line var extra-args warning that was reflowed without changing
its text.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 08:04:21 +00:00