@revturbine/sdk 0.10.7 → 0.10.9

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,97 @@ also require a changelog entry.
47
47
 
48
48
  ---
49
49
 
50
+ ## 0.10.9
51
+
52
+ ### The trial-status overlay no longer changes a number's type (BL-0155)
53
+
54
+ **What changed.** The `UserTrialStatus` → `trial_*` PlanProviderState overlay now
55
+ passes each numeric field through on every port, instead of two ports widening
56
+ it to a float. The canonical behaviour is the TS SDK's
57
+ `synthesizeProviderContext` (`web-sdk/customer-side.ts`, the `planTrialFields`
58
+ block), which spreads every `UserTrialStatus` member verbatim: an integer
59
+ `progress_percent` stays an integer on the provider state, and
60
+ `trial_days_total` is `day_number + days_remaining` in the inputs' own kind, so
61
+ `7 + 3` is `14`. The two server ports diverged from it:
62
+
63
+ | field | Python before | Rust before | all three now |
64
+ |---|---|---|---|
65
+ | `progress_percent` → `trial_progress_percent` | `float(v)` | verbatim | verbatim |
66
+ | `days_remaining` → `trial_days_remaining` | `float(v)` | verbatim | verbatim |
67
+ | `usage_consumed` → `trial_usage_consumed` | `float(v)` | verbatim | verbatim |
68
+ | `usage_limit` → `trial_usage_limit` | `float(v)` | verbatim | verbatim |
69
+ | `in_trial` → `trial_active` | `bool(v)` | verbatim | verbatim |
70
+ | derived `trial_days_total` | `float(a) + float(b)` | `f64` sum | `int + int` → `int`; any fractional half → float |
71
+
72
+ **Who this reaches.** Only a host that reads the provider state it built —
73
+ `revturbine::overlay_trial_status_on_plan_provider` on Rust, or the merged
74
+ `PlanProviderState` on Python — and cares whether it holds `70` or `70.0`
75
+ (serializing it, comparing it to an integer, or logging it). No decision
76
+ changes: every reader in the trial-gating layer widens on read, which is exactly
77
+ why the divergence survived. A non-numeric `day_number`/`days_remaining` half
78
+ now derives no total on Python instead of raising, matching Rust's existing
79
+ numeric guard.
80
+
81
+ **Landed in.** `0.10.9`.
82
+
83
+ **Fail-closed in.** `0.10.9` — there was no tolerated-old-shape window; the
84
+ representation simply changes.
85
+
86
+ **Proving test.**
87
+ `server-python/tests/test_trial_overlay_upsert.py::test_overlay_preserves_integer_numeric_representation`
88
+ and `server-rust/src/sdk.rs::overlay_preserves_integer_numeric_representation`
89
+ — both assert the *type*, not just the value, because `100 == 100.0` in Python.
90
+ The parity corpus cannot see this class of divergence (`tests/parity/normalize.*`
91
+ rule 4 collapses integral floats by design); scenario
92
+ `trial_overlay_integer_fields` locks that the three ports still decide
93
+ identically from an all-integer trial status.
94
+
95
+ ---
96
+
97
+ ## 0.10.8
98
+
99
+ ### `{{recommended_plan_name}}` and `{{plan_name}}` no longer reach end users as raw tokens (BL-0121)
100
+
101
+ **What changed.** The placement-decision lane now substitutes the five tokens
102
+ the SDK resolves from the Playbook — `plan_name`, `plan_price`,
103
+ `upgrade_plan_price`, `recommended_plan_handle`, `recommended_plan_name` —
104
+ into `RevTurbinePlacementDecision.content` (and the mirrored
105
+ `decision.output.content`). It previously substituted `plan_price` and
106
+ `upgrade_plan_price` only.
107
+
108
+ `recommended_plan_handle` / `recommended_plan_name` were already derived, by
109
+ the parity-locked `resolveRecommendedPlanTokens` dispatch, and already readable
110
+ via `getPersonalizationTokens()`. Nothing wrote them into rendered content, so
111
+ a placement authored with `"Upgrade to {{recommended_plan_name}}"` shipped the
112
+ literal braces to the end user. Observed on the CybeDefend demo, 2026-09-22.
113
+
114
+ Two properties of the substitution are deliberate:
115
+
116
+ | input | result |
117
+ |---|---|
118
+ | a token the SDK owns but cannot resolve (top-of-ladder user has no next plan) | empty string — the spec's documented empty-token convention |
119
+ | a token the SDK does not own (`{{usage_percent}}`, an app-defined token) | left verbatim, so the React render lane still resolves it |
120
+
121
+ `plan_name` resolves through the plan's `unique_handle`, the matching identity
122
+ — the lookup used to be handed the plan *object*, which by design resolves
123
+ nothing, so `{{plan_name}}` rendered raw for every user identified by
124
+ `plan_handle`.
125
+
126
+ **Landed in.** `0.10.8`.
127
+
128
+ **Not fixed here.** `{{upgrade_plan_price}}` still renders the ANNUAL amount
129
+ for a plan that has both an annual and a monthly variation. Variation
130
+ preference keys off the user's billing cadence, but no supported input carries
131
+ it (`billing_period` is absent from `UserContextSchema` and from the recognized
132
+ `identify()` / `update()` keys), so selection falls through to the
133
+ alphabetically-first variation and `<plan>_annual` always wins. Closing it
134
+ needs a schema field for the cadence and a contract for which period the token
135
+ reflects when the cadence is unknown; both are open on BL-0121.
136
+
137
+ **Proving test.** `web-sdk/customer-side-upgrade-tokens.test.ts`.
138
+
139
+ ---
140
+
50
141
  ## 0.10.7
51
142
 
52
143
  ### The Python and Rust ports stop filtering payloads on `status` (BL-0151)
@@ -21140,6 +21140,29 @@ declare class RevTurbineCustomerSdk {
21140
21140
  getEligibleAddons(): Promise<EligibleAddon[]>;
21141
21141
  private formatEligiblePrice;
21142
21142
  private priceTokensForProviders;
21143
+ /**
21144
+ * The plan-and-recommendation token family the SDK resolves authoritatively
21145
+ * from the Playbook, for substitution into decision content.
21146
+ *
21147
+ * These five are exactly the tokens whose values come from the Playbook's
21148
+ * plan catalogue plus the recommendation strategy, so the SDK — not the
21149
+ * host app — is the only thing that can fill them in. Every other token in
21150
+ * the studio table (usage, trial, seats, credits) is resolved on the React
21151
+ * render lane by `derivePlacementPersonalizationTokens`, which unknown
21152
+ * tokens pass through untouched.
21153
+ */
21154
+ private derivedContentTokens;
21155
+ /**
21156
+ * Substitute the SDK-resolved plan/price/recommendation tokens into a
21157
+ * decision's content.
21158
+ *
21159
+ * Only the tokens in {@link derivedContentTokens} are substituted; any
21160
+ * other `{{token}}` is left verbatim so the render lane (or the host app)
21161
+ * can still resolve it. A token the SDK owns but cannot resolve — a
21162
+ * top-of-ladder user has no recommended plan — becomes an empty string,
21163
+ * which is the spec's documented empty-token convention and is what keeps
21164
+ * raw `{{...}}` braces off the end user's screen.
21165
+ */
21143
21166
  private applyPriceTokens;
21144
21167
  /**
21145
21168
  * Return current SDK policy snapshot.