SIGN IN SIGN UP

Raise an error for unresolved variables instead of degrading to null (#4043)

Port of RevenueCat/purchases-ios#7460. The predicate fixtures are
byte-identical to the iOS ones.

### Motivation

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

### Description

An absent variable means "unknown", which is not the same answer as
"no". The lookup now throws `UnresolvedVariable(path)` and the caller
decides what an unanswerable rule means. Both callers already drew that
distinction, so neither needed changing: `LocalRulesEvaluator` keeps
walking later rules and fails only if none matched, and
`CheckpointWorkflowResolverImpl` reports `CONFIGURATION_UNAVAILABLE`
rather than `NO_MATCH`.

- Rules that tolerate absence opt in via the spec's own `{"var":
["path", default]}`. A key present but holding `null` is a known value
and still resolves; `missing`/`missing_some` still probe absence.
- This diverges from `json-logic-js`, so the same rule that errors here
answers `false` on web.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Changes core audience/checkpoint matching semantics so rules that
previously coerced missing variables to false/true may now fail
evaluation or map to CONFIGURATION_UNAVAILABLE instead of NO_MATCH.
> 
> **Overview**
> **`var` / `rc.rootVar` no longer treat absent paths as `null`.** When
a path is missing and the rule does not supply `{"var": ["path",
default]}`, evaluation fails with
`EvaluationException.UnresolvedVariable(path)` instead of logging a
warning and returning `null` (which could silently make comparisons like
`expires > now` true via coercion).
> 
> Explicit **`null` in scope still resolves normally**; **`missing` /
short-circuited branches** are unchanged. This **intentionally diverges
from `json-logic-js`**.
> 
> **Callers already distinguish “unanswerable” from “no match”:**
checkpoint resolution now expects **`CONFIGURATION_UNAVAILABLE`** when a
required custom variable was omitted (including under negated
audiences), and local rule matching surfaces predicate errors instead of
treating missing dimensions as a ordinary non-match.
> 
> Coverage is updated across **predicate JSON fixtures** (new
`unresolved_variable.json`, revised iteration-scope cases),
**conformance runner** (`unresolvedVariable` expected errors), and
**integration tests**.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ee8f1caddbaf48c06614acadd8e591bf84499f04. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
A
Antonio Pallares committed
84faa2be1b0c2ad8b33b7a73224036ecae094edb
Parent: 64609c7
Committed by GitHub <noreply@github.com> on 8/25/2026, 8:08:15 AM