@revturbine/sdk 0.11.18 → 0.11.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -47,6 +47,96 @@ also require a changelog entry.
47
47
 
48
48
  ---
49
49
 
50
+ ## 0.11.20
51
+
52
+ ### One entitlement answer for browser and server, with Entitlement Mirroring; Access Gates only fire while denied (D-60, D-61)
53
+
54
+ **What changed.** Behaviour change in every port. Browser and server now run
55
+ the same evaluation on the same inputs: the browser is trusted for
56
+ interaction, and the server verifies.
57
+
58
+ - **One effective entitlement (ts/py/rs/web).** Every entitlement check runs
59
+ the shared core evaluator over the Playbook and the user context (plan,
60
+ segments, usage, reverse-trial grants), then merges app-supplied data. Server
61
+ runtimes previously used a separate rule path that ignored reverse-trial
62
+ grants and allowed everything when the Playbook had no entitlement rules.
63
+ **Server-side results change for the live design partner:** a configured
64
+ entitlement with no rule for the user's plan is now denied on the server
65
+ too, and reverse-trial grants are honoured there.
66
+ - **Entitlement Mirroring.** `UserContext.entitlements` accepts a boolean or a
67
+ grant (`status`, `limit`, `used`), and an app entitlement provider's
68
+ entries count as app data. By default the app's data wins;
69
+ `entitlementMerge` (`entitlement_merge` in Python and Rust) sets provider-level
70
+ `precedence` and per-field overrides. Browser-supplied data shapes the UI;
71
+ the server re-check stays authoritative.
72
+ - **Unknown entitlement handles fail closed.** A handle neither the Playbook
73
+ nor the app knows is denied with `entitlement_not_in_playbook`, warned once,
74
+ and reported (browser telemetry; `onUnknownEntitlement` on the server).
75
+ 0.11.19 returned `entitlement_not_found_default_allow`.
76
+ - **Access Gates (D-60).** A gate placement appears only in an Access Gate
77
+ slot and fires only while its entitlement is denied or limited (a
78
+ tier-scoped gate below its tier counts as limited). Placement gates read the
79
+ same effective entitlements `checkEntitlement` returns.
80
+ - **Reason codes.** New entitlement reason `entitlement_mirrored`; new
81
+ placement reasons `entitlement_not_denied` and `gate_outside_access_gate`.
82
+ - **Scaffold pin 0.1.402 → 0.1.409** (revturbine-scaffold #462, #465
83
+ and #468; vendored `types.py` / `types.rs` refreshed).
84
+
85
+ **Landed in** 0.11.20. **Fail-closed in** 0.11.20.
86
+
87
+ **Proving tests.** `web-sdk/entitlement-mirroring.test.ts`; the D-60/D-61
88
+ blocks in each port's resolver and runtime tests; parity fixtures
89
+ `placement_slot_selection_is_category_first` and
90
+ `placement_category_interactions`.
91
+
92
+ ## 0.11.19
93
+
94
+ ### Placement decisions rank category first; caps run before the pick; overall presentation cap rules enforced (D-59, BL-0536)
95
+
96
+ **What changed.** Behaviour change in every port; no public signature changes.
97
+
98
+ - **Ranking (ts/py/rs).** When several placements qualify for one slot, the
99
+ decision now ranks by category first — Access Gates, then Fixed, then
100
+ usage/credit/seat + trial alerts, then conversion/retention nudges — and
101
+ then within the category: two-stage urgency for alerts (trial transition,
102
+ then limit reached, then approaching; then proximity), drag order
103
+ otherwise. 0.11.18 took the first eligible placement in list order (D-34),
104
+ so an "approaching" warning listed above a "limit reached" alert used to
105
+ win; now the limit-reached alert wins. Equal drag order across the two
106
+ categories of a shared tier resolves Usage before Trials and Conversion
107
+ before Retention. Among fired usage thresholds on one entitlement, the
108
+ highest wins.
109
+ - **Caps before the pick (web).** A placement over its per-payload cap or an
110
+ overall presentation cap is skipped and the next-ranked placement is shown.
111
+ 0.11.18 picked first, then hid the slot. A cached decision whose winner is
112
+ now capped re-resolves instead of blanking the slot.
113
+ - **Payload caps are enforced (web).** Playbook-authored `max_per_period` /
114
+ `cooldown_days` never reached the cap check; they now do. A placement with
115
+ authored caps may now stop showing where 0.11.18 kept showing it.
116
+ - **Overall presentation cap rules (web).** The Placement Settings cap rules
117
+ and session cooldown (`placement_settings[].global_frequency_cap`) are
118
+ enforced for conversion/retention placements, persisted per user;
119
+ `session` periods count from the tab session's start.
120
+ - **Access Gate slots (all ports + web).** A slot that asks for an
121
+ entitlement is answered only by that entitlement's placements; with none
122
+ authored, the decision is `no_gate_for_entitlement` and
123
+ `<AccessGateSurfaceSlot>` renders the new `<AccessDeniedPlaceholder>`
124
+ unless the app passes `deniedFallback`. 0.11.18 showed any other gate on
125
+ the same template.
126
+ - **Reason codes.** New placement reasons `no_gate_for_entitlement`,
127
+ `suppressed_by_presentation_cap` and `suppressed_by_system_cooldown`
128
+ (added to the reason-code baseline).
129
+ - **Scaffold pin 0.1.383 → 0.1.402** (`@revt-eng/core` with revturbine-scaffold #460; vendored `types.py` / `types.rs` refreshed).
130
+
131
+ **Landed in** 0.11.19. **Fail-closed in** 0.11.19.
132
+
133
+ **Proving tests.** Parity fixtures `placement_slot_selection_is_category_first`,
134
+ `placement_limit_reached_beats_approaching` and
135
+ `placement_shared_tier_tie_resolves_by_category` (they replace
136
+ `placement_slot_selection_is_entry_order` and
137
+ `placement_first_eligible_beats_urgency`); `web-sdk/presentation-cap-rules.test.ts`;
138
+ `web-sdk/placements/AccessGateSurfaceSlot.placeholder.test.tsx`.
139
+
50
140
  ## 0.11.18
51
141
 
52
142
  ### Scaffold pin 0.1.383: interaction decision context moves onto core's input type; `growth_signal_observed` retired (plan 282 TASK-9 follow-up, BL-0463)
@@ -293,6 +293,16 @@ type JsonValue = string | number | boolean | null | JsonValue[] | {
293
293
  [key: string]: JsonValue;
294
294
  };
295
295
 
296
+ /** A mirrored grant on `UserContext.entitlements`: a boolean, or a grant-shaped record. */
297
+ type MirroredEntitlement = boolean | {
298
+ status?: string;
299
+ allowed?: boolean;
300
+ limit?: number;
301
+ used?: number;
302
+ remaining?: number;
303
+ reason?: string;
304
+ };
305
+
296
306
  /**
297
307
  * User context lifecycle — pure functions for building, merging,
298
308
  * and deriving targeting state from user context + RevTurbineConfig.
@@ -315,7 +325,12 @@ interface UserContextSnapshot {
315
325
  */
316
326
  plan?: string | UserPlanContext;
317
327
  custom?: Record<string, JsonValue>;
318
- entitlements?: Record<string, boolean>;
328
+ /**
329
+ * App-supplied entitlement data keyed by handle — a boolean or a grant
330
+ * (D-61 Entitlement Mirroring). Merged with the Playbook evaluation by the
331
+ * effective-entitlement core; flattened into segment traits as a boolean.
332
+ */
333
+ entitlements?: Record<string, MirroredEntitlement>;
319
334
  usage?: Record<string, UserUsageEntry>;
320
335
  personalization?: Record<string, JsonValue>;
321
336
  /** Canonical experiment-handle to variant-handle assignments. */