15 Commits
Author SHA1 Message Date
Antonio PallaresandCursor 863a4b1f26 Add rc.regexReplace custom operator (#7558)
* Add rc.regexReplace custom operator

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

* Place the replacement template divergence on the right axis

The note blamed the three regex engines, but the template is parsed by
each platform's own API and never reaches ICU, so this is the one part of
the feature where the two devices differ rather than agreeing against
Funnels. Records what each actually does with $& and with a $1 the
pattern cannot supply.

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

* State the replacement contract without surveying the engines

What a reader needs is that the replacement is taken literally; which
template syntax each engine would otherwise have honoured is background
that belongs with whoever authors the patterns.

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

* Compose rc.regexReplace with an operator that still exists

The fixture proving a replaced string is usable downstream fed it to
rc.length, which main has since removed. rc.upper keeps the point of the
case without depending on an operator that is gone.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 13:09:59 +02:00
Antonio PallaresandCursor fd8441097a Add rc.regexExtract custom operator (#7557)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 13:09:58 +02:00
Antonio PallaresandCursor aa30cdcf1c Add rc.regexMatch custom operator (#7556)
* Add rc.regexMatch custom operator

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

* Report why a regex pattern failed to compile

The engine discarded the compile error and said only that the pattern was
bad, which leaves whoever authored the rule to find the mistake by eye.

NSRegularExpression has little to add today: every syntax failure is the
same NSCocoaErrorDomain 2048 whose description restates the pattern. The
catch is here so the message keeps its shape across the engines, where
the JVM names the fault and points at the offset.

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

* Name the engines the regex operators actually run on

The divergence list said ICU here and java.util.regex on Android, which
is wrong: Android's java.util.regex has wrapped ICU4C since 2.3, so both
devices run ICU and Funnels is the outlier. Read the old note and you
would expect \d to behave differently on the two devices when it behaves
differently in Funnels instead.

Adds the rule the old list was missing — ICU rejects a bare literal }
that JS accepts — and the one genuine device difference, unknown escapes.

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

* Pin how the regex operators read an empty input

An empty input was untested, and it is the input a variable lookup is
most likely to produce by accident. All three engines agree: the string
is still searched, so a pattern that can match nothing succeeds on it
and one that needs a character does not.

The empty *pattern* is deliberately not fixtured. ICU refuses to compile
it while JS reads it as a match at every position, so it is a divergence
rather than a shared rule, and it is now recorded as one.

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

* Delegate regex syntax to the platform's own documentation

The divergence list restated what each engine does, which the vendor
pages already document and do more reliably than a comment that has to be
kept true by hand. The syntax reference now lives behind a link, leaving
only what this file decides for itself.

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

* Name only the pattern when a regex fails to compile

The Android branch hand-assembled the first line of PatternSyntaxException's
own message, and its iOS counterpart appended a localizedDescription that
only repeats the pattern back. Neither is asserted anywhere, and the useful
half is OpenJDK phrasing that a device's ICU-backed engine words differently.

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

* Bind both regex operands before reading them

The arity guard proves the count matches what the caller asked for, not
that it reaches two, so a caller passing a smaller count would index past
the end. An out-of-bounds read traps, and a trap is the one failure the
engine cannot hand back as an error.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 13:09:58 +02:00
Antonio PallaresandCursor 8f4570355e Remove the rc.length and rc.indexOf custom operators (#7568)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 17:13:36 +02:00
Antonio PallaresandCursor cac918ca14 Add rc.indexOf custom operator (#7555)
* Add rc.indexOf custom operator

Arrays get a position, which nothing else expressed: in answers whether
a value is present, never where it sits.

Strings report the position through LengthOperator.stringLength, the
same function behind rc.length, so both operators always measure in one
unit and a later change to that unit moves them together.

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

* Frame stringLength as the engine's single string measurement

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

* Trim rc.indexOf doc comment

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

* Move stringLength out of the rc.length operator

It is the engine's measurement, not that operator's, and rc.indexOf had
to reach across to another operator to state a position in the same
unit. Mirrors the same move in purchases-android.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 09:58:31 +00:00
Antonio Pallares d5986b0f44 Add rc.let custom operator (#7506) 2026-09-01 09:50:17 +02:00
Antonio PallaresandCursor 0712031229 Add rc.slice custom operator (#7505)
* Add rc.sortBy custom operator

Orders an array by a key computed per item, ascending, with ties keeping
their input order. Keys must be all strings or all numbers so the two
engines never have to invent a cross-type ordering.

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

* Add rc.slice custom operator

Takes a run of elements out of an array, with the start and length
semantics substr already uses for strings. Pairs with rc.sortBy to
express top-N without a reduce carrying a compound accumulator.

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

* Exempt the custom operator dispatcher from cyclomatic complexity

The tenth operator pushed the switch past the limit. Operators.dispatch
carries the same exemption for the same reason.

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

* Reject non-finite rc.sortBy keys

A NaN key compares false in both directions, so every pair tied and the
input came back unsorted with no error. Negating a string key to attempt
a descending sort produced exactly that.

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

* Note ascending order in the rc.sortBy summary

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

* Trim the rc.sortBy doc comment

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

* Trim the rc.sortBy stability comment

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

* Drop the dispatcher comment above the lint annotation

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

* Scope the substr hint to string input in rc.slice

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

* Pin rc.slice index and source type strictness in fixtures

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

* Reach the float index branch through arithmetic in rc.slice fixtures

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

* Order rc.sortBy string keys by UTF-16 code unit

Swift's < orders by Unicode scalar and treats canonically equivalent
strings as equal, while JS and Kotlin order by UTF-16 code unit. The two
disagree once a scalar above the surrogate range meets an astral one, so
the same rule would sort differently on iOS and Android.

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

* Keep the rc.sortBy ordering fixtures platform-neutral

The fixture files are byte-identical across the iOS and Android engines, so
a description that explains the contract in terms of one host language's
string comparison does not read correctly in the other repository.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 06:52:53 +00:00
Antonio PallaresandCursor 4bd110f723 Add rc.sortBy custom operator (#7504)
* Add rc.sortBy custom operator

Orders an array by a key computed per item, ascending, with ties keeping
their input order. Keys must be all strings or all numbers so the two
engines never have to invent a cross-type ordering.

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

* Reject non-finite rc.sortBy keys

A NaN key compares false in both directions, so every pair tied and the
input came back unsorted with no error. Negating a string key to attempt
a descending sort produced exactly that.

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

* Note ascending order in the rc.sortBy summary

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

* Trim the rc.sortBy doc comment

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

* Trim the rc.sortBy stability comment

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

* Order rc.sortBy string keys by UTF-16 code unit

Swift's < orders by Unicode scalar and treats canonically equivalent
strings as equal, while JS and Kotlin order by UTF-16 code unit. The two
disagree once a scalar above the surrogate range meets an astral one, so
the same rule would sort differently on iOS and Android.

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

* Keep the rc.sortBy ordering fixtures platform-neutral

The fixture files are byte-identical across the iOS and Android engines, so
a description that explains the contract in terms of one host language's
string comparison does not read correctly in the other repository.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 08:10:18 +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 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