@revturbine/sdk 0.10.3 → 0.10.5

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,110 @@ also require a changelog entry.
47
47
 
48
48
  ---
49
49
 
50
+ ## 0.10.5
51
+
52
+ ### Trial-only local integrations get provider context (BL-0120)
53
+
54
+ **What changed.** `synthesizeProviderContext()` — the fallback that builds
55
+ `DomainProvider` state from `userContext` when no explicit domain provider is
56
+ registered — dropped an integration's trial data whenever no commercial
57
+ `plan` / `plan_handle` was also present. An app supplying **only**
58
+ `initialData.trialStatus` (or `setTrialInstances()` / `setTrialStatus()` with
59
+ no plan) got no provider context at all: `providers.plan` came back
60
+ `undefined`, so `{{trial_days_remaining}}` (and every other `trial*` token
61
+ `derivePlacementPersonalizationTokens` derives from `providers.plan`) stayed
62
+ raw, unresolved, in placement copy. Reported against the CybeDefend demo
63
+ (SDK 0.7.13): a `trial_ending` placement with `in_trial: true,
64
+ days_remaining: 3` left `{{trial_days_remaining}}` literal in the rendered
65
+ body.
66
+
67
+ Two omissions, same root cause — trial was never checked alongside
68
+ `plan` / `plan_handle` / `usage` / `tiers` / `experiments`:
69
+
70
+ - the early-return guard (`if (!plan && !planHandle && !usage && !hasTiers
71
+ && !hasExperiments) return undefined;`) treated a trial-only integration as
72
+ having no signal whatsoever;
73
+ - even past that guard, the returned `plan` key itself was gated on
74
+ `plan || planHandle` — so the trial fields (`trialActive`,
75
+ `trialDaysRemaining`, etc.) it computed were still discarded for a
76
+ plan-less trial.
77
+
78
+ Both gates now also check `hasTrial` (an explicit trial signal — the default
79
+ untouched `{ in_trial: false }` sentinel does not count, so the omitted-signal
80
+ short-circuit is otherwise unchanged for integrations that supply nothing at
81
+ all). `usage`-only, `tiers`-only, and `experiments`-only integrations already
82
+ threaded through correctly before this change; trial was the only signal
83
+ missing from both checks.
84
+
85
+ No public API change — `synthesizeProviderContext` is private, and the fix is
86
+ additive: a plan-less trial integration now gets a `providers.plan` it never
87
+ got before, with an empty `currentPlanHandle: ''` (no plan name/handle was
88
+ ever supplied to derive one from).
89
+
90
+ **Landed in** `0.10.5`. **Fail-closed in** n/a — a previously-dropped signal
91
+ is now included; nothing that resolved before stops resolving.
92
+
93
+ **Proving test:** `web-sdk/customer-side-trial-token-signal.test.ts` — a
94
+ trial-only local integration now gets a non-empty provider context with
95
+ `trialActive`/`trialDaysRemaining` set, and feeding those through
96
+ `derivePlacementPersonalizationTokens` + `resolveContent` renders
97
+ `{{trial_days_remaining}}` as `"3"` instead of leaving it literal. Includes
98
+ the positive control (trial + plan together — unchanged), the negative
99
+ control (no trial data at all — the token stays absent, never coerced to
100
+ `"0"`), and a sibling check confirming usage-only integrations were already
101
+ unaffected by the guard.
102
+
103
+ ---
104
+
105
+ ## 0.10.4
106
+
107
+ ### Every payload is a selection candidate, not just the first (BL-0122)
108
+
109
+ **What changed.** A placement's payloads are now *all* candidates on both
110
+ selection paths, in all three ports. Previously each port considered exactly one
111
+ payload per placement — TypeScript took `payloads[0]`, Python and Rust the first
112
+ payload with `status == "active"` — so payloads 2+ were never candidates and
113
+ their `target.segment_chips` could not be evaluated at all.
114
+
115
+ | | Before | After |
116
+ |---|---|---|
117
+ | Candidacy | one payload per placement | every payload (Python/Rust: every `active` payload) |
118
+ | `target.segment_chips` on payloads 2+ | never evaluated | evaluated per payload |
119
+ | Drag precedence | a pre-filter — it chose the only candidate | a tiebreaker among the payloads the user **matches** |
120
+ | Direct lookup by name/id | mapped a name to one payload | maps a name to every payload, gated individually, first eligible wins |
121
+
122
+ The contract this restores is
123
+ [`placement-prioritization.md`](https://github.com/revt-eng/revturbine-devkit/blob/main/docs/specs/scaffold/placement-prioritization.md)
124
+ §1 stage 3 — "Targeting — the user's plan and segment match **a payload**" — with
125
+ §4 and Appendix D scoping drag precedence to payloads that are equally eligible.
126
+
127
+ **Why it mattered.** A placement holding an admin payload and a member payload
128
+ gave every member either the admin copy (when payload 1 was unchipped) or
129
+ nothing at all (when payload 1 was chipped to admins). An integration hit the
130
+ second shape on 0.7.13 and concluded config-driven segmentation was inert. Plan
131
+ 233 TASK-7 had already made the chip predicate real — but the predicate was only
132
+ ever asked about one payload.
133
+
134
+ **Landed in.** 0.10.4 (all three ports, plus `@revt-eng/core` 0.1.326).
135
+
136
+ **Fail-closed in.** 0.10.4 — the same release. There is no tolerance window:
137
+ a user who matches no payload now receives `visible: false` with
138
+ `segment_target_mismatch` rather than payload 1's content.
139
+
140
+ **Proving test.** `tests/parity/fixtures/segment_chip_payload_selection.json`
141
+ (cross-language), plus `local-resolver-payload-selection.test.ts`,
142
+ `server-python/tests/placements/test_payload_selection.py` and
143
+ `server-rust/tests/payload_selection.rs` — each exercising **both** the slot and
144
+ the direct-lookup path, because gating one and not the other is exactly the back
145
+ door plans 138 and 233 each had to close.
146
+
147
+ **Who is affected.** Any Playbook authoring more than one payload on a
148
+ placement where payload 1 is not the payload a given user matches. Those users
149
+ now see the payload their chips select — which is the fix, and is a live
150
+ behaviour change for such configs. Single-payload placements are unaffected.
151
+
152
+ ---
153
+
50
154
  ## 0.10.3
51
155
 
52
156
  ### The Rust port gains the decision surfaces Python already shipped (BL-0145)
@@ -77,6 +181,8 @@ and the crate could lack the capability entirely without a single fixture
77
181
  going red. The runner now calls `revturbine::plans`, and the inline copies are
78
182
  deleted; all 102 comparisons stay byte-identical.
79
183
 
184
+ ---
185
+
80
186
  ## 0.10.2
81
187
 
82
188
  ### Local mode resolves placements by slot id (BL-0119)