@revturbine/sdk 0.10.8 → 0.10.10

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,115 @@ also require a changelog entry.
47
47
 
48
48
  ---
49
49
 
50
+ ## 0.10.10
51
+
52
+ ### Trial tokens now reach placement content, not only `getPersonalizationTokens()` (BL-0169)
53
+
54
+ **What changed.** A placement whose copy authors `{{trial_days_remaining}}` or
55
+ `{{trial_days_total}}` now renders the live values. Content interpolation sources
56
+ its token map from the **output content itself**, so a provider-derived token
57
+ only reaches the copy if the resolver writes it onto that content first. The
58
+ usage lane always did (`usage_current`, `usage_limit`, `usage_percent`,
59
+ `usage_remaining`, `reset_date`); the trial lane never did, so a `trial_ending`
60
+ body reading `"{{trial_days_remaining}} days left"` shipped the literal
61
+ `{{trial_days_remaining}}` to the end user — on every port. The two names were
62
+ derived for `getPersonalizationTokens()` all along, which is why the gap survived:
63
+ a host that read the token map saw the right numbers and a host that authored the
64
+ token into copy did not.
65
+
66
+ | | before | now |
67
+ |---|---|---|
68
+ | `getPersonalizationTokens().trial_days_remaining` | live value | live value (unchanged) |
69
+ | content `{{trial_days_remaining}}` / `{{trial_days_total}}` | literal `{{…}}` | live value |
70
+ | `outputContent.trial_days_remaining` / `_total` | absent | written when the plan provider carries a finite number |
71
+
72
+ Identical on all three ports (`ts:local-resolver.ts`,
73
+ `py:core/placements/local_resolver.py`, `rs:placements/static_resolver.rs`): the
74
+ same two token names, a `Number.isFinite` guard, provider state winning over an
75
+ authored content value of the same key, and the value passed through **without
76
+ widening** — `day_number` 7 + `days_remaining` 3 is `trial_days_total` `10`, never
77
+ `10.0` (BL-0155).
78
+
79
+ **Who this reaches.** Any host whose placement copy authors either token. Copy
80
+ that hard-coded one of those keys as a decorative literal now has it overwritten
81
+ by live state — the same precedence the usage lane and
82
+ `derivePlacementPersonalizationTokens` have always had. `{{trial_plan_name}}` and
83
+ `{{trial_features_used}}` remain **not yet wired** by the placement-studio-ui
84
+ spec's own note and are untouched.
85
+
86
+ **Landed in.** `0.10.10`.
87
+
88
+ **Fail-closed in.** `0.10.10` — absence still leaves the raw token standing
89
+ rather than rendering `0`, so a host with no trial provider state sees exactly
90
+ what it saw before.
91
+
92
+ **Proving test.** Parity scenario `trial_tokens_in_content` under
93
+ `"normalize": "preserve-representation"` (byte-locked ts ≡ py ≡ rs), plus
94
+ `py:tests/placements/test_local_resolver.py` and `rs:tests/static_resolver.rs`
95
+ `trial_tokens_are_injected_from_plan_provider_state` and its two neighbours,
96
+ mirroring the scaffold-side `local-resolver.test.ts` cases. Needs
97
+ `@revt-eng/core` 0.1.331 or later (scaffold #382) for the TS half; this release
98
+ pins 0.1.332.
99
+
100
+ ---
101
+
102
+ ## 0.10.9
103
+
104
+ ### The trial-status overlay no longer changes a number's type (BL-0155)
105
+
106
+ **What changed.** The `UserTrialStatus` → `trial_*` PlanProviderState overlay now
107
+ passes each numeric field through on every port, instead of two ports widening
108
+ it to a float. The canonical behaviour is the TS SDK's
109
+ `synthesizeProviderContext` (`web-sdk/customer-side.ts`, the `planTrialFields`
110
+ block), which spreads every `UserTrialStatus` member verbatim: an integer
111
+ `progress_percent` stays an integer on the provider state, and
112
+ `trial_days_total` is `day_number + days_remaining` in the inputs' own kind, so
113
+ `7 + 3` is `14`. The two server ports diverged from it:
114
+
115
+ | field | Python before | Rust before | all three now |
116
+ |---|---|---|---|
117
+ | `progress_percent` → `trial_progress_percent` | `float(v)` | verbatim | verbatim |
118
+ | `days_remaining` → `trial_days_remaining` | `float(v)` | verbatim | verbatim |
119
+ | `usage_consumed` → `trial_usage_consumed` | `float(v)` | verbatim | verbatim |
120
+ | `usage_limit` → `trial_usage_limit` | `float(v)` | verbatim | verbatim |
121
+ | `in_trial` → `trial_active` | `bool(v)` | verbatim | verbatim |
122
+ | derived `trial_days_total` | `float(a) + float(b)` | `f64` sum | `int + int` → `int`; any fractional half → float |
123
+
124
+ **Who this reaches.** Only a host that reads the provider state it built —
125
+ `revturbine::overlay_trial_status_on_plan_provider` on Rust, or the merged
126
+ `PlanProviderState` on Python — and cares whether it holds `70` or `70.0`
127
+ (serializing it, comparing it to an integer, or logging it). No decision
128
+ changes: every reader in the trial-gating layer widens on read, which is exactly
129
+ why the divergence survived. A non-numeric `day_number`/`days_remaining` half
130
+ now derives no total on Python instead of raising, matching Rust's existing
131
+ numeric guard.
132
+
133
+ **Landed in.** `0.10.9`.
134
+
135
+ **Fail-closed in.** `0.10.9` — there was no tolerated-old-shape window; the
136
+ representation simply changes.
137
+
138
+ **Proving test.**
139
+ `server-python/tests/test_trial_overlay_upsert.py::test_overlay_preserves_integer_numeric_representation`
140
+ and `server-rust/src/sdk.rs::overlay_preserves_integer_numeric_representation`
141
+ — both assert the *type*, not just the value, because `100 == 100.0` in Python.
142
+ Scenario `trial_overlay_integer_fields` locks that the three ports still decide
143
+ identically from an all-integer trial status.
144
+
145
+ > **Correction, 2026-09-23 (BL-0158, harness-only — no release).** This entry
146
+ > originally said the parity corpus *cannot* see this class of divergence,
147
+ > because `tests/parity/normalize.*` rule 4 collapses integral floats by design
148
+ > and nothing in the corpus returned the overlaid provider state. Both halves
149
+ > are now closed and the sentence no longer holds:
150
+ > `trial_overlay_integer_fields` drives `resolveProviders` (already shipped on
151
+ > all three runtimes) under a new per-scenario
152
+ > `"normalize": "preserve-representation"` flag that turns the collapse off, so
153
+ > `14` and `14.0` are different bytes and the cross-language byte-diff fails on
154
+ > a widened field. No public API changed; the harness gained the ability to see
155
+ > provider state.
156
+
157
+ ---
158
+
50
159
  ## 0.10.8
51
160
 
52
161
  ### `{{recommended_plan_name}}` and `{{plan_name}}` no longer reach end users as raw tokens (BL-0121)
@@ -13185,8 +13185,8 @@ declare const PlaybookObjectSchema: z.ZodObject<{
13185
13185
  /**
13186
13186
  * The one canonical config schema. Parses BOTH a canonical Playbook and a legacy
13187
13187
  * `export-config.json` (via `normalizeConfigHeaderInput`) into the canonical
13188
- * Playbook shape, so `ExportedConfigSchema` / `RevTurbineConfigSchema` are true
13189
- * aliases of it. `.meta({ id: 'Playbook' })` rides on this preprocess wrapper
13188
+ * Playbook shape, so `RevTurbineConfigSchema` (and its deprecated
13189
+ * `ExportedConfigSchema` alias) are true aliases of it. `.meta({ id: 'Playbook' })` rides on this preprocess wrapper
13190
13190
  * (the underlying object is id-less) so the OpenAPI / JSON-Schema generators emit
13191
13191
  * a single well-formed `Playbook` component. Introspection that needs `.shape`
13192
13192
  * must read `PlaybookObjectSchema`, not this wrapper.
@@ -17164,6 +17164,50 @@ type JsonObject = {
17164
17164
  [key: string]: JsonValue;
17165
17165
  };
17166
17166
 
17167
+ /**
17168
+ * BL-0156 — `playbook` is the canonical option name; `exportedConfig` is a
17169
+ * deprecated alias.
17170
+ *
17171
+ * "ExportedConfig" is dead vocabulary. Plan 118 renamed the domain object to
17172
+ * **Playbook** and plan 104 renamed the schema type to `RevTurbineConfig`, but
17173
+ * the *option* and *parameter* names that carry a Playbook around core still
17174
+ * spelled it `exportedConfig`. This module is the single place that reconciles
17175
+ * the two spellings, mirroring the plan-257 `publicKey` mechanism:
17176
+ *
17177
+ * - the canonical `playbook` wins whenever it is supplied,
17178
+ * - the deprecated `exportedConfig` is still accepted,
17179
+ * - supplying only the alias emits ONE warning naming the canonical option,
17180
+ * - supplying BOTH with *different* Playbooks throws instead of silently
17181
+ * picking one — that combination is always a caller bug.
17182
+ *
17183
+ * Every read site resolves through `resolvePlaybookOption` /
17184
+ * `requirePlaybookOption`, so the precedence cannot drift per call site.
17185
+ *
17186
+ * **Why the warning is not gated on a development build.** Core's ESLint config
17187
+ * blocks the `process` global outright (`no-restricted-globals`) because core
17188
+ * must run unchanged on edge runtimes and in browsers, so core cannot read
17189
+ * `NODE_ENV` the way `web-sdk/build-mode.ts` does. The warning is therefore
17190
+ * emitted once per runtime in every build — a single line, only for callers
17191
+ * still on the deprecated spelling, and it names the fix.
17192
+ *
17193
+ * Aliases are removed in the minor after the SDK 0.11.0 cascade.
17194
+ */
17195
+
17196
+ /**
17197
+ * An options bag that must carry a Playbook, under either the canonical
17198
+ * `playbook` name or the deprecated `exportedConfig` alias.
17199
+ *
17200
+ * Modelled as a union so TypeScript still requires one of the two, exactly as
17201
+ * the single required property did before the rename.
17202
+ */
17203
+ type PlaybookOption = {
17204
+ playbook: RevTurbineConfig;
17205
+ exportedConfig?: RevTurbineConfig;
17206
+ } | {
17207
+ playbook?: RevTurbineConfig;
17208
+ exportedConfig: RevTurbineConfig;
17209
+ };
17210
+
17167
17211
  /**
17168
17212
  * User context lifecycle — pure functions for building, merging,
17169
17213
  * and deriving targeting state from user context + RevTurbineConfig.
@@ -18118,12 +18162,18 @@ interface LocalPlacementDataset {
18118
18162
  type LocalPlacementEntry = NonNullable<RevTurbineConfig['placements']>[number];
18119
18163
  type LocalPlacementPayload = LocalPlacementEntry['payloads'][number];
18120
18164
  type LocalPlacementSurface = LocalPlacementPayload['surfaces'][number];
18121
- /** Backward-compat JSON entry point. */
18122
- interface StaticPlacementResolverOptions {
18165
+ /** Backward-compat JSON entry point, minus the Playbook. */
18166
+ interface StaticPlacementResolverOptionsBase {
18123
18167
  placements: LocalPlacementDataset;
18124
- exportedConfig: RevTurbineConfig;
18125
18168
  impressionHistory?: ImpressionHistory;
18126
18169
  }
18170
+ /**
18171
+ * Options for {@link createStaticPlacementResolver}.
18172
+ *
18173
+ * The Playbook is supplied as `playbook`; `exportedConfig` is a deprecated
18174
+ * alias removed in the minor after the SDK 0.11.0 cascade (BL-0156).
18175
+ */
18176
+ type StaticPlacementResolverOptions = StaticPlacementResolverOptionsBase & PlaybookOption;
18127
18177
  /**
18128
18178
  * Backward-compat JSON entry. Thin wrapper around the unified
18129
18179
  * `createLocalPlacementResolver`. Use the unified factory directly
@@ -18551,8 +18601,8 @@ declare function createHydrationProviders(options: HydrationAdapterOptions): Any
18551
18601
  * const runtime = new LocalRuntime({
18552
18602
  * tenantId: 'tenant_abc',
18553
18603
  * userId: 'user_123',
18554
- * exportedConfig: myConfig,
18555
- * providers: createStaticProviders({ config: myConfig, planHandle: 'pro' }),
18604
+ * playbook: myPlaybook,
18605
+ * providers: createStaticProviders({ config: myPlaybook, planHandle: 'pro' }),
18556
18606
  * storage: new InMemoryStorage(),
18557
18607
  * });
18558
18608
  *
@@ -18561,18 +18611,21 @@ declare function createHydrationProviders(options: HydrationAdapterOptions): Any
18561
18611
  * ```
18562
18612
  */
18563
18613
 
18564
- interface LocalRuntimeOptions {
18614
+ /**
18615
+ * `LocalRuntimeOptions` minus the Playbook, which arrives through
18616
+ * {@link PlaybookOption} so both the canonical `playbook` name and the
18617
+ * deprecated `exportedConfig` alias are accepted.
18618
+ */
18619
+ interface LocalRuntimeOptionsBase {
18565
18620
  /** Tenant identifier. */
18566
18621
  tenantId: string;
18567
18622
  /** Current user identifier. */
18568
18623
  userId: string;
18569
- /** Full RevTurbineConfig for local resolution. */
18570
- exportedConfig: RevTurbineConfig;
18571
18624
  /** Pre-built domain providers (from createStaticProviders, etc.). */
18572
18625
  providers: AnyDomainProvider[];
18573
18626
  /**
18574
18627
  * Optional pre-built placements dataset. If not provided, derived from
18575
- * `exportedConfig.placements`.
18628
+ * `playbook.placements`.
18576
18629
  */
18577
18630
  placements?: LocalPlacementDataset;
18578
18631
  /**
@@ -18589,6 +18642,14 @@ interface LocalRuntimeOptions {
18589
18642
  /** Interaction tracker options. */
18590
18643
  interactionOptions?: Partial<Pick<InteractionTrackerOptions, 'defaultDismissCooldownMs' | 'defaultRemindLaterMs'>>;
18591
18644
  }
18645
+ /**
18646
+ * Options for {@link LocalRuntime}.
18647
+ *
18648
+ * The Playbook is supplied as `playbook`. The `exportedConfig` spelling is a
18649
+ * deprecated alias kept so consumers can migrate incrementally; it is removed
18650
+ * in the minor after the SDK 0.11.0 cascade (BL-0156).
18651
+ */
18652
+ type LocalRuntimeOptions = LocalRuntimeOptionsBase & PlaybookOption;
18592
18653
  declare class LocalRuntime {
18593
18654
  readonly tenantId: string;
18594
18655
  readonly registry: DomainProviderRegistry;
@@ -18596,7 +18657,7 @@ declare class LocalRuntime {
18596
18657
  readonly interactionTracker: InteractionTracker;
18597
18658
  readonly capEnforcer: CapEnforcer;
18598
18659
  readonly impressionHistory: ImpressionHistory;
18599
- private readonly exportedConfig;
18660
+ private readonly playbook;
18600
18661
  private readonly registeredPlacements;
18601
18662
  private userId;
18602
18663
  constructor(options: LocalRuntimeOptions);
@@ -18668,7 +18729,14 @@ declare class LocalRuntime {
18668
18729
  setUserId(userId: string): void;
18669
18730
  /** Current user ID. */
18670
18731
  getUserId(): string;
18671
- /** Return the active RevTurbineConfig snapshot. */
18732
+ /** Return the active Playbook snapshot. */
18733
+ getPlaybook(): RevTurbineConfig;
18734
+ /**
18735
+ * Return the active Playbook snapshot.
18736
+ *
18737
+ * @deprecated Use {@link LocalRuntime.getPlaybook}. Removed in the minor
18738
+ * after the SDK 0.11.0 cascade (BL-0156).
18739
+ */
18672
18740
  getExportedConfig(): RevTurbineConfig;
18673
18741
  /**
18674
18742
  * Pre-warm caches (impression history, etc.) for synchronous access.